summaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /web_src
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--web_src/css/actions.css96
-rw-r--r--web_src/css/admin.css51
-rw-r--r--web_src/css/base.css1586
-rw-r--r--web_src/css/chroma/base.css46
-rw-r--r--web_src/css/chroma/dark.css76
-rw-r--r--web_src/css/chroma/light.css76
-rw-r--r--web_src/css/codemirror/base.css49
-rw-r--r--web_src/css/codemirror/dark.css106
-rw-r--r--web_src/css/codemirror/light.css1
-rw-r--r--web_src/css/dashboard.css81
-rw-r--r--web_src/css/editor/combomarkdowneditor.css136
-rw-r--r--web_src/css/editor/fileeditor.css85
-rw-r--r--web_src/css/explore.css31
-rw-r--r--web_src/css/features/codeeditor.css48
-rw-r--r--web_src/css/features/colorpicker.css47
-rw-r--r--web_src/css/features/console.css338
-rw-r--r--web_src/css/features/dropzone.css59
-rw-r--r--web_src/css/features/gitgraph.css309
-rw-r--r--web_src/css/features/heatmap.css58
-rw-r--r--web_src/css/features/imagediff.css114
-rw-r--r--web_src/css/features/projects.css108
-rw-r--r--web_src/css/features/tribute.css42
-rw-r--r--web_src/css/font_i18n.css393
-rw-r--r--web_src/css/form.css520
-rw-r--r--web_src/css/helpers.css60
-rw-r--r--web_src/css/home.css87
-rw-r--r--web_src/css/index.css78
-rw-r--r--web_src/css/install.css62
-rw-r--r--web_src/css/markup/asciicast.css8
-rw-r--r--web_src/css/markup/codecopy.css35
-rw-r--r--web_src/css/markup/content.css585
-rw-r--r--web_src/css/markup/dark.css13
-rw-r--r--web_src/css/markup/filepreview.css41
-rw-r--r--web_src/css/markup/light.css6
-rw-r--r--web_src/css/modules/animations.css116
-rw-r--r--web_src/css/modules/breadcrumb.css14
-rw-r--r--web_src/css/modules/button.css756
-rw-r--r--web_src/css/modules/card.css134
-rw-r--r--web_src/css/modules/checkbox.css121
-rw-r--r--web_src/css/modules/comment.css90
-rw-r--r--web_src/css/modules/container.css66
-rw-r--r--web_src/css/modules/divider.css43
-rw-r--r--web_src/css/modules/flexcontainer.css33
-rw-r--r--web_src/css/modules/grid.css524
-rw-r--r--web_src/css/modules/header.css176
-rw-r--r--web_src/css/modules/input.css197
-rw-r--r--web_src/css/modules/label.css303
-rw-r--r--web_src/css/modules/list.css193
-rw-r--r--web_src/css/modules/message.css114
-rw-r--r--web_src/css/modules/modal.css86
-rw-r--r--web_src/css/modules/navbar.css147
-rw-r--r--web_src/css/modules/normalize.css243
-rw-r--r--web_src/css/modules/segment.css203
-rw-r--r--web_src/css/modules/select.css25
-rw-r--r--web_src/css/modules/svg.css41
-rw-r--r--web_src/css/modules/table.css385
-rw-r--r--web_src/css/modules/tippy.css170
-rw-r--r--web_src/css/modules/toast.css77
-rw-r--r--web_src/css/org.css192
-rw-r--r--web_src/css/repo.css3007
-rw-r--r--web_src/css/repo/header.css68
-rw-r--r--web_src/css/repo/issue-card.css40
-rw-r--r--web_src/css/repo/issue-label.css52
-rw-r--r--web_src/css/repo/issue-list.css112
-rw-r--r--web_src/css/repo/linebutton.css19
-rw-r--r--web_src/css/repo/list-header.css58
-rw-r--r--web_src/css/repo/release-tag.css121
-rw-r--r--web_src/css/repo/wiki.css72
-rw-r--r--web_src/css/review.css297
-rw-r--r--web_src/css/shared/flex-list.css108
-rw-r--r--web_src/css/shared/milestone.css62
-rw-r--r--web_src/css/shared/repoorg.css18
-rw-r--r--web_src/css/shared/settings.css37
-rw-r--r--web_src/css/standalone/devtest.css16
-rw-r--r--web_src/css/standalone/swagger.css42
-rw-r--r--web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css2
-rw-r--r--web_src/css/themes/theme-forgejo-auto-tritanopia.css2
-rw-r--r--web_src/css/themes/theme-forgejo-auto.css2
-rw-r--r--web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css11
-rw-r--r--web_src/css/themes/theme-forgejo-dark-tritanopia.css11
-rw-r--r--web_src/css/themes/theme-forgejo-dark.css357
-rw-r--r--web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css11
-rw-r--r--web_src/css/themes/theme-forgejo-light-tritanopia.css11
-rw-r--r--web_src/css/themes/theme-forgejo-light.css325
-rw-r--r--web_src/css/themes/theme-gitea-auto.css2
-rw-r--r--web_src/css/themes/theme-gitea-dark.css271
-rw-r--r--web_src/css/themes/theme-gitea-light.css247
-rw-r--r--web_src/css/user.css149
-rw-r--r--web_src/fomantic/.npmrc7
-rw-r--r--web_src/fomantic/_site/globals/site.variables61
-rw-r--r--web_src/fomantic/build/semantic.css9935
-rw-r--r--web_src/fomantic/build/semantic.js11992
-rw-r--r--web_src/fomantic/build/themes/default/assets/fonts/icons.woff2bin0 -> 79444 bytes
-rw-r--r--web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2bin0 -> 13584 bytes
-rw-r--r--web_src/fomantic/package-lock.json8742
-rw-r--r--web_src/fomantic/package.json5
-rw-r--r--web_src/fomantic/semantic.json34
-rw-r--r--web_src/fomantic/theme.config.less103
-rw-r--r--web_src/js/bootstrap.js109
-rw-r--r--web_src/js/bootstrap.test.js12
-rw-r--r--web_src/js/components/.eslintrc.yaml21
-rw-r--r--web_src/js/components/ActionRunStatus.vue39
-rw-r--r--web_src/js/components/ActivityHeatmap.vue83
-rw-r--r--web_src/js/components/ContextPopup.test.js163
-rw-r--r--web_src/js/components/ContextPopup.vue130
-rw-r--r--web_src/js/components/DashboardRepoList.vue537
-rw-r--r--web_src/js/components/DiffCommitSelector.vue306
-rw-r--r--web_src/js/components/DiffFileList.vue58
-rw-r--r--web_src/js/components/DiffFileTree.vue144
-rw-r--r--web_src/js/components/DiffFileTreeItem.vue97
-rw-r--r--web_src/js/components/PullRequestMergeForm.test.js34
-rw-r--r--web_src/js/components/PullRequestMergeForm.vue252
-rw-r--r--web_src/js/components/RepoActionView.test.js105
-rw-r--r--web_src/js/components/RepoActionView.vue905
-rw-r--r--web_src/js/components/RepoActivityTopAuthors.vue164
-rw-r--r--web_src/js/components/RepoBranchTagSelector.vue357
-rw-r--r--web_src/js/components/RepoCodeFrequency.vue172
-rw-r--r--web_src/js/components/RepoContributors.vue431
-rw-r--r--web_src/js/components/RepoRecentCommits.vue149
-rw-r--r--web_src/js/components/ScopedAccessTokenSelector.vue115
-rw-r--r--web_src/js/features/admin/common.js258
-rw-r--r--web_src/js/features/admin/config.js24
-rw-r--r--web_src/js/features/admin/emails.js14
-rw-r--r--web_src/js/features/admin/users.js39
-rw-r--r--web_src/js/features/autofocus-end.js6
-rw-r--r--web_src/js/features/captcha.js51
-rw-r--r--web_src/js/features/citation.js50
-rw-r--r--web_src/js/features/clipboard.js32
-rw-r--r--web_src/js/features/code-frequency.js21
-rw-r--r--web_src/js/features/codeeditor.js191
-rw-r--r--web_src/js/features/colorpicker.js66
-rw-r--r--web_src/js/features/common-global.js463
-rw-r--r--web_src/js/features/common-issue-list.js68
-rw-r--r--web_src/js/features/common-issue-list.test.js16
-rw-r--r--web_src/js/features/common-organization.js16
-rw-r--r--web_src/js/features/comp/ComboMarkdownEditor.js413
-rw-r--r--web_src/js/features/comp/ConfirmModal.js30
-rw-r--r--web_src/js/features/comp/EasyMDEToolbarActions.js152
-rw-r--r--web_src/js/features/comp/LabelEdit.js96
-rw-r--r--web_src/js/features/comp/Paste.js144
-rw-r--r--web_src/js/features/comp/QuickSubmit.js17
-rw-r--r--web_src/js/features/comp/ReactionSelector.js38
-rw-r--r--web_src/js/features/comp/SearchUserBox.js51
-rw-r--r--web_src/js/features/comp/TextExpander.js61
-rw-r--r--web_src/js/features/comp/WebHookEditor.js28
-rw-r--r--web_src/js/features/contextpopup.js43
-rw-r--r--web_src/js/features/contributors.js29
-rw-r--r--web_src/js/features/copycontent.js56
-rw-r--r--web_src/js/features/dropzone.js7
-rw-r--r--web_src/js/features/emoji.js38
-rw-r--r--web_src/js/features/eventsource.sharedworker.js141
-rw-r--r--web_src/js/features/file-fold.js19
-rw-r--r--web_src/js/features/heatmap.js40
-rw-r--r--web_src/js/features/imagediff.js271
-rw-r--r--web_src/js/features/install.js119
-rw-r--r--web_src/js/features/notification.js192
-rw-r--r--web_src/js/features/org-team.js26
-rw-r--r--web_src/js/features/pull-view-file.js96
-rw-r--r--web_src/js/features/recent-commits.js21
-rw-r--r--web_src/js/features/repo-branch.js42
-rw-r--r--web_src/js/features/repo-code.js195
-rw-r--r--web_src/js/features/repo-code.test.js17
-rw-r--r--web_src/js/features/repo-commit.js27
-rw-r--r--web_src/js/features/repo-common.js83
-rw-r--r--web_src/js/features/repo-diff-commit.js53
-rw-r--r--web_src/js/features/repo-diff-commitselect.js10
-rw-r--r--web_src/js/features/repo-diff-filetree.js17
-rw-r--r--web_src/js/features/repo-diff.js232
-rw-r--r--web_src/js/features/repo-editor.js203
-rw-r--r--web_src/js/features/repo-findfile.js117
-rw-r--r--web_src/js/features/repo-findfile.test.js34
-rw-r--r--web_src/js/features/repo-graph.js155
-rw-r--r--web_src/js/features/repo-home.js147
-rw-r--r--web_src/js/features/repo-issue-content.js154
-rw-r--r--web_src/js/features/repo-issue-list.js245
-rw-r--r--web_src/js/features/repo-issue-pr-form.js10
-rw-r--r--web_src/js/features/repo-issue-pr-status.js10
-rw-r--r--web_src/js/features/repo-issue.js797
-rw-r--r--web_src/js/features/repo-issue.test.js24
-rw-r--r--web_src/js/features/repo-legacy.js610
-rw-r--r--web_src/js/features/repo-migrate.js64
-rw-r--r--web_src/js/features/repo-migration.js69
-rw-r--r--web_src/js/features/repo-projects.js188
-rw-r--r--web_src/js/features/repo-release.js95
-rw-r--r--web_src/js/features/repo-search.js22
-rw-r--r--web_src/js/features/repo-settings.js120
-rw-r--r--web_src/js/features/repo-template.js51
-rw-r--r--web_src/js/features/repo-unicode-escape.js27
-rw-r--r--web_src/js/features/repo-wiki.js89
-rw-r--r--web_src/js/features/sshkey-helper.js10
-rw-r--r--web_src/js/features/stopwatch.js167
-rw-r--r--web_src/js/features/tablesort.js22
-rw-r--r--web_src/js/features/tribute.js57
-rw-r--r--web_src/js/features/user-auth-webauthn.js194
-rw-r--r--web_src/js/features/user-auth.js22
-rw-r--r--web_src/js/features/user-settings.js63
-rw-r--r--web_src/js/htmx.js21
-rw-r--r--web_src/js/index.js190
-rw-r--r--web_src/js/jquery.js3
-rw-r--r--web_src/js/markup/anchors.js70
-rw-r--r--web_src/js/markup/asciicast.js17
-rw-r--r--web_src/js/markup/codecopy.js21
-rw-r--r--web_src/js/markup/common.js8
-rw-r--r--web_src/js/markup/content.js18
-rw-r--r--web_src/js/markup/math.js47
-rw-r--r--web_src/js/markup/mermaid.js74
-rw-r--r--web_src/js/markup/tasklist.js90
-rw-r--r--web_src/js/modules/dirauto.js40
-rw-r--r--web_src/js/modules/fetch.js41
-rw-r--r--web_src/js/modules/fetch.test.js10
-rw-r--r--web_src/js/modules/fomantic.js34
-rw-r--r--web_src/js/modules/fomantic/api.js40
-rw-r--r--web_src/js/modules/fomantic/aria.md117
-rw-r--r--web_src/js/modules/fomantic/base.js18
-rw-r--r--web_src/js/modules/fomantic/checkbox.js13
-rw-r--r--web_src/js/modules/fomantic/dropdown.js256
-rw-r--r--web_src/js/modules/fomantic/form.js13
-rw-r--r--web_src/js/modules/fomantic/modal.js28
-rw-r--r--web_src/js/modules/fomantic/transition.js54
-rw-r--r--web_src/js/modules/sortable.js19
-rw-r--r--web_src/js/modules/stores.js10
-rw-r--r--web_src/js/modules/tippy.js195
-rw-r--r--web_src/js/modules/toast.js55
-rw-r--r--web_src/js/modules/toast.test.js16
-rw-r--r--web_src/js/render/ansi.js45
-rw-r--r--web_src/js/render/ansi.test.js20
-rw-r--r--web_src/js/render/pdf.js19
-rw-r--r--web_src/js/standalone/devtest.js11
-rw-r--r--web_src/js/standalone/forgejo-swagger.js23
-rw-r--r--web_src/js/standalone/swagger.js34
-rw-r--r--web_src/js/svg.js228
-rw-r--r--web_src/js/svg.test.js27
-rw-r--r--web_src/js/utils.js144
-rw-r--r--web_src/js/utils.test.js189
-rw-r--r--web_src/js/utils/color.js33
-rw-r--r--web_src/js/utils/color.test.js22
-rw-r--r--web_src/js/utils/dom.js305
-rw-r--r--web_src/js/utils/dom.test.js5
-rw-r--r--web_src/js/utils/image.js47
-rw-r--r--web_src/js/utils/image.test.js29
-rw-r--r--web_src/js/utils/match.js43
-rw-r--r--web_src/js/utils/match.test.js50
-rw-r--r--web_src/js/utils/time.js72
-rw-r--r--web_src/js/utils/time.test.js40
-rw-r--r--web_src/js/utils/url.js15
-rw-r--r--web_src/js/utils/url.test.js13
-rw-r--r--web_src/js/vendor/jquery.are-you-sure.js195
-rw-r--r--web_src/js/vitest.setup.js18
-rw-r--r--web_src/js/webcomponents/README.md11
-rw-r--r--web_src/js/webcomponents/absolute-date.js40
-rw-r--r--web_src/js/webcomponents/absolute-date.test.js15
-rw-r--r--web_src/js/webcomponents/index.js5
-rw-r--r--web_src/js/webcomponents/origin-url.js22
-rw-r--r--web_src/js/webcomponents/origin-url.test.js17
-rw-r--r--web_src/js/webcomponents/overflow-menu.js220
-rw-r--r--web_src/js/webcomponents/polyfills.js17
-rw-r--r--web_src/svg/fontawesome-openid.svg3
-rw-r--r--web_src/svg/fontawesome-save.svg1
-rw-r--r--web_src/svg/fontawesome-send.svg1
-rw-r--r--web_src/svg/fontawesome-windows.svg3
-rw-r--r--web_src/svg/gitea-alpine.svg2
-rw-r--r--web_src/svg/gitea-arch.svg1
-rw-r--r--web_src/svg/gitea-azuread.svg1
-rw-r--r--web_src/svg/gitea-azureadv2.svg1
-rw-r--r--web_src/svg/gitea-bitbucket.svg1
-rw-r--r--web_src/svg/gitea-cargo.svg3
-rw-r--r--web_src/svg/gitea-chef.svg20
-rw-r--r--web_src/svg/gitea-codebase.svg13
-rw-r--r--web_src/svg/gitea-composer.svg50
-rw-r--r--web_src/svg/gitea-conan.svg9
-rw-r--r--web_src/svg/gitea-conda.svg20
-rw-r--r--web_src/svg/gitea-cran.svg15
-rw-r--r--web_src/svg/gitea-debian.svg9
-rw-r--r--web_src/svg/gitea-discord.svg1
-rw-r--r--web_src/svg/gitea-double-chevron-left.svg1
-rw-r--r--web_src/svg/gitea-double-chevron-right.svg1
-rw-r--r--web_src/svg/gitea-dropbox.svg1
-rw-r--r--web_src/svg/gitea-empty-checkbox.svg1
-rw-r--r--web_src/svg/gitea-exclamation.svg1
-rw-r--r--web_src/svg/gitea-facebook.svg1
-rw-r--r--web_src/svg/gitea-forgejo.svg9
-rw-r--r--web_src/svg/gitea-git.svg1
-rw-r--r--web_src/svg/gitea-gitbucket.svg39
-rw-r--r--web_src/svg/gitea-gitea.svg1
-rw-r--r--web_src/svg/gitea-gitlab.svg1
-rw-r--r--web_src/svg/gitea-go.svg10
-rw-r--r--web_src/svg/gitea-gogs.svg60
-rw-r--r--web_src/svg/gitea-google.svg1
-rw-r--r--web_src/svg/gitea-helm.svg3
-rw-r--r--web_src/svg/gitea-jetbrains.svg62
-rw-r--r--web_src/svg/gitea-join.svg1
-rw-r--r--web_src/svg/gitea-lock-cog.svg1
-rw-r--r--web_src/svg/gitea-lock.svg1
-rw-r--r--web_src/svg/gitea-mastodon.svg1
-rw-r--r--web_src/svg/gitea-matrix.svg14
-rw-r--r--web_src/svg/gitea-maven.svg1
-rw-r--r--web_src/svg/gitea-microsoftonline.svg1
-rw-r--r--web_src/svg/gitea-nextcloud.svg1
-rw-r--r--web_src/svg/gitea-npm.svg7
-rw-r--r--web_src/svg/gitea-nuget.svg15
-rw-r--r--web_src/svg/gitea-onedev.svg42
-rw-r--r--web_src/svg/gitea-open-with-jetbrains.svg62
-rw-r--r--web_src/svg/gitea-open-with-vscode.svg1
-rw-r--r--web_src/svg/gitea-open-with-vscodium.svg1
-rw-r--r--web_src/svg/gitea-openid.svg1
-rw-r--r--web_src/svg/gitea-pub.svg19
-rw-r--r--web_src/svg/gitea-python.svg19
-rw-r--r--web_src/svg/gitea-rpm.svg9
-rw-r--r--web_src/svg/gitea-rubygems.svg3
-rw-r--r--web_src/svg/gitea-split.svg1
-rw-r--r--web_src/svg/gitea-swift.svg5
-rw-r--r--web_src/svg/gitea-twitter.svg1
-rw-r--r--web_src/svg/gitea-unlock.svg1
-rw-r--r--web_src/svg/gitea-vagrant.svg6
-rw-r--r--web_src/svg/gitea-vscodium.svg1
-rw-r--r--web_src/svg/gitea-whitespace.svg1
-rw-r--r--web_src/svg/gitea-yandex.svg1
-rw-r--r--web_src/svg/material-invert-colors.svg1
-rw-r--r--web_src/svg/material-palette.svg1
319 files changed, 62615 insertions, 0 deletions
diff --git a/web_src/css/actions.css b/web_src/css/actions.css
new file mode 100644
index 0000000..c89a70e
--- /dev/null
+++ b/web_src/css/actions.css
@@ -0,0 +1,96 @@
+.runner-container {
+ padding-bottom: 30px;
+}
+
+.runner-container .ui.table.segment {
+ overflow-x: auto;
+}
+
+.runner-container .runner-ops > a {
+ margin-left: 0.5em;
+}
+
+.runner-container .runner-ops-delete {
+ color: var(--color-red-light);
+}
+
+.runner-container .runner-new-text {
+ color: var(--color-white);
+}
+
+.runner-container #runner-new:hover .runner-new-text {
+ color: var(--color-white) !important;
+}
+
+.runner-container .task-status-success {
+ background-color: var(--color-green);
+ color: var(--color-white);
+}
+
+.runner-container .task-status-failure {
+ background-color: var(--color-red-light);
+ color: var(--color-white);
+}
+
+.runner-container .task-status-running {
+ background-color: var(--color-blue);
+ color: var(--color-white);
+}
+
+.runner-container .task-status-cancelled,
+.runner-container .task-status-blocked {
+ background-color: var(--color-yellow);
+ color: var(--color-white);
+}
+
+.run-list-item-right {
+ width: 130px;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ gap: 3px;
+ color: var(--color-text-light);
+}
+
+.run-list-item-right .run-list-meta {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: .25rem;
+ align-items: center;
+}
+
+.run-list .flex-item-trailing {
+ flex-wrap: nowrap;
+ width: 280px;
+ flex: 0 0 280px;
+}
+
+.run-list-ref {
+ display: inline-block !important;
+}
+
+@media (max-width: 767.98px) {
+ .run-list .flex-item-trailing {
+ flex-direction: column;
+ align-items: flex-end;
+ width: auto;
+ flex-basis: auto;
+ }
+ .run-list-item-right,
+ .run-list-ref {
+ max-width: 110px;
+ }
+}
+
+#workflow_dispatch_dropdown {
+ min-width: min-content;
+}
+#workflow_dispatch_dropdown > button {
+ white-space: nowrap;
+}
+@media (max-width: 640px) or (767.98px < width < 854px) {
+ #workflow_dispatch_dropdown .menu {
+ left: auto;
+ right: 0;
+ }
+}
diff --git a/web_src/css/admin.css b/web_src/css/admin.css
new file mode 100644
index 0000000..e6866b2
--- /dev/null
+++ b/web_src/css/admin.css
@@ -0,0 +1,51 @@
+.admin.hooks .list > .item:not(:first-child) {
+ border-top: 1px solid var(--color-secondary);
+ padding: 0.25rem 1rem;
+ margin: 12px -1rem -1rem;
+}
+
+.admin dl.admin-dl-horizontal {
+ padding: 1em;
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.admin dl.admin-dl-horizontal dt,
+.admin dl.admin-dl-horizontal dd {
+ line-height: var(--line-height-default);
+ padding: 5px 0;
+}
+
+.admin dl.admin-dl-horizontal dt {
+ width: 300px;
+ max-width: calc(100% - 100px - 1em);
+ font-weight: var(--font-weight-semibold);
+}
+
+.admin dl.admin-dl-horizontal dd {
+ margin-left: auto;
+ width: calc(100% - 300px - 1em);
+ min-width: 100px;
+}
+
+.admin code,
+.admin pre {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.admin .ui.table.segment {
+ overflow-x: auto; /* if the screen width is small, many wide tables (eg: user list) need scroll bars */
+}
+
+.admin .table th {
+ white-space: nowrap;
+}
+
+.admin-responsive-columns {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
diff --git a/web_src/css/base.css b/web_src/css/base.css
new file mode 100644
index 0000000..4c1317b
--- /dev/null
+++ b/web_src/css/base.css
@@ -0,0 +1,1586 @@
+:root {
+ /* fonts */
+ --fonts-proportional: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial;
+ --fonts-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, var(--fonts-emoji);
+ --fonts-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
+ /* font weights - use between 400 and 600 for general purposes. Avoid 700 as it is perceived too bold */
+ --font-weight-light: 300;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ /* line-height: use the default value as "modules/normalize.css" */
+ --line-height-default: normal;
+ /* images */
+ --checkbox-mask-checked: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 18 18" width="16" height="16"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>');
+ --checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z"></path></svg>');
+ --octicon-chevron-right: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"></path></svg>');
+ /* other variables */
+ --border-radius: 4px;
+ --border-radius-medium: 6px;
+ --border-radius-full: 99999px; /* TODO: use calc(infinity * 1px) */
+ --opacity-disabled: 0.55;
+ --height-loading: 16rem;
+ --repo-header-issue-min-height: 41px;
+ --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
+ --tab-size: 4;
+ --checkbox-size: 15px; /* height and width of checkbox and radio inputs */
+ --page-spacing: 16px; /* space between page elements */
+ --page-margin-x: 32px; /* minimum space on left and right side of page */
+}
+
+@media (min-width: 768px) and (max-width: 1200px) {
+ :root {
+ --page-margin-x: 16px;
+ }
+}
+
+@media (max-width: 767.98px) {
+ :root {
+ --page-margin-x: 8px;
+ }
+}
+
+:root * {
+ --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Noto Sans", "Liberation Sans", sans-serif, var(--fonts-emoji);
+}
+
+*, ::before, ::after {
+ /* these are needed for tailwind borders to work because we do not load tailwind's base
+ https://github.com/tailwindlabs/tailwindcss/blob/master/src/css/preflight.css */
+ border-width: 0;
+ border-style: solid;
+ border-color: currentcolor;
+}
+
+html, body {
+ height: 100%;
+ font-size: 14px;
+}
+
+body {
+ line-height: 20px;
+ font-family: var(--fonts-regular);
+ color: var(--color-text);
+ background-color: var(--color-body);
+ tab-size: var(--tab-size);
+ display: flex;
+ flex-direction: column;
+ overflow-x: visible;
+ overflow-wrap: break-word;
+}
+
+textarea {
+ font-family: var(--fonts-regular);
+}
+
+pre,
+code,
+kbd,
+samp {
+ font-family: var(--fonts-monospace);
+}
+
+pre,
+code,
+kbd,
+samp,
+.tw-font-mono {
+ font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */
+}
+
+b,
+strong,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: var(--font-weight-semibold);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5 {
+ line-height: 1.28571429;
+ margin: calc(2rem - 0.1428571428571429em) 0 1rem;
+ font-weight: var(--font-weight-medium);
+ padding: 0;
+}
+
+h1 {
+ min-height: 1rem;
+ font-size: 2rem;
+}
+
+h2 {
+ font-size: 1.71428571rem;
+}
+
+h3 {
+ font-size: 1.28571429rem;
+}
+
+h4 {
+ font-size: 1.07142857rem;
+}
+
+h5 {
+ font-size: 1rem;
+}
+
+h1:first-child,
+h2:first-child,
+h3:first-child,
+h4:first-child,
+h5:first-child {
+ margin-top: 0;
+}
+
+h1:last-child,
+h2:last-child,
+h3:last-child,
+h4:last-child,
+h5:last-child {
+ margin-bottom: 0;
+}
+
+p {
+ margin: 0 0 1em;
+ line-height: 1.4285;
+}
+
+p:first-child {
+ margin-top: 0;
+}
+
+p:last-child {
+ margin-bottom: 0;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+button {
+ cursor: pointer;
+}
+
+details summary {
+ cursor: pointer;
+}
+
+details summary > * {
+ display: inline;
+}
+
+progress {
+ background: var(--color-secondary-dark-1);
+ border-radius: var(--border-radius);
+ border: none;
+ overflow: hidden;
+}
+
+progress::-webkit-progress-bar {
+ background: var(--color-secondary-dark-1);
+}
+
+progress::-webkit-progress-value {
+ background-color: var(--color-accent);
+}
+
+progress::-moz-progress-bar {
+ background-color: var(--color-accent);
+}
+
+h1.error-code {
+ font-size: 15em;
+ font-weight: var(--font-weight-bold);
+ color: transparent;
+ --error-code-color-1: #a2a2a2;
+ --error-code-color-2: #797979;
+ --gradient: repeating-linear-gradient(45deg, var(--error-code-color-1), var(--error-code-color-1) 10px, var(--error-code-color-2) 10px, var(--error-code-color-2) 20px);
+ background: var(--gradient);
+ background-clip: text;
+}
+
+* {
+ caret-color: var(--color-caret);
+}
+
+::file-selector-button {
+ border: 1px solid var(--color-light-border);
+ color: var(--color-text-light);
+ background: var(--color-light);
+ border-radius: var(--border-radius);
+}
+
+::file-selector-button:hover {
+ color: var(--color-text);
+ background: var(--color-hover);
+}
+
+::selection {
+ background: var(--color-primary-light-1);
+ color: var(--color-white);
+}
+
+::placeholder,
+.ui.dropdown:not(.button) > .default.text,
+.ui.default.dropdown:not(.button) > .text {
+ color: var(--color-placeholder-text) !important;
+ opacity: 1 !important;
+}
+
+.unselectable,
+.button,
+.lines-num,
+.lines-commit,
+.lines-commit .blame-info,
+.ellipsis-button {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.button-row {
+ gap: 0.5rem;
+}
+
+.button-row .ui.button {
+ margin-right: 0;
+}
+
+.ui.partial.secondary.menu {
+ margin-bottom: 0;
+}
+
+a {
+ color: var(--color-primary);
+ cursor: pointer;
+ text-decoration-line: none;
+ text-decoration-skip-ink: all;
+}
+
+a:hover {
+ text-decoration-line: underline;
+}
+
+/* a = always colored, underlined on hover */
+/* a.muted = colored on hover, underlined on hover */
+/* a.suppressed = never colored, underlined on hover */
+/* a.silenced = never colored, never underlined */
+
+a.muted,
+a.suppressed,
+a.silenced,
+.muted-links a {
+ color: inherit;
+}
+
+a:hover,
+a.suppressed:hover,
+a.muted:hover,
+a.muted:hover [class*="color-text"],
+.muted-links a:hover {
+ color: var(--color-primary);
+}
+
+a.silenced:hover,
+a.suppressed:hover {
+ color: inherit;
+}
+
+a.silenced:hover {
+ text-decoration-line: none;
+}
+
+a.label,
+.ui.search .results a,
+.ui .menu a,
+.ui.cards a.card,
+.issue-keyword a {
+ text-decoration-line: none !important;
+}
+
+.ui.search > .results {
+ background: var(--color-body);
+ border-color: var(--color-secondary);
+ overflow-wrap: anywhere; /* allow text to wrap as fomantic limits this to 18em width */
+}
+
+.ui.search > .results .result {
+ background: var(--color-body);
+ border-color: var(--color-secondary);
+ display: flex;
+ align-items: center;
+}
+
+.ui.search > .results .result .title {
+ color: var(--color-text-dark);
+}
+
+.ui.search > .results .result .description {
+ color: var(--color-text-light-2);
+}
+
+.ui.search > .results .result .image {
+ width: auto;
+ height: auto;
+}
+
+.ui.search > .results .result:hover,
+.ui.category.search > .results .category .result:hover {
+ background: var(--color-hover);
+}
+
+.inline-code-block {
+ padding: 2px 4px;
+ border-radius: .24em;
+ background-color: var(--color-label-bg);
+}
+
+.ui.menu {
+ display: flex;
+}
+
+.ui.menu,
+.ui.vertical.menu {
+ background: var(--color-menu);
+ border-color: var(--color-secondary);
+ box-shadow: none;
+}
+
+.ui.menu .item {
+ color: var(--color-text);
+ user-select: auto;
+ line-height: var(--line-height-default); /* fomantic uses "1" which causes overflow problems because "1" doesn't consider the descent part */
+}
+
+.ui.menu .item > .svg {
+ margin-right: 0.35em;
+}
+
+.ui.menu .dropdown.item:hover,
+.ui.menu a.item:hover,
+.ui.menu details.item summary:hover {
+ color: var(--color-text);
+ background: var(--color-hover);
+}
+
+.ui.menu .active.item,
+.ui.menu .active.item:hover,
+.ui.vertical.menu .active.item,
+.ui.vertical.menu .active.item:hover {
+ color: var(--color-text);
+ background: var(--color-active);
+}
+
+.ui.menu a.item:active {
+ color: var(--color-text);
+ background: none;
+}
+
+.ui.ui.menu .item.disabled {
+ color: var(--color-text-light-3);
+}
+
+.ui.menu .item::before, .ui.vertical.menu .item::before {
+ background: var(--color-secondary);
+}
+
+/* sub menu of vertical menu */
+.ui.vertical.menu .item .menu .item {
+ color: var(--color-text-light-2);
+ text-indent: 16px;
+}
+
+.ui.vertical.menu .item .menu .item:hover,
+.ui.vertical.menu .item .menu a.item:hover {
+ color: var(--color-text-light-1);
+}
+
+.ui.vertical.menu .item .menu .active.item {
+ color: var(--color-text);
+}
+
+/* slightly more contrast for filters on issue list */
+.ui.ui.menu .dropdown.item.disabled {
+ color: var(--color-text-light-2);
+}
+
+.ui.dropdown .menu {
+ background: var(--color-menu);
+ border-color: var(--color-secondary);
+}
+
+.ui.dropdown .menu > .header:not(.ui) {
+ color: var(--color-text);
+}
+
+.ui.dropdown .menu > .item {
+ color: var(--color-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ui.dropdown .menu > .item:hover {
+ color: var(--color-text);
+ background: var(--color-hover);
+}
+
+.ui.dropdown .menu > .item:active {
+ color: var(--color-text);
+ background: var(--color-active);
+}
+
+.ui.dropdown .menu .active.item {
+ color: var(--color-text);
+ background: var(--color-active);
+ border-radius: 0;
+ font-weight: var(--font-weight-normal);
+}
+
+/* fix misaligned images in webhook dropdown */
+.ui.dropdown .menu > .item > img {
+ margin-top: -0.25rem;
+ margin-bottom: -0.25rem;
+}
+.ui.dropdown .menu > .item > svg {
+ margin-right: .78rem; /* use the same margin as for <img> */
+}
+
+.ui.selection.dropdown .menu > .item {
+ border-color: var(--color-secondary);
+ white-space: nowrap;
+}
+
+.ui.selection.visible.dropdown > .text:not(.default) {
+ color: var(--color-text);
+}
+
+.ui.dropdown.selected,
+.ui.dropdown .menu .selected.item {
+ color: var(--color-text);
+ background: var(--color-hover);
+}
+
+.ui.menu .ui.dropdown .menu > .selected.item {
+ color: var(--color-text) !important;
+ background: var(--color-hover) !important;
+}
+
+.ui.dropdown .menu > .message:not(.ui) {
+ color: var(--color-text-light-2);
+}
+
+/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
+.ui.dropdown > .text > .img {
+ margin-left: 0;
+ float: none;
+ margin-right: 0.78571429rem;
+}
+
+.ui.dropdown > .text > .description,
+.ui.dropdown .menu > .item > .description {
+ color: var(--color-text-light-2);
+}
+
+/* replace item margin on secondary menu items with gap and remove both the
+ negative margins on the menu as well as margin on the items */
+.ui.secondary.menu {
+ margin-left: 0;
+ margin-right: 0;
+ gap: .35714286em;
+}
+.ui.secondary.menu .item {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.ui.secondary.menu .dropdown.item:hover,
+.ui.secondary.menu a.item:hover {
+ color: var(--color-text);
+ background: var(--color-hover);
+}
+
+.ui.secondary.menu .active.item,
+.ui.secondary.menu .active.item:hover {
+ color: var(--color-text);
+ background: var(--color-active);
+}
+
+.ui.secondary.menu.tight .item {
+ padding-left: 0.85714286em;
+ padding-right: 0.85714286em;
+}
+
+/* remove the menu clearfix so that it won't add undesired gaps when using "gap" */
+.ui.menu::after {
+ content: normal;
+}
+
+.ui.menu .dropdown.item .menu {
+ background: var(--color-body);
+}
+
+.ui.menu .ui.dropdown .menu > .item {
+ color: var(--color-text) !important;
+}
+
+.ui.menu .ui.dropdown .menu > .item:hover {
+ color: var(--color-text) !important;
+ background: var(--color-hover) !important;
+}
+
+.ui.menu .ui.dropdown .menu > .active.item {
+ color: var(--color-text) !important;
+ background: var(--color-active) !important;
+}
+
+.ui.form textarea:not([rows]) {
+ height: var(--min-height-textarea); /* override fomantic default 12em */
+ min-height: var(--min-height-textarea); /* override fomantic default 8em */
+}
+
+/* styles from removed fomantic transition module */
+.hidden.transition {
+ visibility: hidden;
+ display: none;
+}
+.visible.transition {
+ display: block !important;
+ visibility: visible !important;
+}
+
+.ui.selection.active.dropdown,
+.ui.selection.active.dropdown:hover,
+.ui.selection.active.dropdown .menu,
+.ui.selection.active.dropdown:hover .menu {
+ border-color: var(--color-primary);
+}
+
+.ui.pointing.dropdown > .menu:not(.hidden)::after {
+ background: var(--color-menu);
+ box-shadow: -1px -1px 0 0 var(--color-secondary);
+}
+
+.ui.pointing.upward.dropdown .menu::after,
+.ui.top.pointing.upward.dropdown .menu::after {
+ box-shadow: 1px 1px 0 0 var(--color-secondary);
+}
+
+.ui.comments .comment .text {
+ margin: 0;
+}
+
+.ui.comments .comment .text,
+.ui.comments .comment .author {
+ color: var(--color-text);
+}
+
+.ui.comments .comment a.author:hover {
+ color: var(--color-primary);
+}
+
+.ui.comments .comment .metadata {
+ color: var(--color-text-light-2);
+}
+
+.ui.comments .comment .actions a {
+ color: var(--color-text-light);
+}
+
+.ui.comments .comment .actions a.active,
+.ui.comments .comment .actions a:hover {
+ color: var(--color-primary);
+}
+
+img.ui.avatar,
+.ui.avatar img,
+.ui.avatar svg {
+ border-radius: var(--border-radius);
+ object-fit: contain;
+ aspect-ratio: 1;
+}
+
+.ui.error.message .header,
+.ui.warning.message .header {
+ color: inherit;
+ filter: saturate(2);
+}
+
+.full.height {
+ flex-grow: 1;
+ padding-bottom: 80px;
+}
+
+/* add margin below .secondary nav when it is the first child */
+.page-content > :first-child.secondary-nav {
+ margin-bottom: 14px;
+}
+
+/* add margin to all pages when there is no .secondary.nav */
+.page-content > :first-child:not(.secondary-nav) {
+ margin-top: var(--page-spacing);
+}
+/* if .ui.grid is the first child the first grid-column has 'padding-top: 1rem' which we need
+ to compensate here */
+.page-content > :first-child.ui.grid {
+ margin-top: calc(var(--page-spacing) - 1rem);
+}
+
+.ui.pagination.menu .active.item {
+ color: var(--color-text);
+ background: var(--color-active);
+}
+
+.ui.form .fields.error .field textarea,
+.ui.form .fields.error .field select,
+.ui.form .fields.error .field input:not([type]),
+.ui.form .fields.error .field input[type="date"],
+.ui.form .fields.error .field input[type="datetime-local"],
+.ui.form .fields.error .field input[type="email"],
+.ui.form .fields.error .field input[type="number"],
+.ui.form .fields.error .field input[type="password"],
+.ui.form .fields.error .field input[type="search"],
+.ui.form .fields.error .field input[type="tel"],
+.ui.form .fields.error .field input[type="time"],
+.ui.form .fields.error .field input[type="text"],
+.ui.form .fields.error .field input[type="file"],
+.ui.form .fields.error .field input[type="url"],
+.ui.form .fields.error .field .ui.dropdown,
+.ui.form .fields.error .field .ui.dropdown .item,
+.ui.form .field.error .ui.dropdown,
+.ui.form .field.error .ui.dropdown .text,
+.ui.form .field.error .ui.dropdown .item,
+.ui.form .field.error textarea,
+.ui.form .field.error select,
+.ui.form .field.error input:not([type]),
+.ui.form .field.error input[type="date"],
+.ui.form .field.error input[type="datetime-local"],
+.ui.form .field.error input[type="email"],
+.ui.form .field.error input[type="number"],
+.ui.form .field.error input[type="password"],
+.ui.form .field.error input[type="search"],
+.ui.form .field.error input[type="tel"],
+.ui.form .field.error input[type="time"],
+.ui.form .field.error input[type="text"],
+.ui.form .field.error input[type="file"],
+.ui.form .field.error input[type="url"],
+.ui.form .field.error select:focus,
+.ui.form .field.error input:not([type]):focus,
+.ui.form .field.error input[type="date"]:focus,
+.ui.form .field.error input[type="datetime-local"]:focus,
+.ui.form .field.error input[type="email"]:focus,
+.ui.form .field.error input[type="number"]:focus,
+.ui.form .field.error input[type="password"]:focus,
+.ui.form .field.error input[type="search"]:focus,
+.ui.form .field.error input[type="tel"]:focus,
+.ui.form .field.error input[type="time"]:focus,
+.ui.form .field.error input[type="text"]:focus,
+.ui.form .field.error input[type="file"]:focus,
+.ui.form .field.error input[type="url"]:focus {
+ background-color: var(--color-error-bg);
+ border-color: var(--color-error-border);
+ color: var(--color-error-text);
+}
+
+.ui.form .fields.error .field .ui.dropdown,
+.ui.form .field.error .ui.dropdown,
+.ui.form .fields.error .field .ui.dropdown:hover,
+.ui.form .field.error .ui.dropdown:hover {
+ border-color: var(--color-error-border) !important;
+}
+
+.ui.form .fields.error .field .ui.dropdown .menu .item:hover,
+.ui.form .field.error .ui.dropdown .menu .item:hover {
+ background-color: var(--color-error-bg-hover);
+}
+
+.ui.form .fields.error .field .ui.dropdown .menu .active.item,
+.ui.form .field.error .ui.dropdown .menu .active.item {
+ background-color: var(--color-error-bg-active) !important;
+}
+
+.ui.form .fields.error .dropdown .menu,
+.ui.form .field.error .dropdown .menu {
+ border-color: var(--color-error-border) !important;
+}
+
+input:-webkit-autofill,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:active,
+.ui.form .field.field input:-webkit-autofill,
+.ui.form .field.field input:-webkit-autofill:focus,
+.ui.form .field.field input:-webkit-autofill:hover,
+.ui.form .field.field input:-webkit-autofill:active {
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: var(--color-text);
+ box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important;
+ border-color: var(--color-primary-light-4) !important;
+}
+
+.ui.form .field.muted {
+ opacity: var(--opacity-disabled);
+}
+
+.text.primary {
+ color: var(--color-primary) !important;
+}
+
+.text.red {
+ color: var(--color-red) !important;
+}
+
+.text.orange {
+ color: var(--color-orange) !important;
+}
+
+.text.yellow {
+ color: var(--color-yellow) !important;
+}
+
+.text.olive {
+ color: var(--color-olive) !important;
+}
+
+.text.green {
+ color: var(--color-green) !important;
+}
+
+.text.teal {
+ color: var(--color-teal) !important;
+}
+
+.text.blue {
+ color: var(--color-blue) !important;
+}
+
+.text.violet {
+ color: var(--color-violet) !important;
+}
+
+.text.purple {
+ color: var(--color-purple) !important;
+}
+
+.text.pink {
+ color: var(--color-pink) !important;
+}
+
+.text.brown {
+ color: var(--color-brown) !important;
+}
+
+.text.black {
+ color: var(--color-text) !important;
+}
+
+.text.grey {
+ color: var(--color-text-light) !important;
+}
+
+.text.light {
+ color: var(--color-text-light) !important;
+}
+
+.text.light-2 {
+ color: var(--color-text-light-2) !important;
+}
+
+.text.light-3 {
+ color: var(--color-text-light-3) !important;
+}
+
+.text.light.grey {
+ color: var(--color-grey-light) !important;
+}
+
+.text.gold {
+ color: var(--color-gold) !important;
+}
+
+.text.small {
+ font-size: 0.75em;
+}
+
+.ui.form .ui.button {
+ font-weight: var(--font-weight-normal);
+}
+
+/* replace fomantic popover box shadows */
+.ui.dropdown .menu,
+.ui.upward.dropdown > .menu,
+.ui.menu .dropdown.item .menu,
+.ui.selection.active.dropdown .menu,
+.ui.upward.selection.dropdown .menu,
+.ui.selection.active.dropdown:hover .menu,
+.ui.upward.active.selection.dropdown:hover .menu {
+ box-shadow: 0 6px 18px var(--color-shadow);
+}
+.ui.floating.dropdown .menu {
+ box-shadow: 0 6px 18px var(--color-shadow) !important;
+}
+
+.ui.dimmer {
+ background: var(--color-overlay-backdrop);
+}
+
+/* Override semantic selector '.ui.menu:not(.vertical) .item > .button' */
+/* This fixes the commit graph button on the commits page */
+/* modal svg icons, copied from fomantic except width and height */
+/* center text in fomantic modal dialogs */
+.ui .menu:not(.vertical) .item > .button.compact {
+ padding: 0.58928571em 1.125em;
+}
+
+.ui .menu:not(.vertical) .item > .button.small {
+ font-size: 0.92857143rem;
+}
+
+.ui.menu .ui.dropdown.item .menu .item {
+ width: 100%;
+}
+
+.ui.dropdown .menu > .header {
+ font-size: 0.8em;
+}
+
+.ui .text.left {
+ text-align: left !important;
+}
+
+.ui .text.right {
+ text-align: right !important;
+}
+
+.ui .text.truncate {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: inline-block;
+}
+
+.ui .message.flash-message {
+ text-align: center;
+}
+
+.ui .message > ul {
+ margin-left: auto;
+ margin-right: auto;
+ display: table;
+ text-align: left;
+}
+
+.ui .header > i + .content {
+ padding-left: 0.75rem;
+ vertical-align: middle;
+}
+
+.ui .form .autofill-dummy {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ z-index: -10000;
+}
+
+.ui .form .sub.field {
+ margin-left: 25px;
+}
+
+.ui .sha.label {
+ font-family: var(--fonts-monospace);
+ font-size: 13px;
+ font-weight: var(--font-weight-normal);
+ margin: 0 6px;
+ padding: 5px 10px;
+ flex-shrink: 0;
+}
+
+.ui .sha.label .shortsha {
+ display: inline-block; /* not sure whether it is still needed */
+}
+
+.ui .button.truncate {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+ white-space: nowrap;
+ margin-right: 6px;
+}
+
+.ui.status.buttons .svg {
+ margin-right: 4px;
+}
+
+.ui.inline.delete-button {
+ padding: 8px 15px;
+ font-weight: var(--font-weight-normal);
+}
+
+.ui .background.red {
+ background-color: var(--color-red) !important;
+}
+
+.ui .background.blue {
+ background-color: var(--color-blue) !important;
+}
+
+.ui .background.black {
+ background-color: var(--color-black) !important;
+}
+
+.ui .background.grey {
+ background-color: var(--color-grey) !important;
+}
+
+.ui .background.light.grey {
+ background-color: var(--color-grey) !important;
+}
+
+.ui .background.green {
+ background-color: var(--color-green) !important;
+}
+
+.ui .background.purple {
+ background-color: var(--color-purple) !important;
+}
+
+.ui .background.yellow {
+ background-color: var(--color-yellow) !important;
+}
+
+.ui .background.orange {
+ background-color: var(--color-orange) !important;
+}
+
+.ui .background.gold {
+ background-color: var(--color-gold) !important;
+}
+
+.ui .migrate {
+ color: var(--color-text-light-2) !important;
+}
+
+.ui .migrate a {
+ color: var(--color-text-light) !important;
+}
+
+.ui .migrate a:hover {
+ color: var(--color-text) !important;
+}
+
+.ui .border {
+ border: 1px solid;
+}
+
+.ui .border.red {
+ border-color: var(--color-red) !important;
+}
+
+.ui .border.blue {
+ border-color: var(--color-blue) !important;
+}
+
+.ui .border.black {
+ border-color: var(--color-black) !important;
+}
+
+.ui .border.grey {
+ border-color: var(--color-grey) !important;
+}
+
+.ui .border.light.grey {
+ border-color: var(--color-grey) !important;
+}
+
+.ui .border.green {
+ border-color: var(--color-green) !important;
+}
+
+.ui .border.purple {
+ border-color: var(--color-purple) !important;
+}
+
+.ui .border.yellow {
+ border-color: var(--color-yellow) !important;
+}
+
+.ui .border.orange {
+ border-color: var(--color-orange) !important;
+}
+
+.ui .border.gold {
+ border-color: var(--color-gold) !important;
+}
+
+@media (max-width: 767.98px) {
+ .ui.pagination.menu .item:not(.active,.navigation),
+ .ui.pagination.menu .item.navigation span.navigation_label {
+ display: none;
+ }
+}
+
+.ui.pagination.menu.narrow .item {
+ padding-left: 8px;
+ padding-right: 8px;
+ min-width: 1em;
+ text-align: center;
+}
+
+.ui.pagination.menu.narrow .item .icon {
+ margin-right: 0;
+}
+
+.ui.floating.dropdown .overflow.menu .scrolling.menu.items {
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.user-menu > .item {
+ width: 100%;
+ border-radius: 0 !important;
+}
+
+.scrolling.menu .item.selected {
+ font-weight: var(--font-weight-semibold) !important;
+}
+
+.ui.dropdown .scrolling.menu {
+ border-color: var(--color-secondary);
+}
+
+.color-preview {
+ display: inline-block;
+ margin-left: 0.4em;
+ height: 0.67em;
+ width: 0.67em;
+ border-radius: var(--border-radius);
+}
+
+.attention-icon {
+ margin: auto 0.5em auto 0;
+}
+
+.attention-title {
+ align-items: center;
+ display: flex;
+}
+
+blockquote.attention-note {
+ border-left-color: var(--color-blue-dark-1);
+}
+strong.attention-note, svg.attention-note {
+ color: var(--color-blue-dark-1);
+}
+
+blockquote.attention-tip {
+ border-left-color: var(--color-success-text);
+}
+strong.attention-tip, svg.attention-tip {
+ color: var(--color-success-text);
+}
+
+blockquote.attention-important {
+ border-left-color: var(--color-violet-dark-1);
+}
+strong.attention-important, svg.attention-important {
+ color: var(--color-violet-dark-1);
+}
+
+blockquote.attention-warning {
+ border-left-color: var(--color-warning-text);
+}
+strong.attention-warning, svg.attention-warning {
+ color: var(--color-warning-text);
+}
+
+blockquote.attention-caution {
+ border-left-color: var(--color-red-dark-1);
+}
+strong.attention-caution, svg.attention-caution {
+ color: var(--color-red-dark-1);
+}
+
+.center:not(.popup) {
+ text-align: center;
+}
+
+overflow-menu {
+ border-bottom: 1px solid var(--color-secondary) !important;
+ display: flex;
+}
+
+overflow-menu .overflow-menu-items {
+ display: flex;
+ flex: 1;
+}
+
+overflow-menu .overflow-menu-items .item {
+ margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */
+}
+
+overflow-menu .ui.label {
+ margin-left: 7px !important; /* save some space */
+}
+
+.activity-bar-graph {
+ background-color: var(--color-primary);
+ color: var(--color-primary-contrast);
+}
+
+.archived-icon {
+ color: var(--color-secondary-dark-2) !important;
+}
+
+/* colors of colorful icons */
+svg.text.green,
+.text.green svg {
+ color: var(--color-icon-green) !important;
+}
+svg.text.red,
+.text.red svg {
+ color: var(--color-icon-red) !important;
+}
+svg.text.purple,
+.text.purple svg {
+ color: var(--color-icon-purple) !important;
+}
+
+.oauth2-authorize-application-box {
+ margin-top: 3em !important;
+}
+
+/* multiple radio or checkboxes as inline element */
+.inline-grouped-list {
+ display: inline-block;
+ vertical-align: top;
+}
+
+.inline-grouped-list > .ui {
+ display: block;
+ margin-top: 5px;
+ margin-bottom: 10px;
+}
+
+.inline-grouped-list > .ui:first-child {
+ margin-top: 1px;
+}
+
+.ui.menu .item > .label {
+ background: var(--color-label-bg);
+ color: var(--color-label-text);
+}
+
+.ui.menu .active.item > .label,
+.ui.menu.tabs-with-labels .item:hover > .label {
+ background: var(--color-label-bg-alt, var(--color-label-bg));
+}
+
+.lines-blame-btn {
+ padding: 0 0 0 5px;
+ display: flex;
+ justify-content: center;
+}
+
+.lines-num {
+ padding: 0 8px;
+ text-align: right !important;
+ color: var(--color-text-light-2);
+ width: 1%;
+ font-family: var(--fonts-monospace);
+}
+
+.lines-num span.bottom-line::after {
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.lines-num span::after {
+ content: attr(data-line-number);
+ line-height: 20px !important;
+ padding: 0 10px;
+ cursor: pointer;
+ display: block;
+}
+
+.lines-type-marker {
+ vertical-align: top;
+}
+
+.lines-num,
+.lines-code {
+ font-size: 12px;
+ font-family: var(--fonts-monospace);
+ line-height: 20px;
+ padding-top: 0;
+ padding-bottom: 0;
+ vertical-align: top;
+}
+
+.lines-num pre,
+.lines-code pre,
+.lines-num ol,
+.lines-code ol {
+ background-color: inherit;
+ margin: 0;
+ padding: 0 !important;
+}
+
+.lines-num pre li,
+.lines-code pre li,
+.lines-num ol li,
+.lines-code ol li {
+ display: block;
+ width: calc(100% - 1ch);
+ padding-left: 1ch;
+}
+
+.lines-escape {
+ width: 0;
+}
+
+.lines-code {
+ padding-left: 5px;
+}
+
+.file-view tr.active {
+ color: inherit !important;
+ background: inherit !important;
+}
+
+.file-view tr.active .lines-num,
+.file-view tr.active .lines-code {
+ background: var(--color-highlight-bg) !important;
+}
+
+.file-view tr.active:last-of-type .lines-code {
+ border-bottom-right-radius: var(--border-radius);
+}
+
+.file-view tr.active .lines-num {
+ position: relative;
+}
+
+.file-view tr.active .lines-num::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ width: 2px;
+ height: 100%;
+ background: var(--color-highlight-fg);
+}
+
+.code-inner {
+ font: 12px var(--fonts-monospace);
+ white-space: pre-wrap;
+ word-break: break-all;
+ overflow-wrap: anywhere;
+ line-height: inherit; /* needed for inline code preview in markup */
+}
+
+.blame .code-inner {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+
+.lines-commit {
+ vertical-align: top;
+ color: var(--color-text-light-1);
+ padding: 0 !important;
+ width: 1%;
+}
+
+.lines-commit .blame-info {
+ width: min(26vw, 300px);
+ display: block;
+ padding: 0 0 0 6px;
+ line-height: 20px;
+ box-sizing: content-box;
+}
+
+.lines-commit .blame-info .blame-data {
+ display: flex;
+ font-family: var(--fonts-regular);
+}
+
+.lines-commit .blame-info .blame-data .blame-message {
+ flex-grow: 2;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.lines-commit .blame-info .blame-data .blame-time,
+.lines-commit .blame-info .blame-data .blame-avatar {
+ flex-shrink: 0;
+}
+
+.blame-avatar {
+ display: flex;
+ align-items: center;
+ margin-right: 4px;
+}
+
+.top-line-blame {
+ border-top: 1px solid var(--color-secondary);
+}
+
+.code-view tr.top-line-blame:first-of-type {
+ border-top: none;
+}
+
+.lines-code .bottom-line,
+.lines-commit .bottom-line {
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.code-view {
+ background: var(--color-code-bg);
+ border-radius: var(--border-radius);
+}
+
+.code-view table {
+ width: 100%;
+}
+
+.migrate .svg.gitea-git {
+ color: var(--color-git);
+}
+
+.color-icon {
+ display: inline-block;
+ border-radius: var(--border-radius-full);
+ height: 14px;
+ width: 14px;
+}
+
+.rss-icon {
+ display: inline-flex;
+ color: var(--color-text-light-1);
+}
+
+table th[data-sortt-asc]:hover,
+table th[data-sortt-desc]:hover {
+ background: var(--color-hover) !important;
+ cursor: pointer !important;
+}
+
+table th[data-sortt-asc] .svg,
+table th[data-sortt-desc] .svg {
+ margin-left: 0.25rem;
+}
+
+.ui.dropdown .menu .item {
+ border-radius: 0;
+}
+
+.ui.dropdown .menu .item:first-of-type {
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+.ui.dropdown .menu .item:last-of-type {
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.ui.multiple.dropdown > .label {
+ box-shadow: 0 0 0 1px var(--color-secondary) inset;
+}
+
+.emoji,
+.reaction {
+ font-size: 1.25em;
+ line-height: var(--line-height-default);
+ font-style: normal !important;
+ font-weight: var(--font-weight-normal) !important;
+ vertical-align: -0.075em;
+}
+
+.emoji img,
+.reaction img {
+ border-width: 0 !important;
+ margin: 0 !important;
+ width: 1em !important;
+ height: 1em !important;
+ vertical-align: -0.15em;
+}
+
+.ui.tabular.menu {
+ border-color: var(--color-secondary);
+}
+
+.ui.tabular.menu .active.item,
+.ui.tabular.menu .active.item:hover {
+ background: var(--color-body);
+ border-color: var(--color-secondary);
+ color: var(--color-text);
+}
+
+.ui.segment .ui.tabular.menu .active.item,
+.ui.segment .ui.tabular.menu .active.item:hover {
+ background: var(--color-box-body);
+}
+
+.ui.secondary.pointing.menu {
+ border-color: var(--color-secondary);
+}
+
+.ui.tabular.menu .item,
+.ui.secondary.pointing.menu .item {
+ padding: 11px 12px !important;
+ color: var(--color-text-light-2);
+}
+
+.ui.tabular.menu .item:hover,
+.ui.secondary.pointing.menu a.item:hover, .ui.secondary.pointing.menu a.item:focus {
+ color: var(--color-text);
+}
+
+.ui.secondary.pointing.menu .active.item,
+.ui.secondary.pointing.menu .active.item:hover, .ui.secondary.pointing.menu .active.item:focus,
+.ui.secondary.pointing.menu .dropdown.item:hover, .ui.secondary.pointing.menu .dropdown.item:focus {
+ color: var(--color-text-dark);
+}
+
+.ui.tabular.menu .active.item,
+.ui.secondary.pointing.menu .active.item,
+.resize-for-semibold::before {
+ font-weight: var(--font-weight-semibold);
+}
+
+.resize-for-semibold::before {
+ content: attr(data-text);
+ visibility: hidden;
+ display: block;
+ height: 0;
+}
+
+.flash-error details code,
+.flash-warning details code {
+ display: block;
+ text-align: left;
+}
+
+.truncated-item-container {
+ display: flex !important;
+ align-items: center;
+}
+
+.ellipsis-button {
+ padding: 0 5px 8px !important;
+ display: inline-block !important;
+ font-weight: var(--font-weight-semibold) !important;
+ line-height: 6px !important;
+ vertical-align: middle !important;
+}
+
+.truncated-item-name {
+ line-height: 2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-top: -0.5em;
+ margin-bottom: -0.5em;
+}
+
+.precolors {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-left: 1em;
+}
+
+.precolors .color {
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+}
+
+.ui.dropdown:not(.button) {
+ line-height: var(--line-height-default); /* the dropdown doesn't have default line-height, use this to make the dropdown icon align with plain dropdown */
+}
+
+/* dropdown has some kinds of icons:
+- "> .dropdown.icon": the arrow for opening the dropdown
+- "> .remove.icon": the "x" icon for clearing the dropdown, only used in selection dropdown
+- "> .ui.label > .delete.icon": the "x" icon for removing a label item in multiple selection dropdown
+*/
+
+.ui.dropdown.mini.button,
+.ui.dropdown.tiny.button {
+ padding-right: 20px;
+}
+.ui.dropdown.button {
+ padding-right: 22px;
+}
+.ui.dropdown.large.button {
+ padding-right: 24px;
+}
+
+/* Gitea uses SVG images instead of Fomantic builtin "<i>" font icons, so we need to reset the icon styles */
+.ui.ui.dropdown > .icon.icon {
+ position: initial; /* plain dropdown and button dropdown use flex layout for icons */
+ padding: 0;
+ margin: 0;
+ height: auto;
+}
+
+.ui.ui.dropdown > .icon.icon:hover {
+ opacity: 1;
+}
+
+.ui.ui.button.dropdown > .icon.icon,
+.ui.ui.selection.dropdown > .icon.icon {
+ position: absolute; /* selection dropdown uses absolute layout for icons */
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.ui.ui.dropdown > .dropdown.icon {
+ right: 0.5em;
+}
+
+.ui.ui.dropdown > .remove.icon {
+ right: 2em;
+}
+
+.btn,
+.ui.ui.button,
+.ui.ui.dropdown,
+.flex-items-inline > .item,
+.flex-text-inline {
+ display: inline-flex;
+ align-items: center;
+ gap: .25rem;
+ vertical-align: middle;
+ min-width: 0;
+}
+
+.ui.ui.button {
+ justify-content: center;
+}
+
+.ui.dropdown .ui.label .svg {
+ vertical-align: middle;
+}
+
+.ui.ui.labeled.button {
+ gap: 0;
+ align-items: stretch;
+}
+
+.flex-items-block > .item,
+.flex-text-block {
+ display: flex;
+ align-items: center;
+ gap: .25rem;
+ min-width: 0;
+}
diff --git a/web_src/css/chroma/base.css b/web_src/css/chroma/base.css
new file mode 100644
index 0000000..bce1333
--- /dev/null
+++ b/web_src/css/chroma/base.css
@@ -0,0 +1,46 @@
+/* LineTableTD */
+.chroma .lntd {
+ vertical-align: top;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+/* LineTable */
+.chroma .lntable {
+ border-spacing: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+ width: auto;
+ overflow: auto;
+ display: block;
+}
+
+/* LineHighlight */
+.chroma .hl {
+ display: block;
+ width: 100%;
+}
+
+/* LineNumbersTable */
+.chroma .lnt {
+ margin-right: 0.4em;
+ padding: 0 0.4em;
+}
+
+/* LineNumbers */
+.chroma .ln {
+ margin-right: 0.4em;
+ padding: 0 0.4em;
+}
+
+/* GenericStrong */
+.chroma .gs {
+ font-weight: var(--font-weight-semibold);
+}
+
+/* GenericUnderline */
+.chroma .gl {
+ text-decoration: underline;
+}
diff --git a/web_src/css/chroma/dark.css b/web_src/css/chroma/dark.css
new file mode 100644
index 0000000..bfe9d69
--- /dev/null
+++ b/web_src/css/chroma/dark.css
@@ -0,0 +1,76 @@
+/* https://github.com/alecthomas/chroma/blob/6428fb4e65f3c1493491571c8a6a8f1add1da822/types.go#L208 */
+.chroma .bp { color: #fabd2f; } /* NameBuiltinPseudo */
+.chroma .c { color: #777e94; } /* Comment */
+.chroma .c1 { color: #777e94; } /* CommentSingle */
+.chroma .ch { color: #777e94; } /* CommentHashbang */
+.chroma .cm { color: #777e94; } /* CommentMultiline */
+.chroma .cp { color: #8ec07c; } /* CommentPreproc */
+.chroma .cpf { color: #649bc4; } /* CommentPreprocFile */
+.chroma .cs { color: #9075cd; } /* CommentSpecial */
+.chroma .dl { color: #649bc4; } /* LiteralStringDelimiter */
+.chroma .fm {} /* NameFunctionMagic */
+.chroma .g {} /* Generic */
+.chroma .gd { color: #ffffff; background-color: #5f3737; } /* GenericDeleted */
+.chroma .ge { color: #ddee30; } /* GenericEmph */
+.chroma .gh { color: #ffaa10; } /* GenericHeading */
+.chroma .gi { color: #ffffff; background-color: #3a523a; } /* GenericInserted */
+.chroma .gl {} /* GenericUnderline */
+.chroma .go { color: #777e94; } /* GenericOutput */
+.chroma .gp { color: #ebdbb2; } /* GenericPrompt */
+.chroma .gr { color: #ff4433; } /* GenericError */
+.chroma .gs { color: #ebdbb2; } /* GenericStrong */
+.chroma .gt { color: #ff7540; } /* GenericTraceback */
+.chroma .gu { color: #b8bb26; } /* GenericSubheading */
+.chroma .il { color: #649bc4; } /* LiteralNumberIntegerLong */
+.chroma .k { color: #ff7540; } /* Keyword */
+.chroma .kc { color: #649bc4; } /* KeywordConstant */
+.chroma .kd { color: #ff7540; } /* KeywordDeclaration */
+.chroma .kn { color: #ffaa10; } /* KeywordNamespace */
+.chroma .kp { color: #5f8700; } /* KeywordPseudo */
+.chroma .kr { color: #ff7540; } /* KeywordReserved */
+.chroma .kt { color: #ff7b72; } /* KeywordType */
+.chroma .l {} /* Literal */
+.chroma .ld {} /* LiteralDate */
+.chroma .m { color: #649bc4; } /* LiteralNumber */
+.chroma .mb { color: #649bc4; } /* LiteralNumberBin */
+.chroma .mf { color: #649bc4; } /* LiteralNumberFloat */
+.chroma .mh { color: #649bc4; } /* LiteralNumberHex */
+.chroma .mi { color: #649bc4; } /* LiteralNumberInteger */
+.chroma .mo { color: #649bc4; } /* LiteralNumberOct */
+.chroma .n { color: #c9d1d9; } /* Name */
+.chroma .na { color: #fabd2f; } /* NameAttribute */
+.chroma .nb { color: #fabd2f; } /* NameBuiltin */
+.chroma .nc { color: #ffaa10; } /* NameClass */
+.chroma .nd { color: #8ec07c; } /* NameDecorator */
+.chroma .ne { color: #ff7540; } /* NameException */
+.chroma .nf { color: #fabd2f; } /* NameFunction */
+.chroma .ni { color: #fabd2f; } /* NameEntity */
+.chroma .nl { color: #ff7540; } /* NameLabel */
+.chroma .nn { color: #c9d1d9; } /* NameNamespace */
+.chroma .no { color: #649bc4; } /* NameConstant */
+.chroma .nt { color: #ff7540; } /* NameTag */
+.chroma .nv { color: #ebdbb2; } /* NameVariable */
+.chroma .nx { color: #b6bac5; } /* NameOther */
+.chroma .o { color: #ff7540; } /* Operator */
+.chroma .ow { color: #5f8700; } /* OperatorWord */
+.chroma .p { color: #d2d4db; } /* Punctuation */
+.chroma .py {} /* NameProperty */
+.chroma .s { color: #b8bb26; } /* LiteralString */
+.chroma .s1 { color: #b8bb26; } /* LiteralStringSingle */
+.chroma .s2 { color: #b8bb26; } /* LiteralStringDouble */
+.chroma .sa { color: #ffaa10; } /* LiteralStringAffix */
+.chroma .sb { color: #b8bb26; } /* LiteralStringBacktick */
+.chroma .sc { color: #ffaa10; } /* LiteralStringChar */
+.chroma .sd { color: #b8bb26; } /* LiteralStringDoc */
+.chroma .se { color: #ff8540; } /* LiteralStringEscape */
+.chroma .sh { color: #b8bb26; } /* LiteralStringHeredoc */
+.chroma .si { color: #ffaa10; } /* LiteralStringInterpol */
+.chroma .sr { color: #9075cd; } /* LiteralStringRegex */
+.chroma .ss { color: #ff8540; } /* LiteralStringSymbol */
+.chroma .sx { color: #ffaa10; } /* LiteralStringOther */
+.chroma .vc { color: #649bee; } /* NameVariableClass */
+.chroma .vg { color: #649bee; } /* NameVariableGlobal */
+.chroma .vi { color: #649bee; } /* NameVariableInstance */
+.chroma .vm {} /* NameVariableMagic */
+.chroma .w { color: #7f8699; } /* TextWhitespace */
+.chroma .err {/* not styled because Chroma uses it on too many things like JSX */} /* Error */
diff --git a/web_src/css/chroma/light.css b/web_src/css/chroma/light.css
new file mode 100644
index 0000000..c1e4cb3
--- /dev/null
+++ b/web_src/css/chroma/light.css
@@ -0,0 +1,76 @@
+/* https://github.com/alecthomas/chroma/blob/6428fb4e65f3c1493491571c8a6a8f1add1da822/types.go#L208 */
+.chroma .bp { color: #999999; } /* NameBuiltinPseudo */
+.chroma .c { color: #6a737d; } /* Comment */
+.chroma .c1 { color: #6a737d; } /* CommentSingle */
+.chroma .ch { color: #6a737d; } /* CommentHashbang */
+.chroma .cm { color: #999988; } /* CommentMultiline */
+.chroma .cp { color: #109295; } /* CommentPreproc */
+.chroma .cpf { color: #4c4dbc; } /* CommentPreprocFile */
+.chroma .cs { color: #999999; } /* CommentSpecial */
+.chroma .dl { color: #106303; } /* LiteralStringDelimiter */
+.chroma .fm {} /* NameFunctionMagic */
+.chroma .g {} /* Generic */
+.chroma .gd { color: #000000; background-color: #ffdddd; } /* GenericDeleted */
+.chroma .ge { color: #000000; } /* GenericEmph */
+.chroma .gh { color: #999999; } /* GenericHeading */
+.chroma .gi { color: #000000; background-color: #ddffdd; } /* GenericInserted */
+.chroma .gl {} /* GenericUnderline */
+.chroma .go { color: #888888; } /* GenericOutput */
+.chroma .gp { color: #555555; } /* GenericPrompt */
+.chroma .gr { color: #aa0000; } /* GenericError */
+.chroma .gs {} /* GenericStrong */
+.chroma .gt { color: #aa0000; } /* GenericTraceback */
+.chroma .gu { color: #aaaaaa; } /* GenericSubheading */
+.chroma .il { color: #009999; } /* LiteralNumberIntegerLong */
+.chroma .k { color: #d73a49; } /* Keyword */
+.chroma .kc { color: #d73a49; } /* KeywordConstant */
+.chroma .kd { color: #d73a49; } /* KeywordDeclaration */
+.chroma .kn { color: #d73a49; } /* KeywordNamespace */
+.chroma .kp { color: #d73a49; } /* KeywordPseudo */
+.chroma .kr { color: #d73a49; } /* KeywordReserved */
+.chroma .kt { color: #445588; } /* KeywordType */
+.chroma .l {} /* Literal */
+.chroma .ld {} /* LiteralDate */
+.chroma .m { color: #009999; } /* LiteralNumber */
+.chroma .mb { color: #009999; } /* LiteralNumberBin */
+.chroma .mf { color: #009999; } /* LiteralNumberFloat */
+.chroma .mh { color: #009999; } /* LiteralNumberHex */
+.chroma .mi { color: #009999; } /* LiteralNumberInteger */
+.chroma .mo { color: #009999; } /* LiteralNumberOct */
+.chroma .n {} /* Name */
+.chroma .na { color: #d73a49; } /* NameAttribute */
+.chroma .nb { color: #005cc5; } /* NameBuiltin */
+.chroma .nc { color: #445588; } /* NameClass */
+.chroma .nd { color: #3c5d5d; } /* NameDecorator */
+.chroma .ne { color: #990000; } /* NameException */
+.chroma .nf { color: #005cc5; } /* NameFunction */
+.chroma .ni { color: #6f42c1; } /* NameEntity */
+.chroma .nl { color: #990000; } /* NameLabel */
+.chroma .nn { color: #555555; } /* NameNamespace */
+.chroma .no { color: #008080; } /* NameConstant */
+.chroma .nt { color: #22863a; } /* NameTag */
+.chroma .nv { color: #008080; } /* NameVariable */
+.chroma .nx { color: #24292e; } /* NameOther */
+.chroma .o { color: #d73a49; } /* Operator */
+.chroma .ow { color: #d73a49; } /* OperatorWord */
+.chroma .p {} /* Punctuation */
+.chroma .py {} /* NameProperty */
+.chroma .s { color: #106303; } /* LiteralString */
+.chroma .s1 { color: #106303; } /* LiteralStringSingle */
+.chroma .s2 { color: #106303; } /* LiteralStringDouble */
+.chroma .sa { color: #cc7a00; } /* LiteralStringAffix */
+.chroma .sb { color: #106303; } /* LiteralStringBacktick */
+.chroma .sc { color: #cc7a00; } /* LiteralStringChar */
+.chroma .sd { color: #106303; } /* LiteralStringDoc */
+.chroma .se { color: #994400; } /* LiteralStringEscape */
+.chroma .sh { color: #106303; } /* LiteralStringHeredoc */
+.chroma .si { color: #cc7a00; } /* LiteralStringInterpol */
+.chroma .sr { color: #4c4dbc; } /* LiteralStringRegex */
+.chroma .ss { color: #994400; } /* LiteralStringSymbol */
+.chroma .sx { color: #106303; } /* LiteralStringOther */
+.chroma .vc { color: #008080; } /* NameVariableClass */
+.chroma .vg { color: #008080; } /* NameVariableGlobal */
+.chroma .vi { color: #008080; } /* NameVariableInstance */
+.chroma .vm {} /* NameVariableMagic */
+.chroma .w { color: #bbbbbb; } /* TextWhitespace */
+.chroma .err {/* not styled because Chroma uses it on too many things like JSX */} /* Error */
diff --git a/web_src/css/codemirror/base.css b/web_src/css/codemirror/base.css
new file mode 100644
index 0000000..aedf7d8
--- /dev/null
+++ b/web_src/css/codemirror/base.css
@@ -0,0 +1,49 @@
+.ui .field:not(:last-child) .EasyMDEContainer .editor-statusbar {
+ margin-bottom: -1em; /* when there is a statusbar, the "margin-bottom: 1em" of the "field" is not needed, because the statusbar is likely a blank line */
+}
+
+.EasyMDEContainer .CodeMirror {
+ color: var(--color-input-text);
+ background-color: var(--color-input-background);
+ border-color: var(--color-secondary);
+ font: 14px var(--fonts-monospace);
+}
+
+.EasyMDEContainer .CodeMirror.cm-s-default {
+ border-radius: var(--border-radius);
+ padding: 0 !important;
+}
+
+.EasyMDEContainer .CodeMirror.CodeMirror-fullscreen.CodeMirror-focused {
+ border-right: 1px solid var(--color-primary) !important;
+}
+
+.CodeMirror-cursor {
+ border-color: var(--color-caret) !important;
+}
+
+.CodeMirror .cm-comment {
+ background: inherit !important;
+}
+
+.CodeMirror .CodeMirror-code {
+ font: 14px var(--fonts-monospace);
+}
+
+.CodeMirror-selected {
+ background: var(--color-primary-light-1) !important;
+ color: var(--color-white) !important;
+}
+
+.CodeMirror-placeholder {
+ color: var(--color-placeholder-text) !important;
+ opacity: 1 !important;
+}
+
+.CodeMirror-focused {
+ border-color: var(--color-primary) !important;
+}
+
+.CodeMirror :focus {
+ outline: none;
+}
diff --git a/web_src/css/codemirror/dark.css b/web_src/css/codemirror/dark.css
new file mode 100644
index 0000000..8a20d1c
--- /dev/null
+++ b/web_src/css/codemirror/dark.css
@@ -0,0 +1,106 @@
+.CodeMirror.cm-s-default .cm-property,
+.CodeMirror.cm-s-paper .cm-property {
+ color: #a0cc75;
+}
+
+.CodeMirror.cm-s-default .cm-header,
+.CodeMirror.cm-s-paper .cm-header {
+ color: #9daccc;
+}
+
+.CodeMirror.cm-s-default .cm-quote,
+.CodeMirror.cm-s-paper .cm-quote {
+ color: #009900;
+}
+
+.CodeMirror.cm-s-default .cm-keyword,
+.CodeMirror.cm-s-paper .cm-keyword {
+ color: #cc8a61;
+}
+
+.CodeMirror.cm-s-default .cm-atom,
+.CodeMirror.cm-s-paper .cm-atom {
+ color: #ef5e77;
+}
+
+.CodeMirror.cm-s-default .cm-number,
+.CodeMirror.cm-s-paper .cm-number {
+ color: #ff5656;
+}
+
+.CodeMirror.cm-s-default .cm-def,
+.CodeMirror.cm-s-paper .cm-def {
+ color: #e4e4e4;
+}
+
+.CodeMirror.cm-s-default .cm-variable-2,
+.CodeMirror.cm-s-paper .cm-variable-2 {
+ color: #00bdbf;
+}
+
+.CodeMirror.cm-s-default .cm-variable-3,
+.CodeMirror.cm-s-paper .cm-variable-3 {
+ color: #008855;
+}
+
+.CodeMirror.cm-s-default .cm-comment,
+.CodeMirror.cm-s-paper .cm-comment {
+ color: #8e9ab3;
+}
+
+.CodeMirror.cm-s-default .cm-string,
+.CodeMirror.cm-s-paper .cm-string {
+ color: #a77272;
+}
+
+.CodeMirror.cm-s-default .cm-string-2,
+.CodeMirror.cm-s-paper .cm-string-2 {
+ color: #ff5500;
+}
+
+.CodeMirror.cm-s-default .cm-meta,
+.CodeMirror.cm-s-paper .cm-meta,
+.CodeMirror.cm-s-default .cm-qualifier,
+.CodeMirror.cm-s-paper .cm-qualifier {
+ color: #ffb176;
+}
+
+.CodeMirror.cm-s-default .cm-builtin,
+.CodeMirror.cm-s-paper .cm-builtin {
+ color: #b7c951;
+}
+
+.CodeMirror.cm-s-default .cm-bracket,
+.CodeMirror.cm-s-paper .cm-bracket {
+ color: #999977;
+}
+
+.CodeMirror.cm-s-default .cm-tag,
+.CodeMirror.cm-s-paper .cm-tag {
+ color: #f1d273;
+}
+
+.CodeMirror.cm-s-default .cm-attribute,
+.CodeMirror.cm-s-paper .cm-attribute {
+ color: #bfcc70;
+}
+
+.CodeMirror.cm-s-default .cm-hr,
+.CodeMirror.cm-s-paper .cm-hr {
+ color: #999999;
+}
+
+.CodeMirror.cm-s-default .cm-url,
+.CodeMirror.cm-s-paper .cm-url {
+ color: #c5cfd0;
+}
+
+.CodeMirror.cm-s-default .cm-link,
+.CodeMirror.cm-s-paper .cm-link {
+ color: #d8c792;
+}
+
+.CodeMirror.cm-s-default .cm-error,
+.CodeMirror.cm-s-paper .cm-error {
+ color: #dbdbeb;
+}
diff --git a/web_src/css/codemirror/light.css b/web_src/css/codemirror/light.css
new file mode 100644
index 0000000..aa89263
--- /dev/null
+++ b/web_src/css/codemirror/light.css
@@ -0,0 +1 @@
+/* Placeholder, there is no light theme, it uses CM defaults */
diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css
new file mode 100644
index 0000000..4bb9fa3
--- /dev/null
+++ b/web_src/css/dashboard.css
@@ -0,0 +1,81 @@
+.dashboard.feeds .context.user.menu,
+.dashboard.issues .context.user.menu {
+ z-index: 101;
+ min-width: 200px;
+}
+
+.dashboard.feeds .context.user.menu .ui.header,
+.dashboard.issues .context.user.menu .ui.header {
+ font-size: 1rem;
+ text-transform: none;
+}
+
+.dashboard.feeds .filter.menu,
+.dashboard.issues .filter.menu {
+ width: initial;
+}
+
+.dashboard.feeds .filter.menu .item,
+.dashboard.issues .filter.menu .item {
+ text-align: left;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.dashboard.feeds .filter.menu .item .text.truncate,
+.dashboard.issues .filter.menu .item .text.truncate {
+ width: 75%;
+}
+
+/* Sort */
+.dashboard.feeds .filter.menu .jump.item,
+.dashboard.issues .filter.menu .jump.item {
+ margin: 1px;
+ padding-right: 0;
+}
+
+.dashboard.feeds .filter.menu .menu,
+.dashboard.issues .filter.menu .menu {
+ max-height: 300px;
+ overflow-x: auto;
+ right: 0 !important;
+ left: auto !important;
+}
+
+@media (max-width: 767.98px) {
+ .dashboard.feeds .filter.menu,
+ .dashboard.issues .filter.menu {
+ width: 100%;
+ }
+}
+
+.dashboard.feeds .right.stackable.menu > .item.active,
+.dashboard.issues .right.stackable.menu > .item.active {
+ color: var(--color-red);
+}
+
+.dashboard .dashboard-repos,
+.dashboard .dashboard-orgs {
+ margin: 0 1px; /* Accommodate for Semantic's 1px hacks on .attached elements */
+}
+
+.dashboard .secondary-nav {
+ padding: 1px 12px; /* match .overflow-menu-items in height */
+}
+
+.dashboard .secondary-nav .right.menu {
+ gap: .35714286em;
+}
+
+.dashboard .secondary-nav .right.menu div.item {
+ padding-left: 0.5rem;
+}
+
+.dashboard .secondary-nav .org-visibility .label {
+ margin-left: 5px;
+}
+
+.dashboard .secondary-nav .ui.dropdown {
+ max-width: 100%;
+}
diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css
new file mode 100644
index 0000000..f190c7e
--- /dev/null
+++ b/web_src/css/editor/combomarkdowneditor.css
@@ -0,0 +1,136 @@
+.combo-markdown-editor {
+ width: 100%;
+}
+
+.combo-markdown-editor markdown-toolbar {
+ cursor: default;
+ display: flex;
+ align-items: center;
+ padding-bottom: 10px;
+ gap: .5rem;
+ flex-wrap: wrap;
+}
+
+.combo-markdown-editor .markdown-toolbar-group {
+ display: flex;
+}
+
+.combo-markdown-editor .markdown-toolbar-group:last-child {
+ flex: 1;
+ justify-content: flex-end;
+}
+
+.combo-markdown-editor .markdown-toolbar-button {
+ border: none;
+ background: none;
+ user-select: none;
+ padding: 5px;
+ cursor: pointer;
+ color: var(--color-text);
+ line-height: 20px;
+}
+
+.combo-markdown-editor .markdown-toolbar-button:hover {
+ color: var(--color-primary);
+}
+
+.ui.form .combo-markdown-editor textarea.markdown-text-editor,
+.combo-markdown-editor textarea.markdown-text-editor {
+ display: block;
+ width: 100%;
+ max-height: calc(100vh - var(--min-height-textarea));
+ resize: vertical;
+}
+
+.combo-markdown-editor .CodeMirror-scroll {
+ max-height: calc(100vh - var(--min-height-textarea));
+}
+
+/* use the same styles as markup/content.css */
+.combo-markdown-editor .CodeMirror-scroll .cm-header-1 {
+ font-size: 2em;
+}
+
+.combo-markdown-editor .CodeMirror-scroll .cm-header-2 {
+ font-size: 1.5em;
+}
+
+.combo-markdown-editor .CodeMirror-scroll .cm-header-3 {
+ font-size: 1.25em;
+}
+
+.combo-markdown-editor .CodeMirror-scroll .cm-header-4 {
+ font-size: 1em;
+}
+
+.combo-markdown-editor .CodeMirror-scroll .cm-header-5 {
+ font-size: 0.875em;
+}
+
+.combo-markdown-editor .CodeMirror-scroll .cm-header-6 {
+ font-size: 0.85em;
+}
+
+text-expander {
+ display: block;
+ position: relative;
+}
+
+text-expander .suggestions {
+ position: absolute;
+ min-width: 180px;
+ padding: 0;
+ margin-top: 24px;
+ list-style: none;
+ background: var(--color-box-body);
+ border-radius: var(--border-radius);
+ border: 1px solid var(--color-secondary);
+ box-shadow: 0 .5rem 1rem var(--color-shadow);
+ z-index: 100; /* needs to be > 20 to be on top of dropzone's .dz-details */
+}
+
+text-expander .suggestions li {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ padding: 4px 8px;
+ font-weight: var(--font-weight-medium);
+}
+
+text-expander .suggestions li + li {
+ border-top: 1px solid var(--color-secondary-alpha-40);
+}
+
+text-expander .suggestions li:first-child {
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+text-expander .suggestions li:last-child {
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+text-expander .suggestions li:only-child {
+ border-radius: var(--border-radius);
+}
+
+text-expander .suggestions li:hover {
+ background: var(--color-hover);
+}
+
+text-expander .suggestions .fullname {
+ font-weight: var(--font-weight-normal);
+ margin-left: 4px;
+ color: var(--color-text-light-1);
+}
+
+text-expander .suggestions li[aria-selected="true"],
+text-expander .suggestions li[aria-selected="true"] span {
+ background: var(--color-primary);
+ color: var(--color-primary-contrast);
+}
+
+text-expander .suggestions img {
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+}
diff --git a/web_src/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css
new file mode 100644
index 0000000..444ee8c
--- /dev/null
+++ b/web_src/css/editor/fileeditor.css
@@ -0,0 +1,85 @@
+.repository.file.editor .tab[data-tab="write"] {
+ padding: 0 !important;
+}
+
+.repository.file.editor .tab[data-tab="write"] .editor-toolbar {
+ border: 0 !important;
+}
+
+.repository.file.editor .tab[data-tab="write"] .CodeMirror {
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 0;
+}
+
+.repo-editor-header {
+ display: flex;
+ margin: 1rem 0;
+ padding: 3px 0;
+}
+
+.editor-toolbar {
+ border-color: var(--color-secondary);
+}
+
+.editor-toolbar.fullscreen {
+ background: var(--color-body);
+}
+
+.editor-toolbar button {
+ border: none !important;
+ color: var(--color-text-light);
+}
+
+.editor-toolbar button:not(:hover) {
+ background-color: transparent !important;
+}
+
+.editor-toolbar i.separator {
+ border-left: none;
+ border-right-color: var(--color-secondary);
+}
+
+.editor-toolbar button:hover {
+ background: var(--color-hover);
+}
+
+.editor-toolbar button.active {
+ background: var(--color-active);
+}
+
+/* hide preview button, we have the preview tab for this */
+.editor-toolbar:not(.fullscreen) .preview {
+ display: none;
+}
+
+/* hide revert button in fullscreen, it breaks the page */
+.editor-toolbar.fullscreen .revert-to-textarea {
+ display: none;
+}
+
+.editor-preview {
+ background-color: var(--color-body);
+}
+
+.editor-preview-side {
+ border-color: var(--color-secondary);
+}
+
+.editor-statusbar {
+ color: var(--color-text-light);
+}
+
+.editor-loading {
+ padding: 1rem;
+ text-align: center;
+}
+
+.edit-diff {
+ padding: 0 !important;
+}
+
+.edit-diff > div > .ui.table {
+ border-top: none !important;
+ border-bottom: none !important;
+}
diff --git a/web_src/css/explore.css b/web_src/css/explore.css
new file mode 100644
index 0000000..5cdee82
--- /dev/null
+++ b/web_src/css/explore.css
@@ -0,0 +1,31 @@
+.explore .secondary-nav {
+ border-width: 1px !important;
+}
+
+.explore .secondary-nav .svg {
+ width: 16px;
+ text-align: center;
+ margin-right: 5px;
+}
+
+.ui.repository.branches .info {
+ font-size: 12px;
+ color: var(--color-text-light);
+ display: flex;
+ white-space: pre;
+}
+
+.ui.repository.branches .info .commit-message {
+ max-width: 72em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ui.repository.branches .overflow-visible {
+ overflow: visible;
+}
+
+/* fix alignment of PR popup in branches table */
+.ui.repository.branches table .ui.popup {
+ text-align: left;
+}
diff --git a/web_src/css/features/codeeditor.css b/web_src/css/features/codeeditor.css
new file mode 100644
index 0000000..34a104c
--- /dev/null
+++ b/web_src/css/features/codeeditor.css
@@ -0,0 +1,48 @@
+.monaco-editor-container,
+.editor-loading.is-loading {
+ width: 100%;
+ min-height: 200px;
+ height: 90vh;
+}
+
+.edit.githook .monaco-editor-container {
+ border: 1px solid var(--color-secondary);
+ height: 70vh;
+}
+
+/* overwrite conflicting styles from fomantic */
+.monaco-editor-container .inputarea {
+ min-height: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ resize: none !important;
+ border: none !important;
+ color: transparent !important;
+ background-color: transparent !important;
+}
+
+/* these seem unthemeable */
+.monaco-scrollable-element > .scrollbar > .slider {
+ background: var(--color-primary) !important;
+}
+.monaco-scrollable-element > .scrollbar > .slider:hover {
+ background: var(--color-primary-dark-1) !important;
+}
+.monaco-scrollable-element > .scrollbar > .slider:active {
+ background: var(--color-primary-dark-2) !important;
+}
+
+/* fomantic styles destroy this element only visible on IOS, restore it */
+.monaco-editor .iPadShowKeyboard {
+ border: none !important;
+ width: 58px !important;
+ min-width: 0 !important;
+ height: 36px !important;
+ min-height: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ position: absolute !important;
+ resize: none !important;
+ overflow: hidden !important;
+ border-radius: var(--border-radius-medium) !important;
+}
diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css
new file mode 100644
index 0000000..b743678
--- /dev/null
+++ b/web_src/css/features/colorpicker.css
@@ -0,0 +1,47 @@
+.js-color-picker-input {
+ display: flex;
+ position: relative;
+}
+
+.js-color-picker-input input {
+ padding-top: 8px !important;
+ padding-bottom: 8px !important;
+ padding-left: 32px !important;
+}
+
+.js-color-picker-input .preview-square {
+ position: absolute;
+ aspect-ratio: 1;
+ height: 16px;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ border-radius: 2px;
+ background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+ background-position: 0 0, 4px 4px;
+ background-size: 8px 8px;
+}
+
+.js-color-picker-input .preview-square::after {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ background-color: currentcolor;
+}
+
+hex-color-picker {
+ width: 180px;
+ height: 120px;
+}
+
+hex-color-picker::part(hue-pointer),
+hex-color-picker::part(saturation-pointer) {
+ width: 22px;
+ height: 22px;
+}
+
+hex-color-picker::part(hue) {
+ flex-basis: 16px;
+}
diff --git a/web_src/css/features/console.css b/web_src/css/features/console.css
new file mode 100644
index 0000000..e2d3327
--- /dev/null
+++ b/web_src/css/features/console.css
@@ -0,0 +1,338 @@
+/* Based on https://github.com/buildkite/terminal-to-html/blob/697ff23bd8dc48b9d23f11f259f5256dae2455f0/assets/terminal.css */
+
+.console {
+ background: var(--color-console-bg);
+ color: var(--color-console-fg);
+ font-family: var(--fonts-monospace);
+ border-radius: var(--border-radius);
+ overflow-wrap: anywhere;
+}
+
+.console img { max-width: 100%; }
+
+.console a {
+ color: inherit;
+ text-decoration: underline;
+ text-decoration-style: dashed;
+}
+.console a:hover { color: var(--color-primary); }
+
+@keyframes blink-animation {
+ to {
+ visibility: hidden;
+ }
+}
+
+/* ansi_up colors used in actions */
+
+.ansi-black-fg { color: var(--color-ansi-black); }
+.ansi-red-fg { color: var(--color-ansi-red); }
+.ansi-green-fg { color: var(--color-ansi-green); }
+.ansi-yellow-fg { color: var(--color-ansi-yellow); }
+.ansi-blue-fg { color: var(--color-ansi-blue); }
+.ansi-magenta-fg { color: var(--color-ansi-magenta); }
+.ansi-cyan-fg { color: var(--color-ansi-cyan); }
+.ansi-white-fg { color: var(--color-ansi-white); }
+
+.ansi-bright-black-fg { color: var(--color-ansi-bright-black); }
+.ansi-bright-red-fg { color: var(--color-ansi-bright-red); }
+.ansi-bright-green-fg { color: var(--color-ansi-bright-green); }
+.ansi-bright-yellow-fg { color: var(--color-ansi-bright-yellow); }
+.ansi-bright-blue-fg { color: var(--color-ansi-bright-blue); }
+.ansi-bright-magenta-fg { color: var(--color-ansi-bright-magenta); }
+.ansi-bright-cyan-fg { color: var(--color-ansi-bright-cyan); }
+.ansi-bright-white-fg { color: var(--color-ansi-bright-white); }
+
+.ansi-black-bg { background-color: var(--color-ansi-black); }
+.ansi-red-bg { background-color: var(--color-ansi-red); }
+.ansi-green-bg { background-color: var(--color-ansi-green); }
+.ansi-yellow-bg { background-color: var(--color-ansi-yellow); }
+.ansi-blue-bg { background-color: var(--color-ansi-blue); }
+.ansi-magenta-bg { background-color: var(--color-ansi-magenta); }
+.ansi-cyan-bg { background-color: var(--color-ansi-cyan); }
+.ansi-white-bg { background-color: var(--color-ansi-white); }
+
+.ansi-bright-black-bg { background-color: var(--color-ansi-bright-black); }
+.ansi-bright-red-bg { background-color: var(--color-ansi-bright-red); }
+.ansi-bright-green-bg { background-color: var(--color-ansi-bright-green); }
+.ansi-bright-yellow-bg { background-color: var(--color-ansi-bright-yellow); }
+.ansi-bright-blue-bg { background-color: var(--color-ansi-bright-blue); }
+.ansi-bright-magenta-bg { background-color: var(--color-ansi-bright-magenta); }
+.ansi-bright-cyan-bg { background-color: var(--color-ansi-bright-cyan); }
+.ansi-bright-white-bg { background-color: var(--color-ansi-bright-white); }
+
+/* term colors used in console rendering */
+
+.term-fg2 { color: var(--color-ansi-bright-black); } /* faint (decreased intensity) - same as gray really */
+.term-fg3 { font-style: italic; } /* italic */
+.term-fg4 { text-decoration: underline; } /* underline */
+.term-fg5 { animation: blink-animation 1s steps(3, start) infinite; } /* blink */
+.term-fg9 { text-decoration: line-through; } /* crossed-out */
+
+.term-fg30 { color: var(--color-ansi-black); } /* black (but we can't use black, so a diff color) */
+.term-fg31 { color: var(--color-ansi-red); } /* red */
+.term-fg32 { color: var(--color-ansi-green); } /* green */
+.term-fg33 { color: var(--color-ansi-yellow); } /* yellow */
+.term-fg34 { color: var(--color-ansi-blue); } /* blue */
+.term-fg35 { color: var(--color-ansi-magenta); } /* magenta */
+.term-fg36 { color: var(--color-ansi-cyan); } /* cyan */
+
+/* high intense colors */
+.term-fgi1 { color: var(--color-ansi-bright-green); }
+.term-fgi90 { color: var(--color-ansi-bright-black); } /* grey */
+.term-fgi91 { color: var(--color-ansi-bright-red); } /* red */
+.term-fgi92 { color: var(--color-ansi-bright-green); } /* green */
+.term-fgi93 { color: var(--color-ansi-bright-yellow); } /* yellow */
+.term-fgi94 { color: var(--color-ansi-bright-blue); } /* blue */
+.term-fgi95 { color: var(--color-ansi-bright-magenta); } /* magenta */
+.term-fgi96 { color: var(--color-ansi-bright-cyan); } /* cyan */
+
+/* background colors */
+.term-bg40 { background: var(--color-ansi-bright-black); } /* grey */
+.term-bg41 { background: var(--color-ansi-red); } /* red */
+.term-bg42 { background: var(--color-ansi-green); } /* green */
+
+/* custom foreground/background combos for readability */
+.term-fg31.term-bg40 { color: var(--color-ansi-bright-red); }
+
+/* xterm colors */
+.term-fgx16 { color: #000000; }
+.term-fgx17 { color: #00005f; }
+.term-fgx18 { color: #000087; }
+.term-fgx19 { color: #0000af; }
+.term-fgx20 { color: #0000d7; }
+.term-fgx21 { color: #0000ff; }
+.term-fgx22 { color: #005f00; }
+.term-fgx23 { color: #005f5f; }
+.term-fgx24 { color: #005f87; }
+.term-fgx25 { color: #005faf; }
+.term-fgx26 { color: #005fd7; }
+.term-fgx27 { color: #005fff; }
+.term-fgx28 { color: #008700; }
+.term-fgx29 { color: #00875f; }
+.term-fgx30 { color: #008787; }
+.term-fgx31 { color: #0087af; }
+.term-fgx32 { color: #0087d7; }
+.term-fgx33 { color: #0087ff; }
+.term-fgx34 { color: #00af00; }
+.term-fgx35 { color: #00af5f; }
+.term-fgx36 { color: #00af87; }
+.term-fgx37 { color: #00afaf; }
+.term-fgx38 { color: #00afd7; }
+.term-fgx39 { color: #00afff; }
+.term-fgx40 { color: #00d700; }
+.term-fgx41 { color: #00d75f; }
+.term-fgx42 { color: #00d787; }
+.term-fgx43 { color: #00d7af; }
+.term-fgx44 { color: #00d7d7; }
+.term-fgx45 { color: #00d7ff; }
+.term-fgx46 { color: #00ff00; }
+.term-fgx47 { color: #00ff5f; }
+.term-fgx48 { color: #00ff87; }
+.term-fgx49 { color: #00ffaf; }
+.term-fgx50 { color: #00ffd7; }
+.term-fgx51 { color: #00ffff; }
+.term-fgx52 { color: #5f0000; }
+.term-fgx53 { color: #5f005f; }
+.term-fgx54 { color: #5f0087; }
+.term-fgx55 { color: #5f00af; }
+.term-fgx56 { color: #5f00d7; }
+.term-fgx57 { color: #5f00ff; }
+.term-fgx58 { color: #5f5f00; }
+.term-fgx59 { color: #5f5f5f; }
+.term-fgx60 { color: #5f5f87; }
+.term-fgx61 { color: #5f5faf; }
+.term-fgx62 { color: #5f5fd7; }
+.term-fgx63 { color: #5f5fff; }
+.term-fgx64 { color: #5f8700; }
+.term-fgx65 { color: #5f875f; }
+.term-fgx66 { color: #5f8787; }
+.term-fgx67 { color: #5f87af; }
+.term-fgx68 { color: #5f87d7; }
+.term-fgx69 { color: #5f87ff; }
+.term-fgx70 { color: #5faf00; }
+.term-fgx71 { color: #5faf5f; }
+.term-fgx72 { color: #5faf87; }
+.term-fgx73 { color: #5fafaf; }
+.term-fgx74 { color: #5fafd7; }
+.term-fgx75 { color: #5fafff; }
+.term-fgx76 { color: #5fd700; }
+.term-fgx77 { color: #5fd75f; }
+.term-fgx78 { color: #5fd787; }
+.term-fgx79 { color: #5fd7af; }
+.term-fgx80 { color: #5fd7d7; }
+.term-fgx81 { color: #5fd7ff; }
+.term-fgx82 { color: #5fff00; }
+.term-fgx83 { color: #5fff5f; }
+.term-fgx84 { color: #5fff87; }
+.term-fgx85 { color: #5fffaf; }
+.term-fgx86 { color: #5fffd7; }
+.term-fgx87 { color: #5fffff; }
+.term-fgx88 { color: #870000; }
+.term-fgx89 { color: #87005f; }
+.term-fgx90 { color: #870087; }
+.term-fgx91 { color: #8700af; }
+.term-fgx92 { color: #8700d7; }
+.term-fgx93 { color: #8700ff; }
+.term-fgx94 { color: #875f00; }
+.term-fgx95 { color: #875f5f; }
+.term-fgx96 { color: #875f87; }
+.term-fgx97 { color: #875faf; }
+.term-fgx98 { color: #875fd7; }
+.term-fgx99 { color: #875fff; }
+.term-fgx100 { color: #878700; }
+.term-fgx101 { color: #87875f; }
+.term-fgx102 { color: #878787; }
+.term-fgx103 { color: #8787af; }
+.term-fgx104 { color: #8787d7; }
+.term-fgx105 { color: #8787ff; }
+.term-fgx106 { color: #87af00; }
+.term-fgx107 { color: #87af5f; }
+.term-fgx108 { color: #87af87; }
+.term-fgx109 { color: #87afaf; }
+.term-fgx110 { color: #87afd7; }
+.term-fgx111 { color: #87afff; }
+.term-fgx112 { color: #87d700; }
+.term-fgx113 { color: #87d75f; }
+.term-fgx114 { color: #87d787; }
+.term-fgx115 { color: #87d7af; }
+.term-fgx116 { color: #87d7d7; }
+.term-fgx117 { color: #87d7ff; }
+.term-fgx118 { color: #87ff00; }
+.term-fgx119 { color: #87ff5f; }
+.term-fgx120 { color: #87ff87; }
+.term-fgx121 { color: #87ffaf; }
+.term-fgx122 { color: #87ffd7; }
+.term-fgx123 { color: #87ffff; }
+.term-fgx124 { color: #af0000; }
+.term-fgx125 { color: #af005f; }
+.term-fgx126 { color: #af0087; }
+.term-fgx127 { color: #af00af; }
+.term-fgx128 { color: #af00d7; }
+.term-fgx129 { color: #af00ff; }
+.term-fgx130 { color: #af5f00; }
+.term-fgx131 { color: #af5f5f; }
+.term-fgx132 { color: #af5f87; }
+.term-fgx133 { color: #af5faf; }
+.term-fgx134 { color: #af5fd7; }
+.term-fgx135 { color: #af5fff; }
+.term-fgx136 { color: #af8700; }
+.term-fgx137 { color: #af875f; }
+.term-fgx138 { color: #af8787; }
+.term-fgx139 { color: #af87af; }
+.term-fgx140 { color: #af87d7; }
+.term-fgx141 { color: #af87ff; }
+.term-fgx142 { color: #afaf00; }
+.term-fgx143 { color: #afaf5f; }
+.term-fgx144 { color: #afaf87; }
+.term-fgx145 { color: #afafaf; }
+.term-fgx146 { color: #afafd7; }
+.term-fgx147 { color: #afafff; }
+.term-fgx148 { color: #afd700; }
+.term-fgx149 { color: #afd75f; }
+.term-fgx150 { color: #afd787; }
+.term-fgx151 { color: #afd7af; }
+.term-fgx152 { color: #afd7d7; }
+.term-fgx153 { color: #afd7ff; }
+.term-fgx154 { color: #afff00; }
+.term-fgx155 { color: #afff5f; }
+.term-fgx156 { color: #afff87; }
+.term-fgx157 { color: #afffaf; }
+.term-fgx158 { color: #afffd7; }
+.term-fgx159 { color: #afffff; }
+.term-fgx160 { color: #d70000; }
+.term-fgx161 { color: #d7005f; }
+.term-fgx162 { color: #d70087; }
+.term-fgx163 { color: #d700af; }
+.term-fgx164 { color: #d700d7; }
+.term-fgx165 { color: #d700ff; }
+.term-fgx166 { color: #d75f00; }
+.term-fgx167 { color: #d75f5f; }
+.term-fgx168 { color: #d75f87; }
+.term-fgx169 { color: #d75faf; }
+.term-fgx170 { color: #d75fd7; }
+.term-fgx171 { color: #d75fff; }
+.term-fgx172 { color: #d78700; }
+.term-fgx173 { color: #d7875f; }
+.term-fgx174 { color: #d78787; }
+.term-fgx175 { color: #d787af; }
+.term-fgx176 { color: #d787d7; }
+.term-fgx177 { color: #d787ff; }
+.term-fgx178 { color: #d7af00; }
+.term-fgx179 { color: #d7af5f; }
+.term-fgx180 { color: #d7af87; }
+.term-fgx181 { color: #d7afaf; }
+.term-fgx182 { color: #d7afd7; }
+.term-fgx183 { color: #d7afff; }
+.term-fgx184 { color: #d7d700; }
+.term-fgx185 { color: #d7d75f; }
+.term-fgx186 { color: #d7d787; }
+.term-fgx187 { color: #d7d7af; }
+.term-fgx188 { color: #d7d7d7; }
+.term-fgx189 { color: #d7d7ff; }
+.term-fgx190 { color: #d7ff00; }
+.term-fgx191 { color: #d7ff5f; }
+.term-fgx192 { color: #d7ff87; }
+.term-fgx193 { color: #d7ffaf; }
+.term-fgx194 { color: #d7ffd7; }
+.term-fgx195 { color: #d7ffff; }
+.term-fgx196 { color: #ff0000; }
+.term-fgx197 { color: #ff005f; }
+.term-fgx198 { color: #ff0087; }
+.term-fgx199 { color: #ff00af; }
+.term-fgx200 { color: #ff00d7; }
+.term-fgx201 { color: #ff00ff; }
+.term-fgx202 { color: #ff5f00; }
+.term-fgx203 { color: #ff5f5f; }
+.term-fgx204 { color: #ff5f87; }
+.term-fgx205 { color: #ff5faf; }
+.term-fgx206 { color: #ff5fd7; }
+.term-fgx207 { color: #ff5fff; }
+.term-fgx208 { color: #ff8700; }
+.term-fgx209 { color: #ff875f; }
+.term-fgx210 { color: #ff8787; }
+.term-fgx211 { color: #ff87af; }
+.term-fgx212 { color: #ff87d7; }
+.term-fgx213 { color: #ff87ff; }
+.term-fgx214 { color: #ffaf00; }
+.term-fgx215 { color: #ffaf5f; }
+.term-fgx216 { color: #ffaf87; }
+.term-fgx217 { color: #ffafaf; }
+.term-fgx218 { color: #ffafd7; }
+.term-fgx219 { color: #ffafff; }
+.term-fgx220 { color: #ffd700; }
+.term-fgx221 { color: #ffd75f; }
+.term-fgx222 { color: #ffd787; }
+.term-fgx223 { color: #ffd7af; }
+.term-fgx224 { color: #ffd7d7; }
+.term-fgx225 { color: #ffd7ff; }
+.term-fgx226 { color: #ffff00; }
+.term-fgx227 { color: #ffff5f; }
+.term-fgx228 { color: #ffff87; }
+.term-fgx229 { color: #ffffaf; }
+.term-fgx230 { color: #ffffd7; }
+.term-fgx231 { color: #ffffff; }
+.term-fgx232 { color: #080808; }
+.term-fgx233 { color: #121212; }
+.term-fgx234 { color: #1c1c1c; }
+.term-fgx235 { color: #262626; }
+.term-fgx236 { color: #303030; }
+.term-fgx237 { color: #3a3a3a; }
+.term-fgx238 { color: #444444; }
+.term-fgx239 { color: #4e4e4e; }
+.term-fgx240 { color: #585858; }
+.term-fgx241 { color: #626262; }
+.term-fgx242 { color: #6c6c6c; }
+.term-fgx243 { color: #767676; }
+.term-fgx244 { color: #808080; }
+.term-fgx245 { color: #8a8a8a; }
+.term-fgx246 { color: #949494; }
+.term-fgx247 { color: #9e9e9e; }
+.term-fgx248 { color: #a8a8a8; }
+.term-fgx249 { color: #b2b2b2; }
+.term-fgx250 { color: #bcbcbc; }
+.term-fgx251 { color: #c6c6c6; }
+.term-fgx252 { color: #d0d0d0; }
+.term-fgx253 { color: #dadada; }
+.term-fgx254 { color: #e4e4e4; }
+.term-fgx255 { color: #eeeeee; }
diff --git a/web_src/css/features/dropzone.css b/web_src/css/features/dropzone.css
new file mode 100644
index 0000000..cbc32df
--- /dev/null
+++ b/web_src/css/features/dropzone.css
@@ -0,0 +1,59 @@
+.ui .field .dropzone {
+ border: 2px dashed var(--color-secondary);
+ background: none;
+ box-shadow: none;
+ padding: 0;
+ border-radius: var(--border-radius-medium);
+ min-height: 0;
+}
+
+.ui .field .dropzone .dz-message {
+ margin: 10px 0;
+}
+
+.dropzone .dz-button {
+ color: var(--color-text-light) !important;
+}
+
+.dropzone:hover .dz-button {
+ color: var(--color-text) !important;
+}
+
+.dropzone .dz-error-message {
+ top: 145px !important;
+}
+
+.dropzone .dz-image {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ border-radius: 0 !important;
+}
+
+.dropzone .dz-image img {
+ max-width: 100% !important;
+ max-height: 100% !important;
+ object-fit: contain !important;
+}
+
+.dropzone .dz-preview.dz-image-preview,
+.dropzone-attachments .thumbnails img {
+ background: transparent !important;
+}
+
+.dropzone-attachments .thumbnails img {
+ height: 120px !important;
+ width: 120px !important;
+ object-fit: contain !important;
+ margin-bottom: 0 !important;
+}
+
+.dropzone .dz-preview:hover .dz-image img {
+ filter: opacity(0.5) !important;
+}
+
+.ui .field .dropzone .dz-preview .dz-progress {
+ /* by default the progress-bar is vertically centered (top: 50%), it's better to put it after the "details (size, filename)",
+ then the layout from top to bottom is: size, filename, progress */
+ top: 7em;
+}
diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css
new file mode 100644
index 0000000..4da871d
--- /dev/null
+++ b/web_src/css/features/gitgraph.css
@@ -0,0 +1,309 @@
+#git-graph-content {
+ overflow-x: auto;
+ width: 100%;
+ min-height: 250px;
+}
+
+#git-graph-heading {
+ display: flex;
+ justify-content: space-between;
+ padding-bottom: 20px;
+}
+
+#git-graph-heading-left {
+ display: flex;
+ gap: 1rem;
+}
+
+#git-graph-heading h2 {
+ margin: 0;
+}
+
+@media (min-width: 767.98px) {
+ #git-graph-heading {
+ align-items: center;
+ }
+}
+
+@media (max-width: 767.98px) {
+ #git-graph-heading,
+ #git-graph-heading-left {
+ flex-direction: column;
+ }
+
+ #git-graph-heading-left {
+ margin-bottom: 1rem;
+ }
+
+ h2,
+ #flow-select-refs-dropdown {
+ max-width: 100%;
+ }
+}
+
+#git-graph-container #flow-select-refs-dropdown {
+ min-width: 250px;
+}
+
+#git-graph-container #flow-select-refs-dropdown .ui.label {
+ max-width: 180px;
+ display: inline-flex !important;
+ align-items: center;
+}
+
+#git-graph-container #flow-select-refs-dropdown .default.text {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+#git-graph-container #flow-select-refs-dropdown input.search {
+ position: relative;
+ top: 1px;
+}
+
+#git-graph-container li {
+ list-style-type: none;
+ height: 24px;
+ line-height: 24px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+
+#git-graph-container li .node-relation {
+ font-family: var(--fonts-monospace);
+}
+
+#git-graph-container li .author {
+ color: var(--color-text-light);
+}
+
+#git-graph-container li .time {
+ color: var(--color-text-light-3);
+}
+
+#git-graph-container li a:not(.ui):hover {
+ text-decoration: underline;
+}
+
+#git-graph-container li a em {
+ color: var(--color-red);
+ border-bottom: 1px dotted var(--color-secondary);
+ text-decoration: none;
+ font-style: normal;
+}
+
+#git-graph-container #rel-container {
+ max-width: 30%;
+ overflow-x: auto;
+ float: left;
+}
+
+#git-graph-container #rev-list {
+ margin: 0;
+ padding: 0;
+}
+
+#git-graph-container #rev-list li.highlight.hover {
+ background-color: var(--color-secondary-alpha-30);
+}
+
+#git-graph-container #rev-list .commit-refs .button {
+ padding: 2px 4px;
+ margin-right: 0.25em;
+ display: inline-block;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#git-graph-container #rev-list .sha.label {
+ padding-top: 5px;
+ padding-bottom: 3px;
+}
+
+#git-graph-container #rev-list .sha.label .ui.detail.icon.button {
+ padding-top: 3px;
+ margin-top: -5px;
+ padding-bottom: 1px;
+}
+
+#git-graph-container #rev-list .author img.ui.avatar {
+ width: auto;
+ height: 18px;
+ max-width: none;
+}
+
+#git-graph-container #graph-raw-list {
+ margin: 0;
+}
+
+#git-graph-container.monochrome #rel-container .flow-group {
+ stroke: var(--color-secondary-dark-5);
+ fill: var(--color-secondary-dark-5);
+}
+
+#git-graph-container.monochrome #rel-container .flow-group.highlight {
+ stroke: var(--color-secondary-dark-12);
+ fill: var(--color-secondary-dark-12);
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-1 {
+ stroke: #499a37;
+ fill: #499a37;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-2 {
+ stroke: #ce4751;
+ fill: #ce4751;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-3 {
+ stroke: #8f9121;
+ fill: #8f9121;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-4 {
+ stroke: #ac32a6;
+ fill: #ac32a6;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-5 {
+ stroke: #7445e9;
+ fill: #7445e9;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-6 {
+ stroke: #c67d28;
+ fill: #c67d28;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-7 {
+ stroke: #4db392;
+ fill: #4db392;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-8 {
+ stroke: #aa4d30;
+ fill: #aa4d30;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-9 {
+ stroke: #2a6f84;
+ fill: #2a6f84;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-10 {
+ stroke: #c45327;
+ fill: #c45327;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-11 {
+ stroke: #3d965c;
+ fill: #3d965c;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-12 {
+ stroke: #792a93;
+ fill: #792a93;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-13 {
+ stroke: #439d73;
+ fill: #439d73;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-14 {
+ stroke: #103aad;
+ fill: #103aad;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-15 {
+ stroke: #982e85;
+ fill: #982e85;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.flow-color-16-0 {
+ stroke: #7db233;
+ fill: #7db233;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-1 {
+ stroke: #5ac144;
+ fill: #5ac144;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-2 {
+ stroke: #ed5a8b;
+ fill: #ed5a8b;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-3 {
+ stroke: #ced049;
+ fill: #ced048;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-4 {
+ stroke: #db61d7;
+ fill: #db62d6;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-5 {
+ stroke: #8455f9;
+ fill: #8455f9;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-6 {
+ stroke: #e6a151;
+ fill: #e6a151;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-7 {
+ stroke: #44daaa;
+ fill: #44daaa;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-8 {
+ stroke: #dd7a5c;
+ fill: #dd7a5c;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-9 {
+ stroke: #38859c;
+ fill: #38859c;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-10 {
+ stroke: #d95520;
+ fill: #d95520;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-11 {
+ stroke: #42ae68;
+ fill: #42ae68;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-12 {
+ stroke: #9126b5;
+ fill: #9126b5;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-13 {
+ stroke: #4ab080;
+ fill: #4ab080;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-14 {
+ stroke: #284fb8;
+ fill: #284fb8;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-15 {
+ stroke: #971c80;
+ fill: #971c80;
+}
+
+#git-graph-container:not(.monochrome) #rel-container .flow-group.highlight.flow-color-16-0 {
+ stroke: #87ca28;
+ fill: #87ca28;
+}
diff --git a/web_src/css/features/heatmap.css b/web_src/css/features/heatmap.css
new file mode 100644
index 0000000..c064590
--- /dev/null
+++ b/web_src/css/features/heatmap.css
@@ -0,0 +1,58 @@
+#user-heatmap {
+ width: 100%;
+ font-size: 9px;
+ position: relative;
+}
+
+/* before the Vue component is mounted, show a loading indicator with dummy size */
+/* the ratio is guesswork, see https://github.com/razorness/vue3-calendar-heatmap/issues/26 */
+#user-heatmap.is-loading {
+ aspect-ratio: 5.415; /* the size is about 790 x 145 */
+}
+.user.profile #user-heatmap.is-loading {
+ aspect-ratio: 5.645; /* the size is about 953 x 169 */
+}
+
+#user-heatmap text {
+ fill: currentcolor !important;
+}
+
+/* for the "Less" and "More" legend */
+#user-heatmap .vch__legend .vch__legend {
+ display: flex;
+ font-size: 11px;
+ align-items: center;
+ justify-content: right;
+}
+
+#user-heatmap .vch__legend .vch__legend div:first-child,
+#user-heatmap .vch__legend .vch__legend div:last-child {
+ display: inline-block;
+ padding: 0 5px;
+}
+
+#user-heatmap .vch__day__square:hover {
+ outline: 1.5px solid var(--color-text);
+}
+
+/* move the "? contributions in the last ? months" text from top to bottom */
+#user-heatmap .total-contributions {
+ font-size: 11px;
+ position: absolute;
+ bottom: 0;
+ left: 25px;
+}
+
+@media (max-width: 1200px) {
+ #user-heatmap .total-contributions {
+ left: 21px;
+ }
+}
+
+@media (max-width: 1000px) {
+ #user-heatmap .total-contributions {
+ font-size: 10px;
+ left: 17px;
+ bottom: -4px;
+ }
+}
diff --git a/web_src/css/features/imagediff.css b/web_src/css/features/imagediff.css
new file mode 100644
index 0000000..c8ead2b
--- /dev/null
+++ b/web_src/css/features/imagediff.css
@@ -0,0 +1,114 @@
+.image-diff-tabs {
+ min-height: 60px;
+
+}
+.image-diff-tabs.is-loading .tab {
+ display: none;
+}
+
+.image-diff-container {
+ text-align: center;
+ padding: 0.5em 0 1em;
+}
+
+.image-diff-container img {
+ border: 1px solid var(--color-primary-light-7);
+ --gradient: conic-gradient(var(--checkerboard-color-1) 90deg, var(--checkerboard-color-2) 90deg 180deg, var(--checkerboard-color-1) 180deg 270deg, var(--checkerboard-color-2) 270deg);
+ background: var(--gradient);
+ background-size: 20px 20px;
+}
+
+.image-diff-container .before-container {
+ border: 1px solid var(--color-red);
+ display: block;
+}
+
+.image-diff-container .after-container {
+ border: 1px solid var(--color-green);
+ display: block;
+}
+
+.image-diff-container .diff-side-by-side .side {
+ display: inline-block;
+ line-height: 0;
+ vertical-align: top;
+ margin: 0 1em;
+}
+
+.image-diff-container .diff-side-by-side .side .side-header {
+ font-weight: var(--font-weight-semibold);
+}
+
+.image-diff-container .diff-swipe {
+ margin: auto;
+ padding: 1em 0;
+}
+
+.image-diff-container .diff-swipe .swipe-frame {
+ position: absolute;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .before-container {
+ position: absolute;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .swipe-container {
+ position: absolute;
+ right: 0;
+ display: block;
+ border-left: 2px solid var(--color-secondary-dark-8);
+ height: 100%;
+ overflow: hidden;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .swipe-container .after-container {
+ position: absolute;
+ right: 0;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .swipe-bar {
+ position: absolute;
+ height: 100%;
+ top: 0;
+ left: 0;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .swipe-bar .handle {
+ background: var(--color-secondary-dark-8);
+ left: -5px;
+ height: 12px;
+ width: 12px;
+ position: absolute;
+ transform: rotate(45deg);
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .swipe-bar .top-handle {
+ top: -12px;
+}
+
+.image-diff-container .diff-swipe .swipe-frame .swipe-bar .bottom-handle {
+ bottom: -14px;
+}
+
+.image-diff-container .diff-overlay {
+ margin: 0 auto;
+}
+
+.image-diff-container .diff-overlay .overlay-frame {
+ margin: 1em auto 0;
+ position: relative;
+}
+
+.image-diff-container .diff-overlay .before-container,
+.image-diff-container .diff-overlay .after-container {
+ position: absolute;
+}
+
+.image-diff-container .diff-overlay input {
+ max-width: 300px;
+}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
new file mode 100644
index 0000000..1401916
--- /dev/null
+++ b/web_src/css/features/projects.css
@@ -0,0 +1,108 @@
+.board {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ margin: 0 0.5em;
+}
+
+.project-column {
+ background-color: var(--color-project-column-bg) !important;
+ border: 1px solid var(--color-secondary) !important;
+ border-radius: var(--border-radius);
+ margin: 0 0.5rem !important;
+ padding: 0.5rem !important;
+ width: 320px;
+ height: calc(100vh - 450px);
+ min-height: 60vh;
+ overflow-y: scroll;
+ flex: 0 0 auto;
+ overflow: visible;
+ display: flex;
+ flex-direction: column;
+ cursor: default;
+}
+
+.project-column .issue-card {
+ color: var(--color-text);
+}
+
+.project-column-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.project-column-title {
+ background: none !important;
+ line-height: 1.25 !important;
+ color: inherit !important;
+ cursor: inherit;
+}
+
+.project-column-title-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.project-column-header > .dropdown {
+ flex-shrink: 0;
+}
+
+.project-column > .cards {
+ flex: 1;
+ display: flex;
+ align-content: baseline;
+ margin: 0 !important;
+ padding: 0 !important;
+ flex-wrap: nowrap !important;
+ flex-direction: column;
+ overflow-x: auto;
+ gap: .25rem;
+}
+
+.project-column > .divider {
+ margin: 5px 0;
+ border-color: currentcolor;
+ opacity: .5;
+}
+
+.project-column:first-child {
+ margin-left: auto !important;
+}
+
+.project-column:last-child {
+ margin-right: auto !important;
+}
+
+.card-attachment-images {
+ display: inline-block;
+ white-space: nowrap;
+ overflow: scroll;
+ cursor: default;
+ text-align: center;
+}
+
+.card-attachment-images img {
+ display: inline-block;
+ max-height: 50px;
+ border-radius: var(--border-radius);
+ text-align: left;
+ margin-right: 2px;
+ aspect-ratio: 1;
+}
+
+.card-attachment-images img:only-child {
+ max-height: 90px;
+ margin: auto;
+}
+
+.card-ghost {
+ border-color: var(--color-secondary-dark-4) !important;
+ border-style: dashed !important;
+ background: none !important;
+}
+
+.card-ghost * {
+ opacity: 0;
+}
diff --git a/web_src/css/features/tribute.css b/web_src/css/features/tribute.css
new file mode 100644
index 0000000..bd84367
--- /dev/null
+++ b/web_src/css/features/tribute.css
@@ -0,0 +1,42 @@
+@import "tributejs/dist/tribute.css";
+
+.tribute-container {
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.25);
+ border-radius: var(--border-radius);
+}
+
+.tribute-container ul {
+ margin-top: 0 !important;
+ background: var(--color-body) !important;
+}
+
+.tribute-container li {
+ padding: 3px 0.5rem !important;
+}
+
+.tribute-container li span.fullname {
+ font-weight: var(--font-weight-normal);
+ font-size: 0.8rem;
+ margin-left: 3px;
+}
+
+.tribute-container li.highlight,
+.tribute-container li:hover {
+ background: var(--color-primary) !important;
+ color: var(--color-primary-contrast) !important;
+}
+
+.tribute-item {
+ display: flex;
+ align-items: center;
+}
+
+.tribute-item .emoji,
+.tribute-item img[src*="/avatar/"] {
+ margin-right: 0.5rem;
+}
+
+.tribute-container img {
+ width: 1.5rem !important;
+ height: 1.5rem !important;
+}
diff --git a/web_src/css/font_i18n.css b/web_src/css/font_i18n.css
new file mode 100644
index 0000000..0ac4c83
--- /dev/null
+++ b/web_src/css/font_i18n.css
@@ -0,0 +1,393 @@
+:root :lang(ja) {
+ --fonts-override: var(--fonts-default-override-ja);
+}
+
+:root :lang(zh-CN) {
+ --fonts-override: var(--fonts-default-override-zh-cn);
+}
+
+:root :lang(zh-TW) {
+ --fonts-override: var(--fonts-default-override-zh-tw);
+}
+
+:root :lang(zh-HK) {
+ --fonts-override: var(--fonts-default-override-zh-hk);
+}
+
+:root :lang(ko) {
+ --fonts-override: var(--fonts-default-override-ko);
+}
+
+[lang] {
+ font-family: var(--fonts-regular);
+}
+
+:root {
+ --fonts-default-override-ja: system-ui-ja, var(--fonts-proportional);
+ --fonts-default-override-zh-cn: system-ui-zh-cn, var(--fonts-proportional);
+ --fonts-default-override-zh-tw: system-ui-zh-tw, var(--fonts-proportional);
+ --fonts-default-override-zh-hk: system-ui-zh-hk, var(--fonts-proportional);
+ --fonts-default-override-ko: system-ui-ko, var(--fonts-proportional);
+}
+
+/* Special handling for Firefox on Windows/Linux */
+@supports (-moz-appearance: none) {
+ :root {
+ --fonts-default-override-ja: var(--fonts-proportional), system-ui-ja;
+ --fonts-default-override-zh-cn: var(--fonts-proportional), system-ui-zh-cn;
+ --fonts-default-override-zh-tw: var(--fonts-proportional), system-ui-zh-tw;
+ --fonts-default-override-zh-hk: var(--fonts-proportional), system-ui-zh-hk;
+ --fonts-default-override-ko: var(--fonts-proportional), system-ui-ko;
+ }
+}
+
+@font-face {
+ font-family: system-ui-ja;
+ src: local("HiraKakuProN-W3"), local("Hiragino Kaku Gothic ProN W3"),
+ local("HiraginoSans-W2"), local("Source Han Sans JP Light"),
+ local("SourceHanSansJP-Light"), local("Source Han Sans J Light"),
+ local("SourceHanSansJ-Light"), local("Noto Sans CJK JP Light"),
+ local("NotoSansCJKJP-Light"), local("Source Han Sans Light"),
+ local("SourceHanSans-Light"), local("Yu Gothic Regular"),
+ local("YuGothic Regular"), local("Droid Sans Japanese"), local("Meiryo"),
+ local("MS PGothic");
+ font-weight: 300;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-ja;
+ src: local("HiraKakuProN-W3"), local("Hiragino Kaku Gothic ProN W3"),
+ local("HiraginoSans-W4"), local("Source Han Sans JP Regular"),
+ local("SourceHanSansJP-Regular"), local("Source Han Sans J Regular"),
+ local("SourceHanSansJ-Regular"), local("Noto Sans CJK JP Regular"),
+ local("NotoSansCJKJP-Regular"), local("Source Han Sans Regular"),
+ local("SourceHanSans-Regular"), local("Yu Gothic Medium"),
+ local("YuGothic Medium"), local("Droid Sans Japanese"), local("Meiryo"),
+ local("MS PGothic");
+ font-weight: 400;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-ja;
+ src: local("HiraKakuProN-W3"), local("Hiragino Kaku Gothic ProN W3"),
+ local("HiraginoSans-W5"), local("Source Han Sans JP Medium"),
+ local("SourceHanSansJP-Medium"), local("Source Han Sans J Medium"),
+ local("SourceHanSansJ-Medium"), local("Noto Sans CJK JP Medium"),
+ local("NotoSansCJKJP-Medium"), local("Source Han Sans Medium"),
+ local("SourceHanSans-Medium"), local("Yu Gothic Medium"),
+ local("YuGothic Medium"), local("Droid Sans Japanese"), local("Meiryo"),
+ local("MS PGothic");
+ font-weight: 500;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-ja;
+ src: local("HiraKakuProN-W6"), local("Hiragino Kaku Gothic ProN W6"),
+ local("HiraginoSans-W6"), local("Source Han Sans JP Bold"),
+ local("SourceHanSansJP-Bold"), local("Source Han Sans J Bold"),
+ local("SourceHanSansJ-Bold"), local("Noto Sans CJK JP Bold"),
+ local("NotoSansCJKJP-Bold"), local("Source Han Sans Bold"),
+ local("SourceHanSans-Bold"), local("Yu Gothic Bold"), local("YuGothic Bold"),
+ local("Droid Sans Japanese"), local("Meiryo Bold"), local("MS PGothic");
+ font-weight: 600;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+/* Safari on macOS/iOS */
+@font-face {
+ font-family: system-ui-ja;
+ src: local("HelveticaNeue");
+ unicode-range: U+A0;
+}
+
+/* Other browsers on macOS/iOS */
+@supports not (-webkit-hyphens: none) {
+ @font-face {
+ font-family: system-ui-ja;
+ src: local("HelveticaNeue");
+ unicode-range: U+20;
+ }
+}
+
+@font-face {
+ font-family: system-ui-zh-cn;
+ src: local("PingFangSC-Light"), local("Source Han Sans CN Light"),
+ local("SourceHanSansCN-Light"), local("Source Han Sans SC Light"),
+ local("SourceHanSansSC-Light"), local("Noto Sans CJK SC Light"),
+ local("NotoSansCJKSC-Light"), local("HiraginoSansGB-W3"),
+ local("Hiragino Sans GB W3"), local("Microsoft YaHei Light"),
+ local("Heiti SC Light"), local("SimHei");
+ font-weight: 300;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-cn;
+ src: local("PingFangSC-Regular"), local("Source Han Sans CN Regular"),
+ local("SourceHanSansCN-Regular"), local("Source Han Sans SC Regular"),
+ local("SourceHanSansSC-Regular"), local("Noto Sans CJK SC Regular"),
+ local("NotoSansCJKSC-Regular"), local("HiraginoSansGB-W3"),
+ local("Hiragino Sans GB W3"), local("Microsoft YaHei"),
+ local("Heiti SC Light"), local("SimHei");
+ font-weight: 400;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-cn;
+ src: local("PingFangSC-Medium"), local("Source Han Sans CN Medium"),
+ local("SourceHanSansCN-Medium"), local("Source Han Sans SC Medium"),
+ local("SourceHanSansSC-Medium"), local("Noto Sans CJK SC Medium"),
+ local("NotoSansCJKSC-Medium"), local("HiraginoSansGB-W3"),
+ local("Hiragino Sans GB W3"), local("Microsoft YaHei"),
+ local("Heiti SC Light"), local("SimHei");
+ font-weight: 500;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-cn;
+ src: local("PingFangSC-Semibold"), local("Source Han Sans CN Bold"),
+ local("SourceHanSansCN-Bold"), local("Source Han Sans SC Bold"),
+ local("SourceHanSansSC-Bold"), local("Noto Sans CJK SC Bold"),
+ local("NotoSansCJKSC-Bold"), local("HiraginoSansGB-W6"),
+ local("Hiragino Sans GB W6"), local("Microsoft YaHei Bold"),
+ local("Heiti SC Medium"), local("SimHei");
+ font-weight: 600;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+/* Safari on macOS/iOS */
+@font-face {
+ font-family: system-ui-zh-cn;
+ src: local("HelveticaNeue");
+ unicode-range: U+A0;
+}
+
+/* Other browsers on macOS/iOS */
+@supports not (-webkit-hyphens: none) {
+ @font-face {
+ font-family: system-ui-zh-cn;
+ src: local("HelveticaNeue");
+ unicode-range: U+20;
+ }
+}
+
+@font-face {
+ font-family: system-ui-zh-tw;
+ src: local("PingFangTC-Light"), local("Source Han Sans TW Light"),
+ local("SourceHanSansTW-Light"), local("Source Han Sans TC Light"),
+ local("SourceHanSansTC-Light"), local("Noto Sans CJK TC Light"),
+ local("NotoSansCJKTC-Light"), local("HiraginoSansTC-W3"),
+ local("Hiragino Sans TC W3"), local("Microsoft JhengHei Light"),
+ local("Heiti TC Light"), local("PMingLiU");
+ font-weight: 300;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-tw;
+ src: local("PingFangTC-Regular"), local("Source Han Sans TW Regular"),
+ local("SourceHanSansTW-Regular"), local("Source Han Sans TC Regular"),
+ local("SourceHanSansTC-Regular"), local("Noto Sans CJK TC Regular"),
+ local("NotoSansCJKTC-Regular"), local("HiraginoSansTC-W3"),
+ local("Hiragino Sans TC W3"), local("Microsoft JhengHei"),
+ local("Heiti TC Light"), local("PMingLiU");
+ font-weight: 400;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-tw;
+ src: local("PingFangTC-Medium"), local("Source Han Sans TW Medium"),
+ local("SourceHanSansTW-Medium"), local("Source Han Sans TC Medium"),
+ local("SourceHanSansTC-Medium"), local("Noto Sans CJK TC Medium"),
+ local("NotoSansCJKTC-Medium"), local("HiraginoSansTC-W3"),
+ local("Hiragino Sans TC W3"), local("Microsoft JhengHei"),
+ local("Heiti TC Light"), local("PMingLiU");
+ font-weight: 500;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-tw;
+ src: local("PingFangTC-Semibold"), local("Source Han Sans TW Bold"),
+ local("SourceHanSansTW-Bold"), local("Source Han Sans TC Bold"),
+ local("SourceHanSansTC-Bold"), local("Noto Sans CJK TC Bold"),
+ local("NotoSansCJKTC-Bold"), local("HiraginoSansTC-W6"),
+ local("Hiragino Sans TC W6"), local("Microsoft JhengHei Bold"),
+ local("Heiti TC Medium"), local("PMingLiU");
+ font-weight: 600;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+/* Safari on macOS/iOS */
+@font-face {
+ font-family: system-ui-zh-tw;
+ src: local("HelveticaNeue");
+ unicode-range: U+A0;
+}
+
+/* Other browsers on macOS/iOS */
+@supports not (-webkit-hyphens: none) {
+ @font-face {
+ font-family: system-ui-zh-tw;
+ src: local("HelveticaNeue");
+ unicode-range: U+20;
+ }
+}
+
+@font-face {
+ font-family: system-ui-zh-hk;
+ src: local("PingFangHK-Light"), local("Source Han Sans HK Light"),
+ local("SourceHanSansHK-Light"), local("Source Han Sans HC Light"),
+ local("SourceHanSansHC-Light"), local("Noto Sans CJK HK Light"),
+ local("NotoSansCJKHK-Light"), local("Source Han Sans TC Light"),
+ local("SourceHanSansTC-Light"), local("Noto Sans CJK TC Light"),
+ local("NotoSansCJKTC-Light"), local("HiraginoSansTC-W3"),
+ local("Hiragino Sans TC W3"), local("Microsoft JhengHei Light"),
+ local("Heiti TC Light"), local("PMingLiU_HKSCS"), local("PMingLiU");
+ font-weight: 300;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-hk;
+ src: local("PingFangHK-Regular"), local("Source Han Sans HK Regular"),
+ local("SourceHanSansHK-Regular"), local("Source Han Sans HC Regular"),
+ local("SourceHanSansHC-Regular"), local("Noto Sans CJK HK Regular"),
+ local("NotoSansCJKHK-Regular"), local("Source Han Sans TC Regular"),
+ local("SourceHanSansTC-Regular"), local("Noto Sans CJK TC Regular"),
+ local("NotoSansCJKTC-Regular"), local("HiraginoSansTC-W3"),
+ local("Hiragino Sans TC W3"), local("Microsoft JhengHei"),
+ local("Heiti TC Light"), local("PMingLiU_HKSCS"), local("PMingLiU");
+ font-weight: 400;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-hk;
+ src: local("PingFangHK-Medium"), local("Source Han Sans HK Medium"),
+ local("SourceHanSansHK-Medium"), local("Source Han Sans HC Medium"),
+ local("SourceHanSansHC-Medium"), local("Noto Sans CJK HK Medium"),
+ local("NotoSansCJKHK-Medium"), local("Source Han Sans TC Medium"),
+ local("SourceHanSansTC-Medium"), local("Noto Sans CJK TC Medium"),
+ local("NotoSansCJKTC-Medium"), local("HiraginoSansTC-W3"),
+ local("Hiragino Sans TC W3"), local("Microsoft JhengHei"),
+ local("Heiti TC Light"), local("PMingLiU_HKSCS"), local("PMingLiU");
+ font-weight: 500;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-zh-hk;
+ src: local("PingFangHK-Semibold"), local("Source Han Sans HK Bold"),
+ local("SourceHanSansHK-Bold"), local("Source Han Sans HC Bold"),
+ local("SourceHanSansHC-Bold"), local("Noto Sans CJK HK Bold"),
+ local("NotoSansCJKHK-Bold"), local("Source Han Sans TC Bold"),
+ local("SourceHanSansTC-Bold"), local("Noto Sans CJK TC Bold"),
+ local("NotoSansCJKTC-Bold"), local("HiraginoSansTC-W6"),
+ local("Hiragino Sans TC W6"), local("Microsoft JhengHei Bold"),
+ local("Heiti TC Medium"), local("PMingLiU_HKSCS"), local("PMingLiU");
+ font-weight: 600;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+/* Safari on macOS/iOS */
+@font-face {
+ font-family: system-ui-zh-hk;
+ src: local("HelveticaNeue");
+ unicode-range: U+A0;
+}
+
+/* Other browsers on macOS/iOS */
+@supports not (-webkit-hyphens: none) {
+ @font-face {
+ font-family: system-ui-zh-hk;
+ src: local("HelveticaNeue");
+ unicode-range: U+20;
+ }
+}
+
+@font-face {
+ font-family: system-ui-ko;
+ src: local("AppleSDGothicNeo-Light"), local("Source Han Sans KR Light"),
+ local("SourceHanSansKR-Light"), local("Source Han Sans K Light"),
+ local("SourceHanSansK-Light"), local("Noto Sans CJK KR Light"),
+ local("NotoSansCJKKR-Light"), local("NanumBarunGothic Light"),
+ local("Malgun Gothic Semilight"), local("Nanum Gothic"), local("Dotum");
+ font-weight: 300;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-ko;
+ src: local("AppleSDGothicNeo-Regular"), local("Source Han Sans KR Regular"),
+ local("SourceHanSansKR-Regular"), local("Source Han Sans K Regular"),
+ local("SourceHanSansK-Regular"), local("Noto Sans CJK KR Regular"),
+ local("NotoSansCJKKR-Regular"), local("NanumBarunGothic"),
+ local("Malgun Gothic"), local("Nanum Gothic"), local("Dotum");
+ font-weight: 400;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-ko;
+ src: local("AppleSDGothicNeo-Medium"), local("Source Han Sans KR Medium"),
+ local("SourceHanSansKR-Medium"), local("Source Han Sans K Medium"),
+ local("SourceHanSansK-Medium"), local("Noto Sans CJK KR Medium"),
+ local("NotoSansCJKKR-Medium"), local("NanumBarunGothic"),
+ local("Malgun Gothic"), local("Nanum Gothic"), local("Dotum");
+ font-weight: 500;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+@font-face {
+ font-family: system-ui-ko;
+ src: local("AppleSDGothicNeo-SemiBold"), local("Source Han Sans KR Bold"),
+ local("SourceHanSansKR-Bold"), local("Source Han Sans K Bold"),
+ local("SourceHanSansK-Bold"), local("Noto Sans CJK KR Bold"),
+ local("NotoSansCJKKR-Bold"), local("NanumBarunGothic Bold"),
+ local("Malgun Gothic Bold"), local("Nanum Gothic Bold"), local("Dotum");
+ font-weight: 600;
+ unicode-range: U+11??, U+2E80-4DBF, U+4E00-9FFF, U+A960-A97F, U+AC00-D7FF,
+ U+F900-FAFF, U+FE00-FE6F, U+FF00-FFEF, U+1F2??, U+2????;
+}
+
+/* Safari on macOS/iOS */
+@font-face {
+ font-family: system-ui-ko;
+ src: local("HelveticaNeue");
+ unicode-range: U+A0;
+}
+
+/* Other browsers on macOS/iOS */
+@supports not (-webkit-hyphens: none) {
+ @font-face {
+ font-family: system-ui-ko;
+ src: local("HelveticaNeue");
+ unicode-range: U+20;
+ }
+}
diff --git a/web_src/css/form.css b/web_src/css/form.css
new file mode 100644
index 0000000..fb9364d
--- /dev/null
+++ b/web_src/css/form.css
@@ -0,0 +1,520 @@
+fieldset {
+ margin: 0.5em 0 1em;
+ padding: 0;
+}
+
+fieldset legend {
+ font-weight: var(--font-weight-medium);
+ margin-bottom: 0.75em;
+}
+
+fieldset label {
+ display: block;
+ margin-bottom: 0.6em;
+}
+
+fieldset label:has(input[type="text"]),
+fieldset label:has(input[type="number"]) {
+ font-weight: var(--font-weight-medium);
+}
+
+fieldset .help {
+ font-weight: var(--font-weight-normal);
+}
+
+.form fieldset .help { /* overrides other .form .help rules in this file, remove when obsolete */
+ display: block !important;
+ padding-bottom: 0;
+}
+
+fieldset input[type="checkbox"],
+fieldset input[type="radio"] {
+ margin-right: 0.75em;
+ vertical-align: initial !important; /* overrides a semantic.css rule, remove when obsolete */
+}
+
+@media (min-width: 768px) {
+ .optionmatrix input[type="radio"] {
+ margin: 0;
+ }
+
+ /* center columns except first */
+ .optionmatrix td + td, .optionmatrix th + th {
+ min-width: 10em;
+ text-align: center !important; /* overrides table.css "inherit" rule */
+ }
+}
+
+/* if an element with class "hide-unless-checked" follows a label
+ * that has no checked input, it will be hidden.*/
+label:not(:has(input:checked)) + .hide-unless-checked {
+ display: none;
+}
+
+.ui.input textarea,
+.ui.form textarea,
+.ui.form input:not([type]),
+.ui.form input[type="date"],
+.ui.form input[type="datetime-local"],
+.ui.form input[type="email"],
+.ui.form input[type="number"],
+.ui.form input[type="password"],
+.ui.form input[type="search"],
+.ui.form input[type="tel"],
+.ui.form input[type="time"],
+.ui.form input[type="text"],
+.ui.form input[type="file"],
+.ui.form input[type="url"] {
+ transition: none;
+}
+
+input,
+textarea,
+.ui.input > input,
+.ui.form input:not([type]),
+.ui.form select,
+.ui.form textarea,
+.ui.form input[type="date"],
+.ui.form input[type="datetime-local"],
+.ui.form input[type="email"],
+.ui.form input[type="file"],
+.ui.form input[type="number"],
+.ui.form input[type="password"],
+.ui.form input[type="search"],
+.ui.form input[type="tel"],
+.ui.form input[type="text"],
+.ui.form input[type="time"],
+.ui.form input[type="url"],
+.ui.selection.dropdown {
+ background: var(--color-input-background);
+ border-color: var(--color-input-border);
+ color: var(--color-input-text);
+}
+
+/* fix fomantic small dropdown having inconsistent padding with input */
+.ui.small.selection.dropdown {
+ padding: .67857143em 1.6em .67857143em 1em;
+}
+
+input:hover,
+textarea:hover,
+.ui.input input:hover,
+.ui.form input:not([type]):hover,
+.ui.form select:hover,
+.ui.form textarea:hover,
+.ui.form input[type="date"]:hover,
+.ui.form input[type="datetime-local"]:hover,
+.ui.form input[type="email"]:hover,
+.ui.form input[type="file"]:hover,
+.ui.form input[type="number"]:hover,
+.ui.form input[type="password"]:hover,
+.ui.form input[type="search"]:hover,
+.ui.form input[type="tel"]:hover,
+.ui.form input[type="text"]:hover,
+.ui.form input[type="time"]:hover,
+.ui.form input[type="url"]:hover,
+.ui.selection.dropdown:hover {
+ background: var(--color-input-background);
+ border-color: var(--color-input-border-hover);
+ color: var(--color-input-text);
+}
+
+input:focus,
+textarea:focus,
+.ui.input input:focus,
+.ui.form input:not([type]):focus,
+.ui.form select:focus,
+.ui.form textarea:focus,
+.ui.form input[type="date"]:focus,
+.ui.form input[type="datetime-local"]:focus,
+.ui.form input[type="email"]:focus,
+.ui.form input[type="file"]:focus,
+.ui.form input[type="number"]:focus,
+.ui.form input[type="password"]:focus,
+.ui.form input[type="search"]:focus,
+.ui.form input[type="tel"]:focus,
+.ui.form input[type="text"]:focus,
+.ui.form input[type="time"]:focus,
+.ui.form input[type="url"]:focus,
+.ui.selection.dropdown:focus {
+ background: var(--color-input-background);
+ border-color: var(--color-primary);
+ color: var(--color-input-text);
+}
+
+.ui.form .field > label,
+.ui.form .inline.fields > label,
+.ui.form .inline.fields .field > label,
+.ui.form .inline.fields .field > p,
+.ui.form .inline.field > label,
+.ui.form .inline.field > p {
+ color: var(--color-text);
+}
+
+.ui.form .required.fields > .field > label::after,
+.ui.form .required.field > label::after,
+.ui.form label.required::after {
+ color: var(--color-red);
+}
+
+.ui.input {
+ color: var(--color-input-text);
+}
+
+/* match <select> padding to <input> */
+.ui.form select {
+ padding: 0.67857143em 1em;
+}
+
+.form .help {
+ color: var(--color-secondary-dark-8);
+ padding-bottom: 0.6em;
+ display: inline-block;
+}
+
+#create-page-form form {
+ margin: auto;
+}
+
+#create-page-form form .ui.message {
+ text-align: center;
+}
+
+@media (min-width: 768px) {
+ #create-page-form form {
+ width: 800px !important;
+ }
+ #create-page-form form .header {
+ padding-left: 280px !important;
+ }
+ #create-page-form form .inline.field > label {
+ text-align: right;
+ width: 250px !important;
+ word-wrap: break-word;
+ }
+ #create-page-form form .help {
+ margin-left: 265px !important;
+ }
+ #create-page-form form .optional .title {
+ margin-left: 250px !important;
+ }
+ #create-page-form form .inline.field > input,
+ #create-page-form form .inline.field > textarea {
+ width: 50%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ #create-page-form form .optional .title {
+ margin-left: 15px;
+ }
+ #create-page-form form .inline.field > label {
+ display: block;
+ }
+}
+
+.m-captcha-style {
+ width: 100%;
+ height: 5em;
+ vertical-align: middle;
+ display: inline-block;
+}
+
+@media (min-width: 768px) {
+ .g-recaptcha-style,
+ .h-captcha-style {
+ margin: 0 auto !important;
+ width: 304px;
+ padding-left: 30px;
+ }
+ .g-recaptcha-style iframe,
+ .h-captcha-style iframe {
+ border-radius: var(--border-radius) !important;
+ width: 302px !important;
+ height: 76px !important;
+ }
+ .m-captcha-style {
+ width: 50%;
+ }
+}
+
+@media (max-height: 575px) {
+ #rc-imageselect,
+ .g-recaptcha-style,
+ .h-captcha-style {
+ transform: scale(0.77);
+ transform-origin: 0 0;
+ }
+}
+
+.user.forgot.password form,
+.user.reset.password form,
+.user.signup form {
+ margin: auto;
+ width: 700px !important;
+}
+
+.user.activate form .ui.message,
+.user.forgot.password form .ui.message,
+.user.reset.password form .ui.message,
+.user.link-account form .ui.message,
+.user.signin form .ui.message,
+.user.signup form .ui.message {
+ text-align: center;
+}
+
+@media (min-width: 768px) {
+ .user.activate form,
+ .user.forgot.password form,
+ .user.reset.password form,
+ .user.link-account form,
+ .user.signin form,
+ .user.signup form {
+ width: 800px !important;
+ }
+ .user.activate form .header,
+ .user.forgot.password form .header,
+ .user.reset.password form .header,
+ .user.link-account form .header,
+ .user.signin form .header,
+ .user.signup form .header {
+ padding-left: 280px !important;
+ }
+ .user.activate form .inline.field > label {
+ text-align: right;
+ width: 250px !important;
+ word-wrap: break-word;
+ }
+ .user.activate form .help,
+ .user.forgot.password form .help,
+ .user.reset.password form .help,
+ .user.link-account form .help,
+ .user.signin form .help,
+ .user.signup form .help {
+ margin-left: 265px !important;
+ }
+ .user.activate form .optional .title,
+ .user.forgot.password form .optional .title,
+ .user.reset.password form .optional .title,
+ .user.link-account form .optional .title,
+ .user.signin form .optional .title,
+ .user.signup form .optional .title {
+ margin-left: 250px !important;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .user.activate form .optional .title,
+ .user.forgot.password form .optional .title,
+ .user.reset.password form .optional .title,
+ .user.link-account form .optional .title,
+ .user.signin form .optional .title,
+ .user.signup form .optional .title {
+ margin-left: 15px;
+ }
+ .user.activate form .inline.field > label,
+ .user.forgot.password form .inline.field > label,
+ .user.reset.password form .inline.field > label,
+ .user.link-account form .inline.field > label,
+ .user.signin form .inline.field > label,
+ .user.signup form .inline.field > label {
+ display: block;
+ }
+}
+
+.user.activate form .header,
+.user.forgot.password form .header,
+.user.reset.password form .header,
+.user.link-account form .header,
+.user.signin form .header,
+.user.signup form .header {
+ padding-left: 0 !important;
+ text-align: center;
+}
+
+.user.activate form .inline.field > label,
+.user.forgot.password form .inline.field > label,
+.user.reset.password form .inline.field > label,
+.user.link-account form .inline.field > label,
+.user.signin form .inline.field > label,
+.user.signup form .inline.field > label {
+ width: 200px;
+}
+
+@media (max-width: 767.98px) {
+ .user.activate form .inline.field > label,
+ .user.forgot.password form .inline.field > label,
+ .user.reset.password form .inline.field > label,
+ .user.link-account form .inline.field > label,
+ .user.signin form .inline.field > label,
+ .user.signup form .inline.field > label {
+ width: 100% !important;
+ }
+}
+
+.user.activate form input[type="number"],
+.user.forgot.password form input[type="number"],
+.user.reset.password form input[type="number"],
+.user.link-account form input[type="number"],
+.user.signin form input[type="number"],
+.user.signup form input[type="number"] {
+ -moz-appearance: textfield;
+}
+
+.user.activate form input::-webkit-outer-spin-button,
+.user.forgot.password form input::-webkit-outer-spin-button,
+.user.reset.password form input::-webkit-outer-spin-button,
+.user.link-account form input::-webkit-outer-spin-button,
+.user.signin form input::-webkit-outer-spin-button,
+.user.signup form input::-webkit-outer-spin-button,
+.user.activate form input::-webkit-inner-spin-button,
+.user.forgot.password form input::-webkit-inner-spin-button,
+.user.reset.password form input::-webkit-inner-spin-button,
+.user.link-account form input::-webkit-inner-spin-button,
+.user.signin form input::-webkit-inner-spin-button,
+.user.signup form input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+.repository.new.repo form,
+.repository.new.migrate form,
+.repository.new.fork form {
+ margin: auto;
+}
+
+.repository.new.repo form .ui.message,
+.repository.new.migrate form .ui.message,
+.repository.new.fork form .ui.message {
+ text-align: center;
+}
+
+@media (min-width: 768px) {
+ .repository.new.repo form,
+ .repository.new.migrate form,
+ .repository.new.fork form {
+ width: 800px !important;
+ }
+ .repository.new.repo form .header,
+ .repository.new.migrate form .header,
+ .repository.new.fork form .header {
+ padding-left: 280px !important;
+ }
+ .repository.new.repo form .inline.field > label,
+ .repository.new.migrate form .inline.field > label,
+ .repository.new.fork form .inline.field > label {
+ text-align: right;
+ width: 250px !important;
+ word-wrap: break-word;
+ }
+ .repository.new.repo form .help,
+ .repository.new.migrate form .help,
+ .repository.new.fork form .help {
+ margin-left: 265px !important;
+ }
+ .repository.new.repo form .optional .title,
+ .repository.new.migrate form .optional .title,
+ .repository.new.fork form .optional .title {
+ margin-left: 250px !important;
+ }
+ .repository.new.repo form .inline.field > input,
+ .repository.new.migrate form .inline.field > input,
+ .repository.new.fork form .inline.field > input,
+ .repository.new.repo form .inline.field > textarea,
+ .repository.new.migrate form .inline.field > textarea,
+ .repository.new.fork form .inline.field > textarea {
+ width: 50%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .repository.new.repo form .optional .title,
+ .repository.new.migrate form .optional .title,
+ .repository.new.fork form .optional .title {
+ margin-left: 15px;
+ }
+ .repository.new.repo form .inline.field > label,
+ .repository.new.migrate form .inline.field > label,
+ .repository.new.fork form .inline.field > label {
+ display: block;
+ }
+}
+
+.repository.new.repo form .dropdown .text,
+.repository.new.migrate form .dropdown .text,
+.repository.new.fork form .dropdown .text {
+ margin-right: 0 !important;
+}
+
+.repository.new.repo form .header,
+.repository.new.migrate form .header,
+.repository.new.fork form .header {
+ padding-left: 0 !important;
+ text-align: center;
+}
+
+.repository.new.repo form .selection.dropdown,
+.repository.new.migrate form .selection.dropdown,
+.repository.new.fork form .selection.dropdown,
+.repository.new.fork form .field a {
+ vertical-align: middle;
+ width: 50% !important;
+}
+
+@media (max-width: 767.98px) {
+ .repository.new.repo form label,
+ .repository.new.migrate form label,
+ .repository.new.fork form label,
+ .repository.new.repo form .inline.field > input,
+ .repository.new.migrate form .inline.field > input,
+ .repository.new.fork form .inline.field > input,
+ .repository.new.fork form .field a,
+ .repository.new.repo form .selection.dropdown,
+ .repository.new.migrate form .selection.dropdown,
+ .repository.new.fork form .selection.dropdown {
+ width: 100% !important;
+ }
+ .repository.new.repo form .field button,
+ .repository.new.migrate form .field button,
+ .repository.new.fork form .field button,
+ .repository.new.repo form .field a,
+ .repository.new.migrate form .field a {
+ margin-bottom: 1em;
+ width: 100%;
+ }
+}
+
+@media (min-width: 768px) {
+ .repository.new.repo .ui.form #auto-init {
+ margin-left: 265px !important;
+ }
+}
+
+.repository.new.repo .ui.form .selection.dropdown:not(.owner) {
+ width: 50% !important;
+}
+
+@media (max-width: 767.98px) {
+ .repository.new.repo .ui.form .selection.dropdown:not(.owner) {
+ width: 100% !important;
+ }
+}
+
+.ui.form .field > .selection.dropdown {
+ min-width: 14em; /* matches the default min width */
+}
+
+.githook textarea {
+ font-family: var(--fonts-monospace);
+}
+
+@media (max-width: 767.98px) {
+ .new.org .ui.form .field button,
+ .new.org .ui.form .field a {
+ margin-bottom: 1em;
+ width: 100%;
+ }
+ .new.org .ui.form .field input:not([type="checkbox"], [type="radio"]) {
+ width: 100% !important;
+ }
+}
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
new file mode 100644
index 0000000..42d06e2
--- /dev/null
+++ b/web_src/css/helpers.css
@@ -0,0 +1,60 @@
+/*
+Gitea's tailwind-style CSS helper classes have `gt-` prefix.
+Gitea's private styles use `g-` prefix.
+*/
+
+.gt-ellipsis {
+ overflow: hidden !important;
+ white-space: nowrap !important;
+ text-overflow: ellipsis !important;
+}
+
+.g-table-auto-ellipsis td.auto-ellipsis {
+ position: relative;
+}
+
+.g-table-auto-ellipsis td.auto-ellipsis span {
+ position: absolute;
+ inset: 0;
+ padding: inherit;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.interact-fg { color: inherit !important; }
+.interact-fg:hover { color: var(--color-primary) !important; }
+.interact-fg:active { color: var(--color-primary-active) !important; }
+
+.interact-bg { background: transparent !important; }
+.interact-bg:hover { background: var(--color-hover) !important; }
+.interact-bg:active { background: var(--color-active) !important; }
+
+@media (max-width: 767.98px) {
+ /* double selector so it wins over .tw-flex (old .gt-df) etc */
+ .not-mobile.not-mobile {
+ display: none !important;
+ }
+}
+@media (min-width: 767.98px) {
+ .only-mobile.only-mobile {
+ display: none !important;
+ }
+}
+
+.tab-size-1 { tab-size: 1 !important; }
+.tab-size-2 { tab-size: 2 !important; }
+.tab-size-3 { tab-size: 3 !important; }
+.tab-size-4 { tab-size: 4 !important; }
+.tab-size-5 { tab-size: 5 !important; }
+.tab-size-6 { tab-size: 6 !important; }
+.tab-size-7 { tab-size: 7 !important; }
+.tab-size-8 { tab-size: 8 !important; }
+.tab-size-9 { tab-size: 9 !important; }
+.tab-size-10 { tab-size: 10 !important; }
+.tab-size-11 { tab-size: 11 !important; }
+.tab-size-12 { tab-size: 12 !important; }
+.tab-size-13 { tab-size: 13 !important; }
+.tab-size-14 { tab-size: 14 !important; }
+.tab-size-15 { tab-size: 15 !important; }
+.tab-size-16 { tab-size: 16 !important; }
diff --git a/web_src/css/home.css b/web_src/css/home.css
new file mode 100644
index 0000000..22f1a2d
--- /dev/null
+++ b/web_src/css/home.css
@@ -0,0 +1,87 @@
+.home .logo {
+ max-width: 220px;
+}
+
+@media (max-width: 767.98px) {
+ .home .hero h1 {
+ font-size: 3.5em;
+ }
+ .home .hero h2 {
+ font-size: 2em;
+ }
+}
+
+@media (min-width: 768px) {
+ .home .hero h1 {
+ font-size: 5.5em;
+ }
+ .home .hero h2 {
+ font-size: 3em;
+ }
+}
+
+.home .hero .svg {
+ color: var(--color-primary);
+ height: 40px;
+ width: 50px;
+ vertical-align: bottom;
+}
+
+.home .hero.header {
+ font-size: 20px;
+}
+
+.home p.large {
+ font-size: 16px;
+}
+
+.home .stackable {
+ padding-top: 30px;
+}
+
+.home a {
+ color: var(--color-primary);
+}
+
+.page-footer {
+ display: flex;
+ justify-content: space-between;
+ background-color: var(--color-footer);
+ border-top: 1px solid var(--color-secondary);
+ padding: 8px 20px;
+}
+
+.page-footer .left-links {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25em;
+}
+
+.page-footer .right-links {
+ min-width: 180px; /* make sure the menu dropdown doesn't overflow horizontally when language name is short */
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+}
+
+.page-footer .right-links > a {
+ border-left: 1px solid var(--color-secondary-dark-1);
+ padding-left: 8px;
+ margin-left: 5px;
+}
+
+.page-footer .ui.dropdown.language .menu {
+ max-height: min(500px, calc(100vh - 60px));
+ overflow-y: auto;
+ margin-bottom: 10px;
+}
+
+@media (max-width: 880px) {
+ .page-footer {
+ flex-direction: column;
+ gap: 0.5em;
+ }
+}
diff --git a/web_src/css/index.css b/web_src/css/index.css
new file mode 100644
index 0000000..49ceb2c
--- /dev/null
+++ b/web_src/css/index.css
@@ -0,0 +1,78 @@
+@import "./modules/normalize.css";
+@import "./modules/animations.css";
+
+/* fomantic replacements */
+@import "./modules/button.css";
+@import "./modules/container.css";
+@import "./modules/divider.css";
+@import "./modules/header.css";
+@import "./modules/input.css";
+@import "./modules/label.css";
+@import "./modules/list.css";
+@import "./modules/segment.css";
+@import "./modules/grid.css";
+@import "./modules/message.css";
+@import "./modules/table.css";
+@import "./modules/card.css";
+@import "./modules/checkbox.css";
+@import "./modules/modal.css";
+
+@import "./modules/select.css";
+@import "./modules/tippy.css";
+@import "./modules/breadcrumb.css";
+@import "./modules/comment.css";
+@import "./modules/navbar.css";
+@import "./modules/toast.css";
+@import "./modules/svg.css";
+@import "./modules/flexcontainer.css";
+
+@import "./shared/flex-list.css";
+@import "./shared/milestone.css";
+@import "./shared/repoorg.css";
+@import "./shared/settings.css";
+
+@import "./features/dropzone.css";
+@import "./features/gitgraph.css";
+@import "./features/heatmap.css";
+@import "./features/imagediff.css";
+@import "./features/codeeditor.css";
+@import "./features/projects.css";
+@import "./features/tribute.css";
+@import "./features/console.css";
+
+@import "./markup/content.css";
+@import "./markup/codecopy.css";
+@import "./markup/asciicast.css";
+@import "./markup/filepreview.css";
+
+@import "./chroma/base.css";
+@import "./codemirror/base.css";
+@import "./font_i18n.css";
+@import "./base.css";
+@import "./home.css";
+@import "./install.css";
+@import "./form.css";
+
+@import "./repo.css";
+@import "./repo/release-tag.css";
+@import "./repo/issue-card.css";
+@import "./repo/issue-label.css";
+@import "./repo/issue-list.css";
+@import "./repo/list-header.css";
+@import "./repo/linebutton.css";
+@import "./repo/wiki.css";
+@import "./repo/header.css";
+
+@import "./editor/fileeditor.css";
+@import "./editor/combomarkdowneditor.css";
+
+@import "./org.css";
+@import "./user.css";
+@import "./dashboard.css";
+@import "./admin.css";
+@import "./explore.css";
+@import "./review.css";
+@import "./actions.css";
+
+@tailwind utilities;
+@import "./helpers.css";
diff --git a/web_src/css/install.css b/web_src/css/install.css
new file mode 100644
index 0000000..df2a7cb
--- /dev/null
+++ b/web_src/css/install.css
@@ -0,0 +1,62 @@
+.page-content.install .install-config-container {
+ max-width: 900px;
+ margin: auto;
+}
+
+.page-content.install form.ui.form .inline.field > label {
+ text-align: right;
+ width: 30%;
+ padding-right: 10px;
+ margin-right: 0;
+}
+
+.page-content.install .ui.form .field > .help,
+.page-content.install .ui.form .field > .ui.checkbox:first-child,
+.page-content.install .ui.form .field > .right-content {
+ margin-left: calc(30% + 5px);
+ width: auto;
+}
+
+.page-content.install form.ui.form input:not([type="checkbox"],[type="radio"]),
+.page-content.install form.ui.form .ui.selection.dropdown {
+ width: 60%;
+}
+
+.page-content.install form.ui.form details.optional.field[open] {
+ padding-bottom: 10px;
+}
+.page-content.install form.ui.form details.optional.field[open] summary {
+ margin-bottom: 10px;
+}
+
+.page-content.install form.ui.form details.optional.field * {
+ box-sizing: border-box;
+}
+
+.page-content.install form.ui.form .field {
+ text-align: left;
+}
+
+.page-content.install .ui .reinstall-message {
+ width: 70%;
+ margin: 20px auto;
+ color: var(--color-red);
+ text-align: left;
+ font-weight: var(--font-weight-semibold);
+}
+
+.page-content.install .ui .reinstall-confirm {
+ width: 70%;
+ text-align: left;
+ margin: 10px auto;
+}
+
+.page-content.install details.collapsible {
+ border: 1px solid var(--color-light-border);
+ border-radius: 0.28571429rem;
+}
+.page-content.install .collapsible summary {
+ background: transparent;
+ margin: auto;
+ text-align: center;
+}
diff --git a/web_src/css/markup/asciicast.css b/web_src/css/markup/asciicast.css
new file mode 100644
index 0000000..89696bc
--- /dev/null
+++ b/web_src/css/markup/asciicast.css
@@ -0,0 +1,8 @@
+.asciinema-player-container {
+ width: 100%;
+ height: auto;
+}
+
+.ap-terminal {
+ overflow: hidden !important;
+}
diff --git a/web_src/css/markup/codecopy.css b/web_src/css/markup/codecopy.css
new file mode 100644
index 0000000..e3017ae
--- /dev/null
+++ b/web_src/css/markup/codecopy.css
@@ -0,0 +1,35 @@
+.markup .code-block,
+.markup .mermaid-block {
+ position: relative;
+}
+
+.markup .code-copy {
+ position: absolute;
+ top: 8px;
+ right: 6px;
+ padding: 9px;
+ visibility: hidden;
+ animation: fadeout 0.2s both;
+}
+
+/* adjustments for comment content having only 14px font size */
+.repository.view.issue .comment-list .comment .markup .code-copy {
+ right: 5px;
+ padding: 8px;
+}
+
+/* can not use regular transparent button colors for hover and active states because
+ we need opaque colors here as code can appear behind the button */
+.markup .code-copy:hover {
+ background: var(--color-secondary) !important;
+}
+
+.markup .code-copy:active {
+ background: var(--color-secondary-dark-1) !important;
+}
+
+.markup .code-block:hover .code-copy,
+.markup .mermaid-block:hover .code-copy {
+ visibility: visible;
+ animation: fadein 0.2s both;
+}
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
new file mode 100644
index 0000000..947480a
--- /dev/null
+++ b/web_src/css/markup/content.css
@@ -0,0 +1,585 @@
+.markup {
+ overflow: hidden;
+ font-size: 16px;
+ line-height: 1.5 !important;
+ word-wrap: break-word;
+}
+
+.markup > *:first-child {
+ margin-top: 0 !important;
+}
+
+.markup > *:last-child {
+ margin-bottom: 0 !important;
+}
+
+.markup a:not([href]) {
+ color: inherit;
+ text-decoration: none;
+}
+
+.markup .absent {
+ color: var(--color-red);
+}
+
+.markup .anchor {
+ float: left;
+ padding-right: 4px;
+ margin-left: -20px;
+ color: inherit;
+}
+
+.markup .anchor .svg {
+ vertical-align: middle;
+}
+
+.markup .anchor:focus {
+ outline: none;
+}
+
+.markup h1 .anchor {
+ margin-top: -2px; /* re-align to center */
+}
+
+.markup h1 .anchor .svg,
+.markup h2 .anchor .svg,
+.markup h3 .anchor .svg,
+.markup h4 .anchor .svg,
+.markup h5 .anchor .svg,
+.markup h6 .anchor .svg {
+ visibility: hidden;
+}
+
+.markup h1:hover .anchor .svg,
+.markup h2:hover .anchor .svg,
+.markup h3:hover .anchor .svg,
+.markup h4:hover .anchor .svg,
+.markup h5:hover .anchor .svg,
+.markup h6:hover .anchor .svg {
+ visibility: visible;
+}
+
+.markup h2 .anchor .svg,
+.markup h3 .anchor .svg,
+.markup h4 .anchor .svg {
+ position: relative;
+ top: -2px;
+}
+
+.markup h1,
+.markup h2,
+.markup h3,
+.markup h4,
+.markup h5,
+.markup h6 {
+ margin-top: 24px !important;
+ margin-bottom: 16px;
+ font-weight: var(--font-weight-semibold);
+ line-height: 1.25;
+}
+
+.markup h1 tt,
+.markup h1 code,
+.markup h2 tt,
+.markup h2 code,
+.markup h3 tt,
+.markup h3 code,
+.markup h4 tt,
+.markup h4 code,
+.markup h5 tt,
+.markup h5 code,
+.markup h6 tt,
+.markup h6 code {
+ font-size: inherit;
+}
+
+.markup h1 {
+ padding-bottom: 0.3em;
+ font-size: 2em;
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.markup h2 {
+ padding-bottom: 0.3em;
+ font-size: 1.5em;
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.markup h3 {
+ font-size: 1.25em;
+}
+
+.markup h4 {
+ font-size: 1em;
+}
+
+.markup h5 {
+ font-size: 0.875em;
+}
+
+.markup h6 {
+ font-size: 0.85em;
+ color: var(--color-text-light-2);
+}
+
+.markup p,
+.markup blockquote,
+.markup details,
+.markup ul,
+.markup ol,
+.markup dl,
+.markup table,
+.markup pre {
+ margin-top: 0;
+ margin-bottom: 16px;
+}
+
+.markup hr {
+ height: 4px;
+ padding: 0;
+ margin: 16px 0;
+ background-color: var(--color-secondary);
+ border: 0;
+}
+
+.markup ul,
+.markup ol {
+ padding-left: 2em;
+}
+
+.markup ul.no-list,
+.markup ol.no-list {
+ padding: 0;
+ list-style-type: none;
+}
+
+.markup .task-list-item {
+ list-style-type: none;
+}
+
+.markup .task-list-item p + ul {
+ margin-top: 16px;
+}
+
+.markup .task-list-item input[type="checkbox"] {
+ margin: 0 .3em .25em -1.4em;
+ vertical-align: middle;
+ padding: 0;
+}
+
+.markup .task-list-item input[type="checkbox"] + p {
+ margin-left: -0.2em;
+ display: inline;
+}
+
+.markup .task-list-item > p {
+ margin-inline: 16px;
+}
+
+.markup .task-list-item + .task-list-item {
+ margin-top: 4px;
+}
+
+.markup input[type="checkbox"] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ position: relative;
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ background: var(--color-input-background);
+ height: 14px;
+ width: 14px;
+ opacity: 1 !important; /* override fomantic on edit preview */
+ pointer-events: auto !important; /* override fomantic on edit preview */
+ vertical-align: middle !important; /* override fomantic on edit preview */
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+}
+
+.markup input[type="checkbox"]:not([disabled]):hover,
+.markup input[type="checkbox"]:not([disabled]):active {
+ border-color: var(--color-primary);
+}
+
+.markup input[type="checkbox"]::after {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: var(--color-text);
+ mask-size: cover;
+ -webkit-mask-size: cover;
+}
+
+.markup input[type="checkbox"]:checked::after {
+ content: "";
+ mask-image: var(--checkbox-mask-checked);
+ -webkit-mask-image: var(--checkbox-mask-checked);
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+}
+
+.markup input[type="checkbox"]:indeterminate::after {
+ content: "";
+ mask-image: var(--checkbox-mask-indeterminate);
+ -webkit-mask-image: var(--checkbox-mask-indeterminate);
+}
+
+.markup ul ul,
+.markup ul ol,
+.markup ol ol,
+.markup ol ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.markup ol ol,
+.markup ul ol {
+ list-style-type: lower-roman;
+}
+
+.markup li > p {
+ margin-top: 16px;
+}
+
+.markup li + li {
+ margin-top: 0.25em;
+}
+
+.markup dl {
+ padding: 0;
+}
+
+.markup dl dt {
+ padding: 0;
+ margin-top: 16px;
+ font-size: 1em;
+ font-style: italic;
+ font-weight: var(--font-weight-semibold);
+}
+
+.markup dl dd {
+ padding: 0 16px;
+ margin-bottom: 16px;
+}
+
+.markup blockquote {
+ margin-left: 0;
+ padding: 0 15px;
+ color: var(--color-text-light-2);
+ border-left: 0.25em solid var(--color-secondary);
+}
+
+.markup blockquote > :first-child {
+ margin-top: 0;
+}
+
+.markup blockquote > :last-child {
+ margin-bottom: 0;
+}
+
+.markup table {
+ display: block;
+ width: 100%;
+ width: max-content;
+ max-width: 100%;
+ overflow: auto;
+}
+
+.markup table th {
+ font-weight: var(--font-weight-semibold);
+}
+
+.markup table th,
+.markup table td {
+ padding: 6px 13px !important;
+ border: 1px solid var(--color-secondary) !important;
+}
+
+.markup table tr {
+ border-top: 1px solid var(--color-secondary);
+}
+
+.markup table tr:nth-child(2n) {
+ background-color: var(--color-markup-table-row);
+}
+
+.markup img,
+.markup video {
+ max-width: 100%;
+ box-sizing: initial;
+}
+
+/* this background ensures images can break <hr>. We can only do this on
+ cases where the background is known and not transparent. */
+.markup.file-view img,
+.markup.file-view video,
+.comment-body .markup img, /* regular comment */
+.comment-body .markup video,
+.comment-content .markup img, /* code comment */
+.comment-content .markup video,
+.wiki .markup img,
+.wiki .markup video {
+ background: var(--color-box-body);
+}
+
+.markup img[align="right"],
+.markup video[align="right"] {
+ padding-left: 20px;
+}
+
+.markup img[align="left"],
+.markup video[align="left"] {
+ padding-right: 28px;
+}
+
+.markup .emoji {
+ max-width: none;
+ vertical-align: text-top;
+}
+
+.markup span.frame {
+ display: block;
+ overflow: hidden;
+}
+
+.markup span.frame > span {
+ display: block;
+ float: left;
+ width: auto;
+ padding: 7px;
+ margin: 13px 0 0;
+ overflow: hidden;
+ border: 1px solid var(--color-secondary);
+}
+
+.markup span.frame span img,
+.markup span.frame span video {
+ display: block;
+ float: left;
+}
+
+.markup span.frame span span {
+ display: block;
+ padding: 5px 0 0;
+ clear: both;
+ color: var(--color-text);
+}
+
+.markup span.align-center {
+ display: block;
+ overflow: hidden;
+ clear: both;
+}
+
+.markup span.align-center > span {
+ display: block;
+ margin: 13px auto 0;
+ overflow: hidden;
+ text-align: center;
+}
+
+.markup span.align-center span img
+.markup span.align-center span video {
+ margin: 0 auto;
+ text-align: center;
+}
+
+.markup span.align-right {
+ display: block;
+ overflow: hidden;
+ clear: both;
+}
+
+.markup span.align-right > span {
+ display: block;
+ margin: 13px 0 0;
+ overflow: hidden;
+ text-align: right;
+}
+
+.markup span.align-right span img,
+.markup span.align-right span video {
+ margin: 0;
+ text-align: right;
+}
+
+.markup span.float-left {
+ display: block;
+ float: left;
+ margin-right: 13px;
+ overflow: hidden;
+}
+
+.markup span.float-left span {
+ margin: 13px 0 0;
+}
+
+.markup span.float-right {
+ display: block;
+ float: right;
+ margin-left: 13px;
+ overflow: hidden;
+}
+
+.markup span.float-right > span {
+ display: block;
+ margin: 13px auto 0;
+ overflow: hidden;
+ text-align: right;
+}
+
+.markup code,
+.markup tt {
+ padding: 0.2em 0.4em;
+ margin: 0;
+ font-size: 85%;
+ white-space: break-spaces;
+ background-color: var(--color-markup-code-inline);
+ border-radius: var(--border-radius);
+}
+
+.markup code br,
+.markup tt br {
+ display: none;
+}
+
+.markup del code {
+ text-decoration: inherit;
+}
+
+.markup pre > code,
+.markup .file-preview code {
+ padding: 0;
+ margin: 0;
+ font-size: 100%;
+ white-space: pre-wrap;
+ word-break: break-all;
+ overflow-wrap: break-word;
+ background: transparent;
+ border: 0;
+}
+
+.markup .highlight {
+ margin-bottom: 16px;
+}
+
+.markup .highlight pre,
+.markup pre {
+ padding: 16px;
+ font-size: 85%;
+ line-height: 1.45;
+ background-color: var(--color-markup-code-block);
+ border-radius: var(--border-radius);
+}
+
+.markup .highlight pre {
+ margin-bottom: 0;
+ word-break: normal;
+}
+
+.markup pre {
+ word-wrap: normal;
+}
+
+.markup pre code,
+.markup pre tt {
+ display: inline;
+ padding: 0;
+ line-height: inherit;
+ word-wrap: normal;
+ background-color: transparent;
+ border: 0;
+}
+
+.markup pre code::before,
+.markup pre code::after,
+.markup pre tt::before,
+.markup pre tt::after {
+ content: normal;
+}
+
+.markup kbd {
+ display: inline-block;
+ padding: 3px 5px;
+ font-size: 11px;
+ line-height: 10px;
+ color: var(--color-text-light);
+ vertical-align: middle;
+ background-color: var(--color-markup-code-inline);
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ box-shadow: inset 0 -1px 0 var(--color-secondary);
+}
+
+.markup .ui.list .list,
+.markup ol.ui.list ol,
+.markup ul.ui.list ul {
+ padding-left: 2em;
+}
+
+.file-revisions-btn {
+ display: block;
+ float: left;
+ margin-bottom: 2px !important;
+ padding: 11px !important;
+ margin-right: 10px !important;
+}
+
+.file-revisions-btn i {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.markup-render {
+ display: block;
+ border: none;
+ width: 100%;
+ height: var(--height-loading); /* actual height is set in JS after loading */
+ overflow: hidden;
+ color-scheme: normal; /* match the value inside the iframe to allow it to become transparent */
+}
+
+.markup-block-error {
+ display: block !important; /* override fomantic .ui.form .error.message {display: none} */
+ border: none !important;
+ margin-bottom: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+ box-shadow: none !important;
+ font-size: 85% !important;
+ white-space: pre-wrap !important;
+ padding: 0.5rem 1rem !important;
+ text-align: left !important;
+}
+
+.markup-block-error + pre {
+ border-top: none !important;
+ margin-top: 0 !important;
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+}
+
+.file-view.markup.orgmode li.unchecked::before {
+ content: "[ ] ";
+}
+
+.file-view.markup.orgmode li.checked::before {
+ content: "[x] ";
+}
+
+.file-view.markup.orgmode li.indeterminate::before {
+ content: "[-] ";
+}
+
+/* This is only needed for <p> because they are literally acting as paragraphs,
+ * and thus having an ::before on the same line would force the paragraph to
+ * move to the next line. This can be avoided by an inline-block display that
+ * avoids that property while still having the other properties of the block
+ * display. */
+.file-view.markup.orgmode li.unchecked > p,
+.file-view.markup.orgmode li.checked > p,
+.file-view.markup.orgmode li.indeterminate > p {
+ display: inline-block;
+}
diff --git a/web_src/css/markup/dark.css b/web_src/css/markup/dark.css
new file mode 100644
index 0000000..700a485
--- /dev/null
+++ b/web_src/css/markup/dark.css
@@ -0,0 +1,13 @@
+.markup [src$="#gh-light-mode-only"],
+.markup [src$="#light-mode-only"],
+.markup [href$="#gh-light-mode-only"],
+.markup [href$="#light-mode-only"] {
+ display: none;
+}
+
+.markup [src$="#gh-dark-mode-only"],
+.markup [src$="#dark-mode-only"],
+.markup [href$="#gh-dark-mode-only"],
+.markup [href$="#dark-mode-only"] {
+ display: unset;
+}
diff --git a/web_src/css/markup/filepreview.css b/web_src/css/markup/filepreview.css
new file mode 100644
index 0000000..d2ec16e
--- /dev/null
+++ b/web_src/css/markup/filepreview.css
@@ -0,0 +1,41 @@
+.markup table.file-preview {
+ margin-bottom: 0;
+}
+
+.markup table.file-preview td {
+ padding: 0 10px !important;
+ border: none !important;
+}
+
+.markup table.file-preview tr {
+ border-top: none;
+ background-color: inherit !important;
+}
+
+.markup .file-preview-box {
+ margin-bottom: 16px;
+}
+
+.markup .file-preview-box .header {
+ padding: .5rem;
+ padding-left: 1rem;
+ border: 1px solid var(--color-secondary);
+ border-bottom: none;
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+ background: var(--color-box-header);
+}
+
+.markup .file-preview-box .warning {
+ border-radius: 0;
+ margin: 0;
+ padding: .5rem .5rem .5rem 1rem;
+}
+
+.markup .file-preview-box .header > a {
+ display: block;
+}
+
+.markup .file-preview-box .table {
+ margin-top: 0;
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
diff --git a/web_src/css/markup/light.css b/web_src/css/markup/light.css
new file mode 100644
index 0000000..88fc4b7
--- /dev/null
+++ b/web_src/css/markup/light.css
@@ -0,0 +1,6 @@
+.markup [src$="#gh-dark-mode-only"],
+.markup [src$="#dark-mode-only"],
+.markup [href$="#gh-dark-mode-only"],
+.markup [href$="#dark-mode-only"] {
+ display: none;
+}
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
new file mode 100644
index 0000000..a86c923
--- /dev/null
+++ b/web_src/css/modules/animations.css
@@ -0,0 +1,116 @@
+@keyframes isloadingspin {
+ 0% { transform: translate(-50%, -50%) rotate(0deg); }
+ 100% { transform: translate(-50%, -50%) rotate(360deg); }
+}
+
+.is-loading {
+ pointer-events: none !important;
+ position: relative !important;
+}
+
+.is-loading > * {
+ opacity: 0.3;
+}
+
+.btn.is-loading > *,
+.button.is-loading > * {
+ opacity: 0;
+}
+
+.is-loading::after {
+ content: "";
+ position: absolute;
+ display: block;
+ left: 50%;
+ top: 50%;
+ height: min(4em, 66.6%);
+ width: fit-content; /* compat: safari - https://bugs.webkit.org/show_bug.cgi?id=267625 */
+ aspect-ratio: 1;
+ transform: translate(-50%, -50%);
+ animation: isloadingspin 1000ms infinite linear;
+ border-width: 4px;
+ border-style: solid;
+ border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8);
+ border-radius: var(--border-radius-full);
+}
+
+.is-loading.loading-icon-2px::after {
+ border-width: 2px;
+}
+
+.is-loading.loading-icon-3px::after {
+ border-width: 3px;
+}
+
+/* for single form button, the loading state should be on the button, but not go semi-transparent, just replace the text on the button with the loader. */
+form.single-button-form.is-loading > * {
+ opacity: 1;
+}
+
+form.single-button-form.is-loading .button {
+ color: transparent;
+}
+
+.markup pre.is-loading,
+.editor-loading.is-loading,
+.pdf-content.is-loading {
+ height: var(--height-loading);
+}
+
+.markup .is-loading > * {
+ visibility: hidden;
+}
+
+.markup .is-loading {
+ color: transparent;
+ background: transparent;
+}
+
+/* TODO: not needed, use "is-loading loading-icon-2px" instead */
+code.language-math.is-loading::after {
+ padding: 0;
+ border-width: 2px;
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+@keyframes fadein {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes fadeout {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.8);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+.pulse {
+ animation: pulse 2s linear;
+}
+
+.ui.modal,
+.ui.dimmer.transition {
+ animation-name: fadein;
+ animation-duration: 100ms;
+ animation-timing-function: ease-in-out;
+}
diff --git a/web_src/css/modules/breadcrumb.css b/web_src/css/modules/breadcrumb.css
new file mode 100644
index 0000000..ca488c2
--- /dev/null
+++ b/web_src/css/modules/breadcrumb.css
@@ -0,0 +1,14 @@
+.breadcrumb {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 3px;
+}
+
+.breadcrumb .breadcrumb-divider {
+ color: var(--color-text-light-2);
+}
+
+.breadcrumb > * {
+ display: inline;
+}
diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css
new file mode 100644
index 0000000..0799ab8
--- /dev/null
+++ b/web_src/css/modules/button.css
@@ -0,0 +1,756 @@
+/* this contains override styles for buttons and related elements */
+
+/* these styles changed the Fomantic UI's rules, Fomantic UI expects only "basic" buttons have borders */
+.ui.button {
+ background: var(--color-button);
+ border: 1px solid var(--color-light-border);
+ color: var(--color-text);
+}
+
+.ui.button:hover,
+.ui.button:focus {
+ background: var(--color-hover);
+ color: var(--color-text);
+}
+
+.page-content .ui.button {
+ box-shadow: none !important;
+}
+
+.ui.active.button,
+.ui.button:active,
+.ui.active.button:active,
+.ui.active.button:hover,
+.ui.active.button:focus {
+ background: var(--color-active);
+ color: var(--color-text);
+}
+
+.delete-button,
+.delete-button:hover,
+.delete-button:focus {
+ color: var(--color-red);
+}
+
+/* btn is a plain button without any opinionated styling, it only uses flex for vertical alignment like ".ui.button" in base.css */
+
+.btn {
+ background: transparent;
+ border-radius: var(--border-radius);
+ border: none;
+ color: inherit;
+ margin: 0;
+ padding: 0;
+}
+
+.btn:hover,
+.btn:active,
+.btn:focus {
+ background: none;
+ border: none;
+}
+
+a.btn,
+a.btn:hover {
+ color: inherit;
+}
+
+/* By default, Fomantic UI doesn't support "bordered" buttons group, but Gitea would like to use it.
+And the default buttons always have borders now (not the same as Fomantic UI's default buttons, see above).
+It needs some tricks to tweak the left/right borders with active state */
+
+.ui.buttons .button {
+ border-right: none;
+}
+
+.ui.buttons .button:hover {
+ border-color: var(--color-secondary-dark-2);
+}
+
+.ui.buttons .button:hover + .button {
+ border-left: 1px solid var(--color-secondary-dark-2);
+}
+
+/* TODO: these "tw-hidden" selectors are only used by "blame.tmpl" buttons: Raw/Normal View/History/Unescape, need to be refactored to a clear solution later */
+.ui.buttons .button:first-child,
+.ui.buttons .button.tw-hidden:first-child + .button {
+ border-left: 1px solid var(--color-light-border);
+}
+
+.ui.buttons .button:last-child,
+.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) {
+ border-right: 1px solid var(--color-light-border);
+}
+
+.ui.buttons .button.active {
+ border-left: 1px solid var(--color-light-border);
+ border-right: 1px solid var(--color-light-border);
+}
+
+.ui.buttons .button.active + .button {
+ border-left: none;
+}
+
+.ui.basic.buttons .button,
+.ui.basic.button,
+.ui.basic.buttons .button:hover,
+.ui.basic.button:hover {
+ box-shadow: none;
+}
+
+/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they
+ would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */
+.ui.small.button:not(.compact):has(.svg) {
+ padding-top: 0.58928571em;
+ padding-bottom: 0.58928571em;
+}
+
+.ui.labeled.button.disabled > .button,
+.ui.basic.buttons .button,
+.ui.basic.button {
+ color: var(--color-text-light);
+ background: var(--color-button);
+}
+
+.ui.basic.buttons .button:hover,
+.ui.basic.button:hover,
+.ui.basic.buttons .button:focus,
+.ui.basic.button:focus {
+ color: var(--color-text);
+ background: var(--color-hover);
+ border-color: var(--color-secondary-dark-2);
+}
+
+.ui.basic.buttons .button:active,
+.ui.basic.button:active,
+.ui.basic.buttons .active.button,
+.ui.basic.active.button,
+.ui.basic.buttons .active.button:hover,
+.ui.basic.active.button:hover,
+.ui.basic.buttons .active.button:focus,
+.ui.basic.active.button:focus {
+ color: var(--color-text);
+ background: var(--color-active);
+}
+
+.ui.labeled.button > .label {
+ border-color: var(--color-light-border);
+}
+
+.ui.labeled.icon.buttons > .button > .icon,
+.ui.labeled.icon.button > .icon {
+ background: var(--color-hover);
+}
+
+/* primary */
+
+.ui.primary.labels .label,
+.ui.ui.ui.primary.label,
+.ui.primary.button,
+.ui.primary.buttons .button {
+ background: var(--color-primary);
+ color: var(--color-primary-contrast);
+}
+
+.ui.primary.button:hover,
+.ui.primary.buttons .button:hover,
+.ui.primary.button:focus,
+.ui.primary.buttons .button:focus {
+ background: var(--color-primary-hover);
+ color: var(--color-primary-contrast);
+}
+
+.ui.primary.button:active,
+.ui.primary.buttons .button:active {
+ background: var(--color-primary-active);
+}
+
+.ui.basic.primary.buttons .button,
+.ui.basic.primary.button {
+ color: var(--color-primary);
+ border-color: var(--color-primary);
+}
+
+.ui.basic.primary.buttons .button:hover,
+.ui.basic.primary.button:hover,
+.ui.basic.primary.buttons .button:focus,
+.ui.basic.primary.button:focus {
+ color: var(--color-primary-hover);
+ border-color: var(--color-primary-hover);
+}
+
+.ui.basic.primary.buttons .button:active,
+.ui.basic.primary.button:active {
+ color: var(--color-primary-active);
+ border-color: var(--color-primary-active);
+}
+
+/* secondary */
+
+.ui.secondary.labels .label,
+.ui.ui.ui.secondary.label,
+.ui.secondary.button,
+.ui.secondary.buttons .button,
+.ui.secondary.button:focus,
+.ui.secondary.buttons .button:focus {
+ background: var(--color-secondary-button);
+}
+
+.ui.secondary.button:hover,
+.ui.secondary.buttons .button:hover {
+ background: var(--color-secondary-hover);
+}
+
+.ui.secondary.button:active,
+.ui.secondary.buttons .button:active {
+ background: var(--color-secondary-active);
+}
+
+.ui.basic.secondary.buttons .button,
+.ui.basic.secondary.button {
+ color: var(--color-secondary-button);
+ border-color: var(--color-secondary-button);
+}
+
+.ui.basic.secondary.buttons .button:hover,
+.ui.basic.secondary.button:hover,
+.ui.basic.secondary.button:focus,
+.ui.basic.secondary.buttons .button:focus {
+ color: var(--color-secondary-hover);
+ border-color: var(--color-secondary-hover);
+}
+
+.ui.basic.secondary.buttons .button:active,
+.ui.basic.secondary.button:active {
+ color: var(--color-secondary-active);
+ border-color: var(--color-secondary-active);
+}
+
+/* red */
+
+.ui.red.labels .label,
+.ui.ui.ui.red.label,
+.ui.red.button,
+.ui.red.buttons .button {
+ background: var(--color-red);
+}
+
+.ui.red.button:hover,
+.ui.red.buttons .button:hover,
+.ui.red.button:focus,
+.ui.red.buttons .button:focus {
+ background: var(--color-red-dark-1);
+}
+
+.ui.red.button:active,
+.ui.red.buttons .button:active {
+ background: var(--color-red-dark-2);
+}
+
+.ui.basic.red.buttons .button,
+.ui.basic.red.button {
+ color: var(--color-red);
+ border-color: var(--color-red);
+}
+
+.ui.basic.red.buttons .button:hover,
+.ui.basic.red.button:hover,
+.ui.basic.red.buttons .button:focus,
+.ui.basic.red.button:focus {
+ color: var(--color-red-dark-1);
+ border-color: var(--color-red-dark-1);
+}
+
+.ui.basic.red.buttons .button:active,
+.ui.basic.red.button:active {
+ color: var(--color-red-dark-2);
+ border-color: var(--color-red-dark-2);
+}
+
+/* orange */
+
+.ui.orange.labels .label,
+.ui.ui.ui.orange.label,
+.ui.orange.button,
+.ui.orange.buttons .button,
+.ui.orange.button:focus,
+.ui.orange.buttons .button:focus {
+ background: var(--color-orange);
+}
+
+.ui.orange.button:hover,
+.ui.orange.buttons .button:hover {
+ background: var(--color-orange-dark-1);
+}
+
+.ui.orange.button:active,
+.ui.orange.buttons .button:active {
+ background: var(--color-orange-dark-2);
+}
+
+.ui.basic.orange.buttons .button,
+.ui.basic.orange.button,
+.ui.basic.orange.buttons .button:focus,
+.ui.basic.orange.button:focus {
+ color: var(--color-orange);
+ border-color: var(--color-orange);
+}
+
+.ui.basic.orange.buttons .button:hover,
+.ui.basic.orange.button:hover {
+ color: var(--color-orange-dark-1);
+ border-color: var(--color-orange-dark-1);
+}
+
+.ui.basic.orange.buttons .button:active,
+.ui.basic.orange.button:active {
+ color: var(--color-orange-dark-2);
+ border-color: var(--color-orange-dark-2);
+}
+
+/* yellow */
+
+.ui.yellow.labels .label,
+.ui.ui.ui.yellow.label,
+.ui.yellow.button,
+.ui.yellow.buttons .button,
+.ui.yellow.button:focus,
+.ui.yellow.buttons .button:focus {
+ background: var(--color-yellow);
+}
+
+.ui.yellow.button:hover,
+.ui.yellow.buttons .button:hover {
+ background: var(--color-yellow-dark-1);
+}
+
+.ui.yellow.button:active,
+.ui.yellow.buttons .button:active {
+ background: var(--color-yellow-dark-2);
+}
+
+.ui.basic.yellow.buttons .button,
+.ui.basic.yellow.button,
+.ui.basic.yellow.buttons .button:focus,
+.ui.basic.yellow.button:focus {
+ color: var(--color-yellow);
+ border-color: var(--color-yellow);
+}
+
+.ui.basic.yellow.buttons .button:hover,
+.ui.basic.yellow.button:hover {
+ color: var(--color-yellow-dark-1);
+ border-color: var(--color-yellow-dark-1);
+}
+
+.ui.basic.yellow.buttons .button:active,
+.ui.basic.yellow.button:active {
+ color: var(--color-yellow-dark-2);
+ border-color: var(--color-yellow-dark-2);
+}
+
+/* olive */
+
+.ui.olive.labels .label,
+.ui.ui.ui.olive.label,
+.ui.olive.button,
+.ui.olive.buttons .button,
+.ui.olive.button:focus,
+.ui.olive.buttons .button:focus {
+ background: var(--color-olive);
+}
+
+.ui.olive.button:hover,
+.ui.olive.buttons .button:hover {
+ background: var(--color-olive-dark-1);
+}
+
+.ui.olive.button:active,
+.ui.olive.buttons .button:active {
+ background: var(--color-olive-dark-2);
+}
+
+.ui.basic.olive.buttons .button,
+.ui.basic.olive.button,
+.ui.basic.olive.buttons .button:focus,
+.ui.basic.olive.button:focus {
+ color: var(--color-olive);
+ border-color: var(--color-olive);
+}
+
+.ui.basic.olive.buttons .button:hover,
+.ui.basic.olive.button:hover {
+ color: var(--color-olive-dark-1);
+ border-color: var(--color-olive-dark-1);
+}
+
+.ui.basic.olive.buttons .button:active,
+.ui.basic.olive.button:active {
+ color: var(--color-olive-dark-2);
+ border-color: var(--color-olive-dark-2);
+}
+
+/* green */
+
+.ui.green.labels .label,
+.ui.ui.ui.green.label,
+.ui.green.button,
+.ui.green.buttons .button,
+.ui.green.button:focus,
+.ui.green.buttons .button:focus {
+ background: var(--color-green);
+}
+
+.ui.green.button:hover,
+.ui.green.buttons .button:hover {
+ background: var(--color-green-dark-1);
+}
+
+.ui.green.button:active,
+.ui.green.buttons .button:active {
+ background: var(--color-green-dark-2);
+}
+
+.ui.basic.green.buttons .button,
+.ui.basic.green.button,
+.ui.basic.green.buttons .button:focus,
+.ui.basic.green.button:focus {
+ color: var(--color-green);
+ border-color: var(--color-green);
+}
+
+.ui.basic.green.buttons .button:hover,
+.ui.basic.green.button:hover {
+ color: var(--color-green-dark-1);
+ border-color: var(--color-green-dark-1);
+}
+
+.ui.basic.green.buttons .button:active,
+.ui.basic.green.button:active {
+ color: var(--color-green-dark-2);
+ border-color: var(--color-green-dark-2);
+}
+
+/* teal */
+
+.ui.teal.labels .label,
+.ui.ui.ui.teal.label,
+.ui.teal.button,
+.ui.teal.buttons .button,
+.ui.teal.button:focus,
+.ui.teal.buttons .button:focus {
+ background: var(--color-teal);
+}
+
+.ui.teal.button:hover,
+.ui.teal.buttons .button:hover {
+ background: var(--color-teal-dark-1);
+}
+
+.ui.teal.button:active,
+.ui.teal.buttons .button:active {
+ background: var(--color-teal-dark-2);
+}
+
+.ui.basic.teal.buttons .button,
+.ui.basic.teal.button,
+.ui.basic.teal.buttons .button:focus,
+.ui.basic.teal.button:focus {
+ color: var(--color-teal);
+ border-color: var(--color-teal);
+}
+
+.ui.basic.teal.buttons .button:hover,
+.ui.basic.teal.button:hover {
+ color: var(--color-teal-dark-1);
+ border-color: var(--color-teal-dark-1);
+}
+
+.ui.basic.teal.buttons .button:active,
+.ui.basic.teal.button:active {
+ color: var(--color-teal-dark-2);
+ border-color: var(--color-teal-dark-2);
+}
+
+/* blue */
+
+.ui.blue.labels .label,
+.ui.ui.ui.blue.label,
+.ui.blue.button,
+.ui.blue.buttons .button,
+.ui.blue.button:focus,
+.ui.blue.buttons .button:focus {
+ background: var(--color-blue);
+}
+
+.ui.blue.button:hover,
+.ui.blue.buttons .button:hover {
+ background: var(--color-blue-dark-1);
+}
+
+.ui.blue.button:active,
+.ui.blue.buttons .button:active {
+ background: var(--color-blue-dark-2);
+}
+
+.ui.basic.blue.buttons .button,
+.ui.basic.blue.button,
+.ui.basic.blue.buttons .button:focus,
+.ui.basic.blue.button:focus {
+ color: var(--color-blue);
+ border-color: var(--color-blue);
+}
+
+.ui.basic.blue.buttons .button:hover,
+.ui.basic.blue.button:hover {
+ color: var(--color-blue-dark-1);
+ border-color: var(--color-blue-dark-1);
+}
+
+.ui.basic.blue.buttons .button:active,
+.ui.basic.blue.button:active {
+ color: var(--color-blue-dark-2);
+ border-color: var(--color-blue-dark-2);
+}
+
+/* violet */
+
+.ui.violet.labels .label,
+.ui.ui.ui.violet.label,
+.ui.violet.button,
+.ui.violet.buttons .button,
+.ui.violet.button:focus,
+.ui.violet.buttons .button:focus {
+ background: var(--color-violet);
+}
+
+.ui.violet.button:hover,
+.ui.violet.buttons .button:hover {
+ background: var(--color-violet-dark-1);
+}
+
+.ui.violet.button:active,
+.ui.violet.buttons .button:active {
+ background: var(--color-violet-dark-2);
+}
+
+.ui.basic.violet.buttons .button,
+.ui.basic.violet.button,
+.ui.basic.violet.buttons .button:focus,
+.ui.basic.violet.button:focus {
+ color: var(--color-violet);
+ border-color: var(--color-violet);
+}
+
+.ui.basic.violet.buttons .button:hover,
+.ui.basic.violet.button:hover {
+ color: var(--color-violet-dark-1);
+ border-color: var(--color-violet-dark-1);
+}
+
+.ui.basic.violet.buttons .button:active,
+.ui.basic.violet.button:active {
+ color: var(--color-violet-dark-2);
+ border-color: var(--color-violet-dark-2);
+}
+
+/* purple */
+
+.ui.purple.labels .label,
+.ui.ui.ui.purple.label,
+.ui.purple.button,
+.ui.purple.buttons .button,
+.ui.purple.button:focus,
+.ui.purple.buttons .button:focus {
+ background: var(--color-purple);
+}
+
+.ui.purple.button:hover,
+.ui.purple.buttons .button:hover {
+ background: var(--color-purple-dark-1);
+}
+
+.ui.purple.button:active,
+.ui.purple.buttons .button:active {
+ background: var(--color-purple-dark-2);
+}
+
+.ui.basic.purple.buttons .button,
+.ui.basic.purple.button,
+.ui.basic.purple.buttons .button:focus,
+.ui.basic.purple.button:focus {
+ color: var(--color-purple);
+ border-color: var(--color-purple);
+}
+
+.ui.basic.purple.buttons .button:hover,
+.ui.basic.purple.button:hover {
+ color: var(--color-purple-dark-1);
+ border-color: var(--color-purple-dark-1);
+}
+
+.ui.basic.purple.buttons .button:active,
+.ui.basic.purple.button:active {
+ color: var(--color-purple-dark-2);
+ border-color: var(--color-purple-dark-2);
+}
+
+/* pink */
+
+.ui.pink.labels .label,
+.ui.ui.ui.pink.label,
+.ui.pink.button,
+.ui.pink.buttons .button,
+.ui.pink.button:focus,
+.ui.pink.buttons .button:focus {
+ background: var(--color-pink);
+}
+
+.ui.pink.button:hover,
+.ui.pink.buttons .button:hover {
+ background: var(--color-pink-dark-1);
+}
+
+.ui.pink.button:active,
+.ui.pink.buttons .button:active {
+ background: var(--color-pink-dark-2);
+}
+
+.ui.basic.pink.buttons .button,
+.ui.basic.pink.button,
+.ui.basic.pink.buttons .button:focus,
+.ui.basic.pink.button:focus {
+ color: var(--color-pink);
+ border-color: var(--color-pink);
+}
+
+.ui.basic.pink.buttons .button:hover,
+.ui.basic.pink.button:hover {
+ color: var(--color-pink-dark-1);
+ border-color: var(--color-pink-dark-1);
+}
+
+.ui.basic.pink.buttons .button:active,
+.ui.basic.pink.button:active {
+ color: var(--color-pink-dark-2);
+ border-color: var(--color-pink-dark-2);
+}
+
+/* brown */
+
+.ui.brown.labels .label,
+.ui.ui.ui.brown.label,
+.ui.brown.button,
+.ui.brown.buttons .button,
+.ui.brown.button:focus,
+.ui.brown.buttons .button:focus {
+ background: var(--color-brown);
+}
+
+.ui.brown.button:hover,
+.ui.brown.buttons .button:hover {
+ background: var(--color-brown-dark-1);
+}
+
+.ui.brown.button:active,
+.ui.brown.buttons .button:active {
+ background: var(--color-brown-dark-2);
+}
+
+.ui.basic.brown.buttons .button,
+.ui.basic.brown.button,
+.ui.basic.brown.buttons .button:focus,
+.ui.basic.brown.button:focus {
+ color: var(--color-brown);
+ border-color: var(--color-brown);
+}
+
+.ui.basic.brown.buttons .button:hover,
+.ui.basic.brown.button:hover {
+ color: var(--color-brown-dark-1);
+ border-color: var(--color-brown-dark-1);
+}
+
+.ui.basic.brown.buttons .button:active,
+.ui.basic.brown.button:active {
+ color: var(--color-brown-dark-2);
+ border-color: var(--color-brown-dark-2);
+}
+
+/* negative */
+
+.ui.negative.buttons .button,
+.ui.negative.button,
+.ui.negative.buttons .button:focus,
+.ui.negative.button:focus {
+ background: var(--color-red);
+}
+
+.ui.negative.buttons .button:hover,
+.ui.negative.button:hover {
+ background: var(--color-red-dark-1);
+}
+
+.ui.negative.buttons .button:active,
+.ui.negative.button:active {
+ background: var(--color-red-dark-2);
+}
+
+.ui.basic.negative.buttons .button,
+.ui.basic.negative.button,
+.ui.basic.negative.buttons .button:focus,
+.ui.basic.negative.button:focus {
+ color: var(--color-red);
+ border-color: var(--color-red);
+}
+
+.ui.basic.negative.buttons .button:hover,
+.ui.basic.negative.button:hover {
+ color: var(--color-red-dark-1);
+ border-color: var(--color-red-dark-1);
+}
+
+.ui.basic.negative.buttons .button:active,
+.ui.basic.negative.button:active {
+ color: var(--color-red-dark-2);
+ border-color: var(--color-red-dark-2);
+}
+
+/* positive */
+
+.ui.positive.buttons .button,
+.ui.positive.button,
+.ui.positive.buttons .button:focus,
+.ui.positive.button:focus {
+ background: var(--color-green);
+}
+
+.ui.positive.buttons .button:hover,
+.ui.positive.button:hover {
+ background: var(--color-green-dark-1);
+}
+
+.ui.positive.buttons .button:active,
+.ui.positive.button:active {
+ background: var(--color-green-dark-2);
+}
+
+.ui.basic.positive.buttons .button,
+.ui.basic.positive.button,
+.ui.basic.positive.buttons .button:focus,
+.ui.basic.positive.button:focus {
+ color: var(--color-green);
+ border-color: var(--color-green);
+}
+
+.ui.basic.positive.buttons .button:hover,
+.ui.basic.positive.button:hover {
+ color: var(--color-green-dark-1);
+ border-color: var(--color-green-dark-1);
+}
+
+.ui.basic.positive.buttons .button:active,
+.ui.basic.positive.button:active {
+ color: var(--color-green-dark-2);
+ border-color: var(--color-green-dark-2);
+}
diff --git a/web_src/css/modules/card.css b/web_src/css/modules/card.css
new file mode 100644
index 0000000..2406def
--- /dev/null
+++ b/web_src/css/modules/card.css
@@ -0,0 +1,134 @@
+/* Below styles are a subset of the full fomantic card styles which are */
+/* needed to get all current uses of fomantic cards working. */
+/* TODO: remove all these styles and use custom styling instead */
+
+.ui.card:last-child {
+ margin-bottom: 0;
+}
+.ui.card:first-child {
+ margin-top: 0;
+}
+
+.ui.cards > .card,
+.ui.card {
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ width: 290px;
+ min-height: 0;
+ padding: 0;
+ background: var(--color-card);
+ border: 1px solid var(--color-secondary);
+ box-shadow: none;
+ word-wrap: break-word;
+}
+
+.ui.card {
+ margin: 1em 0;
+}
+
+.ui.cards {
+ display: flex;
+ margin: -0.875em -0.5em;
+ flex-wrap: wrap;
+}
+
+.ui.cards > .card {
+ display: flex;
+ margin: 0.875em 0.5em;
+ float: none;
+}
+
+.ui.cards > .card > .content,
+.ui.card > .content {
+ border-top: 1px solid var(--color-secondary);
+ max-width: 100%;
+ padding: 1em;
+ font-size: 1em;
+}
+
+.ui.cards > .card > .content > .meta + .description,
+.ui.cards > .card > .content > .header + .description,
+.ui.card > .content > .meta + .description,
+.ui.card > .content > .header + .description {
+ margin-top: .5em;
+}
+
+.ui.cards > .card > .content > .header:not(.ui),
+.ui.card > .content > .header:not(.ui) {
+ font-weight: var(--font-weight-medium);
+ font-size: 1.28571429em;
+ margin-top: -.21425em;
+ line-height: 1.28571429;
+}
+
+.ui.cards > .card > .content:first-child,
+.ui.card > .content:first-child {
+ border-top: none;
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+.ui.cards > .card > :last-child,
+.ui.card > :last-child {
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.ui.cards > .card > :only-child,
+.ui.card > :only-child {
+ border-radius: var(--border-radius) !important;
+}
+
+.ui.cards > .card > .extra,
+.ui.card > .extra,
+.ui.cards > .card > .extra a:not(.ui),
+.ui.card > .extra a:not(.ui) {
+ color: var(--color-text);
+}
+
+.ui.cards > .card > .extra a:not(.ui):hover,
+.ui.card > .extra a:not(.ui):hover {
+ color: var(--color-primary);
+}
+
+.ui.cards > .card > .content > .header,
+.ui.card > .content > .header {
+ color: var(--color-text);
+}
+
+.ui.cards > .card > .content > .description,
+.ui.card > .content > .description {
+ color: var(--color-text);
+}
+
+.ui.cards > .card .meta > a:not(.ui),
+.ui.card .meta > a:not(.ui) {
+ color: var(--color-text-light-2);
+}
+
+.ui.cards > .card .meta > a:not(.ui):hover,
+.ui.card .meta > a:not(.ui):hover {
+ color: var(--color-text);
+}
+
+.ui.cards a.card:hover,
+a.ui.card:hover {
+ border: 1px solid var(--color-secondary);
+ background: var(--color-card);
+}
+
+.ui.cards > .card > .extra,
+.ui.card > .extra {
+ color: var(--color-text);
+ border-top-color: var(--color-secondary-light-1) !important;
+}
+
+.ui.three.cards {
+ margin-left: -1em;
+ margin-right: -1em;
+}
+
+.ui.three.cards > .card {
+ width: calc(33.33333333333333% - 2em);
+ margin-left: 1em;
+ margin-right: 1em;
+}
diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css
new file mode 100644
index 0000000..8d73573
--- /dev/null
+++ b/web_src/css/modules/checkbox.css
@@ -0,0 +1,121 @@
+/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+input[type="checkbox"],
+input[type="radio"] {
+ width: var(--checkbox-size);
+ height: var(--checkbox-size);
+}
+
+.ui.checkbox {
+ position: relative;
+ display: inline-block;
+ vertical-align: baseline;
+ min-height: var(--checkbox-size);
+ line-height: var(--checkbox-size);
+ min-width: var(--checkbox-size);
+ padding: 1px;
+}
+
+.ui.checkbox input[type="checkbox"],
+.ui.checkbox input[type="radio"] {
+ position: absolute;
+ top: 1px;
+ left: 0;
+ width: var(--checkbox-size);
+ height: var(--checkbox-size);
+}
+
+.ui.checkbox input[type="checkbox"]:enabled,
+.ui.checkbox input[type="radio"]:enabled,
+.ui.checkbox label:enabled {
+ cursor: pointer;
+}
+
+.ui.checkbox label {
+ cursor: auto;
+ position: relative;
+ display: block;
+ user-select: none;
+}
+
+.ui.checkbox label,
+.ui.radio.checkbox label {
+ margin-left: 1.85714em;
+}
+
+.ui.checkbox + label {
+ vertical-align: middle;
+}
+
+.ui.disabled.checkbox label,
+.ui.checkbox input[disabled] ~ label {
+ cursor: default !important;
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+.ui.radio.checkbox {
+ min-height: var(--checkbox-size);
+}
+
+/* "switch" styled checkbox */
+
+.ui.toggle.checkbox {
+ min-height: 1.5rem;
+}
+.ui.toggle.checkbox input {
+ width: 3.5rem;
+ height: 21px;
+ opacity: 0;
+ z-index: 3;
+}
+.ui.toggle.checkbox label {
+ min-height: 1.5rem;
+ padding-left: 4.5rem;
+ padding-top: 0.15em;
+}
+.ui.toggle.checkbox label::before {
+ display: block;
+ position: absolute;
+ content: "";
+ z-index: 1;
+ top: 0;
+ width: 49px;
+ height: 21px;
+ border-radius: 500rem;
+ left: 0;
+}
+.ui.toggle.checkbox label::after {
+ background: var(--color-white);
+ box-shadow: 1px 1px 4px 1px var(--color-shadow);
+ position: absolute;
+ content: "";
+ opacity: 1;
+ z-index: 2;
+ width: 18px;
+ height: 18px;
+ top: 1.5px;
+ left: 1.5px;
+ border-radius: 500rem;
+ transition: background 0.3s ease, left 0.3s ease;
+}
+.ui.toggle.checkbox input ~ label::after {
+ left: 1.5px;
+}
+.ui.toggle.checkbox input:checked ~ label::after {
+ left: 29px;
+}
+.ui.toggle.checkbox input:focus ~ label::before,
+.ui.toggle.checkbox label::before {
+ background: var(--color-input-toggle-background);
+}
+.ui.toggle.checkbox label,
+.ui.toggle.checkbox input:checked ~ label,
+.ui.toggle.checkbox input:focus:checked ~ label {
+ color: var(--color-text) !important;
+}
+.ui.toggle.checkbox input:checked ~ label::before,
+.ui.toggle.checkbox input:focus:checked ~ label::before {
+ background: var(--color-primary) !important;
+}
diff --git a/web_src/css/modules/comment.css b/web_src/css/modules/comment.css
new file mode 100644
index 0000000..799eeb8
--- /dev/null
+++ b/web_src/css/modules/comment.css
@@ -0,0 +1,90 @@
+/* These are the remnants of the fomantic comment module */
+/* TODO: remove all of these rules */
+
+.ui.comments {
+ margin: 1.5em 0;
+}
+
+.ui.comments:first-child {
+ margin-top: 0;
+}
+
+.ui.comments:last-child {
+ margin-bottom: 0;
+}
+
+.ui.comments .comment {
+ position: relative;
+ background: none;
+ margin: 0.5em 0 0;
+ padding: 0.5em 0 0;
+ border: none;
+ border-top: none;
+ line-height: 1.2;
+}
+
+.ui.comments .comment:first-child {
+ margin-top: 0;
+ padding-top: 0;
+}
+
+.ui.comments .comment > .comments {
+ margin: 0 0 0.5em 0.5em;
+ padding: 1em 0 1em 1em;
+}
+
+.ui.comments .comment > .comments::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.ui.comments .comment > .comments .comment {
+ border: none;
+ border-top: none;
+ background: none;
+}
+
+.ui.comments .comment .avatar {
+ float: left;
+ width: 2.5em;
+}
+
+.ui.comments .comment > .content {
+ display: block;
+}
+
+.ui.comments .comment > .avatar ~ .content {
+ margin-left: 3.5em;
+}
+
+.ui.comments .comment .author {
+ font-size: 1em;
+ font-weight: var(--font-weight-medium);
+}
+
+.ui.comments .comment a.author {
+ cursor: pointer;
+}
+
+.ui.comments .comment .metadata {
+ display: inline-block;
+ margin-left: 0.5em;
+ font-size: 0.875em;
+}
+
+.ui.comments .comment .metadata > * {
+ display: inline-block;
+ margin: 0 0.5em 0 0;
+}
+
+.ui.comments .comment .metadata > :last-child {
+ margin-right: 0;
+}
+
+.ui.comments .comment .text {
+ margin: 0.25em 0 0.5em;
+ font-size: 1em;
+ word-wrap: break-word;
+ line-height: 1.3;
+}
diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css
new file mode 100644
index 0000000..95c71b2
--- /dev/null
+++ b/web_src/css/modules/container.css
@@ -0,0 +1,66 @@
+/* based on Fomantic UI container module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.container {
+ display: block;
+ --container-width: 100%;
+ max-width: var(--container-width);
+}
+
+@media (max-width: 767.98px) {
+ .ui.ui.ui.container:not(.fluid) {
+ --container-width: auto;
+ width: var(--container-width);
+ margin-left: 1em;
+ margin-right: 1em;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+ .ui.ui.ui.container:not(.fluid) {
+ --container-width: 723px;
+ width: var(--container-width);
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+@media (min-width: 992px) and (max-width: 1199.98px) {
+ .ui.ui.ui.container:not(.fluid) {
+ --container-width: 933px;
+ width: var(--container-width);
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+@media (min-width: 1200px) {
+ .ui.ui.ui.container:not(.fluid) {
+ --container-width: 1127px;
+ width: var(--container-width);
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.ui.fluid.container {
+ --container-width: 100%;
+ width: var(--container-width);
+}
+
+.ui[class*="center aligned"].container {
+ text-align: center;
+}
+
+/* overwrite width of containers inside the main page content div (div with class "page-content") */
+.page-content .ui.ui.ui.container:not(.fluid) {
+ --container-width: 1280px;
+ width: var(--container-width);
+ max-width: calc(100% - calc(2 * var(--page-margin-x)));
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.ui.container.fluid.padded {
+ padding: 0 var(--page-margin-x);
+}
diff --git a/web_src/css/modules/divider.css b/web_src/css/modules/divider.css
new file mode 100644
index 0000000..acc8408
--- /dev/null
+++ b/web_src/css/modules/divider.css
@@ -0,0 +1,43 @@
+.divider {
+ margin: 10px 0;
+ height: 0;
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text);
+ font-size: 1rem;
+ width: 100%;
+}
+
+h4.divider {
+ margin-top: 1.25rem;
+ margin-bottom: 1.25rem;
+}
+
+.divider:not(.divider-text) {
+ border-top: 1px solid var(--color-secondary);
+}
+
+.divider.divider-text {
+ display: flex;
+ align-items: center;
+ padding: 5px 0;
+}
+
+.divider.divider-text::before,
+.divider.divider-text::after {
+ content: "";
+ flex: 1;
+ border-top: 1px solid var(--color-secondary);
+}
+
+.divider.divider-text::before {
+ margin-right: .75em;
+}
+
+.divider.divider-text::after {
+ margin-left: .75em;
+}
+
+.ui.dropdown .menu > .divider {
+ border-top: 1px solid var(--color-secondary);
+ margin: 4px 0;
+}
diff --git a/web_src/css/modules/flexcontainer.css b/web_src/css/modules/flexcontainer.css
new file mode 100644
index 0000000..5d4e12c
--- /dev/null
+++ b/web_src/css/modules/flexcontainer.css
@@ -0,0 +1,33 @@
+/* container for full-page content with sidebar on left */
+
+.flex-container {
+ display: flex !important;
+ gap: var(--page-spacing);
+ margin-top: var(--page-spacing);
+}
+
+/* small options menu on the left, used in settings/admin pages */
+.flex-container-nav {
+ width: 240px;
+}
+
+/* wide sidebar on the right, used in frontpage */
+.flex-container-sidebar {
+ width: 35%;
+}
+
+.flex-container-main {
+ flex: 1;
+ min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */
+}
+
+@media (max-width: 767.98px) {
+ .flex-container {
+ flex-direction: column;
+ }
+ .flex-container-nav,
+ .flex-container-sidebar {
+ order: -1;
+ width: auto;
+ }
+}
diff --git a/web_src/css/modules/grid.css b/web_src/css/modules/grid.css
new file mode 100644
index 0000000..1df9a11
--- /dev/null
+++ b/web_src/css/modules/grid.css
@@ -0,0 +1,524 @@
+.simple-grid {
+ display: grid;
+ gap: 1em 2em;
+}
+
+@media (min-width: 30em) {
+ .simple-grid.grid-2 {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+/* based on Fomantic UI grid module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.grid {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: stretch;
+ padding: 0;
+ margin: -1rem;
+}
+
+.ui.relaxed.grid {
+ margin-left: -1.5rem;
+ margin-right: -1.5rem;
+}
+.ui[class*="very relaxed"].grid {
+ margin-left: -2.5rem;
+ margin-right: -2.5rem;
+}
+
+.ui.grid + .grid {
+ margin-top: 1rem;
+}
+
+.ui.grid > .column:not(.row),
+.ui.grid > .row > .column {
+ position: relative;
+ display: inline-block;
+ width: 6.25%;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ vertical-align: top;
+}
+.ui.grid > * {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.ui.grid > .row {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: inherit;
+ align-items: stretch;
+ width: 100% !important;
+ padding: 0;
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.ui.grid > .column:not(.row) {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+.ui.grid > .row > .column {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.ui.grid > .row > img,
+.ui.grid > .row > .column > img {
+ max-width: 100%;
+}
+
+.ui.grid > .ui.grid:first-child {
+ margin-top: 0;
+}
+.ui.grid > .ui.grid:last-child {
+ margin-bottom: 0;
+}
+
+.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached),
+.ui.aligned.grid .column > .segment:not(.compact):not(.attached) {
+ width: 100%;
+}
+
+.ui.grid .row + .ui.divider {
+ flex-grow: 1;
+ margin: 1rem;
+}
+.ui.grid .column + .ui.vertical.divider {
+ height: calc(50% - 1rem);
+}
+
+.ui.grid > .row > .column:last-child > .horizontal.segment,
+.ui.grid > .column:last-child > .horizontal.segment {
+ box-shadow: none;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.page.grid {
+ width: auto;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 0;
+ margin-right: 0;
+ }
+}
+@media only screen and (min-width: 768px) and (max-width: 991.98px) {
+ .ui.page.grid {
+ width: auto;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 2em;
+ padding-right: 2em;
+ }
+}
+@media only screen and (min-width: 992px) and (max-width: 1199.98px) {
+ .ui.page.grid {
+ width: auto;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 3%;
+ padding-right: 3%;
+ }
+}
+@media only screen and (min-width: 1200px) and (max-width: 1919.98px) {
+ .ui.page.grid {
+ width: auto;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 15%;
+ padding-right: 15%;
+ }
+}
+@media only screen and (min-width: 1920px) {
+ .ui.page.grid {
+ width: auto;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 23%;
+ padding-right: 23%;
+ }
+}
+
+.ui.grid > .column:only-child,
+.ui.grid > .row > .column:only-child {
+ width: 100%;
+}
+
+.ui[class*="one column"].grid > .row > .column,
+.ui[class*="one column"].grid > .column:not(.row) {
+ width: 100%;
+}
+.ui[class*="two column"].grid > .row > .column,
+.ui[class*="two column"].grid > .column:not(.row) {
+ width: 50%;
+}
+.ui[class*="three column"].grid > .row > .column,
+.ui[class*="three column"].grid > .column:not(.row) {
+ width: 33.33333333%;
+}
+.ui[class*="four column"].grid > .row > .column,
+.ui[class*="four column"].grid > .column:not(.row) {
+ width: 25%;
+}
+.ui[class*="five column"].grid > .row > .column,
+.ui[class*="five column"].grid > .column:not(.row) {
+ width: 20%;
+}
+.ui[class*="six column"].grid > .row > .column,
+.ui[class*="six column"].grid > .column:not(.row) {
+ width: 16.66666667%;
+}
+.ui[class*="seven column"].grid > .row > .column,
+.ui[class*="seven column"].grid > .column:not(.row) {
+ width: 14.28571429%;
+}
+.ui[class*="eight column"].grid > .row > .column,
+.ui[class*="eight column"].grid > .column:not(.row) {
+ width: 12.5%;
+}
+.ui[class*="nine column"].grid > .row > .column,
+.ui[class*="nine column"].grid > .column:not(.row) {
+ width: 11.11111111%;
+}
+.ui[class*="ten column"].grid > .row > .column,
+.ui[class*="ten column"].grid > .column:not(.row) {
+ width: 10%;
+}
+.ui[class*="eleven column"].grid > .row > .column,
+.ui[class*="eleven column"].grid > .column:not(.row) {
+ width: 9.09090909%;
+}
+.ui[class*="twelve column"].grid > .row > .column,
+.ui[class*="twelve column"].grid > .column:not(.row) {
+ width: 8.33333333%;
+}
+.ui[class*="thirteen column"].grid > .row > .column,
+.ui[class*="thirteen column"].grid > .column:not(.row) {
+ width: 7.69230769%;
+}
+.ui[class*="fourteen column"].grid > .row > .column,
+.ui[class*="fourteen column"].grid > .column:not(.row) {
+ width: 7.14285714%;
+}
+.ui[class*="fifteen column"].grid > .row > .column,
+.ui[class*="fifteen column"].grid > .column:not(.row) {
+ width: 6.66666667%;
+}
+.ui[class*="sixteen column"].grid > .row > .column,
+.ui[class*="sixteen column"].grid > .column:not(.row) {
+ width: 6.25%;
+}
+
+.ui.grid > [class*="one column"].row > .column {
+ width: 100% !important;
+}
+.ui.grid > [class*="two column"].row > .column {
+ width: 50% !important;
+}
+.ui.grid > [class*="three column"].row > .column {
+ width: 33.33333333% !important;
+}
+.ui.grid > [class*="four column"].row > .column {
+ width: 25% !important;
+}
+.ui.grid > [class*="five column"].row > .column {
+ width: 20% !important;
+}
+.ui.grid > [class*="six column"].row > .column {
+ width: 16.66666667% !important;
+}
+.ui.grid > [class*="seven column"].row > .column {
+ width: 14.28571429% !important;
+}
+.ui.grid > [class*="eight column"].row > .column {
+ width: 12.5% !important;
+}
+.ui.grid > [class*="nine column"].row > .column {
+ width: 11.11111111% !important;
+}
+.ui.grid > [class*="ten column"].row > .column {
+ width: 10% !important;
+}
+.ui.grid > [class*="eleven column"].row > .column {
+ width: 9.09090909% !important;
+}
+.ui.grid > [class*="twelve column"].row > .column {
+ width: 8.33333333% !important;
+}
+.ui.grid > [class*="thirteen column"].row > .column {
+ width: 7.69230769% !important;
+}
+.ui.grid > [class*="fourteen column"].row > .column {
+ width: 7.14285714% !important;
+}
+.ui.grid > [class*="fifteen column"].row > .column {
+ width: 6.66666667% !important;
+}
+.ui.grid > [class*="sixteen column"].row > .column {
+ width: 6.25% !important;
+}
+
+.ui.grid > .row > [class*="one wide"].column,
+.ui.grid > .column.row > [class*="one wide"].column,
+.ui.grid > [class*="one wide"].column,
+.ui.column.grid > [class*="one wide"].column {
+ width: 6.25% !important;
+}
+.ui.grid > .row > [class*="two wide"].column,
+.ui.grid > .column.row > [class*="two wide"].column,
+.ui.grid > [class*="two wide"].column,
+.ui.column.grid > [class*="two wide"].column {
+ width: 12.5% !important;
+}
+.ui.grid > .row > [class*="three wide"].column,
+.ui.grid > .column.row > [class*="three wide"].column,
+.ui.grid > [class*="three wide"].column,
+.ui.column.grid > [class*="three wide"].column {
+ width: 18.75% !important;
+}
+.ui.grid > .row > [class*="four wide"].column,
+.ui.grid > .column.row > [class*="four wide"].column,
+.ui.grid > [class*="four wide"].column,
+.ui.column.grid > [class*="four wide"].column {
+ width: 25% !important;
+}
+.ui.grid > .row > [class*="five wide"].column,
+.ui.grid > .column.row > [class*="five wide"].column,
+.ui.grid > [class*="five wide"].column,
+.ui.column.grid > [class*="five wide"].column {
+ width: 31.25% !important;
+}
+.ui.grid > .row > [class*="six wide"].column,
+.ui.grid > .column.row > [class*="six wide"].column,
+.ui.grid > [class*="six wide"].column,
+.ui.column.grid > [class*="six wide"].column {
+ width: 37.5% !important;
+}
+.ui.grid > .row > [class*="seven wide"].column,
+.ui.grid > .column.row > [class*="seven wide"].column,
+.ui.grid > [class*="seven wide"].column,
+.ui.column.grid > [class*="seven wide"].column {
+ width: 43.75% !important;
+}
+.ui.grid > .row > [class*="eight wide"].column,
+.ui.grid > .column.row > [class*="eight wide"].column,
+.ui.grid > [class*="eight wide"].column,
+.ui.column.grid > [class*="eight wide"].column {
+ width: 50% !important;
+}
+.ui.grid > .row > [class*="nine wide"].column,
+.ui.grid > .column.row > [class*="nine wide"].column,
+.ui.grid > [class*="nine wide"].column,
+.ui.column.grid > [class*="nine wide"].column {
+ width: 56.25% !important;
+}
+.ui.grid > .row > [class*="ten wide"].column,
+.ui.grid > .column.row > [class*="ten wide"].column,
+.ui.grid > [class*="ten wide"].column,
+.ui.column.grid > [class*="ten wide"].column {
+ width: 62.5% !important;
+}
+.ui.grid > .row > [class*="eleven wide"].column,
+.ui.grid > .column.row > [class*="eleven wide"].column,
+.ui.grid > [class*="eleven wide"].column,
+.ui.column.grid > [class*="eleven wide"].column {
+ width: 68.75% !important;
+}
+.ui.grid > .row > [class*="twelve wide"].column,
+.ui.grid > .column.row > [class*="twelve wide"].column,
+.ui.grid > [class*="twelve wide"].column,
+.ui.column.grid > [class*="twelve wide"].column {
+ width: 75% !important;
+}
+.ui.grid > .row > [class*="thirteen wide"].column,
+.ui.grid > .column.row > [class*="thirteen wide"].column,
+.ui.grid > [class*="thirteen wide"].column,
+.ui.column.grid > [class*="thirteen wide"].column {
+ width: 81.25% !important;
+}
+.ui.grid > .row > [class*="fourteen wide"].column,
+.ui.grid > .column.row > [class*="fourteen wide"].column,
+.ui.grid > [class*="fourteen wide"].column,
+.ui.column.grid > [class*="fourteen wide"].column {
+ width: 87.5% !important;
+}
+.ui.grid > .row > [class*="fifteen wide"].column,
+.ui.grid > .column.row > [class*="fifteen wide"].column,
+.ui.grid > [class*="fifteen wide"].column,
+.ui.column.grid > [class*="fifteen wide"].column {
+ width: 93.75% !important;
+}
+.ui.grid > .row > [class*="sixteen wide"].column,
+.ui.grid > .column.row > [class*="sixteen wide"].column,
+.ui.grid > [class*="sixteen wide"].column,
+.ui.column.grid > [class*="sixteen wide"].column {
+ width: 100% !important;
+}
+
+.ui.centered.grid,
+.ui.centered.grid > .row,
+.ui.grid > .centered.row {
+ text-align: center;
+ justify-content: center;
+}
+.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row),
+.ui.centered.grid > .row > .column:not(.aligned):not(.justified),
+.ui.grid .centered.row > .column:not(.aligned):not(.justified) {
+ text-align: left;
+}
+.ui.grid > .centered.column,
+.ui.grid > .row > .centered.column {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.ui.relaxed.grid > .column:not(.row),
+.ui.relaxed.grid > .row > .column,
+.ui.grid > .relaxed.row > .column {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+}
+.ui[class*="very relaxed"].grid > .column:not(.row),
+.ui[class*="very relaxed"].grid > .row > .column,
+.ui.grid > [class*="very relaxed"].row > .column {
+ padding-left: 2.5rem;
+ padding-right: 2.5rem;
+}
+
+.ui.relaxed.grid .row + .ui.divider,
+.ui.grid .relaxed.row + .ui.divider {
+ margin-left: 1.5rem;
+ margin-right: 1.5rem;
+}
+.ui[class*="very relaxed"].grid .row + .ui.divider,
+.ui.grid [class*="very relaxed"].row + .ui.divider {
+ margin-left: 2.5rem;
+ margin-right: 2.5rem;
+}
+
+.ui[class*="middle aligned"].grid > .column:not(.row),
+.ui[class*="middle aligned"].grid > .row > .column,
+.ui.grid > [class*="middle aligned"].row > .column,
+.ui.grid > [class*="middle aligned"].column:not(.row),
+.ui.grid > .row > [class*="middle aligned"].column {
+ flex-direction: column;
+ vertical-align: middle;
+ align-self: center !important;
+}
+
+.ui[class*="left aligned"].grid > .column,
+.ui[class*="left aligned"].grid > .row > .column,
+.ui.grid > [class*="left aligned"].row > .column,
+.ui.grid > [class*="left aligned"].column.column,
+.ui.grid > .row > [class*="left aligned"].column.column {
+ text-align: left;
+ align-self: inherit;
+}
+
+.ui[class*="center aligned"].grid > .column,
+.ui[class*="center aligned"].grid > .row > .column,
+.ui.grid > [class*="center aligned"].row > .column,
+.ui.grid > [class*="center aligned"].column.column,
+.ui.grid > .row > [class*="center aligned"].column.column {
+ text-align: center;
+ align-self: inherit;
+}
+.ui[class*="center aligned"].grid {
+ justify-content: center;
+}
+
+.ui[class*="right aligned"].grid > .column,
+.ui[class*="right aligned"].grid > .row > .column,
+.ui.grid > [class*="right aligned"].row > .column,
+.ui.grid > [class*="right aligned"].column.column,
+.ui.grid > .row > [class*="right aligned"].column.column {
+ text-align: right;
+ align-self: inherit;
+}
+
+.ui[class*="equal width"].grid > .column:not(.row),
+.ui[class*="equal width"].grid > .row > .column,
+.ui.grid > [class*="equal width"].row > .column {
+ display: inline-block;
+ flex-grow: 1;
+}
+.ui[class*="equal width"].grid > .wide.column,
+.ui[class*="equal width"].grid > .row > .wide.column,
+.ui.grid > [class*="equal width"].row > .wide.column {
+ flex-grow: 0;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui[class*="mobile reversed"].grid,
+ .ui[class*="mobile reversed"].grid > .row,
+ .ui.grid > [class*="mobile reversed"].row {
+ flex-direction: row-reverse;
+ }
+ .ui.stackable[class*="mobile reversed"] {
+ flex-direction: column-reverse;
+ }
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.stackable.grid {
+ width: auto;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ }
+ .ui.stackable.grid > .row > .wide.column,
+ .ui.stackable.grid > .wide.column,
+ .ui.stackable.grid > .column.grid > .column,
+ .ui.stackable.grid > .column.row > .column,
+ .ui.stackable.grid > .row > .column,
+ .ui.stackable.grid > .column:not(.row),
+ .ui.grid > .stackable.stackable.stackable.row > .column {
+ width: 100% !important;
+ margin: 0 !important;
+ box-shadow: none !important;
+ padding: 1rem;
+ }
+ .ui.stackable.grid:not(.vertically) > .row {
+ margin: 0;
+ padding: 0;
+ }
+
+ .ui.container > .ui.stackable.grid > .column,
+ .ui.container > .ui.stackable.grid > .row > .column {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ }
+
+ .ui.grid .ui.stackable.grid,
+ .ui.segment:not(.vertical) .ui.stackable.page.grid {
+ margin-left: -1rem !important;
+ margin-right: -1rem !important;
+ }
+}
+
+.ui.ui.ui.compact.grid > .column:not(.row),
+.ui.ui.ui.compact.grid > .row > .column {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+.ui.ui.ui.compact.grid > * {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+
+.ui.ui.ui.compact.grid > .row {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.ui.ui.ui.compact.grid > .column:not(.row) {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css
new file mode 100644
index 0000000..9cec5fc
--- /dev/null
+++ b/web_src/css/modules/header.css
@@ -0,0 +1,176 @@
+/* based on Fomantic UI header module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.header {
+ color: var(--color-text);
+ border: none;
+ margin: calc(2rem - 0.1428571428571429em) 0 1rem;
+ padding: 0;
+ font-family: var(--fonts-regular);
+ font-weight: var(--font-weight-medium);
+ line-height: 1.28571429;
+}
+
+.ui.header:first-child {
+ margin-top: -0.14285714em;
+}
+
+.ui.header:last-child {
+ margin-bottom: 0;
+}
+
+.ui.header .ui.label {
+ margin-left: 0.25rem;
+ vertical-align: middle;
+}
+
+.ui.header > .ui.label.compact {
+ margin-top: inherit;
+}
+
+.ui.header .sub.header {
+ display: block;
+ font-weight: var(--font-weight-normal);
+ padding: 0;
+ margin: 0;
+ font-size: 1rem;
+ line-height: 1.2;
+ color: var(--color-text-light-1);
+}
+
+.ui.header > i.icon {
+ display: table-cell;
+ opacity: 1;
+ font-size: 1.5em;
+ padding-top: 0;
+ vertical-align: middle;
+}
+
+.ui.header > i.icon:only-child {
+ display: inline-block;
+ padding: 0;
+ margin-right: 0.75rem;
+}
+
+.ui.header + p {
+ margin-top: 0;
+}
+
+h2.ui.header {
+ font-size: 1.71428571rem;
+}
+h2.ui.header .sub.header {
+ font-size: 1.14285714rem;
+}
+
+h4.ui.header {
+ font-size: 1.07142857rem;
+}
+h4.ui.header .sub.header {
+ font-size: 1rem;
+}
+
+.ui.sub.header {
+ padding: 0;
+ margin-bottom: 0.14285714rem;
+ font-weight: var(--font-weight-normal);
+ font-size: 0.85714286em;
+}
+
+.ui.icon.header svg {
+ width: 3em;
+ height: 3em;
+ float: none;
+ display: block;
+ line-height: var(--line-height-default);
+ padding: 0;
+ margin: 0 auto 0.5rem;
+ opacity: 1;
+}
+
+.ui.header:not(h1,h2,h3,h4,h5,h6) {
+ font-size: 1.28571429em;
+}
+
+.ui.attached.header {
+ position: relative;
+ background: var(--color-box-header);
+ padding: 0.78571429rem 1rem;
+ margin: 0 -1px;
+ border-radius: 0;
+ border: 1px solid var(--color-secondary);
+}
+
+.ui.attached:not(.top).header {
+ border-top: none;
+}
+
+.ui.top.attached.header {
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.bottom.attached.header {
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.attached.header:not(h1,h2,h3,h4,h5,h6) {
+ font-size: 1em;
+}
+
+/* fix misaligned right buttons on box headers */
+.ui.attached.header > .ui.right {
+ position: absolute;
+ right: 0.78571429rem;
+ top: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+}
+
+/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */
+.ui.attached.header > .ui.right .ui.tiny.button {
+ padding: 6px 10px;
+ font-weight: var(--font-weight-normal);
+}
+
+/* open dropdown menus to the left in right-attached headers */
+.ui.attached.header > .ui.right .ui.dropdown .menu {
+ right: 0;
+ left: auto;
+}
+
+/* if a .top.attached.header is followed by a .segment, add some margin */
+.ui.segments + .ui.top.attached.header,
+.ui.attached.segment + .ui.top.attached.header {
+ margin-top: 1rem;
+}
+
+.ui.dividing.header {
+ border-bottom-color: var(--color-secondary);
+}
+
+.ui.dividing.header .sub.header {
+ padding-bottom: 0.21428571rem;
+}
+
+.ui.dividing.header i.icon {
+ margin-bottom: 0;
+}
+
+.ui.error.header {
+ background: var(--color-error-bg) !important;
+ color: var(--color-error-text) !important;
+ border-color: var(--color-error-border) !important;
+}
+
+.ui.warning.header {
+ background: var(--color-warning-bg) !important;
+ color: var(--color-warning-text) !important;
+ border-color: var(--color-warning-border) !important;
+}
+
+.attention-header {
+ padding: 0.5em 0.75em !important;
+ color: var(--color-text) !important;
+}
diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css
new file mode 100644
index 0000000..18b785a
--- /dev/null
+++ b/web_src/css/modules/input.css
@@ -0,0 +1,197 @@
+/* based on Fomantic UI input module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.input {
+ position: relative;
+ font-weight: var(--font-weight-normal);
+ display: inline-flex;
+ color: var(--color-input-text);
+}
+.ui.input > input {
+ margin: 0;
+ max-width: 100%;
+ flex: 1 0 auto;
+ outline: none;
+ font-family: var(--fonts-regular);
+ padding: 0.67857143em 1em;
+ border: 1px solid var(--color-input-border);
+ color: var(--color-input-text);
+ border-radius: 0.28571429rem;
+ line-height: var(--line-height-default);
+ text-align: start;
+}
+
+.ui.disabled.input,
+.ui.input:not(.disabled) input[disabled] {
+ opacity: var(--opacity-disabled);
+}
+.ui.disabled.input > input,
+.ui.input:not(.disabled) input[disabled] {
+ pointer-events: none;
+}
+
+.ui.input.focus > input,
+.ui.input > input:focus {
+ border-color: var(--color-primary);
+}
+
+.ui.input.error > input {
+ background: var(--color-error-bg);
+ border-color: var(--color-error-border);
+ color: var(--color-error-text);
+}
+
+.ui.icon.input > i.icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: default;
+ position: absolute;
+ text-align: center;
+ top: 0;
+ right: 0;
+ margin: 0;
+ height: 100%;
+ width: 2.67142857em;
+ opacity: 0.5;
+ border-radius: 0 0.28571429rem 0.28571429rem 0;
+ pointer-events: none;
+ padding: 4px;
+}
+
+.ui.icon.input > i.icon.is-loading {
+ position: absolute !important;
+ height: 28px;
+ top: 4px;
+}
+
+.ui.icon.input > i.icon.is-loading > * {
+ visibility: hidden;
+}
+
+.ui.ui.ui.ui.icon.input > textarea,
+.ui.ui.ui.ui.icon.input > input {
+ padding-right: 2.67142857em;
+}
+.ui.icon.input > i.link.icon {
+ cursor: pointer;
+}
+.ui.icon.input > i.circular.icon {
+ top: 0.35em;
+ right: 0.5em;
+}
+
+.ui[class*="left icon"].input > i.icon {
+ right: auto;
+ left: 1px;
+ border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui[class*="left icon"].input > i.circular.icon {
+ right: auto;
+ left: 0.5em;
+}
+.ui.ui.ui.ui[class*="left icon"].input > textarea,
+.ui.ui.ui.ui[class*="left icon"].input > input {
+ padding-left: 2.67142857em;
+ padding-right: 1em;
+}
+
+.ui.icon.input > textarea:focus ~ .icon,
+.ui.icon.input > input:focus ~ .icon {
+ opacity: 1;
+}
+
+.ui.icon.input > textarea ~ i.icon {
+ height: 3em;
+}
+
+.ui.form .field.error > .ui.action.input > .ui.button,
+.ui.action.input.error > .ui.button {
+ border-top: 1px solid var(--color-error-border);
+ border-bottom: 1px solid var(--color-error-border);
+}
+
+.ui.action.input > .button,
+.ui.action.input > .buttons {
+ display: flex;
+ align-items: center;
+ flex: 0 0 auto;
+}
+.ui.action.input > .button,
+.ui.action.input > .buttons > .button {
+ padding-top: 0.78571429em;
+ padding-bottom: 0.78571429em;
+ margin: 0;
+}
+
+.ui.action.input:not([class*="left action"]) > input {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right-color: transparent;
+}
+
+.ui.action.input > .dropdown:first-child,
+.ui.action.input > .button:first-child,
+.ui.action.input > .buttons:first-child > .button {
+ border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui.action.input > .dropdown:not(:first-child),
+.ui.action.input > .button:not(:first-child),
+.ui.action.input > .buttons:not(:first-child) > .button {
+ border-radius: 0;
+}
+.ui.action.input > .dropdown:last-child,
+.ui.action.input > .button:last-child,
+.ui.action.input > .buttons:last-child > .button {
+ border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.fluid.input {
+ display: flex;
+}
+.ui.fluid.input > input {
+ width: 0 !important;
+}
+
+.ui.tiny.input {
+ font-size: 0.85714286em;
+}
+.ui.small.input {
+ font-size: 0.92857143em;
+}
+
+.ui.action.input .ui.ui.button {
+ border-color: var(--color-input-border);
+ padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */
+ padding-bottom: 0;
+}
+
+/* currently used for search bar dropdowns in repo search and explore code */
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection {
+ min-width: 10em;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) {
+ border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover {
+ border-color: var(--color-input-border);
+}
+.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible {
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+.ui.action.input:not([class*="left action"]) > input,
+.ui.action.input:not([class*="left action"]) > input:hover {
+ border-right: none;
+}
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection,
+.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button,
+.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover {
+ border-left-color: var(--color-primary);
+}
+.ui.action.input:not([class*="left action"]) > input:focus {
+ border-right-color: var(--color-primary);
+}
diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css
new file mode 100644
index 0000000..bc30baa
--- /dev/null
+++ b/web_src/css/modules/label.css
@@ -0,0 +1,303 @@
+/* based on Fomantic UI label module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.label {
+ display: inline-flex;
+ align-items: center;
+ gap: .25rem;
+ min-width: 0;
+ vertical-align: middle;
+ line-height: 1;
+ background: var(--color-label-bg);
+ color: var(--color-label-text);
+ padding: 0.3em 0.5em;
+ font-size: 0.85714286rem;
+ font-weight: var(--font-weight-medium);
+ border: 0 solid transparent;
+ border-radius: 0.28571429rem;
+ white-space: nowrap;
+}
+
+.ui.label:first-child {
+ margin-left: 0;
+}
+.ui.label:last-child {
+ margin-right: 0;
+}
+
+a.ui.label {
+ cursor: pointer;
+}
+
+.ui.label > a {
+ cursor: pointer;
+ color: inherit;
+ opacity: 0.75;
+}
+.ui.label > a:hover {
+ opacity: 1;
+}
+
+.ui.label > img {
+ width: auto;
+ vertical-align: middle;
+ height: 2.1666em;
+}
+
+.ui.label > .color-icon {
+ margin-left: 0;
+}
+
+.ui.label > .icon {
+ width: auto;
+ margin: 0 0.75em 0 0;
+}
+
+.ui.label > .detail {
+ display: inline-block;
+ vertical-align: top;
+ font-weight: var(--font-weight-medium);
+ margin-left: 1em;
+ opacity: 0.8;
+}
+.ui.label > .detail .icon {
+ margin: 0 0.25em 0 0;
+}
+
+.ui.label > .close.icon,
+.ui.label > .delete.icon {
+ cursor: pointer;
+ font-size: 0.92857143em;
+ opacity: 0.5;
+}
+.ui.label > .close.icon:hover,
+.ui.label > .delete.icon:hover {
+ opacity: 1;
+}
+
+.ui.label.left.icon > .close.icon,
+.ui.label.left.icon > .delete.icon {
+ margin: 0 0.5em 0 0;
+}
+.ui.label:not(.icon) > .close.icon,
+.ui.label:not(.icon) > .delete.icon {
+ margin: 0 0 0 0.5em;
+}
+
+.ui.header > .ui.label {
+ margin-top: -0.29165em;
+}
+
+a.ui.label:hover {
+ background: var(--color-label-hover-bg);
+ border-color: var(--color-label-hover-bg);
+ color: var(--color-label-text);
+}
+
+.ui.label.visible:not(.dropdown) {
+ display: inline-block !important;
+}
+
+.ugc-labels .item {
+ text-overflow: unset !important;
+}
+
+.ugc-labels .item .ui.label {
+ text-wrap: auto;
+ overflow-wrap: anywhere;
+}
+
+.ui.basic.label {
+ background: var(--color-button);
+ border: 1px solid var(--color-light-border);
+ color: var(--color-text-light);
+ padding: calc(0.5833em - 1px) calc(0.833em - 1px);
+}
+a.ui.basic.label:hover {
+ text-decoration: none;
+ color: var(--color-text);
+ border-color: var(--color-light-border);
+ background: var(--color-hover);
+}
+
+.ui.ui.ui.primary.label {
+ background: var(--color-primary);
+ border-color: var(--color-primary-dark-2);
+ color: var(--color-primary-contrast);
+}
+a.ui.ui.ui.primary.label:hover {
+ background: var(--color-primary-dark-3);
+ border-color: var(--color-primary-dark-3);
+ color: var(--color-primary-contrast);
+}
+.ui.ui.ui.basic.primary.label {
+ background: transparent;
+ border-color: var(--color-primary);
+ color: var(--color-primary);
+}
+a.ui.ui.ui.basic.primary.label:hover {
+ background: var(--color-hover);
+ border-color: var(--color-primary-dark-1);
+ color: var(--color-primary-dark-1);
+}
+
+.ui.ui.ui.red.label {
+ background: var(--color-red);
+ border-color: var(--color-red);
+ color: var(--color-white);
+}
+a.ui.ui.ui.red.label:hover {
+ background: var(--color-red-dark-1);
+ border-color: var(--color-red-dark-1);
+ color: var(--color-white);
+}
+.ui.ui.ui.basic.red.label {
+ background: transparent;
+ border-color: var(--color-red);
+ color: var(--color-red);
+}
+a.ui.ui.ui.basic.red.label:hover {
+ background: transparent;
+ border-color: var(--color-red-dark-1);
+ color: var(--color-red-dark-1);
+}
+
+.ui.ui.ui.orange.label {
+ background: var(--color-orange);
+ border-color: var(--color-orange);
+ color: var(--color-white);
+}
+a.ui.ui.ui.orange.label:hover {
+ background: var(--color-orange-dark-1);
+ border-color: var(--color-orange-dark-1);
+ color: var(--color-white);
+}
+.ui.ui.ui.basic.orange.label {
+ background: transparent;
+ border-color: var(--color-orange);
+ color: var(--color-orange);
+}
+a.ui.ui.ui.basic.orange.label:hover {
+ background: transparent;
+ border-color: var(--color-orange-dark-1);
+ color: var(--color-orange-dark-1);
+}
+
+.ui.ui.ui.yellow.label {
+ background: var(--color-yellow);
+ border-color: var(--color-yellow);
+ color: var(--color-white);
+}
+a.ui.ui.ui.yellow.label:hover {
+ background: var(--color-yellow-dark-1);
+ border-color: var(--color-yellow-dark-1);
+ color: var(--color-white);
+}
+.ui.ui.ui.basic.yellow.label {
+ background: transparent;
+ border-color: var(--color-yellow);
+ color: var(--color-yellow);
+}
+a.ui.ui.ui.basic.yellow.label:hover {
+ background: transparent;
+ border-color: var(--color-yellow-dark-1);
+ color: var(--color-yellow-dark-1);
+}
+.ui.ui.ui.olive.label {
+ background: var(--color-olive);
+ border-color: var(--color-olive);
+ color: var(--color-white);
+}
+
+.ui.ui.ui.green.label {
+ background: var(--color-green);
+ border-color: var(--color-green);
+ color: var(--color-white);
+}
+a.ui.ui.ui.green.label:hover {
+ background: var(--color-green-dark-1);
+ border-color: var(--color-green-dark-1);
+ color: var(--color-white);
+}
+.ui.ui.ui.basic.green.label {
+ background: transparent;
+ border-color: var(--color-green);
+ color: var(--color-green);
+}
+a.ui.ui.ui.basic.green.label:hover {
+ background: transparent;
+ border-color: var(--color-green-dark-1);
+ color: var(--color-green-dark-1);
+}
+
+.ui.ui.ui.purple.label {
+ background: var(--color-purple);
+ border-color: var(--color-purple);
+ color: var(--color-white);
+}
+a.ui.ui.ui.purple.label:hover {
+ background: var(--color-purple-dark-1);
+ border-color: var(--color-purple-dark-1);
+ color: var(--color-white);
+}
+.ui.ui.ui.basic.purple.label {
+ background: transparent;
+ border-color: var(--color-purple);
+ color: var(--color-purple);
+}
+a.ui.ui.ui.basic.purple.label:hover {
+ background: transparent;
+ border-color: var(--color-purple-dark-1);
+ color: var(--color-purple-dark-1);
+}
+
+.ui.ui.ui.grey.label {
+ background: var(--color-label-bg);
+ border-color: var(--color-label-bg);
+ color: var(--color-label-text);
+}
+a.ui.ui.ui.grey.label:hover {
+ background: var(--color-label-hover-bg);
+ border-color: var(--color-label-hover-bg);
+ color: var(--color-white);
+}
+.ui.ui.ui.basic.grey.label {
+ background: transparent;
+ border-color: var(--color-label-bg);
+ color: var(--color-label-text);
+}
+a.ui.ui.ui.basic.grey.label:hover {
+ background: transparent;
+ border-color: var(--color-label-hover-bg);
+ color: var(--color-label-hover-bg);
+}
+
+.ui.horizontal.label {
+ margin: 0 0.5em 0 0;
+ padding: 0.4em 0.833em;
+ min-width: 3em;
+ text-align: center;
+}
+
+.ui.circular.label {
+ min-width: 2em;
+ min-height: 2em;
+ padding: 0.5em !important;
+ line-height: 1;
+ text-align: center;
+ border-radius: 500rem;
+ justify-content: center;
+}
+
+.ui.mini.label {
+ font-size: 0.64285714rem;
+}
+.ui.tiny.label {
+ font-size: 0.71428571rem;
+}
+.ui.small.label {
+ font-size: 0.78571429rem;
+}
+.ui.large.label {
+ font-size: 1rem;
+}
diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css
new file mode 100644
index 0000000..32c71e8
--- /dev/null
+++ b/web_src/css/modules/list.css
@@ -0,0 +1,193 @@
+/* based on Fomantic UI list module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.list {
+ list-style-type: none;
+ margin: 1em 0;
+ padding: 0;
+ font-size: 1em;
+}
+
+.ui.list:first-child {
+ margin-top: 0;
+ padding-top: 0;
+}
+
+.ui.list:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+.ui.list > .item,
+.ui.list .list > .item {
+ display: list-item;
+ table-layout: fixed;
+ list-style-type: none;
+ list-style-position: outside;
+}
+
+.ui.list > .list > .item::after,
+.ui.list > .item::after {
+ content: "";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.ui.list .list:not(.icon) {
+ clear: both;
+ margin: 0;
+ padding: 0.75em 0 0.25em 0.5em;
+}
+
+.ui.list .list > .item {
+ padding: 0.14285714em 0;
+}
+
+.ui.list .list > .item > i.icon,
+.ui.list > .item > i.icon {
+ display: table-cell;
+ min-width: 1.55em;
+ padding-top: 0;
+ transition: color 0.1s ease;
+ padding-right: 0.28571429em;
+ vertical-align: top;
+}
+.ui.list .list > .item > i.icon:only-child,
+.ui.list > .item > i.icon:only-child {
+ display: inline-block;
+ min-width: auto;
+ vertical-align: top;
+}
+
+.ui.list .list > .item > .image,
+.ui.list > .item > .image {
+ display: table-cell;
+ background-color: transparent;
+ vertical-align: top;
+}
+.ui.list .list > .item > .image:not(:only-child):not(img),
+.ui.list > .item > .image:not(:only-child):not(img) {
+ padding-right: 0.5em;
+}
+.ui.list .list > .item > .image img,
+.ui.list > .item > .image img {
+ vertical-align: top;
+}
+.ui.list .list > .item > img.image,
+.ui.list .list > .item > .image:only-child,
+.ui.list > .item > img.image,
+.ui.list > .item > .image:only-child {
+ display: inline-block;
+}
+
+.ui.list .list > .item > .content,
+.ui.list > .item > .content {
+ color: var(--color-text);
+}
+.ui.list .list > .item > .image + .content,
+.ui.list .list > .item > i.icon + .content,
+.ui.list > .item > .image + .content,
+.ui.list > .item > i.icon + .content {
+ display: table-cell;
+ width: 100%;
+ padding: 0 0 0 0.5em;
+ vertical-align: top;
+}
+.ui.list .list > .item > img.image + .content,
+.ui.list > .item > img.image + .content {
+ display: inline-block;
+ width: auto;
+}
+.ui.list .list > .item > .content > .list,
+.ui.list > .item > .content > .list {
+ margin-left: 0;
+ padding-left: 0;
+}
+
+.ui.list .list > .item .header,
+.ui.list > .item .header {
+ display: block;
+ margin: 0;
+ font-family: var(--fonts-regular);
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text-dark);
+}
+
+.ui.list .list > .item .description,
+.ui.list > .item .description {
+ display: block;
+ color: var(--color-text);
+}
+
+.ui.list > .item a,
+.ui.list .list > .item a {
+ cursor: pointer;
+}
+
+.ui.list .list > .item [class*="right floated"],
+.ui.list > .item [class*="right floated"] {
+ float: right;
+ margin: 0 0 0 1em;
+}
+
+.ui.menu .ui.list > .item,
+.ui.menu .ui.list .list > .item {
+ display: list-item;
+ table-layout: fixed;
+ background-color: transparent;
+ list-style-type: none;
+ list-style-position: outside;
+ padding: 0.21428571em 0;
+}
+.ui.menu .ui.list .list > .item::before,
+.ui.menu .ui.list > .item::before {
+ border: none;
+ background: none;
+}
+.ui.menu .ui.list .list > .item:first-child,
+.ui.menu .ui.list > .item:first-child {
+ padding-top: 0;
+}
+.ui.menu .ui.list .list > .item:last-child,
+.ui.menu .ui.list > .item:last-child {
+ padding-bottom: 0;
+}
+
+.ui.list .list > .disabled.item,
+.ui.list > .disabled.item {
+ pointer-events: none;
+ opacity: var(--opacity-disabled);
+}
+
+.ui.list .list > a.item:hover > .icons,
+.ui.list > a.item:hover > .icons,
+.ui.list .list > a.item:hover > i.icon,
+.ui.list > a.item:hover > i.icon {
+ color: var(--color-text-dark);
+}
+
+.ui.divided.list > .item {
+ border-top: 1px solid var(--color-secondary);
+}
+.ui.divided.list .list > .item {
+ border-top: none;
+}
+.ui.divided.list .item .list > .item {
+ border-top: none;
+}
+.ui.divided.list .list > .item:first-child,
+.ui.divided.list > .item:first-child {
+ border-top: none;
+}
+.ui.divided.list .list > .item:first-child {
+ border-top-width: 1px;
+}
+
+.ui.relaxed.list > .item:not(:first-child) {
+ padding-top: 0.42857143em;
+}
+.ui.relaxed.list > .item:not(:last-child) {
+ padding-bottom: 0.42857143em;
+}
diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css
new file mode 100644
index 0000000..c62dbdd
--- /dev/null
+++ b/web_src/css/modules/message.css
@@ -0,0 +1,114 @@
+/* based on Fomantic UI message module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.message {
+ background: var(--color-box-body);
+ color: var(--color-text);
+ border: 1px solid var(--color-secondary);
+ position: relative;
+ min-height: 1em;
+ margin: 1em 0;
+ padding: 1em 1.5em;
+ border-radius: var(--border-radius);
+}
+
+.ui.message:first-child {
+ margin-top: 0;
+}
+
+.ui.message:last-child {
+ margin-bottom: 0;
+}
+
+.ui.attached.message {
+ margin-bottom: -1px;
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+ margin-left: -1px;
+ margin-right: -1px;
+}
+
+.ui.attached + .ui.attached.message:not(.top):not(.bottom) {
+ margin-top: -1px;
+ border-radius: 0;
+}
+
+.ui.bottom.attached.message {
+ margin-top: -1px;
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.ui.bottom.attached.message:not(:last-child) {
+ margin-bottom: 1em;
+}
+
+.ui.info.message .header,
+.ui.blue.message .header {
+ color: var(--color-blue);
+}
+
+.ui.info.message,
+.ui.attached.info.message,
+.ui.blue.message,
+.ui.attached.blue.message {
+ background: var(--color-info-bg);
+ color: var(--color-info-text);
+ border-color: var(--color-info-border);
+}
+
+.ui.success.message .header,
+.ui.positive.message .header,
+.ui.green.message .header {
+ color: var(--color-green);
+}
+
+.ui.success.message,
+.ui.attached.success.message,
+.ui.positive.message,
+.ui.attached.positive.message {
+ background: var(--color-success-bg);
+ color: var(--color-success-text);
+ border-color: var(--color-success-border);
+}
+
+.ui.error.message .header,
+.ui.negative.message .header,
+.ui.red.message .header {
+ color: var(--color-red);
+}
+
+.ui.error.message,
+.ui.attached.error.message,
+.ui.red.message,
+.ui.attached.red.message,
+.ui.negative.message,
+.ui.attached.negative.message {
+ background: var(--color-error-bg);
+ color: var(--color-error-text);
+ border-color: var(--color-error-border);
+}
+
+.ui.warning.message .header,
+.ui.yellow.message .header {
+ color: var(--color-yellow);
+}
+
+.ui.warning.message,
+.ui.attached.warning.message,
+.ui.yellow.message,
+.ui.attached.yellow.message {
+ background: var(--color-warning-bg);
+ color: var(--color-warning-text);
+ border-color: var(--color-warning-border);
+}
+
+.ui.message > .close.icon {
+ cursor: pointer;
+ position: absolute;
+ top: 9px;
+ right: 9px;
+ opacity: .7;
+}
+
+.ui.message > .close.icon:hover {
+ opacity: 1;
+}
diff --git a/web_src/css/modules/modal.css b/web_src/css/modules/modal.css
new file mode 100644
index 0000000..54a4ef8
--- /dev/null
+++ b/web_src/css/modules/modal.css
@@ -0,0 +1,86 @@
+.ui.modal.g-modal-confirm {
+ max-width: min(800px, 90vw);
+ width: fit-content;
+}
+
+.ui.modal.g-modal-confirm > .inside.close.icon {
+ padding: 0;
+ width: 1em;
+ height: 1em;
+ top: 1.2em;
+}
+
+.ui.modal > .close.icon[height="16"] {
+ top: 0.7em; /* fomantic uses absolute layout, so if we have special icon size, it needs this trick to align vertically */
+ color: var(--color-text-dark);
+}
+
+.ui.modal > .header {
+ /* can't use display:flex, because some headers have space-separated elements, eg: delete branch modal */
+ color: var(--color-text-dark);
+ background: var(--color-body);
+ border-color: var(--color-secondary);
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+ vertical-align: middle;
+}
+
+.ui.modal > .header .svg {
+ vertical-align: middle;
+ display: inline-block;
+}
+
+.ui.modal {
+ background: var(--color-body);
+ box-shadow: 1px 3px 3px 0 var(--color-shadow), 1px 3px 15px 2px var(--color-shadow);
+}
+
+/* Gitea sometimes use a form in a modal dialog, then the "positive" button could submit the form directly
+Fomantic UI only supports the layout: <div .modal><div .content/><div .actions/></div>
+However, Gitea uses the following layouts:
+* <div .modal><div .content><div .actions/></div></div>
+* <div .modal><form><div .content/><div .actions/></form></div>
+* <div .modal><div .content><form><div .actions/></form></div></div>
+* <div .modal><div .content></div><form><div .actions/></form></div>
+* ...
+These inconsistent layouts should be refactored to simple ones.
+*/
+
+.ui.modal > .content,
+.ui.modal form > .content {
+ padding: 1.5em;
+ background: var(--color-body);
+}
+
+.ui.modal > .actions,
+.ui.modal .content + .actions,
+.ui.modal .content + form > .actions {
+ background: var(--color-secondary-bg);
+ border-color: var(--color-secondary);
+ padding: 1rem;
+ text-align: right;
+}
+
+.ui.modal .content > .actions {
+ padding-top: 1em; /* if the "actions" is in the "content", some paddings are already added by the "content" */
+ text-align: right;
+}
+
+/* positive/negative action buttons */
+.ui.modal .actions > .ui.button {
+ display: inline-flex;
+ align-items: center;
+ padding: 10px 12px 10px 10px;
+ margin-right: 0;
+}
+
+.ui.modal .actions > .ui.button.danger {
+ display: block;
+ width: 100%;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.ui.modal .actions > .ui.button .svg {
+ margin-right: 5px;
+}
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
new file mode 100644
index 0000000..02d470f
--- /dev/null
+++ b/web_src/css/modules/navbar.css
@@ -0,0 +1,147 @@
+#navbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: var(--color-nav-bg);
+ border-bottom: 1px solid var(--color-secondary);
+ margin: 0 !important;
+ padding: 0 10px;
+}
+
+#navbar,
+#navbar .navbar-left,
+#navbar .navbar-right {
+ min-height: 49px; /* +1px border-bottom */
+}
+
+#navbar .navbar-left,
+#navbar .navbar-right {
+ margin: 0;
+ display: flex;
+ align-items: center;
+}
+
+#navbar-logo {
+ margin: 0;
+}
+
+#navbar .item {
+ min-height: 36px;
+ min-width: 36px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ display: flex;
+}
+
+#navbar > .menu > .item {
+ color: var(--color-nav-text);
+}
+
+#navbar .dropdown .item {
+ justify-content: stretch;
+}
+
+#navbar a.item:hover, #navbar a.item:focus,
+#navbar button.item:hover, #navbar button.item:focus {
+ background: var(--color-nav-hover-bg);
+}
+
+#navbar .secondary.menu > .item > .svg,
+#navbar .right.menu > .item > .svg {
+ margin-right: 0;
+}
+
+@media (max-width: 767.98px) {
+ #navbar {
+ align-items: stretch;
+ }
+ /* hide all items */
+ #navbar .item {
+ display: none;
+ }
+ #navbar #navbar-logo {
+ display: flex;
+ }
+ /* show the first navbar item (logo and its mobile right items) */
+ #navbar .navbar-left {
+ flex: 1;
+ display: flex;
+ justify-content: space-between;
+ }
+ #navbar .navbar-mobile-right {
+ display: flex;
+ margin-left: auto !important;
+ width: auto !important;
+ }
+ #navbar .navbar-mobile-right > .item {
+ display: flex;
+ width: auto !important;
+ }
+ /* show items if the navbar is open */
+ #navbar.navbar-menu-open {
+ padding-bottom: 8px;
+ }
+ #navbar.navbar-menu-open,
+ #navbar.navbar-menu-open .navbar-right {
+ flex-direction: column;
+ }
+ #navbar.navbar-menu-open .navbar-left {
+ display: flex;
+ flex-wrap: wrap;
+ }
+ #navbar.navbar-menu-open .item {
+ display: flex;
+ width: 100%;
+ margin: 0;
+ }
+ #navbar.navbar-menu-open .navbar-left #navbar-logo {
+ justify-content: flex-start;
+ width: auto;
+ }
+ #navbar.navbar-menu-open .navbar-left .navbar-mobile-right {
+ justify-content: flex-end;
+ width: 50%;
+ min-height: 48px;
+ }
+ #navbar #mobile-notifications-icon {
+ margin-right: 6px !important;
+ }
+}
+
+#navbar a.item .notification_count {
+ color: var(--color-nav-bg);
+ padding: 0 3.75px;
+ font-size: 12px;
+ line-height: 12px;
+ font-weight: var(--font-weight-bold);
+}
+
+#navbar a.item:hover .notification_count,
+#navbar a.item:hover .header-stopwatch-dot {
+ border-color: var(--color-nav-hover-bg);
+}
+
+#navbar a.item .notification_count,
+#navbar a.item .header-stopwatch-dot {
+ background: var(--color-primary);
+ border: 2px solid var(--color-nav-bg);
+ position: absolute;
+ left: 6px;
+ top: -9px;
+ min-width: 17px;
+ height: 17px;
+ border-radius: 11px; /* (height + 2 * borderThickness) / 2 */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1; /* prevent menu button background from overlaying icon */
+}
+
+.secondary-nav {
+ background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */
+}
+
+.issue-navbar {
+ display: flex;
+ justify-content: space-between;
+}
diff --git a/web_src/css/modules/normalize.css b/web_src/css/modules/normalize.css
new file mode 100644
index 0000000..63fb04a
--- /dev/null
+++ b/web_src/css/modules/normalize.css
@@ -0,0 +1,243 @@
+/*
+ This is copy of modern-normalize with these changes done:
+
+ - Remove html font-family, we set our own
+ - Remove html tab-size, we set our own
+ - Remove b,strong font-weight, we set our own
+ - Remove b,code,samp,pre font-size, we set our own
+*/
+
+/*! modern-normalize v2.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */
+
+/*
+Document
+========
+*/
+
+/**
+Use a better box model (opinionated).
+*/
+
+*,
+::before,
+::after {
+ box-sizing: border-box;
+}
+
+html {
+ line-height: normal; /* 1. (not following the "modern-normalize") Do not change the browser's default line-height, the default value is font-dependent and roughly 1.2 */
+ -webkit-text-size-adjust: 100%; /* 2. Prevent adjustments of font size after orientation changes in iOS. */
+}
+
+/*
+Sections
+========
+*/
+
+body {
+ margin: 0; /* Remove the margin in all browsers. */
+}
+
+/*
+Grouping content
+================
+*/
+
+/**
+1. Add the correct height in Firefox.
+2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+*/
+
+hr {
+ height: 0; /* 1 */
+ color: inherit; /* 2 */
+}
+
+/*
+Text-level semantics
+====================
+*/
+
+/**
+Add the correct text decoration in Chrome, Edge, and Safari.
+*/
+
+abbr[title] {
+ text-decoration: underline dotted;
+}
+
+/**
+Add the correct font size in all browsers.
+*/
+
+small {
+ font-size: 80%;
+}
+
+/**
+Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
+*/
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/*
+Tabular data
+============
+*/
+
+/**
+1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+2. Correct table border color inheritance in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+*/
+
+table {
+ text-indent: 0; /* 1 */
+ border-color: inherit; /* 2 */
+}
+
+/*
+Forms
+=====
+*/
+
+/**
+1. Change the font styles in all browsers.
+2. Remove the margin in Firefox and Safari.
+*/
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+Remove the inheritance of text transform in Edge and Firefox.
+*/
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+Correct the inability to style clickable types in iOS and Safari.
+*/
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+Remove the inner border and padding in Firefox.
+*/
+
+::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+Restore the focus styles unset by the previous rule.
+*/
+
+:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+Remove the additional ':invalid' styles in Firefox.
+See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737
+*/
+
+:-moz-ui-invalid {
+ box-shadow: none;
+}
+
+/**
+Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
+*/
+
+legend {
+ padding: 0;
+}
+
+/**
+Add the correct vertical alignment in Chrome and Firefox.
+*/
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+Correct the cursor style of increment and decrement buttons in Safari.
+*/
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+1. Correct the odd appearance in Chrome and Safari.
+2. Correct the outline style in Safari.
+*/
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+Remove the inner padding in Chrome and Safari on macOS.
+*/
+
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Change font properties to 'inherit' in Safari.
+*/
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/*
+Interactive
+===========
+*/
+
+/*
+Add the correct display in Chrome and Safari.
+*/
+
+summary {
+ display: list-item;
+}
diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css
new file mode 100644
index 0000000..04543f0
--- /dev/null
+++ b/web_src/css/modules/segment.css
@@ -0,0 +1,203 @@
+/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.segment {
+ position: relative;
+ margin: 1rem 0;
+ padding: 1em;
+ border-radius: 0.28571429rem;
+ border: 1px solid var(--color-secondary);
+ background: var(--color-box-body);
+ color: var(--color-text);
+}
+.ui.segment:first-child {
+ margin-top: 0;
+}
+.ui.segment:last-child {
+ margin-bottom: 0;
+}
+
+.ui.grid.segment {
+ margin: 1rem 0;
+ border-radius: 0.28571429rem;
+}
+
+.ui.segment.tab:last-child {
+ margin-bottom: 1rem;
+}
+
+.ui.segments {
+ flex-direction: column;
+ position: relative;
+ margin: 1rem 0;
+ border: 1px solid var(--color-secondary);
+ border-radius: 0.28571429rem;
+ background: var(--color-box-body);
+ color: var(--color-text);
+}
+.ui.segments:first-child {
+ margin-top: 0;
+}
+.ui.segments:last-child {
+ margin-bottom: 0;
+}
+
+.ui.segments > .segment {
+ top: 0;
+ bottom: 0;
+ border-radius: 0;
+ margin: 0;
+ width: auto;
+ box-shadow: none;
+ border: none;
+ border-top: 1px solid var(--color-secondary);
+}
+.ui.segments:not(.horizontal) > .segment:first-child {
+ top: 0;
+ bottom: 0;
+ border-top: none;
+ margin-top: 0;
+ margin-bottom: 0;
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.segments:not(.horizontal) > .segment:last-child {
+ top: 0;
+ bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.segments:not(.horizontal) > .segment:only-child {
+ border-radius: 0.214285717rem;
+}
+.ui.segments:not(.horizontal) > .segment:has(~ .tw-hidden) { /* workaround issue with :last-child ignoring hidden elements */
+ border-radius: 0.28571429rem;
+}
+
+.ui.segments > .ui.segments {
+ border-top: 1px solid var(--color-secondary);
+ margin: 1rem;
+}
+.ui.segments > .segments:first-child {
+ border-top: none;
+}
+.ui.segments > .segment + .segments:not(.horizontal) {
+ margin-top: 0;
+}
+
+.ui.horizontal.segments {
+ display: flex;
+ flex-direction: row;
+ background-color: transparent;
+ padding: 0;
+ margin: 1rem 0;
+ border-radius: 0.28571429rem;
+ border: 1px solid var(--color-secondary);
+}
+
+.ui.horizontal.segments > .segment {
+ margin: 0;
+ min-width: 0;
+ border-radius: 0;
+ border: none;
+ box-shadow: none;
+ border-left: 1px solid var(--color-secondary);
+}
+
+.ui.segments > .horizontal.segments:first-child {
+ border-top: none;
+}
+.ui.horizontal.segments:not(.stackable) > .segment:first-child {
+ border-left: none;
+}
+.ui.horizontal.segments > .segment:first-child {
+ border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+.ui.horizontal.segments > .segment:last-child {
+ border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.clearing.segment::after {
+ content: "";
+ display: block;
+ clear: both;
+}
+
+.ui[class*="left aligned"].segment {
+ text-align: left;
+}
+.ui[class*="center aligned"].segment {
+ text-align: center;
+}
+
+.ui.secondary.segment {
+ background: var(--color-secondary-bg);
+ color: var(--color-text-light);
+}
+
+.ui.attached.segment {
+ top: 0;
+ bottom: 0;
+ border-radius: 0;
+ margin: 0 -1px;
+ width: calc(100% + 2px);
+ max-width: calc(100% + 2px);
+ box-shadow: none;
+ border: 1px solid var(--color-secondary);
+ background: var(--color-box-body);
+ color: var(--color-text);
+}
+.ui.attached:not(.message) + .ui.attached.segment:not(.top) {
+ border-top: none;
+}
+
+.ui.attached.segment:has(+ .ui[class*="top attached"].header),
+.ui.attached.segment:last-child {
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui[class*="top attached"].segment {
+ bottom: 0;
+ margin-bottom: 0;
+ top: 0;
+ margin-top: 1rem;
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+.ui.segment[class*="top attached"]:first-child {
+ margin-top: 0;
+}
+
+.ui.segment[class*="bottom attached"] {
+ bottom: 0;
+ margin-top: 0;
+ top: 0;
+ margin-bottom: 1rem;
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+.ui.segment[class*="bottom attached"]:last-child {
+ margin-bottom: 1rem;
+}
+
+.ui.fitted.segment:not(.horizontally) {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+.ui.fitted.segment:not(.vertically) {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.ui.segments .segment,
+.ui.segment {
+ font-size: 1rem;
+}
+
+.ui.error.segment {
+ border-color: var(--color-error-border) !important;
+}
+
+.ui.warning.segment {
+ border-color: var(--color-warning-border) !important;
+}
diff --git a/web_src/css/modules/select.css b/web_src/css/modules/select.css
new file mode 100644
index 0000000..1d7d749
--- /dev/null
+++ b/web_src/css/modules/select.css
@@ -0,0 +1,25 @@
+.gitea-select {
+ position: relative;
+}
+
+.gitea-select select {
+ appearance: none; /* hide default triangle */
+}
+
+/* ::before and ::after pseudo elements don't work on select elements,
+ so we need to put it on the parent. */
+.gitea-select::after {
+ position: absolute;
+ top: 12px;
+ right: 8px;
+ pointer-events: none;
+ content: "";
+ width: 14px;
+ height: 14px;
+ mask-size: cover;
+ -webkit-mask-size: cover;
+ mask-image: var(--octicon-chevron-right);
+ -webkit-mask-image: var(--octicon-chevron-right);
+ transform: rotate(90deg); /* point the chevron down */
+ background: currentcolor;
+}
diff --git a/web_src/css/modules/svg.css b/web_src/css/modules/svg.css
new file mode 100644
index 0000000..b3060bd
--- /dev/null
+++ b/web_src/css/modules/svg.css
@@ -0,0 +1,41 @@
+.svg {
+ display: inline-block;
+ vertical-align: text-top;
+ fill: currentcolor;
+}
+
+.middle .svg {
+ vertical-align: middle;
+}
+
+/* prevent SVGs from shrinking, like in space-starved flexboxes. the sizes
+ here are cherry-picked for our use cases, feel free to add more. after
+ https://developer.mozilla.org/en-US/docs/Web/CSS/attr#type-or-unit is
+ supported in browsers, use `attr(width px)` instead for a generic
+ solution. */
+
+.svg[height="12"] { min-height: 12px; }
+.svg[height="13"] { min-height: 13px; }
+.svg[height="14"] { min-height: 14px; }
+.svg[height="15"] { min-height: 15px; }
+.svg[height="16"] { min-height: 16px; }
+.svg[height="18"] { min-height: 18px; }
+.svg[height="20"] { min-height: 20px; }
+.svg[height="22"] { min-height: 22px; }
+.svg[height="24"] { min-height: 24px; }
+.svg[height="36"] { min-height: 36px; }
+.svg[height="48"] { min-height: 48px; }
+.svg[height="56"] { min-height: 56px; }
+
+.svg[width="12"] { min-width: 12px; }
+.svg[width="13"] { min-width: 13px; }
+.svg[width="14"] { min-width: 14px; }
+.svg[width="15"] { min-width: 15px; }
+.svg[width="16"] { min-width: 16px; }
+.svg[width="18"] { min-width: 18px; }
+.svg[width="20"] { min-width: 20px; }
+.svg[width="22"] { min-width: 22px; }
+.svg[width="24"] { min-width: 24px; }
+.svg[width="36"] { min-width: 36px; }
+.svg[width="48"] { min-width: 48px; }
+.svg[width="56"] { min-width: 56px; }
diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css
new file mode 100644
index 0000000..4fb9d42
--- /dev/null
+++ b/web_src/css/modules/table.css
@@ -0,0 +1,385 @@
+/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any
+ unused rules here after refactoring, please remove them. */
+
+.ui.table {
+ width: 100%;
+ margin: 1em 0;
+ border: 1px solid var(--color-secondary);
+ border-radius: 0.28571429rem;
+ vertical-align: middle;
+ border-collapse: separate;
+ border-spacing: 0;
+ color: var(--color-text);
+ background: var(--color-box-body);
+ border-color: var(--color-secondary);
+ text-align: start;
+}
+
+.ui.table:first-child {
+ margin-top: 0;
+}
+.ui.table:last-child {
+ margin-bottom: 0;
+}
+.ui.table > thead,
+.ui.table > tbody {
+ text-align: inherit;
+ vertical-align: inherit;
+}
+
+.ui.table > thead > tr > th {
+ background: var(--color-box-header);
+ text-align: inherit;
+ color: var(--color-text);
+ padding: 6px 5px;
+ vertical-align: inherit;
+ font-weight: var(--font-weight-normal);
+ border-bottom: 1px solid var(--color-secondary);
+ border-left: none;
+}
+.ui.table > thead > tr > th:first-child {
+ border-left: none;
+}
+.ui.table > thead > tr:first-child > th:first-child {
+ border-radius: 0.28571429rem 0 0;
+}
+.ui.table > thead > tr:first-child > th:last-child {
+ border-radius: 0 0.28571429rem 0 0;
+}
+.ui.table > thead > tr:first-child > th:only-child {
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.table > tfoot > tr > th,
+.ui.table > tfoot > tr > td {
+ border-top: 1px solid var(--color-secondary);
+ background: var(--color-box-body);
+ text-align: inherit;
+ color: var(--color-text);
+ padding: 0.78571429em;
+ vertical-align: inherit;
+ font-weight: var(--font-weight-normal);
+}
+.ui.table > tfoot > tr > th:first-child,
+.ui.table > tfoot > tr > td:first-child {
+ border-left: none;
+}
+.ui.table > tfoot > tr:first-child > th:first-child,
+.ui.table > tfoot > tr:first-child > td:first-child {
+ border-radius: 0 0 0 0.28571429rem;
+}
+.ui.table > tfoot > tr:first-child > th:last-child,
+.ui.table > tfoot > tr:first-child > td:last-child {
+ border-radius: 0 0 0.28571429rem;
+}
+.ui.table > tfoot > tr:first-child > th:only-child,
+.ui.table > tfoot > tr:first-child > td:only-child {
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.table > tr > td,
+.ui.table > tbody > tr > td {
+ border-top: 1px solid var(--color-secondary-alpha-50);
+ padding: 6px 5px;
+ text-align: inherit;
+}
+.ui.table > tr:first-child > td,
+.ui.table > tbody > tr:first-child > td {
+ border-top: none;
+}
+
+.ui.table.segment {
+ padding: 0;
+}
+.ui.table.segment::after {
+ display: none;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.table:not(.unstackable) {
+ width: 100%;
+ padding: 0;
+ }
+ .ui.table:not(.unstackable) > thead,
+ .ui.table:not(.unstackable) > thead > tr,
+ .ui.table:not(.unstackable) > tfoot,
+ .ui.table:not(.unstackable) > tfoot > tr,
+ .ui.table:not(.unstackable) > tbody,
+ .ui.table:not(.unstackable) > tr,
+ .ui.table:not(.unstackable) > tbody > tr,
+ .ui.table:not(.unstackable) > tr > th,
+ .ui.table:not(.unstackable) > thead > tr > th,
+ .ui.table:not(.unstackable) > tbody > tr > th,
+ .ui.table:not(.unstackable) > tfoot > tr > th,
+ .ui.table:not(.unstackable) > tr > td,
+ .ui.table:not(.unstackable) > tbody > tr > td,
+ .ui.table:not(.unstackable) > tfoot > tr > td {
+ display: block !important;
+ width: auto !important;
+ }
+ .ui.table:not(.unstackable) > thead {
+ display: block;
+ }
+ .ui.table:not(.unstackable) > tfoot {
+ display: block;
+ }
+ .ui.ui.ui.ui.table:not(.unstackable) > tr,
+ .ui.ui.ui.ui.table:not(.unstackable) > thead > tr,
+ .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr,
+ .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr {
+ padding-top: 1em;
+ padding-bottom: 1em;
+ }
+ .ui.ui.ui.ui.table:not(.unstackable) > tr > th,
+ .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th,
+ .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th,
+ .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th,
+ .ui.ui.ui.ui.table:not(.unstackable) > tr > td,
+ .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td,
+ .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td {
+ background: none;
+ border: none;
+ padding: 0.25em 0.75em;
+ }
+ .ui.table:not(.unstackable) > tr > th:first-child,
+ .ui.table:not(.unstackable) > thead > tr > th:first-child,
+ .ui.table:not(.unstackable) > tbody > tr > th:first-child,
+ .ui.table:not(.unstackable) > tfoot > tr > th:first-child,
+ .ui.table:not(.unstackable) > tr > td:first-child,
+ .ui.table:not(.unstackable) > tbody > tr > td:first-child,
+ .ui.table:not(.unstackable) > tfoot > tr > td:first-child {
+ font-weight: var(--font-weight-normal);
+ }
+}
+
+.ui.table[class*="left aligned"],
+.ui.table [class*="left aligned"] {
+ text-align: left;
+}
+
+.ui.table[class*="center aligned"],
+.ui.table [class*="center aligned"] {
+ text-align: center;
+}
+
+.ui.table[class*="right aligned"],
+.ui.table [class*="right aligned"] {
+ text-align: right;
+}
+
+.ui.table[class*="top aligned"],
+.ui.table [class*="top aligned"] {
+ vertical-align: top;
+}
+
+.ui.table[class*="middle aligned"],
+.ui.table [class*="middle aligned"] {
+ vertical-align: middle;
+}
+
+.ui.table th.collapsing,
+.ui.table td.collapsing {
+ width: 1px;
+ white-space: nowrap;
+}
+
+.ui.fixed.table {
+ table-layout: fixed;
+}
+.ui.fixed.table th,
+.ui.fixed.table td {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ui.attached.table {
+ top: 0;
+ bottom: 0;
+ border-radius: 0;
+ margin: 0 -1px;
+ width: calc(100% + 2px);
+ max-width: calc(100% + 2px);
+ border: 1px solid var(--color-secondary);
+}
+.ui.attached + .ui.attached.table:not(.top) {
+ border-top: none;
+}
+
+.ui[class*="bottom attached"].table {
+ bottom: 0;
+ margin-top: 0;
+ top: 0;
+ margin-bottom: 1em;
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+.ui[class*="bottom attached"].table:last-child {
+ margin-bottom: 0;
+}
+
+.ui.striped.table > tr:nth-child(2n),
+.ui.striped.table > tbody > tr:nth-child(2n) {
+ background: var(--color-light);
+}
+
+.ui.table[class*="single line"],
+.ui.table [class*="single line"] {
+ white-space: nowrap;
+}
+
+/* Column Width */
+.ui.table th.one.wide,
+.ui.table td.one.wide {
+ width: 6.25%;
+}
+.ui.table th.two.wide,
+.ui.table td.two.wide {
+ width: 12.5%;
+}
+.ui.table th.three.wide,
+.ui.table td.three.wide {
+ width: 18.75%;
+}
+.ui.table th.four.wide,
+.ui.table td.four.wide {
+ width: 25%;
+}
+.ui.table th.five.wide,
+.ui.table td.five.wide {
+ width: 31.25%;
+}
+.ui.table th.six.wide,
+.ui.table td.six.wide {
+ width: 37.5%;
+}
+.ui.table th.seven.wide,
+.ui.table td.seven.wide {
+ width: 43.75%;
+}
+.ui.table th.eight.wide,
+.ui.table td.eight.wide {
+ width: 50%;
+}
+.ui.table th.nine.wide,
+.ui.table td.nine.wide {
+ width: 56.25%;
+}
+.ui.table th.ten.wide,
+.ui.table td.ten.wide {
+ width: 62.5%;
+}
+.ui.table th.eleven.wide,
+.ui.table td.eleven.wide {
+ width: 68.75%;
+}
+.ui.table th.twelve.wide,
+.ui.table td.twelve.wide {
+ width: 75%;
+}
+.ui.table th.thirteen.wide,
+.ui.table td.thirteen.wide {
+ width: 81.25%;
+}
+.ui.table th.fourteen.wide,
+.ui.table td.fourteen.wide {
+ width: 87.5%;
+}
+.ui.table th.fifteen.wide,
+.ui.table td.fifteen.wide {
+ width: 93.75%;
+}
+.ui.table th.sixteen.wide,
+.ui.table td.sixteen.wide {
+ width: 100%;
+}
+
+.ui.basic.table {
+ background: transparent;
+ border: 1px solid var(--color-secondary);
+}
+.ui.basic.table > thead > tr > th,
+.ui.basic.table > tbody > tr > th,
+.ui.basic.table > tfoot > tr > th,
+.ui.basic.table > tr > th {
+ background: transparent;
+ border-left: none;
+}
+.ui.basic.table > tbody > tr {
+ border-bottom: 1px solid var(--color-secondary);
+}
+.ui.basic.table > tbody > tr > td,
+.ui.basic.table > tfoot > tr > td,
+.ui.basic.table > tr > td {
+ background: transparent;
+}
+.ui.basic.striped.table > tbody > tr:nth-child(2n) {
+ background: var(--color-light);
+}
+
+.ui[class*="very basic"].table {
+ border: none;
+}
+.ui[class*="very basic"].table:not(.striped) > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > thead > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:first-child,
+.ui[class*="very basic"].table:not(.striped) > tr > td:first-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:first-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:first-child {
+ padding-left: 0;
+}
+.ui[class*="very basic"].table:not(.striped) > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > thead > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:last-child,
+.ui[class*="very basic"].table:not(.striped) > tr > td:last-child,
+.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:last-child,
+.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:last-child {
+ padding-right: 0;
+}
+.ui[class*="very basic"].table:not(.striped) > thead > tr:first-child > th {
+ padding-top: 0;
+}
+
+.ui.celled.table > tr > th,
+.ui.celled.table > thead > tr > th,
+.ui.celled.table > tbody > tr > th,
+.ui.celled.table > tfoot > tr > th,
+.ui.celled.table > tr > td,
+.ui.celled.table > tbody > tr > td,
+.ui.celled.table > tfoot > tr > td {
+ border-left: 1px solid var(--color-secondary-alpha-50);
+}
+.ui.celled.table > tr > th:first-child,
+.ui.celled.table > thead > tr > th:first-child,
+.ui.celled.table > tbody > tr > th:first-child,
+.ui.celled.table > tfoot > tr > th:first-child,
+.ui.celled.table > tr > td:first-child,
+.ui.celled.table > tbody > tr > td:first-child,
+.ui.celled.table > tfoot > tr > td:first-child {
+ border-left: none;
+}
+
+.ui.compact.table > tr > th,
+.ui.compact.table > thead > tr > th,
+.ui.compact.table > tbody > tr > th,
+.ui.compact.table > tfoot > tr > th {
+ padding-left: 0.7em;
+ padding-right: 0.7em;
+}
+.ui.compact.table > tr > td,
+.ui.compact.table > tbody > tr > td,
+.ui.compact.table > tfoot > tr > td {
+ padding: 0.5em 0.7em;
+}
+
+/* use more horizontal padding on first and last items for visuals */
+.ui.table > thead > tr > th:first-of-type,
+.ui.table > tbody > tr > td:first-of-type,
+.ui.table > tr > td:first-of-type {
+ padding-left: 10px;
+}
+.ui.table > thead > tr > th:last-of-type,
+.ui.table > tbody > tr > td:last-of-type,
+.ui.table > tr > td:last-of-type {
+ padding-right: 10px;
+}
diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css
new file mode 100644
index 0000000..6ac7c37
--- /dev/null
+++ b/web_src/css/modules/tippy.css
@@ -0,0 +1,170 @@
+/* styles are based on node_modules/tippy.js/dist/tippy.css */
+
+/* class to hide tippy target elements on page load */
+.tippy-target {
+ display: none !important;
+}
+
+/* show target element once it's been moved by tippy.js */
+.tippy-content .tippy-target {
+ display: unset !important;
+}
+
+[data-tippy-root] {
+ max-width: calc(100vw - 32px);
+}
+
+.tippy-box {
+ position: relative;
+ background-color: var(--color-body);
+ color: var(--color-secondary-dark-6);
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ font-size: 1rem;
+}
+
+.tippy-content {
+ position: relative;
+ padding: 1rem; /* if you need different padding, use different data-theme */
+ z-index: 1;
+}
+
+/* bare theme, no styling at all, except box-shadow */
+.tippy-box[data-theme="bare"] {
+ border: none;
+ box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="bare"] .tippy-content {
+ padding: 0;
+ background: transparent;
+}
+
+/* tooltip theme for text tooltips */
+
+.tippy-box[data-theme="tooltip"] {
+ background-color: var(--color-tooltip-bg);
+ color: var(--color-tooltip-text);
+ border: none;
+}
+
+.tippy-box[data-theme="tooltip"] .tippy-content {
+ padding: 0.5rem 1rem;
+}
+
+.tippy-box[data-theme="tooltip"] .tippy-svg-arrow-inner,
+.tippy-box[data-theme="tooltip"] .tippy-svg-arrow-outer {
+ fill: var(--color-tooltip-bg);
+}
+
+/* menu theme for .ui.menu */
+
+.tippy-box[data-theme="menu"] {
+ background-color: var(--color-menu);
+ color: var(--color-text);
+ box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="menu"] .tippy-content {
+ padding: 4px 0;
+}
+
+.tippy-box[data-theme="menu"] .tippy-svg-arrow-inner {
+ fill: var(--color-menu);
+}
+
+.tippy-box[data-theme="menu"] .item {
+ display: flex;
+ align-items: center;
+ padding: 9px 18px;
+ color: inherit;
+ text-decoration: none;
+ gap: 10px;
+}
+
+.tippy-box[data-theme="menu"] .item:hover {
+ background: var(--color-hover);
+}
+
+.tippy-box[data-theme="menu"] .item:focus {
+ background: var(--color-active);
+}
+
+/* box-with-header theme to look like .ui.attached.segment. can contain .ui.attached.header */
+
+.tippy-box[data-theme="box-with-header"] {
+ box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="box-with-header"] .tippy-content {
+ background: var(--color-box-body);
+ border-radius: var(--border-radius);
+ padding: 0;
+}
+
+.tippy-box[data-theme="box-with-header"][data-placement^="top"] .tippy-svg-arrow-inner {
+ fill: var(--color-box-body);
+}
+
+.tippy-box[data-theme="box-with-header"][data-placement^="bottom"] .tippy-svg-arrow-inner {
+ fill: var(--color-box-header);
+}
+
+.tippy-box[data-placement^="top"] > .tippy-svg-arrow {
+ bottom: 0;
+}
+
+.tippy-box[data-placement^="top"] > .tippy-svg-arrow::after,
+.tippy-box[data-placement^="top"] > .tippy-svg-arrow > svg {
+ top: 16px;
+ transform: rotate(180deg);
+}
+
+.tippy-box[data-placement^="bottom"] > .tippy-svg-arrow {
+ top: 0;
+}
+
+.tippy-box[data-placement^="bottom"] > .tippy-svg-arrow > svg {
+ bottom: 16px;
+}
+
+.tippy-box[data-placement^="left"] > .tippy-svg-arrow {
+ right: 0;
+}
+
+.tippy-box[data-placement^="left"] > .tippy-svg-arrow::after,
+.tippy-box[data-placement^="left"] > .tippy-svg-arrow > svg {
+ transform: rotate(90deg);
+ top: calc(50% - 3px);
+ left: 11px;
+}
+
+.tippy-box[data-placement^="right"] > .tippy-svg-arrow {
+ left: 0;
+}
+
+.tippy-box[data-placement^="right"] > .tippy-svg-arrow::after,
+.tippy-box[data-placement^="right"] > .tippy-svg-arrow > svg {
+ transform: rotate(-90deg);
+ top: calc(50% - 3px);
+ right: 11px;
+}
+
+.tippy-svg-arrow {
+ width: 16px;
+ height: 16px;
+ text-align: initial;
+}
+
+.tippy-svg-arrow,
+.tippy-svg-arrow > svg {
+ position: absolute;
+}
+
+.tippy-svg-arrow-outer {
+ fill: var(--color-secondary);
+}
+
+.tippy-svg-arrow-inner {
+ fill: var(--color-body);
+}
diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css
new file mode 100644
index 0000000..2a9f78e
--- /dev/null
+++ b/web_src/css/modules/toast.css
@@ -0,0 +1,77 @@
+.toastify {
+ color: var(--color-white);
+ position: fixed;
+ opacity: 0;
+ transition: all .2s ease;
+ z-index: 500;
+ border-radius: var(--border-radius);
+ box-shadow: 0 8px 24px var(--color-shadow);
+ display: flex;
+ max-width: 50vw;
+ min-width: 300px;
+ padding: 4px;
+}
+
+.toastify.on {
+ opacity: 1;
+}
+
+.toast-body {
+ flex: 1;
+ padding: 5px 0;
+ overflow-wrap: anywhere;
+}
+
+.toast-close,
+.toast-icon {
+ color: currentcolor;
+ border-radius: var(--border-radius);
+ background: transparent;
+ border: none;
+ display: flex;
+ width: 30px;
+ height: 30px;
+ justify-content: center;
+ align-items: center;
+}
+
+.toast-close:hover {
+ background: var(--color-hover);
+}
+
+.toast-close:active {
+ background: var(--color-active);
+}
+
+.toastify-right {
+ right: 15px;
+}
+
+.toastify-left {
+ left: 15px;
+}
+
+.toastify-top {
+ top: -150px;
+}
+
+.toastify-bottom {
+ bottom: -150px;
+}
+
+.toastify-center {
+ margin-left: auto;
+ margin-right: auto;
+ left: 0;
+ right: 0;
+}
+
+@media (max-width: 360px) {
+ .toastify-right, .toastify-left {
+ margin-left: auto;
+ margin-right: auto;
+ left: 0;
+ right: 0;
+ max-width: fit-content;
+ }
+}
diff --git a/web_src/css/org.css b/web_src/css/org.css
new file mode 100644
index 0000000..6853a26
--- /dev/null
+++ b/web_src/css/org.css
@@ -0,0 +1,192 @@
+#create-page-form form {
+ margin: auto;
+}
+
+#create-page-form form .ui.message {
+ text-align: center;
+}
+
+@media (min-width: 768px) {
+ #create-page-form form {
+ width: 800px !important;
+ }
+ #create-page-form form .header {
+ padding-left: 280px !important;
+ }
+ #create-page-form form .inline.field > label {
+ text-align: right;
+ width: 250px !important;
+ word-wrap: break-word;
+ }
+ #create-page-form form .help {
+ margin-left: 265px !important;
+ }
+ #create-page-form form .optional .title {
+ margin-left: 250px !important;
+ }
+ #create-page-form form .inline.field > input,
+ #create-page-form form .inline.field > textarea {
+ width: 50%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ #create-page-form form .optional .title {
+ margin-left: 15px;
+ }
+ #create-page-form form .inline.field > label {
+ display: block;
+ }
+}
+
+.organization .head .ui.header .ui.right {
+ margin-top: 5px;
+}
+
+.organization.new.org form {
+ margin: auto;
+}
+
+.organization.new.org form .ui.message {
+ text-align: center;
+}
+
+@media (min-width: 768px) {
+ .organization.new.org form {
+ width: 800px !important;
+ }
+ .organization.new.org form .header {
+ padding-left: 280px !important;
+ }
+ .organization.new.org form .inline.field > label {
+ text-align: right;
+ width: 250px !important;
+ word-wrap: break-word;
+ }
+ .organization.new.org form .help {
+ margin-left: 265px !important;
+ }
+ .organization.new.org form .optional .title {
+ margin-left: 250px !important;
+ }
+ .organization.new.org form .inline.field > input,
+ .organization.new.org form .inline.field > textarea {
+ width: 50%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .organization.new.org form .optional .title {
+ margin-left: 15px;
+ }
+ .organization.new.org form .inline.field > label {
+ display: block;
+ }
+}
+
+.organization.new.org form .header {
+ padding-left: 0 !important;
+ text-align: center;
+}
+
+.page-content.organization .org-avatar {
+ margin-right: 15px;
+}
+
+.page-content.organization #org-info {
+ overflow-wrap: anywhere;
+ flex: 1;
+}
+
+.page-content.organization #org-info .ui.header {
+ display: flex;
+ align-items: center;
+ font-size: 36px;
+ margin-bottom: 0;
+}
+
+@media (max-width: 767.98px) {
+ .page-content.organization #org-info .ui.header {
+ flex-direction: column;
+ margin-bottom: 1rem;
+ }
+ .page-content.organization #org-info .org-title {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ }
+}
+
+.page-content.organization #org-info .desc {
+ font-size: 16px;
+ margin-bottom: 10px;
+}
+
+.page-content.organization #org-info .meta {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.page-content.organization .ui.top.header .ui.right {
+ margin-top: 0;
+}
+
+.page-content.organization .teams .item {
+ padding: 10px 15px;
+}
+
+.page-content.organization .members .ui.avatar {
+ width: 48px;
+ height: 48px;
+ margin-right: 5px;
+ margin-bottom: 5px;
+}
+
+.organization.invite #invite-box {
+ margin: 50px auto auto;
+ width: 500px !important;
+}
+
+.organization.invite #invite-box #search-user-box input {
+ margin-left: 0;
+ width: 300px;
+}
+
+.organization.invite #invite-box .ui.button {
+ margin-left: 5px;
+ margin-top: -3px;
+}
+
+.organization.invite .ui.avatar {
+ width: 100%;
+ height: 100%;
+}
+
+.organization.teams .detail .item {
+ padding: 10px 15px;
+}
+
+.organization.teams .detail .item:not(:last-child) {
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.organization.teams .repositories .item,
+.organization.teams .members .item {
+ padding: 10px 19px;
+}
+
+.organization.teams .repositories .item:not(:last-child),
+.organization.teams .members .item:not(:last-child) {
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.organization.teams .repositories .item .button,
+.organization.teams .members .item .button {
+ padding: 9px 10px;
+ margin: 0;
+}
+
+.org-team-navbar .active.item {
+ background: var(--color-box-body) !important;
+}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
new file mode 100644
index 0000000..3bc1a1e
--- /dev/null
+++ b/web_src/css/repo.css
@@ -0,0 +1,3007 @@
+.repository .data-table .line-num,
+.repository .diff-file-box .file-body.file-code .lines-num,
+.repository .diff-file-box .code-diff tbody tr .lines-type-marker {
+ user-select: none;
+}
+
+.repository .owner.dropdown {
+ min-width: 40% !important;
+}
+
+.repository .unicode-escaped .escaped-code-point[data-escaped]::before {
+ visibility: visible;
+ content: attr(data-escaped);
+ font-family: var(--fonts-monospace);
+ color: var(--color-red);
+}
+
+.repository .unicode-escaped .escaped-code-point .char {
+ display: none;
+}
+
+.repository .broken-code-point {
+ font-family: var(--fonts-monospace);
+ color: var(--color-blue);
+}
+
+.repository .unicode-escaped .ambiguous-code-point {
+ border: 1px var(--color-yellow) solid;
+}
+
+.issue-content {
+ display: flex;
+ align-items: flex-start;
+ gap: 16px;
+}
+
+@media (max-width: 767.98px) {
+ .issue-content {
+ flex-direction: column;
+ }
+}
+
+.issue-content-left {
+ margin: 0 !important;
+ width: calc(100% - 316px);
+}
+
+.issue-content-right {
+ margin: 0 !important;
+ width: 300px;
+}
+
+.issue-content-right .dropdown > .menu {
+ max-width: 270px;
+ min-width: 0;
+}
+
+@media (max-width: 767.98px) {
+ .issue-content-left,
+ .issue-content-right {
+ width: 100%;
+ }
+}
+
+.repository .issue-content-right .ui.list .dependency {
+ padding: 0;
+ white-space: nowrap;
+}
+
+.repository .issue-content-right .ui.list .title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.repository .issue-content-right #deadlineForm input {
+ width: 12.8rem;
+ border-radius: var(--border-radius) 0 0 var(--border-radius);
+ border-right: 0;
+ white-space: nowrap;
+}
+
+.repository .issue-content-right .filter.menu {
+ max-height: 500px;
+ overflow-x: auto;
+}
+
+.repository .filter.menu.labels .label-filter .menu .info {
+ display: inline-block;
+ padding: 0.5rem 0;
+ font-size: 12px;
+ width: 100%;
+ white-space: nowrap;
+ margin-left: 10px;
+ margin-right: 8px;
+ text-align: left;
+}
+
+.repository .filter.menu.labels .label-filter .menu .info code {
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ padding: 1px 2px;
+ font-size: 11px;
+}
+
+/* make all issue filter dropdown menus popup leftward, to avoid go out the viewport (right side) */
+.repository .filter.menu .ui.dropdown .menu {
+ max-height: 500px;
+ max-width: 300px;
+ overflow-x: hidden;
+ right: 0;
+ left: auto;
+}
+
+/* the label-filter is the first dropdown, it shouldn't be shown leftward, otherwise it may go out the viewport (left side) */
+.repository .filter.menu .ui.dropdown.label-filter .menu {
+ min-width: max-content;
+ right: unset;
+ left: 0;
+}
+
+.repository .select-label .desc {
+ padding-left: 23px;
+}
+
+/* For the secondary pointing menu, respect its own border-bottom */
+/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
+.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
+ border-bottom: 0;
+}
+
+.repository .ui.tabs.divider {
+ margin-top: -1px;
+ margin-bottom: 12px;
+}
+
+.repository .clone-panel #repo-clone-url {
+ width: 320px;
+ border-radius: 0;
+}
+
+@media (max-width: 991.98px) {
+ .repository .clone-panel #repo-clone-url {
+ width: 200px;
+ }
+}
+
+.repository .ui.action.input.clone-panel > button + button,
+.repository .ui.action.input.clone-panel > button + input {
+ margin-left: -1px; /* make the borders overlap to avoid double borders */
+}
+
+.repository .clone-panel > button:first-of-type {
+ border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
+}
+
+.repository .clone-panel > button:last-of-type {
+ border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
+}
+
+.repository .clone-panel .dropdown .menu {
+ right: 0 !important;
+ left: auto !important;
+}
+
+.repository.file.list .repo-description {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 5px;
+ margin-bottom: 5px;
+}
+
+@media (max-width: 767.98px) {
+ .repository.file.list .repo-description {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+
+.commit-summary {
+ flex: 1;
+ overflow-wrap: anywhere;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.commit-header .commit-summary,
+td .commit-summary {
+ white-space: normal;
+}
+
+.commit-list .js-toggle-commit-body {
+ margin: 0 0 0 0.25em;
+}
+
+.commit-list .commit-status {
+ margin: 0 0 0 0.25em;
+}
+
+.latest-commit {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.latest-commit .commit-status {
+ margin-right: 0.25em;
+}
+
+@media (max-width: 767.98px) {
+ .latest-commit .sha {
+ display: none;
+ }
+ .latest-commit .commit-summary {
+ margin-left: 8px;
+ }
+}
+
+.repo-path {
+ display: flex;
+ overflow-wrap: anywhere;
+}
+
+/* this is what limits the commit table width to a value that works on all viewport sizes */
+#repo-files-table th:first-of-type {
+ max-width: calc(calc(min(100vw, 1280px)) - 145px - calc(2 * var(--page-margin-x)));
+}
+
+.repository.file.list #repo-files-table thead th {
+ font-weight: var(--font-weight-normal);
+}
+
+.repository.file.list #repo-files-table tbody .svg {
+ margin-left: 3px;
+ margin-right: 5px;
+}
+
+.repository.file.list #repo-files-table tbody .svg.octicon-reply {
+ margin-right: 10px;
+}
+
+.repository.file.list #repo-files-table tbody .svg.octicon-file-directory-fill,
+.repository.file.list #repo-files-table tbody .svg.octicon-file-submodule {
+ color: var(--color-primary);
+}
+
+.repository.file.list #repo-files-table tbody .svg.octicon-file,
+.repository.file.list #repo-files-table tbody .svg.octicon-file-symlink-file,
+.repository.file.list #repo-files-table tbody .svg.octicon-file-directory-symlink {
+ color: var(--color-secondary-dark-7);
+}
+
+.repository.file.list #repo-files-table td {
+ padding-top: 0;
+ padding-bottom: 0;
+ overflow: initial;
+}
+
+.repository.file.list #repo-files-table td.name {
+ width: 33%;
+ max-width: calc(100vw - 140px);
+}
+
+@media (min-width: 1201px) {
+ .repository.file.list #repo-files-table td.name {
+ max-width: 150px;
+ }
+}
+
+@media (min-width: 992px) and (max-width: 1200px) {
+ .repository.file.list #repo-files-table td.name {
+ max-width: 200px;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+ .repository.file.list #repo-files-table td.name {
+ max-width: 300px;
+ }
+}
+
+.repository.file.list #repo-files-table td.message {
+ color: var(--color-text-light-1);
+ width: 66%;
+}
+
+@media (min-width: 1201px) {
+ .repository.file.list #repo-files-table td.message {
+ max-width: 400px;
+ }
+}
+
+@media (min-width: 992px) and (max-width: 1200px) {
+ .repository.file.list #repo-files-table td.message {
+ max-width: 350px;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+ .repository.file.list #repo-files-table td.message {
+ max-width: 250px;
+ }
+}
+
+.repository.file.list #repo-files-table td.age {
+ color: var(--color-text-light-1);
+}
+
+.repository.file.list #repo-files-table td .truncate {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.repository.file.list #repo-files-table td a {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.repository.file.list #repo-files-table td .at {
+ margin-left: 3px;
+ margin-right: 3px;
+}
+
+.repository.file.list #repo-files-table td > * {
+ vertical-align: middle;
+}
+
+.repository.file.list #repo-files-table td.message .isSigned {
+ cursor: default;
+}
+
+.repository.file.list #repo-files-table tr:last-of-type td:first-child {
+ border-bottom-left-radius: var(--border-radius);
+}
+
+.repository.file.list #repo-files-table tr:last-of-type td:last-child {
+ border-bottom-right-radius: var(--border-radius);
+}
+
+.repository.file.list #repo-files-table tr:hover {
+ background-color: var(--color-hover);
+}
+
+.repository.file.list #repo-files-table tr.has-parent a {
+ display: inline-block;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ width: calc(100% - 1.25rem);
+}
+
+.repository.file.list .non-diff-file-content .header .icon {
+ font-size: 1em;
+}
+
+.repository.file.list .non-diff-file-content .header .small.icon {
+ font-size: 0.75em;
+}
+
+.repository.file.list .non-diff-file-content .header .tiny.icon {
+ font-size: 0.5em;
+}
+
+.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon {
+ line-height: var(--line-height-default);
+ padding: 8px;
+ vertical-align: middle;
+ color: var(--color-text);
+}
+
+.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon:hover {
+ color: var(--color-primary);
+}
+
+.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon-danger:hover {
+ color: var(--color-red);
+}
+
+.repository.file.list .non-diff-file-content .header .file-actions .btn-octicon.disabled {
+ color: inherit;
+ opacity: var(--opacity-disabled);
+ cursor: default;
+}
+
+.view-raw {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.view-raw > * {
+ max-width: 100%;
+}
+
+.view-raw audio,
+.view-raw video,
+.view-raw img {
+ margin: 1rem 0;
+ border-radius: 0;
+ object-fit: contain;
+}
+
+.view-raw img[src$=".svg" i] {
+ max-height: 600px !important;
+ max-width: 600px !important;
+}
+
+.pdf-content {
+ width: 100%;
+ height: 100vh;
+ border: none !important;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.pdf-content .pdf-fallback-button {
+ margin: 50px auto;
+}
+
+.repository.file.list .non-diff-file-content .plain-text {
+ padding: 1em 2em;
+}
+
+.repository.file.list .non-diff-file-content .plain-text pre {
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+}
+
+.repository.file.list .non-diff-file-content .csv {
+ overflow-x: auto;
+ padding: 0 !important;
+}
+
+.repository.file.list .non-diff-file-content pre {
+ overflow: auto;
+}
+
+.repository.file.list .non-diff-file-content .asciicast {
+ padding: 0 !important;
+}
+
+.non-diff-file-content .pdfobject {
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.repository.file.list .sidebar {
+ padding-left: 0;
+}
+
+.repository.file.list .sidebar .svg {
+ width: 16px;
+}
+
+.repo-editor-header {
+ width: 100%;
+}
+
+.repo-editor-header input {
+ vertical-align: middle !important;
+ width: auto !important;
+ padding: 7px 8px !important;
+ margin-right: 5px !important;
+}
+
+.repository.file.editor .tabular.menu .svg {
+ margin-right: 5px;
+}
+
+.repository.file.editor .commit-form-wrapper {
+ padding-left: 64px;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-avatar {
+ float: left;
+ margin-left: -64px;
+ width: 3em;
+ height: auto;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form {
+ position: relative;
+ padding: 15px;
+ margin-bottom: 10px;
+ border: 1px solid var(--color-secondary);
+ background: var(--color-box-body);
+ border-radius: var(--border-radius);
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form::before,
+.repository.file.editor .commit-form-wrapper .commit-form::after {
+ right: 100%;
+ top: 20px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form::before {
+ border-right-color: var(--color-secondary);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form::after {
+ border-right-color: var(--color-box-body);
+ border-width: 8px;
+ margin-top: -8px;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form .quick-pull-choice .branch-name {
+ display: inline-block;
+ padding: 2px 4px;
+ font: 12px var(--fonts-monospace);
+ color: var(--color-text);
+ background: var(--color-secondary);
+ border-radius: var(--border-radius);
+ margin: 0 2px;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form .quick-pull-choice .new-branch-name-input {
+ position: relative;
+ margin-left: 25px;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form .quick-pull-choice .new-branch-name-input input {
+ width: 240px !important;
+ padding-left: 26px !important;
+}
+
+.repository.file.editor .commit-form-wrapper .commit-form .quick-pull-choice .octicon-git-branch {
+ position: absolute;
+ top: 9px;
+ left: 10px;
+ color: var(--color-grey);
+}
+
+.repository.options #interval {
+ width: 100px !important;
+ min-width: 100px;
+}
+
+.repository.new.issue .comment.form .comment .avatar {
+ width: 3em;
+}
+
+.repository.new.issue .comment.form .content {
+ margin-left: 4em;
+}
+
+.repository.new.issue .comment.form .content::before,
+.repository.new.issue .comment.form .content::after {
+ right: 100%;
+ top: 20px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.repository.new.issue .comment.form .content::before {
+ border-right-color: var(--color-secondary);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+.repository.new.issue .comment.form .content::after {
+ border-right-color: var(--color-box-body);
+ border-width: 8px;
+ margin-top: -8px;
+}
+
+.repository.new.issue .comment.form .content .markup {
+ font-size: 14px;
+}
+
+.repository.view.issue .instruct-toggle {
+ display: inline-block;
+}
+
+.issue-title-header {
+ width: 100%;
+ padding-bottom: 4px;
+ margin-bottom: 1rem;
+}
+
+.issue-title-meta {
+ display: flex;
+ align-items: center;
+}
+
+.issue-title .button-row,
+.repository.releases .button-row {
+ display: flex;
+}
+
+@media (max-width: 767.98px) {
+ .repository.view.issue .issue-title {
+ flex-direction: column;
+ }
+ .issue-title .button-row {
+ width: 100%;
+ margin-top: .5rem;
+ justify-content: space-between;
+ }
+ .comment.form .issue-content-left .avatar {
+ display: none;
+ }
+ .comment.form .issue-content-left .content {
+ margin-left: 0 !important;
+ }
+ .comment.form .issue-content-left .content::before,
+ .comment.form .issue-content-left .content::after,
+ .comment.form .content .form::before,
+ .comment.form .content .form::after {
+ display: none;
+ }
+
+ .repository.view.issue .issue-title.edit-active h1 {
+ padding-right: 0;
+ }
+}
+
+.repository.view.issue .issue-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ margin-bottom: 8px;
+ min-height: var(--repo-header-issue-min-height);
+}
+
+.repository.view.issue .issue-title h1 {
+ flex: 1;
+ width: 100%;
+ font-weight: var(--font-weight-normal);
+ font-size: 32px;
+ line-height: 40px;
+ margin: 0;
+ padding-right: 0.5rem;
+}
+
+.repository.view.issue .issue-title .ui.input {
+ font-size: 16px;
+}
+
+.repository.view.issue .issue-title .ui.input input {
+ font-size: 1.5em;
+ padding: 2px .5rem;
+}
+
+.repository.view.issue .issue-title .index {
+ color: var(--color-text-light-2);
+}
+
+.repository.view.issue .issue-title .label {
+ margin-right: 10px;
+}
+
+.issue-state-label {
+ display: flex !important;
+ align-items: center !important;
+ font-size: 14px !important;
+ padding: 7px 10px !important;
+ border-radius: var(--border-radius-medium) !important;
+ flex-shrink: 0;
+}
+
+.issue-state-label .svg {
+ margin-right: 4px;
+}
+
+.repository.view.issue .pull-desc code {
+ color: var(--color-primary);
+}
+
+.repository.view.issue .pull-desc a[data-clipboard-text] {
+ cursor: pointer;
+}
+
+.repository.view.issue .pull-desc a[data-clipboard-text] svg {
+ vertical-align: middle;
+ position: relative;
+ top: -2px;
+ right: 1px;
+}
+
+.repository.view.issue .pull.tabs.container {
+ width: 100%;
+ max-width: 100%;
+}
+
+.repository.view.issue .pull.tabular.menu {
+ margin-bottom: 0;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.repository.view.issue .pull.tabular.menu .svg {
+ margin-right: 5px;
+}
+
+.repository.view.issue .merge.box .branch-update.grid .row {
+ padding-bottom: 1rem;
+}
+
+.repository.view.issue .merge.box .branch-update.grid .row .icon {
+ margin-top: 1.1rem;
+}
+
+.repository.view.issue .comment-list:not(.prevent-before-timeline)::before {
+ display: block;
+ content: "";
+ position: absolute;
+ margin-top: 12px;
+ margin-bottom: 14px;
+ top: 0;
+ bottom: 0;
+ left: 96px;
+ width: 2px;
+ background-color: var(--color-timeline);
+ z-index: -1;
+}
+
+.repository.view.issue .comment-list .timeline {
+ position: relative;
+ display: block;
+ margin-left: 40px;
+ padding-left: 16px;
+}
+
+.repository.view.issue .comment-list .timeline::before { /* ciara */
+ display: block;
+ content: "";
+ position: absolute;
+ margin-top: 12px;
+ margin-bottom: 14px;
+ top: 0;
+ bottom: 0;
+ left: 30px;
+ width: 2px;
+ background-color: var(--color-timeline);
+ z-index: -1;
+}
+
+.repository.view.issue .comment-list .timeline-item,
+.repository.view.issue .comment-list .timeline-item-group {
+ padding: 16px 0;
+}
+
+.repository.view.issue .comment-list .timeline-item-group .timeline-item {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.repository.view.issue .comment-list .timeline-avatar-offset {
+ margin-top: 48px;
+}
+
+.repository.view.issue .comment-list .timeline-item {
+ margin-left: 16px;
+ position: relative;
+}
+
+.repository.view.issue .comment-list .timeline-item .timeline-avatar {
+ position: absolute;
+ left: -68px;
+}
+
+/* Don't show the mobile oriented avatar ".inline-timeline-avatar" on desktop. Desktop uses the avatar with class ".timeline-avatar" */
+.repository.view.issue .comment-list .timeline-item .inline-timeline-avatar {
+ display: none;
+}
+
+.repository.view.issue .comment-list .timeline-item:first-child:not(.commit) {
+ padding-top: 0 !important;
+}
+
+.repository.view.issue .comment-list .timeline-item:last-child:not(.commit) {
+ padding-bottom: 0 !important;
+}
+
+.repository.view.issue .comment-list .timeline-item .badge.badge-commit {
+ border-color: transparent;
+ background: radial-gradient(var(--color-body) 40%, transparent 40%) no-repeat;
+}
+
+.repository.view.issue .comment-list .timeline-item .badge {
+ width: 34px;
+ height: 34px;
+ background-color: var(--color-timeline);
+ border-radius: var(--border-radius-full);
+ display: flex;
+ float: left;
+ margin-left: -33px;
+ margin-right: 8px;
+ color: var(--color-text);
+ align-items: center;
+ justify-content: center;
+}
+
+.repository.view.issue .comment-list .timeline-item .badge .svg {
+ width: 22px;
+ height: 22px;
+ padding: 3px;
+}
+
+.repository.view.issue .comment-list .timeline-item .badge .svg.octicon-comment {
+ margin-top: 2px;
+}
+
+.repository.view.issue .comment-list .timeline-item.comment > .content {
+ margin-left: -16px;
+}
+
+.repository.view.issue .comment-list .timeline-item.event > .text {
+ line-height: 32px;
+ vertical-align: middle;
+}
+
+.repository.view.issue .comment-list .timeline-item.commits-list {
+ padding-left: 15px;
+ padding-top: 0;
+}
+
+.repository.view.issue .comment-list .timeline-item.commits-list .ui.avatar,
+.repository.view.issue .comment-list .timeline-item.event .ui.avatar {
+ margin-right: 0.25em;
+}
+
+.singular-commit {
+ display: flex;
+ align-items: center;
+}
+
+.singular-commit .badge {
+ height: 30px !important;
+}
+
+.singular-commit .shabox .sha.label {
+ margin: 0;
+ border: 1px solid var(--color-light-border);
+}
+
+.singular-commit .shabox .sha.label.isSigned.isWarning {
+ border: 1px solid var(--color-red-badge);
+ background: var(--color-red-badge-bg);
+}
+
+.singular-commit .shabox .sha.label.isSigned.isWarning:hover {
+ background: var(--color-red-badge-hover-bg) !important;
+}
+
+.singular-commit .shabox .sha.label.isSigned.isVerified {
+ border: 1px solid var(--color-green-badge);
+ background: var(--color-green-badge-bg);
+}
+
+.singular-commit .shabox .sha.label.isSigned.isVerified:hover {
+ background: var(--color-green-badge-hover-bg) !important;
+}
+
+.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted {
+ border: 1px solid var(--color-yellow-badge);
+ background: var(--color-yellow-badge-bg);
+}
+
+.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover {
+ background: var(--color-yellow-badge-hover-bg) !important;
+}
+
+.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched {
+ border: 1px solid var(--color-orange-badge);
+ background: var(--color-orange-badge-bg);
+}
+
+.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover {
+ background: var(--color-orange-badge-hover-bg) !important;
+}
+
+.repository.view.issue .comment-list .timeline-item .comparebox {
+ line-height: 32px;
+ vertical-align: middle;
+}
+
+.repository.view.issue .comment-list .timeline-item .comparebox .compare.label {
+ font-size: 1rem;
+ margin: 0;
+ border: 1px solid var(--color-light-border);
+}
+
+@media (max-width: 767.98px) {
+ .repository.view.issue .comment-list .timeline-item .ui.segments {
+ margin-left: -2rem;
+ }
+}
+
+.repository.view.issue .comment-list .ui.comments {
+ max-width: 100%;
+}
+
+.repository.view.issue .comment-list .comment > .content > div:first-child {
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+}
+
+.repository.view.issue .comment-list .comment > .content > div:last-child {
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.repository.view.issue .comment-list .comment .comment-container {
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+}
+
+@media (max-width: 767.98px) {
+ .repository.view.issue .comment-list .comment .content .form .button {
+ width: 100%;
+ margin: 0;
+ }
+ .repository.view.issue .comment-list .comment .content .form .button:not(:last-child) {
+ margin-bottom: 1rem;
+ }
+}
+
+.repository.view.issue .comment-list .comment .merge-section {
+ background-color: var(--color-box-body);
+}
+
+.repository.view.issue .comment-list .comment .merge-section .item-section {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0;
+ gap: 0.5em;
+}
+
+.repository.view.issue .comment-list .comment .merge-section .divider {
+ margin-left: -1rem;
+ width: calc(100% + 2rem);
+}
+
+.repository.view.issue .comment-list .comment .merge-section.no-header::before,
+.repository.view.issue .comment-list .comment .merge-section.no-header::after {
+ right: 100%;
+ top: 20px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.repository.view.issue .comment-list .comment .merge-section.no-header::before {
+ border-right-color: var(--color-secondary);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+.repository.view.issue .comment-list .comment .merge-section.no-header::after {
+ border-right-color: var(--color-box-body);
+ border-width: 8px;
+ margin-top: -8px;
+}
+
+.merge-section-info code {
+ border: 1px solid var(--color-light-border);
+ border-radius: var(--border-radius);
+ padding: 2px 4px;
+ background: var(--color-light);
+}
+
+.repository.view.issue .comment-list .comment .markup {
+ font-size: 14px;
+}
+
+.repository.view.issue .comment-list .comment .no-content {
+ color: var(--color-text-light-2);
+ font-style: italic;
+}
+
+.repository.view.issue .comment-list .comment .ui.form .field:first-child {
+ clear: none;
+}
+
+.repository.view.issue .comment-list .comment .ui.form .field.footer {
+ overflow: hidden;
+}
+
+.repository.view.issue .comment-list .comment .ui.form .field .tab.markup {
+ min-height: 5rem;
+}
+
+.repository.view.issue .comment-list .comment .edit.buttons {
+ margin-top: 10px;
+}
+
+.repository.view.issue .comment-list .code-comment {
+ border: 1px solid transparent;
+ margin: 0;
+}
+
+.repository.view.issue .comment-list .code-comment .comment-header {
+ background: transparent;
+ border-bottom: 0 !important;
+ padding: 0 !important;
+}
+
+.repository.view.issue .comment-list .code-comment .comment-header::after,
+.repository.view.issue .comment-list .code-comment .comment-header::before {
+ display: none;
+}
+
+.repository.view.issue .comment-list .code-comment .comment-content {
+ margin-left: 36px;
+}
+
+.repository.view.issue .comment-list .comment > .avatar {
+ margin-top: 6px;
+}
+
+.repository.view.issue .comment-list .comment > .avatar ~ .content {
+ margin-left: 42px;
+}
+
+.repository.view.issue .comment-list .comment-code-cloud .segment.reactions {
+ margin-top: 16px !important;
+ margin-bottom: -8px !important;
+ border-top: none !important;
+}
+
+.repository.view.issue .comment-list .comment-code-cloud .segment.reactions .ui.label {
+ border: 1px solid;
+ padding: 5px 8px !important;
+ margin: 0 2px;
+ border-radius: var(--border-radius);
+ border-color: var(--color-secondary-dark-1) !important;
+}
+
+.repository.view.issue .comment-list .comment-code-cloud .segment.reactions .ui.label.basic.primary {
+ background-color: var(--color-reaction-active-bg) !important;
+ border-color: var(--color-primary-alpha-80) !important;
+}
+
+.repository.view.issue .comment-list .comment-code-cloud .segment.reactions .ui.label.basic.primary:hover {
+ background-color: var(--color-reaction-hover-bg) !important;
+ border-color: var(--color-primary-alpha-80) !important;
+}
+
+.repository.view.issue .comment-list .comment-code-cloud button.comment-form-reply {
+ margin: 0;
+}
+
+.repository.view.issue .comment-list .event {
+ padding-left: 15px;
+}
+
+.repository.view.issue .comment-list .event .detail {
+ margin-top: 4px;
+ margin-left: 15px;
+}
+
+.repository.view.issue .comment-list .event .detail .text {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.repository.view.issue .comment-list .event .segments {
+ box-shadow: none;
+}
+
+@media (max-width: 767.98px) {
+ .repository.view.issue .comment-list {
+ padding: 1rem 0 !important; /* Important is required here to override existing fomantic styles. */
+ }
+}
+
+.repository.view.issue .ui.depending .item.is-closed .title {
+ text-decoration: line-through;
+}
+
+.repository .comment.form .content .field:first-child {
+ clear: none;
+}
+
+.repository .comment.form .content .form::before,
+.repository .comment.form .content .form::after {
+ right: 100%;
+ top: 20px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.repository .comment.form .content .form::before {
+ border-right-color: var(--color-secondary);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+.repository .comment.form .content .form::after {
+ border-right-color: var(--color-box-body);
+ border-width: 8px;
+ margin-top: -8px;
+}
+
+.repository.new.milestone textarea {
+ height: 200px;
+}
+
+.milestone-progress-big {
+ width: min(420px, 96vw);
+ height: 10px;
+}
+
+.repository.compare.pull .show-form-container {
+ text-align: left;
+}
+
+.repository .choose.branch {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.repository .choose .compare-separator {
+ width: 100%;
+ margin-top: -1rem;
+ text-align: center;
+}
+
+.repository.compare.pull .comment.form .content::before,
+.repository.compare.pull .comment.form .content::after {
+ right: 100%;
+ top: 20px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.repository.compare.pull .comment.form .content::before {
+ border-right-color: var(--color-secondary);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+.repository.compare.pull .comment.form .content::after {
+ border-right-color: var(--color-box-body);
+ border-width: 8px;
+ margin-top: -8px;
+}
+
+.repository.compare.pull .pullrequest-form {
+ margin-top: 16px;
+ margin-bottom: 16px;
+}
+
+.repository.compare.pull .markup {
+ font-size: 14px;
+}
+
+.repository.compare.pull .title .issue-title {
+ margin-bottom: 0.5rem;
+}
+
+.repository.compare.pull .title .issue-title .index {
+ color: var(--color-text-light-2);
+}
+
+.repository.branches .commit-divergence .bar-group {
+ position: relative;
+ float: left;
+ padding-bottom: 6px;
+ width: 50%;
+ max-width: 90px;
+}
+
+.repository.branches .commit-divergence .bar-group:last-child {
+ border-left: 1px solid var(--color-secondary-dark-2);
+}
+
+.repository.branches .commit-divergence .count {
+ margin: 0 3px;
+}
+
+.repository.branches .commit-divergence .count.count-ahead {
+ text-align: left;
+}
+
+.repository.branches .commit-divergence .count.count-behind {
+ text-align: right;
+}
+
+.repository.branches .commit-divergence .bar {
+ height: 4px;
+ position: absolute;
+ background-color: var(--color-secondary-dark-2);
+}
+
+.repository.branches .commit-divergence .bar.bar-behind {
+ right: 0;
+}
+
+.repository.branches .commit-divergence .bar.bar-ahead {
+ left: 0;
+}
+
+.repository.commits .header .search input {
+ font-weight: var(--font-weight-normal);
+ padding: 5px 10px;
+}
+
+.repository #commits-table td:not(.message) {
+ white-space: nowrap;
+}
+.repository #commits-table thead .sha {
+ width: 200px;
+}
+
+.repository #commits-table thead .shatd {
+ text-align: center;
+}
+
+.repository #commits-table td.sha .sha.label {
+ margin: 0;
+}
+
+.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
+ background-color: var(--color-light) !important;
+}
+
+.repository #commits-table td.sha .sha.label,
+.repository #repo-files-table .sha.label,
+.repository #repo-file-commit-box .sha.label,
+.repository #rev-list .sha.label,
+.repository .timeline-item.commits-list .singular-commit .sha.label {
+ border: 1px solid var(--color-light-border);
+}
+
+.repository #commits-table td.sha .sha.label .ui.signature.avatar,
+.repository #repo-files-table .sha.label .ui.signature.avatar,
+.repository #repo-file-commit-box .sha.label .ui.signature.avatar,
+.repository #rev-list .sha.label .ui.signature.avatar,
+.repository .timeline-item.commits-list .singular-commit .sha.label .ui.signature.avatar {
+ height: 16px;
+ margin-bottom: 0;
+ width: 16px;
+}
+
+.repository #commits-table td.sha .sha.label .detail.icon,
+.repository #repo-files-table .sha.label .detail.icon,
+.repository #repo-file-commit-box .sha.label .detail.icon,
+.repository #rev-list .sha.label .detail.icon,
+.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon {
+ background: var(--color-light);
+ margin: -6px -10px -4px 0;
+ padding: 5px 4px 5px 6px;
+ border-left: 1px solid var(--color-light-border);
+ border-top: 0;
+ border-right: 0;
+ border-bottom: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.repository #commits-table td.sha .sha.label .detail.icon img,
+.repository #repo-files-table .sha.label .detail.icon img,
+.repository #repo-file-commit-box .sha.label .detail.icon img,
+.repository #rev-list .sha.label .detail.icon img,
+.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon img {
+ margin-right: 0;
+}
+
+.repository #commits-table td.sha .sha.label .detail.icon .svg,
+.repository #repo-files-table .sha.label .detail.icon .svg,
+.repository #repo-file-commit-box .sha.label .detail.icon .svg,
+.repository #rev-list .sha.label .detail.icon .svg,
+.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg {
+ margin: 0 0.25em 0 0;
+}
+
+.repository #commits-table td.sha .sha.label .detail.icon > div,
+.repository #repo-files-table .sha.label .detail.icon > div,
+.repository #repo-file-commit-box .sha.label .detail.icon > div,
+.repository #rev-list .sha.label .detail.icon > div,
+.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div {
+ display: flex;
+ align-items: center;
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isWarning,
+.repository #repo-files-table .sha.label.isSigned.isWarning,
+.repository #repo-file-commit-box .sha.label.isSigned.isWarning,
+.repository #rev-list .sha.label.isSigned.isWarning,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning {
+ border: 1px solid var(--color-red-badge);
+ background: var(--color-red-badge-bg);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon,
+.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon,
+.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon,
+.repository #rev-list .sha.label.isSigned.isWarning .detail.icon,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon {
+ border-left: 1px solid var(--color-red-badge);
+ color: var(--color-red-badge);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover,
+.repository #repo-files-table .sha.label.isSigned.isWarning:hover,
+.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover,
+.repository #rev-list .sha.label.isSigned.isWarning:hover,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover {
+ background: var(--color-red-badge-hover-bg) !important;
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerified,
+.repository #repo-files-table .sha.label.isSigned.isVerified,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerified,
+.repository #rev-list .sha.label.isSigned.isVerified,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified {
+ border: 1px solid var(--color-green-badge);
+ background: var(--color-green-badge-bg);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon,
+.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon,
+.repository #rev-list .sha.label.isSigned.isVerified .detail.icon,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon {
+ border-left: 1px solid var(--color-green-badge);
+ color: var(--color-green-badge);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover,
+.repository #repo-files-table .sha.label.isSigned.isVerified:hover,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover,
+.repository #rev-list .sha.label.isSigned.isVerified:hover,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover {
+ background: var(--color-green-badge-hover-bg) !important;
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted,
+.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted,
+.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted {
+ border: 1px solid var(--color-yellow-badge);
+ background: var(--color-yellow-badge-bg);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
+.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
+.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon {
+ border-left: 1px solid var(--color-yellow-badge);
+ color: var(--color-yellow-badge);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover,
+.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover,
+.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover {
+ background: var(--color-yellow-badge-hover-bg) !important;
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched,
+.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched,
+.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched {
+ border: 1px solid var(--color-orange-badge);
+ background: var(--color-orange-badge-bg);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
+.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
+.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon {
+ border-left: 1px solid var(--color-orange-badge);
+ color: var(--color-orange-badge);
+}
+
+.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover,
+.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover,
+.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover,
+.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover,
+.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover {
+ background: var(--color-orange-badge-hover-bg) !important;
+}
+
+.repository .data-table {
+ width: 100%;
+}
+
+.repository .data-table tr {
+ border-top: 0;
+}
+
+.repository .data-table td,
+.repository .data-table th {
+ padding: 5px !important;
+ overflow: hidden;
+ font-size: 12px;
+ text-align: left;
+ white-space: nowrap;
+ border: 1px solid var(--color-secondary);
+}
+
+/* the border css competes with .markup where all tables have outer border which would add a double
+ border here, remove only the outer borders from this table */
+.repository .data-table tr:first-child :is(td,th) {
+ border-top: none !important;
+}
+.repository .data-table tr:last-child :is(td,th) {
+ border-bottom: none !important;
+}
+.repository .data-table tr :is(td,th):first-child {
+ border-left: none !important;
+}
+.repository .data-table tr :is(td,th):last-child {
+ border-right: none !important;
+}
+
+.repository .data-table td {
+ white-space: pre-line;
+}
+
+.repository .data-table th {
+ font-weight: var(--font-weight-semibold);
+ background: var(--color-box-header);
+ border-top: 0;
+}
+
+.repository .data-table td.added,
+.repository .data-table th.added,
+.repository .data-table tr.added {
+ background-color: var(--color-diff-added-row-bg) !important;
+}
+
+.repository .data-table td.removed,
+.repository .data-table th.removed,
+.repository .data-table tr.removed {
+ background-color: var(--color-diff-removed-row-bg) !important;
+}
+
+.repository .data-table td.moved,
+.repository .data-table th.moved,
+.repository .data-table tr.moved {
+ background-color: var(--color-diff-moved-row-bg) !important;
+}
+
+.repository .data-table tbody.section {
+ border-top: 2px solid var(--color-secondary);
+}
+
+.repository .data-table .line-num {
+ width: 1%;
+ min-width: 50px;
+ font-family: monospace;
+ line-height: 20px;
+ color: var(--color-text-light-1);
+ white-space: nowrap;
+ vertical-align: top;
+ cursor: pointer;
+ text-align: right;
+ background: var(--color-body);
+ border: 0;
+}
+
+.repository .diff-detail-box {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: sticky;
+ top: 0;
+ z-index: 8;
+ padding: 7px 5px;
+ margin: 0 -5px; /* negative margin so it covers active file shadow */
+ height: 44px; /* this height should match sticky-2nd-row */
+ background: var(--color-body);
+}
+
+@media (max-width: 480px) {
+ .repository .diff-detail-box {
+ flex-wrap: wrap;
+ }
+}
+.repository .diff-detail-box .diff-detail-stats strong {
+ margin-left: 0.25rem;
+ margin-right: 0.25rem;
+}
+
+/* Because the translations contain the <strong> we need to style with nth-of-type */
+
+.repository .diff-detail-box .diff-detail-stats strong:nth-of-type(1) {
+ color: var(--color-yellow);
+}
+
+.repository .diff-detail-box .diff-detail-stats strong:nth-of-type(2) {
+ color: var(--color-green);
+}
+
+.repository .diff-detail-box .diff-detail-stats strong:nth-of-type(3) {
+ color: var(--color-red);
+}
+
+@media (max-width: 800px) {
+ .repository .diff-detail-box .diff-detail-stats {
+ display: none !important;
+ }
+}
+
+.diff-detail-actions {
+ display: flex;
+ align-items: center;
+ justify-content: end;
+}
+
+.diff-detail-actions > *,
+.diff-detail-actions .button {
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+}
+
+.repository .diff-detail-box span.status {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ margin-right: 8px;
+ vertical-align: middle;
+}
+
+.repository .diff-detail-box span.status.modify {
+ background-color: var(--color-yellow);
+}
+
+.repository .diff-detail-box span.status.add {
+ background-color: var(--color-green);
+}
+
+.repository .diff-detail-box span.status.del {
+ background-color: var(--color-red);
+}
+
+.repository .diff-detail-box span.status.rename {
+ background-color: var(--color-teal);
+}
+
+.repository .diff-detail-box .ui.button {
+ padding: 0 1.125em;
+ height: 30px;
+}
+
+.repository .diff-box .header:not(.resolved-placeholder) .file {
+ min-width: 0;
+}
+
+.repository .diff-box .header:not(.resolved-placeholder) .file .file-link {
+ max-width: fit-content;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+}
+
+.repository .diff-box .header:not(.resolved-placeholder) .button {
+ padding: 0 1.125em;
+ flex: 0 0 auto;
+ margin-right: 0;
+ height: 30px;
+}
+
+.repository .diff-box .resolved-placeholder {
+ display: flex;
+ align-items: center;
+ font-size: 14px !important;
+ height: 36px;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+.repository .diff-box .resolved-placeholder .button {
+ padding: 8px 12px;
+}
+
+.repository .diff-file-box .header {
+ background-color: var(--color-box-header);
+}
+
+.repository .diff-file-box .file-body.file-code {
+ background: var(--color-code-bg);
+}
+
+.repository .diff-file-box .file-body.file-code .lines-num {
+ text-align: right;
+ width: 1%;
+ min-width: 50px;
+}
+
+.repository .diff-file-box .file-body.file-code .lines-num span.fold {
+ display: block;
+ text-align: center;
+}
+
+.repository .diff-file-box .code-diff td {
+ padding: 0 0 0 10px !important;
+ border-top: 0;
+}
+
+.repository .diff-file-box .code-diff .lines-num {
+ padding: 0 5px !important;
+}
+
+.repository .diff-file-box .code-diff .tag-code .lines-num,
+.repository .diff-file-box .code-diff .tag-code td {
+ padding: 0 !important;
+}
+
+.repository .diff-file-box .code-diff table {
+ table-layout: fixed;
+}
+
+.repository .diff-file-box .code-diff tbody tr td.lines-num,
+.repository .diff-file-box .code-diff tbody tr td.lines-escape,
+.repository .diff-file-box .code-diff tbody tr td.lines-type-marker {
+ white-space: nowrap;
+}
+
+.repository .diff-file-box .code-diff tbody tr td.center {
+ text-align: center;
+}
+
+.repository .diff-file-box .code-diff tbody tr [data-line-num]::before {
+ content: attr(data-line-num);
+ text-align: right;
+}
+
+.repository .diff-file-box .code-diff tbody tr .lines-type-marker {
+ width: 10px;
+ min-width: 10px;
+}
+
+.repository .diff-file-box .code-diff tbody tr [data-type-marker]::before {
+ content: attr(data-type-marker);
+ text-align: right;
+ display: inline-block;
+}
+
+.repository .diff-file-box .code-diff-split .tag-code .lines-code code.code-inner {
+ padding-left: 10px !important;
+}
+
+.repository .diff-file-box .code-diff-split table,
+.repository .diff-file-box .code-diff-split tbody {
+ width: 100%;
+}
+
+.repository .diff-file-box.file-content {
+ clear: right;
+}
+
+.repository .diff-file-box.file-content .image-diff img {
+ max-width: 100%;
+ padding: 0;
+ border-radius: 0;
+}
+
+.repository .diff-file-box .ui.bottom.attached.table.segment {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+.diff-file-box {
+ border: 1px solid transparent;
+ border-radius: var(--border-radius);
+ scroll-margin-top: 47px; /* match .repository .diff-detail-box */
+}
+
+.file.editor .diff-file-box {
+ border: none;
+}
+
+.file.editor .diff-file-box .ui.attached.table {
+ border: none;
+}
+
+/* TODO: this can potentially be made "global" by removing the class prefix */
+.diff-file-box .ui.attached.header,
+.diff-file-box .ui.attached.table {
+ margin: 0; /* remove fomantic negative margins */
+ width: initial; /* remove fomantic over 100% width */
+ max-width: initial; /* remove fomantic over 100% width */
+}
+
+.repository .diff-stats {
+ clear: both;
+ margin-bottom: 5px;
+ max-height: 200px;
+ height: fit-content;
+ overflow: auto;
+ padding-left: 0;
+}
+
+.repository .diff-stats li {
+ list-style: none;
+ padding-bottom: 4px;
+ margin-bottom: 4px;
+ padding-left: 6px;
+}
+
+.repository .diff-stats li + li {
+ border-top: 1px solid var(--color-secondary);
+}
+
+.repository .repo-search-result {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.repository .repo-search-result .lines-num a {
+ color: inherit;
+}
+
+/* workaround for safari as it does not support 'display: list-item' for summary tags yet
+** see https://codeberg.org/forgejo/forgejo/issues/5259 */
+details.repo-search-result summary::-webkit-details-marker,
+details.repo-search-result summary::marker {
+ display: none;
+}
+
+.search-highlight {
+ background: var(--color-primary-alpha-40);
+}
+
+.repository.quickstart .guide .item {
+ padding: 1em;
+}
+
+.repository.quickstart .guide .item small {
+ font-weight: var(--font-weight-normal);
+}
+
+.repository.quickstart .guide #repo-clone-url {
+ border-radius: 0;
+ padding: 5px 10px;
+ font-size: 1.2em;
+ line-height: 1.4;
+ flex: 1
+}
+
+.empty-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding-top: 40px;
+ padding-bottom: 40px;
+}
+
+.repository.packages .file-size {
+ white-space: nowrap;
+}
+
+.file-view.markup {
+ padding: 2em;
+}
+.repository .activity-header {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+/* if the element is for a checkbox, then it should have a padding-left to align to the checkbox's text */
+.repository.settings.branches .branch-protection .ui.checkbox .help,
+.repository.settings.branches .branch-protection .checkbox-sub-item {
+ padding-left: 26px;
+}
+
+.repository.settings.branches .branch-protection .status-check-matched-mark {
+ font-weight: var(--font-weight-semibold);
+ font-style: italic;
+}
+
+.repository .ui.attached.isSigned.isWarning {
+ border-left: 1px solid var(--color-error-border);
+ border-right: 1px solid var(--color-error-border);
+}
+
+.repository .ui.attached.isSigned.isWarning.top,
+.repository .ui.attached.isSigned.isWarning.message {
+ border-top: 1px solid var(--color-error-border);
+}
+
+.repository .ui.attached.isSigned.isWarning.message {
+ box-shadow: none;
+ background-color: var(--color-error-bg);
+ color: var(--color-error-text);
+}
+
+.repository .ui.attached.isSigned.isWarning.message .ui.text {
+ color: var(--color-error-text);
+}
+
+.repository .ui.attached.isSigned.isWarning:last-child,
+.repository .ui.attached.isSigned.isWarning.bottom {
+ border-bottom: 1px solid var(--color-error-border);
+}
+
+.repository .ui.attached.isSigned.isVerified {
+ border-left: 1px solid var(--color-success-border);
+ border-right: 1px solid var(--color-success-border);
+}
+
+.repository .ui.attached.isSigned.isVerified.top,
+.repository .ui.attached.isSigned.isVerified.message {
+ border-top: 1px solid var(--color-success-border);
+}
+
+.repository .ui.attached.isSigned.isVerified.message {
+ box-shadow: none;
+ background-color: var(--color-success-bg);
+ color: var(--color-success-text);
+}
+
+.repository .ui.attached.isSigned.isVerified.message .pull-right {
+ color: var(--color-text);
+}
+
+.repository .ui.attached.isSigned.isVerified.message .ui.text {
+ color: var(--color-success-text);
+}
+
+.repository .ui.attached.isSigned.isVerified:last-child,
+.repository .ui.attached.isSigned.isVerified.bottom {
+ border-bottom: 1px solid var(--color-success-border);
+}
+
+.repository .ui.attached.isSigned.isVerifiedUntrusted,
+.repository .ui.attached.isSigned.isVerifiedUnmatched {
+ border-left: 1px solid var(--color-warning-border);
+ border-right: 1px solid var(--color-warning-border);
+}
+
+.repository .ui.attached.isSigned.isVerifiedUntrusted.top,
+.repository .ui.attached.isSigned.isVerifiedUnmatched.top,
+.repository .ui.attached.isSigned.isVerifiedUntrusted.message,
+.repository .ui.attached.isSigned.isVerifiedUnmatched.message {
+ border-top: 1px solid var(--color-warning-border);
+}
+
+.repository .ui.attached.isSigned.isVerifiedUntrusted.message,
+.repository .ui.attached.isSigned.isVerifiedUnmatched.message {
+ box-shadow: none;
+ background-color: var(--color-warning-bg);
+ color: var(--color-warning-text);
+}
+
+.repository .ui.attached.isSigned.isVerifiedUntrusted.message .ui.text,
+.repository .ui.attached.isSigned.isVerifiedUnmatched.message .ui.text {
+ color: var(--color-warning-text);
+}
+
+.repository .ui.attached.isSigned.isVerifiedUntrusted:last-child,
+.repository .ui.attached.isSigned.isVerifiedUnmatched:last-child,
+.repository .ui.attached.isSigned.isVerifiedUntrusted.bottom,
+.repository .ui.attached.isSigned.isVerifiedUnmatched.bottom {
+ border-bottom: 1px solid var(--color-warning-border);
+}
+
+.repository .release-tag-name .ui.label.isSigned,
+.repository .release-list-title .ui.label.isSigned {
+ padding: 0 0.5em;
+ box-shadow: none;
+}
+
+.repository .release-tag-name .ui.label.isSigned .avatar,
+.repository .release-list-title .ui.label.isSigned .avatar {
+ margin-left: .5rem;
+}
+
+.repository .release-tag-name .ui.label.isSigned.isVerified,
+.repository .release-list-title .ui.label.isSigned.isVerified {
+ border: 1px solid var(--color-success-border);
+ background-color: var(--color-success-bg);
+ color: var(--color-success-text);
+}
+
+.repository .release-tag-name .ui.label.isSigned.isWarning,
+.repository .release-list-title .ui.label.isSigned.isWarning {
+ border: 1px solid var(--color-warning-border);
+ background-color: var(--color-warning-bg);
+ color: var(--color-warning-text);
+}
+
+.repository .segment.reactions.dropdown .menu,
+.repository .select-reaction.dropdown .menu {
+ right: 0 !important;
+ left: auto !important;
+ min-width: 170px;
+}
+
+.repository .segment.reactions.dropdown .menu > .header,
+.repository .select-reaction.dropdown .menu > .header {
+ margin: 0.75rem 0 0.5rem;
+}
+
+.repository .segment.reactions.dropdown .menu > .item,
+.repository .select-reaction.dropdown .menu > .item {
+ float: left;
+ margin: 4px;
+ font-size: 20px;
+ width: 34px;
+ height: 34px;
+ min-height: 0 !important;
+ border-radius: var(--border-radius);
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+}
+
+.repository .segment.reactions {
+ padding: 0;
+ display: flex;
+ border: none !important;
+ border-top: 1px solid var(--color-secondary) !important;
+ width: 100% !important;
+ max-width: 100% !important;
+ margin: 0 !important;
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+}
+
+.repository .segment.reactions .ui.label {
+ max-height: 40px;
+ padding: 8px 16px !important;
+ display: flex !important;
+ align-items: center;
+ border: 0;
+ border-right: 1px solid;
+ border-radius: 0;
+ margin: 0;
+ font-size: 12px;
+ font-weight: var(--font-weight-normal);
+ border-color: var(--color-secondary) !important;
+ background: var(--color-reaction-bg);
+}
+
+.repository .segment.reactions .ui.label:first-of-type {
+ border-bottom-left-radius: 3px;
+}
+
+.repository .segment.reactions .ui.label.disabled {
+ cursor: default;
+ opacity: 1;
+}
+
+.repository .segment.reactions .ui.label.basic.primary {
+ color: var(--color-primary) !important;
+ background-color: var(--color-reaction-active-bg) !important;
+ border-color: var(--color-secondary-dark-1) !important;
+}
+
+.repository .segment.reactions .ui.label.basic:hover {
+ background-color: var(--color-reaction-hover-bg) !important;
+}
+
+.repository .segment.reactions .reaction-count {
+ margin-left: 0.5rem;
+}
+
+.repository .segment.reactions .select-reaction {
+ display: flex;
+ align-items: center;
+}
+
+.repository .segment.reactions .select-reaction a {
+ padding: 0 14px;
+}
+
+.repository .segment.reactions .select-reaction:not(.active) a {
+ display: none;
+}
+
+.repository .segment.reactions:hover .select-reaction a {
+ display: block;
+}
+
+.repository .ui.fluid.action.input .ui.search.action.input {
+ flex: auto;
+}
+
+.repository .repository-summary {
+ box-shadow: none;
+}
+
+.repository .repository-summary .segment.sub-menu {
+ border: none;
+ display: flex;
+ align-items: center;
+ padding: 0;
+ overflow: hidden;
+}
+
+.repository .repository-summary .sub-menu .item {
+ flex: 1;
+ height: 30px;
+ line-height: var(--line-height-default);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25em;
+ padding: 0 0.5em; /* make the UI look better for narrow (mobile) view */
+ text-decoration: none;
+}
+
+.repository .repository-summary .sub-menu .item.active {
+ background: var(--color-secondary);
+}
+
+.repository .repository-summary .segment.language-stats {
+ display: flex;
+ gap: 2px;
+ padding: 0;
+ height: 10px;
+ white-space: nowrap;
+ border-radius: 0 0 3px 3px !important;
+ overflow: hidden;
+}
+
+#cite-repo-modal #citation-panel {
+ display: flex;
+ width: 100%;
+}
+
+#cite-repo-modal #citation-panel input {
+ border-radius: 0;
+ padding: 5px 10px;
+ width: 50%;
+ line-height: 1.4;
+}
+
+#cite-repo-modal #citation-panel #citation-copy-content {
+ border-radius: 0;
+ padding: 5px 10px;
+ font-size: 1.2em;
+ line-height: 1.4;
+ flex: 1;
+}
+
+#cite-repo-modal #citation-panel #citation-copy-bibtex {
+ font-size: 13px;
+ padding: 7.5px 5px;
+ border-right: none;
+}
+
+#cite-repo-modal #citation-panel #goto-citation-btn {
+ border-left: none;
+}
+
+#cite-repo-modal #citation-panel > :first-child {
+ border-radius: var(--border-radius) 0 0 var(--border-radius) !important;
+}
+
+#cite-repo-modal #citation-panel > :last-child {
+ border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
+}
+
+#cite-repo-modal #citation-panel .icon.button {
+ padding: 0 10px;
+}
+
+.user-cards .list {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 15px;
+ margin: 0 0 10px;
+ padding: 0;
+}
+
+@media (max-width: 767.98px) {
+ .user-cards .list {
+ grid-template-columns: repeat(1, 1fr);
+ }
+}
+
+@media (max-width: 900px) {
+ .user.profile .user-cards .list {
+ grid-template-columns: repeat(1, 1fr);
+ }
+}
+
+.user-cards .list .card {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ margin: 0;
+ padding: 14px;
+ border-radius: 0.28571429rem;
+ border: 1px solid var(--color-secondary);
+ background: var(--color-box-body);
+}
+
+.user-cards .list .card .avatar {
+ width: 48px;
+ height: 48px;
+ margin-right: 14px;
+}
+
+.user-cards .list .card .name {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-weight: var(--font-weight-normal);
+}
+
+.user-cards .list .card .meta {
+ margin-top: 5px;
+}
+
+#search-user-box .results .result .image {
+ order: 0;
+ margin-right: 12px;
+ width: 2em;
+ height: 2em;
+ min-width: 2em;
+ min-height: 2em;
+}
+
+#search-user-box .results .result .content {
+ margin: 0; /* remove margin reserved for avatar because we move it to left via `order: 0` */
+}
+
+.ui.menu .item > img:not(.ui) {
+ width: auto;
+}
+
+.page.buttons {
+ padding-top: 15px;
+}
+
+.commit-header-row,
+.tag-signature-row {
+ min-height: 50px !important;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+}
+
+.ui.attached.message.tag-signature-row {
+ border-radius: var(--border-radius);
+}
+
+.tag-signature-row div {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ display: inline-block !important;
+}
+
+.commit-header-buttons {
+ display: flex;
+ gap: 4px;
+ align-items: flex-start;
+ white-space: nowrap;
+}
+
+@media (max-width: 767.98px) {
+ .commit-header-buttons {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+
+.settings.webhooks .list > .item:not(:first-child),
+.settings.githooks .list > .item:not(:first-child),
+.settings.actions .list > .item:not(:first-child) {
+ padding: 0.25rem 1rem;
+ margin: 12px -1rem -1rem;
+}
+
+.settings .list > .item:not(:first-child) {
+ border-top: 1px solid var(--color-secondary);
+ padding: 1rem;
+ margin: 16px -1rem -1rem;
+}
+
+.settings .list > .item > .svg {
+ display: table-cell;
+}
+
+.settings .list > .item > .svg + .content {
+ display: table-cell;
+ padding: 0 0 0 0.5em;
+ vertical-align: top;
+}
+
+.settings .list > .item .info {
+ margin-top: 10px;
+}
+
+.settings .list > .item .info .tab.segment {
+ border: 0;
+ padding: 10px 0 0;
+}
+
+.ui.vertical.menu .header.item {
+ font-size: 1.1em;
+ background: var(--color-box-header);
+}
+
+.comment:target .comment-container {
+ border-color: var(--color-primary) !important;
+ box-shadow: 0 0 0 3px var(--color-primary-alpha-30) !important;
+}
+
+.comment:target .header::before {
+ border-right-color: var(--color-primary) !important;
+ filter: drop-shadow(-3px 0 0 var(--color-primary-alpha-30)) !important;
+}
+
+.code-comment:target,
+.diff-file-box:target {
+ border-color: var(--color-primary) !important;
+ border-radius: var(--border-radius) !important;
+ box-shadow: 0 0 0 3px var(--color-primary-alpha-30) !important;
+}
+
+.code-comment:target .content {
+ box-shadow: none !important;
+}
+
+.comment-header {
+ border: none !important;
+ background: var(--color-box-header);
+ border-bottom: 1px solid var(--color-secondary) !important;
+ font-weight: var(--font-weight-normal) !important;
+ padding: 0.5rem 1rem;
+ margin: 0 !important;
+ position: relative;
+ color: var(--color-text);
+ min-height: var(--repo-header-issue-min-height);
+ background-color: var(--color-box-header);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.comment-header::before,
+.comment-header::after {
+ right: 100%;
+ top: 20px;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.comment-header::before {
+ border-right-color: var(--color-secondary);
+ border-width: 9px;
+ margin-top: -9px;
+}
+
+.comment-header::after {
+ border-right-color: var(--color-box-header);
+ border-width: 8px;
+ margin-top: -8px;
+}
+
+.comment-header.arrow-top::before,
+.comment-header.arrow-top::after {
+ transform: rotate(90deg);
+}
+
+.comment-header.arrow-top::before {
+ top: -9px;
+ left: 6px;
+}
+
+.comment-header.arrow-top::after {
+ top: -8px;
+ left: 7px;
+}
+
+.comment-header .actions a:not(.label) {
+ padding: 0.5rem !important;
+}
+
+.comment-header .actions .label {
+ margin: 0 !important;
+}
+
+.comment-header-left,
+.comment-header-right {
+ gap: 4px;
+}
+
+.comment-body {
+ background: var(--color-box-body);
+ border: none !important;
+ width: 100% !important;
+ max-width: 100% !important;
+ margin: 0 !important;
+ padding: 1em;
+}
+
+.edit-label.modal .form .column,
+.new-label.modal .form .column {
+ padding-right: 0;
+}
+
+.edit-label.modal .form .buttons,
+.new-label.modal .form .buttons {
+ margin-left: auto;
+ padding-top: 15px;
+}
+
+.stats-table {
+ display: table;
+ width: 100%;
+ margin: 6px 0;
+ border-spacing: 2px;
+}
+
+.stats-table .table-cell {
+ display: table-cell;
+}
+
+.stats-table .table-cell.tiny {
+ height: 8px;
+}
+
+.stats-table .table-cell:first-child {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.stats-table .table-cell:last-child {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.labels-list {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 2.5px;
+ align-items: center;
+}
+
+.labels-list .label {
+ padding: 0 6px;
+ min-height: 20px;
+ line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
+}
+
+/* Scoped labels with different colors on left and right */
+.ui.label.scope-parent {
+ background: none !important;
+ padding: 0 !important;
+ gap: 0 !important;
+}
+
+.ui.label.scope-left {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ margin-right: 0;
+}
+
+.ui.label.scope-right {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+ margin-left: 0;
+}
+
+.archived-label {
+ filter: grayscale(0.25) saturate(0.75);
+}
+
+.repo-button-row {
+ margin: 10px 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.repo-button-row .button {
+ padding: 6px 10px !important;
+ height: 30px;
+}
+
+.repo-button-row .button.dropdown:not(.icon) {
+ padding-right: 22px !important; /* normal buttons have !important paddings, so we need to override it for dropdown (Add File) icons */
+}
+
+.repo-button-row input {
+ height: 30px;
+}
+
+tbody.commit-list {
+ vertical-align: baseline;
+}
+
+.message-wrapper,
+.author-wrapper {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.author-wrapper {
+ max-width: 180px;
+ align-self: center;
+ white-space: nowrap;
+}
+
+/* in the commit list, messages can wrap so we can use inline */
+.commit-list .message-wrapper {
+ display: inline;
+ overflow-wrap: anywhere;
+}
+
+/* but in the repo-files-table we cannot */
+#repo-files-table .commit-list .message-wrapper {
+ display: inline-block;
+}
+
+@media (max-width: 767.98px) {
+ tr.commit-list {
+ width: 100%;
+ }
+ .author-wrapper {
+ max-width: 80px;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+ tr.commit-list {
+ width: 723px;
+ }
+}
+
+@media (min-width: 992px) and (max-width: 1200px) {
+ tr.commit-list {
+ width: 933px;
+ }
+}
+
+@media (min-width: 1201px) {
+ tr.commit-list {
+ width: 1127px;
+ }
+}
+
+.commit-body {
+ margin: 0.25em 0;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ line-height: initial;
+}
+
+.git-notes.top {
+ text-align: left;
+}
+
+.comment-diff-data {
+ background: var(--color-code-bg);
+ min-height: 12em;
+ max-height: calc(100vh - 10.5rem);
+ overflow-y: auto;
+ tab-size: 4;
+}
+
+.comment-diff-data pre {
+ line-height: 18px;
+ margin: 1em;
+ white-space: pre-wrap;
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+
+.content-history-detail-dialog .header .avatar {
+ position: relative;
+ top: -2px;
+}
+
+#repo-topics .repo-topic {
+ font-weight: var(--font-weight-normal);
+ cursor: pointer;
+ margin: 0;
+}
+
+#new-dependency-drop-list.ui.selection.dropdown {
+ min-width: 0;
+ width: 100%;
+ border-radius: var(--border-radius) 0 0 var(--border-radius);
+ border-right: 0;
+ white-space: nowrap;
+}
+
+#new-dependency-drop-list .text {
+ width: 100%;
+ overflow: hidden;
+}
+
+.tag-code {
+ height: 28px;
+}
+
+.tag-code,
+.tag-code td,
+.tag-code .blob-excerpt {
+ background-color: var(--color-box-body-highlight);
+ vertical-align: middle;
+}
+
+.resolved-placeholder {
+ font-weight: var(--font-weight-normal) !important;
+ border: 1px solid var(--color-secondary) !important;
+ border-radius: var(--border-radius) !important;
+ margin: 4px !important;
+}
+
+.resolved-placeholder + .comment-code-cloud {
+ padding-top: 0 !important;
+}
+
+.blob-excerpt {
+ background-color: var(--color-secondary-alpha-30);
+}
+
+.issue-keyword {
+ border-bottom: 1px dotted var(--color-text-light-3) !important;
+}
+
+.issue-keyword:hover {
+ border-bottom: none !important;
+}
+
+.file-header {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ overflow-x: auto;
+ padding: 6px 12px !important;
+ font-size: 13px !important;
+}
+
+.file-info {
+ display: flex;
+ align-items: center;
+}
+
+.file-info-entry {
+ display: flex;
+ align-items: center;
+ width: max-content;
+}
+
+.file-info-entry + .file-info-entry {
+ border-left: 1px solid currentcolor;
+ margin-left: 8px;
+ padding-left: 8px;
+}
+
+#diff-container {
+ display: flex;
+}
+
+#diff-file-boxes {
+ flex: 1;
+ max-width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+#diff-file-tree {
+ flex: 0 0 20%;
+ max-width: 380px;
+ line-height: inherit;
+ position: sticky;
+ padding-top: 0;
+ top: 47px;
+ max-height: calc(100vh - 47px);
+ height: 100%;
+ overflow-y: auto;
+}
+
+.ui.message.unicode-escape-prompt {
+ margin-bottom: 0;
+ border-radius: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+/* fomantic's last-child selector does not work with hidden last child */
+.ui.buttons .unescape-button {
+ border-top-right-radius: 0.28571429rem;
+ border-bottom-right-radius: 0.28571429rem;
+}
+
+.webhook-info {
+ padding: 7px 12px;
+ margin: 10px 0;
+ background-color: var(--color-markup-code-block);
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ font-size: 13px;
+ line-height: 1.5;
+ overflow: auto;
+}
+
+.title_wip_desc {
+ margin-top: 1em;
+}
+
+.sidebar-item-link {
+ display: inline-flex;
+ align-items: center;
+ overflow-wrap: anywhere;
+}
+
+.diff-file-header {
+ padding: 5px 8px !important;
+}
+
+.diff-file-box[data-folded="true"] .diff-file-body {
+ display: none;
+}
+
+.diff-file-box[data-folded="true"] .diff-file-header {
+ border-radius: var(--border-radius) !important;
+}
+
+.ui.attached.header.diff-file-header.sticky-2nd-row {
+ position: sticky;
+ top: 44px; /* match .repository .diff-detail-box */
+ z-index: 7;
+}
+
+.diff-file-name {
+ flex: auto;
+ min-width: 100px;
+}
+
+.diff-file-name .ui.label {
+ margin-left: 0 !important;
+}
+
+.diff-stats-bar {
+ display: inline-block;
+ background-color: var(--color-red);
+ height: 12px;
+ width: 44px;
+}
+
+.diff-stats-bar .diff-stats-add-bar {
+ background-color: var(--color-green);
+ height: 100%;
+}
+
+.ui.form .right .ui.button {
+ margin-left: 0.25em;
+ margin-right: 0;
+}
+
+.removed-code {
+ background: var(--color-diff-removed-word-bg);
+}
+
+.added-code {
+ background: var(--color-diff-added-word-bg);
+}
+
+.code-diff-unified .del-code,
+.code-diff-unified .del-code td,
+.code-diff-split .del-code .lines-num-old,
+.code-diff-split .del-code .lines-escape-old,
+.code-diff-split .del-code .lines-type-marker-old,
+.code-diff-split .del-code .lines-code-old {
+ background: var(--color-diff-removed-row-bg);
+ border-color: var(--color-diff-removed-row-border);
+}
+
+.code-diff-unified .add-code,
+.code-diff-unified .add-code td,
+.code-diff-split .add-code .lines-num-new,
+.code-diff-split .add-code .lines-type-marker-new,
+.code-diff-split .add-code .lines-escape-new,
+.code-diff-split .add-code .lines-code-new,
+.code-diff-split .del-code .add-code.lines-num-new,
+.code-diff-split .del-code .add-code.lines-type-marker-new,
+.code-diff-split .del-code .add-code.lines-escape-new,
+.code-diff-split .del-code .add-code.lines-code-new {
+ background: var(--color-diff-added-row-bg);
+ border-color: var(--color-diff-added-row-border);
+}
+
+.code-diff-split .del-code .lines-num-new,
+.code-diff-split .del-code .lines-type-marker-new,
+.code-diff-split .del-code .lines-code-new,
+.code-diff-split .del-code .lines-escape-new,
+.code-diff-split .add-code .lines-num-old,
+.code-diff-split .add-code .lines-escape-old,
+.code-diff-split .add-code .lines-type-marker-old,
+.code-diff-split .add-code .lines-code-old {
+ background: var(--color-diff-inactive);
+}
+
+.code-diff-split tbody tr td:nth-child(5),
+.code-diff-split tbody tr td.add-comment-right {
+ border-left: 1px solid var(--color-secondary);
+}
+
+.migrate-entries {
+ display: grid !important;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 25px;
+ margin: 0 !important;
+}
+
+@media (max-width: 767.98px) {
+ .migrate-entries {
+ grid-template-columns: repeat(1, 1fr);
+ }
+}
+
+.migrate-entry {
+ transition: all 0.1s ease-in-out;
+ box-shadow: none !important;
+ border: 1px solid var(--color-secondary);
+ color: var(--color-text) !important;
+ width: auto !important;
+ margin: 0 !important;
+}
+
+.migrate-entry:hover {
+ transform: scale(105%);
+ box-shadow: 0 0.5rem 1rem var(--color-shadow) !important;
+}
+
+.migrate-entry .description {
+ text-wrap: balance;
+}
+
+.commits-table .commits-table-right form {
+ display: flex;
+ align-items: center;
+ gap: 0.75em;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+@media (max-width: 767.98px) {
+ .repository.file.list #repo-files-table .entry,
+ .repository.file.list #repo-files-table .commit-list {
+ align-items: center;
+ display: flex !important;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ }
+ .repository.file.list #repo-files-table .entry td.age,
+ .repository.file.list #repo-files-table .commit-list td.age,
+ .repository.file.list #repo-files-table .entry th.age,
+ .repository.file.list #repo-files-table .commit-list th.age {
+ margin-left: auto;
+ }
+ .repository.file.list #repo-files-table .entry td.message,
+ .repository.file.list #repo-files-table .commit-list td.message,
+ .repository.file.list #repo-files-table .entry span.commit-summary,
+ .repository.file.list #repo-files-table .commit-list tr span.commit-summary {
+ display: none !important;
+ }
+ .repository.view.issue .comment-list .timeline,
+ .repository.view.issue .comment-list .timeline-item {
+ margin-left: 0;
+ }
+ .repository.view.issue .comment-list .timeline::before {
+ left: 14px;
+ }
+ .repository.view.issue .comment-list .timeline .inline-timeline-avatar {
+ display: flex;
+ margin-bottom: auto;
+ margin-left: 6px;
+ margin-right: 2px;
+ }
+ .repository.view.issue .comment-list .timeline .comment-header {
+ padding-left: 4px;
+ }
+ .repository.view.issue .comment-list .timeline .comment-header::before,
+ .repository.view.issue .comment-list .timeline .comment-header::after {
+ content: unset;
+ }
+ /* Don't show the general avatar, we show the inline avatar on mobile.
+ * And don't show the role labels, there's no place for that. */
+ .repository.view.issue .comment-list .timeline .timeline-avatar,
+ .repository.view.issue .comment-list .timeline .comment-header-right .role-label {
+ display: none;
+ }
+ .commit-header-row .ui.horizontal.list {
+ width: 100%;
+ overflow-x: auto;
+ margin-top: 2px;
+ }
+ .commit-header-row .ui.horizontal.list .item {
+ align-items: center;
+ display: flex;
+ }
+ .commit-header-row .author {
+ padding: 3px 0;
+ }
+ .commit-header h3 {
+ flex-basis: auto !important;
+ margin-bottom: 0.5rem !important;
+ }
+ .commits-table {
+ flex-direction: column;
+ }
+ .commits-table .commits-table-left {
+ align-items: initial !important;
+ margin-bottom: 6px;
+ }
+ .commits-table .commits-table-right form > div:nth-child(1) {
+ order: 1; /* the "commit search" input */
+ }
+ .commits-table .commits-table-right form > div:nth-child(2) {
+ order: 3; /* the "search all" checkbox */
+ }
+ .commits-table .commits-table-right form > button:nth-child(3) {
+ order: 2; /* the "search" button */
+ }
+ .commit-table {
+ overflow-x: auto;
+ }
+ .commit-table td.sha,
+ .commit-table th.sha {
+ display: none !important;
+ }
+ .comment-header {
+ flex-wrap: wrap;
+ }
+ .comment-header .comment-header-left {
+ flex-wrap: wrap;
+ }
+ .comment-header .comment-header-right {
+ margin-left: auto;
+ }
+}
+
+.branch-dropdown-button {
+ max-width: 340px;
+ vertical-align: bottom !important;
+}
+
+@media (min-width: 768px) and (max-width: 991.98px) {
+ .branch-dropdown-button {
+ max-width: 185px;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .branch-dropdown-button {
+ max-width: 165px;
+ }
+}
+
+.commit-status-header {
+ /* reset the default ".ui.attached.header" styles, to use the outer border */
+ border: none !important;
+ /* add a bottom border to make sure the there is always a divider between the header and list when the list is scrolling */
+ border-bottom: 1px solid var(--color-secondary) !important;
+ /* use negative margin to avoid the newly added border conflict with the list's top border */
+ margin: 0 0 -1px !important;
+}
+
+.commit-status-list {
+ max-height: 240px; /* fit exactly 6 items, commit-status-item.height * 6 */
+ overflow-x: hidden;
+ transition: max-height .2s;
+}
+
+.commit-status-item {
+ height: 40px;
+ padding: 0 10px;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.commit-status-item + .commit-status-item {
+ border-top: 1px solid var(--color-secondary);
+}
+
+.commit-status-item .commit-status {
+ flex-shrink: 0;
+}
+
+.commit-status-item .status-context {
+ color: var(--color-text);
+ flex: 1;
+}
+
+.commit-status-item .status-details {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+@media (max-width: 767.98px) {
+ .commit-status-item .status-details {
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: center;
+ }
+}
+
+.commit-status-item .status-details > span {
+ padding-right: 0.5em; /* To match the alignment with the "required" label */
+}
+
+.search-fullname {
+ color: var(--color-text-light-2);
+}
+
+#issue-pins {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+@media (max-width: 767.98px) {
+ #issue-pins {
+ grid-template-columns: repeat(1, 1fr);
+ }
+}
+
+#cherry-pick-modal .scrolling.menu {
+ max-height: 200px;
+}
+
+/* Branch tag selector - TODO: Merge this into the same selector on repo page */
+.repository .issue-content .issue-content-right .ui.grid .column.row {
+ padding: 10px;
+ padding-bottom: 0;
+}
+.repository .issue-content .issue-content-right .ui.grid .column.muted {
+ padding: 0;
+}
+.repository .issue-content .issue-content-right .ui.grid .column.muted .text {
+ display: inline-block;
+ padding: 10px;
+ width: 100%;
+ text-align: center;
+ border: 1px solid transparent;
+ border-bottom: none;
+}
+.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black {
+ border-color: var(--color-secondary);
+ background: var(--color-menu);
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+}
+.repository .issue-content .issue-content-right .ui.dropdown .scrolling.menu {
+ border-top: none;
+}
+.repository .issue-content .issue-content-right .branch-tag-divider {
+ margin-top: -1px;
+ border-top: 1px solid var(--color-secondary);
+}
+#issue-info-popup .emoji {
+ font-size: inherit;
+ line-height: inherit;
+}
+
+#repo-activity-top-authors-chart {
+ height: 150px; /* Pre-allocate the height that will be taken up by the chart, to avoid the container 'jumping'. */
+}
diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css
new file mode 100644
index 0000000..9da5fe6
--- /dev/null
+++ b/web_src/css/repo/header.css
@@ -0,0 +1,68 @@
+.repository .secondary-nav {
+ padding-top: 12px;
+}
+
+.repository .secondary-nav .fork-flag {
+ margin-top: 0.5rem;
+ font-size: 12px;
+}
+
+.repo-header {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: space-between;
+ gap: 0.5rem;
+ margin-bottom: 4px;
+}
+
+.repo-header .flex-item {
+ padding: 0;
+}
+
+.repo-header .flex-item-main {
+ flex: 0;
+ flex-basis: unset;
+}
+
+.repo-header .flex-item-trailing {
+ flex-wrap: nowrap;
+}
+
+.repo-buttons {
+ align-items: center;
+ display: flex;
+ flex-flow: row wrap;
+ word-break: keep-all;
+}
+
+.repo-buttons .ui.labeled.button > .label:hover {
+ color: var(--color-primary-light-2);
+ background: var(--color-light);
+}
+
+.repo-buttons button[disabled] ~ .label {
+ opacity: var(--opacity-disabled);
+ color: var(--color-text-dark);
+ background: var(--color-light-mimic-enabled) !important;
+}
+
+.repo-buttons button[disabled] ~ .label:hover {
+ color: var(--color-primary-dark-1);
+}
+
+.repo-buttons .ui.labeled.button.disabled {
+ pointer-events: inherit !important;
+}
+
+.repo-buttons .ui.labeled.button.disabled > .label {
+ color: var(--color-text-dark);
+ background: var(--color-light-mimic-enabled) !important;
+}
+
+.repo-buttons .ui.labeled.button.disabled > .label:hover {
+ color: var(--color-primary-dark-1);
+}
+
+.repo-buttons .ui.labeled.button.disabled > .button {
+ pointer-events: none !important;
+}
diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css
new file mode 100644
index 0000000..fb832bd
--- /dev/null
+++ b/web_src/css/repo/issue-card.css
@@ -0,0 +1,40 @@
+.issue-card {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ align-items: stretch;
+ border-radius: var(--border-radius);
+ padding: 8px 10px;
+ border: 1px solid var(--color-secondary);
+ background: var(--color-card);
+}
+
+.issue-card-icon,
+.issue-card-unpin {
+ margin-top: 1px;
+ flex-shrink: 0;
+}
+
+.issue-card-title {
+ flex: 1;
+ font-size: 14px;
+}
+
+.issue-card.sortable-chosen .issue-card-title {
+ cursor: inherit;
+}
+
+.issue-card-bottom {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ gap: 0.25em;
+}
+
+.issue-card-assignees {
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+ justify-content: end;
+ flex-wrap: wrap;
+}
diff --git a/web_src/css/repo/issue-label.css b/web_src/css/repo/issue-label.css
new file mode 100644
index 0000000..9b4b144
--- /dev/null
+++ b/web_src/css/repo/issue-label.css
@@ -0,0 +1,52 @@
+.issue-label-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.issue-label-list .item {
+ border-bottom: 1px solid var(--color-secondary);
+ display: flex;
+ padding: 1em 0;
+ margin: 0;
+}
+
+.issue-label-list .item:first-child {
+ padding-top: 0;
+}
+
+.issue-label-list .item:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.issue-label-list .item .label-title {
+ width: 33%;
+}
+
+.issue-label-list .item .label-issues {
+ width: 33%;
+}
+
+.issue-label-list .item .label-operation {
+ width: 33%;
+}
+
+.issue-label-list .item a {
+ font-size: 12px;
+ padding-right: 10px;
+ color: var(--color-text-light);
+}
+
+.issue-label-list .item.org-label {
+ opacity: 0.7;
+}
+
+.label-operation .label {
+ height: fit-content;
+}
+
+.archived-label-hint {
+ float: right;
+ margin: -12px;
+}
diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css
new file mode 100644
index 0000000..9143b01
--- /dev/null
+++ b/web_src/css/repo/issue-list.css
@@ -0,0 +1,112 @@
+.issue-list-toolbar {
+ display: flex;
+ flex-wrap: wrap-reverse;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.issue-list-toolbar-left {
+ display: flex;
+ align-items: center;
+}
+
+.issue-list-toolbar-right .filter.menu {
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.issue-list-new.button {
+ margin-right: 0;
+}
+
+.list-header-issues {
+ min-height: var(--repo-header-issue-min-height);
+}
+
+@media (max-width: 767.98px) {
+ .issue-list-navbar {
+ order: 0;
+ }
+ .issue-list-new {
+ order: 1;
+ margin-left: auto !important;
+ }
+ .issue-list-search {
+ order: 2 !important;
+ }
+ /* Don't use flex wrap on mobile as it takes too much vertical space.
+ * Only set overflow properties on mobile screens, because while the
+ * CSS trick to pop out from overflowing works on desktop screen, it
+ * has a massive flaw that it cannot inherited any max width from it's 'real'
+ * parent and therefor ends up taking more vertical space than is desired.
+ **/
+ .issue-list-toolbar-right .filter.menu {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
+
+ /* The following few CSS was created with care and built with the information
+ * from CSS-Tricks: https://css-tricks.com/popping-hidden-overflow/
+ */
+
+ /* It's important that every element up to .issue-list-toolbar-right doesn't
+ * have a position set, such that element that wants to pop out will use
+ * .issue-list-toolbar-right as 'clip parent' and thereby avoids the
+ * overflow-y: hidden.
+ */
+ .issue-list-toolbar-right .filter.menu > .dropdown.item {
+ position: initial;
+ }
+ /* It's important that this element and not an child has `position` set.
+ * Set width so that overflow-x knows where to stop overflowing.
+ */
+ .issue-list-toolbar-right {
+ position: relative;
+ width: 100%;
+ }
+}
+
+#issue-list .flex-item-body .branches {
+ display: inline-flex;
+}
+
+#issue-list .flex-item-body .branches .branch {
+ background-color: var(--color-secondary-alpha-50);
+ border-radius: var(--border-radius);
+ padding: 0 4px;
+}
+
+#issue-list .flex-item-body .branches .truncated-name {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 200px;
+ display: inline-block;
+ vertical-align: top;
+}
+
+#issue-list .flex-item-body .checklist progress {
+ margin-left: 2px;
+ width: 80px;
+ height: 6px;
+ display: inline-block;
+}
+
+#issue-list .flex-item-body .checklist progress::-webkit-progress-value {
+ background-color: var(--color-secondary-dark-4);
+}
+
+#issue-list .flex-item-body .checklist progress::-moz-progress-bar {
+ background-color: var(--color-secondary-dark-4);
+}
+
+.archived-label-filter {
+ margin-left: 10px;
+ font-size: 12px;
+ display: flex !important;
+ margin-bottom: 8px;
+ min-width: fit-content;
+}
diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css
new file mode 100644
index 0000000..d32899a
--- /dev/null
+++ b/web_src/css/repo/linebutton.css
@@ -0,0 +1,19 @@
+.code-view .lines-num:hover,
+.file-preview .lines-num:hover {
+ color: var(--color-text-dark) !important;
+}
+
+.code-line-button {
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ padding: 1px 4px !important;
+ position: absolute;
+ font-family: var(--fonts-regular);
+ left: 0;
+ transform: translateX(calc(-50% + 6px));
+ cursor: pointer;
+}
+
+.code-line-button:hover {
+ background: var(--color-secondary) !important;
+}
diff --git a/web_src/css/repo/list-header.css b/web_src/css/repo/list-header.css
new file mode 100644
index 0000000..304cfbc
--- /dev/null
+++ b/web_src/css/repo/list-header.css
@@ -0,0 +1,58 @@
+.list-header {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: .5rem;
+}
+
+.list-header-sort {
+ display: flex;
+ align-items: center;
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.list-header-search {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: center;
+ min-width: 200px; /* to enable flexbox wrapping on mobile */
+}
+
+.list-header-search .input {
+ flex: 1;
+}
+
+.small-menu-items {
+ min-height: 35.4px !important; /* match .small.button in height */
+ background: none !important; /* fomantic sets a color here which does not play well with active transparent color on the item, so unset and set the colors on the item */
+}
+
+.small-menu-items .item {
+ background: var(--color-menu) !important;
+ padding-top: 6px !important;
+ padding-bottom: 6px !important;
+}
+
+.small-menu-items .item:hover {
+ background: var(--color-hover) !important;
+}
+
+.small-menu-items .item.active {
+ background: var(--color-active) !important;
+}
+
+@media (max-width: 767.98px) {
+ .list-header-search {
+ order: 0;
+ }
+ .list-header-toggle {
+ order: 1;
+ }
+ .list-header-sort {
+ order: 2;
+ margin-left: auto;
+ }
+}
diff --git a/web_src/css/repo/release-tag.css b/web_src/css/repo/release-tag.css
new file mode 100644
index 0000000..9860813
--- /dev/null
+++ b/web_src/css/repo/release-tag.css
@@ -0,0 +1,121 @@
+.repository.releases #release-list {
+ margin-top: 12px;
+ padding-top: 12px;
+ padding-left: 0;
+}
+
+.repository.releases #release-list .release-list-title {
+ font-size: 2rem;
+ font-weight: var(--font-weight-normal);
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+ margin: 0;
+}
+
+.repository.releases #release-list > li .meta {
+ padding-top: 25px;
+ position: relative;
+ text-align: right;
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+}
+
+.repository.releases #release-list > li .detail {
+ padding-bottom: 20px;
+ border-left: 1px solid var(--color-secondary);
+}
+
+.repository.releases #release-list > li .detail .author img {
+ margin-bottom: 2px; /* the legacy trick to align the avatar vertically, no better solution at the moment */
+}
+
+.repository.releases #release-list > li .detail .download .list {
+ padding-left: 0;
+}
+
+.repository.releases #release-list > li .detail .download .list li {
+ background: var(--color-light);
+ border: 1px solid var(--color-secondary);
+ border-top: none;
+ display: flex;
+ justify-content: space-between;
+ padding: 8px;
+}
+
+.repository.releases #release-list > li .detail .download .list :is(li:first-child, .start-gap + hr + li) {
+ border-top: 1px solid var(--color-secondary);
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+}
+
+.repository.releases #release-list > li .detail .download .list :is(li:last-child, .start-gap) {
+ border-bottom: 1px solid var(--color-secondary);
+ border-bottom-left-radius: var(--border-radius);
+ border-bottom-right-radius: var(--border-radius);
+}
+
+.repository.releases #release-list > li .detail .download .list hr {
+ height: 8px;
+ margin: 0;
+}
+
+.repository.releases #release-list > li .detail .dot {
+ width: 10px;
+ height: 10px;
+ background-color: var(--color-secondary-dark-3);
+ position: absolute;
+ left: -5.5px;
+ top: 30px;
+ border-radius: var(--border-radius-full);
+ border: 2.5px solid var(--color-body);
+}
+
+.repository.tags #tags-table .tag {
+ padding: 8px 12px;
+}
+
+.repository.tags #tags-table .release-tag-name {
+ font-size: 18px;
+ font-weight: var(--font-weight-normal);
+}
+
+.repository.new.release .target {
+ min-width: 500px;
+}
+
+.repository.new.release .target #tag-name {
+ margin-top: -4px;
+}
+
+.repository.new.release .target .at {
+ margin-left: -5px;
+ margin-right: 5px;
+}
+
+.repository.new.release .target .selection.dropdown {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+@media (max-width: 438px) {
+ .repository.new.release .field button,
+ .repository.new.release .field input {
+ width: 100%;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .repository.new.release .field button {
+ margin-bottom: 1em;
+ }
+}
+
+.repository.new.release .field .attachment_edit {
+ max-width: 48em;
+}
+
+.repository.new.release .markup {
+ min-height: 240px;
+}
diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css
new file mode 100644
index 0000000..ba502d3
--- /dev/null
+++ b/web_src/css/repo/wiki.css
@@ -0,0 +1,72 @@
+.repository.wiki .wiki-pages-list tr:hover {
+ background-color: var(--color-hover);
+}
+
+.repository.wiki .wiki-pages-list .wiki-git-entry {
+ margin-left: 10px;
+ display: none;
+}
+
+.repository.wiki .wiki-pages-list tr:hover .wiki-git-entry {
+ display: inline-block;
+}
+
+.repository.wiki .markup {
+ overflow: visible;
+}
+
+.repository.wiki .markup[data-tab-panel="markdown-previewer"] {
+ min-height: 340px; /* This height matches the markdown editor's height */
+}
+
+.repository.wiki .wiki-content-parts .markup {
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+ padding: 1em;
+ margin-top: 1em;
+ font-size: 1em;
+}
+
+.repository.wiki .wiki-content-main.with-sidebar {
+ float: left;
+ width: 80%;
+ max-width: calc(100% - 150px - 1em); /* match the min-width of .wiki-content-sidebar */
+}
+
+.repository.wiki .wiki-content-sidebar {
+ float: right;
+ width: calc(20% - 1em);
+ min-width: 150px;
+}
+
+.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p {
+ display: none;
+}
+
+.repository.wiki .wiki-content-footer {
+ margin-top: 1em;
+}
+
+.repository.wiki .wiki-content-toc ul {
+ margin: 0;
+ list-style: none;
+ padding: 5px 0 5px 1em;
+}
+
+.repository.wiki .wiki-content-toc ul ul {
+ border-left: 1px var(--color-secondary);
+ border-left-style: dashed;
+}
+
+@media (max-width: 767.98px) {
+ .repository.wiki .clone-panel #repo-clone-url {
+ width: 160px;
+ }
+ .repository.wiki .wiki-content-main.with-sidebar,
+ .repository.wiki .wiki-content-sidebar {
+ float: none;
+ width: 100%;
+ min-width: unset;
+ max-width: unset;
+ }
+}
diff --git a/web_src/css/review.css b/web_src/css/review.css
new file mode 100644
index 0000000..a198edf
--- /dev/null
+++ b/web_src/css/review.css
@@ -0,0 +1,297 @@
+.show-outdated,
+.hide-outdated {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.ui.button.add-code-comment {
+ padding: 2px;
+ position: absolute;
+ margin-left: -22px;
+ z-index: 5;
+ opacity: 0;
+ transition: transform 0.1s ease-in-out;
+ transform: scale(1);
+ box-shadow: none !important;
+ border: none !important;
+}
+
+.ui.button.add-code-comment:hover {
+ transform: scale(1.1);
+}
+
+.lines-escape .toggle-escape-button::before {
+ visibility: visible;
+ content: "⚠️";
+ font-family: var(--fonts-emoji);
+ color: var(--color-red);
+}
+
+.repository .diff-file-box .code-diff td.lines-escape {
+ padding-left: 0 !important;
+}
+
+.diff-file-box .lines-code:hover .ui.button.add-code-comment {
+ opacity: 1;
+}
+
+.ui.button.add-code-comment:focus {
+ opacity: 1;
+}
+
+.repository .diff-file-box .code-diff .add-comment-left,
+.repository .diff-file-box .code-diff .add-comment-right,
+.repository .diff-file-box .code-diff .add-code-comment .add-comment-left,
+.repository .diff-file-box .code-diff .add-code-comment .add-comment-right,
+.repository .diff-file-box .code-diff .add-code-comment .lines-type-marker {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+}
+
+.add-comment-left.add-comment-right .ui.attached.header {
+ border: 1px solid var(--color-secondary);
+}
+
+.add-comment-left.add-comment-right .ui.attached.header:not(.top) {
+ margin-bottom: 0.5em;
+}
+
+.show-outdated:hover,
+.hide-outdated:hover {
+ text-decoration: underline;
+}
+
+.comment-code-cloud {
+ padding: 0.5rem 1rem !important;
+ position: relative;
+}
+
+.code-diff .conversation-holder .comment-code-cloud {
+ max-width: 820px;
+}
+
+@media (max-width: 767.98px) {
+ .comment-code-cloud {
+ max-width: none;
+ padding: 0.75rem !important;
+ }
+ .comment-code-cloud .code-comment-buttons {
+ margin: 0.5rem 0 0.25rem !important;
+ }
+ .comment-code-cloud .code-comment-buttons .code-comment-buttons-buttons {
+ width: 100%;
+ }
+ .comment-code-cloud .ui.buttons {
+ width: 100%;
+ margin: 0 !important;
+ }
+ .comment-code-cloud .ui.buttons .button {
+ flex: 1;
+ }
+}
+
+.comment-code-cloud .comments .comment {
+ padding: 0;
+}
+
+@media (max-width: 767.98px) {
+ .comment-code-cloud .comments .comment .comment-header-right.actions .ui.basic.label {
+ display: none;
+ }
+ .comment-code-cloud .comments .comment .avatar {
+ width: auto;
+ float: none;
+ margin: 0 0.5rem 0 0;
+ flex-shrink: 0;
+ }
+ .comment-code-cloud .comments .comment .avatar ~ .content {
+ margin-left: 1em;
+ }
+ .comment-code-cloud .comments .comment img.avatar {
+ margin: 0 !important;
+ }
+ .comment-code-cloud .comments .comment .comment-content {
+ margin-left: 0 !important;
+ }
+ .comment-code-cloud .comments .comment .comment-container {
+ width: 100%;
+ }
+ .comment-code-cloud .comments .comment.code-comment {
+ padding: 0 0 0.5rem !important;
+ }
+}
+
+.comment-code-cloud .attached.tab {
+ border: 0;
+ padding: 0;
+ margin: 0;
+}
+
+.comment-code-cloud .attached.header {
+ padding: 1px 8px 1px 12px;
+}
+
+.comment-code-cloud .attached.header .text {
+ margin: 0;
+}
+
+.comment-code-cloud .right.menu.options .item {
+ padding: 0.85714286em 0.442857em;
+ cursor: pointer;
+}
+
+.comment-code-cloud .ui.active.tab {
+ padding: 0.5em;
+}
+
+.comment-code-cloud .ui.active.tab.markup {
+ padding: 1em;
+ min-height: 168px;
+}
+
+.comment-code-cloud .ui.tab.markup {
+ font-size: 14px;
+}
+
+.comment-code-cloud .ui.tabular.menu {
+ margin: 0.5em;
+}
+
+.comment-code-cloud .editor-statusbar {
+ display: none;
+}
+
+.comment-code-cloud .footer {
+ padding: 10px 0;
+}
+
+.comment-code-cloud .footer .markup-info {
+ display: inline-block;
+ margin: 5px 0;
+ font-size: 12px;
+ color: var(--color-text-light);
+}
+
+.comment-code-cloud .footer .ui.right.floated {
+ padding-top: 6px;
+}
+
+.comment-code-cloud .footer::after {
+ clear: both;
+ content: "";
+ display: block;
+}
+
+@media (max-width: 767.98px) {
+ .comment-code-cloud .button {
+ width: 100%;
+ margin: 0 !important;
+ margin-bottom: 0.75rem !important;
+ }
+}
+
+.diff-file-body .comment-form {
+ margin: 0 0 0 3em;
+}
+
+.diff-file-body.binary {
+ padding: 5px 10px;
+}
+
+.file-comment {
+ color: var(--color-text);
+}
+
+.code-expander-button {
+ border: none;
+ color: var(--color-text-light);
+ height: 28px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ background: var(--color-expand-button);
+ flex: 1;
+}
+
+.code-expander-button:hover {
+ background: var(--color-primary);
+ color: var(--color-primary-contrast);
+}
+
+.review-box-panel .ui.segment {
+ border: none;
+}
+
+/* See the comment of createCommentEasyMDE() for the review editor */
+/* EasyMDE's options can not handle minHeight & maxHeight together correctly, we have to set minHeight in JS code */
+.review-box-panel .CodeMirror-scroll {
+ min-height: 80px;
+ max-height: calc(100vh - 360px);
+}
+
+.review-box-panel .combo-markdown-editor {
+ width: 730px; /* this width matches current EasyMDE's toolbar's width */
+ max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
+}
+
+#review-box {
+ position: relative;
+}
+
+#review-box .review-comments-counter {
+ background-color: var(--color-primary-light-4);
+ color: var(--color-primary-contrast);
+}
+
+#review-box:hover .review-comments-counter {
+ background-color: var(--color-primary-light-5);
+}
+
+#review-box .review-comments-counter[data-pending-comment-number="0"] {
+ display: none;
+}
+
+.pull.files.diff .comment {
+ scroll-margin-top: 99px;
+}
+
+@media (max-width: 991.98px) {
+ .pull.files.diff .comment {
+ scroll-margin-top: 130px;
+ }
+}
+
+.changed-since-last-review {
+ border: 1px var(--color-accent) solid;
+ background-color: var(--color-small-accent);
+ border-radius: var(--border-radius);
+ padding: 4px 8px;
+ margin: -8px 0; /* just like other buttons in the diff box header */
+ font-size: 0.857rem; /* just like .ui.tiny.button */
+}
+
+.viewed-file-form {
+ display: flex;
+ align-items: center;
+ border: 1px solid transparent;
+ padding: 4px 8px;
+ margin: -8px 0; /* just like other buttons in the diff box header */
+ border-radius: var(--border-radius);
+ font-size: 0.857rem; /* just like .ui.tiny.button */
+}
+
+.viewed-file-form input {
+ margin-right: 4px;
+}
+
+.viewed-file-checked-form {
+ background-color: var(--color-small-accent);
+ border-color: var(--color-accent);
+}
+
+#viewed-files-summary {
+ width: 100%;
+ height: 8px;
+}
diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css
new file mode 100644
index 0000000..0f54779
--- /dev/null
+++ b/web_src/css/shared/flex-list.css
@@ -0,0 +1,108 @@
+.flex-list {
+ list-style: none;
+}
+
+.flex-item {
+ display: flex;
+ gap: 8px;
+ align-items: flex-start;
+ padding: 10px 0;
+}
+
+.flex-item .flex-item-leading {
+ display: flex;
+ align-items: flex-start;
+}
+
+.flex-item .flex-item-main {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ flex-basis: 60%; /* avoid wrapping the "flex-item-trailing" too aggressively */
+ min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */
+}
+
+.flex-item-header {
+ display: flex;
+ gap: .25rem;
+ justify-content: space-between;
+ flex-wrap: wrap;
+}
+
+.flex-item a:not(.label, .button):hover {
+ color: var(--color-primary) !important;
+}
+
+.flex-item .flex-item-icon {
+ align-self: baseline; /* mainly used by the issue list, to align the leading icon with the title */
+}
+
+.flex-item .flex-item-icon + .flex-item-main {
+ align-self: baseline;
+}
+
+.flex-item .flex-item-trailing {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ flex-grow: 0;
+ flex-wrap: wrap;
+ justify-content: end;
+}
+
+.flex-item .flex-item-title {
+ display: inline-flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: .25rem;
+ max-width: 100%;
+ color: var(--color-text);
+ font-size: 16px;
+ font-weight: var(--font-weight-semibold);
+ overflow-wrap: anywhere;
+ min-width: 0;
+}
+
+.flex-item .flex-item-title a {
+ color: var(--color-text);
+ overflow-wrap: anywhere;
+}
+
+.flex-item .flex-item-body {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: .25rem;
+ color: var(--color-text-light-2);
+ overflow-wrap: anywhere;
+}
+
+.flex-item .flex-item-body a {
+ color: inherit;
+ overflow-wrap: anywhere;
+}
+
+.flex-list > .flex-item + .flex-item {
+ border-top: 1px solid var(--color-secondary);
+}
+
+/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `tw-hidden` siblings).
+Developers could also use "flex-space-fitted" class to remove the first item's padding-top and the last item's padding-bottom */
+.flex-list.flex-space-fitted > .flex-item:first-child,
+.ui.segment > .flex-list > .flex-item:first-child {
+ padding-top: 0;
+}
+
+.flex-list.flex-space-fitted > .flex-item:last-child,
+.ui.segment > .flex-list > .flex-item:last-child {
+ padding-bottom: 0;
+}
+
+/* If there is a divider besides the flex-list, some padding/margin are not needs */
+.divider + .flex-list > .flex-item:first-child {
+ padding-top: 0;
+}
+
+.flex-list + .divider {
+ margin-top: 0;
+}
diff --git a/web_src/css/shared/milestone.css b/web_src/css/shared/milestone.css
new file mode 100644
index 0000000..91e6b5e
--- /dev/null
+++ b/web_src/css/shared/milestone.css
@@ -0,0 +1,62 @@
+.milestone-list {
+ list-style: none;
+}
+
+.milestone-card {
+ width: 100%;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.milestone-card + .milestone-card {
+ border-top: 1px solid var(--color-secondary);
+}
+
+.milestone-card .content {
+ padding-top: 10px;
+}
+
+.milestone-header progress {
+ width: 200px;
+ height: 16px;
+}
+
+.milestone-header {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.milestone-toolbar {
+ padding-top: 5px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ justify-content: space-between;
+}
+
+.milestone-toolbar .group {
+ color: var(--color-text-light-2);
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.milestone-toolbar .group > a {
+ font-size: 15px;
+ color: var(--color-text-light-2);
+}
+
+.milestone-toolbar .group > a:hover {
+ color: var(--color-text);
+}
+
+@media (max-width: 767.98px) {
+ .milestone-card {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+}
diff --git a/web_src/css/shared/repoorg.css b/web_src/css/shared/repoorg.css
new file mode 100644
index 0000000..5573ae4
--- /dev/null
+++ b/web_src/css/shared/repoorg.css
@@ -0,0 +1,18 @@
+.repository .head .ui.header .text,
+.organization .head .ui.header .text {
+ vertical-align: middle;
+ font-size: 1.6rem;
+ margin-left: 15px;
+}
+
+.repository .ui.tabs.container,
+.organization .ui.tabs.container {
+ margin-top: 14px;
+ margin-bottom: 0;
+}
+
+.repository .head .ui.header .org-visibility .label,
+.organization .head .ui.header .org-visibility .label {
+ margin-left: 5px;
+ margin-top: 5px;
+}
diff --git a/web_src/css/shared/settings.css b/web_src/css/shared/settings.css
new file mode 100644
index 0000000..33f8861
--- /dev/null
+++ b/web_src/css/shared/settings.css
@@ -0,0 +1,37 @@
+details.toggleable-item {
+ user-select: none !important;
+ padding: 0 !important;
+}
+
+details.toggleable-item .menu {
+ margin: 4px 0 10px !important;
+}
+
+details.toggleable-item summary {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.92857143em 1.14285714em;
+}
+
+details.toggleable-item summary::marker, /* Chrome, Edge, Firefox */
+details.toggleable-item summary::-webkit-details-marker /* Safari */ {
+ display: none;
+}
+
+details.toggleable-item summary::after {
+ transition: transform 0.25s ease;
+ content: "";
+ width: 14px;
+ height: 14px;
+ mask-size: cover;
+ -webkit-mask-size: cover;
+ mask-image: var(--octicon-chevron-right);
+ -webkit-mask-image: var(--octicon-chevron-right);
+ background: currentcolor;
+ border: 1px solid var(--color-body); /* workaround https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 */
+}
+
+details.toggleable-item[open] summary::after {
+ transform: rotate(90deg);
+}
diff --git a/web_src/css/standalone/devtest.css b/web_src/css/standalone/devtest.css
new file mode 100644
index 0000000..a7b00e1
--- /dev/null
+++ b/web_src/css/standalone/devtest.css
@@ -0,0 +1,16 @@
+.button-sample-groups {
+ margin: 0; padding: 0;
+}
+
+.button-sample-groups .sample-group {
+ list-style: none; margin: 0; padding: 0;
+}
+
+.button-sample-groups .sample-group .ui.button {
+ margin-bottom: 5px;
+}
+
+h1, h2 {
+ margin: 0;
+ padding: 10px 0;
+}
diff --git a/web_src/css/standalone/swagger.css b/web_src/css/standalone/swagger.css
new file mode 100644
index 0000000..c32e392
--- /dev/null
+++ b/web_src/css/standalone/swagger.css
@@ -0,0 +1,42 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ background: #fff;
+}
+
+.swagger-back-link {
+ color: #1f69c0;
+ text-decoration: none;
+ position: absolute;
+ top: 1rem;
+ right: 1.5rem;
+ display: flex;
+ align-items: center;
+}
+
+.swagger-back-link:hover {
+ text-decoration: underline;
+}
+
+.swagger-back-link svg {
+ color: inherit;
+ fill: currentcolor;
+ margin-right: 0.5rem;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background: #1e1e1e;
+ }
+ .swagger-ui, .swagger-back-link {
+ filter: invert(88%) hue-rotate(180deg);
+ }
+ .swagger-ui .microlight {
+ filter: invert(100%) hue-rotate(180deg);
+ }
+}
diff --git a/web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css b/web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css
new file mode 100644
index 0000000..5f97fa3
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-auto-deuteranopia-protanopia.css
@@ -0,0 +1,2 @@
+@import "theme-forgejo-light-deuteranopia-protanopia.css";
+@import "theme-forgejo-dark-deuteranopia-protanopia.css" (prefers-color-scheme: dark);
diff --git a/web_src/css/themes/theme-forgejo-auto-tritanopia.css b/web_src/css/themes/theme-forgejo-auto-tritanopia.css
new file mode 100644
index 0000000..256a703
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-auto-tritanopia.css
@@ -0,0 +1,2 @@
+@import "theme-forgejo-light-tritanopia.css";
+@import "theme-forgejo-dark-tritanopia.css" (prefers-color-scheme: dark);
diff --git a/web_src/css/themes/theme-forgejo-auto.css b/web_src/css/themes/theme-forgejo-auto.css
new file mode 100644
index 0000000..ebf5994
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-auto.css
@@ -0,0 +1,2 @@
+@import "theme-forgejo-light.css";
+@import "theme-forgejo-dark.css" (prefers-color-scheme: dark);
diff --git a/web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css b/web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css
new file mode 100644
index 0000000..b30cfd6
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-dark-deuteranopia-protanopia.css
@@ -0,0 +1,11 @@
+@import "./theme-forgejo-dark.css";
+
+:root {
+ --color-diff-removed-word-bg: #693f17;
+ --color-diff-removed-row-border: #693f17;
+ --color-diff-removed-row-bg: #221b17;
+ --color-diff-added-word-bg: #214d88;
+ --color-diff-added-row-border: #214d88;
+ --color-diff-added-row-bg: #13233a;
+ --color-code-bg: #0d1117;
+}
diff --git a/web_src/css/themes/theme-forgejo-dark-tritanopia.css b/web_src/css/themes/theme-forgejo-dark-tritanopia.css
new file mode 100644
index 0000000..aefdaa1
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-dark-tritanopia.css
@@ -0,0 +1,11 @@
+@import "./theme-forgejo-dark.css";
+
+:root {
+ --color-diff-removed-word-bg: #792e2e;
+ --color-diff-removed-row-border: #792e2e;
+ --color-diff-removed-row-bg: #25171c;
+ --color-diff-added-word-bg: #214d88;
+ --color-diff-added-row-border: #214d88;
+ --color-diff-added-row-bg: #13233a;
+ --color-code-bg: #0d1117;
+}
diff --git a/web_src/css/themes/theme-forgejo-dark.css b/web_src/css/themes/theme-forgejo-dark.css
new file mode 100644
index 0000000..bc321f3
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-dark.css
@@ -0,0 +1,357 @@
+@import "../chroma/dark.css";
+@import "../codemirror/dark.css";
+@import "../markup/dark.css";
+
+:root {
+ --steel-900: #10161d;
+ --steel-850: #131a21;
+ --steel-800: #171e26;
+ --steel-750: #1d262f;
+ --steel-700: #242d38;
+ --steel-650: #2b3642;
+ --steel-600: #374351;
+ --steel-550: #445161;
+ --steel-500: #515f70;
+ --steel-450: #5f6e80;
+ --steel-400: #6d7d8f;
+ --steel-350: #7c8c9f;
+ --steel-300: #8c9caf;
+ --steel-250: #9dadc0;
+ --steel-200: #aebed0;
+ --steel-150: #c0cfe0;
+ --steel-100: #d2e0f0;
+ --is-dark-theme: true;
+ --color-primary: #fb923c;
+ --color-primary-contrast: #000;
+ --color-primary-dark-1: #fdba74;
+ --color-primary-dark-2: #fdba74;
+ --color-primary-dark-3: #fed7aa;
+ --color-primary-dark-4: #fed7aa;
+ --color-primary-dark-5: #ffedd5;
+ --color-primary-dark-6: #ffedd5;
+ --color-primary-dark-7: #fff7ed;
+ --color-primary-light-1: #f97316;
+ --color-primary-light-2: #ea580c;
+ --color-primary-light-3: #c2410c;
+ --color-primary-light-4: #9a3412;
+ --color-primary-light-5: #9a3412;
+ --color-primary-light-6: #7c2d12;
+ --color-primary-light-7: #7c2d12;
+ --color-primary-alpha-10: #ea580c19;
+ --color-primary-alpha-20: #ea580c33;
+ --color-primary-alpha-30: #ea580c4b;
+ --color-primary-alpha-40: #ea580c66;
+ --color-primary-alpha-50: #ea580c80;
+ --color-primary-alpha-60: #ea580c99;
+ --color-primary-alpha-70: #ea580cb3;
+ --color-primary-alpha-80: #ea580ccc;
+ --color-primary-alpha-90: #ea580ce1;
+ --color-primary-hover: var(--color-primary-light-1);
+ --color-primary-active: var(--color-primary-light-2);
+ --color-secondary: var(--steel-700);
+ --color-secondary-dark-1: var(--steel-550);
+ --color-secondary-dark-2: var(--steel-500);
+ --color-secondary-dark-3: var(--steel-450);
+ --color-secondary-dark-4: var(--steel-400);
+ --color-secondary-dark-5: var(--steel-350);
+ --color-secondary-dark-6: var(--steel-300);
+ --color-secondary-dark-7: var(--steel-250);
+ --color-secondary-dark-8: var(--steel-200);
+ --color-secondary-dark-9: var(--steel-150);
+ --color-secondary-dark-10: var(--steel-100);
+ --color-secondary-dark-11: var(--steel-100);
+ --color-secondary-dark-12: var(--steel-100);
+ --color-secondary-dark-13: var(--steel-100);
+ --color-secondary-light-1: var(--steel-650);
+ --color-secondary-light-2: var(--steel-700);
+ --color-secondary-light-3: var(--steel-750);
+ --color-secondary-light-4: var(--steel-800);
+ --color-secondary-alpha-10: #2b364219;
+ --color-secondary-alpha-20: #2b364233;
+ --color-secondary-alpha-30: #2b36424b;
+ --color-secondary-alpha-40: #2b364266;
+ --color-secondary-alpha-50: #2b364280;
+ --color-secondary-alpha-60: #2b364299;
+ --color-secondary-alpha-70: #2b3642b3;
+ --color-secondary-alpha-80: #2b3642cc;
+ --color-secondary-alpha-90: #2b3642e1;
+ --color-secondary-hover: var(--color-secondary-light-1);
+ --color-secondary-active: var(--color-secondary-light-2);
+ /* console colors - used for actions console and console files */
+ --color-console-fg: #eeeff2;
+ --color-console-fg-subtle: #959cab;
+ --color-console-bg: #1f212b;
+ --color-console-border: #383c47;
+ --color-console-hover-bg: #ffffff16;
+ --color-console-active-bg: #454a57;
+ --color-console-menu-bg: #383c47;
+ --color-console-menu-border: #5c6374;
+ /* colors */
+ --color-red: #b91c1c;
+ --color-orange: #ea580c;
+ --color-yellow: #ca8a04;
+ --color-olive: #91a313;
+ --color-green: #15803d;
+ --color-teal: #0d9488;
+ --color-blue: #2563eb;
+ --color-violet: #7c3aed;
+ --color-purple: #9333ea;
+ --color-pink: #db2777;
+ --color-brown: #a47252;
+ --color-grey: var(--steel-500);
+ --color-black: #111827;
+ /* light variants */
+ --color-red-light: #dc2626;
+ --color-orange-light: #f97316;
+ --color-yellow-light: #eab308;
+ --color-olive-light: #839311;
+ --color-green-light: #16a34a;
+ --color-teal-light: #14b8a6;
+ --color-blue-light: #3b82f6;
+ --color-violet-light: #8b5cf6;
+ --color-purple-light: #a855f7;
+ --color-pink-light: #ec4899;
+ --color-brown-light: #94674a;
+ --color-grey-light: var(--steel-300);
+ --color-black-light: #1f2937;
+ /* dark 1 variants produced via Sass scale-color(color, $lightness: -10%) */
+ --color-red-dark-1: #a71919;
+ --color-orange-dark-1: #d34f0b;
+ --color-yellow-dark-1: #b67c04;
+ --color-olive-dark-1: #839311;
+ --color-green-dark-1: #137337;
+ --color-teal-dark-1: #0c857a;
+ --color-blue-dark-1: #1554e0;
+ --color-violet-dark-1: #6a1feb;
+ --color-purple-dark-1: #8519e7;
+ --color-pink-dark-1: #c7216b;
+ --color-brown-dark-1: #94674a;
+ --color-black-dark-1: #0f1623;
+ /* dark 2 variants produced via Sass scale-color(color, $lightness: -20%) */
+ --color-red-dark-2: #941616;
+ --color-orange-dark-2: #bb460a;
+ --color-yellow-dark-2: #ca8a04;
+ --color-olive-dark-2: #91a313;
+ --color-green-dark-2: #15803d;
+ --color-teal-dark-2: #0a766d;
+ --color-blue-dark-2: #2563eb;
+ --color-violet-dark-2: #5c14d8;
+ --color-purple-dark-2: #7c3aed;
+ --color-pink-dark-2: #b11d5f;
+ --color-brown-dark-2: #a47252;
+ --color-black-dark-2: #111827;
+ /* ansi colors used for actions console and console files */
+ --color-ansi-black: #1d2328;
+ --color-ansi-red: #cc4848;
+ --color-ansi-green: #87ab63;
+ --color-ansi-yellow: #cc9903;
+ --color-ansi-blue: #3a8ac6;
+ --color-ansi-magenta: #d22e8b;
+ --color-ansi-cyan: #00918a;
+ --color-ansi-white: var(--color-console-fg-subtle);
+ --color-ansi-bright-black: #424851;
+ --color-ansi-bright-red: #d15a5a;
+ --color-ansi-bright-green: #93b373;
+ --color-ansi-bright-yellow: #eaaf03;
+ --color-ansi-bright-blue: #4e96cc;
+ --color-ansi-bright-magenta: #d74397;
+ --color-ansi-bright-cyan: #00b6ad;
+ --color-ansi-bright-white: var(--color-console-fg);
+ /* other colors */
+ --color-gold: #b1983b;
+ --color-white: #ffffff;
+ --color-pure-black: #000000;
+ --color-diff-removed-word-bg: #783030;
+ --color-diff-added-word-bg: #255c39;
+ --color-diff-removed-row-bg: #432121;
+ --color-diff-moved-row-bg: #825718;
+ --color-diff-added-row-bg: #1b3625;
+ --color-diff-removed-row-border: #783030;
+ --color-diff-moved-row-border: #a67a1d;
+ --color-diff-added-row-border: #255c39;
+ --color-diff-inactive: var(--steel-650);
+ --color-error-border: #783030;
+ --color-error-bg: #5f2525;
+ --color-error-bg-active: #783030;
+ --color-error-bg-hover: #783030;
+ --color-error-text: #fef2f2;
+ --color-success-border: #1f6e3c;
+ --color-success-bg: #1d462c;
+ --color-success-text: #aef0c2;
+ --color-warning-border: #a67a1d;
+ --color-warning-bg: #644821;
+ --color-warning-text: #fff388;
+ --color-info-border: #2e50b0;
+ --color-info-bg: #2a396b;
+ --color-info-text: var(--steel-100);
+ --color-red-badge: #b91c1c;
+ --color-red-badge-bg: #b91c1c22;
+ --color-red-badge-hover-bg: #b91c1c44;
+ --color-green-badge: #16a34a;
+ --color-green-badge-bg: #16a34a22;
+ --color-green-badge-hover-bg: #16a34a44;
+ --color-yellow-badge: #ca8a04;
+ --color-yellow-badge-bg: #ca8a0422;
+ --color-yellow-badge-hover-bg: #ca8a0444;
+ --color-orange-badge: #ea580c;
+ --color-orange-badge-bg: #ea580c22;
+ --color-orange-badge-hover-bg: #ea580c44;
+ --color-git: #f05133;
+ /* Icon colors (PR/Issue/...) */
+ --color-icon-green: #3fb950;
+ --color-icon-red: #f85149;
+ --color-icon-purple: #aa76ff;
+ /* target-based colors */
+ --color-body: var(--steel-800);
+ --color-box-header: var(--steel-700);
+ --color-box-body: var(--steel-750);
+ --color-box-body-highlight: var(--steel-650);
+ --color-text-dark: #fff;
+ --color-text: var(--steel-100);
+ --color-text-light: var(--steel-150);
+ --color-text-light-1: var(--steel-150);
+ --color-text-light-2: var(--steel-200);
+ --color-text-light-3: var(--steel-200);
+ --color-footer: var(--steel-900);
+ --color-timeline: var(--steel-650);
+ --color-input-text: var(--steel-100);
+ --color-input-background: var(--steel-650);
+ --color-input-toggle-background: var(--steel-650);
+ --color-input-border: var(--steel-550);
+ --color-input-border-hover: var(--steel-450);
+ --color-header-wrapper: var(--steel-850);
+ --color-header-wrapper-transparent: #242d3800;
+ --color-light: #00000028;
+ --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
+ --color-light-border: #ffffff28;
+ --color-hover: var(--steel-600);
+ --color-active: var(--steel-650);
+ --color-menu: var(--steel-700);
+ --color-card: var(--steel-700);
+ --color-markup-table-row: #ffffff06;
+ --color-markup-code-block: var(--steel-800);
+ --color-markup-code-inline: var(--steel-850);
+ --color-button: var(--steel-600);
+ --color-code-bg: var(--steel-750);
+ --color-shadow: #00000060;
+ --color-secondary-bg: var(--steel-700);
+ --color-text-focus: #fff;
+ --color-expand-button: #3c404d;
+ --color-placeholder-text: var(--color-text-light-3);
+ --color-editor-line-highlight: var(--steel-700);
+ --color-project-board-bg: var(--color-secondary-light-3);
+ --color-project-board-dark-label: var(--color-text-light-3);
+ --color-caret: var(--color-text);
+ /* should ideally be --color-text-dark, see #15651 */
+ --color-reaction-bg: #ffffff12;
+ --color-reaction-active-bg: var(--color-primary-alpha-30);
+ --color-reaction-hover-bg: var(--color-primary-alpha-40);
+ --color-tooltip-text: #ffffff;
+ --color-tooltip-bg: #000000f0;
+ --color-nav-bg: var(--steel-900);
+ --color-nav-hover-bg: var(--steel-600);
+ --color-nav-text: var(--color-text);
+ --color-secondary-nav-bg: var(--color-body);
+ --color-label-text: #fff;
+ --color-label-bg: var(--steel-600);
+ --color-label-hover-bg: var(--steel-550);
+ --color-label-active-bg: var(--steel-500);
+ --color-label-bg-alt: var(--steel-550);
+ --color-accent: var(--color-primary-light-1);
+ --color-small-accent: var(--color-primary-light-5);
+ --color-highlight-fg: var(--color-primary-light-4);
+ --color-highlight-bg: var(--color-primary-alpha-20);
+ --color-overlay-backdrop: #080808c0;
+ /* pattern colors for image diff */
+ --checkerboard-color-1: #474747;
+ --checkerboard-color-2: #313131;
+ accent-color: var(--color-accent);
+ color-scheme: dark;
+}
+/* invert emojis that are hard to read otherwise */
+.emoji[aria-label="check mark"],
+.emoji[aria-label="currency exchange"],
+.emoji[aria-label="TOP arrow"],
+.emoji[aria-label="END arrow"],
+.emoji[aria-label="ON! arrow"],
+.emoji[aria-label="SOON arrow"],
+.emoji[aria-label="heavy dollar sign"],
+.emoji[aria-label="copyright"],
+.emoji[aria-label="registered"],
+.emoji[aria-label="trade mark"],
+.emoji[aria-label="multiply"],
+.emoji[aria-label="plus"],
+.emoji[aria-label="minus"],
+.emoji[aria-label="divide"],
+.emoji[aria-label="curly loop"],
+.emoji[aria-label="double curly loop"],
+.emoji[aria-label="wavy dash"],
+.emoji[aria-label="paw prints"],
+.emoji[aria-label="musical note"],
+.emoji[aria-label="musical notes"] {
+ filter: invert(100%) hue-rotate(180deg);
+}
+i.grey.icon.icon.icon.icon {
+ color: var(--steel-350) !important;
+}
+.ui.secondary.vertical.menu {
+ border-radius: 0.28571429rem !important;
+ overflow: hidden;
+}
+.ui.basic.primary.button.item {
+ background-color: var(--color-active) !important;
+ color: var(--color-text) !important;
+ box-shadow: none !important;
+}
+.ui.red.label.notification_count,
+.ui.primary.label,
+.ui.primary.labels .label {
+ background-color: var(--color-primary-light-3) !important;
+}
+.repository.view.issue .comment-list .code-comment + .code-comment {
+ margin: 1.25rem 0 !important;
+ padding-top: 1.25rem !important;
+ border-top-color: var(--steel-650) !important;
+}
+.ui.labeled.icon.buttons > .button > .icon,
+.ui.labeled.icon.button > .icon {
+ background-color: var(--color-light) !important;
+}
+#review-box .review-comments-counter {
+ background-color: var(--color-shadow) !important;
+ color: var(--color-white) !important;
+ margin-left: 0.5em;
+}
+.ui.basic.labels .primary.label,
+.ui.ui.ui.basic.primary.label {
+ color: var(--color-text-dark) !important;
+}
+.ui.basic.yellow.label.pending-label {
+ background: var(--color-light) !important;
+}
+::selection {
+ background: var(--steel-100) !important;
+ color: var(--color-pure-black) !important;
+}
+strong.attention-important, svg.attention-important {
+ color: var(--color-violet-light);
+}
+strong.attention-note, svg.attention-note {
+ color: var(--color-blue-light);
+}
+strong.attention-caution, svg.attention-caution {
+ color: var(--color-red-light);
+}
+.ui.basic.red.button {
+ background-color: var(--color-red);
+ color: var(--color-white);
+}
+.ui.basic.red.button:hover,
+.ui.basic.red.button:focus {
+ background-color: var(--color-red-dark-1);
+ color: var(--color-white);
+}
+.ui.basic.red.button:active {
+ background-color: var(--color-red-dark-2);
+ color: var(--color-white);
+}
diff --git a/web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css b/web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css
new file mode 100644
index 0000000..eb48b75
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-light-deuteranopia-protanopia.css
@@ -0,0 +1,11 @@
+@import "./theme-forgejo-light.css";
+
+:root {
+ --color-diff-removed-word-bg: #ffdbb0;
+ --color-diff-removed-row-border: #ffdbb0;
+ --color-diff-removed-row-bg: #fffaf3;
+ --color-diff-added-word-bg: #b1dbff;
+ --color-diff-added-row-border: #b1dbff;
+ --color-diff-added-row-bg: #eef9ff;
+ --color-code-bg: #ffffff;
+}
diff --git a/web_src/css/themes/theme-forgejo-light-tritanopia.css b/web_src/css/themes/theme-forgejo-light-tritanopia.css
new file mode 100644
index 0000000..208da55
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-light-tritanopia.css
@@ -0,0 +1,11 @@
+@import "./theme-forgejo-light.css";
+
+:root {
+ --color-diff-removed-word-bg: #ffd0ce;
+ --color-diff-removed-row-border: #ffd0ce;
+ --color-diff-removed-row-bg: #fff5f4;
+ --color-diff-added-word-bg: #b1dbff;
+ --color-diff-added-row-border: #eef9ff;
+ --color-diff-added-row-bg: #eef9ff;
+ --color-code-bg: #ffffff;
+}
diff --git a/web_src/css/themes/theme-forgejo-light.css b/web_src/css/themes/theme-forgejo-light.css
new file mode 100644
index 0000000..fa89cc2
--- /dev/null
+++ b/web_src/css/themes/theme-forgejo-light.css
@@ -0,0 +1,325 @@
+@import "../chroma/light.css";
+@import "../codemirror/light.css";
+@import "../markup/light.css";
+
+:root {
+ --steel-900: #10161d;
+ --steel-850: #131a21;
+ --steel-800: #171e26;
+ --steel-750: #1d262f;
+ --steel-700: #242d38;
+ --steel-650: #2b3642;
+ --steel-600: #374351;
+ --steel-550: #445161;
+ --steel-500: #515f70;
+ --steel-450: #5f6e80;
+ --steel-400: #6d7d8f;
+ --steel-350: #7c8c9f;
+ --steel-300: #8c9caf;
+ --steel-250: #9dadc0;
+ --steel-200: #aebed0;
+ --steel-150: #c0cfe0;
+ --steel-100: #d2e0f0;
+ --zinc-50: #fafafa;
+ --zinc-100: #f4f4f5;
+ --zinc-150: #ececee;
+ --zinc-200: #e4e4e7;
+ --zinc-250: #dcdce0;
+ --zinc-300: #d4d4d8;
+ --zinc-350: #babac1;
+ --zinc-400: #a1a1aa;
+ --zinc-450: #898992;
+ --zinc-500: #71717a;
+ --zinc-550: #61616a;
+ --zinc-600: #52525b;
+ --zinc-650: #484850;
+ --zinc-700: #3f3f46;
+ --zinc-750: #333338;
+ --zinc-800: #27272a;
+ --zinc-850: #1f1f23;
+ --zinc-900: #18181b;
+ --color-primary: #c2410c;
+ --color-primary-contrast: #ffffff;
+ --color-primary-dark-1: #c2410c;
+ --color-primary-dark-2: #9a3412;
+ --color-primary-dark-3: #9a3412;
+ --color-primary-dark-4: #7c2d12;
+ --color-primary-dark-5: #7c2d12;
+ --color-primary-dark-6: #7c2d12;
+ --color-primary-dark-7: #7c2d12;
+ --color-primary-light-1: #ea580c;
+ --color-primary-light-2: #f97316;
+ --color-primary-light-3: #fb923c;
+ --color-primary-light-4: #fdba74;
+ --color-primary-light-5: #fed7aa;
+ --color-primary-light-6: #ffedd5;
+ --color-primary-light-7: #fff7ed;
+ --color-primary-alpha-10: #c2410c19;
+ --color-primary-alpha-20: #c2410c33;
+ --color-primary-alpha-30: #c2410c4b;
+ --color-primary-alpha-40: #c2410c66;
+ --color-primary-alpha-50: #c2410c80;
+ --color-primary-alpha-60: #c2410c99;
+ --color-primary-alpha-70: #c2410cb3;
+ --color-primary-alpha-80: #c2410ccc;
+ --color-primary-alpha-90: #c2410ce1;
+ --color-primary-hover: var(--color-primary-dark-2);
+ --color-primary-active: var(--color-primary-dark-4);
+ --color-secondary: var(--zinc-200);
+ --color-secondary-dark-1: var(--zinc-200);
+ --color-secondary-dark-2: var(--zinc-300);
+ --color-secondary-dark-3: var(--zinc-300);
+ --color-secondary-dark-4: var(--zinc-400);
+ --color-secondary-dark-5: var(--zinc-400);
+ --color-secondary-dark-6: var(--zinc-500);
+ --color-secondary-dark-7: var(--zinc-500);
+ --color-secondary-dark-8: var(--zinc-600);
+ --color-secondary-dark-9: var(--zinc-600);
+ --color-secondary-dark-10: var(--zinc-700);
+ --color-secondary-dark-11: var(--zinc-700);
+ --color-secondary-dark-12: var(--zinc-800);
+ --color-secondary-dark-13: var(--zinc-800);
+ --color-secondary-light-1: var(--zinc-200);
+ --color-secondary-light-2: var(--zinc-100);
+ --color-secondary-light-3: var(--zinc-100);
+ --color-secondary-light-4: var(--zinc-50);
+ --color-secondary-alpha-10: #d4d4d819;
+ --color-secondary-alpha-20: #d4d4d833;
+ --color-secondary-alpha-30: #d4d4d84b;
+ --color-secondary-alpha-40: #d4d4d866;
+ --color-secondary-alpha-50: #d4d4d880;
+ --color-secondary-alpha-60: #d4d4d899;
+ --color-secondary-alpha-70: #d4d4d8b3;
+ --color-secondary-alpha-80: #d4d4d8cc;
+ --color-secondary-alpha-90: #d4d4d8e1;
+ --color-secondary-hover: var(--color-secondary-dark-2);
+ --color-secondary-active: var(--color-secondary-dark-4);
+ /* console colors - used for actions console and console files */
+ --color-console-fg: #eeeff2;
+ --color-console-fg-subtle: #959cab;
+ --color-console-bg: #1f212b;
+ --color-console-border: #383c47;
+ --color-console-hover-bg: #ffffff16;
+ --color-console-active-bg: #454a57;
+ --color-console-menu-bg: #383c47;
+ --color-console-menu-border: #5c6374;
+ /* colors */
+ --color-red: #dc2626;
+ --color-orange: #ea580c;
+ --color-yellow: #ca8a04;
+ --color-olive: #91a313;
+ --color-green: #15803d;
+ --color-teal: #0d9488;
+ --color-blue: #2563eb;
+ --color-violet: #7c3aed;
+ --color-purple: #9333ea;
+ --color-pink: #db2777;
+ --color-brown: #a47252;
+ --color-grey: #4b5563;
+ --color-black: #000000;
+ /* light variants */
+ --color-red-light: #ef4444;
+ --color-orange-light: #f97316;
+ --color-yellow-light: #eab308;
+ --color-olive-light: #839311;
+ --color-green-light: #16a34a;
+ --color-teal-light: #14b8a6;
+ --color-blue-light: #3b82f6;
+ --color-violet-light: #8b5cf6;
+ --color-purple-light: #a855f7;
+ --color-pink-light: #ec4899;
+ --color-brown-light: #94674a;
+ --color-grey-light: #6b7280;
+ --color-black-light: #181818;
+ /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
+ --color-red-dark-1: #c82020;
+ --color-orange-dark-1: #d34f0b;
+ --color-yellow-dark-1: #b67c04;
+ --color-olive-dark-1: #839311;
+ --color-green-dark-1: #137337;
+ --color-teal-dark-1: #0c857a;
+ --color-blue-dark-1: #1554e0;
+ --color-violet-dark-1: #6a1feb;
+ --color-purple-dark-1: #8519e7;
+ --color-pink-dark-1: #c7216b;
+ --color-brown-dark-1: #94674a;
+ --color-black-dark-1: #000000;
+ /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
+ --color-red-dark-2: #b21d1d;
+ --color-orange-dark-2: #bb460a;
+ --color-yellow-dark-2: #a26e03;
+ --color-olive-dark-2: #74820f;
+ --color-green-dark-2: #116631;
+ --color-teal-dark-2: #0a766d;
+ --color-blue-dark-2: #124bc7;
+ --color-violet-dark-2: #5c14d8;
+ --color-purple-dark-2: #7715cf;
+ --color-pink-dark-2: #b11d5f;
+ --color-brown-dark-2: #835b42;
+ --color-black-dark-2: #000000;
+ /* ansi colors used for actions console and console files */
+ --color-ansi-black: #1f2326;
+ --color-ansi-red: #cc4848;
+ --color-ansi-green: #87ab63;
+ --color-ansi-yellow: #cc9903;
+ --color-ansi-blue: #3a8ac6;
+ --color-ansi-magenta: #d22e8b;
+ --color-ansi-cyan: #00918a;
+ --color-ansi-white: var(--color-console-fg-subtle);
+ --color-ansi-bright-black: #46494d;
+ --color-ansi-bright-red: #d15a5a;
+ --color-ansi-bright-green: #93b373;
+ --color-ansi-bright-yellow: #eaaf03;
+ --color-ansi-bright-blue: #4e96cc;
+ --color-ansi-bright-magenta: #d74397;
+ --color-ansi-bright-cyan: #00b6ad;
+ --color-ansi-bright-white: var(--color-console-fg);
+ /* other colors */
+ --color-gold: #b1983b;
+ --color-white: #ffffff;
+ --color-diff-removed-word-bg: #fca5a5;
+ --color-diff-added-word-bg: #86efac;
+ --color-diff-removed-row-bg: #fee2e2;
+ --color-diff-moved-row-bg: #fef9c3;
+ --color-diff-added-row-bg: #dcfce7;
+ --color-diff-removed-row-border: #fca5a5;
+ --color-diff-moved-row-border: #fde047;
+ --color-diff-added-row-border: #86efac;
+ --color-diff-inactive: var(--zinc-100);
+ --color-error-border: #fecaca;
+ --color-error-bg: #fee2e2;
+ --color-error-bg-active: #fca5a5;
+ --color-error-bg-hover: #fecaca;
+ --color-error-text: #7f1d1d;
+ --color-success-border: #bbf7d0;
+ --color-success-bg: #dcfce7;
+ --color-success-text: #14532d;
+ --color-warning-border: #fde047;
+ --color-warning-bg: #fef3c7;
+ --color-warning-text: #78350f;
+ --color-info-border: #bae6fd;
+ --color-info-bg: #e0f2fe;
+ --color-info-text: #0c4a6e;
+ --color-red-badge: #b91c1c;
+ --color-red-badge-bg: #b91c1c22;
+ --color-red-badge-hover-bg: #b91c1c44;
+ --color-green-badge: #16a34a;
+ --color-green-badge-bg: #16a34a22;
+ --color-green-badge-hover-bg: #16a34a44;
+ --color-yellow-badge: #ca8a04;
+ --color-yellow-badge-bg: #ca8a0422;
+ --color-yellow-badge-hover-bg: #ca8a0444;
+ --color-orange-badge: #ea580c;
+ --color-orange-badge-bg: #ea580c22;
+ --color-orange-badge-hover-bg: #ea580c44;
+ --color-git: #f05133;
+ /* Icon colors (PR/Issue/...) */
+ --color-icon-green: var(--color-green-light);
+ --color-icon-red: var(--color-red-light);
+ --color-icon-purple: var(--color-purple-light);
+ /* target-based colors */
+ --color-body: #fff;
+ --color-box-header: var(--zinc-100);
+ --color-box-body: var(--zinc-50);
+ --color-box-body-highlight: var(--zinc-200);
+ --color-text-dark: #000;
+ --color-text: var(--zinc-900);
+ --color-text-light: var(--zinc-700);
+ --color-text-light-1: var(--zinc-650);
+ --color-text-light-2: var(--zinc-600);
+ --color-text-light-3: var(--zinc-550);
+ --color-footer: var(--zinc-100);
+ --color-timeline: var(--zinc-200);
+ --color-input-text: var(--zinc-800);
+ --color-input-background: #fff;
+ --color-input-toggle-background: #fff;
+ --color-input-border: var(--zinc-300);
+ --color-input-border-hover: var(--zinc-400);
+ --color-header-wrapper: var(--zinc-50);
+ --color-header-wrapper-transparent: #d2e0f000;
+ --color-light: #ffffffcc;
+ --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
+ --color-light-border: #0000001d;
+ --color-hover: #e4e4e4aa;
+ --color-active: #d4d4d8aa;
+ --color-menu: var(--zinc-100);
+ --color-card: var(--zinc-50);
+ --color-markup-table-row: #ffffff06;
+ --color-markup-code-block: var(--zinc-150);
+ --color-markup-code-inline: var(--zinc-200);
+ --color-button: var(--zinc-150);
+ --color-code-bg: var(--zinc-50);
+ --color-shadow: #00000060;
+ --color-secondary-bg: var(--zinc-100);
+ --color-text-focus: #fff;
+ --color-expand-button: var(--zinc-200);
+ --color-placeholder-text: var(--color-text-light-3);
+ --color-editor-line-highlight: var(--zinc-100);
+ --color-project-board-bg: var(--color-secondary-light-2);
+ --color-project-board-dark-label: var(--color-text-light-3);
+ --color-caret: var(--color-text);
+ /* should ideally be --color-text-dark, see #15651 */
+ --color-reaction-bg: #0000000a;
+ --color-reaction-active-bg: var(--color-primary-alpha-20);
+ --color-reaction-hover-bg: var(--color-primary-alpha-30);
+ --color-tooltip-text: #ffffff;
+ --color-tooltip-bg: #000000f0;
+ --color-nav-bg: var(--zinc-100);
+ --color-nav-hover-bg: var(--zinc-300);
+ --color-nav-text: var(--color-text);
+ --color-secondary-nav-bg: var(--color-body);
+ --color-label-text: var(--color-text);
+ --color-label-bg: #cacaca5b;
+ --color-label-hover-bg: #cacacaa0;
+ --color-label-active-bg: #cacacaff;
+ --color-label-bg-alt: #cacacaff;
+ --color-accent: var(--color-primary-light-1);
+ --color-small-accent: var(--color-primary-light-5);
+ --color-highlight-fg: var(--color-primary-light-4);
+ --color-highlight-bg: var(--color-primary-light-6);
+ --color-overlay-backdrop: #080808c0;
+ /* pattern colors for gradient */
+ --checkerboard-color-1: #ffffff;
+ --checkerboard-color-2: #e5e5e5;
+ accent-color: var(--color-accent);
+ color-scheme: light;
+}
+.ui.secondary.vertical.menu {
+ border-radius: 0.28571429rem !important;
+ overflow: hidden;
+}
+.ui.basic.primary.button.item {
+ background-color: var(--color-active) !important;
+ color: var(--color-text) !important;
+ box-shadow: none !important;
+}
+.ui.red.label.notification_count,
+.ui.primary.labels .label {
+ background-color: var(--color-primary-dark-1) !important;
+}
+.repository.view.issue .comment-list .code-comment + .code-comment {
+ margin: 1.25rem 0 !important;
+ padding-top: 1.25rem !important;
+ border-top-color: var(--zinc-250) !important;
+}
+.ui.labeled.icon.buttons > .button > .icon,
+.ui.labeled.icon.button > .icon {
+ background-color: var(--color-shadow) !important;
+}
+#review-box .review-comments-counter {
+ background-color: var(--color-label-bg) !important;
+ margin-left: 0.5em;
+}
+.ui.basic.labels .primary.label,
+.ui.ui.ui.basic.primary.label {
+ color: var(--color-text-dark) !important;
+}
+.ui.basic.yellow.label.pending-label {
+ background: var(--color-warning-bg) !important;
+ color: var(--color-warning-text) !important;
+ border-color: var(--color-yellow-light) !important;
+}
+::selection {
+ background: var(--steel-450) !important;
+ color: var(--color-white) !important;
+}
diff --git a/web_src/css/themes/theme-gitea-auto.css b/web_src/css/themes/theme-gitea-auto.css
new file mode 100644
index 0000000..509889e
--- /dev/null
+++ b/web_src/css/themes/theme-gitea-auto.css
@@ -0,0 +1,2 @@
+@import "./theme-gitea-light.css" (prefers-color-scheme: light);
+@import "./theme-gitea-dark.css" (prefers-color-scheme: dark);
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
new file mode 100644
index 0000000..6ad6efe
--- /dev/null
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -0,0 +1,271 @@
+@import "../chroma/dark.css";
+@import "../codemirror/dark.css";
+@import "../markup/dark.css";
+
+:root {
+ --is-dark-theme: true;
+ --color-primary: #4183c4;
+ --color-primary-contrast: #ffffff;
+ --color-primary-dark-1: #548fca;
+ --color-primary-dark-2: #679cd0;
+ --color-primary-dark-3: #7aa8d6;
+ --color-primary-dark-4: #8db5dc;
+ --color-primary-dark-5: #b3cde7;
+ --color-primary-dark-6: #d9e6f3;
+ --color-primary-dark-7: #f4f8fb;
+ --color-primary-light-1: #3876b3;
+ --color-primary-light-2: #31699f;
+ --color-primary-light-3: #2b5c8b;
+ --color-primary-light-4: #254f77;
+ --color-primary-light-5: #193450;
+ --color-primary-light-6: #0c1a28;
+ --color-primary-light-7: #04080c;
+ --color-primary-alpha-10: #4183c419;
+ --color-primary-alpha-20: #4183c433;
+ --color-primary-alpha-30: #4183c44b;
+ --color-primary-alpha-40: #4183c466;
+ --color-primary-alpha-50: #4183c480;
+ --color-primary-alpha-60: #4183c499;
+ --color-primary-alpha-70: #4183c4b3;
+ --color-primary-alpha-80: #4183c4cc;
+ --color-primary-alpha-90: #4183c4e1;
+ --color-primary-hover: var(--color-primary-light-1);
+ --color-primary-active: var(--color-primary-light-2);
+ --color-secondary: #3b444a;
+ --color-secondary-dark-1: #424b51;
+ --color-secondary-dark-2: #4a545b;
+ --color-secondary-dark-3: #59646c;
+ --color-secondary-dark-4: #6b7681;
+ --color-secondary-dark-5: #78858f;
+ --color-secondary-dark-6: #87929d;
+ --color-secondary-dark-7: #939ea9;
+ --color-secondary-dark-8: #a1acb4;
+ --color-secondary-dark-9: #aab3bc;
+ --color-secondary-dark-10: #b6bfc8;
+ --color-secondary-dark-11: #c2cbd3;
+ --color-secondary-dark-12: #ccd4dc;
+ --color-secondary-dark-13: #cfd7df;
+ --color-secondary-light-1: #2e353b;
+ --color-secondary-light-2: #2b353e;
+ --color-secondary-light-3: #1c2227;
+ --color-secondary-light-4: #161b1f;
+ --color-secondary-alpha-10: #3b444a19;
+ --color-secondary-alpha-20: #3b444a33;
+ --color-secondary-alpha-30: #3b444a4b;
+ --color-secondary-alpha-40: #3b444a66;
+ --color-secondary-alpha-50: #3b444a80;
+ --color-secondary-alpha-60: #3b444a99;
+ --color-secondary-alpha-70: #3b444ab3;
+ --color-secondary-alpha-80: #3b444acc;
+ --color-secondary-alpha-90: #3b444ae1;
+ --color-secondary-button: var(--color-secondary-dark-4);
+ --color-secondary-hover: var(--color-secondary-dark-3);
+ --color-secondary-active: var(--color-secondary-dark-2);
+ /* console colors - used for actions console and console files */
+ --color-console-fg: #f8f8f9;
+ --color-console-fg-subtle: #bec4c8;
+ --color-console-bg: #171b1e;
+ --color-console-border: #2e353b;
+ --color-console-hover-bg: #292d31;
+ --color-console-active-bg: #2e353b;
+ --color-console-menu-bg: #252b30;
+ --color-console-menu-border: #424b51;
+ /* named colors */
+ --color-red: #cc4848;
+ --color-orange: #cc580c;
+ --color-yellow: #cc9903;
+ --color-olive: #91a313;
+ --color-green: #87ab63;
+ --color-teal: #00918a;
+ --color-blue: #3a8ac6;
+ --color-violet: #906ae1;
+ --color-purple: #b259d0;
+ --color-pink: #d22e8b;
+ --color-brown: #a47252;
+ --color-black: #1d2328;
+ /* light variants - produced via Sass scale-color(color, $lightness: +10%) */
+ --color-red-light: #d15a5a;
+ --color-orange-light: #f6a066;
+ --color-yellow-light: #eaaf03;
+ --color-olive-light: #abc016;
+ --color-green-light: #93b373;
+ --color-teal-light: #00b6ad;
+ --color-blue-light: #4e96cc;
+ --color-violet-light: #9b79e4;
+ --color-purple-light: #ba6ad5;
+ --color-pink-light: #d74397;
+ --color-brown-light: #b08061;
+ --color-black-light: #424851;
+ /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
+ --color-red-dark-1: #c23636;
+ --color-orange-dark-1: #f38236;
+ --color-yellow-dark-1: #b88a03;
+ --color-olive-dark-1: #839311;
+ --color-green-dark-1: #7a9e55;
+ --color-teal-dark-1: #00837c;
+ --color-blue-dark-1: #347cb3;
+ --color-violet-dark-1: #7b4edb;
+ --color-purple-dark-1: #a742c9;
+ --color-pink-dark-1: #be297d;
+ --color-brown-dark-1: #94674a;
+ --color-black-dark-1: #292e38;
+ /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
+ --color-red-dark-2: #ad3030;
+ --color-orange-dark-2: #f16e17;
+ --color-yellow-dark-2: #a37a02;
+ --color-olive-dark-2: #74820f;
+ --color-green-dark-2: #6c8c4c;
+ --color-teal-dark-2: #00746e;
+ --color-blue-dark-2: #2e6e9f;
+ --color-violet-dark-2: #6733d6;
+ --color-purple-dark-2: #9834b9;
+ --color-pink-dark-2: #a9246f;
+ --color-brown-dark-2: #835b42;
+ --color-black-dark-2: #272930;
+ /* ansi colors used for actions console and console files */
+ --color-ansi-black: #1d2328;
+ --color-ansi-red: #cc4848;
+ --color-ansi-green: #87ab63;
+ --color-ansi-yellow: #cc9903;
+ --color-ansi-blue: #3a8ac6;
+ --color-ansi-magenta: #d22e8b;
+ --color-ansi-cyan: #00918a;
+ --color-ansi-white: var(--color-console-fg-subtle);
+ --color-ansi-bright-black: #424851;
+ --color-ansi-bright-red: #d15a5a;
+ --color-ansi-bright-green: #93b373;
+ --color-ansi-bright-yellow: #eaaf03;
+ --color-ansi-bright-blue: #4e96cc;
+ --color-ansi-bright-magenta: #d74397;
+ --color-ansi-bright-cyan: #00b6ad;
+ --color-ansi-bright-white: var(--color-console-fg);
+ /* other colors */
+ --color-grey: #384147;
+ --color-grey-light: #828f99;
+ --color-gold: #b1983b;
+ --color-white: #ffffff;
+ --color-diff-removed-word-bg: #6f3333;
+ --color-diff-added-word-bg: #3c653c;
+ --color-diff-removed-row-bg: #3c2626;
+ --color-diff-moved-row-bg: #818044;
+ --color-diff-added-row-bg: #283e2d;
+ --color-diff-removed-row-border: #634343;
+ --color-diff-moved-row-border: #bcca6f;
+ --color-diff-added-row-border: #314a37;
+ --color-diff-inactive: #22282d;
+ --color-error-border: #a04141;
+ --color-error-bg: #522;
+ --color-error-bg-active: #744;
+ --color-error-bg-hover: #633;
+ --color-error-text: #f9cbcb;
+ --color-success-border: #458a57;
+ --color-success-bg: #284034;
+ --color-success-text: #6cc664;
+ --color-warning-border: #bb9d00;
+ --color-warning-bg: #3a3a30;
+ --color-warning-text: #fbbd08;
+ --color-info-border: #306090;
+ --color-info-bg: #26354c;
+ --color-info-text: #38a8e8;
+ --color-red-badge: #db2828;
+ --color-red-badge-bg: #db28281a;
+ --color-red-badge-hover-bg: #db28284d;
+ --color-green-badge: #21ba45;
+ --color-green-badge-bg: #21ba451a;
+ --color-green-badge-hover-bg: #21ba454d;
+ --color-yellow-badge: #fbbd08;
+ --color-yellow-badge-bg: #fbbd081a;
+ --color-yellow-badge-hover-bg: #fbbd084d;
+ --color-orange-badge: #f2711c;
+ --color-orange-badge-bg: #f2711c1a;
+ --color-orange-badge-hover-bg: #f2711c4d;
+ --color-git: #f05133;
+ /* Icon colors (PR/Issue/...) */
+ --color-icon-green: var(--color-green);
+ --color-icon-red: var(--color-red);
+ --color-icon-purple: var(--color-purple);
+ /* target-based colors */
+ --color-body: #1c1f25;
+ --color-box-header: #1a1d1f;
+ --color-box-body: #14171a;
+ --color-box-body-highlight: #1c2227;
+ --color-text-dark: #f8f8f9;
+ --color-text: #d1d5d8;
+ --color-text-light: #bdc3c7;
+ --color-text-light-1: #a8afb5;
+ --color-text-light-2: #929ba2;
+ --color-text-light-3: #7c8790;
+ --color-footer: var(--color-nav-bg);
+ --color-timeline: #353c42;
+ --color-input-text: var(--color-text-dark);
+ --color-input-background: #151a1e;
+ --color-input-toggle-background: #2e353b;
+ --color-input-border: var(--color-secondary);
+ --color-input-border-hover: var(--color-secondary-dark-1);
+ --color-light: #00001728;
+ --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
+ --color-light-border: #e8e8ff28;
+ --color-hover: #e8e8ff19;
+ --color-active: #e8e8ff24;
+ --color-menu: #151a1e;
+ --color-card: #151a1e;
+ --color-markup-table-row: #e8e8ff0f;
+ --color-markup-code-block: #e8e8ff12;
+ --color-markup-code-inline: #e8e8ff28;
+ --color-button: #151a1e;
+ --color-code-bg: #14171a;
+ --color-shadow: #00001758;
+ --color-secondary-bg: #2f3138;
+ --color-expand-button: #2b353e;
+ --color-placeholder-text: var(--color-text-light-3);
+ --color-editor-line-highlight: var(--color-primary-light-5);
+ --color-project-column-bg: var(--color-secondary-light-2);
+ --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
+ --color-reaction-bg: #e8e8ff12;
+ --color-reaction-hover-bg: var(--color-primary-light-4);
+ --color-reaction-active-bg: var(--color-primary-light-5);
+ --color-tooltip-text: #fafafb;
+ --color-tooltip-bg: #000017f0;
+ --color-nav-bg: #16191c;
+ --color-nav-hover-bg: var(--color-secondary-light-1);
+ --color-nav-text: var(--color-text);
+ --color-secondary-nav-bg: #181c20;
+ --color-label-text: var(--color-text);
+ --color-label-bg: #73828e4b;
+ --color-label-hover-bg: #73828ea0;
+ --color-label-active-bg: #73828eff;
+ --color-accent: var(--color-primary-light-1);
+ --color-small-accent: var(--color-primary-light-5);
+ --color-highlight-fg: #87651e;
+ --color-highlight-bg: #352c1c;
+ --color-overlay-backdrop: #080808c0;
+ /* pattern colors for image diff */
+ --checkerboard-color-1: #313131;
+ --checkerboard-color-2: #212121;
+ accent-color: var(--color-accent);
+ color-scheme: dark;
+}
+
+/* invert emojis that are hard to read otherwise */
+.emoji[aria-label="check mark"],
+.emoji[aria-label="currency exchange"],
+.emoji[aria-label="TOP arrow"],
+.emoji[aria-label="END arrow"],
+.emoji[aria-label="ON! arrow"],
+.emoji[aria-label="SOON arrow"],
+.emoji[aria-label="heavy dollar sign"],
+.emoji[aria-label="copyright"],
+.emoji[aria-label="registered"],
+.emoji[aria-label="trade mark"],
+.emoji[aria-label="multiply"],
+.emoji[aria-label="plus"],
+.emoji[aria-label="minus"],
+.emoji[aria-label="divide"],
+.emoji[aria-label="curly loop"],
+.emoji[aria-label="double curly loop"],
+.emoji[aria-label="wavy dash"],
+.emoji[aria-label="paw prints"],
+.emoji[aria-label="musical note"],
+.emoji[aria-label="musical notes"] {
+ filter: invert(100%) hue-rotate(180deg);
+}
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
new file mode 100644
index 0000000..830b96f
--- /dev/null
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -0,0 +1,247 @@
+@import "../chroma/light.css";
+@import "../codemirror/light.css";
+@import "../markup/light.css";
+
+:root {
+ --is-dark-theme: false;
+ --color-primary: #4183c4;
+ --color-primary-contrast: #ffffff;
+ --color-primary-dark-1: #3876b3;
+ --color-primary-dark-2: #31699f;
+ --color-primary-dark-3: #2b5c8b;
+ --color-primary-dark-4: #254f77;
+ --color-primary-dark-5: #193450;
+ --color-primary-dark-6: #0c1a28;
+ --color-primary-dark-7: #04080c;
+ --color-primary-light-1: #548fca;
+ --color-primary-light-2: #679cd0;
+ --color-primary-light-3: #7aa8d6;
+ --color-primary-light-4: #8db5dc;
+ --color-primary-light-5: #b3cde7;
+ --color-primary-light-6: #d9e6f3;
+ --color-primary-light-7: #f4f8fb;
+ --color-primary-alpha-10: #4183c419;
+ --color-primary-alpha-20: #4183c433;
+ --color-primary-alpha-30: #4183c44b;
+ --color-primary-alpha-40: #4183c466;
+ --color-primary-alpha-50: #4183c480;
+ --color-primary-alpha-60: #4183c499;
+ --color-primary-alpha-70: #4183c4b3;
+ --color-primary-alpha-80: #4183c4cc;
+ --color-primary-alpha-90: #4183c4e1;
+ --color-primary-hover: var(--color-primary-dark-1);
+ --color-primary-active: var(--color-primary-dark-2);
+ --color-secondary: #d0d7de;
+ --color-secondary-dark-1: #c7ced5;
+ --color-secondary-dark-2: #b9c0c7;
+ --color-secondary-dark-3: #99a0a7;
+ --color-secondary-dark-4: #899097;
+ --color-secondary-dark-5: #7a8188;
+ --color-secondary-dark-6: #6a7178;
+ --color-secondary-dark-7: #5b6269;
+ --color-secondary-dark-8: #4b5259;
+ --color-secondary-dark-9: #3c434a;
+ --color-secondary-dark-10: #2c333a;
+ --color-secondary-dark-11: #1d242b;
+ --color-secondary-dark-12: #0d141b;
+ --color-secondary-dark-13: #00040b;
+ --color-secondary-light-1: #dee5ec;
+ --color-secondary-light-2: #e4ebf2;
+ --color-secondary-light-3: #ebf2f9;
+ --color-secondary-light-4: #f1f8ff;
+ --color-secondary-alpha-10: #d0d7de19;
+ --color-secondary-alpha-20: #d0d7de33;
+ --color-secondary-alpha-30: #d0d7de4b;
+ --color-secondary-alpha-40: #d0d7de66;
+ --color-secondary-alpha-50: #d0d7de80;
+ --color-secondary-alpha-60: #d0d7de99;
+ --color-secondary-alpha-70: #d0d7deb3;
+ --color-secondary-alpha-80: #d0d7decc;
+ --color-secondary-alpha-90: #d0d7dee1;
+ --color-secondary-button: var(--color-secondary-dark-4);
+ --color-secondary-hover: var(--color-secondary-dark-5);
+ --color-secondary-active: var(--color-secondary-dark-6);
+ /* console colors - used for actions console and console files */
+ --color-console-fg: #f8f8f9;
+ --color-console-fg-subtle: #bec4c8;
+ --color-console-bg: #171b1e;
+ --color-console-border: #2e353b;
+ --color-console-hover-bg: #292d31;
+ --color-console-active-bg: #2e353b;
+ --color-console-menu-bg: #252b30;
+ --color-console-menu-border: #424b51;
+ /* named colors */
+ --color-red: #db2828;
+ --color-orange: #f2711c;
+ --color-yellow: #fbbd08;
+ --color-olive: #b5cc18;
+ --color-green: #21ba45;
+ --color-teal: #00b5ad;
+ --color-blue: #2185d0;
+ --color-violet: #6435c9;
+ --color-purple: #a333c8;
+ --color-pink: #e03997;
+ --color-brown: #a5673f;
+ --color-black: #191c1d;
+ /* light variants - produced via Sass scale-color(color, $lightness: +25%) */
+ --color-red-light: #e45e5e;
+ --color-orange-light: #f59555;
+ --color-yellow-light: #fcce46;
+ --color-olive-light: #d3e942;
+ --color-green-light: #46de6a;
+ --color-teal-light: #08fff4;
+ --color-blue-light: #51a5e3;
+ --color-violet-light: #8b67d7;
+ --color-purple-light: #bb64d8;
+ --color-pink-light: #e86bb1;
+ --color-brown-light: #c58b66;
+ --color-black-light: #525558;
+ /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */
+ --color-red-dark-1: #c82121;
+ --color-orange-dark-1: #e6630d;
+ --color-yellow-dark-1: #e5ac04;
+ --color-olive-dark-1: #a3b816;
+ --color-green-dark-1: #1ea73e;
+ --color-teal-dark-1: #00a39c;
+ --color-blue-dark-1: #1e78bb;
+ --color-violet-dark-1: #5a30b5;
+ --color-purple-dark-1: #932eb4;
+ --color-pink-dark-1: #db228a;
+ --color-brown-dark-1: #955d39;
+ --color-black-dark-1: #16191c;
+ /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
+ --color-red-dark-2: #b11e1e;
+ --color-orange-dark-2: #cc580c;
+ --color-yellow-dark-2: #cc9903;
+ --color-olive-dark-2: #91a313;
+ --color-green-dark-2: #1a9537;
+ --color-teal-dark-2: #00918a;
+ --color-blue-dark-2: #1a6aa6;
+ --color-violet-dark-2: #502aa1;
+ --color-purple-dark-2: #8229a0;
+ --color-pink-dark-2: #c21e7b;
+ --color-brown-dark-2: #845232;
+ --color-black-dark-2: #131619;
+ /* ansi colors used for actions console and console files */
+ --color-ansi-black: #1f2326;
+ --color-ansi-red: #cc4848;
+ --color-ansi-green: #87ab63;
+ --color-ansi-yellow: #cc9903;
+ --color-ansi-blue: #3a8ac6;
+ --color-ansi-magenta: #d22e8b;
+ --color-ansi-cyan: #00918a;
+ --color-ansi-white: var(--color-console-fg-subtle);
+ --color-ansi-bright-black: #46494d;
+ --color-ansi-bright-red: #d15a5a;
+ --color-ansi-bright-green: #93b373;
+ --color-ansi-bright-yellow: #eaaf03;
+ --color-ansi-bright-blue: #4e96cc;
+ --color-ansi-bright-magenta: #d74397;
+ --color-ansi-bright-cyan: #00b6ad;
+ --color-ansi-bright-white: var(--color-console-fg);
+ /* other colors */
+ --color-grey: #697077;
+ --color-grey-light: #7c838a;
+ --color-gold: #a1882b;
+ --color-white: #ffffff;
+ --color-diff-removed-word-bg: #fdb8c0;
+ --color-diff-added-word-bg: #acf2bd;
+ --color-diff-removed-row-bg: #ffeef0;
+ --color-diff-moved-row-bg: #f1f8d1;
+ --color-diff-added-row-bg: #e6ffed;
+ --color-diff-removed-row-border: #f1c0c0;
+ --color-diff-moved-row-border: #d0e27f;
+ --color-diff-added-row-border: #e6ffed;
+ --color-diff-inactive: #f0f2f4;
+ --color-error-border: #e0b4b4;
+ --color-error-bg: #fff6f6;
+ --color-error-bg-active: #fbb;
+ --color-error-bg-hover: #fdd;
+ --color-error-text: #9f3a38;
+ --color-success-border: #a3c293;
+ --color-success-bg: #fcfff5;
+ --color-success-text: #2c662d;
+ --color-warning-border: #c9ba9b;
+ --color-warning-bg: #fffaf3;
+ --color-warning-text: #573a08;
+ --color-info-border: #a9d5de;
+ --color-info-bg: #f8ffff;
+ --color-info-text: #276f86;
+ --color-red-badge: #db2828;
+ --color-red-badge-bg: #db28281a;
+ --color-red-badge-hover-bg: #db28284d;
+ --color-green-badge: #21ba45;
+ --color-green-badge-bg: #21ba451a;
+ --color-green-badge-hover-bg: #21ba454d;
+ --color-yellow-badge: #fbbd08;
+ --color-yellow-badge-bg: #fbbd081a;
+ --color-yellow-badge-hover-bg: #fbbd084d;
+ --color-orange-badge: #f2711c;
+ --color-orange-badge-bg: #f2711c1a;
+ --color-orange-badge-hover-bg: #f2711c4d;
+ --color-git: #f05133;
+ /* Icon colors (PR/Issue/...) */
+ --color-icon-green: var(--color-green);
+ --color-icon-red: var(--color-red);
+ --color-icon-purple: var(--color-purple);
+ /* target-based colors */
+ --color-body: #ffffff;
+ --color-box-header: #f1f3f5;
+ --color-box-body: #ffffff;
+ --color-box-body-highlight: #ecf5fd;
+ --color-text-dark: #01050a;
+ --color-text: #181c21;
+ --color-text-light: #30363b;
+ --color-text-light-1: #40474d;
+ --color-text-light-2: #5b6167;
+ --color-text-light-3: #747c84;
+ --color-footer: var(--color-nav-bg);
+ --color-timeline: #d0d7de;
+ --color-input-text: var(--color-text-dark);
+ --color-input-background: #fff;
+ --color-input-toggle-background: #d0d7de;
+ --color-input-border: var(--color-secondary);
+ --color-input-border-hover: var(--color-secondary-dark-1);
+ --color-light: #00001706;
+ --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
+ --color-light-border: #0000171d;
+ --color-hover: #00001708;
+ --color-active: #00001714;
+ --color-menu: #f8f9fb;
+ --color-card: #f8f9fb;
+ --color-markup-table-row: #0030600a;
+ --color-markup-code-block: #00306010;
+ --color-markup-code-inline: #00306012;
+ --color-button: #f8f9fb;
+ --color-code-bg: #fafdff;
+ --color-shadow: #00001726;
+ --color-secondary-bg: #f2f5f8;
+ --color-expand-button: #cfe8fa;
+ --color-placeholder-text: var(--color-text-light-3);
+ --color-editor-line-highlight: var(--color-primary-light-6);
+ --color-project-column-bg: var(--color-secondary-light-4);
+ --color-caret: var(--color-text-dark);
+ --color-reaction-bg: #0000170a;
+ --color-reaction-hover-bg: var(--color-primary-light-5);
+ --color-reaction-active-bg: var(--color-primary-light-6);
+ --color-tooltip-text: #fbfdff;
+ --color-tooltip-bg: #000017f0;
+ --color-nav-bg: #f6f7fa;
+ --color-nav-hover-bg: var(--color-secondary-light-1);
+ --color-nav-text: var(--color-text);
+ --color-secondary-nav-bg: #f9fafb;
+ --color-label-text: var(--color-text);
+ --color-label-bg: #949da64b;
+ --color-label-hover-bg: #949da6a0;
+ --color-label-active-bg: #949da6ff;
+ --color-accent: var(--color-primary-light-1);
+ --color-small-accent: var(--color-primary-light-6);
+ --color-highlight-fg: #eed200;
+ --color-highlight-bg: #fffbdd;
+ --color-overlay-backdrop: #080808c0;
+ /* pattern colors for gradient */
+ --checkerboard-color-1: #ffffff;
+ --checkerboard-color-2: #e5e5e5;
+ accent-color: var(--color-accent);
+ color-scheme: light;
+}
diff --git a/web_src/css/user.css b/web_src/css/user.css
new file mode 100644
index 0000000..16d431e
--- /dev/null
+++ b/web_src/css/user.css
@@ -0,0 +1,149 @@
+.user.profile .ui.card .header {
+ display: block;
+ font-weight: var(--font-weight-semibold);
+ font-size: 1.3rem;
+ margin-top: -0.2rem;
+ line-height: 1.3rem;
+}
+
+.user.profile .ui.card .profile-avatar-name {
+ border-top: none;
+ text-align: center;
+}
+
+.user.profile .ui.card .extra.content {
+ padding: 0;
+}
+
+.user.profile .ui.card .extra.content > ul {
+ margin: 0;
+ padding: 0;
+}
+
+.user.profile .ui.card .extra.content > ul > li {
+ padding: 10px;
+ display: flex;
+ list-style: none;
+ align-items: center;
+ gap: 0.25em;
+}
+
+.user.profile .ui.card .extra.content > ul > li:not(:last-child) {
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.user.profile .ui.card .extra.content > ul > li .svg {
+ margin-left: 1px;
+ margin-right: 5px;
+}
+
+.user.profile .ui.card .extra.content > ul > li.follow .ui.button,
+.user.profile .ui.card .extra.content > ul > li.block .ui.button {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+}
+
+.user.profile .ui.card #profile-avatar {
+ padding: 1rem 1rem 0.25rem;
+ justify-content: center;
+}
+
+.user.profile .ui.card #profile-avatar img {
+ max-width: 100%;
+ height: auto;
+}
+
+@media (max-width: 767.98px) {
+ .user.profile .ui.card #profile-avatar img {
+ width: 30vw;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .user.profile .ui.card {
+ width: 100%;
+ }
+}
+
+.user.profile .ui.secondary.stackable.pointing.menu {
+ flex-wrap: wrap;
+}
+
+.user.link-account:not(.icon) {
+ padding-top: 15px;
+ padding-bottom: 5px;
+}
+
+.user.settings .iconFloat {
+ float: left;
+}
+
+.user-orgs {
+ display: flex;
+ flex-flow: row wrap;
+ padding: 0;
+ margin: -3px !important;
+}
+
+.user-orgs > li {
+ display: flex;
+ border-bottom: 0 !important;
+ padding: 3px !important;
+ max-width: 60px;
+}
+
+.user-badges {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, 64px);
+ gap: 2px;
+}
+
+.user-badges img {
+ object-fit: contain;
+}
+
+#readme_profile {
+ padding: 1.5em;
+ background: var(--color-box-body);
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+}
+
+#profile-avatar-card {
+ background: var(--color-box-body);
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+}
+
+#notification_table {
+ background: var(--color-box-body);
+ border: 1px solid var(--color-secondary);
+ border-radius: var(--border-radius);
+}
+
+.notifications-item:hover {
+ background: var(--color-hover);
+}
+
+.notifications-buttons {
+ display: none;
+ min-width: 74px;
+}
+
+.notifications-updated {
+ display: flex;
+}
+
+.notifications-item:hover .notifications-buttons {
+ display: flex;
+}
+
+.notifications-item:hover .notifications-updated {
+ display: none;
+}
+
+#pronouns-dropdown, #pronouns-custom {
+ width: 140px;
+}
diff --git a/web_src/fomantic/.npmrc b/web_src/fomantic/.npmrc
new file mode 100644
index 0000000..fbacc98
--- /dev/null
+++ b/web_src/fomantic/.npmrc
@@ -0,0 +1,7 @@
+audit=false
+fund=false
+update-notifier=false
+package-lock=true
+save-exact=true
+lockfile-version=3
+optional=false
diff --git a/web_src/fomantic/_site/globals/site.variables b/web_src/fomantic/_site/globals/site.variables
new file mode 100644
index 0000000..fbed597
--- /dev/null
+++ b/web_src/fomantic/_site/globals/site.variables
@@ -0,0 +1,61 @@
+/* https://github.com/fomantic/Fomantic-UI/blob/develop/src/themes/default/globals/site.variables */
+
+@headerFont: var(--fonts-regular);
+@pageFont: var(--fonts-regular);
+@bold: 500;
+@useCustomScrollbars: false;
+@disabledOpacity: var(--opacity-disabled);
+@linkHoverUnderline: underline;
+@pageOverflowX: visible;
+
+@variationAccordionInverted: false;
+@variationBreadcrumbInverted: false;
+@variationButtonAnimated: false;
+@variationButtonAnimatedFade: false;
+@variationButtonAttached: false;
+@variationButtonInverted: false;
+@variationButtonSocial: false;
+@variationButtonTertiary: false;
+@variationCalendarInverted: false;
+@variationCardInverted: false;
+@variationCheckboxInverted: false;
+@variationCommentInverted: false;
+@variationDimmerInverted: false;
+@variationDividerInverted: false;
+@variationDropdownInverted: false;
+@variationFeedInverted: false;
+@variationFlyoutInverted: false;
+@variationFormInverted: false;
+@variationFormTransparent: false;
+@variationGridDoubling: false;
+@variationGridInverted: false;
+@variationHeaderInverted: false;
+@variationIconInverted: false;
+@variationInputInverted: false;
+@variationItemInverted: false;
+@variationLabelCorner: false;
+@variationLabelImage: false;
+@variationLabelInverted: false;
+@variationLabelRibbon: false;
+@variationLabelTag: false;
+@variationListInverted: false;
+@variationMenuInverted: false;
+@variationMessageInverted: false;
+@variationModalInverted: false;
+@variationNagInverted: false;
+@variationPlaceholderInverted: false;
+@variationPopupInverted: false;
+@variationPopupTooltip: false;
+@variationProgressInverted: false;
+@variationSegmentInverted: false;
+@variationSegmentPiled: false;
+@variationSegmentStacked: false;
+@variationSliderInverted: false;
+@variationStatisticInverted: false;
+@variationStepInverted: false;
+@variationTableInverted: false;
+@variationTableMarked: false;
+@variationTableStackable: false;
+@variationTextInverted: false;
+@variationToastInverted: false;
+@variationTransitionInverted: false;
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
new file mode 100644
index 0000000..49c00c4
--- /dev/null
+++ b/web_src/fomantic/build/semantic.css
@@ -0,0 +1,9935 @@
+ /*
+ * # Fomantic UI - 2.8.7
+ * https://github.com/fomantic/Fomantic-UI
+ * http://fomantic-ui.com/
+ *
+ * Copyright 2014 Contributors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+/*!
+ * # Fomantic-UI - Button
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Button
+*******************************/
+
+.ui.button {
+ cursor: pointer;
+ display: inline-block;
+ min-height: 1em;
+ outline: none;
+ border: none;
+ vertical-align: baseline;
+ background: #E0E1E2 none;
+ color: rgba(0, 0, 0, 0.6);
+ font-family: var(--fonts-regular);
+ margin: 0 0.25em 0 0;
+ padding: 0.78571429em 1.5em 0.78571429em;
+ text-transform: none;
+ text-shadow: none;
+ font-weight: 500;
+ line-height: 1em;
+ font-style: normal;
+ text-align: center;
+ text-decoration: none;
+ border-radius: 0.28571429rem;
+ box-shadow: 0 0 0 1px transparent inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
+ will-change: auto;
+ -webkit-tap-highlight-color: transparent;
+}
+
+/*******************************
+ States
+*******************************/
+
+/*--------------
+ Hover
+---------------*/
+
+.ui.button:hover {
+ background-color: #CACBCD;
+ background-image: none;
+ box-shadow: 0 0 0 1px transparent inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+ color: rgba(0, 0, 0, 0.8);
+}
+
+.ui.button:hover .icon {
+ opacity: 0.85;
+}
+
+/*--------------
+ Focus
+---------------*/
+
+.ui.button:focus {
+ background-color: #CACBCD;
+ color: rgba(0, 0, 0, 0.8);
+ background-image: none;
+ box-shadow: '';
+}
+
+.ui.button:focus .icon {
+ opacity: 0.85;
+}
+
+/*--------------
+ Down
+---------------*/
+
+.ui.button:active,
+.ui.active.button:active {
+ background-color: #BABBBC;
+ background-image: '';
+ color: rgba(0, 0, 0, 0.9);
+ box-shadow: 0 0 0 1px transparent inset, none;
+}
+
+/*--------------
+ Active
+---------------*/
+
+.ui.active.button {
+ background-color: #C0C1C2;
+ background-image: none;
+ box-shadow: 0 0 0 1px transparent inset;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+.ui.active.button:hover {
+ background-color: #C0C1C2;
+ background-image: none;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+.ui.active.button:active {
+ background-color: #C0C1C2;
+ background-image: none;
+}
+
+/*--------------
+ Loading
+---------------*/
+
+/* Specificity hack */
+
+.ui.loading.loading.loading.loading.loading.loading.button {
+ position: relative;
+ cursor: default;
+ text-shadow: none !important;
+ color: transparent;
+ opacity: 1;
+ pointer-events: auto;
+ transition: all 0s linear, opacity 0.1s ease;
+}
+
+.ui.loading.button:before {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -0.64285714em 0 0 -0.64285714em;
+ width: 1.28571429em;
+ height: 1.28571429em;
+ border-radius: 500rem;
+ border: 0.2em solid rgba(0, 0, 0, 0.15);
+}
+
+.ui.loading.button:after {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -0.64285714em 0 0 -0.64285714em;
+ width: 1.28571429em;
+ height: 1.28571429em;
+ border-radius: 500rem;
+ animation: loader 0.6s infinite linear;
+ border: 0.2em solid currentColor;
+ color: #FFFFFF;
+ box-shadow: 0 0 0 1px transparent;
+}
+
+.ui.labeled.icon.loading.button .icon {
+ background-color: transparent;
+ box-shadow: none;
+}
+
+.ui.basic.loading.button:not(.inverted):before {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.ui.basic.loading.button:not(.inverted):after {
+ border-color: #767676;
+}
+
+/*-------------------
+ Disabled
+ --------------------*/
+
+.ui.buttons .disabled.button:not(.basic),
+.ui.disabled.button,
+.ui.button:disabled,
+.ui.disabled.button:hover,
+.ui.disabled.active.button {
+ cursor: default;
+ opacity: var(--opacity-disabled) !important;
+ background-image: none;
+ box-shadow: none;
+ pointer-events: none !important;
+}
+
+/* Basic Group With Disabled */
+
+.ui.basic.buttons .ui.disabled.button {
+ border-color: rgba(34, 36, 38, 0.5);
+}
+
+/*******************************
+ Types
+*******************************/
+
+/*-------------------
+ Labeled Button
+ --------------------*/
+
+.ui.labeled.button:not(.icon) {
+ display: inline-flex;
+ flex-direction: row;
+ background: none;
+ padding: 0 !important;
+ border: none;
+ box-shadow: none;
+}
+
+.ui.labeled.button > .button {
+ margin: 0;
+}
+
+.ui.labeled.button > .label {
+ display: flex;
+ align-items: center;
+ margin: 0 0 0 -1px !important;
+ font-size: 1em;
+ padding: '';
+ border-color: rgba(34, 36, 38, 0.15);
+}
+
+/* Tag */
+
+.ui.labeled.button > .tag.label:before {
+ width: 1.85em;
+ height: 1.85em;
+}
+
+/* Right */
+
+.ui.labeled.button:not([class*="left labeled"]) > .button {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.ui.labeled.button:not([class*="left labeled"]) > .label {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+/* Left Side */
+
+.ui[class*="left labeled"].button > .button {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.ui[class*="left labeled"].button > .label {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/*--------------
+ Icon
+---------------*/
+
+.ui.button > .icon:not(.button) {
+ height: auto;
+ opacity: 0.8;
+ transition: opacity 0.1s ease;
+ color: '';
+}
+
+.ui.button:not(.icon) > .icon:not(.button):not(.dropdown),
+.ui.button:not(.icon) > .icons:not(.button):not(.dropdown) {
+ margin: 0 0.42857143em 0 -0.21428571em;
+ vertical-align: baseline;
+}
+
+.ui.button:not(.icon) > .icons:not(.button):not(.dropdown) > .icon {
+ vertical-align: baseline;
+}
+
+.ui.button:not(.icon) > .right.icon:not(.button):not(.dropdown) {
+ margin: 0 -0.21428571em 0 0.42857143em;
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*-------------------
+ Floated
+ --------------------*/
+
+.ui[class*="left floated"].buttons,
+.ui[class*="left floated"].button {
+ float: left;
+ margin-left: 0;
+ margin-right: 0.25em;
+}
+
+.ui[class*="right floated"].buttons,
+.ui[class*="right floated"].button {
+ float: right;
+ margin-right: 0;
+ margin-left: 0.25em;
+}
+
+/*-------------------
+ Compact
+ --------------------*/
+
+.ui.compact.buttons .button,
+.ui.compact.button {
+ padding: 0.58928571em 1.125em 0.58928571em;
+}
+
+.ui.compact.icon.buttons .button,
+.ui.compact.icon.button {
+ padding: 0.58928571em 0.58928571em 0.58928571em;
+}
+
+.ui.compact.labeled.icon.buttons .button,
+.ui.compact.labeled.icon.button {
+ padding: 0.58928571em 3.69642857em 0.58928571em;
+}
+
+.ui.compact.labeled.icon.buttons .button > .icon,
+.ui.compact.labeled.icon.button > .icon {
+ padding: 0.58928571em 0 0.58928571em 0;
+}
+
+/*-------------------
+ Sizes
+--------------------*/
+
+.ui.buttons .button,
+.ui.buttons .or,
+.ui.button {
+ font-size: 1rem;
+}
+
+.ui.mini.buttons .dropdown,
+.ui.mini.buttons .dropdown .menu > .item,
+.ui.mini.buttons .button,
+.ui.mini.buttons .or,
+.ui.ui.ui.ui.mini.button {
+ font-size: 0.78571429rem;
+}
+
+.ui.tiny.buttons .dropdown,
+.ui.tiny.buttons .dropdown .menu > .item,
+.ui.tiny.buttons .button,
+.ui.tiny.buttons .or,
+.ui.ui.ui.ui.tiny.button {
+ font-size: 0.85714286rem;
+}
+
+.ui.small.buttons .dropdown,
+.ui.small.buttons .dropdown .menu > .item,
+.ui.small.buttons .button,
+.ui.small.buttons .or,
+.ui.ui.ui.ui.small.button {
+ font-size: 0.92857143rem;
+}
+
+.ui.large.buttons .dropdown,
+.ui.large.buttons .dropdown .menu > .item,
+.ui.large.buttons .button,
+.ui.large.buttons .or,
+.ui.ui.ui.ui.large.button {
+ font-size: 1.14285714rem;
+}
+
+.ui.big.buttons .dropdown,
+.ui.big.buttons .dropdown .menu > .item,
+.ui.big.buttons .button,
+.ui.big.buttons .or,
+.ui.ui.ui.ui.big.button {
+ font-size: 1.28571429rem;
+}
+
+.ui.huge.buttons .dropdown,
+.ui.huge.buttons .dropdown .menu > .item,
+.ui.huge.buttons .button,
+.ui.huge.buttons .or,
+.ui.ui.ui.ui.huge.button {
+ font-size: 1.42857143rem;
+}
+
+.ui.massive.buttons .dropdown,
+.ui.massive.buttons .dropdown .menu > .item,
+.ui.massive.buttons .button,
+.ui.massive.buttons .or,
+.ui.ui.ui.ui.massive.button {
+ font-size: 1.71428571rem;
+}
+
+/*--------------
+ Icon Only
+---------------*/
+
+.ui.icon.buttons .button,
+.ui.icon.button:not(.animated):not(.compact) {
+ padding: 0.78571429em 0.78571429em 0.78571429em;
+}
+
+.ui.animated.icon.button > .content > .icon,
+.ui.icon.buttons .button > .icon,
+.ui.icon.button > .icon {
+ opacity: 0.9;
+ margin: 0 !important;
+ vertical-align: top;
+}
+
+.ui.animated.button > .content > .icon {
+ vertical-align: top;
+}
+
+/*-------------------
+ Basic
+ --------------------*/
+
+.ui.basic.buttons .button,
+.ui.basic.button {
+ background: transparent none;
+ color: rgba(0, 0, 0, 0.6);
+ font-weight: normal;
+ border-radius: 0.28571429rem;
+ text-transform: none;
+ text-shadow: none !important;
+ box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.basic.buttons {
+ box-shadow: none;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ border-radius: 0.28571429rem;
+}
+
+.ui.basic.buttons .button {
+ border-radius: 0;
+}
+
+.ui.basic.buttons .button:hover,
+.ui.basic.button:hover {
+ background: #FFFFFF;
+ color: rgba(0, 0, 0, 0.8);
+ box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.35) inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.basic.buttons .button:focus,
+.ui.basic.button:focus {
+ background: #FFFFFF;
+ color: rgba(0, 0, 0, 0.8);
+ box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.35) inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.basic.buttons .button:active,
+.ui.basic.button:active {
+ background: #F8F8F8;
+ color: rgba(0, 0, 0, 0.9);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset, 0 1px 4px 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.basic.buttons .active.button,
+.ui.basic.active.button {
+ background: rgba(0, 0, 0, 0.05);
+ box-shadow: '';
+ color: rgba(0, 0, 0, 0.95);
+}
+
+.ui.basic.buttons .active.button:hover,
+.ui.basic.active.button:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+/* Vertical */
+
+.ui.basic.buttons .button:hover {
+ box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.35) inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset inset;
+}
+
+.ui.basic.buttons .button:active {
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset, 0 1px 4px 0 rgba(34, 36, 38, 0.15) inset inset;
+}
+
+.ui.basic.buttons .active.button {
+ box-shadow: '';
+}
+
+/* Basic Group */
+
+.ui.basic.buttons .button {
+ border-left: 1px solid rgba(34, 36, 38, 0.15);
+ box-shadow: none;
+}
+
+.ui.basic.vertical.buttons .button {
+ border-left: none;
+ border-left-width: 0;
+ border-top: 1px solid rgba(34, 36, 38, 0.15);
+}
+
+.ui.basic.vertical.buttons .button:first-child {
+ border-top-width: 0;
+}
+
+/*--------------
+ Labeled Icon
+ ---------------*/
+
+.ui.labeled.icon.buttons .button,
+.ui.labeled.icon.button {
+ position: relative;
+ padding-left: 4.07142857em !important;
+ padding-right: 1.5em !important;
+}
+
+/* Left Labeled */
+
+.ui.labeled.icon.buttons > .button > .icon,
+.ui.labeled.icon.button > .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ line-height: 1;
+ border-radius: 0;
+ border-top-left-radius: inherit;
+ border-bottom-left-radius: inherit;
+ text-align: center;
+ animation: none;
+ padding: 0.78571429em 0 0.78571429em 0;
+ margin: 0;
+ width: 2.57142857em;
+ background-color: rgba(0, 0, 0, 0.05);
+ color: '';
+ box-shadow: -1px 0 0 0 transparent inset;
+}
+
+/* Right Labeled */
+
+.ui[class*="right labeled"].icon.button {
+ padding-right: 4.07142857em !important;
+ padding-left: 1.5em !important;
+}
+
+.ui[class*="right labeled"].icon.button > .icon {
+ left: auto;
+ right: 0;
+ border-radius: 0;
+ border-top-right-radius: inherit;
+ border-bottom-right-radius: inherit;
+ box-shadow: 1px 0 0 0 transparent inset;
+}
+
+.ui.labeled.icon.buttons > .button > .icon:before,
+.ui.labeled.icon.button > .icon:before,
+.ui.labeled.icon.buttons > .button > .icon:after,
+.ui.labeled.icon.button > .icon:after {
+ display: block;
+ position: relative;
+ width: 100%;
+ top: 0;
+ text-align: center;
+}
+
+.ui.labeled.icon.buttons .button > .icon {
+ border-radius: 0;
+}
+
+.ui.labeled.icon.buttons .button:first-child > .icon {
+ border-top-left-radius: 0.28571429rem;
+ border-bottom-left-radius: 0.28571429rem;
+}
+
+.ui.labeled.icon.buttons .button:last-child > .icon {
+ border-top-right-radius: 0.28571429rem;
+ border-bottom-right-radius: 0.28571429rem;
+}
+
+.ui.vertical.labeled.icon.buttons .button:first-child > .icon {
+ border-radius: 0;
+ border-top-left-radius: 0.28571429rem;
+}
+
+.ui.vertical.labeled.icon.buttons .button:last-child > .icon {
+ border-radius: 0;
+ border-bottom-left-radius: 0.28571429rem;
+}
+
+/* Loading Icon in Labeled Button */
+
+.ui.labeled.icon.button > .loading.icon:before {
+ animation: loader 2s linear infinite;
+}
+
+/*--------------
+ Toggle
+ ---------------*/
+
+/* Toggle (Modifies active state to give affordances) */
+
+.ui.toggle.buttons .active.button,
+.ui.buttons .button.toggle.active,
+.ui.button.toggle.active {
+ background-color: #21BA45;
+ box-shadow: none;
+ text-shadow: none;
+ color: #FFFFFF;
+}
+
+.ui.button.toggle.active:hover {
+ background-color: #16ab39;
+ text-shadow: none;
+ color: #FFFFFF;
+}
+
+/*--------------
+ Circular
+ ---------------*/
+
+.ui.circular.button {
+ border-radius: 10em;
+}
+
+.ui.circular.button > .icon {
+ width: 1em;
+ vertical-align: baseline;
+}
+
+/*-------------------
+ Or Buttons
+ --------------------*/
+
+.ui.buttons .or {
+ position: relative;
+ width: 0.3em;
+ height: 2.57142857em;
+ z-index: 3;
+}
+
+.ui.buttons .or:before {
+ position: absolute;
+ text-align: center;
+ border-radius: 500rem;
+ content: 'or';
+ top: 50%;
+ left: 50%;
+ background-color: #FFFFFF;
+ text-shadow: none;
+ margin-top: -0.89285714em;
+ margin-left: -0.89285714em;
+ width: 1.78571429em;
+ height: 1.78571429em;
+ line-height: 1.78571429em;
+ color: rgba(0, 0, 0, 0.4);
+ font-style: normal;
+ font-weight: 500;
+ box-shadow: 0 0 0 1px transparent inset;
+}
+
+.ui.buttons .or[data-text]:before {
+ content: attr(data-text);
+}
+
+/* Fluid Or */
+
+.ui.fluid.buttons .or {
+ width: 0 !important;
+}
+
+.ui.fluid.buttons .or:after {
+ display: none;
+}
+
+/*-------------------
+ Fluid
+ --------------------*/
+
+.ui.fluid.buttons,
+.ui.fluid.button {
+ width: 100%;
+}
+
+.ui.fluid.button {
+ display: block;
+}
+
+.ui.two.buttons {
+ width: 100%;
+}
+
+.ui.two.buttons > .button {
+ width: 50%;
+}
+
+.ui.three.buttons {
+ width: 100%;
+}
+
+.ui.three.buttons > .button {
+ width: 33.333%;
+}
+
+.ui.four.buttons {
+ width: 100%;
+}
+
+.ui.four.buttons > .button {
+ width: 25%;
+}
+
+.ui.five.buttons {
+ width: 100%;
+}
+
+.ui.five.buttons > .button {
+ width: 20%;
+}
+
+.ui.six.buttons {
+ width: 100%;
+}
+
+.ui.six.buttons > .button {
+ width: 16.666%;
+}
+
+.ui.seven.buttons {
+ width: 100%;
+}
+
+.ui.seven.buttons > .button {
+ width: 14.285%;
+}
+
+.ui.eight.buttons {
+ width: 100%;
+}
+
+.ui.eight.buttons > .button {
+ width: 12.5%;
+}
+
+.ui.nine.buttons {
+ width: 100%;
+}
+
+.ui.nine.buttons > .button {
+ width: 11.11%;
+}
+
+.ui.ten.buttons {
+ width: 100%;
+}
+
+.ui.ten.buttons > .button {
+ width: 10%;
+}
+
+.ui.eleven.buttons {
+ width: 100%;
+}
+
+.ui.eleven.buttons > .button {
+ width: 9.09%;
+}
+
+.ui.twelve.buttons {
+ width: 100%;
+}
+
+.ui.twelve.buttons > .button {
+ width: 8.3333%;
+}
+
+/* Fluid Vertical Buttons */
+
+.ui.fluid.vertical.buttons,
+.ui.fluid.vertical.buttons > .button {
+ display: flex;
+ width: auto;
+ justify-content: center;
+}
+
+.ui.two.vertical.buttons > .button {
+ height: 50%;
+}
+
+.ui.three.vertical.buttons > .button {
+ height: 33.333%;
+}
+
+.ui.four.vertical.buttons > .button {
+ height: 25%;
+}
+
+.ui.five.vertical.buttons > .button {
+ height: 20%;
+}
+
+.ui.six.vertical.buttons > .button {
+ height: 16.666%;
+}
+
+.ui.seven.vertical.buttons > .button {
+ height: 14.285%;
+}
+
+.ui.eight.vertical.buttons > .button {
+ height: 12.5%;
+}
+
+.ui.nine.vertical.buttons > .button {
+ height: 11.11%;
+}
+
+.ui.ten.vertical.buttons > .button {
+ height: 10%;
+}
+
+.ui.eleven.vertical.buttons > .button {
+ height: 9.09%;
+}
+
+.ui.twelve.vertical.buttons > .button {
+ height: 8.3333%;
+}
+
+/*-------------------
+ Colors
+--------------------*/
+
+.ui.primary.buttons .button,
+.ui.primary.button {
+ background-color: #2185D0;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.primary.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.primary.buttons .button:hover,
+.ui.primary.button:hover {
+ background-color: #1678c2;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.primary.buttons .button:focus,
+.ui.primary.button:focus {
+ background-color: #0d71bb;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.primary.buttons .button:active,
+.ui.primary.button:active {
+ background-color: #1a69a4;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.primary.buttons .active.button,
+.ui.primary.buttons .active.button:active,
+.ui.primary.active.button,
+.ui.primary.button .active.button:active {
+ background-color: #1279c6;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.primary.buttons .button,
+.ui.basic.primary.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #2185D0 inset;
+ color: #2185D0;
+}
+
+.ui.basic.primary.buttons .button:hover,
+.ui.basic.primary.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #1678c2 inset;
+ color: #1678c2;
+}
+
+.ui.basic.primary.buttons .button:focus,
+.ui.basic.primary.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #0d71bb inset;
+ color: #1678c2;
+}
+
+.ui.basic.primary.buttons .active.button,
+.ui.basic.primary.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #1279c6 inset;
+ color: #1a69a4;
+}
+
+.ui.basic.primary.buttons .button:active,
+.ui.basic.primary.button:active {
+ box-shadow: 0 0 0 1px #1a69a4 inset;
+ color: #1a69a4;
+}
+
+.ui.buttons:not(.vertical) > .basic.primary.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.secondary.buttons .button,
+.ui.secondary.button {
+ background-color: #1B1C1D;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.secondary.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.secondary.buttons .button:hover,
+.ui.secondary.button:hover {
+ background-color: #27292a;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.secondary.buttons .button:focus,
+.ui.secondary.button:focus {
+ background-color: #2e3032;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.secondary.buttons .button:active,
+.ui.secondary.button:active {
+ background-color: #343637;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.secondary.buttons .active.button,
+.ui.secondary.buttons .active.button:active,
+.ui.secondary.active.button,
+.ui.secondary.button .active.button:active {
+ background-color: #27292a;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.secondary.buttons .button,
+.ui.basic.secondary.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #1B1C1D inset;
+ color: #1B1C1D;
+}
+
+.ui.basic.secondary.buttons .button:hover,
+.ui.basic.secondary.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #27292a inset;
+ color: #27292a;
+}
+
+.ui.basic.secondary.buttons .button:focus,
+.ui.basic.secondary.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #2e3032 inset;
+ color: #27292a;
+}
+
+.ui.basic.secondary.buttons .active.button,
+.ui.basic.secondary.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #27292a inset;
+ color: #343637;
+}
+
+.ui.basic.secondary.buttons .button:active,
+.ui.basic.secondary.button:active {
+ box-shadow: 0 0 0 1px #343637 inset;
+ color: #343637;
+}
+
+.ui.buttons:not(.vertical) > .basic.secondary.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.red.buttons .button,
+.ui.red.button {
+ background-color: #DB2828;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.red.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.red.buttons .button:hover,
+.ui.red.button:hover {
+ background-color: #d01919;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.red.buttons .button:focus,
+.ui.red.button:focus {
+ background-color: #ca1010;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.red.buttons .button:active,
+.ui.red.button:active {
+ background-color: #b21e1e;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.red.buttons .active.button,
+.ui.red.buttons .active.button:active,
+.ui.red.active.button,
+.ui.red.button .active.button:active {
+ background-color: #d41515;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.red.buttons .button,
+.ui.basic.red.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #DB2828 inset;
+ color: #DB2828;
+}
+
+.ui.basic.red.buttons .button:hover,
+.ui.basic.red.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #d01919 inset;
+ color: #d01919;
+}
+
+.ui.basic.red.buttons .button:focus,
+.ui.basic.red.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #ca1010 inset;
+ color: #d01919;
+}
+
+.ui.basic.red.buttons .active.button,
+.ui.basic.red.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #d41515 inset;
+ color: #b21e1e;
+}
+
+.ui.basic.red.buttons .button:active,
+.ui.basic.red.button:active {
+ box-shadow: 0 0 0 1px #b21e1e inset;
+ color: #b21e1e;
+}
+
+.ui.buttons:not(.vertical) > .basic.red.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.orange.buttons .button,
+.ui.orange.button {
+ background-color: #F2711C;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.orange.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.orange.buttons .button:hover,
+.ui.orange.button:hover {
+ background-color: #f26202;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.orange.buttons .button:focus,
+.ui.orange.button:focus {
+ background-color: #e55b00;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.orange.buttons .button:active,
+.ui.orange.button:active {
+ background-color: #cf590c;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.orange.buttons .active.button,
+.ui.orange.buttons .active.button:active,
+.ui.orange.active.button,
+.ui.orange.button .active.button:active {
+ background-color: #f56100;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.orange.buttons .button,
+.ui.basic.orange.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #F2711C inset;
+ color: #F2711C;
+}
+
+.ui.basic.orange.buttons .button:hover,
+.ui.basic.orange.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #f26202 inset;
+ color: #f26202;
+}
+
+.ui.basic.orange.buttons .button:focus,
+.ui.basic.orange.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #e55b00 inset;
+ color: #f26202;
+}
+
+.ui.basic.orange.buttons .active.button,
+.ui.basic.orange.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #f56100 inset;
+ color: #cf590c;
+}
+
+.ui.basic.orange.buttons .button:active,
+.ui.basic.orange.button:active {
+ box-shadow: 0 0 0 1px #cf590c inset;
+ color: #cf590c;
+}
+
+.ui.buttons:not(.vertical) > .basic.orange.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.yellow.buttons .button,
+.ui.yellow.button {
+ background-color: #FBBD08;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.yellow.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.yellow.buttons .button:hover,
+.ui.yellow.button:hover {
+ background-color: #eaae00;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.yellow.buttons .button:focus,
+.ui.yellow.button:focus {
+ background-color: #daa300;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.yellow.buttons .button:active,
+.ui.yellow.button:active {
+ background-color: #cd9903;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.yellow.buttons .active.button,
+.ui.yellow.buttons .active.button:active,
+.ui.yellow.active.button,
+.ui.yellow.button .active.button:active {
+ background-color: #eaae00;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.yellow.buttons .button,
+.ui.basic.yellow.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #FBBD08 inset;
+ color: #FBBD08;
+}
+
+.ui.basic.yellow.buttons .button:hover,
+.ui.basic.yellow.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #eaae00 inset;
+ color: #eaae00;
+}
+
+.ui.basic.yellow.buttons .button:focus,
+.ui.basic.yellow.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #daa300 inset;
+ color: #eaae00;
+}
+
+.ui.basic.yellow.buttons .active.button,
+.ui.basic.yellow.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #eaae00 inset;
+ color: #cd9903;
+}
+
+.ui.basic.yellow.buttons .button:active,
+.ui.basic.yellow.button:active {
+ box-shadow: 0 0 0 1px #cd9903 inset;
+ color: #cd9903;
+}
+
+.ui.buttons:not(.vertical) > .basic.yellow.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.olive.buttons .button,
+.ui.olive.button {
+ background-color: #B5CC18;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.olive.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.olive.buttons .button:hover,
+.ui.olive.button:hover {
+ background-color: #a7bd0d;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.olive.buttons .button:focus,
+.ui.olive.button:focus {
+ background-color: #a0b605;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.olive.buttons .button:active,
+.ui.olive.button:active {
+ background-color: #8d9e13;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.olive.buttons .active.button,
+.ui.olive.buttons .active.button:active,
+.ui.olive.active.button,
+.ui.olive.button .active.button:active {
+ background-color: #aac109;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.olive.buttons .button,
+.ui.basic.olive.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #B5CC18 inset;
+ color: #B5CC18;
+}
+
+.ui.basic.olive.buttons .button:hover,
+.ui.basic.olive.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #a7bd0d inset;
+ color: #a7bd0d;
+}
+
+.ui.basic.olive.buttons .button:focus,
+.ui.basic.olive.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #a0b605 inset;
+ color: #a7bd0d;
+}
+
+.ui.basic.olive.buttons .active.button,
+.ui.basic.olive.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #aac109 inset;
+ color: #8d9e13;
+}
+
+.ui.basic.olive.buttons .button:active,
+.ui.basic.olive.button:active {
+ box-shadow: 0 0 0 1px #8d9e13 inset;
+ color: #8d9e13;
+}
+
+.ui.buttons:not(.vertical) > .basic.olive.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.green.buttons .button,
+.ui.green.button {
+ background-color: #21BA45;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.green.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.green.buttons .button:hover,
+.ui.green.button:hover {
+ background-color: #16ab39;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.green.buttons .button:focus,
+.ui.green.button:focus {
+ background-color: #0ea432;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.green.buttons .button:active,
+.ui.green.button:active {
+ background-color: #198f35;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.green.buttons .active.button,
+.ui.green.buttons .active.button:active,
+.ui.green.active.button,
+.ui.green.button .active.button:active {
+ background-color: #13ae38;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.green.buttons .button,
+.ui.basic.green.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #21BA45 inset;
+ color: #21BA45;
+}
+
+.ui.basic.green.buttons .button:hover,
+.ui.basic.green.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #16ab39 inset;
+ color: #16ab39;
+}
+
+.ui.basic.green.buttons .button:focus,
+.ui.basic.green.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #0ea432 inset;
+ color: #16ab39;
+}
+
+.ui.basic.green.buttons .active.button,
+.ui.basic.green.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #13ae38 inset;
+ color: #198f35;
+}
+
+.ui.basic.green.buttons .button:active,
+.ui.basic.green.button:active {
+ box-shadow: 0 0 0 1px #198f35 inset;
+ color: #198f35;
+}
+
+.ui.buttons:not(.vertical) > .basic.green.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.teal.buttons .button,
+.ui.teal.button {
+ background-color: #00B5AD;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.teal.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.teal.buttons .button:hover,
+.ui.teal.button:hover {
+ background-color: #009c95;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.teal.buttons .button:focus,
+.ui.teal.button:focus {
+ background-color: #008c86;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.teal.buttons .button:active,
+.ui.teal.button:active {
+ background-color: #00827c;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.teal.buttons .active.button,
+.ui.teal.buttons .active.button:active,
+.ui.teal.active.button,
+.ui.teal.button .active.button:active {
+ background-color: #009c95;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.teal.buttons .button,
+.ui.basic.teal.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #00B5AD inset;
+ color: #00B5AD;
+}
+
+.ui.basic.teal.buttons .button:hover,
+.ui.basic.teal.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #009c95 inset;
+ color: #009c95;
+}
+
+.ui.basic.teal.buttons .button:focus,
+.ui.basic.teal.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #008c86 inset;
+ color: #009c95;
+}
+
+.ui.basic.teal.buttons .active.button,
+.ui.basic.teal.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #009c95 inset;
+ color: #00827c;
+}
+
+.ui.basic.teal.buttons .button:active,
+.ui.basic.teal.button:active {
+ box-shadow: 0 0 0 1px #00827c inset;
+ color: #00827c;
+}
+
+.ui.buttons:not(.vertical) > .basic.teal.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.blue.buttons .button,
+.ui.blue.button {
+ background-color: #2185D0;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.blue.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.blue.buttons .button:hover,
+.ui.blue.button:hover {
+ background-color: #1678c2;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.blue.buttons .button:focus,
+.ui.blue.button:focus {
+ background-color: #0d71bb;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.blue.buttons .button:active,
+.ui.blue.button:active {
+ background-color: #1a69a4;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.blue.buttons .active.button,
+.ui.blue.buttons .active.button:active,
+.ui.blue.active.button,
+.ui.blue.button .active.button:active {
+ background-color: #1279c6;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.blue.buttons .button,
+.ui.basic.blue.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #2185D0 inset;
+ color: #2185D0;
+}
+
+.ui.basic.blue.buttons .button:hover,
+.ui.basic.blue.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #1678c2 inset;
+ color: #1678c2;
+}
+
+.ui.basic.blue.buttons .button:focus,
+.ui.basic.blue.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #0d71bb inset;
+ color: #1678c2;
+}
+
+.ui.basic.blue.buttons .active.button,
+.ui.basic.blue.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #1279c6 inset;
+ color: #1a69a4;
+}
+
+.ui.basic.blue.buttons .button:active,
+.ui.basic.blue.button:active {
+ box-shadow: 0 0 0 1px #1a69a4 inset;
+ color: #1a69a4;
+}
+
+.ui.buttons:not(.vertical) > .basic.blue.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.violet.buttons .button,
+.ui.violet.button {
+ background-color: #6435C9;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.violet.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.violet.buttons .button:hover,
+.ui.violet.button:hover {
+ background-color: #5829bb;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.violet.buttons .button:focus,
+.ui.violet.button:focus {
+ background-color: #4f20b5;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.violet.buttons .button:active,
+.ui.violet.button:active {
+ background-color: #502aa1;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.violet.buttons .active.button,
+.ui.violet.buttons .active.button:active,
+.ui.violet.active.button,
+.ui.violet.button .active.button:active {
+ background-color: #5626bf;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.violet.buttons .button,
+.ui.basic.violet.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #6435C9 inset;
+ color: #6435C9;
+}
+
+.ui.basic.violet.buttons .button:hover,
+.ui.basic.violet.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #5829bb inset;
+ color: #5829bb;
+}
+
+.ui.basic.violet.buttons .button:focus,
+.ui.basic.violet.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #4f20b5 inset;
+ color: #5829bb;
+}
+
+.ui.basic.violet.buttons .active.button,
+.ui.basic.violet.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #5626bf inset;
+ color: #502aa1;
+}
+
+.ui.basic.violet.buttons .button:active,
+.ui.basic.violet.button:active {
+ box-shadow: 0 0 0 1px #502aa1 inset;
+ color: #502aa1;
+}
+
+.ui.buttons:not(.vertical) > .basic.violet.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.purple.buttons .button,
+.ui.purple.button {
+ background-color: #A333C8;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.purple.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.purple.buttons .button:hover,
+.ui.purple.button:hover {
+ background-color: #9627ba;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.purple.buttons .button:focus,
+.ui.purple.button:focus {
+ background-color: #8f1eb4;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.purple.buttons .button:active,
+.ui.purple.button:active {
+ background-color: #82299f;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.purple.buttons .active.button,
+.ui.purple.buttons .active.button:active,
+.ui.purple.active.button,
+.ui.purple.button .active.button:active {
+ background-color: #9724be;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.purple.buttons .button,
+.ui.basic.purple.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #A333C8 inset;
+ color: #A333C8;
+}
+
+.ui.basic.purple.buttons .button:hover,
+.ui.basic.purple.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #9627ba inset;
+ color: #9627ba;
+}
+
+.ui.basic.purple.buttons .button:focus,
+.ui.basic.purple.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #8f1eb4 inset;
+ color: #9627ba;
+}
+
+.ui.basic.purple.buttons .active.button,
+.ui.basic.purple.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #9724be inset;
+ color: #82299f;
+}
+
+.ui.basic.purple.buttons .button:active,
+.ui.basic.purple.button:active {
+ box-shadow: 0 0 0 1px #82299f inset;
+ color: #82299f;
+}
+
+.ui.buttons:not(.vertical) > .basic.purple.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.pink.buttons .button,
+.ui.pink.button {
+ background-color: #E03997;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.pink.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.pink.buttons .button:hover,
+.ui.pink.button:hover {
+ background-color: #e61a8d;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.pink.buttons .button:focus,
+.ui.pink.button:focus {
+ background-color: #e10f85;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.pink.buttons .button:active,
+.ui.pink.button:active {
+ background-color: #c71f7e;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.pink.buttons .active.button,
+.ui.pink.buttons .active.button:active,
+.ui.pink.active.button,
+.ui.pink.button .active.button:active {
+ background-color: #ea158d;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.pink.buttons .button,
+.ui.basic.pink.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #E03997 inset;
+ color: #E03997;
+}
+
+.ui.basic.pink.buttons .button:hover,
+.ui.basic.pink.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #e61a8d inset;
+ color: #e61a8d;
+}
+
+.ui.basic.pink.buttons .button:focus,
+.ui.basic.pink.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #e10f85 inset;
+ color: #e61a8d;
+}
+
+.ui.basic.pink.buttons .active.button,
+.ui.basic.pink.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #ea158d inset;
+ color: #c71f7e;
+}
+
+.ui.basic.pink.buttons .button:active,
+.ui.basic.pink.button:active {
+ box-shadow: 0 0 0 1px #c71f7e inset;
+ color: #c71f7e;
+}
+
+.ui.buttons:not(.vertical) > .basic.pink.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.brown.buttons .button,
+.ui.brown.button {
+ background-color: #A5673F;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.brown.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.brown.buttons .button:hover,
+.ui.brown.button:hover {
+ background-color: #975b33;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.brown.buttons .button:focus,
+.ui.brown.button:focus {
+ background-color: #90532b;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.brown.buttons .button:active,
+.ui.brown.button:active {
+ background-color: #805031;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.brown.buttons .active.button,
+.ui.brown.buttons .active.button:active,
+.ui.brown.active.button,
+.ui.brown.button .active.button:active {
+ background-color: #995a31;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.brown.buttons .button,
+.ui.basic.brown.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #A5673F inset;
+ color: #A5673F;
+}
+
+.ui.basic.brown.buttons .button:hover,
+.ui.basic.brown.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #975b33 inset;
+ color: #975b33;
+}
+
+.ui.basic.brown.buttons .button:focus,
+.ui.basic.brown.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #90532b inset;
+ color: #975b33;
+}
+
+.ui.basic.brown.buttons .active.button,
+.ui.basic.brown.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #995a31 inset;
+ color: #805031;
+}
+
+.ui.basic.brown.buttons .button:active,
+.ui.basic.brown.button:active {
+ box-shadow: 0 0 0 1px #805031 inset;
+ color: #805031;
+}
+
+.ui.buttons:not(.vertical) > .basic.brown.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.grey.buttons .button,
+.ui.grey.button {
+ background-color: #767676;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.grey.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.grey.buttons .button:hover,
+.ui.grey.button:hover {
+ background-color: #838383;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.grey.buttons .button:focus,
+.ui.grey.button:focus {
+ background-color: #8a8a8a;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.grey.buttons .button:active,
+.ui.grey.button:active {
+ background-color: #909090;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.grey.buttons .active.button,
+.ui.grey.buttons .active.button:active,
+.ui.grey.active.button,
+.ui.grey.button .active.button:active {
+ background-color: #696969;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.grey.buttons .button,
+.ui.basic.grey.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #767676 inset;
+ color: #767676;
+}
+
+.ui.basic.grey.buttons .button:hover,
+.ui.basic.grey.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #838383 inset;
+ color: #838383;
+}
+
+.ui.basic.grey.buttons .button:focus,
+.ui.basic.grey.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #8a8a8a inset;
+ color: #838383;
+}
+
+.ui.basic.grey.buttons .active.button,
+.ui.basic.grey.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #696969 inset;
+ color: #909090;
+}
+
+.ui.basic.grey.buttons .button:active,
+.ui.basic.grey.button:active {
+ box-shadow: 0 0 0 1px #909090 inset;
+ color: #909090;
+}
+
+.ui.buttons:not(.vertical) > .basic.grey.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+.ui.black.buttons .button,
+.ui.black.button {
+ background-color: #1B1C1D;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.black.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.black.buttons .button:hover,
+.ui.black.button:hover {
+ background-color: #27292a;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.black.buttons .button:focus,
+.ui.black.button:focus {
+ background-color: #2f3032;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.black.buttons .button:active,
+.ui.black.button:active {
+ background-color: #343637;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.black.buttons .active.button,
+.ui.black.buttons .active.button:active,
+.ui.black.active.button,
+.ui.black.button .active.button:active {
+ background-color: #0f0f10;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.black.buttons .button,
+.ui.basic.black.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #1B1C1D inset;
+ color: #1B1C1D;
+}
+
+.ui.basic.black.buttons .button:hover,
+.ui.basic.black.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #27292a inset;
+ color: #27292a;
+}
+
+.ui.basic.black.buttons .button:focus,
+.ui.basic.black.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #2f3032 inset;
+ color: #27292a;
+}
+
+.ui.basic.black.buttons .active.button,
+.ui.basic.black.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #0f0f10 inset;
+ color: #343637;
+}
+
+.ui.basic.black.buttons .button:active,
+.ui.basic.black.button:active {
+ box-shadow: 0 0 0 1px #343637 inset;
+ color: #343637;
+}
+
+.ui.buttons:not(.vertical) > .basic.black.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+/*---------------
+ Positive
+----------------*/
+
+/* Standard */
+
+.ui.positive.buttons .button,
+.ui.positive.button {
+ background-color: #21BA45;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.positive.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.positive.buttons .button:hover,
+.ui.positive.button:hover {
+ background-color: #16ab39;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.positive.buttons .button:focus,
+.ui.positive.button:focus {
+ background-color: #0ea432;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.positive.buttons .button:active,
+.ui.positive.button:active {
+ background-color: #198f35;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.positive.buttons .active.button,
+.ui.positive.buttons .active.button:active,
+.ui.positive.active.button,
+.ui.positive.button .active.button:active {
+ background-color: #13ae38;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.positive.buttons .button,
+.ui.basic.positive.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #21BA45 inset;
+ color: #21BA45;
+}
+
+.ui.basic.positive.buttons .button:hover,
+.ui.basic.positive.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #16ab39 inset;
+ color: #16ab39;
+}
+
+.ui.basic.positive.buttons .button:focus,
+.ui.basic.positive.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #0ea432 inset;
+ color: #16ab39;
+}
+
+.ui.basic.positive.buttons .active.button,
+.ui.basic.positive.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #13ae38 inset;
+ color: #198f35;
+}
+
+.ui.basic.positive.buttons .button:active,
+.ui.basic.positive.button:active {
+ box-shadow: 0 0 0 1px #198f35 inset;
+ color: #198f35;
+}
+
+.ui.buttons:not(.vertical) > .basic.positive.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+/*---------------
+ Negative
+----------------*/
+
+/* Standard */
+
+.ui.negative.buttons .button,
+.ui.negative.button {
+ background-color: #DB2828;
+ color: #FFFFFF;
+ text-shadow: none;
+ background-image: none;
+}
+
+.ui.negative.button {
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.negative.buttons .button:hover,
+.ui.negative.button:hover {
+ background-color: #d01919;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.negative.buttons .button:focus,
+.ui.negative.button:focus {
+ background-color: #ca1010;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.negative.buttons .button:active,
+.ui.negative.button:active {
+ background-color: #b21e1e;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+.ui.negative.buttons .active.button,
+.ui.negative.buttons .active.button:active,
+.ui.negative.active.button,
+.ui.negative.button .active.button:active {
+ background-color: #d41515;
+ color: #FFFFFF;
+ text-shadow: none;
+}
+
+/* Basic */
+
+.ui.basic.negative.buttons .button,
+.ui.basic.negative.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #DB2828 inset;
+ color: #DB2828;
+}
+
+.ui.basic.negative.buttons .button:hover,
+.ui.basic.negative.button:hover {
+ background: transparent;
+ box-shadow: 0 0 0 1px #d01919 inset;
+ color: #d01919;
+}
+
+.ui.basic.negative.buttons .button:focus,
+.ui.basic.negative.button:focus {
+ background: transparent;
+ box-shadow: 0 0 0 1px #ca1010 inset;
+ color: #d01919;
+}
+
+.ui.basic.negative.buttons .active.button,
+.ui.basic.negative.active.button {
+ background: transparent;
+ box-shadow: 0 0 0 1px #d41515 inset;
+ color: #b21e1e;
+}
+
+.ui.basic.negative.buttons .button:active,
+.ui.basic.negative.button:active {
+ box-shadow: 0 0 0 1px #b21e1e inset;
+ color: #b21e1e;
+}
+
+.ui.buttons:not(.vertical) > .basic.negative.button:not(:first-child) {
+ margin-left: -1px;
+}
+
+/*******************************
+ Groups
+ *******************************/
+
+.ui.buttons {
+ display: inline-flex;
+ flex-direction: row;
+ font-size: 0;
+ vertical-align: baseline;
+ margin: 0 0.25em 0 0;
+}
+
+.ui.buttons:not(.basic):not(.inverted) {
+ box-shadow: none;
+}
+
+/* Clearfix */
+
+.ui.buttons:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+/* Standard Group */
+
+.ui.buttons .button {
+ flex: 1 0 auto;
+ border-radius: 0;
+ margin: 0 0 0 0;
+}
+
+.ui.buttons:not(.basic):not(.inverted) > .button:not(.basic):not(.inverted) {
+ box-shadow: 0 0 0 1px transparent inset, 0 0 0 0 rgba(34, 36, 38, 0.15) inset;
+}
+
+.ui.buttons .button:first-child {
+ border-left: none;
+ margin-left: 0;
+ border-top-left-radius: 0.28571429rem;
+ border-bottom-left-radius: 0.28571429rem;
+}
+
+.ui.buttons .button:last-child {
+ border-top-right-radius: 0.28571429rem;
+ border-bottom-right-radius: 0.28571429rem;
+}
+
+/* Vertical Style */
+
+.ui.vertical.buttons {
+ display: inline-flex;
+ flex-direction: column;
+}
+
+.ui.vertical.buttons .button {
+ display: block;
+ float: none;
+ width: 100%;
+ margin: 0 0 0 0;
+ box-shadow: none;
+ border-radius: 0;
+}
+
+.ui.vertical.buttons .button:first-child {
+ border-top-left-radius: 0.28571429rem;
+ border-top-right-radius: 0.28571429rem;
+}
+
+.ui.vertical.buttons .button:last-child {
+ margin-bottom: 0;
+ border-bottom-left-radius: 0.28571429rem;
+ border-bottom-right-radius: 0.28571429rem;
+}
+
+.ui.vertical.buttons .button:only-child {
+ border-radius: 0.28571429rem;
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/*******************************
+ Site Overrides
+*******************************/
+/*!
+ * # Fomantic-UI - Dimmer
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Dimmer
+*******************************/
+
+.dimmable:not(body) {
+ position: relative;
+}
+
+.ui.dimmer {
+ display: none;
+ position: absolute;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ vertical-align: middle;
+ padding: 1em;
+ background: rgba(0, 0, 0, 0.85);
+ opacity: 0;
+ line-height: 1;
+ animation-fill-mode: both;
+ animation-duration: 0.5s;
+ transition: background-color 0.5s linear;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ will-change: opacity;
+ z-index: 1000;
+}
+
+/* Dimmer Content */
+
+.ui.dimmer > .content {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ user-select: text;
+ color: #FFFFFF;
+}
+
+/* Loose Coupling */
+
+.ui.segment > .ui.dimmer:not(.page) {
+ border-radius: inherit;
+}
+
+/* Scrollbars */
+
+/*******************************
+ States
+*******************************/
+
+/* Animating */
+
+.animating.dimmable:not(body),
+.dimmed.dimmable:not(body) {
+ overflow: hidden;
+}
+
+/* Animating / Active / Visible */
+
+.dimmed.dimmable > .ui.animating.dimmer,
+.dimmed.dimmable > .ui.visible.dimmer,
+.ui.active.dimmer {
+ display: flex;
+ opacity: 1;
+}
+
+/* Disabled */
+
+.ui.disabled.dimmer {
+ width: 0 !important;
+ height: 0 !important;
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*--------------
+ Legacy
+ ---------------*/
+
+/* Animating / Active / Visible */
+
+.dimmed.dimmable > .ui.animating.legacy.dimmer,
+.dimmed.dimmable > .ui.visible.legacy.dimmer,
+.ui.active.legacy.dimmer {
+ display: block;
+}
+
+/*--------------
+ Alignment
+ ---------------*/
+
+.ui[class*="top aligned"].dimmer {
+ justify-content: flex-start;
+}
+
+.ui[class*="bottom aligned"].dimmer {
+ justify-content: flex-end;
+}
+
+/*--------------
+ Page
+ ---------------*/
+
+.ui.page.dimmer {
+ position: fixed;
+ transform-style: '';
+ perspective: 2000px;
+ transform-origin: center center;
+}
+
+.ui.page.dimmer.modals {
+ -moz-perspective: none;
+}
+
+body.animating.in.dimmable,
+body.dimmed.dimmable {
+ overflow: hidden;
+}
+
+body.dimmable > .dimmer {
+ position: fixed;
+}
+
+/*--------------
+ Blurring
+ ---------------*/
+
+.blurring.dimmable > :not(.dimmer) {
+ filter: initial;
+ transition: 800ms filter ease;
+}
+
+.blurring.dimmed.dimmable > :not(.dimmer):not(.popup) {
+ filter: blur(5px) grayscale(0.7);
+}
+
+/* Dimmer Color */
+
+.blurring.dimmable > .dimmer {
+ background: rgba(0, 0, 0, 0.6);
+}
+
+.blurring.dimmable > .inverted.dimmer {
+ background: rgba(255, 255, 255, 0.6);
+}
+
+/*--------------
+ Aligned
+ ---------------*/
+
+.ui.dimmer > .top.aligned.content > * {
+ vertical-align: top;
+}
+
+.ui.dimmer > .bottom.aligned.content > * {
+ vertical-align: bottom;
+}
+
+/*--------------
+ Shades
+ ---------------*/
+
+.medium.medium.medium.medium.medium.dimmer {
+ background: rgba(0, 0, 0, 0.65);
+}
+
+.light.light.light.light.light.dimmer {
+ background: rgba(0, 0, 0, 0.45);
+}
+
+.very.light.light.light.light.dimmer {
+ background: rgba(0, 0, 0, 0.25);
+}
+
+/*--------------
+ Simple
+ ---------------*/
+
+/* Displays without javascript */
+
+.ui.simple.dimmer {
+ display: block;
+ overflow: hidden;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ z-index: -100;
+ background: rgba(0, 0, 0, 0);
+}
+
+.dimmed.dimmable > .ui.simple.dimmer {
+ overflow: visible;
+ opacity: 1;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.85);
+ z-index: 1;
+}
+
+.ui.simple.inverted.dimmer {
+ background: rgba(255, 255, 255, 0);
+}
+
+.dimmed.dimmable > .ui.simple.inverted.dimmer {
+ background: rgba(255, 255, 255, 0.85);
+}
+
+/*--------------
+ Partially
+ ----------------*/
+
+.ui[class*="top dimmer"],
+.ui[class*="center dimmer"],
+.ui[class*="bottom dimmer"] {
+ height: auto;
+}
+
+.ui[class*="bottom dimmer"] {
+ top: auto !important;
+ bottom: 0;
+}
+
+.ui[class*="center dimmer"] {
+ top: 50% !important;
+ transform: translateY(-50%);
+ -webkit-transform: translateY(calc(-50% - 0.5px));
+}
+
+.ui.segment > .ui.ui[class*="top dimmer"] {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.ui.segment > .ui.ui[class*="center dimmer"] {
+ border-radius: 0;
+}
+
+.ui.segment > .ui.ui[class*="bottom dimmer"] {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.ui[class*="center dimmer"].transition[class*="fade up"].in {
+ animation-name: fadeInUpCenter;
+}
+
+.ui[class*="center dimmer"].transition[class*="fade down"].in {
+ animation-name: fadeInDownCenter;
+}
+
+.ui[class*="center dimmer"].transition[class*="fade up"].out {
+ animation-name: fadeOutUpCenter;
+}
+
+.ui[class*="center dimmer"].transition[class*="fade down"].out {
+ animation-name: fadeOutDownCenter;
+}
+
+.ui[class*="center dimmer"].bounce.transition {
+ animation-name: bounceCenter;
+}
+
+@keyframes fadeInUpCenter {
+ 0% {
+ opacity: 0;
+ transform: translateY(-40%);
+ -webkit-transform: translateY(calc(-40% - 0.5px));
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(-50%);
+ -webkit-transform: translateY(calc(-50% - 0.5px));
+ }
+}
+
+@keyframes fadeInDownCenter {
+ 0% {
+ opacity: 0;
+ transform: translateY(-60%);
+ -webkit-transform: translateY(calc(-60% - 0.5px));
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(-50%);
+ -webkit-transform: translateY(calc(-50% - 0.5px));
+ }
+}
+
+@keyframes fadeOutUpCenter {
+ 0% {
+ opacity: 1;
+ transform: translateY(-50%);
+ -webkit-transform: translateY(calc(-50% - 0.5px));
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(-45%);
+ -webkit-transform: translateY(calc(-45% - 0.5px));
+ }
+}
+
+@keyframes fadeOutDownCenter {
+ 0% {
+ opacity: 1;
+ transform: translateY(-50%);
+ -webkit-transform: translateY(calc(-50% - 0.5px));
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(-55%);
+ -webkit-transform: translateY(calc(-55% - 0.5px));
+ }
+}
+
+@keyframes bounceCenter {
+ 0%, 20%, 50%, 80%, 100% {
+ transform: translateY(-50%);
+ -webkit-transform: translateY(calc(-50% - 0.5px));
+ }
+
+ 40% {
+ transform: translateY(calc(-50% - 30px));
+ }
+
+ 60% {
+ transform: translateY(calc(-50% - 15px));
+ }
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/*******************************
+ User Overrides
+*******************************/
+/*!
+ * # Fomantic-UI - Dropdown
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Dropdown
+*******************************/
+
+.ui.dropdown {
+ cursor: pointer;
+ position: relative;
+ display: inline-block;
+ outline: none;
+ text-align: left;
+ transition: box-shadow 0.1s ease, width 0.1s ease;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+/*******************************
+ Content
+*******************************/
+
+/*--------------
+ Menu
+---------------*/
+
+.ui.dropdown .menu {
+ cursor: auto;
+ position: absolute;
+ display: none;
+ outline: none;
+ top: 100%;
+ min-width: -moz-max-content;
+ min-width: max-content;
+ margin: 0;
+ padding: 0 0;
+ background: #FFFFFF;
+ font-size: 1em;
+ text-shadow: none;
+ text-align: left;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ border-radius: 0.28571429rem;
+ transition: opacity 0.1s ease;
+ z-index: 11;
+ will-change: transform, opacity;
+}
+
+.ui.dropdown .menu > * {
+ white-space: nowrap;
+}
+
+/*--------------
+ Hidden Input
+---------------*/
+
+.ui.dropdown > input:not(.search):first-child,
+.ui.dropdown > select {
+ display: none !important;
+}
+
+/*--------------
+ Dropdown Icon
+---------------*/
+
+.ui.dropdown:not(.labeled) > .dropdown.icon {
+ position: relative;
+ width: auto;
+ font-size: 0.85714286em;
+ margin: 0 0 0 1em;
+}
+
+.ui.dropdown .menu > .item .dropdown.icon {
+ width: auto;
+ float: right;
+ margin: 0em 0 0 1em;
+}
+
+.ui.dropdown .menu > .item .dropdown.icon + .text {
+ margin-right: 1em;
+}
+
+/*--------------
+ Text
+---------------*/
+
+.ui.dropdown > .text {
+ display: inline-block;
+ transition: none;
+}
+
+/*--------------
+ Menu Item
+---------------*/
+
+.ui.dropdown .menu > .item {
+ position: relative;
+ cursor: pointer;
+ display: block;
+ border: none;
+ height: auto;
+ min-height: 2.57142857rem;
+ text-align: left;
+ border-top: none;
+ line-height: 1em;
+ font-size: 1rem;
+ color: rgba(0, 0, 0, 0.87);
+ padding: 0.78571429rem 1.14285714rem !important;
+ text-transform: none;
+ font-weight: normal;
+ box-shadow: none;
+ -webkit-touch-callout: none;
+}
+
+.ui.dropdown .menu > .item:first-child {
+ border-top-width: 0;
+}
+
+.ui.dropdown .menu > .item.vertical {
+ display: flex;
+ flex-direction: column-reverse;
+}
+
+/*--------------
+ Floated Content
+---------------*/
+
+.ui.dropdown > .text > [class*="right floated"],
+.ui.dropdown .menu .item > [class*="right floated"] {
+ float: right !important;
+ margin-right: 0 !important;
+ margin-left: 1em !important;
+}
+
+.ui.dropdown > .text > [class*="left floated"],
+.ui.dropdown .menu .item > [class*="left floated"] {
+ float: left !important;
+ margin-left: 0 !important;
+ margin-right: 1em !important;
+}
+
+.ui.dropdown .menu .item > i.icon.floated,
+.ui.dropdown .menu .item > .flag.floated,
+.ui.dropdown .menu .item > .image.floated,
+.ui.dropdown .menu .item > img.floated {
+ margin-top: 0em;
+}
+
+/*--------------
+ Menu Divider
+---------------*/
+
+.ui.dropdown .menu > .header {
+ margin: 1rem 0 0.75rem;
+ padding: 0 1.14285714rem;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.ui.dropdown .menu > .header:not(.ui) {
+ color: rgba(0, 0, 0, 0.85);
+ font-size: 0.78571429em;
+}
+
+.ui.dropdown .menu > .divider {
+ border-top: 1px solid rgba(34, 36, 38, 0.1);
+ height: 0;
+ margin: 0.5em 0;
+}
+
+.ui.dropdown .menu > .horizontal.divider {
+ border-top: none;
+}
+
+.ui.dropdown.dropdown .menu > .input {
+ width: auto;
+ display: flex;
+ margin: 1.14285714rem 0.78571429rem;
+ min-width: 10rem;
+}
+
+.ui.dropdown .menu > .header + .input {
+ margin-top: 0;
+}
+
+.ui.dropdown .menu > .input:not(.transparent) input {
+ padding: 0.5em 1em;
+}
+
+.ui.dropdown .menu > .input:not(.transparent) .button,
+.ui.dropdown .menu > .input:not(.transparent) i.icon,
+.ui.dropdown .menu > .input:not(.transparent) .label {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+/*-----------------
+ Item Description
+-------------------*/
+
+.ui.dropdown > .text > .description,
+.ui.dropdown .menu > .item > .description {
+ float: right;
+ margin: 0 0 0 1em;
+ color: rgba(0, 0, 0, 0.4);
+}
+
+.ui.dropdown .menu > .item.vertical > .description {
+ margin: 0;
+}
+
+/*-----------------
+ Item Text
+-------------------*/
+
+.ui.dropdown .menu > .item.vertical > .text {
+ margin-bottom: 0.25em;
+}
+
+/*-----------------
+ Message
+-------------------*/
+
+.ui.dropdown .menu > .message {
+ padding: 0.78571429rem 1.14285714rem;
+ font-weight: normal;
+}
+
+.ui.dropdown .menu > .message:not(.ui) {
+ color: rgba(0, 0, 0, 0.4);
+}
+
+/*--------------
+ Sub Menu
+---------------*/
+
+.ui.dropdown .menu .menu {
+ top: 0;
+ left: 100%;
+ right: auto;
+ margin: 0 -0.5em !important;
+ border-radius: 0.28571429rem !important;
+ z-index: 21 !important;
+}
+
+/* Hide Arrow */
+
+.ui.dropdown .menu .menu:after {
+ display: none;
+}
+
+/*--------------
+ Sub Elements
+---------------*/
+
+/* Icons / Flags / Labels / Image */
+
+.ui.dropdown > .text > i.icon,
+.ui.dropdown > .text > .label,
+.ui.dropdown > .text > .flag,
+.ui.dropdown > .text > img,
+.ui.dropdown > .text > .image {
+ margin-top: 0em;
+}
+
+.ui.dropdown .menu > .item > i.icon,
+.ui.dropdown .menu > .item > .label,
+.ui.dropdown .menu > .item > .flag,
+.ui.dropdown .menu > .item > .image,
+.ui.dropdown .menu > .item > img {
+ margin-top: 0em;
+}
+
+.ui.dropdown > .text > i.icon,
+.ui.dropdown > .text > .label,
+.ui.dropdown > .text > .flag,
+.ui.dropdown > .text > img,
+.ui.dropdown > .text > .image,
+.ui.dropdown .menu > .item > i.icon,
+.ui.dropdown .menu > .item > .label,
+.ui.dropdown .menu > .item > .flag,
+.ui.dropdown .menu > .item > .image,
+.ui.dropdown .menu > .item > img {
+ margin-left: 0;
+ float: none;
+ margin-right: 0.78571429rem;
+}
+
+/*--------------
+ Image
+---------------*/
+
+.ui.dropdown > .text > img,
+.ui.dropdown > .text > .image:not(.icon),
+.ui.dropdown .menu > .item > .image:not(.icon),
+.ui.dropdown .menu > .item > img {
+ display: inline-block;
+ vertical-align: top;
+ width: auto;
+ margin-top: -0.5em;
+ margin-bottom: -0.5em;
+ max-height: 2em;
+}
+
+/*******************************
+ Coupling
+*******************************/
+
+/*--------------
+ Menu
+---------------*/
+
+/* Remove Menu Item Divider */
+
+.ui.dropdown .ui.menu > .item:before,
+.ui.menu .ui.dropdown .menu > .item:before {
+ display: none;
+}
+
+/* Prevent Menu Item Border */
+
+.ui.menu .ui.dropdown .menu .active.item {
+ border-left: none;
+}
+
+/* Automatically float dropdown menu right on last menu item */
+
+.ui.menu .right.menu .dropdown:last-child > .menu:not(.left),
+.ui.menu .right.dropdown.item > .menu:not(.left),
+.ui.buttons > .ui.dropdown:last-child > .menu:not(.left) {
+ left: auto;
+ right: 0;
+}
+
+/*--------------
+ Label
+ ---------------*/
+
+/* Dropdown Menu */
+
+.ui.label.dropdown .menu {
+ min-width: 100%;
+}
+
+/*--------------
+ Button
+ ---------------*/
+
+/* No Margin On Icon Button */
+
+.ui.dropdown.icon.button > .dropdown.icon {
+ margin: 0;
+}
+
+.ui.button.dropdown .menu {
+ min-width: 100%;
+}
+
+/*******************************
+ Types
+*******************************/
+
+select.ui.dropdown {
+ height: 38px;
+ padding: 0.5em;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ visibility: visible;
+}
+
+/*--------------
+ Selection
+ ---------------*/
+
+/* Displays like a select box */
+
+.ui.selection.dropdown {
+ cursor: pointer;
+ word-wrap: break-word;
+ line-height: 1em;
+ white-space: normal;
+ outline: 0;
+ transform: rotateZ(0deg);
+ min-width: 14em;
+ min-height: 2.71428571em;
+ background: #FFFFFF;
+ display: inline-block;
+ padding: 0.78571429em 3.2em 0.78571429em 1em;
+ color: rgba(0, 0, 0, 0.87);
+ box-shadow: none;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ border-radius: 0.28571429rem;
+ transition: box-shadow 0.1s ease, width 0.1s ease;
+}
+
+.ui.selection.dropdown.visible,
+.ui.selection.dropdown.active {
+ z-index: 10;
+}
+
+.ui.selection.dropdown > .search.icon,
+.ui.selection.dropdown > .delete.icon,
+.ui.selection.dropdown > .dropdown.icon {
+ cursor: pointer;
+ position: absolute;
+ width: auto;
+ height: auto;
+ line-height: 1.21428571em;
+ top: 0.78571429em;
+ right: 1em;
+ z-index: 3;
+ margin: -0.78571429em;
+ padding: 0.91666667em;
+ opacity: 0.8;
+ transition: opacity 0.1s ease;
+}
+
+/* Compact */
+
+.ui.compact.selection.dropdown {
+ min-width: 0;
+}
+
+/* Selection Menu */
+
+.ui.selection.dropdown .menu {
+ overflow-x: hidden;
+ overflow-y: auto;
+ backface-visibility: hidden;
+ -webkit-overflow-scrolling: touch;
+ border-top-width: 0 !important;
+ width: auto;
+ outline: none;
+ margin: 0 -1px;
+ min-width: calc(100% + 2px);
+ width: calc(100% + 2px);
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+ transition: opacity 0.1s ease;
+}
+
+.ui.selection.dropdown .menu:after,
+.ui.selection.dropdown .menu:before {
+ display: none;
+}
+
+/*--------------
+ Message
+ ---------------*/
+
+.ui.selection.dropdown .menu > .message {
+ padding: 0.78571429rem 1.14285714rem;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.selection.dropdown.short .menu {
+ max-height: 6.01071429rem;
+ }
+
+ .ui.selection.dropdown[class*="very short"] .menu {
+ max-height: 4.00714286rem;
+ }
+
+ .ui.selection.dropdown .menu {
+ max-height: 8.01428571rem;
+ }
+
+ .ui.selection.dropdown.long .menu {
+ max-height: 16.02857143rem;
+ }
+
+ .ui.selection.dropdown[class*="very long"] .menu {
+ max-height: 24.04285714rem;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.selection.dropdown.short .menu {
+ max-height: 8.01428571rem;
+ }
+
+ .ui.selection.dropdown[class*="very short"] .menu {
+ max-height: 5.34285714rem;
+ }
+
+ .ui.selection.dropdown .menu {
+ max-height: 10.68571429rem;
+ }
+
+ .ui.selection.dropdown.long .menu {
+ max-height: 21.37142857rem;
+ }
+
+ .ui.selection.dropdown[class*="very long"] .menu {
+ max-height: 32.05714286rem;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.selection.dropdown.short .menu {
+ max-height: 12.02142857rem;
+ }
+
+ .ui.selection.dropdown[class*="very short"] .menu {
+ max-height: 8.01428571rem;
+ }
+
+ .ui.selection.dropdown .menu {
+ max-height: 16.02857143rem;
+ }
+
+ .ui.selection.dropdown.long .menu {
+ max-height: 32.05714286rem;
+ }
+
+ .ui.selection.dropdown[class*="very long"] .menu {
+ max-height: 48.08571429rem;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.selection.dropdown.short .menu {
+ max-height: 16.02857143rem;
+ }
+
+ .ui.selection.dropdown[class*="very short"] .menu {
+ max-height: 10.68571429rem;
+ }
+
+ .ui.selection.dropdown .menu {
+ max-height: 21.37142857rem;
+ }
+
+ .ui.selection.dropdown.long .menu {
+ max-height: 42.74285714rem;
+ }
+
+ .ui.selection.dropdown[class*="very long"] .menu {
+ max-height: 64.11428571rem;
+ }
+}
+
+/* Menu Item */
+
+.ui.selection.dropdown .menu > .item {
+ border-top: 1px solid #FAFAFA;
+ padding: 0.78571429rem 1.14285714rem !important;
+ white-space: normal;
+ word-wrap: normal;
+}
+
+/* User Item */
+
+.ui.selection.dropdown .menu > .hidden.addition.item {
+ display: none;
+}
+
+/* Hover */
+
+.ui.selection.dropdown:hover {
+ border-color: rgba(34, 36, 38, 0.35);
+ box-shadow: none;
+}
+
+/* Active */
+
+.ui.selection.active.dropdown {
+ border-color: #96C8DA;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+}
+
+.ui.selection.active.dropdown .menu {
+ border-color: #96C8DA;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+}
+
+/* Focus */
+
+.ui.selection.dropdown:focus {
+ border-color: #96C8DA;
+ box-shadow: none;
+}
+
+.ui.selection.dropdown:focus .menu {
+ border-color: #96C8DA;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+}
+
+/* Visible */
+
+.ui.selection.visible.dropdown > .text:not(.default) {
+ font-weight: normal;
+ color: rgba(0, 0, 0, 0.8);
+}
+
+/* Visible Hover */
+
+.ui.selection.active.dropdown:hover {
+ border-color: #96C8DA;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+}
+
+.ui.selection.active.dropdown:hover .menu {
+ border-color: #96C8DA;
+ box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15);
+}
+
+/* Dropdown Icon */
+
+.ui.active.selection.dropdown > .dropdown.icon,
+.ui.visible.selection.dropdown > .dropdown.icon {
+ opacity: '';
+ z-index: 3;
+}
+
+/* Connecting Border */
+
+.ui.active.selection.dropdown {
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+/* Empty Connecting Border */
+
+.ui.active.empty.selection.dropdown {
+ border-radius: 0.28571429rem !important;
+ box-shadow: none !important;
+}
+
+.ui.active.empty.selection.dropdown .menu {
+ border: none !important;
+ box-shadow: none !important;
+}
+
+/* CSS specific to iOS devices or firefox mobile only */
+
+@supports (-webkit-touch-callout: none) or (-webkit-overflow-scrolling: touch) or (-moz-appearance:none) {
+@media (-moz-touch-enabled), (pointer: coarse) {
+ .ui.dropdown .scrollhint.menu:not(.hidden):before {
+ animation: scrollhint 2s ease 2;
+ content: '';
+ z-index: 15;
+ display: block;
+ position: absolute;
+ opacity: 0;
+ right: 0.25em;
+ top: 0;
+ height: 100%;
+ border-right: 0.25em solid;
+ border-left: 0;
+ -o-border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)) 1 100%;
+ border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)) 1 100%;
+ }
+
+ .ui.inverted.dropdown .scrollhint.menu:not(.hidden):before {
+ -o-border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%;
+ border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%;
+ }
+
+@keyframes scrollhint {
+ 0% {
+ opacity: 1;
+ top: 100%;
+ }
+
+ 100% {
+ opacity: 0;
+ top: 0;
+ }
+}
+}
+}
+
+/*--------------
+ Searchable
+ ---------------*/
+
+/* Search Selection */
+
+.ui.search.dropdown {
+ min-width: '';
+}
+
+/* Search Dropdown */
+
+.ui.search.dropdown > input.search {
+ background: none transparent !important;
+ border: none !important;
+ box-shadow: none !important;
+ cursor: text;
+ top: 0;
+ left: 1px;
+ width: 100%;
+ outline: none;
+ -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+ padding: inherit;
+}
+
+/* Text Layering */
+
+.ui.search.dropdown > input.search {
+ position: absolute;
+ z-index: 2;
+}
+
+.ui.search.dropdown > .text {
+ cursor: text;
+ position: relative;
+ left: 1px;
+ z-index: auto;
+}
+
+/* Search Selection */
+
+.ui.search.selection.dropdown > input.search {
+ line-height: 1.21428571em;
+ padding: 0.67857143em 3.2em 0.67857143em 1em;
+}
+
+/* Used to size multi select input to character width */
+
+.ui.search.selection.dropdown > span.sizer {
+ line-height: 1.21428571em;
+ padding: 0.67857143em 3.2em 0.67857143em 1em;
+ display: none;
+ white-space: pre;
+}
+
+/* Active/Visible Search */
+
+.ui.search.dropdown.active > input.search,
+.ui.search.dropdown.visible > input.search {
+ cursor: auto;
+}
+
+.ui.search.dropdown.active > .text,
+.ui.search.dropdown.visible > .text {
+ pointer-events: none;
+}
+
+/* Filtered Text */
+
+.ui.active.search.dropdown input.search:focus + .text i.icon,
+.ui.active.search.dropdown input.search:focus + .text .flag {
+ opacity: var(--opacity-disabled);
+}
+
+.ui.active.search.dropdown input.search:focus + .text {
+ color: rgba(115, 115, 115, 0.87) !important;
+}
+
+.ui.search.dropdown.button > span.sizer {
+ display: none;
+}
+
+/* Search Menu */
+
+.ui.search.dropdown .menu {
+ overflow-x: hidden;
+ overflow-y: auto;
+ backface-visibility: hidden;
+ -webkit-overflow-scrolling: touch;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.search.dropdown .menu {
+ max-height: 8.01428571rem;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.search.dropdown .menu {
+ max-height: 10.68571429rem;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.search.dropdown .menu {
+ max-height: 16.02857143rem;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.search.dropdown .menu {
+ max-height: 21.37142857rem;
+ }
+}
+
+/* Clearable Selection */
+
+.ui.dropdown > .remove.icon {
+ cursor: pointer;
+ font-size: 0.85714286em;
+ margin: -0.78571429em;
+ padding: 0.91666667em;
+ right: 3em;
+ top: 0.78571429em;
+ position: absolute;
+ opacity: 0.6;
+ z-index: 3;
+}
+
+.ui.clearable.dropdown .text,
+.ui.clearable.dropdown a:last-of-type {
+ margin-right: 1.5em;
+}
+
+.ui.dropdown select.noselection ~ .remove.icon,
+.ui.dropdown input[value=''] ~ .remove.icon,
+.ui.dropdown input:not([value]) ~ .remove.icon,
+.ui.dropdown.loading > .remove.icon {
+ display: none;
+}
+
+/*--------------
+ Multiple
+ ---------------*/
+
+/* Multiple Selection */
+
+.ui.ui.multiple.dropdown {
+ padding: 0.22619048em 3.2em 0.22619048em 0.35714286em;
+}
+
+.ui.multiple.dropdown .menu {
+ cursor: auto;
+}
+
+/* Selection Label */
+
+.ui.multiple.dropdown > .label {
+ display: inline-block;
+ white-space: normal;
+ font-size: 1em;
+ padding: 0.35714286em 0.78571429em;
+ margin: 0.14285714rem 0.28571429rem 0.14285714rem 0;
+ box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset;
+}
+
+/* Dropdown Icon */
+
+.ui.multiple.dropdown .dropdown.icon {
+ margin: '';
+ padding: '';
+}
+
+/* Text */
+
+.ui.multiple.dropdown > .text {
+ position: static;
+ padding: 0;
+ max-width: 100%;
+ margin: 0.45238095em 0 0.45238095em 0.64285714em;
+ line-height: 1.21428571em;
+}
+
+.ui.multiple.dropdown > .text.default {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ui.multiple.dropdown > .label ~ input.search {
+ margin-left: 0.14285714em !important;
+}
+
+.ui.multiple.dropdown > .label ~ .text {
+ display: none;
+}
+
+.ui.multiple.dropdown > .label:not(.image) > img:not(.centered) {
+ margin-right: 0.78571429rem;
+}
+
+.ui.multiple.dropdown > .label:not(.image) > img.ui:not(.avatar) {
+ margin-bottom: 0.39285714rem;
+}
+
+.ui.multiple.dropdown > .image.label img {
+ margin: -0.35714286em 0.78571429em -0.35714286em -0.78571429em;
+ height: 1.71428571em;
+}
+
+/*-----------------
+ Multiple Search
+ -----------------*/
+
+/* Multiple Search Selection */
+
+.ui.multiple.search.dropdown,
+.ui.multiple.search.dropdown > input.search {
+ cursor: text;
+}
+
+/* Prompt Text */
+
+.ui.multiple.search.dropdown > .text {
+ display: inline-block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: inherit;
+ margin: 0.45238095em 0 0.45238095em 0.64285714em;
+ line-height: 1.21428571em;
+}
+
+.ui.multiple.search.dropdown > .label ~ .text {
+ display: none;
+}
+
+/* Search */
+
+.ui.multiple.search.dropdown > input.search {
+ position: static;
+ padding: 0;
+ max-width: 100%;
+ margin: 0.45238095em 0 0.45238095em 0.64285714em;
+ width: 2.2em;
+ line-height: 1.21428571em;
+}
+
+.ui.multiple.search.dropdown.button {
+ min-width: 14em;
+}
+
+/*--------------
+ Inline
+ ---------------*/
+
+.ui.inline.dropdown {
+ cursor: pointer;
+ display: inline-block;
+ color: inherit;
+}
+
+.ui.inline.dropdown .dropdown.icon {
+ margin: 0 0.21428571em 0 0.21428571em;
+ vertical-align: baseline;
+}
+
+.ui.inline.dropdown > .text {
+ font-weight: 500;
+}
+
+.ui.inline.dropdown .menu {
+ cursor: auto;
+ margin-top: 0.21428571em;
+ border-radius: 0.28571429rem;
+}
+
+/*******************************
+ States
+*******************************/
+
+/*--------------------
+ Active
+----------------------*/
+
+/* Menu Item Active */
+
+.ui.dropdown .menu .active.item {
+ background: transparent;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.95);
+ box-shadow: none;
+ z-index: 12;
+}
+
+/*--------------------
+ Hover
+----------------------*/
+
+/* Menu Item Hover */
+
+.ui.dropdown .menu > .item:hover {
+ background: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+ z-index: 13;
+}
+
+/*--------------------
+ Default Text
+----------------------*/
+
+.ui.dropdown:not(.button) > .default.text,
+.ui.default.dropdown:not(.button) > .text {
+ color: rgba(191, 191, 191, 0.87);
+}
+
+.ui.dropdown:not(.button) > input:focus ~ .default.text,
+.ui.default.dropdown:not(.button) > input:focus ~ .text {
+ color: rgba(115, 115, 115, 0.87);
+}
+
+/*--------------------
+ Loading
+ ---------------------*/
+
+.ui.loading.dropdown > i.icon {
+ height: 1em !important;
+}
+
+.ui.loading.selection.dropdown > i.icon {
+ padding: 1.5em 1.28571429em !important;
+}
+
+.ui.loading.dropdown > i.icon:before {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -0.64285714em 0 0 -0.64285714em;
+ width: 1.28571429em;
+ height: 1.28571429em;
+ border-radius: 500rem;
+ border: 0.2em solid rgba(0, 0, 0, 0.1);
+}
+
+.ui.loading.dropdown > i.icon:after {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ box-shadow: 0 0 0 1px transparent;
+ margin: -0.64285714em 0 0 -0.64285714em;
+ width: 1.28571429em;
+ height: 1.28571429em;
+ animation: loader 0.6s infinite linear;
+ border: 0.2em solid #767676;
+ border-radius: 500rem;
+}
+
+/* Coupling */
+
+.ui.loading.dropdown.button > i.icon:before,
+.ui.loading.dropdown.button > i.icon:after {
+ display: none;
+}
+
+.ui.loading.dropdown > .text {
+ transition: none;
+}
+
+/* Used To Check Position */
+
+.ui.dropdown .loading.menu {
+ display: block;
+ visibility: hidden;
+ z-index: -1;
+}
+
+.ui.dropdown > .loading.menu {
+ left: 0 !important;
+ right: auto !important;
+}
+
+.ui.dropdown > .menu .loading.menu {
+ left: 100% !important;
+ right: auto !important;
+}
+
+/*--------------------
+ Keyboard Select
+----------------------*/
+
+/* Selected Item */
+
+.ui.dropdown.selected,
+.ui.dropdown .menu .selected.item {
+ background: rgba(0, 0, 0, 0.03);
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--------------------
+ Search Filtered
+----------------------*/
+
+/* Filtered Item */
+
+.ui.dropdown > .filtered.text {
+ visibility: hidden;
+}
+
+.ui.dropdown .filtered.item {
+ display: none !important;
+}
+
+/*--------------------
+ States
+ ----------------------*/
+
+.ui.dropdown.error,
+.ui.dropdown.error > .text,
+.ui.dropdown.error > .default.text {
+ color: #9F3A38;
+}
+
+.ui.selection.dropdown.error {
+ background: #FFF6F6;
+ border-color: #E0B4B4;
+}
+
+.ui.selection.dropdown.error:hover {
+ border-color: #E0B4B4;
+}
+
+.ui.multiple.selection.error.dropdown > .label {
+ border-color: #E0B4B4;
+}
+
+.ui.dropdown.error > .menu,
+.ui.dropdown.error > .menu .menu {
+ border-color: #E0B4B4;
+}
+
+.ui.dropdown.error > .menu > .item {
+ color: #9F3A38;
+}
+
+/* Item Hover */
+
+.ui.dropdown.error > .menu > .item:hover {
+ background-color: #FBE7E7;
+}
+
+/* Item Active */
+
+.ui.dropdown.error > .menu .active.item {
+ background-color: #FDCFCF;
+}
+
+.ui.dropdown.info,
+.ui.dropdown.info > .text,
+.ui.dropdown.info > .default.text {
+ color: #276F86;
+}
+
+.ui.selection.dropdown.info {
+ background: #F8FFFF;
+ border-color: #A9D5DE;
+}
+
+.ui.selection.dropdown.info:hover {
+ border-color: #A9D5DE;
+}
+
+.ui.multiple.selection.info.dropdown > .label {
+ border-color: #A9D5DE;
+}
+
+.ui.dropdown.info > .menu,
+.ui.dropdown.info > .menu .menu {
+ border-color: #A9D5DE;
+}
+
+.ui.dropdown.info > .menu > .item {
+ color: #276F86;
+}
+
+/* Item Hover */
+
+.ui.dropdown.info > .menu > .item:hover {
+ background-color: #e9f2fb;
+}
+
+/* Item Active */
+
+.ui.dropdown.info > .menu .active.item {
+ background-color: #cef1fd;
+}
+
+.ui.dropdown.success,
+.ui.dropdown.success > .text,
+.ui.dropdown.success > .default.text {
+ color: #2C662D;
+}
+
+.ui.selection.dropdown.success {
+ background: #FCFFF5;
+ border-color: #A3C293;
+}
+
+.ui.selection.dropdown.success:hover {
+ border-color: #A3C293;
+}
+
+.ui.multiple.selection.success.dropdown > .label {
+ border-color: #A3C293;
+}
+
+.ui.dropdown.success > .menu,
+.ui.dropdown.success > .menu .menu {
+ border-color: #A3C293;
+}
+
+.ui.dropdown.success > .menu > .item {
+ color: #2C662D;
+}
+
+/* Item Hover */
+
+.ui.dropdown.success > .menu > .item:hover {
+ background-color: #e9fbe9;
+}
+
+/* Item Active */
+
+.ui.dropdown.success > .menu .active.item {
+ background-color: #dafdce;
+}
+
+.ui.dropdown.warning,
+.ui.dropdown.warning > .text,
+.ui.dropdown.warning > .default.text {
+ color: #573A08;
+}
+
+.ui.selection.dropdown.warning {
+ background: #FFFAF3;
+ border-color: #C9BA9B;
+}
+
+.ui.selection.dropdown.warning:hover {
+ border-color: #C9BA9B;
+}
+
+.ui.multiple.selection.warning.dropdown > .label {
+ border-color: #C9BA9B;
+}
+
+.ui.dropdown.warning > .menu,
+.ui.dropdown.warning > .menu .menu {
+ border-color: #C9BA9B;
+}
+
+.ui.dropdown.warning > .menu > .item {
+ color: #573A08;
+}
+
+/* Item Hover */
+
+.ui.dropdown.warning > .menu > .item:hover {
+ background-color: #fbfbe9;
+}
+
+/* Item Active */
+
+.ui.dropdown.warning > .menu .active.item {
+ background-color: #fdfdce;
+}
+
+/*--------------------
+ Clear
+----------------------*/
+
+.ui.dropdown > .clear.dropdown.icon {
+ opacity: 0.8;
+ transition: opacity 0.1s ease;
+}
+
+.ui.dropdown > .clear.dropdown.icon:hover {
+ opacity: 1;
+}
+
+/*--------------------
+ Disabled
+ ----------------------*/
+
+/* Disabled */
+
+.ui.disabled.dropdown,
+.ui.dropdown .menu > .disabled.item {
+ cursor: default;
+ pointer-events: none;
+ opacity: var(--opacity-disabled);
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*--------------
+ Direction
+---------------*/
+
+/* Flyout Direction */
+
+.ui.dropdown .menu {
+ left: 0;
+}
+
+/* Default Side (Right) */
+
+.ui.dropdown .right.menu > .menu,
+.ui.dropdown .menu .right.menu {
+ left: 100% !important;
+ right: auto !important;
+ border-radius: 0.28571429rem !important;
+}
+
+/* Leftward Opening Menu */
+
+.ui.dropdown > .left.menu {
+ left: auto !important;
+ right: 0 !important;
+}
+
+.ui.dropdown > .left.menu .menu,
+.ui.dropdown .menu .left.menu {
+ left: auto;
+ right: 100%;
+ margin: 0 -0.5em 0 0 !important;
+ border-radius: 0.28571429rem !important;
+}
+
+.ui.dropdown .item .left.dropdown.icon,
+.ui.dropdown .left.menu .item .dropdown.icon {
+ width: auto;
+ float: left;
+ margin: 0em 0 0 0;
+}
+
+.ui.dropdown .item .left.dropdown.icon,
+.ui.dropdown .left.menu .item .dropdown.icon {
+ width: auto;
+ float: left;
+ margin: 0em 0 0 0;
+}
+
+.ui.dropdown .item .left.dropdown.icon + .text,
+.ui.dropdown .left.menu .item .dropdown.icon + .text {
+ margin-left: 1em;
+ margin-right: 0;
+}
+
+/*--------------
+ Upward
+ ---------------*/
+
+/* Upward Main Menu */
+
+.ui.upward.dropdown > .menu {
+ top: auto;
+ bottom: 100%;
+ box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08);
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+/* Upward Sub Menu */
+
+.ui.dropdown .upward.menu {
+ top: auto !important;
+ bottom: 0 !important;
+}
+
+/* Active Upward */
+
+.ui.simple.upward.active.dropdown,
+.ui.simple.upward.dropdown:hover {
+ border-radius: 0.28571429rem 0.28571429rem 0 0 !important;
+}
+
+.ui.upward.dropdown.button:not(.pointing):not(.floating).active {
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+/* Selection */
+
+.ui.upward.selection.dropdown .menu {
+ border-top-width: 1px !important;
+ border-bottom-width: 0 !important;
+ box-shadow: 0 -2px 3px 0 rgba(0, 0, 0, 0.08);
+}
+
+.ui.upward.selection.dropdown:hover {
+ box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+/* Active Upward */
+
+.ui.active.upward.selection.dropdown {
+ border-radius: 0 0 0.28571429rem 0.28571429rem !important;
+}
+
+/* Visible Upward */
+
+.ui.upward.selection.dropdown.visible {
+ box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08);
+ border-radius: 0 0 0.28571429rem 0.28571429rem !important;
+}
+
+/* Visible Hover Upward */
+
+.ui.upward.active.selection.dropdown:hover {
+ box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.05);
+}
+
+.ui.upward.active.selection.dropdown:hover .menu {
+ box-shadow: 0 -2px 3px 0 rgba(0, 0, 0, 0.08);
+}
+
+/*--------------
+ Scrolling
+ ---------------*/
+
+/* Selection Menu */
+
+.ui.scrolling.dropdown .menu,
+.ui.dropdown .scrolling.menu {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.ui.scrolling.dropdown .menu {
+ overflow-x: hidden;
+ overflow-y: auto;
+ backface-visibility: hidden;
+ -webkit-overflow-scrolling: touch;
+ min-width: 100% !important;
+ width: auto !important;
+}
+
+.ui.dropdown .scrolling.menu {
+ position: static;
+ overflow-y: auto;
+ border: none;
+ box-shadow: none !important;
+ border-radius: 0 !important;
+ margin: 0 !important;
+ min-width: 100% !important;
+ width: auto !important;
+ border-top: 1px solid rgba(34, 36, 38, 0.15);
+}
+
+.ui.scrolling.dropdown .menu .item.item.item,
+.ui.dropdown .scrolling.menu > .item.item.item {
+ border-top: none;
+}
+
+.ui.scrolling.dropdown .menu .item:first-child,
+.ui.dropdown .scrolling.menu .item:first-child {
+ border-top: none;
+}
+
+.ui.dropdown > .animating.menu .scrolling.menu,
+.ui.dropdown > .visible.menu .scrolling.menu {
+ display: block;
+}
+
+/* Scrollbar in IE */
+
+@media all and (-ms-high-contrast: none) {
+ .ui.scrolling.dropdown .menu,
+ .ui.dropdown .scrolling.menu {
+ min-width: calc(100% - 17px);
+ }
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.scrolling.dropdown .menu,
+ .ui.dropdown .scrolling.menu {
+ max-height: 10.28571429rem;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.scrolling.dropdown .menu,
+ .ui.dropdown .scrolling.menu {
+ max-height: 15.42857143rem;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.scrolling.dropdown .menu,
+ .ui.dropdown .scrolling.menu {
+ max-height: 20.57142857rem;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.scrolling.dropdown .menu,
+ .ui.dropdown .scrolling.menu {
+ max-height: 20.57142857rem;
+ }
+}
+
+/*--------------
+ Columnar
+---------------*/
+
+.ui.column.dropdown > .menu {
+ flex-wrap: wrap;
+}
+
+.ui.dropdown[class*="two column"] > .menu > .item {
+ width: 50%;
+}
+
+.ui.dropdown[class*="three column"] > .menu > .item {
+ width: 33%;
+}
+
+.ui.dropdown[class*="four column"] > .menu > .item {
+ width: 25%;
+}
+
+.ui.dropdown[class*="five column"] > .menu > .item {
+ width: 20%;
+}
+
+/*--------------
+ Simple
+ ---------------*/
+
+/* Displays without javascript */
+
+.ui.simple.dropdown .menu:before,
+.ui.simple.dropdown .menu:after {
+ display: none;
+}
+
+.ui.simple.dropdown .menu {
+ position: absolute;
+ /* IE hack to make dropdown icons appear inline */
+ display: -ms-inline-flexbox !important;
+ display: block;
+ overflow: hidden;
+ top: -9999px;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ transition: opacity 0.1s ease;
+ margin-top: 0 !important;
+}
+
+.ui.simple.active.dropdown,
+.ui.simple.dropdown:hover {
+ border-bottom-left-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+.ui.simple.active.dropdown > .menu,
+.ui.simple.dropdown:hover > .menu {
+ overflow: visible;
+ width: auto;
+ height: auto;
+ top: 100%;
+ opacity: 1;
+}
+
+.ui.simple.dropdown > .menu > .item:active > .menu,
+.ui.simple.dropdown .menu .item:hover > .menu {
+ overflow: visible;
+ width: auto;
+ height: auto;
+ top: 0 !important;
+ left: 100%;
+ opacity: 1;
+}
+
+.ui.simple.dropdown > .menu > .item:active > .left.menu,
+.ui.simple.dropdown .menu .item:hover > .left.menu,
+.right.menu .ui.simple.dropdown > .menu > .item:active > .menu:not(.right),
+.right.menu .ui.simple.dropdown > .menu .item:hover > .menu:not(.right) {
+ left: auto;
+ right: 100%;
+}
+
+.ui.simple.disabled.dropdown:hover .menu {
+ display: none;
+ height: 0;
+ width: 0;
+ overflow: hidden;
+}
+
+/* Visible */
+
+.ui.simple.visible.dropdown > .menu {
+ display: block;
+}
+
+/* Scrolling */
+
+.ui.simple.scrolling.active.dropdown > .menu,
+.ui.simple.scrolling.dropdown:hover > .menu {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+/*--------------
+ Fluid
+ ---------------*/
+
+.ui.fluid.dropdown {
+ display: block;
+ width: 100% !important;
+ min-width: 0;
+}
+
+.ui.fluid.dropdown > .dropdown.icon {
+ float: right;
+}
+
+/*--------------
+ Floating
+ ---------------*/
+
+.ui.floating.dropdown .menu {
+ left: 0;
+ right: auto;
+ box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important;
+ border-radius: 0.28571429rem !important;
+}
+
+.ui.floating.dropdown > .menu {
+ border-radius: 0.28571429rem !important;
+}
+
+.ui:not(.upward).floating.dropdown > .menu {
+ margin-top: 0.5em;
+}
+
+.ui.upward.floating.dropdown > .menu {
+ margin-bottom: 0.5em;
+}
+
+/*--------------
+ Pointing
+ ---------------*/
+
+.ui.pointing.dropdown > .menu {
+ top: 100%;
+ margin-top: 0.78571429rem;
+ border-radius: 0.28571429rem;
+}
+
+.ui.pointing.dropdown > .menu:not(.hidden):after {
+ display: block;
+ position: absolute;
+ pointer-events: none;
+ content: '';
+ visibility: visible;
+ transform: rotate(45deg);
+ width: 0.5em;
+ height: 0.5em;
+ box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15);
+ background: #FFFFFF;
+ z-index: 2;
+}
+
+.ui.pointing.dropdown > .menu:not(.hidden):after {
+ top: -0.25em;
+ left: 50%;
+ margin: 0 0 0 -0.25em;
+}
+
+/* Top Left Pointing */
+
+.ui.top.left.pointing.dropdown > .menu {
+ top: 100%;
+ bottom: auto;
+ left: 0;
+ right: auto;
+ margin: 1em 0 0;
+}
+
+.ui.top.left.pointing.dropdown > .menu {
+ top: 100%;
+ bottom: auto;
+ left: 0;
+ right: auto;
+ margin: 1em 0 0;
+}
+
+.ui.top.left.pointing.dropdown > .menu:after {
+ top: -0.25em;
+ left: 1em;
+ right: auto;
+ margin: 0;
+ transform: rotate(45deg);
+}
+
+/* Top Right Pointing */
+
+.ui.top.right.pointing.dropdown > .menu {
+ top: 100%;
+ bottom: auto;
+ right: 0;
+ left: auto;
+ margin: 1em 0 0;
+}
+
+.ui.top.pointing.dropdown > .left.menu:after,
+.ui.top.right.pointing.dropdown > .menu:after {
+ top: -0.25em;
+ left: auto !important;
+ right: 1em !important;
+ margin: 0;
+ transform: rotate(45deg);
+}
+
+/* Left Pointing */
+
+.ui.left.pointing.dropdown > .menu {
+ top: 0;
+ left: 100%;
+ right: auto;
+ margin: 0 0 0 1em;
+}
+
+.ui.left.pointing.dropdown > .menu:after {
+ top: 1em;
+ left: -0.25em;
+ margin: 0 0 0 0;
+ transform: rotate(-45deg);
+}
+
+.ui.left:not(.top):not(.bottom).pointing.dropdown > .left.menu {
+ left: auto !important;
+ right: 100% !important;
+ margin: 0 1em 0 0;
+}
+
+.ui.left:not(.top):not(.bottom).pointing.dropdown > .left.menu:after {
+ top: 1em;
+ left: auto;
+ right: -0.25em;
+ margin: 0 0 0 0;
+ transform: rotate(135deg);
+}
+
+/* Right Pointing */
+
+.ui.right.pointing.dropdown > .menu {
+ top: 0;
+ left: auto;
+ right: 100%;
+ margin: 0 1em 0 0;
+}
+
+.ui.right.pointing.dropdown > .menu:after {
+ top: 1em;
+ left: auto;
+ right: -0.25em;
+ margin: 0 0 0 0;
+ transform: rotate(135deg);
+}
+
+/* Bottom Pointing */
+
+.ui.bottom.pointing.dropdown > .menu {
+ top: auto;
+ bottom: 100%;
+ left: 0;
+ right: auto;
+ margin: 0 0 1em;
+}
+
+.ui.bottom.pointing.dropdown > .menu:after {
+ top: auto;
+ bottom: -0.25em;
+ right: auto;
+ margin: 0;
+ transform: rotate(-135deg);
+}
+
+/* Reverse Sub-Menu Direction */
+
+.ui.bottom.pointing.dropdown > .menu .menu {
+ top: auto !important;
+ bottom: 0 !important;
+}
+
+/* Bottom Left */
+
+.ui.bottom.left.pointing.dropdown > .menu {
+ left: 0;
+ right: auto;
+}
+
+.ui.bottom.left.pointing.dropdown > .menu:after {
+ left: 1em;
+ right: auto;
+}
+
+/* Bottom Right */
+
+.ui.bottom.right.pointing.dropdown > .menu {
+ right: 0;
+ left: auto;
+}
+
+.ui.bottom.right.pointing.dropdown > .menu:after {
+ left: auto;
+ right: 1em;
+}
+
+/* Upward pointing */
+
+.ui.pointing.upward.dropdown .menu,
+.ui.top.pointing.upward.dropdown .menu {
+ top: auto !important;
+ bottom: 100% !important;
+ margin: 0 0 0.78571429rem;
+ border-radius: 0.28571429rem;
+}
+
+.ui.pointing.upward.dropdown .menu:after,
+.ui.top.pointing.upward.dropdown .menu:after {
+ top: 100% !important;
+ bottom: auto !important;
+ box-shadow: 1px 1px 0 0 rgba(34, 36, 38, 0.15);
+ margin: -0.25em 0 0;
+}
+
+/* Right Pointing Upward */
+
+.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu {
+ top: auto !important;
+ bottom: 0 !important;
+ margin: 0 1em 0 0;
+}
+
+.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after {
+ top: auto !important;
+ bottom: 0 !important;
+ margin: 0 0 1em 0;
+ box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15);
+}
+
+/* Left Pointing Upward */
+
+.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu {
+ top: auto !important;
+ bottom: 0 !important;
+ margin: 0 0 0 1em;
+}
+
+.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after {
+ top: auto !important;
+ bottom: 0 !important;
+ margin: 0 0 1em 0;
+ box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15);
+}
+
+/*--------------------
+ Sizes
+---------------------*/
+
+.ui.dropdown,
+.ui.dropdown .menu > .item {
+ font-size: 1rem;
+}
+
+.ui.mini.dropdown,
+.ui.mini.dropdown .menu > .item {
+ font-size: 0.78571429rem;
+}
+
+.ui.tiny.dropdown,
+.ui.tiny.dropdown .menu > .item {
+ font-size: 0.85714286rem;
+}
+
+.ui.small.dropdown,
+.ui.small.dropdown .menu > .item {
+ font-size: 0.92857143rem;
+}
+
+.ui.large.dropdown,
+.ui.large.dropdown .menu > .item {
+ font-size: 1.14285714rem;
+}
+
+.ui.big.dropdown,
+.ui.big.dropdown .menu > .item {
+ font-size: 1.28571429rem;
+}
+
+.ui.huge.dropdown,
+.ui.huge.dropdown .menu > .item {
+ font-size: 1.42857143rem;
+}
+
+.ui.massive.dropdown,
+.ui.massive.dropdown .menu > .item {
+ font-size: 1.71428571rem;
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/* Dropdown Carets */
+
+@font-face {
+ font-family: 'Dropdown';
+ src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjB5AAAAC8AAAAYGNtYXAPfuIIAAABHAAAAExnYXNwAAAAEAAAAWgAAAAIZ2x5Zjo82LgAAAFwAAABVGhlYWQAQ88bAAACxAAAADZoaGVhAwcB6QAAAvwAAAAkaG10eAS4ABIAAAMgAAAAIGxvY2EBNgDeAAADQAAAABJtYXhwAAoAFgAAA1QAAAAgbmFtZVcZpu4AAAN0AAABRXBvc3QAAwAAAAAEvAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADw2gHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIPDa//3//wAAAAAAIPDX//3//wAB/+MPLQADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAIABJQElABMAABM0NzY3BTYXFhUUDwEGJwYvASY1AAUGBwEACAUGBoAFCAcGgAUBEgcGBQEBAQcECQYHfwYBAQZ/BwYAAQAAAG4BJQESABMAADc0PwE2MzIfARYVFAcGIyEiJyY1AAWABgcIBYAGBgUI/wAHBgWABwaABQWABgcHBgUFBgcAAAABABIASQC3AW4AEwAANzQ/ATYXNhcWHQEUBwYnBi8BJjUSBoAFCAcFBgYFBwgFgAbbBwZ/BwEBBwQJ/wgEBwEBB38GBgAAAAABAAAASQClAW4AEwAANxE0NzYzMh8BFhUUDwEGIyInJjUABQYHCAWABgaABQgHBgVbAQAIBQYGgAUIBwWABgYFBwAAAAEAAAABAADZuaKOXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAAAAACgAUAB4AQgBkAIgAqgAAAAEAAAAIABQAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAOAAAAAQAAAAAAAgAOAEcAAQAAAAAAAwAOACQAAQAAAAAABAAOAFUAAQAAAAAABQAWAA4AAQAAAAAABgAHADIAAQAAAAAACgA0AGMAAwABBAkAAQAOAAAAAwABBAkAAgAOAEcAAwABBAkAAwAOACQAAwABBAkABAAOAFUAAwABBAkABQAWAA4AAwABBAkABgAOADkAAwABBAkACgA0AGMAaQBjAG8AbQBvAG8AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AbgBSAGUAZwB1AGwAYQByAGkAYwBvAG0AbwBvAG4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('truetype'), url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAVwAAoAAAAABSgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAdkAAAHZLDXE/09TLzIAAALQAAAAYAAAAGAIIweQY21hcAAAAzAAAABMAAAATA9+4ghnYXNwAAADfAAAAAgAAAAIAAAAEGhlYWQAAAOEAAAANgAAADYAQ88baGhlYQAAA7wAAAAkAAAAJAMHAelobXR4AAAD4AAAACAAAAAgBLgAEm1heHAAAAQAAAAABgAAAAYACFAAbmFtZQAABAgAAAFFAAABRVcZpu5wb3N0AAAFUAAAACAAAAAgAAMAAAEABAQAAQEBCGljb21vb24AAQIAAQA6+BwC+BsD+BgEHgoAGVP/i4seCgAZU/+LiwwHi2v4lPh0BR0AAACIDx0AAACNER0AAAAJHQAAAdASAAkBAQgPERMWGyAlKmljb21vb25pY29tb29udTB1MXUyMHVGMEQ3dUYwRDh1RjBEOXVGMERBAAACAYkABgAIAgABAAQABwAKAA0AVgCfAOgBL/yUDvyUDvyUDvuUDvtvi/emFYuQjZCOjo+Pj42Qiwj3lIsFkIuQiY6Hj4iNhouGi4aJh4eHCPsU+xQFiIiGiYaLhouHjYeOCPsU9xQFiI+Jj4uQCA77b4v3FBWLkI2Pjo8I9xT3FAWPjo+NkIuQi5CJjogI9xT7FAWPh42Hi4aLhomHh4eIiIaJhosI+5SLBYaLh42HjoiPiY+LkAgO+92d928Vi5CNkI+OCPcU9xQFjo+QjZCLkIuPiY6Hj4iNhouGCIv7lAWLhomHh4iIh4eJhouGi4aNiI8I+xT3FAWHjomPi5AIDvvdi+YVi/eUBYuQjZCOjo+Pj42Qi5CLkImOhwj3FPsUBY+IjYaLhouGiYeHiAj7FPsUBYiHhomGi4aLh42Hj4iOiY+LkAgO+JQU+JQViwwKAAAAAAMCAAGQAAUAAAFMAWYAAABHAUwBZgAAAPUAGQCEAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA8NoB4P/g/+AB4AAgAAAAAQAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABAA4AAAACgAIAAIAAgABACDw2v/9//8AAAAAACDw1//9//8AAf/jDy0AAwABAAAAAAAAAAAAAAABAAH//wAPAAEAAAABAAA5emozXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAUAAACAAAAAAADgCuAAEAAAAAAAEADgAAAAEAAAAAAAIADgBHAAEAAAAAAAMADgAkAAEAAAAAAAQADgBVAAEAAAAAAAUAFgAOAAEAAAAAAAYABwAyAAEAAAAAAAoANABjAAMAAQQJAAEADgAAAAMAAQQJAAIADgBHAAMAAQQJAAMADgAkAAMAAQQJAAQADgBVAAMAAQQJAAUAFgAOAAMAAQQJAAYADgA5AAMAAQQJAAoANABjAGkAYwBvAG0AbwBvAG4AVgBlAHIAcwBpAG8AbgAgADEALgAwAGkAYwBvAG0AbwBvAG5pY29tb29uAGkAYwBvAG0AbwBvAG4AUgBlAGcAdQBsAGEAcgBpAGMAbwBtAG8AbwBuAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.ui.dropdown > .dropdown.icon {
+ font-family: 'Dropdown';
+ line-height: 1;
+ height: 1em;
+ width: 1.23em;
+ backface-visibility: hidden;
+ font-weight: normal;
+ font-style: normal;
+ text-align: center;
+}
+
+.ui.dropdown > .dropdown.icon {
+ width: auto;
+}
+
+.ui.dropdown > .dropdown.icon:before {
+ content: '\f0d7';
+}
+
+/* Sub Menu */
+
+.ui.dropdown .menu .item .dropdown.icon:before {
+ content: '\f0da' ;
+}
+
+.ui.dropdown .item .left.dropdown.icon:before,
+.ui.dropdown .left.menu .item .dropdown.icon:before {
+ content: "\f0d9" ;
+}
+
+/* Vertical Menu Dropdown */
+
+.ui.vertical.menu .dropdown.item > .dropdown.icon:before {
+ content: "\f0da" ;
+}
+
+/* Icons for Reference
+.dropdown.down.icon {
+ content: "\f0d7";
+}
+.dropdown.up.icon {
+ content: "\f0d8";
+}
+.dropdown.left.icon {
+ content: "\f0d9";
+}
+.dropdown.icon.icon {
+ content: "\f0da";
+}
+*/
+
+/*******************************
+ User Overrides
+*******************************/
+/*!
+ * # Fomantic-UI - Form
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Elements
+*******************************/
+
+/*--------------------
+ Form
+---------------------*/
+
+.ui.form {
+ position: relative;
+ max-width: 100%;
+}
+
+/*--------------------
+ Content
+---------------------*/
+
+.ui.form > p {
+ margin: 1em 0;
+}
+
+/*--------------------
+ Field
+---------------------*/
+
+.ui.form .field {
+ clear: both;
+ margin: 0 0 1em;
+}
+
+.ui.form .fields .fields,
+.ui.form .field:last-child,
+.ui.form .fields:last-child .field {
+ margin-bottom: 0;
+}
+
+.ui.form .fields .field {
+ clear: both;
+ margin: 0;
+}
+
+/*--------------------
+ Labels
+---------------------*/
+
+.ui.form .field > label {
+ display: block;
+ margin: 0 0 0.28571429rem 0;
+ color: rgba(0, 0, 0, 0.87);
+ font-size: 0.92857143em;
+ font-weight: 500;
+ text-transform: none;
+}
+
+/*--------------------
+ Standard Inputs
+---------------------*/
+
+.ui.form textarea,
+.ui.form input:not([type]),
+.ui.form input[type="date"],
+.ui.form input[type="datetime-local"],
+.ui.form input[type="email"],
+.ui.form input[type="number"],
+.ui.form input[type="password"],
+.ui.form input[type="search"],
+.ui.form input[type="tel"],
+.ui.form input[type="time"],
+.ui.form input[type="text"],
+.ui.form input[type="file"],
+.ui.form input[type="url"] {
+ width: 100%;
+ vertical-align: top;
+}
+
+/* Set max height on unusual input */
+
+.ui.form ::-webkit-datetime-edit,
+.ui.form ::-webkit-inner-spin-button {
+ height: 1.21428571em;
+}
+
+.ui.form input:not([type]),
+.ui.form input[type="date"],
+.ui.form input[type="datetime-local"],
+.ui.form input[type="email"],
+.ui.form input[type="number"],
+.ui.form input[type="password"],
+.ui.form input[type="search"],
+.ui.form input[type="tel"],
+.ui.form input[type="time"],
+.ui.form input[type="text"],
+.ui.form input[type="file"],
+.ui.form input[type="url"] {
+ font-family: var(--fonts-regular);
+ margin: 0;
+ outline: none;
+ -webkit-appearance: none;
+ -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+ line-height: 1.21428571em;
+ padding: 0.67857143em 1em;
+ font-size: 1em;
+ background: #FFFFFF;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ color: rgba(0, 0, 0, 0.87);
+ border-radius: 0.28571429rem;
+ box-shadow: 0 0 0 0 transparent inset;
+ transition: color 0.1s ease, border-color 0.1s ease;
+}
+
+/* Text Area */
+
+.ui.input textarea,
+.ui.form textarea {
+ margin: 0;
+ -webkit-appearance: none;
+ -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+ padding: 0.78571429em 1em;
+ background: #FFFFFF;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ outline: none;
+ color: rgba(0, 0, 0, 0.87);
+ border-radius: 0.28571429rem;
+ box-shadow: 0 0 0 0 transparent inset;
+ transition: color 0.1s ease, border-color 0.1s ease;
+ font-size: 1em;
+ font-family: var(--fonts-regular);
+ line-height: 1.2857;
+ resize: vertical;
+}
+
+.ui.form textarea:not([rows]) {
+ height: 12em;
+ min-height: 8em;
+ max-height: 24em;
+}
+
+.ui.form textarea,
+.ui.form input[type="checkbox"] {
+ vertical-align: top;
+}
+
+/*--------------------
+ Checkbox margin
+---------------------*/
+
+.ui.form .fields:not(.grouped):not(.inline) .field:not(:only-child) label + .ui.ui.checkbox {
+ margin-top: 0.7em;
+}
+
+.ui.form .fields:not(.grouped):not(.inline) .field:not(:only-child) .ui.checkbox {
+ margin-top: 2.41428571em;
+}
+
+.ui.form .fields:not(.grouped):not(.inline) .field:not(:only-child) .ui.toggle.checkbox {
+ margin-top: 2.21428571em;
+}
+
+.ui.form .fields:not(.grouped):not(.inline) .field:not(:only-child) .ui.slider.checkbox {
+ margin-top: 2.61428571em;
+}
+
+.ui.ui.form .field .fields .field:not(:only-child) .ui.checkbox {
+ margin-top: 0.6em;
+}
+
+.ui.ui.form .field .fields .field:not(:only-child) .ui.toggle.checkbox {
+ margin-top: 0.5em;
+}
+
+.ui.ui.form .field .fields .field:not(:only-child) .ui.slider.checkbox {
+ margin-top: 0.7em;
+}
+
+/*--------------------------
+ Input w/ attached Button
+---------------------------*/
+
+.ui.form input.attached {
+ width: auto;
+}
+
+/*--------------------
+ Basic Select
+---------------------*/
+
+.ui.form select {
+ display: block;
+ height: auto;
+ width: 100%;
+ background: #FFFFFF;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ border-radius: 0.28571429rem;
+ box-shadow: 0 0 0 0 transparent inset;
+ padding: 0.62em 1em;
+ color: rgba(0, 0, 0, 0.87);
+ transition: color 0.1s ease, border-color 0.1s ease;
+}
+
+/*--------------------
+ Dropdown
+---------------------*/
+
+/* Block */
+
+.ui.form .field > .selection.dropdown {
+ min-width: auto;
+ width: 100%;
+}
+
+.ui.form .field > .selection.dropdown > .dropdown.icon {
+ float: right;
+}
+
+/* Inline */
+
+.ui.form .inline.fields .field > .selection.dropdown,
+.ui.form .inline.field > .selection.dropdown {
+ width: auto;
+}
+
+.ui.form .inline.fields .field > .selection.dropdown > .dropdown.icon,
+.ui.form .inline.field > .selection.dropdown > .dropdown.icon {
+ float: none;
+}
+
+/*--------------------
+ UI Input
+---------------------*/
+
+/* Block */
+
+.ui.form .field .ui.input,
+.ui.form .fields .field .ui.input,
+.ui.form .wide.field .ui.input {
+ width: 100%;
+}
+
+/* Inline */
+
+.ui.form .inline.fields .field:not(.wide) .ui.input,
+.ui.form .inline.field:not(.wide) .ui.input {
+ width: auto;
+ vertical-align: middle;
+}
+
+/* Auto Input */
+
+.ui.form .fields .field .ui.input input,
+.ui.form .field .ui.input input {
+ width: auto;
+}
+
+/* Full Width Input */
+
+.ui.form .ten.fields .ui.input input,
+.ui.form .nine.fields .ui.input input,
+.ui.form .eight.fields .ui.input input,
+.ui.form .seven.fields .ui.input input,
+.ui.form .six.fields .ui.input input,
+.ui.form .five.fields .ui.input input,
+.ui.form .four.fields .ui.input input,
+.ui.form .three.fields .ui.input input,
+.ui.form .two.fields .ui.input input,
+.ui.form .wide.field .ui.input input {
+ flex: 1 0 auto;
+ width: 0;
+}
+
+/*--------------------
+ Types of Messages
+---------------------*/
+
+.ui.form .error.message,
+.ui.form .error.message:empty {
+ display: none;
+}
+
+.ui.form .info.message,
+.ui.form .info.message:empty {
+ display: none;
+}
+
+.ui.form .success.message,
+.ui.form .success.message:empty {
+ display: none;
+}
+
+.ui.form .warning.message,
+.ui.form .warning.message:empty {
+ display: none;
+}
+
+/* Assumptions */
+
+.ui.form .message:first-child {
+ margin-top: 0;
+}
+
+/*--------------------
+ Validation Prompt
+---------------------*/
+
+.ui.form .field .prompt.label {
+ white-space: normal;
+ background: #FFFFFF !important;
+ border: 1px solid #E0B4B4 !important;
+ color: #9F3A38 !important;
+}
+
+.ui.form .inline.fields .field .prompt,
+.ui.form .inline.field .prompt {
+ vertical-align: top;
+ margin: -0.25em 0 -0.5em 0.5em;
+}
+
+.ui.form .inline.fields .field .prompt:before,
+.ui.form .inline.field .prompt:before {
+ border-width: 0 0 1px 1px;
+ bottom: auto;
+ right: auto;
+ top: 50%;
+ left: 0;
+}
+
+/*******************************
+ States
+*******************************/
+
+/*--------------------
+ Autofilled
+---------------------*/
+
+.ui.form .field.field input:-webkit-autofill {
+ box-shadow: 0 0 0 100px #FFFFF0 inset !important;
+ border-color: #E5DFA1 !important;
+}
+
+/* Focus */
+
+.ui.form .field.field input:-webkit-autofill:focus {
+ box-shadow: 0 0 0 100px #FFFFF0 inset !important;
+ border-color: #D5C315 !important;
+}
+
+/*--------------------
+ Placeholder
+---------------------*/
+
+/* browsers require these rules separate */
+
+.ui.form ::-webkit-input-placeholder {
+ color: rgba(191, 191, 191, 0.87);
+}
+
+.ui.form :-ms-input-placeholder {
+ color: rgba(191, 191, 191, 0.87) !important;
+}
+
+.ui.form ::-moz-placeholder {
+ color: rgba(191, 191, 191, 0.87);
+}
+
+.ui.form :focus::-webkit-input-placeholder {
+ color: rgba(115, 115, 115, 0.87);
+}
+
+.ui.form :focus:-ms-input-placeholder {
+ color: rgba(115, 115, 115, 0.87) !important;
+}
+
+.ui.form :focus::-moz-placeholder {
+ color: rgba(115, 115, 115, 0.87);
+}
+
+/*--------------------
+ Focus
+---------------------*/
+
+.ui.form input:not([type]):focus,
+.ui.form input[type="date"]:focus,
+.ui.form input[type="datetime-local"]:focus,
+.ui.form input[type="email"]:focus,
+.ui.form input[type="number"]:focus,
+.ui.form input[type="password"]:focus,
+.ui.form input[type="search"]:focus,
+.ui.form input[type="tel"]:focus,
+.ui.form input[type="time"]:focus,
+.ui.form input[type="text"]:focus,
+.ui.form input[type="file"]:focus,
+.ui.form input[type="url"]:focus {
+ color: rgba(0, 0, 0, 0.95);
+ border-color: #85B7D9;
+ border-radius: 0.28571429rem;
+ background: #FFFFFF;
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.35) inset;
+}
+
+.ui.form .ui.action.input:not([class*="left action"]) input:not([type]):focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="date"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="datetime-local"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="email"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="number"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="password"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="search"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="tel"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="time"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="text"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="file"]:focus,
+.ui.form .ui.action.input:not([class*="left action"]) input[type="url"]:focus {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.ui.form .ui[class*="left action"].input input:not([type]),
+.ui.form .ui[class*="left action"].input input[type="date"],
+.ui.form .ui[class*="left action"].input input[type="datetime-local"],
+.ui.form .ui[class*="left action"].input input[type="email"],
+.ui.form .ui[class*="left action"].input input[type="number"],
+.ui.form .ui[class*="left action"].input input[type="password"],
+.ui.form .ui[class*="left action"].input input[type="search"],
+.ui.form .ui[class*="left action"].input input[type="tel"],
+.ui.form .ui[class*="left action"].input input[type="time"],
+.ui.form .ui[class*="left action"].input input[type="text"],
+.ui.form .ui[class*="left action"].input input[type="file"],
+.ui.form .ui[class*="left action"].input input[type="url"] {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+}
+
+.ui.form textarea:focus {
+ color: rgba(0, 0, 0, 0.95);
+ border-color: #85B7D9;
+ border-radius: 0.28571429rem;
+ background: #FFFFFF;
+ box-shadow: 0 0 0 0 rgba(34, 36, 38, 0.35) inset;
+ -webkit-appearance: none;
+}
+
+/*--------------------
+ States
+ ---------------------*/
+
+/* On Form */
+
+.ui.form.error .error.message:not(:empty) {
+ display: block;
+}
+
+.ui.form.error .compact.error.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form.error .icon.error.message:not(:empty) {
+ display: flex;
+}
+
+/* On Field(s) */
+
+.ui.form .fields.error .error.message:not(:empty),
+.ui.form .field.error .error.message:not(:empty) {
+ display: block;
+}
+
+.ui.form .fields.error .compact.error.message:not(:empty),
+.ui.form .field.error .compact.error.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form .fields.error .icon.error.message:not(:empty),
+.ui.form .field.error .icon.error.message:not(:empty) {
+ display: flex;
+}
+
+.ui.ui.form .fields.error .field label,
+.ui.ui.form .field.error label,
+.ui.ui.form .fields.error .field .input,
+.ui.ui.form .field.error .input {
+ color: #9F3A38;
+}
+
+.ui.form .fields.error .field .corner.label,
+.ui.form .field.error .corner.label {
+ border-color: #9F3A38;
+ color: #FFFFFF;
+}
+
+.ui.form .fields.error .field textarea,
+.ui.form .fields.error .field select,
+.ui.form .fields.error .field input:not([type]),
+.ui.form .fields.error .field input[type="date"],
+.ui.form .fields.error .field input[type="datetime-local"],
+.ui.form .fields.error .field input[type="email"],
+.ui.form .fields.error .field input[type="number"],
+.ui.form .fields.error .field input[type="password"],
+.ui.form .fields.error .field input[type="search"],
+.ui.form .fields.error .field input[type="tel"],
+.ui.form .fields.error .field input[type="time"],
+.ui.form .fields.error .field input[type="text"],
+.ui.form .fields.error .field input[type="file"],
+.ui.form .fields.error .field input[type="url"],
+.ui.form .field.error textarea,
+.ui.form .field.error select,
+.ui.form .field.error input:not([type]),
+.ui.form .field.error input[type="date"],
+.ui.form .field.error input[type="datetime-local"],
+.ui.form .field.error input[type="email"],
+.ui.form .field.error input[type="number"],
+.ui.form .field.error input[type="password"],
+.ui.form .field.error input[type="search"],
+.ui.form .field.error input[type="tel"],
+.ui.form .field.error input[type="time"],
+.ui.form .field.error input[type="text"],
+.ui.form .field.error input[type="file"],
+.ui.form .field.error input[type="url"] {
+ color: #9F3A38;
+ background: #FFF6F6;
+ border-color: #E0B4B4;
+ border-radius: '';
+ box-shadow: none;
+}
+
+.ui.form .field.error textarea:focus,
+.ui.form .field.error select:focus,
+.ui.form .field.error input:not([type]):focus,
+.ui.form .field.error input[type="date"]:focus,
+.ui.form .field.error input[type="datetime-local"]:focus,
+.ui.form .field.error input[type="email"]:focus,
+.ui.form .field.error input[type="number"]:focus,
+.ui.form .field.error input[type="password"]:focus,
+.ui.form .field.error input[type="search"]:focus,
+.ui.form .field.error input[type="tel"]:focus,
+.ui.form .field.error input[type="time"]:focus,
+.ui.form .field.error input[type="text"]:focus,
+.ui.form .field.error input[type="file"]:focus,
+.ui.form .field.error input[type="url"]:focus {
+ background: #FFF6F6;
+ border-color: #E0B4B4;
+ color: #9F3A38;
+ box-shadow: none;
+}
+
+/* Preserve Native Select Stylings */
+
+.ui.form .field.error select {
+ -webkit-appearance: menulist-button;
+}
+
+/*------------------
+ Input State
+ --------------------*/
+
+/* Transparent */
+
+.ui.form .field.error .transparent.input input,
+.ui.form .field.error .transparent.input textarea,
+.ui.form .field.error input.transparent,
+.ui.form .field.error textarea.transparent {
+ background-color: #FFF6F6 !important;
+ color: #9F3A38 !important;
+}
+
+/* Autofilled */
+
+.ui.form .error.error input:-webkit-autofill {
+ box-shadow: 0 0 0 100px #FFFAF0 inset !important;
+ border-color: #E0B4B4 !important;
+}
+
+/* Placeholder */
+
+.ui.form .error ::-webkit-input-placeholder {
+ color: #e7bdbc;
+}
+
+.ui.form .error :-ms-input-placeholder {
+ color: #e7bdbc !important;
+}
+
+.ui.form .error ::-moz-placeholder {
+ color: #e7bdbc;
+}
+
+.ui.form .error :focus::-webkit-input-placeholder {
+ color: #da9796;
+}
+
+.ui.form .error :focus:-ms-input-placeholder {
+ color: #da9796 !important;
+}
+
+.ui.form .error :focus::-moz-placeholder {
+ color: #da9796;
+}
+
+/*------------------
+ Dropdown State
+ --------------------*/
+
+.ui.form .fields.error .field .ui.dropdown,
+.ui.form .fields.error .field .ui.dropdown .item,
+.ui.form .field.error .ui.dropdown,
+.ui.form .field.error .ui.dropdown .text,
+.ui.form .field.error .ui.dropdown .item {
+ background: #FFF6F6;
+ color: #9F3A38;
+}
+
+.ui.form .fields.error .field .ui.dropdown,
+.ui.form .field.error .ui.dropdown {
+ border-color: #E0B4B4 !important;
+}
+
+.ui.form .fields.error .field .ui.dropdown:hover,
+.ui.form .field.error .ui.dropdown:hover {
+ border-color: #E0B4B4 !important;
+}
+
+.ui.form .fields.error .field .ui.dropdown:hover .menu,
+.ui.form .field.error .ui.dropdown:hover .menu {
+ border-color: #E0B4B4;
+}
+
+.ui.form .fields.error .field .ui.multiple.selection.dropdown > .label,
+.ui.form .field.error .ui.multiple.selection.dropdown > .label {
+ background-color: #EACBCB;
+ color: #9F3A38;
+}
+
+/* Hover */
+
+.ui.form .fields.error .field .ui.dropdown .menu .item:hover,
+.ui.form .field.error .ui.dropdown .menu .item:hover {
+ background-color: #FBE7E7;
+}
+
+/* Selected */
+
+.ui.form .fields.error .field .ui.dropdown .menu .selected.item,
+.ui.form .field.error .ui.dropdown .menu .selected.item {
+ background-color: #FBE7E7;
+}
+
+/* Active */
+
+.ui.form .fields.error .field .ui.dropdown .menu .active.item,
+.ui.form .field.error .ui.dropdown .menu .active.item {
+ background-color: #FDCFCF !important;
+}
+
+/*--------------------
+ Checkbox State
+ ---------------------*/
+
+.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) label,
+.ui.form .field.error .checkbox:not(.toggle):not(.slider) label,
+.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) .box,
+.ui.form .field.error .checkbox:not(.toggle):not(.slider) .box {
+ color: #9F3A38;
+}
+
+.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .field.error .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) .box:before,
+.ui.form .field.error .checkbox:not(.toggle):not(.slider) .box:before {
+ background: #FFF6F6;
+ border-color: #E0B4B4;
+}
+
+.ui.form .fields.error .field .checkbox label:after,
+.ui.form .field.error .checkbox label:after,
+.ui.form .fields.error .field .checkbox .box:after,
+.ui.form .field.error .checkbox .box:after {
+ color: #9F3A38;
+}
+
+/* On Form */
+
+.ui.form.info .info.message:not(:empty) {
+ display: block;
+}
+
+.ui.form.info .compact.info.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form.info .icon.info.message:not(:empty) {
+ display: flex;
+}
+
+/* On Field(s) */
+
+.ui.form .fields.info .info.message:not(:empty),
+.ui.form .field.info .info.message:not(:empty) {
+ display: block;
+}
+
+.ui.form .fields.info .compact.info.message:not(:empty),
+.ui.form .field.info .compact.info.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form .fields.info .icon.info.message:not(:empty),
+.ui.form .field.info .icon.info.message:not(:empty) {
+ display: flex;
+}
+
+.ui.ui.form .fields.info .field label,
+.ui.ui.form .field.info label,
+.ui.ui.form .fields.info .field .input,
+.ui.ui.form .field.info .input {
+ color: #276F86;
+}
+
+.ui.form .fields.info .field .corner.label,
+.ui.form .field.info .corner.label {
+ border-color: #276F86;
+ color: #FFFFFF;
+}
+
+.ui.form .fields.info .field textarea,
+.ui.form .fields.info .field select,
+.ui.form .fields.info .field input:not([type]),
+.ui.form .fields.info .field input[type="date"],
+.ui.form .fields.info .field input[type="datetime-local"],
+.ui.form .fields.info .field input[type="email"],
+.ui.form .fields.info .field input[type="number"],
+.ui.form .fields.info .field input[type="password"],
+.ui.form .fields.info .field input[type="search"],
+.ui.form .fields.info .field input[type="tel"],
+.ui.form .fields.info .field input[type="time"],
+.ui.form .fields.info .field input[type="text"],
+.ui.form .fields.info .field input[type="file"],
+.ui.form .fields.info .field input[type="url"],
+.ui.form .field.info textarea,
+.ui.form .field.info select,
+.ui.form .field.info input:not([type]),
+.ui.form .field.info input[type="date"],
+.ui.form .field.info input[type="datetime-local"],
+.ui.form .field.info input[type="email"],
+.ui.form .field.info input[type="number"],
+.ui.form .field.info input[type="password"],
+.ui.form .field.info input[type="search"],
+.ui.form .field.info input[type="tel"],
+.ui.form .field.info input[type="time"],
+.ui.form .field.info input[type="text"],
+.ui.form .field.info input[type="file"],
+.ui.form .field.info input[type="url"] {
+ color: #276F86;
+ background: #F8FFFF;
+ border-color: #A9D5DE;
+ border-radius: '';
+ box-shadow: none;
+}
+
+.ui.form .field.info textarea:focus,
+.ui.form .field.info select:focus,
+.ui.form .field.info input:not([type]):focus,
+.ui.form .field.info input[type="date"]:focus,
+.ui.form .field.info input[type="datetime-local"]:focus,
+.ui.form .field.info input[type="email"]:focus,
+.ui.form .field.info input[type="number"]:focus,
+.ui.form .field.info input[type="password"]:focus,
+.ui.form .field.info input[type="search"]:focus,
+.ui.form .field.info input[type="tel"]:focus,
+.ui.form .field.info input[type="time"]:focus,
+.ui.form .field.info input[type="text"]:focus,
+.ui.form .field.info input[type="file"]:focus,
+.ui.form .field.info input[type="url"]:focus {
+ background: #F8FFFF;
+ border-color: #A9D5DE;
+ color: #276F86;
+ box-shadow: none;
+}
+
+/* Preserve Native Select Stylings */
+
+.ui.form .field.info select {
+ -webkit-appearance: menulist-button;
+}
+
+/*------------------
+ Input State
+ --------------------*/
+
+/* Transparent */
+
+.ui.form .field.info .transparent.input input,
+.ui.form .field.info .transparent.input textarea,
+.ui.form .field.info input.transparent,
+.ui.form .field.info textarea.transparent {
+ background-color: #F8FFFF !important;
+ color: #276F86 !important;
+}
+
+/* Autofilled */
+
+.ui.form .info.info input:-webkit-autofill {
+ box-shadow: 0 0 0 100px #F0FAFF inset !important;
+ border-color: #b3e0e0 !important;
+}
+
+/* Placeholder */
+
+.ui.form .info ::-webkit-input-placeholder {
+ color: #98cfe1;
+}
+
+.ui.form .info :-ms-input-placeholder {
+ color: #98cfe1 !important;
+}
+
+.ui.form .info ::-moz-placeholder {
+ color: #98cfe1;
+}
+
+.ui.form .info :focus::-webkit-input-placeholder {
+ color: #70bdd6;
+}
+
+.ui.form .info :focus:-ms-input-placeholder {
+ color: #70bdd6 !important;
+}
+
+.ui.form .info :focus::-moz-placeholder {
+ color: #70bdd6;
+}
+
+/*------------------
+ Dropdown State
+ --------------------*/
+
+.ui.form .fields.info .field .ui.dropdown,
+.ui.form .fields.info .field .ui.dropdown .item,
+.ui.form .field.info .ui.dropdown,
+.ui.form .field.info .ui.dropdown .text,
+.ui.form .field.info .ui.dropdown .item {
+ background: #F8FFFF;
+ color: #276F86;
+}
+
+.ui.form .fields.info .field .ui.dropdown,
+.ui.form .field.info .ui.dropdown {
+ border-color: #A9D5DE !important;
+}
+
+.ui.form .fields.info .field .ui.dropdown:hover,
+.ui.form .field.info .ui.dropdown:hover {
+ border-color: #A9D5DE !important;
+}
+
+.ui.form .fields.info .field .ui.dropdown:hover .menu,
+.ui.form .field.info .ui.dropdown:hover .menu {
+ border-color: #A9D5DE;
+}
+
+.ui.form .fields.info .field .ui.multiple.selection.dropdown > .label,
+.ui.form .field.info .ui.multiple.selection.dropdown > .label {
+ background-color: #cce3ea;
+ color: #276F86;
+}
+
+/* Hover */
+
+.ui.form .fields.info .field .ui.dropdown .menu .item:hover,
+.ui.form .field.info .ui.dropdown .menu .item:hover {
+ background-color: #e9f2fb;
+}
+
+/* Selected */
+
+.ui.form .fields.info .field .ui.dropdown .menu .selected.item,
+.ui.form .field.info .ui.dropdown .menu .selected.item {
+ background-color: #e9f2fb;
+}
+
+/* Active */
+
+.ui.form .fields.info .field .ui.dropdown .menu .active.item,
+.ui.form .field.info .ui.dropdown .menu .active.item {
+ background-color: #cef1fd !important;
+}
+
+/*--------------------
+ Checkbox State
+ ---------------------*/
+
+.ui.form .fields.info .field .checkbox:not(.toggle):not(.slider) label,
+.ui.form .field.info .checkbox:not(.toggle):not(.slider) label,
+.ui.form .fields.info .field .checkbox:not(.toggle):not(.slider) .box,
+.ui.form .field.info .checkbox:not(.toggle):not(.slider) .box {
+ color: #276F86;
+}
+
+.ui.form .fields.info .field .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .field.info .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .fields.info .field .checkbox:not(.toggle):not(.slider) .box:before,
+.ui.form .field.info .checkbox:not(.toggle):not(.slider) .box:before {
+ background: #F8FFFF;
+ border-color: #A9D5DE;
+}
+
+.ui.form .fields.info .field .checkbox label:after,
+.ui.form .field.info .checkbox label:after,
+.ui.form .fields.info .field .checkbox .box:after,
+.ui.form .field.info .checkbox .box:after {
+ color: #276F86;
+}
+
+/* On Form */
+
+.ui.form.success .success.message:not(:empty) {
+ display: block;
+}
+
+.ui.form.success .compact.success.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form.success .icon.success.message:not(:empty) {
+ display: flex;
+}
+
+/* On Field(s) */
+
+.ui.form .fields.success .success.message:not(:empty),
+.ui.form .field.success .success.message:not(:empty) {
+ display: block;
+}
+
+.ui.form .fields.success .compact.success.message:not(:empty),
+.ui.form .field.success .compact.success.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form .fields.success .icon.success.message:not(:empty),
+.ui.form .field.success .icon.success.message:not(:empty) {
+ display: flex;
+}
+
+.ui.ui.form .fields.success .field label,
+.ui.ui.form .field.success label,
+.ui.ui.form .fields.success .field .input,
+.ui.ui.form .field.success .input {
+ color: #2C662D;
+}
+
+.ui.form .fields.success .field .corner.label,
+.ui.form .field.success .corner.label {
+ border-color: #2C662D;
+ color: #FFFFFF;
+}
+
+.ui.form .fields.success .field textarea,
+.ui.form .fields.success .field select,
+.ui.form .fields.success .field input:not([type]),
+.ui.form .fields.success .field input[type="date"],
+.ui.form .fields.success .field input[type="datetime-local"],
+.ui.form .fields.success .field input[type="email"],
+.ui.form .fields.success .field input[type="number"],
+.ui.form .fields.success .field input[type="password"],
+.ui.form .fields.success .field input[type="search"],
+.ui.form .fields.success .field input[type="tel"],
+.ui.form .fields.success .field input[type="time"],
+.ui.form .fields.success .field input[type="text"],
+.ui.form .fields.success .field input[type="file"],
+.ui.form .fields.success .field input[type="url"],
+.ui.form .field.success textarea,
+.ui.form .field.success select,
+.ui.form .field.success input:not([type]),
+.ui.form .field.success input[type="date"],
+.ui.form .field.success input[type="datetime-local"],
+.ui.form .field.success input[type="email"],
+.ui.form .field.success input[type="number"],
+.ui.form .field.success input[type="password"],
+.ui.form .field.success input[type="search"],
+.ui.form .field.success input[type="tel"],
+.ui.form .field.success input[type="time"],
+.ui.form .field.success input[type="text"],
+.ui.form .field.success input[type="file"],
+.ui.form .field.success input[type="url"] {
+ color: #2C662D;
+ background: #FCFFF5;
+ border-color: #A3C293;
+ border-radius: '';
+ box-shadow: none;
+}
+
+.ui.form .field.success textarea:focus,
+.ui.form .field.success select:focus,
+.ui.form .field.success input:not([type]):focus,
+.ui.form .field.success input[type="date"]:focus,
+.ui.form .field.success input[type="datetime-local"]:focus,
+.ui.form .field.success input[type="email"]:focus,
+.ui.form .field.success input[type="number"]:focus,
+.ui.form .field.success input[type="password"]:focus,
+.ui.form .field.success input[type="search"]:focus,
+.ui.form .field.success input[type="tel"]:focus,
+.ui.form .field.success input[type="time"]:focus,
+.ui.form .field.success input[type="text"]:focus,
+.ui.form .field.success input[type="file"]:focus,
+.ui.form .field.success input[type="url"]:focus {
+ background: #FCFFF5;
+ border-color: #A3C293;
+ color: #2C662D;
+ box-shadow: none;
+}
+
+/* Preserve Native Select Stylings */
+
+.ui.form .field.success select {
+ -webkit-appearance: menulist-button;
+}
+
+/*------------------
+ Input State
+ --------------------*/
+
+/* Transparent */
+
+.ui.form .field.success .transparent.input input,
+.ui.form .field.success .transparent.input textarea,
+.ui.form .field.success input.transparent,
+.ui.form .field.success textarea.transparent {
+ background-color: #FCFFF5 !important;
+ color: #2C662D !important;
+}
+
+/* Autofilled */
+
+.ui.form .success.success input:-webkit-autofill {
+ box-shadow: 0 0 0 100px #F0FFF0 inset !important;
+ border-color: #bee0b3 !important;
+}
+
+/* Placeholder */
+
+.ui.form .success ::-webkit-input-placeholder {
+ color: #8fcf90;
+}
+
+.ui.form .success :-ms-input-placeholder {
+ color: #8fcf90 !important;
+}
+
+.ui.form .success ::-moz-placeholder {
+ color: #8fcf90;
+}
+
+.ui.form .success :focus::-webkit-input-placeholder {
+ color: #6cbf6d;
+}
+
+.ui.form .success :focus:-ms-input-placeholder {
+ color: #6cbf6d !important;
+}
+
+.ui.form .success :focus::-moz-placeholder {
+ color: #6cbf6d;
+}
+
+/*------------------
+ Dropdown State
+ --------------------*/
+
+.ui.form .fields.success .field .ui.dropdown,
+.ui.form .fields.success .field .ui.dropdown .item,
+.ui.form .field.success .ui.dropdown,
+.ui.form .field.success .ui.dropdown .text,
+.ui.form .field.success .ui.dropdown .item {
+ background: #FCFFF5;
+ color: #2C662D;
+}
+
+.ui.form .fields.success .field .ui.dropdown,
+.ui.form .field.success .ui.dropdown {
+ border-color: #A3C293 !important;
+}
+
+.ui.form .fields.success .field .ui.dropdown:hover,
+.ui.form .field.success .ui.dropdown:hover {
+ border-color: #A3C293 !important;
+}
+
+.ui.form .fields.success .field .ui.dropdown:hover .menu,
+.ui.form .field.success .ui.dropdown:hover .menu {
+ border-color: #A3C293;
+}
+
+.ui.form .fields.success .field .ui.multiple.selection.dropdown > .label,
+.ui.form .field.success .ui.multiple.selection.dropdown > .label {
+ background-color: #cceacc;
+ color: #2C662D;
+}
+
+/* Hover */
+
+.ui.form .fields.success .field .ui.dropdown .menu .item:hover,
+.ui.form .field.success .ui.dropdown .menu .item:hover {
+ background-color: #e9fbe9;
+}
+
+/* Selected */
+
+.ui.form .fields.success .field .ui.dropdown .menu .selected.item,
+.ui.form .field.success .ui.dropdown .menu .selected.item {
+ background-color: #e9fbe9;
+}
+
+/* Active */
+
+.ui.form .fields.success .field .ui.dropdown .menu .active.item,
+.ui.form .field.success .ui.dropdown .menu .active.item {
+ background-color: #dafdce !important;
+}
+
+/*--------------------
+ Checkbox State
+ ---------------------*/
+
+.ui.form .fields.success .field .checkbox:not(.toggle):not(.slider) label,
+.ui.form .field.success .checkbox:not(.toggle):not(.slider) label,
+.ui.form .fields.success .field .checkbox:not(.toggle):not(.slider) .box,
+.ui.form .field.success .checkbox:not(.toggle):not(.slider) .box {
+ color: #2C662D;
+}
+
+.ui.form .fields.success .field .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .field.success .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .fields.success .field .checkbox:not(.toggle):not(.slider) .box:before,
+.ui.form .field.success .checkbox:not(.toggle):not(.slider) .box:before {
+ background: #FCFFF5;
+ border-color: #A3C293;
+}
+
+.ui.form .fields.success .field .checkbox label:after,
+.ui.form .field.success .checkbox label:after,
+.ui.form .fields.success .field .checkbox .box:after,
+.ui.form .field.success .checkbox .box:after {
+ color: #2C662D;
+}
+
+/* On Form */
+
+.ui.form.warning .warning.message:not(:empty) {
+ display: block;
+}
+
+.ui.form.warning .compact.warning.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form.warning .icon.warning.message:not(:empty) {
+ display: flex;
+}
+
+/* On Field(s) */
+
+.ui.form .fields.warning .warning.message:not(:empty),
+.ui.form .field.warning .warning.message:not(:empty) {
+ display: block;
+}
+
+.ui.form .fields.warning .compact.warning.message:not(:empty),
+.ui.form .field.warning .compact.warning.message:not(:empty) {
+ display: inline-block;
+}
+
+.ui.form .fields.warning .icon.warning.message:not(:empty),
+.ui.form .field.warning .icon.warning.message:not(:empty) {
+ display: flex;
+}
+
+.ui.ui.form .fields.warning .field label,
+.ui.ui.form .field.warning label,
+.ui.ui.form .fields.warning .field .input,
+.ui.ui.form .field.warning .input {
+ color: #573A08;
+}
+
+.ui.form .fields.warning .field .corner.label,
+.ui.form .field.warning .corner.label {
+ border-color: #573A08;
+ color: #FFFFFF;
+}
+
+.ui.form .fields.warning .field textarea,
+.ui.form .fields.warning .field select,
+.ui.form .fields.warning .field input:not([type]),
+.ui.form .fields.warning .field input[type="date"],
+.ui.form .fields.warning .field input[type="datetime-local"],
+.ui.form .fields.warning .field input[type="email"],
+.ui.form .fields.warning .field input[type="number"],
+.ui.form .fields.warning .field input[type="password"],
+.ui.form .fields.warning .field input[type="search"],
+.ui.form .fields.warning .field input[type="tel"],
+.ui.form .fields.warning .field input[type="time"],
+.ui.form .fields.warning .field input[type="text"],
+.ui.form .fields.warning .field input[type="file"],
+.ui.form .fields.warning .field input[type="url"],
+.ui.form .field.warning textarea,
+.ui.form .field.warning select,
+.ui.form .field.warning input:not([type]),
+.ui.form .field.warning input[type="date"],
+.ui.form .field.warning input[type="datetime-local"],
+.ui.form .field.warning input[type="email"],
+.ui.form .field.warning input[type="number"],
+.ui.form .field.warning input[type="password"],
+.ui.form .field.warning input[type="search"],
+.ui.form .field.warning input[type="tel"],
+.ui.form .field.warning input[type="time"],
+.ui.form .field.warning input[type="text"],
+.ui.form .field.warning input[type="file"],
+.ui.form .field.warning input[type="url"] {
+ color: #573A08;
+ background: #FFFAF3;
+ border-color: #C9BA9B;
+ border-radius: '';
+ box-shadow: none;
+}
+
+.ui.form .field.warning textarea:focus,
+.ui.form .field.warning select:focus,
+.ui.form .field.warning input:not([type]):focus,
+.ui.form .field.warning input[type="date"]:focus,
+.ui.form .field.warning input[type="datetime-local"]:focus,
+.ui.form .field.warning input[type="email"]:focus,
+.ui.form .field.warning input[type="number"]:focus,
+.ui.form .field.warning input[type="password"]:focus,
+.ui.form .field.warning input[type="search"]:focus,
+.ui.form .field.warning input[type="tel"]:focus,
+.ui.form .field.warning input[type="time"]:focus,
+.ui.form .field.warning input[type="text"]:focus,
+.ui.form .field.warning input[type="file"]:focus,
+.ui.form .field.warning input[type="url"]:focus {
+ background: #FFFAF3;
+ border-color: #C9BA9B;
+ color: #573A08;
+ box-shadow: none;
+}
+
+/* Preserve Native Select Stylings */
+
+.ui.form .field.warning select {
+ -webkit-appearance: menulist-button;
+}
+
+/*------------------
+ Input State
+ --------------------*/
+
+/* Transparent */
+
+.ui.form .field.warning .transparent.input input,
+.ui.form .field.warning .transparent.input textarea,
+.ui.form .field.warning input.transparent,
+.ui.form .field.warning textarea.transparent {
+ background-color: #FFFAF3 !important;
+ color: #573A08 !important;
+}
+
+/* Autofilled */
+
+.ui.form .warning.warning input:-webkit-autofill {
+ box-shadow: 0 0 0 100px #FFFFe0 inset !important;
+ border-color: #e0e0b3 !important;
+}
+
+/* Placeholder */
+
+.ui.form .warning ::-webkit-input-placeholder {
+ color: #edad3e;
+}
+
+.ui.form .warning :-ms-input-placeholder {
+ color: #edad3e !important;
+}
+
+.ui.form .warning ::-moz-placeholder {
+ color: #edad3e;
+}
+
+.ui.form .warning :focus::-webkit-input-placeholder {
+ color: #e39715;
+}
+
+.ui.form .warning :focus:-ms-input-placeholder {
+ color: #e39715 !important;
+}
+
+.ui.form .warning :focus::-moz-placeholder {
+ color: #e39715;
+}
+
+/*------------------
+ Dropdown State
+ --------------------*/
+
+.ui.form .fields.warning .field .ui.dropdown,
+.ui.form .fields.warning .field .ui.dropdown .item,
+.ui.form .field.warning .ui.dropdown,
+.ui.form .field.warning .ui.dropdown .text,
+.ui.form .field.warning .ui.dropdown .item {
+ background: #FFFAF3;
+ color: #573A08;
+}
+
+.ui.form .fields.warning .field .ui.dropdown,
+.ui.form .field.warning .ui.dropdown {
+ border-color: #C9BA9B !important;
+}
+
+.ui.form .fields.warning .field .ui.dropdown:hover,
+.ui.form .field.warning .ui.dropdown:hover {
+ border-color: #C9BA9B !important;
+}
+
+.ui.form .fields.warning .field .ui.dropdown:hover .menu,
+.ui.form .field.warning .ui.dropdown:hover .menu {
+ border-color: #C9BA9B;
+}
+
+.ui.form .fields.warning .field .ui.multiple.selection.dropdown > .label,
+.ui.form .field.warning .ui.multiple.selection.dropdown > .label {
+ background-color: #eaeacc;
+ color: #573A08;
+}
+
+/* Hover */
+
+.ui.form .fields.warning .field .ui.dropdown .menu .item:hover,
+.ui.form .field.warning .ui.dropdown .menu .item:hover {
+ background-color: #fbfbe9;
+}
+
+/* Selected */
+
+.ui.form .fields.warning .field .ui.dropdown .menu .selected.item,
+.ui.form .field.warning .ui.dropdown .menu .selected.item {
+ background-color: #fbfbe9;
+}
+
+/* Active */
+
+.ui.form .fields.warning .field .ui.dropdown .menu .active.item,
+.ui.form .field.warning .ui.dropdown .menu .active.item {
+ background-color: #fdfdce !important;
+}
+
+/*--------------------
+ Checkbox State
+ ---------------------*/
+
+.ui.form .fields.warning .field .checkbox:not(.toggle):not(.slider) label,
+.ui.form .field.warning .checkbox:not(.toggle):not(.slider) label,
+.ui.form .fields.warning .field .checkbox:not(.toggle):not(.slider) .box,
+.ui.form .field.warning .checkbox:not(.toggle):not(.slider) .box {
+ color: #573A08;
+}
+
+.ui.form .fields.warning .field .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .field.warning .checkbox:not(.toggle):not(.slider) label:before,
+.ui.form .fields.warning .field .checkbox:not(.toggle):not(.slider) .box:before,
+.ui.form .field.warning .checkbox:not(.toggle):not(.slider) .box:before {
+ background: #FFFAF3;
+ border-color: #C9BA9B;
+}
+
+.ui.form .fields.warning .field .checkbox label:after,
+.ui.form .field.warning .checkbox label:after,
+.ui.form .fields.warning .field .checkbox .box:after,
+.ui.form .field.warning .checkbox .box:after {
+ color: #573A08;
+}
+
+/*--------------------
+ Disabled
+ ---------------------*/
+
+.ui.form .disabled.fields .field,
+.ui.form .disabled.field,
+.ui.form .field :disabled {
+ pointer-events: none;
+ opacity: var(--opacity-disabled);
+}
+
+.ui.form .field.disabled > label,
+.ui.form .fields.disabled > label {
+ opacity: var(--opacity-disabled);
+}
+
+.ui.form .field.disabled :disabled {
+ opacity: 1;
+}
+
+/*--------------
+ Loading
+ ---------------*/
+
+.ui.loading.form {
+ position: relative;
+ cursor: default;
+ pointer-events: none;
+}
+
+.ui.loading.form:before {
+ position: absolute;
+ content: '';
+ top: 0;
+ left: 0;
+ background: rgba(255, 255, 255, 0.8);
+ width: 100%;
+ height: 100%;
+ z-index: 100;
+}
+
+.ui.loading.form.segments:before {
+ border-radius: 0.28571429rem;
+}
+
+.ui.loading.form:after {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -1.5em 0 0 -1.5em;
+ width: 3em;
+ height: 3em;
+ animation: loader 0.6s infinite linear;
+ border: 0.2em solid #767676;
+ border-radius: 500rem;
+ box-shadow: 0 0 0 1px transparent;
+ visibility: visible;
+ z-index: 101;
+}
+
+/*******************************
+ Element Types
+*******************************/
+
+/*--------------------
+ Required Field
+ ---------------------*/
+
+.ui.form .required.fields:not(.grouped) > .field > label:after,
+.ui.form .required.fields.grouped > label:after,
+.ui.form .required.field > label:after,
+.ui.form .required.fields:not(.grouped) > .field > .checkbox:after,
+.ui.form .required.field > .checkbox:after,
+.ui.form label.required:after {
+ margin: -0.2em 0 0 0.2em;
+ content: '*';
+ color: #DB2828;
+}
+
+.ui.form .required.fields:not(.grouped) > .field > label:after,
+.ui.form .required.fields.grouped > label:after,
+.ui.form .required.field > label:after,
+.ui.form label.required:after {
+ display: inline-block;
+ vertical-align: top;
+}
+
+.ui.form .required.fields:not(.grouped) > .field > .checkbox:after,
+.ui.form .required.field > .checkbox:after {
+ position: absolute;
+ top: 0;
+ left: 100%;
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*--------------------
+ Field Groups
+ ---------------------*/
+
+/* Grouped Vertically */
+
+.ui.form .grouped.fields {
+ display: block;
+ margin: 0 0 1em;
+}
+
+.ui.form .grouped.fields:last-child {
+ margin-bottom: 0;
+}
+
+.ui.form .grouped.fields > label {
+ margin: 0 0 0.28571429rem 0;
+ color: rgba(0, 0, 0, 0.87);
+ font-size: 0.92857143em;
+ font-weight: 500;
+ text-transform: none;
+}
+
+.ui.form .grouped.fields .field,
+.ui.form .grouped.inline.fields .field {
+ display: block;
+ margin: 0.5em 0;
+ padding: 0;
+}
+
+.ui.form .grouped.inline.fields .ui.checkbox {
+ margin-bottom: 0.4em;
+}
+
+/*--------------------
+ Fields
+---------------------*/
+
+/* Split fields */
+
+.ui.form .fields {
+ display: flex;
+ flex-direction: row;
+ margin: 0 -0.5em 1em;
+}
+
+.ui.form .fields > .field {
+ flex: 0 1 auto;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+.ui.form .fields > .field:first-child {
+ border-left: none;
+ box-shadow: none;
+}
+
+/* Other Combinations */
+
+.ui.form .two.fields > .fields,
+.ui.form .two.fields > .field {
+ width: 50%;
+}
+
+.ui.form .three.fields > .fields,
+.ui.form .three.fields > .field {
+ width: 33.33333333%;
+}
+
+.ui.form .four.fields > .fields,
+.ui.form .four.fields > .field {
+ width: 25%;
+}
+
+.ui.form .five.fields > .fields,
+.ui.form .five.fields > .field {
+ width: 20%;
+}
+
+.ui.form .six.fields > .fields,
+.ui.form .six.fields > .field {
+ width: 16.66666667%;
+}
+
+.ui.form .seven.fields > .fields,
+.ui.form .seven.fields > .field {
+ width: 14.28571429%;
+}
+
+.ui.form .eight.fields > .fields,
+.ui.form .eight.fields > .field {
+ width: 12.5%;
+}
+
+.ui.form .nine.fields > .fields,
+.ui.form .nine.fields > .field {
+ width: 11.11111111%;
+}
+
+.ui.form .ten.fields > .fields,
+.ui.form .ten.fields > .field {
+ width: 10%;
+}
+
+/* Swap to full width on mobile */
+
+@media only screen and (max-width: 767.98px) {
+ .ui.form .fields {
+ flex-wrap: wrap;
+ margin-bottom: 0;
+ }
+
+ .ui.form:not(.unstackable) .fields:not(.unstackable) > .fields,
+ .ui.form:not(.unstackable) .fields:not(.unstackable) > .field {
+ width: 100%;
+ margin: 0 0 1em;
+ }
+}
+
+/* Sizing Combinations */
+
+.ui.form .fields .wide.field {
+ width: 6.25%;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+.ui.form .one.wide.field {
+ width: 6.25%;
+}
+
+.ui.form .two.wide.field {
+ width: 12.5%;
+}
+
+.ui.form .three.wide.field {
+ width: 18.75%;
+}
+
+.ui.form .four.wide.field {
+ width: 25%;
+}
+
+.ui.form .five.wide.field {
+ width: 31.25%;
+}
+
+.ui.form .six.wide.field {
+ width: 37.5%;
+}
+
+.ui.form .seven.wide.field {
+ width: 43.75%;
+}
+
+.ui.form .eight.wide.field {
+ width: 50%;
+}
+
+.ui.form .nine.wide.field {
+ width: 56.25%;
+}
+
+.ui.form .ten.wide.field {
+ width: 62.5%;
+}
+
+.ui.form .eleven.wide.field {
+ width: 68.75%;
+}
+
+.ui.form .twelve.wide.field {
+ width: 75%;
+}
+
+.ui.form .thirteen.wide.field {
+ width: 81.25%;
+}
+
+.ui.form .fourteen.wide.field {
+ width: 87.5%;
+}
+
+.ui.form .fifteen.wide.field {
+ width: 93.75%;
+}
+
+.ui.form .sixteen.wide.field {
+ width: 100%;
+}
+
+/*--------------------
+ Equal Width
+---------------------*/
+
+.ui[class*="equal width"].form .fields > .field,
+.ui.form [class*="equal width"].fields > .field {
+ width: 100%;
+ flex: 1 1 auto;
+}
+
+/*--------------------
+ Inline Fields
+ ---------------------*/
+
+.ui.form .inline.fields {
+ margin: 0 0 1em;
+ align-items: center;
+}
+
+.ui.form .inline.fields .field {
+ margin: 0;
+ padding: 0 1em 0 0;
+}
+
+/* Inline Label */
+
+.ui.form .inline.fields > label,
+.ui.form .inline.fields .field > label,
+.ui.form .inline.fields .field > p,
+.ui.form .inline.field > label,
+.ui.form .inline.field > p {
+ display: inline-block;
+ width: auto;
+ margin-top: 0;
+ margin-bottom: 0;
+ vertical-align: baseline;
+ font-size: 0.92857143em;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.87);
+ text-transform: none;
+}
+
+/* Grouped Inline Label */
+
+.ui.form .inline.fields > label {
+ margin: 0.035714em 1em 0 0;
+}
+
+/* Inline Input */
+
+.ui.form .inline.fields .field > input,
+.ui.form .inline.fields .field > select,
+.ui.form .inline.field > input,
+.ui.form .inline.field > select {
+ display: inline-block;
+ width: auto;
+ margin-top: 0;
+ margin-bottom: 0;
+ vertical-align: middle;
+ font-size: 1em;
+}
+
+.ui.form .inline.fields .field .calendar:not(.popup),
+.ui.form .inline.field .calendar:not(.popup) {
+ display: inline-block;
+}
+
+.ui.form .inline.fields .field .calendar:not(.popup) > .input > input,
+.ui.form .inline.field .calendar:not(.popup) > .input > input {
+ width: 13.11em;
+}
+
+/* Label */
+
+.ui.form .inline.fields .field > :first-child,
+.ui.form .inline.field > :first-child {
+ margin: 0 0.85714286em 0 0;
+}
+
+.ui.form .inline.fields .field > :only-child,
+.ui.form .inline.field > :only-child {
+ margin: 0;
+}
+
+/* Wide */
+
+.ui.form .inline.fields .wide.field {
+ display: flex;
+ align-items: center;
+}
+
+.ui.form .inline.fields .wide.field > input,
+.ui.form .inline.fields .wide.field > select {
+ width: 100%;
+}
+
+/*--------------------
+ Sizes
+---------------------*/
+
+.ui.form,
+.ui.form .field .dropdown,
+.ui.form .field .dropdown .menu > .item {
+ font-size: 1rem;
+}
+
+.ui.mini.form,
+.ui.mini.form .field .dropdown,
+.ui.mini.form .field .dropdown .menu > .item {
+ font-size: 0.78571429rem;
+}
+
+.ui.tiny.form,
+.ui.tiny.form .field .dropdown,
+.ui.tiny.form .field .dropdown .menu > .item {
+ font-size: 0.85714286rem;
+}
+
+.ui.small.form,
+.ui.small.form .field .dropdown,
+.ui.small.form .field .dropdown .menu > .item {
+ font-size: 0.92857143rem;
+}
+
+.ui.large.form,
+.ui.large.form .field .dropdown,
+.ui.large.form .field .dropdown .menu > .item {
+ font-size: 1.14285714rem;
+}
+
+.ui.big.form,
+.ui.big.form .field .dropdown,
+.ui.big.form .field .dropdown .menu > .item {
+ font-size: 1.28571429rem;
+}
+
+.ui.huge.form,
+.ui.huge.form .field .dropdown,
+.ui.huge.form .field .dropdown .menu > .item {
+ font-size: 1.42857143rem;
+}
+
+.ui.massive.form,
+.ui.massive.form .field .dropdown,
+.ui.massive.form .field .dropdown .menu > .item {
+ font-size: 1.71428571rem;
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/*******************************
+ Site Overrides
+*******************************/
+/*
+ * # Fomantic - Menu
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Copyright 2015 Contributor
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Standard
+*******************************/
+
+/*--------------
+ Menu
+---------------*/
+
+.ui.menu {
+ display: flex;
+ margin: 1rem 0;
+ font-family: var(--fonts-regular);
+ background: #FFFFFF;
+ font-weight: normal;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
+ border-radius: 0.28571429rem;
+ min-height: 2.85714286em;
+}
+
+.ui.menu:after {
+ content: '';
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.ui.menu:first-child {
+ margin-top: 0;
+}
+
+.ui.menu:last-child {
+ margin-bottom: 0;
+}
+
+/*--------------
+ Sub-Menu
+---------------*/
+
+.ui.menu .menu {
+ margin: 0;
+}
+
+.ui.menu:not(.vertical) > .menu {
+ display: flex;
+}
+
+/*--------------
+ Item
+---------------*/
+
+.ui.menu:not(.vertical) .item {
+ display: flex;
+ align-items: center;
+}
+
+.ui.menu .item {
+ position: relative;
+ vertical-align: middle;
+ line-height: 1;
+ text-decoration: none;
+ -webkit-tap-highlight-color: transparent;
+ flex: 0 0 auto;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ background: none;
+ padding: 0.92857143em 1.14285714em;
+ text-transform: none;
+ color: rgba(0, 0, 0, 0.87);
+ font-weight: normal;
+ transition: background 0.1s ease, box-shadow 0.1s ease, color 0.1s ease;
+}
+
+.ui.menu > .item:first-child {
+ border-radius: 0.28571429rem 0 0 0.28571429rem;
+}
+
+/* Border */
+
+.ui.menu .item:before {
+ position: absolute;
+ content: '';
+ top: 0;
+ right: 0;
+ height: 100%;
+ width: 1px;
+ background: rgba(34, 36, 38, 0.1);
+}
+
+/*--------------
+ Text Content
+---------------*/
+
+.ui.menu .text.item > *,
+.ui.menu .item > a:not(.ui),
+.ui.menu .item > p:only-child {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ user-select: text;
+ line-height: 1.3;
+}
+
+.ui.menu .item > p:first-child {
+ margin-top: 0;
+}
+
+.ui.menu .item > p:last-child {
+ margin-bottom: 0;
+}
+
+/*--------------
+ Icons
+---------------*/
+
+.ui.menu .item > i.icon {
+ opacity: 0.9;
+ float: none;
+ margin: 0 0.35714286em 0 0;
+}
+
+/*--------------
+ Button
+---------------*/
+
+.ui.menu:not(.vertical) .item > .button {
+ position: relative;
+ top: 0;
+ margin: -0.5em 0;
+ padding-bottom: 0.78571429em;
+ padding-top: 0.78571429em;
+ font-size: 1em;
+}
+
+/*----------------
+ Grid / Container
+-----------------*/
+
+.ui.menu > .grid,
+.ui.menu > .container {
+ display: flex;
+ align-items: inherit;
+ flex-direction: inherit;
+}
+
+/*--------------
+ Inputs
+---------------*/
+
+.ui.menu .item > .input {
+ width: 100%;
+}
+
+.ui.menu:not(.vertical) .item > .input {
+ position: relative;
+ top: 0;
+ margin: -0.5em 0;
+}
+
+.ui.menu .item > .input input {
+ font-size: 1em;
+ padding-top: 0.57142857em;
+ padding-bottom: 0.57142857em;
+}
+
+/*--------------
+ Header
+---------------*/
+
+.ui.menu .header.item,
+.ui.vertical.menu .header.item {
+ margin: 0;
+ background: '';
+ text-transform: normal;
+ font-weight: 500;
+}
+
+.ui.vertical.menu .item > .header:not(.ui) {
+ margin: 0 0 0.5em;
+ font-size: 1em;
+ font-weight: 500;
+}
+
+/*--------------
+ Dropdowns
+---------------*/
+
+/* Dropdown Icon */
+
+.ui.menu .item > i.dropdown.icon {
+ padding: 0;
+ float: right;
+ margin: 0 0 0 1em;
+}
+
+/* Menu */
+
+.ui.menu .dropdown.item .menu {
+ min-width: calc(100% - 1px);
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+ background: #FFFFFF;
+ margin: 0 0 0;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
+ flex-direction: column !important;
+}
+
+/* Menu Items */
+
+.ui.menu .ui.dropdown .menu > .item {
+ margin: 0;
+ text-align: left;
+ font-size: 1em !important;
+ padding: 0.78571429em 1.14285714em !important;
+ background: transparent !important;
+ color: rgba(0, 0, 0, 0.87) !important;
+ text-transform: none !important;
+ font-weight: normal !important;
+ box-shadow: none !important;
+ transition: none !important;
+}
+
+.ui.menu .ui.dropdown .menu > .item:hover {
+ background: rgba(0, 0, 0, 0.05) !important;
+ color: rgba(0, 0, 0, 0.95) !important;
+}
+
+.ui.menu .ui.dropdown .menu > .selected.item {
+ background: rgba(0, 0, 0, 0.05) !important;
+ color: rgba(0, 0, 0, 0.95) !important;
+}
+
+.ui.menu .ui.dropdown .menu > .active.item {
+ background: rgba(0, 0, 0, 0.03) !important;
+ font-weight: 500 !important;
+ color: rgba(0, 0, 0, 0.95) !important;
+}
+
+.ui.menu .ui.dropdown.item .menu .item:not(.filtered) {
+ display: block;
+}
+
+.ui.menu .ui.dropdown .menu > .item > .icons,
+.ui.menu .ui.dropdown .menu > .item > i.icon:not(.dropdown) {
+ display: inline-block;
+ font-size: 1em !important;
+ float: none;
+ margin: 0 0.75em 0 0 !important;
+}
+
+/* Secondary */
+
+.ui.secondary.menu .dropdown.item > .menu,
+.ui.text.menu .dropdown.item > .menu {
+ border-radius: 0.28571429rem;
+ margin-top: 0.35714286em;
+}
+
+/* Pointing */
+
+.ui.menu .pointing.dropdown.item .menu {
+ margin-top: 0.75em;
+}
+
+/* Vertical */
+
+.ui.vertical.menu .dropdown.item > i.icon {
+ float: right;
+ content: "\f0da";
+ margin-left: 1em;
+}
+
+.ui.vertical.menu .dropdown.item .menu {
+ left: 100%;
+ /* IE needs 0, all others support max-content to show dropdown icon inline, so keep both settings! */
+ min-width: 0;
+ min-width: -moz-max-content;
+ min-width: max-content;
+ margin: 0 0 0 0;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
+ border-radius: 0 0.28571429rem 0.28571429rem 0.28571429rem;
+}
+
+.ui.vertical.menu .dropdown.item.upward .menu {
+ bottom: 0;
+}
+
+.ui.vertical.menu .dropdown.item:not(.upward) .menu {
+ top: 0;
+}
+
+.ui.vertical.menu .active.dropdown.item {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.ui.vertical.menu .dropdown.active.item {
+ box-shadow: none;
+}
+
+/* Evenly Divided */
+
+.ui.item.menu .dropdown .menu .item {
+ width: 100%;
+}
+
+/*--------------
+ Labels
+---------------*/
+
+.ui.menu .item > .label:not(.floating) {
+ margin-left: 1em;
+ padding: 0.3em 0.78571429em;
+}
+
+.ui.vertical.menu .item > .label {
+ margin-top: -0.15em;
+ margin-bottom: -0.15em;
+ padding: 0.3em 0.78571429em;
+}
+
+.ui.menu .item > .floating.label {
+ padding: 0.3em 0.78571429em;
+}
+
+.ui.menu .item > .label {
+ background: #999999;
+ color: #FFFFFF;
+}
+
+.ui.menu .item > .image.label img {
+ margin: -0.2833em 0.8em -0.2833em -0.8em;
+ height: 1.5666em;
+}
+
+/*--------------
+ Images
+---------------*/
+
+.ui.menu .item > img:not(.ui) {
+ display: inline-block;
+ vertical-align: middle;
+ margin: -0.3em 0;
+ width: 2.5em;
+}
+
+.ui.vertical.menu .item > img:not(.ui):only-child {
+ display: block;
+ max-width: 100%;
+ width: auto;
+}
+
+/*******************************
+ Coupling
+*******************************/
+
+/*--------------
+ List
+---------------*/
+
+/* Menu divider shouldnt apply */
+
+.ui.menu .list .item:before {
+ background: none !important;
+}
+
+/*--------------
+ Sidebar
+ ---------------*/
+
+/* Show vertical dividers below last */
+
+.ui.vertical.sidebar.menu > .item:first-child:before {
+ display: block !important;
+}
+
+.ui.vertical.sidebar.menu > .item::before {
+ top: auto;
+ bottom: 0;
+}
+
+/*--------------
+ Container
+---------------*/
+
+@media only screen and (max-width: 767.98px) {
+ .ui.menu > .ui.container {
+ width: 100% !important;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.menu:not(.secondary):not(.text):not(.tabular):not(.borderless) > .container > .item:not(.right):not(.borderless):first-child {
+ border-left: 1px solid rgba(34, 36, 38, 0.1);
+ }
+
+ .ui.menu:not(.secondary):not(.text):not(.tabular):not(.borderless) > .container > .right.item:not(.borderless):last-child,
+ .ui.menu:not(.secondary):not(.text):not(.tabular):not(.borderless) > .container > .right.menu > .item:not(.borderless):last-child {
+ border-right: 1px solid rgba(34, 36, 38, 0.1);
+ }
+}
+
+/*******************************
+ States
+*******************************/
+
+/*--------------
+ Hover
+---------------*/
+
+.ui.link.menu .item:hover,
+.ui.menu .dropdown.item:hover,
+.ui.menu .link.item:hover,
+.ui.menu a.item:hover {
+ cursor: pointer;
+ background: rgba(0, 0, 0, 0.03);
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--------------
+ Pressed
+---------------*/
+
+.ui.link.menu .item:active,
+.ui.menu .link.item:active,
+.ui.menu a.item:active {
+ background: rgba(0, 0, 0, 0.03);
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--------------
+ Active
+---------------*/
+
+.ui.menu .active.item {
+ background: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+ font-weight: normal;
+ box-shadow: none;
+}
+
+.ui.menu .active.item > i.icon {
+ opacity: 1;
+}
+
+/*--------------
+ Active Hover
+---------------*/
+
+.ui.menu .active.item:hover,
+.ui.vertical.menu .active.item:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--------------
+ Disabled
+---------------*/
+
+.ui.ui.menu .item.disabled {
+ cursor: default;
+ background-color: transparent;
+ color: rgba(40, 40, 40, 0.3);
+ pointer-events: none;
+}
+
+/*******************************
+ Types
+*******************************/
+
+/*------------------
+Floated Menu / Item
+-------------------*/
+
+/* Left Floated */
+
+.ui.menu:not(.vertical) .left.item,
+.ui.menu:not(.vertical) .left.menu {
+ display: flex;
+ margin-right: auto !important;
+}
+
+/* Right Floated */
+
+.ui.menu:not(.vertical) .right.item,
+.ui.menu:not(.vertical) .right.menu {
+ display: flex;
+ margin-left: auto !important;
+}
+
+.ui.menu:not(.vertical) :not(.dropdown) > .left.menu,
+.ui.menu:not(.vertical) :not(.dropdown) > .right.menu {
+ display: inherit;
+}
+
+/* Center */
+
+.ui.menu:not(.vertical) .center.item,
+.ui.menu:not(.vertical) .center.menu {
+ display: flex;
+ margin-left: auto !important;
+ margin-right: auto !important;
+}
+
+/* Swapped Borders */
+
+.ui.menu .right.item::before,
+.ui.menu .right.menu > .item::before {
+ right: auto;
+ left: 0;
+}
+
+/* Remove Outer Borders */
+
+.ui.menu .center.item:last-child::before,
+.ui.menu .center.menu > .item:last-child::before {
+ display: none;
+}
+
+/*--------------
+ Vertical
+ ---------------*/
+
+.ui.vertical.menu {
+ display: block;
+ flex-direction: column;
+ background: #FFFFFF;
+ box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
+}
+
+/*--- Item ---*/
+
+.ui.vertical.menu .item {
+ display: block;
+ background: none;
+ border-top: none;
+ border-right: none;
+}
+
+.ui.vertical.menu > .item:first-child {
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.vertical.menu > .item:last-child {
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+/*--- Label ---*/
+
+.ui.vertical.menu .item > .label {
+ float: right;
+ text-align: center;
+}
+
+/*--- Icon ---*/
+
+.ui.vertical.menu .item > i.icon,
+.ui.vertical.menu .item > i.icons {
+ width: 1.18em;
+ float: right;
+ margin: 0 0 0 0.5em;
+}
+
+.ui.vertical.menu .item > .label + i.icon {
+ float: none;
+ margin: 0 0.5em 0 0;
+}
+
+/*--- Border ---*/
+
+.ui.vertical.menu .item:before {
+ position: absolute;
+ content: '';
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 1px;
+ background: rgba(34, 36, 38, 0.1);
+}
+
+.ui.vertical.menu .item:first-child:before {
+ display: none !important;
+}
+
+/*--- Sub Menu ---*/
+
+.ui.vertical.menu .item > .menu {
+ margin: 0.5em -1.14285714em 0;
+}
+
+.ui.vertical.menu .menu .item {
+ background: none;
+ padding: 0.5em 1.33333333em;
+ font-size: 0.85714286em;
+ color: rgba(0, 0, 0, 0.5);
+}
+
+.ui.vertical.menu .item .menu a.item:hover,
+.ui.vertical.menu .item .menu .link.item:hover {
+ color: rgba(0, 0, 0, 0.85);
+}
+
+.ui.vertical.menu .menu .item:before {
+ display: none;
+}
+
+/* Vertical Active */
+
+.ui.vertical.menu .active.item {
+ background: rgba(0, 0, 0, 0.05);
+ border-radius: 0;
+ box-shadow: none;
+}
+
+.ui.vertical.menu > .active.item:first-child {
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.vertical.menu > .active.item:last-child {
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui.vertical.menu > .active.item:only-child {
+ border-radius: 0.28571429rem;
+}
+
+.ui.vertical.menu .active.item .menu .active.item {
+ border-left: none;
+}
+
+.ui.vertical.menu .item .menu .active.item {
+ background-color: transparent;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--------------
+ Tabular
+ ---------------*/
+
+.ui.tabular.menu {
+ border-radius: 0;
+ box-shadow: none !important;
+ border: none;
+ background: none transparent;
+ border-bottom: 1px solid #D4D4D5;
+}
+
+.ui.tabular.fluid.menu {
+ width: calc(100% + 2px) !important;
+}
+
+.ui.tabular.menu .item {
+ background: transparent;
+ border-bottom: none;
+ border-left: 1px solid transparent;
+ border-right: 1px solid transparent;
+ border-top: 2px solid transparent;
+ padding: 0.92857143em 1.42857143em;
+ color: rgba(0, 0, 0, 0.87);
+}
+
+.ui.tabular.menu .item:before {
+ display: none;
+}
+
+/* Hover */
+
+.ui.tabular.menu .item:hover {
+ background-color: transparent;
+ color: rgba(0, 0, 0, 0.8);
+}
+
+/* Active */
+
+.ui.tabular.menu .active.item {
+ background: none #FFFFFF;
+ color: rgba(0, 0, 0, 0.95);
+ border-top-width: 1px;
+ border-color: #D4D4D5;
+ font-weight: 500;
+ margin-bottom: -1px;
+ box-shadow: none;
+ border-radius: 0.28571429rem 0.28571429rem 0 0 !important;
+}
+
+/* Coupling with segment for attachment */
+
+.ui.tabular.menu + .attached:not(.top).segment,
+.ui.tabular.menu + .attached:not(.top).segment + .attached:not(.top).segment {
+ border-top: none;
+ margin-left: 0;
+ margin-top: 0;
+ margin-right: 0;
+ width: 100%;
+}
+
+.top.attached.segment + .ui.bottom.tabular.menu {
+ position: relative;
+ width: calc(100% + 2px);
+ left: -1px;
+}
+
+/* Bottom Vertical Tabular */
+
+.ui.bottom.tabular.menu {
+ background: none transparent;
+ border-radius: 0;
+ box-shadow: none !important;
+ border-bottom: none;
+ border-top: 1px solid #D4D4D5;
+}
+
+.ui.bottom.tabular.menu .item {
+ background: none;
+ border-left: 1px solid transparent;
+ border-right: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-top: none;
+}
+
+.ui.bottom.tabular.menu .active.item {
+ background: none #FFFFFF;
+ color: rgba(0, 0, 0, 0.95);
+ border-color: #D4D4D5;
+ margin: -1px 0 0 0;
+ border-radius: 0 0 0.28571429rem 0.28571429rem !important;
+}
+
+/* Vertical Tabular (Left) */
+
+.ui.vertical.tabular.menu {
+ background: none transparent;
+ border-radius: 0;
+ box-shadow: none !important;
+ border-bottom: none;
+ border-right: 1px solid #D4D4D5;
+}
+
+.ui.vertical.tabular.menu .item {
+ background: none;
+ border-left: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-top: 1px solid transparent;
+ border-right: none;
+}
+
+.ui.vertical.tabular.menu .active.item {
+ background: none #FFFFFF;
+ color: rgba(0, 0, 0, 0.95);
+ border-color: #D4D4D5;
+ margin: 0 -1px 0 0;
+ border-radius: 0.28571429rem 0 0 0.28571429rem !important;
+}
+
+/* Vertical Right Tabular */
+
+.ui.vertical.right.tabular.menu {
+ background: none transparent;
+ border-radius: 0;
+ box-shadow: none !important;
+ border-bottom: none;
+ border-right: none;
+ border-left: 1px solid #D4D4D5;
+}
+
+.ui.vertical.right.tabular.menu .item {
+ background: none;
+ border-right: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-top: 1px solid transparent;
+ border-left: none;
+}
+
+.ui.vertical.right.tabular.menu .active.item {
+ background: none #FFFFFF;
+ color: rgba(0, 0, 0, 0.95);
+ border-color: #D4D4D5;
+ margin: 0 0 0 -1px;
+ border-radius: 0 0.28571429rem 0.28571429rem 0 !important;
+}
+
+/* Dropdown */
+
+.ui.tabular.menu .active.dropdown.item {
+ margin-bottom: 0;
+ border-left: 1px solid transparent;
+ border-right: 1px solid transparent;
+ border-top: 2px solid transparent;
+ border-bottom: none;
+}
+
+/*--------------
+ Pagination
+ ---------------*/
+
+.ui.pagination.menu {
+ margin: 0;
+ display: inline-flex;
+ vertical-align: middle;
+}
+
+.ui.pagination.menu .item:last-child {
+ border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.compact.menu .item:last-child {
+ border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.pagination.menu .item:last-child:before {
+ display: none;
+}
+
+.ui.pagination.menu .item {
+ min-width: 3em;
+ text-align: center;
+}
+
+.ui.pagination.menu .icon.item i.icon {
+ vertical-align: top;
+}
+
+/* Active */
+
+.ui.pagination.menu .active.item {
+ border-top: none;
+ padding-top: 0.92857143em;
+ background-color: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+ box-shadow: none;
+}
+
+/*--------------
+ Secondary
+ ---------------*/
+
+.ui.secondary.menu {
+ background: none;
+ margin-left: -0.35714286em;
+ margin-right: -0.35714286em;
+ border-radius: 0;
+ border: none;
+ box-shadow: none;
+}
+
+/* Item */
+
+.ui.secondary.menu .item {
+ align-self: center;
+ box-shadow: none;
+ border: none;
+ padding: 0.78571429em 0.92857143em;
+ margin: 0 0.35714286em;
+ background: none;
+ transition: color 0.1s ease;
+ border-radius: 0.28571429rem;
+}
+
+/* No Divider */
+
+.ui.secondary.menu .item:before {
+ display: none !important;
+}
+
+/* Header */
+
+.ui.secondary.menu .header.item {
+ border-radius: 0;
+ border-right: none;
+ background: none transparent;
+}
+
+/* Image */
+
+.ui.secondary.menu .item > img:not(.ui) {
+ margin: 0;
+}
+
+/* Hover */
+
+.ui.secondary.menu .dropdown.item:hover,
+.ui.secondary.menu .link.item:hover,
+.ui.secondary.menu a.item:hover {
+ background: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/* Active */
+
+.ui.secondary.menu .active.item {
+ box-shadow: none;
+ background: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+ border-radius: 0.28571429rem;
+}
+
+/* Active Hover */
+
+.ui.secondary.menu .active.item:hover {
+ box-shadow: none;
+ background: rgba(0, 0, 0, 0.05);
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/* Fix item margins */
+
+.ui.secondary.item.menu {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.ui.secondary.item.menu .item:last-child {
+ margin-right: 0;
+}
+
+.ui.secondary.attached.menu {
+ box-shadow: none;
+}
+
+/*---------------------
+ Secondary Vertical
+ -----------------------*/
+
+/* Sub Menu */
+
+.ui.vertical.secondary.menu .item:not(.dropdown) > .menu {
+ margin: 0 -0.92857143em;
+}
+
+.ui.vertical.secondary.menu .item:not(.dropdown) > .menu > .item {
+ margin: 0;
+ padding: 0.5em 1.33333333em;
+}
+
+.ui.secondary.vertical.menu > .item {
+ border: none;
+ margin: 0 0 0.35714286em;
+ border-radius: 0.28571429rem !important;
+}
+
+.ui.secondary.vertical.menu > .header.item {
+ border-radius: 0;
+}
+
+/* Sub Menu */
+
+.ui.vertical.secondary.menu .item > .menu .item {
+ background-color: transparent;
+}
+
+/* Inverted */
+
+.ui.secondary.inverted.menu {
+ background-color: transparent;
+}
+
+/*---------------------
+ Secondary Pointing
+ -----------------------*/
+
+.ui.secondary.pointing.menu {
+ margin-left: 0;
+ margin-right: 0;
+ border-bottom: 2px solid rgba(34, 36, 38, 0.15);
+}
+
+.ui.secondary.pointing.menu .item {
+ border-bottom-color: transparent;
+ border-bottom-style: solid;
+ border-radius: 0;
+ align-self: flex-end;
+ margin: 0 0 -2px;
+ padding: 0.85714286em 1.14285714em;
+ border-bottom-width: 2px;
+ transition: color 0.1s ease;
+}
+
+.ui.secondary.pointing.menu .ui.dropdown .menu .item {
+ border-bottom-width: 0;
+}
+
+.ui.secondary.pointing.menu .item > .label:not(.floating) {
+ margin-top: -0.3em;
+ margin-bottom: -0.3em;
+}
+
+.ui.secondary.pointing.menu .item > .circular.label {
+ margin-top: -0.5em;
+ margin-bottom: -0.5em;
+}
+
+/* Item Types */
+
+.ui.secondary.pointing.menu .header.item {
+ color: rgba(0, 0, 0, 0.85) !important;
+}
+
+.ui.secondary.pointing.menu .text.item {
+ box-shadow: none !important;
+}
+
+.ui.secondary.pointing.menu .item:after {
+ display: none;
+}
+
+/* Hover */
+
+.ui.secondary.pointing.menu .dropdown.item:hover,
+.ui.secondary.pointing.menu .link.item:hover,
+.ui.secondary.pointing.menu a.item:hover {
+ background-color: transparent;
+ color: rgba(0, 0, 0, 0.87);
+}
+
+/* Pressed */
+
+.ui.secondary.pointing.menu .dropdown.item:active,
+.ui.secondary.pointing.menu .link.item:active,
+.ui.secondary.pointing.menu a.item:active {
+ background-color: transparent;
+ border-color: rgba(34, 36, 38, 0.15);
+}
+
+/* Active */
+
+.ui.secondary.pointing.menu .active.item {
+ background-color: transparent;
+ box-shadow: none;
+ border-color: currentColor;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/* Active Hover */
+
+.ui.secondary.pointing.menu .active.item:hover {
+ border-color: currentColor;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/* Active Dropdown */
+
+.ui.secondary.pointing.menu .active.dropdown.item {
+ border-color: transparent;
+}
+
+/* Vertical Pointing */
+
+.ui.secondary.vertical.pointing.menu {
+ border-bottom-width: 0;
+ border-right-width: 2px;
+ border-right-style: solid;
+ border-right-color: rgba(34, 36, 38, 0.15);
+}
+
+.ui.secondary.vertical.pointing.menu .item {
+ border-bottom: none;
+ border-right-style: solid;
+ border-right-color: transparent;
+ border-radius: 0 !important;
+ margin: 0 -2px 0 0;
+ border-right-width: 2px;
+}
+
+/* Vertical Active */
+
+.ui.secondary.vertical.pointing.menu .active.item {
+ border-color: currentColor;
+}
+
+/*--------------
+ Text Menu
+ ---------------*/
+
+.ui.text.menu {
+ background: none transparent;
+ border-radius: 0;
+ box-shadow: none;
+ border: none;
+ margin: 1em -0.5em;
+}
+
+.ui.text.menu .item {
+ border-radius: 0;
+ box-shadow: none;
+ align-self: center;
+ margin: 0 0;
+ padding: 0.35714286em 0.5em;
+ font-weight: normal;
+ color: rgba(0, 0, 0, 0.6);
+ transition: opacity 0.1s ease;
+}
+
+/* Border */
+
+.ui.text.menu .item:before,
+.ui.text.menu .menu .item:before {
+ display: none !important;
+}
+
+/* Header */
+
+.ui.text.menu .header.item {
+ background-color: transparent;
+ opacity: 1;
+ color: rgba(0, 0, 0, 0.85);
+ font-size: 0.92857143em;
+ text-transform: uppercase;
+ font-weight: 500;
+}
+
+/* Image */
+
+.ui.text.menu .item > img:not(.ui) {
+ margin: 0;
+}
+
+/*--- fluid text ---*/
+
+.ui.text.item.menu .item {
+ margin: 0;
+}
+
+/*--- vertical text ---*/
+
+.ui.vertical.text.menu {
+ margin: 1em 0;
+}
+
+.ui.vertical.text.menu:first-child {
+ margin-top: 0;
+}
+
+.ui.vertical.text.menu:last-child {
+ margin-bottom: 0;
+}
+
+.ui.vertical.text.menu .item {
+ margin: 0.57142857em 0;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.ui.vertical.text.menu .item > i.icon {
+ float: none;
+ margin: 0 0.35714286em 0 0;
+}
+
+.ui.vertical.text.menu .header.item {
+ margin: 0.57142857em 0 0.71428571em;
+}
+
+/* Vertical Sub Menu */
+
+.ui.vertical.text.menu .item:not(.dropdown) > .menu {
+ margin: 0;
+}
+
+.ui.vertical.text.menu .item:not(.dropdown) > .menu > .item {
+ margin: 0;
+ padding: 0.5em 0;
+}
+
+/*--- hover ---*/
+
+.ui.text.menu .item:hover {
+ opacity: 1;
+ background-color: transparent;
+}
+
+/*--- active ---*/
+
+.ui.text.menu .active.item {
+ background-color: transparent;
+ border: none;
+ box-shadow: none;
+ font-weight: normal;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--- active hover ---*/
+
+.ui.text.menu .active.item:hover {
+ background-color: transparent;
+}
+
+/* Disable Bariations */
+
+.ui.text.pointing.menu .active.item:after {
+ box-shadow: none;
+}
+
+.ui.text.attached.menu {
+ box-shadow: none;
+}
+
+/* Fluid */
+
+.ui.fluid.text.menu {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+/*--------------
+ Icon Only
+---------------*/
+
+/* Vertical Menu */
+
+.ui.vertical.icon.menu {
+ display: inline-block;
+ width: auto;
+}
+
+/* Item */
+
+.ui.icon.menu .item {
+ height: auto;
+ text-align: center;
+ color: #1B1C1D;
+}
+
+/* Icon */
+
+.ui.icon.menu .item > i.icon:not(.dropdown) {
+ margin: 0;
+ opacity: 1;
+}
+
+/* Icon Gylph */
+
+.ui.icon.menu i.icon:before {
+ opacity: 1;
+}
+
+/* (x) Item Icon */
+
+.ui.menu .icon.item > i.icon {
+ width: auto;
+ margin: 0 auto;
+}
+
+/* Vertical Icon */
+
+.ui.vertical.icon.menu .item > i.icon:not(.dropdown) {
+ display: block;
+ opacity: 1;
+ margin: 0 auto;
+ float: none;
+}
+
+/* Inverted */
+
+.ui.inverted.icon.menu .item {
+ color: #FFFFFF;
+}
+
+/*--------------
+ Labeled Icon
+ ---------------*/
+
+/* Menu */
+
+.ui.labeled.icon.menu {
+ text-align: center;
+}
+
+/* Item */
+
+.ui.labeled.icon.menu .item {
+ min-width: 6em;
+ flex-direction: column;
+}
+
+/* Icon */
+
+.ui.labeled.icon.menu > .item > i.icon:not(.dropdown) {
+ height: 1em;
+ display: block;
+ font-size: 1.71428571em !important;
+ margin: 0 auto 0.5rem !important;
+}
+
+/* Fluid */
+
+.ui.fluid.labeled.icon.menu > .item {
+ min-width: 0;
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*--------------
+ Stackable
+ ---------------*/
+
+@media only screen and (max-width: 767.98px) {
+ .ui.stackable.menu {
+ flex-direction: column;
+ }
+
+ .ui.stackable.menu .item {
+ width: 100% !important;
+ }
+
+ .ui.stackable.menu .item:before {
+ position: absolute;
+ content: '';
+ top: auto;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 1px;
+ background: rgba(34, 36, 38, 0.1);
+ }
+
+ .ui.stackable.menu .left.menu,
+ .ui.stackable.menu .left.item {
+ margin-right: 0 !important;
+ }
+
+ .ui.stackable.menu .right.menu,
+ .ui.stackable.menu .right.item {
+ margin-left: 0 !important;
+ }
+
+ .ui.stackable.menu .center.menu,
+ .ui.stackable.menu .center.item {
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ }
+
+ .ui.stackable.menu .right.menu,
+ .ui.stackable.menu .center.menu,
+ .ui.stackable.menu .left.menu {
+ flex-direction: column;
+ }
+}
+
+/*--------------
+ Colors
+---------------*/
+
+.ui.ui.ui.menu .primary.active.item,
+.ui.ui.primary.menu .active.item:hover,
+.ui.ui.primary.menu .active.item {
+ color: #2185D0;
+}
+
+.ui.ui.ui.menu .red.active.item,
+.ui.ui.red.menu .active.item:hover,
+.ui.ui.red.menu .active.item {
+ color: #DB2828;
+}
+
+.ui.ui.ui.menu .orange.active.item,
+.ui.ui.orange.menu .active.item:hover,
+.ui.ui.orange.menu .active.item {
+ color: #F2711C;
+}
+
+.ui.ui.ui.menu .yellow.active.item,
+.ui.ui.yellow.menu .active.item:hover,
+.ui.ui.yellow.menu .active.item {
+ color: #FBBD08;
+}
+
+.ui.ui.ui.menu .olive.active.item,
+.ui.ui.olive.menu .active.item:hover,
+.ui.ui.olive.menu .active.item {
+ color: #B5CC18;
+}
+
+.ui.ui.ui.menu .green.active.item,
+.ui.ui.green.menu .active.item:hover,
+.ui.ui.green.menu .active.item {
+ color: #21BA45;
+}
+
+.ui.ui.ui.menu .teal.active.item,
+.ui.ui.teal.menu .active.item:hover,
+.ui.ui.teal.menu .active.item {
+ color: #00B5AD;
+}
+
+.ui.ui.ui.menu .blue.active.item,
+.ui.ui.blue.menu .active.item:hover,
+.ui.ui.blue.menu .active.item {
+ color: #2185D0;
+}
+
+.ui.ui.ui.menu .violet.active.item,
+.ui.ui.violet.menu .active.item:hover,
+.ui.ui.violet.menu .active.item {
+ color: #6435C9;
+}
+
+.ui.ui.ui.menu .purple.active.item,
+.ui.ui.purple.menu .active.item:hover,
+.ui.ui.purple.menu .active.item {
+ color: #A333C8;
+}
+
+.ui.ui.ui.menu .pink.active.item,
+.ui.ui.pink.menu .active.item:hover,
+.ui.ui.pink.menu .active.item {
+ color: #E03997;
+}
+
+.ui.ui.ui.menu .brown.active.item,
+.ui.ui.brown.menu .active.item:hover,
+.ui.ui.brown.menu .active.item {
+ color: #A5673F;
+}
+
+.ui.ui.ui.menu .grey.active.item,
+.ui.ui.grey.menu .active.item:hover,
+.ui.ui.grey.menu .active.item {
+ color: #767676;
+}
+
+.ui.ui.ui.menu .black.active.item,
+.ui.ui.black.menu .active.item:hover,
+.ui.ui.black.menu .active.item {
+ color: #1B1C1D;
+}
+
+/*--------------
+ Floated
+ ---------------*/
+
+.ui.floated.menu {
+ float: left;
+ margin: 0 0.5rem 0 0;
+}
+
+.ui.floated.menu .item:last-child:before {
+ display: none;
+}
+
+.ui.right.floated.menu {
+ float: right;
+ margin: 0 0 0 0.5rem;
+}
+
+/*--------------
+ Fitted
+ ---------------*/
+
+.ui.fitted.menu .item,
+.ui.fitted.menu .item .menu .item,
+.ui.menu .fitted.item {
+ padding: 0;
+}
+
+.ui.horizontally.fitted.menu .item,
+.ui.horizontally.fitted.menu .item .menu .item,
+.ui.menu .horizontally.fitted.item {
+ padding-top: 0.92857143em;
+ padding-bottom: 0.92857143em;
+}
+
+.ui.vertically.fitted.menu .item,
+.ui.vertically.fitted.menu .item .menu .item,
+.ui.menu .vertically.fitted.item {
+ padding-left: 1.14285714em;
+ padding-right: 1.14285714em;
+}
+
+/*--------------
+ Borderless
+ ---------------*/
+
+.ui.borderless.menu .item:before,
+.ui.borderless.menu .item .menu .item:before,
+.ui.menu .borderless.item:before {
+ background: none !important;
+}
+
+/*-------------------
+ Compact
+ --------------------*/
+
+.ui.compact.menu {
+ display: inline-flex;
+ margin: 0;
+ vertical-align: middle;
+}
+
+.ui.compact.vertical.menu {
+ /* IE hack to make dropdown icons appear inline */
+ display: -ms-inline-flexbox !important;
+ display: inline-block;
+}
+
+.ui.compact.menu:not(.secondary) .item:last-child {
+ border-radius: 0 0.28571429rem 0.28571429rem 0;
+}
+
+.ui.compact.menu .item:last-child:before {
+ display: none;
+}
+
+.ui.compact.vertical.menu {
+ width: auto !important;
+}
+
+.ui.compact.vertical.menu .item:last-child::before {
+ display: block;
+}
+
+/*-------------------
+ Fluid
+ --------------------*/
+
+.ui.menu.fluid,
+.ui.vertical.menu.fluid {
+ width: 100% !important;
+}
+
+/*-------------------
+ Evenly Sized
+--------------------*/
+
+.ui.item.menu,
+.ui.item.menu .item {
+ width: 100%;
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ text-align: center;
+ justify-content: center;
+}
+
+.ui.attached.item.menu:not(.tabular) {
+ margin: 0 -1px !important;
+}
+
+.ui.item.menu .item:last-child:before {
+ display: none;
+}
+
+.ui.menu.two.item .item {
+ width: 50%;
+}
+
+.ui.menu.three.item .item {
+ width: 33.333%;
+}
+
+.ui.menu.four.item .item {
+ width: 25%;
+}
+
+.ui.menu.five.item .item {
+ width: 20%;
+}
+
+.ui.menu.six.item .item {
+ width: 16.666%;
+}
+
+.ui.menu.seven.item .item {
+ width: 14.285%;
+}
+
+.ui.menu.eight.item .item {
+ width: 12.5%;
+}
+
+.ui.menu.nine.item .item {
+ width: 11.11%;
+}
+
+.ui.menu.ten.item .item {
+ width: 10%;
+}
+
+.ui.menu.eleven.item .item {
+ width: 9.09%;
+}
+
+.ui.menu.twelve.item .item {
+ width: 8.333%;
+}
+
+/*--------------
+ Fixed
+ ---------------*/
+
+.ui.menu.fixed {
+ position: fixed;
+ z-index: 101;
+ margin: 0;
+ width: 100%;
+}
+
+.ui.menu.fixed,
+.ui.menu.fixed .item:first-child,
+.ui.menu.fixed .item:last-child {
+ border-radius: 0 !important;
+}
+
+.ui.fixed.menu,
+.ui[class*="top fixed"].menu {
+ top: 0;
+ left: 0;
+ right: auto;
+ bottom: auto;
+}
+
+.ui[class*="top fixed"].menu {
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+.ui[class*="right fixed"].menu {
+ border-top: none;
+ border-bottom: none;
+ border-right: none;
+ top: 0;
+ right: 0;
+ left: auto;
+ bottom: auto;
+ width: auto;
+ height: 100%;
+}
+
+.ui[class*="bottom fixed"].menu {
+ border-bottom: none;
+ border-left: none;
+ border-right: none;
+ bottom: 0;
+ left: 0;
+ top: auto;
+ right: auto;
+}
+
+.ui[class*="left fixed"].menu {
+ border-top: none;
+ border-bottom: none;
+ border-left: none;
+ top: 0;
+ left: 0;
+ right: auto;
+ bottom: auto;
+ width: auto;
+ height: 100%;
+}
+
+/* Coupling with Grid */
+
+.ui.fixed.menu + .ui.grid {
+ padding-top: 2.75rem;
+}
+
+/*-------------------
+ Pointing
+ --------------------*/
+
+.ui.pointing.menu .item:after {
+ visibility: hidden;
+ position: absolute;
+ content: '';
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-50%) rotate(45deg);
+ background: none;
+ margin: 0.5px 0 0;
+ width: 0.57142857em;
+ height: 0.57142857em;
+ border: none;
+ border-bottom: 1px solid #D4D4D5;
+ border-right: 1px solid #D4D4D5;
+ z-index: 2;
+ transition: background 0.1s ease;
+}
+
+.ui.vertical.pointing.menu .item:after {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ bottom: auto;
+ left: auto;
+ transform: translateX(50%) translateY(-50%) rotate(45deg);
+ margin: 0 -0.5px 0 0;
+ border: none;
+ border-top: 1px solid #D4D4D5;
+ border-right: 1px solid #D4D4D5;
+}
+
+.ui.pointing.menu .ui.dropdown .menu .item:after,
+.ui.vertical.pointing.menu .ui.dropdown .menu .item:after {
+ display: none;
+}
+
+/* Active */
+
+.ui.pointing.menu .active.item:after {
+ visibility: visible;
+}
+
+.ui.pointing.menu .active.dropdown.item:after {
+ visibility: hidden;
+}
+
+/* Don't double up pointers */
+
+.ui.pointing.menu .dropdown.active.item:after,
+.ui.pointing.menu .active.item .menu .active.item:after {
+ display: none;
+}
+
+/* Colors */
+
+.ui.pointing.menu .active.item:hover:after {
+ background-color: #F2F2F2;
+}
+
+.ui.pointing.menu .active.item:after {
+ background-color: #F2F2F2;
+}
+
+.ui.pointing.menu .active.item:hover:after {
+ background-color: #F2F2F2;
+}
+
+.ui.vertical.pointing.menu .active.item:hover:after {
+ background-color: #F2F2F2;
+}
+
+.ui.vertical.pointing.menu .active.item:after {
+ background-color: #F2F2F2;
+}
+
+.ui.vertical.pointing.menu .menu .active.item:after {
+ background-color: #FFFFFF;
+}
+
+.ui.inverted.pointing.menu .primary.active.item:after {
+ background-color: #2185D0;
+}
+
+.ui.inverted.pointing.menu .secondary.active.item:after {
+ background-color: #1B1C1D;
+}
+
+.ui.inverted.pointing.menu .red.active.item:after {
+ background-color: #DB2828;
+}
+
+.ui.inverted.pointing.menu .orange.active.item:after {
+ background-color: #F2711C;
+}
+
+.ui.inverted.pointing.menu .yellow.active.item:after {
+ background-color: #FBBD08;
+}
+
+.ui.inverted.pointing.menu .olive.active.item:after {
+ background-color: #B5CC18;
+}
+
+.ui.inverted.pointing.menu .green.active.item:after {
+ background-color: #21BA45;
+}
+
+.ui.inverted.pointing.menu .teal.active.item:after {
+ background-color: #00B5AD;
+}
+
+.ui.inverted.pointing.menu .blue.active.item:after {
+ background-color: #2185D0;
+}
+
+.ui.inverted.pointing.menu .violet.active.item:after {
+ background-color: #6435C9;
+}
+
+.ui.inverted.pointing.menu .purple.active.item:after {
+ background-color: #A333C8;
+}
+
+.ui.inverted.pointing.menu .pink.active.item:after {
+ background-color: #E03997;
+}
+
+.ui.inverted.pointing.menu .brown.active.item:after {
+ background-color: #A5673F;
+}
+
+.ui.inverted.pointing.menu .grey.active.item:after {
+ background-color: #767676;
+}
+
+.ui.inverted.pointing.menu .black.active.item:after {
+ background-color: #1B1C1D;
+}
+
+/*--------------
+ Attached
+ ---------------*/
+
+/* Middle */
+
+.ui.attached.menu {
+ top: 0;
+ bottom: 0;
+ border-radius: 0;
+ margin: 0 -1px;
+ width: calc(100% + 2px);
+ max-width: calc(100% + 2px);
+ box-shadow: none;
+}
+
+.ui.attached + .ui.attached.menu:not(.top) {
+ border-top: none;
+}
+
+/* Top */
+
+.ui[class*="top attached"].menu {
+ bottom: 0;
+ margin-bottom: 0;
+ top: 0;
+ margin-top: 1rem;
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.menu[class*="top attached"]:first-child {
+ margin-top: 0;
+}
+
+/* Bottom */
+
+.ui[class*="bottom attached"].menu {
+ bottom: 0;
+ margin-top: 0;
+ top: 0;
+ margin-bottom: 1rem;
+ box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none;
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+.ui[class*="bottom attached"].menu:last-child {
+ margin-bottom: 0;
+}
+
+/* Attached Menu Item */
+
+.ui.top.attached.menu > .item:first-child {
+ border-radius: 0.28571429rem 0 0 0;
+}
+
+.ui.bottom.attached.menu > .item:first-child {
+ border-radius: 0 0 0 0.28571429rem;
+}
+
+/* Tabular Attached */
+
+.ui.attached.menu:not(.tabular) {
+ border: 1px solid #D4D4D5;
+}
+
+.ui.attached.tabular.menu {
+ margin-left: 0;
+ margin-right: 0;
+ width: 100%;
+}
+
+/*--------------
+ Sizes
+---------------*/
+
+.ui.menu {
+ font-size: 1rem;
+}
+
+.ui.vertical.menu {
+ width: 15rem;
+}
+
+.ui.mini.menu,
+.ui.mini.menu .dropdown,
+.ui.mini.menu .dropdown .menu > .item {
+ font-size: 0.78571429rem;
+}
+
+.ui.mini.vertical.menu:not(.icon) {
+ width: 9rem;
+}
+
+.ui.tiny.menu,
+.ui.tiny.menu .dropdown,
+.ui.tiny.menu .dropdown .menu > .item {
+ font-size: 0.85714286rem;
+}
+
+.ui.tiny.vertical.menu:not(.icon) {
+ width: 11rem;
+}
+
+.ui.small.menu,
+.ui.small.menu .dropdown,
+.ui.small.menu .dropdown .menu > .item {
+ font-size: 0.92857143rem;
+}
+
+.ui.small.vertical.menu:not(.icon) {
+ width: 13rem;
+}
+
+.ui.large.menu,
+.ui.large.menu .dropdown,
+.ui.large.menu .dropdown .menu > .item {
+ font-size: 1.07142857rem;
+}
+
+.ui.large.vertical.menu:not(.icon) {
+ width: 18rem;
+}
+
+.ui.big.menu,
+.ui.big.menu .dropdown,
+.ui.big.menu .dropdown .menu > .item {
+ font-size: 1.14285714rem;
+}
+
+.ui.big.vertical.menu:not(.icon) {
+ width: 20rem;
+}
+
+.ui.huge.menu,
+.ui.huge.menu .dropdown,
+.ui.huge.menu .dropdown .menu > .item {
+ font-size: 1.21428571rem;
+}
+
+.ui.huge.vertical.menu:not(.icon) {
+ width: 22rem;
+}
+
+.ui.massive.menu,
+.ui.massive.menu .dropdown,
+.ui.massive.menu .dropdown .menu > .item {
+ font-size: 1.28571429rem;
+}
+
+.ui.massive.vertical.menu:not(.icon) {
+ width: 25rem;
+}
+
+/*-------------------
+ Inverted dropdowns
+--------------------*/
+
+.ui.menu .ui.inverted.inverted.dropdown.item .menu {
+ background: #1B1C1D;
+ box-shadow: none;
+}
+
+.ui.menu .ui.inverted.dropdown .menu > .item {
+ color: rgba(255, 255, 255, 0.8) !important;
+}
+
+.ui.menu .ui.inverted.dropdown .menu > .active.item {
+ background: transparent !important;
+ color: rgba(255, 255, 255, 0.8) !important;
+}
+
+.ui.menu .ui.inverted.dropdown .menu > .item:hover {
+ background: rgba(255, 255, 255, 0.08) !important;
+ color: rgba(255, 255, 255, 0.8) !important;
+}
+
+.ui.menu .ui.inverted.dropdown .menu > .selected.item {
+ background: rgba(255, 255, 255, 0.15) !important;
+ color: rgba(255, 255, 255, 0.8) !important;
+}
+
+/* Vertical */
+
+.ui.vertical.menu .inverted.dropdown.item .menu {
+ box-shadow: none;
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/*******************************
+ Site Overrides
+*******************************/
+/*!
+ * # Fomantic-UI - Modal
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Modal
+*******************************/
+
+.ui.modal {
+ position: absolute;
+ display: none;
+ z-index: 1001;
+ text-align: left;
+ background: #FFFFFF;
+ border: none;
+ box-shadow: 1px 3px 3px 0 rgba(0, 0, 0, 0.2), 1px 3px 15px 2px rgba(0, 0, 0, 0.2);
+ transform-origin: 50% 25%;
+ flex: 0 0 auto;
+ border-radius: 0.28571429rem;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ user-select: text;
+ will-change: top, left, margin, transform, opacity;
+}
+
+.ui.modal > :first-child:not(.icon):not(.dimmer),
+.ui.modal > i.icon:first-child + *,
+.ui.modal > .dimmer:first-child + *:not(.icon),
+.ui.modal > .dimmer:first-child + i.icon + * {
+ border-top-left-radius: 0.28571429rem;
+ border-top-right-radius: 0.28571429rem;
+}
+
+.ui.modal > :last-child {
+ border-bottom-left-radius: 0.28571429rem;
+ border-bottom-right-radius: 0.28571429rem;
+}
+
+.ui.modal > .ui.dimmer {
+ border-radius: inherit;
+}
+
+/*******************************
+ Content
+*******************************/
+
+/*--------------
+ Close
+---------------*/
+
+.ui.modal > .close {
+ cursor: pointer;
+ position: absolute;
+ top: -2.5rem;
+ right: -2.5rem;
+ z-index: 1;
+ opacity: 0.8;
+ font-size: 1.25em;
+ color: #FFFFFF;
+ width: 2.25rem;
+ height: 2.25rem;
+ padding: 0.625rem 0 0 0;
+}
+
+.ui.modal > .close:hover {
+ opacity: 1;
+}
+
+/*--------------
+ Header
+---------------*/
+
+.ui.modal > .header {
+ display: block;
+ font-family: var(--fonts-regular);
+ background: #FFFFFF;
+ margin: 0;
+ padding: 1.25rem 1.5rem;
+ box-shadow: none;
+ color: rgba(0, 0, 0, 0.85);
+ border-bottom: 1px solid rgba(34, 36, 38, 0.15);
+}
+
+.ui.modal > .header:not(.ui) {
+ font-size: 1.42857143rem;
+ line-height: 1.28571429em;
+ font-weight: 500;
+}
+
+/*--------------
+ Content
+---------------*/
+
+.ui.modal > .content {
+ display: block;
+ width: 100%;
+ font-size: 1em;
+ line-height: 1.4;
+ padding: 1.5rem;
+ background: #FFFFFF;
+}
+
+.ui.modal > .image.content {
+ display: flex;
+ flex-direction: row;
+}
+
+/* Image */
+
+.ui.modal > .content > .image {
+ display: block;
+ flex: 0 1 auto;
+ width: '';
+ align-self: start;
+ max-width: 100%;
+}
+
+.ui.modal > [class*="top aligned"] {
+ align-self: start;
+}
+
+.ui.modal > [class*="middle aligned"] {
+ align-self: center;
+}
+
+.ui.modal > [class*="stretched"] {
+ align-self: stretch;
+}
+
+/* Description */
+
+.ui.modal > .content > .description {
+ display: block;
+ flex: 1 0 auto;
+ min-width: 0;
+ align-self: start;
+}
+
+.ui.modal > .content > i.icon + .description,
+.ui.modal > .content > .image + .description {
+ flex: 0 1 auto;
+ min-width: '';
+ width: auto;
+ padding-left: 2em;
+}
+
+/*rtl:ignore*/
+
+.ui.modal > .content > .image > i.icon {
+ margin: 0;
+ opacity: 1;
+ width: auto;
+ line-height: 1;
+ font-size: 8rem;
+}
+
+/*--------------
+ Actions
+---------------*/
+
+.ui.modal > .actions {
+ background: #F9FAFB;
+ padding: 1rem 1rem;
+ border-top: 1px solid rgba(34, 36, 38, 0.15);
+ text-align: right;
+}
+
+.ui.modal .actions > .button:not(.fluid) {
+ margin-left: 0.75em;
+}
+
+.ui.basic.modal > .actions {
+ border-top: none;
+}
+
+/*-------------------
+ Responsive
+--------------------*/
+
+/* Modal Width */
+
+@media only screen and (max-width: 767.98px) {
+ .ui.modal:not(.fullscreen) {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.modal:not(.fullscreen) {
+ width: 88%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.modal:not(.fullscreen) {
+ width: 850px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.modal:not(.fullscreen) {
+ width: 900px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.modal:not(.fullscreen) {
+ width: 950px;
+ margin: 0 0 0 0;
+ }
+}
+
+/* Tablet and Mobile */
+
+@media only screen and (max-width: 991.98px) {
+ .ui.modal > .header {
+ padding-right: 2.25rem;
+ }
+
+ .ui.modal > .close {
+ top: 1.0535rem;
+ right: 1rem;
+ color: rgba(0, 0, 0, 0.87);
+ }
+}
+
+/* Mobile */
+
+@media only screen and (max-width: 767.98px) {
+ .ui.modal > .header {
+ padding: 0.75rem 1rem !important;
+ padding-right: 2.25rem !important;
+ }
+
+ .ui.overlay.fullscreen.modal > .content.content.content {
+ min-height: calc(100vh - 8.1rem);
+ }
+
+ .ui.overlay.fullscreen.modal > .scrolling.content.content.content {
+ max-height: calc(100vh - 8.1rem);
+ }
+
+ .ui.modal > .content {
+ display: block;
+ padding: 1rem !important;
+ }
+
+ .ui.modal > .close {
+ top: 0.5rem !important;
+ right: 0.5rem !important;
+ }
+
+ /*rtl:ignore*/
+
+ .ui.modal .image.content {
+ flex-direction: column;
+ }
+
+ .ui.modal > .content > .image {
+ display: block;
+ max-width: 100%;
+ margin: 0 auto !important;
+ text-align: center;
+ padding: 0 0 1rem !important;
+ }
+
+ .ui.modal > .content > .image > i.icon {
+ font-size: 5rem;
+ text-align: center;
+ }
+
+ /*rtl:ignore*/
+
+ .ui.modal > .content > .description {
+ display: block;
+ width: 100% !important;
+ margin: 0 !important;
+ padding: 1rem 0 !important;
+ box-shadow: none;
+ }
+
+ /* Let Buttons Stack */
+
+ .ui.modal > .actions {
+ padding: 1rem 1rem 0rem !important;
+ }
+
+ .ui.modal .actions > .buttons,
+ .ui.modal .actions > .button {
+ margin-bottom: 1rem;
+ }
+}
+
+/*--------------
+ Coupling
+---------------*/
+
+.ui.inverted.dimmer > .ui.modal {
+ box-shadow: 1px 3px 10px 2px rgba(0, 0, 0, 0.2);
+}
+
+/*******************************
+ Types
+*******************************/
+
+.ui.basic.modal {
+ background-color: transparent;
+ border: none;
+ border-radius: 0;
+ box-shadow: none !important;
+ color: #FFFFFF;
+}
+
+.ui.basic.modal > .header,
+.ui.basic.modal > .content,
+.ui.basic.modal > .actions {
+ background-color: transparent;
+}
+
+.ui.basic.modal > .header {
+ color: #FFFFFF;
+ border-bottom: none;
+}
+
+.ui.basic.modal > .close {
+ top: 1rem;
+ right: 1.5rem;
+ color: #FFFFFF;
+}
+
+.ui.inverted.dimmer > .basic.modal {
+ color: rgba(0, 0, 0, 0.87);
+}
+
+.ui.inverted.dimmer > .ui.basic.modal > .header {
+ color: rgba(0, 0, 0, 0.85);
+}
+
+/* Resort to margin positioning if legacy */
+
+.ui.legacy.legacy.modal,
+.ui.legacy.legacy.page.dimmer > .ui.modal {
+ left: 50% !important;
+}
+
+.ui.legacy.legacy.modal:not(.aligned),
+.ui.legacy.legacy.page.dimmer > .ui.modal:not(.aligned) {
+ top: 50%;
+}
+
+.ui.legacy.legacy.page.dimmer > .ui.scrolling.modal:not(.aligned),
+.ui.page.dimmer > .ui.scrolling.legacy.legacy.modal:not(.aligned),
+.ui.top.aligned.legacy.legacy.page.dimmer > .ui.modal:not(.aligned),
+.ui.top.aligned.dimmer > .ui.legacy.legacy.modal:not(.aligned) {
+ top: auto;
+}
+
+.ui.legacy.overlay.fullscreen.modal {
+ margin-top: -2rem !important;
+}
+
+/*******************************
+ States
+*******************************/
+
+.ui.loading.modal {
+ display: block;
+ visibility: hidden;
+ z-index: -1;
+}
+
+.ui.active.modal {
+ display: block;
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*--------------
+ Aligned
+ ---------------*/
+
+.modals.dimmer .ui.top.aligned.modal {
+ top: 5vh;
+}
+
+.modals.dimmer .ui.bottom.aligned.modal {
+ bottom: 5vh;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .modals.dimmer .ui.top.aligned.modal {
+ top: 1rem;
+ }
+
+ .modals.dimmer .ui.bottom.aligned.modal {
+ bottom: 1rem;
+ }
+}
+
+/*--------------
+ Scrolling
+ ---------------*/
+
+/* Scrolling Dimmer */
+
+.scrolling.dimmable.dimmed {
+ overflow: hidden;
+}
+
+.scrolling.dimmable > .dimmer {
+ justify-content: flex-start;
+ position: fixed;
+}
+
+.scrolling.dimmable.dimmed > .dimmer {
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.modals.dimmer .ui.scrolling.modal:not(.fullscreen) {
+ margin: 2rem auto;
+}
+
+/* Fix for Firefox, Edge, IE11 */
+
+.modals.dimmer .ui.scrolling.modal:not([class*="overlay fullscreen"])::after {
+ content: '\00A0';
+ position: absolute;
+ height: 2rem;
+}
+
+/* Undetached Scrolling */
+
+.scrolling.undetached.dimmable.dimmed {
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.scrolling.undetached.dimmable.dimmed > .dimmer {
+ overflow: hidden;
+}
+
+.scrolling.undetached.dimmable .ui.scrolling.modal:not(.fullscreen) {
+ position: absolute;
+ left: 50%;
+}
+
+/* Scrolling Content */
+
+.ui.modal > .scrolling.content {
+ max-height: calc(80vh - 10rem);
+ overflow: auto;
+}
+
+.ui.overlay.fullscreen.modal > .content {
+ min-height: calc(100vh - 9.1rem);
+}
+
+.ui.overlay.fullscreen.modal > .scrolling.content {
+ max-height: calc(100vh - 9.1rem);
+}
+
+/*--------------
+ Full Screen
+ ---------------*/
+
+.ui.fullscreen.modal {
+ width: 95%;
+ left: 2.5%;
+ margin: 1em auto;
+}
+
+.ui.overlay.fullscreen.modal {
+ width: 100%;
+ left: 0;
+ margin: 0 auto;
+ top: 0;
+ border-radius: 0;
+}
+
+.ui.modal > .close.inside + .header,
+.ui.fullscreen.modal > .header {
+ padding-right: 2.25rem;
+}
+
+.ui.modal > .close.inside,
+.ui.fullscreen.modal > .close {
+ top: 1.0535rem;
+ right: 1rem;
+ color: rgba(0, 0, 0, 0.87);
+}
+
+.ui.basic.fullscreen.modal > .close {
+ color: #FFFFFF;
+}
+
+/*--------------
+ Size
+---------------*/
+
+.ui.modal {
+ font-size: 1rem;
+}
+
+.ui.mini.modal > .header:not(.ui) {
+ font-size: 1.3em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.mini.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.mini.modal {
+ width: 35.2%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.mini.modal {
+ width: 340px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.mini.modal {
+ width: 360px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.mini.modal {
+ width: 380px;
+ margin: 0 0 0 0;
+ }
+}
+
+.ui.tiny.modal > .header:not(.ui) {
+ font-size: 1.3em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.tiny.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.tiny.modal {
+ width: 52.8%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.tiny.modal {
+ width: 510px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.tiny.modal {
+ width: 540px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.tiny.modal {
+ width: 570px;
+ margin: 0 0 0 0;
+ }
+}
+
+.ui.small.modal > .header:not(.ui) {
+ font-size: 1.3em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.small.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.small.modal {
+ width: 70.4%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.small.modal {
+ width: 680px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.small.modal {
+ width: 720px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.small.modal {
+ width: 760px;
+ margin: 0 0 0 0;
+ }
+}
+
+.ui.large.modal > .header:not(.ui) {
+ font-size: 1.6em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.large.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.large.modal {
+ width: 88%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.large.modal {
+ width: 1020px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.large.modal {
+ width: 1080px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.large.modal {
+ width: 1140px;
+ margin: 0 0 0 0;
+ }
+}
+
+.ui.big.modal > .header:not(.ui) {
+ font-size: 1.6em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.big.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.big.modal {
+ width: 88%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.big.modal {
+ width: 1190px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.big.modal {
+ width: 1260px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.big.modal {
+ width: 1330px;
+ margin: 0 0 0 0;
+ }
+}
+
+.ui.huge.modal > .header:not(.ui) {
+ font-size: 1.6em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.huge.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.huge.modal {
+ width: 88%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.huge.modal {
+ width: 1360px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.huge.modal {
+ width: 1440px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.huge.modal {
+ width: 1520px;
+ margin: 0 0 0 0;
+ }
+}
+
+.ui.massive.modal > .header:not(.ui) {
+ font-size: 1.8em;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.massive.modal {
+ width: 95%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.massive.modal {
+ width: 88%;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.massive.modal {
+ width: 1530px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.massive.modal {
+ width: 1620px;
+ margin: 0 0 0 0;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.massive.modal {
+ width: 1710px;
+ margin: 0 0 0 0;
+ }
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/*******************************
+ Site Overrides
+*******************************/
+/*!
+ * # Fomantic-UI - Search
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ Search
+*******************************/
+
+.ui.search {
+ position: relative;
+}
+
+.ui.search > .prompt {
+ margin: 0;
+ outline: none;
+ -webkit-appearance: none;
+ -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+ text-shadow: none;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1.21428571em;
+ padding: 0.67857143em 1em;
+ font-size: 1em;
+ background: #FFFFFF;
+ border: 1px solid rgba(34, 36, 38, 0.15);
+ color: rgba(0, 0, 0, 0.87);
+ box-shadow: 0 0 0 0 transparent inset;
+ transition: background-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease, border-color 0.1s ease;
+}
+
+.ui.search .prompt {
+ border-radius: 500rem;
+}
+
+/*--------------
+ Icon
+---------------*/
+
+.ui.search .prompt ~ .search.icon {
+ cursor: pointer;
+}
+
+/*--------------
+ Results
+---------------*/
+
+.ui.search > .results {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ transform-origin: center top;
+ white-space: normal;
+ text-align: left;
+ text-transform: none;
+ background: #FFFFFF;
+ margin-top: 0.5em;
+ width: 18em;
+ border-radius: 0.28571429rem;
+ box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15);
+ border: 1px solid #D4D4D5;
+ z-index: 998;
+}
+
+.ui.search > .results > :first-child {
+ border-radius: 0.28571429rem 0.28571429rem 0 0;
+}
+
+.ui.search > .results > :last-child {
+ border-radius: 0 0 0.28571429rem 0.28571429rem;
+}
+
+/*--------------
+ Result
+---------------*/
+
+.ui.search > .results .result {
+ cursor: pointer;
+ display: block;
+ overflow: hidden;
+ font-size: 1em;
+ padding: 0.85714286em 1.14285714em;
+ color: rgba(0, 0, 0, 0.87);
+ line-height: 1.33;
+ border-bottom: 1px solid rgba(34, 36, 38, 0.1);
+}
+
+.ui.search > .results .result:last-child {
+ border-bottom: none !important;
+}
+
+/* Image */
+
+.ui.search > .results .result .image {
+ float: right;
+ overflow: hidden;
+ background: none;
+ width: 5em;
+ height: 3em;
+ border-radius: 0.25em;
+}
+
+.ui.search > .results .result .image img {
+ display: block;
+ width: auto;
+ height: 100%;
+}
+
+/*--------------
+ Info
+---------------*/
+
+.ui.search > .results .result .image + .content {
+ margin: 0 6em 0 0;
+}
+
+.ui.search > .results .result .title {
+ margin: -0.14285714em 0 0;
+ font-family: var(--fonts-regular);
+ font-weight: 500;
+ font-size: 1em;
+ color: rgba(0, 0, 0, 0.85);
+}
+
+.ui.search > .results .result .description {
+ margin-top: 0;
+ font-size: 0.92857143em;
+ color: rgba(0, 0, 0, 0.4);
+}
+
+.ui.search > .results .result .price {
+ float: right;
+ color: #21BA45;
+}
+
+/*--------------
+ Message
+---------------*/
+
+.ui.search > .results > .message {
+ padding: 1em 1em;
+}
+
+.ui.search > .results > .message .header {
+ font-family: var(--fonts-regular);
+ font-size: 1rem;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.87);
+}
+
+.ui.search > .results > .message .description {
+ margin-top: 0.25rem;
+ font-size: 1em;
+ color: rgba(0, 0, 0, 0.87);
+}
+
+/* View All Results */
+
+.ui.search > .results > .action {
+ display: block;
+ border-top: none;
+ background: #F3F4F5;
+ padding: 0.92857143em 1em;
+ color: rgba(0, 0, 0, 0.87);
+ font-weight: 500;
+ text-align: center;
+}
+
+/*******************************
+ States
+*******************************/
+
+/*--------------------
+ Focus
+---------------------*/
+
+.ui.search > .prompt:focus {
+ border-color: rgba(34, 36, 38, 0.35);
+ background: #FFFFFF;
+ color: rgba(0, 0, 0, 0.95);
+}
+
+/*--------------------
+ Loading
+ ---------------------*/
+
+.ui.loading.search .input > i.icon:before {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -0.64285714em 0 0 -0.64285714em;
+ width: 1.28571429em;
+ height: 1.28571429em;
+ border-radius: 500rem;
+ border: 0.2em solid rgba(0, 0, 0, 0.1);
+}
+
+.ui.loading.search .input > i.icon:after {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -0.64285714em 0 0 -0.64285714em;
+ width: 1.28571429em;
+ height: 1.28571429em;
+ animation: loader 0.6s infinite linear;
+ border: 0.2em solid #767676;
+ border-radius: 500rem;
+ box-shadow: 0 0 0 1px transparent;
+}
+
+/*--------------
+ Hover
+---------------*/
+
+.ui.search > .results .result:hover,
+.ui.category.search > .results .category .result:hover {
+ background: #F9FAFB;
+}
+
+.ui.search .action:hover:not(div) {
+ background: #E0E0E0;
+}
+
+/*--------------
+ Active
+---------------*/
+
+.ui.category.search > .results .category.active {
+ background: #F3F4F5;
+}
+
+.ui.category.search > .results .category.active > .name {
+ color: rgba(0, 0, 0, 0.87);
+}
+
+.ui.search > .results .result.active,
+.ui.category.search > .results .category .result.active {
+ position: relative;
+ border-left-color: rgba(34, 36, 38, 0.1);
+ background: #F3F4F5;
+ box-shadow: none;
+}
+
+.ui.search > .results .result.active .title {
+ color: rgba(0, 0, 0, 0.85);
+}
+
+.ui.search > .results .result.active .description {
+ color: rgba(0, 0, 0, 0.85);
+}
+
+/*--------------------
+ Disabled
+ ----------------------*/
+
+/* Disabled */
+
+.ui.disabled.search {
+ cursor: default;
+ pointer-events: none;
+ opacity: var(--opacity-disabled);
+}
+
+/*******************************
+ Types
+*******************************/
+
+/*--------------
+ Selection
+ ---------------*/
+
+.ui.search.selection .prompt {
+ border-radius: 0.28571429rem;
+}
+
+/* Remove input */
+
+.ui.search.selection > .icon.input > .remove.icon {
+ pointer-events: none;
+ position: absolute;
+ left: auto;
+ opacity: 0;
+ color: '';
+ top: 0;
+ right: 0;
+ transition: color 0.1s ease, opacity 0.1s ease;
+}
+
+.ui.search.selection > .icon.input > .active.remove.icon {
+ cursor: pointer;
+ opacity: 0.8;
+ pointer-events: auto;
+}
+
+.ui.search.selection > .icon.input:not([class*="left icon"]) > .icon ~ .remove.icon {
+ right: 1.85714em;
+}
+
+.ui.search.selection > .icon.input > .remove.icon:hover {
+ opacity: 1;
+ color: #DB2828;
+}
+
+/*--------------
+ Category
+ ---------------*/
+
+.ui.category.search .results {
+ width: 28em;
+}
+
+.ui.category.search .results.animating,
+.ui.category.search .results.visible {
+ display: table;
+}
+
+/* Category */
+
+.ui.category.search > .results .category {
+ display: table-row;
+ background: #F3F4F5;
+ box-shadow: none;
+ transition: background 0.1s ease, border-color 0.1s ease;
+}
+
+/* Last Category */
+
+.ui.category.search > .results .category:last-child {
+ border-bottom: none;
+}
+
+/* First / Last */
+
+.ui.category.search > .results .category:first-child .name + .result {
+ border-radius: 0 0.28571429rem 0 0;
+}
+
+.ui.category.search > .results .category:last-child .result:last-child {
+ border-radius: 0 0 0.28571429rem 0;
+}
+
+/* Category Result Name */
+
+.ui.category.search > .results .category > .name {
+ display: table-cell;
+ text-overflow: ellipsis;
+ width: 100px;
+ white-space: nowrap;
+ background: transparent;
+ font-family: var(--fonts-regular);
+ font-size: 1em;
+ padding: 0.4em 1em;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.4);
+ border-bottom: 1px solid rgba(34, 36, 38, 0.1);
+}
+
+/* Category Result */
+
+.ui.category.search > .results .category .results {
+ display: table-cell;
+ background: #FFFFFF;
+ border-left: 1px solid rgba(34, 36, 38, 0.15);
+ border-bottom: 1px solid rgba(34, 36, 38, 0.1);
+}
+
+.ui.category.search > .results .category .result {
+ border-bottom: 1px solid rgba(34, 36, 38, 0.1);
+ transition: background 0.1s ease, border-color 0.1s ease;
+ padding: 0.85714286em 1.14285714em;
+}
+
+/*******************************
+ Variations
+*******************************/
+
+/*-------------------
+ Scrolling
+ --------------------*/
+
+.ui.scrolling.search > .results,
+.ui.search.long > .results,
+.ui.search.short > .results {
+ overflow-x: hidden;
+ overflow-y: auto;
+ backface-visibility: hidden;
+ -webkit-overflow-scrolling: touch;
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.scrolling.search > .results {
+ max-height: 12.17714286em;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.scrolling.search > .results {
+ max-height: 18.26571429em;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.scrolling.search > .results {
+ max-height: 24.35428571em;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.scrolling.search > .results {
+ max-height: 36.53142857em;
+ }
+}
+
+@media only screen and (max-width: 767.98px) {
+ .ui.search.short > .results {
+ max-height: 12.17714286em;
+ }
+
+ .ui.search[class*="very short"] > .results {
+ max-height: 9.13285714em;
+ }
+
+ .ui.search.long > .results {
+ max-height: 24.35428571em;
+ }
+
+ .ui.search[class*="very long"] > .results {
+ max-height: 36.53142857em;
+ }
+}
+
+@media only screen and (min-width: 768px) {
+ .ui.search.short > .results {
+ max-height: 18.26571429em;
+ }
+
+ .ui.search[class*="very short"] > .results {
+ max-height: 13.69928571em;
+ }
+
+ .ui.search.long > .results {
+ max-height: 36.53142857em;
+ }
+
+ .ui.search[class*="very long"] > .results {
+ max-height: 54.79714286em;
+ }
+}
+
+@media only screen and (min-width: 992px) {
+ .ui.search.short > .results {
+ max-height: 24.35428571em;
+ }
+
+ .ui.search[class*="very short"] > .results {
+ max-height: 18.26571429em;
+ }
+
+ .ui.search.long > .results {
+ max-height: 48.70857143em;
+ }
+
+ .ui.search[class*="very long"] > .results {
+ max-height: 73.06285714em;
+ }
+}
+
+@media only screen and (min-width: 1920px) {
+ .ui.search.short > .results {
+ max-height: 36.53142857em;
+ }
+
+ .ui.search[class*="very short"] > .results {
+ max-height: 27.39857143em;
+ }
+
+ .ui.search.long > .results {
+ max-height: 73.06285714em;
+ }
+
+ .ui.search[class*="very long"] > .results {
+ max-height: 109.59428571em;
+ }
+}
+
+/*-------------------
+ Left / Right
+ --------------------*/
+
+.ui[class*="left aligned"].search > .results {
+ right: auto;
+ left: 0;
+}
+
+.ui[class*="right aligned"].search > .results {
+ right: 0;
+ left: auto;
+}
+
+/*--------------
+ Fluid
+---------------*/
+
+.ui.fluid.search .results {
+ width: 100%;
+}
+
+/*--------------
+ Sizes
+---------------*/
+
+.ui.search {
+ font-size: 1em;
+}
+
+.ui.mini.search {
+ font-size: 0.78571429em;
+}
+
+.ui.tiny.search {
+ font-size: 0.85714286em;
+}
+
+.ui.small.search {
+ font-size: 0.92857143em;
+}
+
+.ui.large.search {
+ font-size: 1.14285714em;
+}
+
+.ui.big.search {
+ font-size: 1.28571429em;
+}
+
+.ui.huge.search {
+ font-size: 1.42857143em;
+}
+
+.ui.massive.search {
+ font-size: 1.71428571em;
+}
+
+/*--------------
+ Mobile
+---------------*/
+
+@media only screen and (max-width: 767.98px) {
+ .ui.search .results {
+ max-width: calc(100vw - 2rem);
+ }
+}
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+/*******************************
+ Site Overrides
+*******************************/
+/*!
+ * # Fomantic-UI - Tab
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*******************************
+ UI Tabs
+*******************************/
+
+.ui.tab {
+ display: none;
+}
+
+/*******************************
+ States
+*******************************/
+
+/*--------------------
+ Active
+---------------------*/
+
+.ui.tab.active,
+.ui.tab.open {
+ display: block;
+}
+
+/*--------------------
+ Loading
+ ---------------------*/
+
+.ui.tab.loading {
+ position: relative;
+ overflow: hidden;
+ display: block;
+ min-height: 250px;
+}
+
+.ui.tab.loading * {
+ position: relative !important;
+ left: -10000px !important;
+}
+
+.ui.tab.loading:before,
+.ui.tab.loading.segment:before {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -1.25em 0 0 -1.25em;
+ width: 2.5em;
+ height: 2.5em;
+ border-radius: 500rem;
+ border: 0.2em solid rgba(0, 0, 0, 0.1);
+}
+
+.ui.tab.loading:after,
+.ui.tab.loading.segment:after {
+ position: absolute;
+ content: '';
+ top: 50%;
+ left: 50%;
+ margin: -1.25em 0 0 -1.25em;
+ width: 2.5em;
+ height: 2.5em;
+ animation: loader 0.6s infinite linear;
+ border: 0.2em solid #767676;
+ border-radius: 500rem;
+ box-shadow: 0 0 0 1px transparent;
+}
+
+/*******************************
+ Tab Overrides
+*******************************/
+
+/*******************************
+ User Overrides
+*******************************/ \ No newline at end of file
diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js
new file mode 100644
index 0000000..affd6a4
--- /dev/null
+++ b/web_src/fomantic/build/semantic.js
@@ -0,0 +1,11992 @@
+ /*
+ * # Fomantic UI - 2.8.7
+ * https://github.com/fomantic/Fomantic-UI
+ * http://fomantic-ui.com/
+ *
+ * Copyright 2014 Contributors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+/*!
+ * # Fomantic-UI - API
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isWindow = $.isWindow || function(obj) {
+ return obj != null && obj === obj.window;
+};
+
+ window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.api = $.fn.api = function(parameters) {
+
+ var
+ // use window context if none specified
+ $allModules = $.isFunction(this)
+ ? $(window)
+ : $(this),
+ moduleSelector = $allModules.selector || '',
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+
+ returnedValue
+ ;
+
+ $allModules
+ .each(function() {
+ var
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.api.settings, parameters)
+ : $.extend({}, $.fn.api.settings),
+
+ // internal aliases
+ namespace = settings.namespace,
+ metadata = settings.metadata,
+ selector = settings.selector,
+ error = settings.error,
+ className = settings.className,
+
+ // define namespaces for modules
+ eventNamespace = '.' + namespace,
+ moduleNamespace = 'module-' + namespace,
+
+ // element that creates request
+ $module = $(this),
+ $form = $module.closest(selector.form),
+
+ // context used for state
+ $context = (settings.stateContext)
+ ? $(settings.stateContext)
+ : $module,
+
+ // request details
+ ajaxSettings,
+ requestSettings,
+ url,
+ data,
+ requestStartTime,
+
+ // standard module
+ element = this,
+ context = $context[0],
+ instance = $module.data(moduleNamespace),
+ module
+ ;
+
+ module = {
+
+ initialize: function() {
+ if(!methodInvoked) {
+ module.bind.events();
+ }
+ module.instantiate();
+ },
+
+ instantiate: function() {
+ module.verbose('Storing instance of module', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, instance)
+ ;
+ },
+
+ destroy: function() {
+ module.verbose('Destroying previous module for', element);
+ $module
+ .removeData(moduleNamespace)
+ .off(eventNamespace)
+ ;
+ },
+
+ bind: {
+ events: function() {
+ var
+ triggerEvent = module.get.event()
+ ;
+ if( triggerEvent ) {
+ module.verbose('Attaching API events to element', triggerEvent);
+ $module
+ .on(triggerEvent + eventNamespace, module.event.trigger)
+ ;
+ }
+ else if(settings.on == 'now') {
+ module.debug('Querying API endpoint immediately');
+ module.query();
+ }
+ }
+ },
+
+ decode: {
+ json: function(response) {
+ if(response !== undefined && typeof response == 'string') {
+ try {
+ response = JSON.parse(response);
+ }
+ catch(e) {
+ // isn't json string
+ }
+ }
+ return response;
+ }
+ },
+
+ read: {
+ cachedResponse: function(url) {
+ var
+ response
+ ;
+ if(window.Storage === undefined) {
+ module.error(error.noStorage);
+ return;
+ }
+ response = sessionStorage.getItem(url);
+ module.debug('Using cached response', url, response);
+ response = module.decode.json(response);
+ return response;
+ }
+ },
+ write: {
+ cachedResponse: function(url, response) {
+ if(response && response === '') {
+ module.debug('Response empty, not caching', response);
+ return;
+ }
+ if(window.Storage === undefined) {
+ module.error(error.noStorage);
+ return;
+ }
+ if( $.isPlainObject(response) ) {
+ response = JSON.stringify(response);
+ }
+ sessionStorage.setItem(url, response);
+ module.verbose('Storing cached response for url', url, response);
+ }
+ },
+
+ query: function() {
+
+ if(module.is.disabled()) {
+ module.debug('Element is disabled API request aborted');
+ return;
+ }
+
+ if(module.is.loading()) {
+ if(settings.interruptRequests) {
+ module.debug('Interrupting previous request');
+ module.abort();
+ }
+ else {
+ module.debug('Cancelling request, previous request is still pending');
+ return;
+ }
+ }
+
+ // pass element metadata to url (value, text)
+ if(settings.defaultData) {
+ $.extend(true, settings.urlData, module.get.defaultData());
+ }
+
+ // Add form content
+ if(settings.serializeForm) {
+ settings.data = module.add.formData(settings.data);
+ }
+
+ // call beforesend and get any settings changes
+ requestSettings = module.get.settings();
+
+ // check if before send cancelled request
+ if(requestSettings === false) {
+ module.cancelled = true;
+ module.error(error.beforeSend);
+ return;
+ }
+ else {
+ module.cancelled = false;
+ }
+
+ // get url
+ url = module.get.templatedURL();
+
+ if(!url && !module.is.mocked()) {
+ module.error(error.missingURL);
+ return;
+ }
+
+ // replace variables
+ url = module.add.urlData( url );
+ // missing url parameters
+ if( !url && !module.is.mocked()) {
+ return;
+ }
+
+ requestSettings.url = settings.base + url;
+
+ // look for jQuery ajax parameters in settings
+ ajaxSettings = $.extend(true, {}, settings, {
+ type : settings.method || settings.type,
+ data : data,
+ url : settings.base + url,
+ beforeSend : settings.beforeXHR,
+ success : function() {},
+ failure : function() {},
+ complete : function() {}
+ });
+
+ module.debug('Querying URL', ajaxSettings.url);
+ module.verbose('Using AJAX settings', ajaxSettings);
+ if(settings.cache === 'local' && module.read.cachedResponse(url)) {
+ module.debug('Response returned from local cache');
+ module.request = module.create.request();
+ module.request.resolveWith(context, [ module.read.cachedResponse(url) ]);
+ return;
+ }
+
+ if( !settings.throttle ) {
+ module.debug('Sending request', data, ajaxSettings.method);
+ module.send.request();
+ }
+ else {
+ if(!settings.throttleFirstRequest && !module.timer) {
+ module.debug('Sending request', data, ajaxSettings.method);
+ module.send.request();
+ module.timer = setTimeout(function(){}, settings.throttle);
+ }
+ else {
+ module.debug('Throttling request', settings.throttle);
+ clearTimeout(module.timer);
+ module.timer = setTimeout(function() {
+ if(module.timer) {
+ delete module.timer;
+ }
+ module.debug('Sending throttled request', data, ajaxSettings.method);
+ module.send.request();
+ }, settings.throttle);
+ }
+ }
+
+ },
+
+ should: {
+ removeError: function() {
+ return ( settings.hideError === true || (settings.hideError === 'auto' && !module.is.form()) );
+ }
+ },
+
+ is: {
+ disabled: function() {
+ return ($module.filter(selector.disabled).length > 0);
+ },
+ expectingJSON: function() {
+ return settings.dataType === 'json' || settings.dataType === 'jsonp';
+ },
+ form: function() {
+ return $module.is('form') || $context.is('form');
+ },
+ mocked: function() {
+ return (settings.mockResponse || settings.mockResponseAsync || settings.response || settings.responseAsync);
+ },
+ input: function() {
+ return $module.is('input');
+ },
+ loading: function() {
+ return (module.request)
+ ? (module.request.state() == 'pending')
+ : false
+ ;
+ },
+ abortedRequest: function(xhr) {
+ if(xhr && xhr.readyState !== undefined && xhr.readyState === 0) {
+ module.verbose('XHR request determined to be aborted');
+ return true;
+ }
+ else {
+ module.verbose('XHR request was not aborted');
+ return false;
+ }
+ },
+ validResponse: function(response) {
+ if( (!module.is.expectingJSON()) || !$.isFunction(settings.successTest) ) {
+ module.verbose('Response is not JSON, skipping validation', settings.successTest, response);
+ return true;
+ }
+ module.debug('Checking JSON returned success', settings.successTest, response);
+ if( settings.successTest(response) ) {
+ module.debug('Response passed success test', response);
+ return true;
+ }
+ else {
+ module.debug('Response failed success test', response);
+ return false;
+ }
+ }
+ },
+
+ was: {
+ cancelled: function() {
+ return (module.cancelled || false);
+ },
+ succesful: function() { // codespell-ignore
+ module.verbose('This behavior will be deleted due to typo. Use "was successful" instead.');
+ return module.was.successful();
+ },
+ successful: function() {
+ return (module.request && module.request.state() == 'resolved');
+ },
+ failure: function() {
+ return (module.request && module.request.state() == 'rejected');
+ },
+ complete: function() {
+ return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
+ }
+ },
+
+ add: {
+ urlData: function(url, urlData) {
+ var
+ requiredVariables,
+ optionalVariables
+ ;
+ if(url) {
+ requiredVariables = url.match(settings.regExp.required);
+ optionalVariables = url.match(settings.regExp.optional);
+ urlData = urlData || settings.urlData;
+ if(requiredVariables) {
+ module.debug('Looking for required URL variables', requiredVariables);
+ $.each(requiredVariables, function(index, templatedString) {
+ var
+ // allow legacy {$var} style
+ variable = (templatedString.indexOf('$') !== -1)
+ ? templatedString.substr(2, templatedString.length - 3)
+ : templatedString.substr(1, templatedString.length - 2),
+ value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
+ ? urlData[variable]
+ : ($module.data(variable) !== undefined)
+ ? $module.data(variable)
+ : ($context.data(variable) !== undefined)
+ ? $context.data(variable)
+ : urlData[variable]
+ ;
+ // remove value
+ if(value === undefined) {
+ module.error(error.requiredParameter, variable, url);
+ url = false;
+ return false;
+ }
+ else {
+ module.verbose('Found required variable', variable, value);
+ value = (settings.encodeParameters)
+ ? module.get.urlEncodedValue(value)
+ : value
+ ;
+ url = url.replace(templatedString, value);
+ }
+ });
+ }
+ if(optionalVariables) {
+ module.debug('Looking for optional URL variables', requiredVariables);
+ $.each(optionalVariables, function(index, templatedString) {
+ var
+ // allow legacy {/$var} style
+ variable = (templatedString.indexOf('$') !== -1)
+ ? templatedString.substr(3, templatedString.length - 4)
+ : templatedString.substr(2, templatedString.length - 3),
+ value = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
+ ? urlData[variable]
+ : ($module.data(variable) !== undefined)
+ ? $module.data(variable)
+ : ($context.data(variable) !== undefined)
+ ? $context.data(variable)
+ : urlData[variable]
+ ;
+ // optional replacement
+ if(value !== undefined) {
+ module.verbose('Optional variable Found', variable, value);
+ url = url.replace(templatedString, value);
+ }
+ else {
+ module.verbose('Optional variable not found', variable);
+ // remove preceding slash if set
+ if(url.indexOf('/' + templatedString) !== -1) {
+ url = url.replace('/' + templatedString, '');
+ }
+ else {
+ url = url.replace(templatedString, '');
+ }
+ }
+ });
+ }
+ }
+ return url;
+ },
+ formData: function(data) {
+ var
+ canSerialize = ($.fn.serializeObject !== undefined),
+ formData = (canSerialize)
+ ? $form.serializeObject()
+ : $form.serialize(),
+ hasOtherData
+ ;
+ data = data || settings.data;
+ hasOtherData = $.isPlainObject(data);
+
+ if(hasOtherData) {
+ if(canSerialize) {
+ module.debug('Extending existing data with form data', data, formData);
+ data = $.extend(true, {}, data, formData);
+ }
+ else {
+ module.error(error.missingSerialize);
+ module.debug('Cant extend data. Replacing data with form data', data, formData);
+ data = formData;
+ }
+ }
+ else {
+ module.debug('Adding form data', formData);
+ data = formData;
+ }
+ return data;
+ }
+ },
+
+ send: {
+ request: function() {
+ module.set.loading();
+ module.request = module.create.request();
+ if( module.is.mocked() ) {
+ module.mockedXHR = module.create.mockedXHR();
+ }
+ else {
+ module.xhr = module.create.xhr();
+ }
+ settings.onRequest.call(context, module.request, module.xhr);
+ }
+ },
+
+ event: {
+ trigger: function(event) {
+ module.query();
+ if(event.type == 'submit' || event.type == 'click') {
+ event.preventDefault();
+ }
+ },
+ xhr: {
+ always: function() {
+ // nothing special
+ },
+ done: function(response, textStatus, xhr) {
+ var
+ context = this,
+ elapsedTime = (new Date().getTime() - requestStartTime),
+ timeLeft = (settings.loadingDuration - elapsedTime),
+ translatedResponse = ( $.isFunction(settings.onResponse) )
+ ? module.is.expectingJSON() && !settings.rawResponse
+ ? settings.onResponse.call(context, $.extend(true, {}, response))
+ : settings.onResponse.call(context, response)
+ : false
+ ;
+ timeLeft = (timeLeft > 0)
+ ? timeLeft
+ : 0
+ ;
+ if(translatedResponse) {
+ module.debug('Modified API response in onResponse callback', settings.onResponse, translatedResponse, response);
+ response = translatedResponse;
+ }
+ if(timeLeft > 0) {
+ module.debug('Response completed early delaying state change by', timeLeft);
+ }
+ setTimeout(function() {
+ if( module.is.validResponse(response) ) {
+ module.request.resolveWith(context, [response, xhr]);
+ }
+ else {
+ module.request.rejectWith(context, [xhr, 'invalid']);
+ }
+ }, timeLeft);
+ },
+ fail: function(xhr, status, httpMessage) {
+ var
+ context = this,
+ elapsedTime = (new Date().getTime() - requestStartTime),
+ timeLeft = (settings.loadingDuration - elapsedTime)
+ ;
+ timeLeft = (timeLeft > 0)
+ ? timeLeft
+ : 0
+ ;
+ if(timeLeft > 0) {
+ module.debug('Response completed early delaying state change by', timeLeft);
+ }
+ setTimeout(function() {
+ if( module.is.abortedRequest(xhr) ) {
+ module.request.rejectWith(context, [xhr, 'aborted', httpMessage]);
+ }
+ else {
+ module.request.rejectWith(context, [xhr, 'error', status, httpMessage]);
+ }
+ }, timeLeft);
+ }
+ },
+ request: {
+ done: function(response, xhr) {
+ module.debug('Successful API Response', response);
+ if(settings.cache === 'local' && url) {
+ module.write.cachedResponse(url, response);
+ module.debug('Saving server response locally', module.cache);
+ }
+ settings.onSuccess.call(context, response, $module, xhr);
+ },
+ complete: function(firstParameter, secondParameter) {
+ var
+ xhr,
+ response
+ ;
+ // have to guess callback parameters based on request success
+ if( module.was.successful() ) {
+ response = firstParameter;
+ xhr = secondParameter;
+ }
+ else {
+ xhr = firstParameter;
+ response = module.get.responseFromXHR(xhr);
+ }
+ module.remove.loading();
+ settings.onComplete.call(context, response, $module, xhr);
+ },
+ fail: function(xhr, status, httpMessage) {
+ var
+ // pull response from xhr if available
+ response = module.get.responseFromXHR(xhr),
+ errorMessage = module.get.errorFromRequest(response, status, httpMessage)
+ ;
+ if(status == 'aborted') {
+ module.debug('XHR Aborted (Most likely caused by page navigation or CORS Policy)', status, httpMessage);
+ settings.onAbort.call(context, status, $module, xhr);
+ return true;
+ }
+ else if(status == 'invalid') {
+ module.debug('JSON did not pass success test. A server-side error has most likely occurred', response);
+ }
+ else if(status == 'error') {
+ if(xhr !== undefined) {
+ module.debug('XHR produced a server error', status, httpMessage);
+ // make sure we have an error to display to console
+ if( (xhr.status < 200 || xhr.status >= 300) && httpMessage !== undefined && httpMessage !== '') {
+ module.error(error.statusMessage + httpMessage, ajaxSettings.url);
+ }
+ settings.onError.call(context, errorMessage, $module, xhr);
+ }
+ }
+
+ if(settings.errorDuration && status !== 'aborted') {
+ module.debug('Adding error state');
+ module.set.error();
+ if( module.should.removeError() ) {
+ setTimeout(module.remove.error, settings.errorDuration);
+ }
+ }
+ module.debug('API Request failed', errorMessage, xhr);
+ settings.onFailure.call(context, response, $module, xhr);
+ }
+ }
+ },
+
+ create: {
+
+ request: function() {
+ // api request promise
+ return $.Deferred()
+ .always(module.event.request.complete)
+ .done(module.event.request.done)
+ .fail(module.event.request.fail)
+ ;
+ },
+
+ mockedXHR: function () {
+ var
+ // xhr does not simulate these properties of xhr but must return them
+ textStatus = false,
+ status = false,
+ httpMessage = false,
+ responder = settings.mockResponse || settings.response,
+ asyncResponder = settings.mockResponseAsync || settings.responseAsync,
+ asyncCallback,
+ response,
+ mockedXHR
+ ;
+
+ mockedXHR = $.Deferred()
+ .always(module.event.xhr.complete)
+ .done(module.event.xhr.done)
+ .fail(module.event.xhr.fail)
+ ;
+
+ if(responder) {
+ if( $.isFunction(responder) ) {
+ module.debug('Using specified synchronous callback', responder);
+ response = responder.call(context, requestSettings);
+ }
+ else {
+ module.debug('Using settings specified response', responder);
+ response = responder;
+ }
+ // simulating response
+ mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
+ }
+ else if( $.isFunction(asyncResponder) ) {
+ asyncCallback = function(response) {
+ module.debug('Async callback returned response', response);
+
+ if(response) {
+ mockedXHR.resolveWith(context, [ response, textStatus, { responseText: response }]);
+ }
+ else {
+ mockedXHR.rejectWith(context, [{ responseText: response }, status, httpMessage]);
+ }
+ };
+ module.debug('Using specified async response callback', asyncResponder);
+ asyncResponder.call(context, requestSettings, asyncCallback);
+ }
+ return mockedXHR;
+ },
+
+ xhr: function() {
+ var
+ xhr
+ ;
+ // ajax request promise
+ xhr = $.ajax(ajaxSettings)
+ .always(module.event.xhr.always)
+ .done(module.event.xhr.done)
+ .fail(module.event.xhr.fail)
+ ;
+ module.verbose('Created server request', xhr, ajaxSettings);
+ return xhr;
+ }
+ },
+
+ set: {
+ error: function() {
+ module.verbose('Adding error state to element', $context);
+ $context.addClass(className.error);
+ },
+ loading: function() {
+ module.verbose('Adding loading state to element', $context);
+ $context.addClass(className.loading);
+ requestStartTime = new Date().getTime();
+ }
+ },
+
+ remove: {
+ error: function() {
+ module.verbose('Removing error state from element', $context);
+ $context.removeClass(className.error);
+ },
+ loading: function() {
+ module.verbose('Removing loading state from element', $context);
+ $context.removeClass(className.loading);
+ }
+ },
+
+ get: {
+ responseFromXHR: function(xhr) {
+ return $.isPlainObject(xhr)
+ ? (module.is.expectingJSON())
+ ? module.decode.json(xhr.responseText)
+ : xhr.responseText
+ : false
+ ;
+ },
+ errorFromRequest: function(response, status, httpMessage) {
+ return ($.isPlainObject(response) && response.error !== undefined)
+ ? response.error // use json error message
+ : (settings.error[status] !== undefined) // use server error message
+ ? settings.error[status]
+ : httpMessage
+ ;
+ },
+ request: function() {
+ return module.request || false;
+ },
+ xhr: function() {
+ return module.xhr || false;
+ },
+ settings: function() {
+ var
+ runSettings
+ ;
+ runSettings = settings.beforeSend.call($module, settings);
+ if(runSettings) {
+ if(runSettings.success !== undefined) {
+ module.debug('Legacy success callback detected', runSettings);
+ module.error(error.legacyParameters, runSettings.success);
+ runSettings.onSuccess = runSettings.success;
+ }
+ if(runSettings.failure !== undefined) {
+ module.debug('Legacy failure callback detected', runSettings);
+ module.error(error.legacyParameters, runSettings.failure);
+ runSettings.onFailure = runSettings.failure;
+ }
+ if(runSettings.complete !== undefined) {
+ module.debug('Legacy complete callback detected', runSettings);
+ module.error(error.legacyParameters, runSettings.complete);
+ runSettings.onComplete = runSettings.complete;
+ }
+ }
+ if(runSettings === undefined) {
+ module.error(error.noReturnedValue);
+ }
+ if(runSettings === false) {
+ return runSettings;
+ }
+ return (runSettings !== undefined)
+ ? $.extend(true, {}, runSettings)
+ : $.extend(true, {}, settings)
+ ;
+ },
+ urlEncodedValue: function(value) {
+ var
+ decodedValue = window.decodeURIComponent(value),
+ encodedValue = window.encodeURIComponent(value),
+ alreadyEncoded = (decodedValue !== value)
+ ;
+ if(alreadyEncoded) {
+ module.debug('URL value is already encoded, avoiding double encoding', value);
+ return value;
+ }
+ module.verbose('Encoding value using encodeURIComponent', value, encodedValue);
+ return encodedValue;
+ },
+ defaultData: function() {
+ var
+ data = {}
+ ;
+ if( !$.isWindow(element) ) {
+ if( module.is.input() ) {
+ data.value = $module.val();
+ }
+ else if( module.is.form() ) {
+
+ }
+ else {
+ data.text = $module.text();
+ }
+ }
+ return data;
+ },
+ event: function() {
+ if( $.isWindow(element) || settings.on == 'now' ) {
+ module.debug('API called without element, no events attached');
+ return false;
+ }
+ else if(settings.on == 'auto') {
+ if( $module.is('input') ) {
+ return (element.oninput !== undefined)
+ ? 'input'
+ : (element.onpropertychange !== undefined)
+ ? 'propertychange'
+ : 'keyup'
+ ;
+ }
+ else if( $module.is('form') ) {
+ return 'submit';
+ }
+ else {
+ return 'click';
+ }
+ }
+ else {
+ return settings.on;
+ }
+ },
+ templatedURL: function(action) {
+ action = action || $module.data(metadata.action) || settings.action || false;
+ url = $module.data(metadata.url) || settings.url || false;
+ if(url) {
+ module.debug('Using specified url', url);
+ return url;
+ }
+ if(action) {
+ module.debug('Looking up url for action', action, settings.api);
+ if(settings.api[action] === undefined && !module.is.mocked()) {
+ module.error(error.missingAction, settings.action, settings.api);
+ return;
+ }
+ url = settings.api[action];
+ }
+ else if( module.is.form() ) {
+ url = $module.attr('action') || $context.attr('action') || false;
+ module.debug('No url or action specified, defaulting to form action', url);
+ }
+ return url;
+ }
+ },
+
+ abort: function() {
+ var
+ xhr = module.get.xhr()
+ ;
+ if( xhr && xhr.state() !== 'resolved') {
+ module.debug('Cancelling API request');
+ xhr.abort();
+ }
+ },
+
+ // reset state
+ reset: function() {
+ module.remove.error();
+ module.remove.loading();
+ },
+
+ setting: function(name, value) {
+ module.debug('Changing setting', name, value);
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ if($.isPlainObject(settings[name])) {
+ $.extend(true, settings[name], value);
+ }
+ else {
+ settings[name] = value;
+ }
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ //'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ module.error(error.method, query);
+ return false;
+ }
+ });
+ }
+ if ( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.initialize();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.initialize();
+ }
+ })
+ ;
+
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : this
+ ;
+};
+
+$.api.settings = {
+
+ name : 'API',
+ namespace : 'api',
+
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ // object containing all templates endpoints
+ api : {},
+
+ // whether to cache responses
+ cache : true,
+
+ // whether new requests should abort previous requests
+ interruptRequests : true,
+
+ // event binding
+ on : 'auto',
+
+ // context for applying state classes
+ stateContext : false,
+
+ // duration for loading state
+ loadingDuration : 0,
+
+ // whether to hide errors after a period of time
+ hideError : 'auto',
+
+ // duration for error state
+ errorDuration : 2000,
+
+ // whether parameters should be encoded with encodeURIComponent
+ encodeParameters : true,
+
+ // API action to use
+ action : false,
+
+ // templated URL to use
+ url : false,
+
+ // base URL to apply to all endpoints
+ base : '',
+
+ // data that will
+ urlData : {},
+
+ // whether to add default data to url data
+ defaultData : true,
+
+ // whether to serialize closest form
+ serializeForm : false,
+
+ // how long to wait before request should occur
+ throttle : 0,
+
+ // whether to throttle first request or only repeated
+ throttleFirstRequest : true,
+
+ // standard ajax settings
+ method : 'get',
+ data : {},
+ dataType : 'json',
+
+ // mock response
+ mockResponse : false,
+ mockResponseAsync : false,
+
+ // aliases for mock
+ response : false,
+ responseAsync : false,
+
+// whether onResponse should work with response value without force converting into an object
+ rawResponse : false,
+
+ // callbacks before request
+ beforeSend : function(settings) { return settings; },
+ beforeXHR : function(xhr) {},
+ onRequest : function(promise, xhr) {},
+
+ // after request
+ onResponse : false, // function(response) { },
+
+ // response was successful, if JSON passed validation
+ onSuccess : function(response, $module) {},
+
+ // request finished without aborting
+ onComplete : function(response, $module) {},
+
+ // failed JSON success test
+ onFailure : function(response, $module) {},
+
+ // server error
+ onError : function(errorMessage, $module) {},
+
+ // request aborted
+ onAbort : function(errorMessage, $module) {},
+
+ successTest : false,
+
+ // errors
+ error : {
+ beforeSend : 'The before send function has aborted the request',
+ error : 'There was an error with your request',
+ exitConditions : 'API Request Aborted. Exit conditions met',
+ JSONParse : 'JSON could not be parsed during error handling',
+ legacyParameters : 'You are using legacy API success callback names',
+ method : 'The method you called is not defined',
+ missingAction : 'API action used but no url was defined',
+ missingSerialize : 'jquery-serialize-object is required to add form data to an existing data object',
+ missingURL : 'No URL specified for api event',
+ noReturnedValue : 'The beforeSend callback must return a settings object, beforeSend ignored.',
+ noStorage : 'Caching responses locally requires session storage',
+ parseError : 'There was an error parsing your request',
+ requiredParameter : 'Missing a required URL parameter: ',
+ statusMessage : 'Server gave an error: ',
+ timeout : 'Your request timed out'
+ },
+
+ regExp : {
+ required : /\{\$*[A-z0-9]+\}/g,
+ optional : /\{\/\$*[A-z0-9]+\}/g,
+ },
+
+ className: {
+ loading : 'loading',
+ error : 'error'
+ },
+
+ selector: {
+ disabled : '.disabled',
+ form : 'form'
+ },
+
+ metadata: {
+ action : 'action',
+ url : 'url'
+ }
+};
+
+
+
+})( jQuery, window, document );
+
+/*!
+ * # Fomantic-UI - Dimmer
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.dimmer = function(parameters) {
+ var
+ $allModules = $(this),
+
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+
+ returnedValue
+ ;
+
+ $allModules
+ .each(function() {
+ var
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.dimmer.settings, parameters)
+ : $.extend({}, $.fn.dimmer.settings),
+
+ selector = settings.selector,
+ namespace = settings.namespace,
+ className = settings.className,
+ error = settings.error,
+
+ eventNamespace = '.' + namespace,
+ moduleNamespace = 'module-' + namespace,
+ moduleSelector = $allModules.selector || '',
+
+ clickEvent = "click", unstableClickEvent = ('ontouchstart' in document.documentElement)
+ ? 'touchstart'
+ : 'click',
+
+ $module = $(this),
+ $dimmer,
+ $dimmable,
+
+ element = this,
+ instance = $module.data(moduleNamespace),
+ module
+ ;
+
+ module = {
+
+ preinitialize: function() {
+ if( module.is.dimmer() ) {
+
+ $dimmable = $module.parent();
+ $dimmer = $module;
+ }
+ else {
+ $dimmable = $module;
+ if( module.has.dimmer() ) {
+ if(settings.dimmerName) {
+ $dimmer = $dimmable.find(selector.dimmer).filter('.' + settings.dimmerName);
+ }
+ else {
+ $dimmer = $dimmable.find(selector.dimmer);
+ }
+ }
+ else {
+ $dimmer = module.create();
+ }
+ }
+ },
+
+ initialize: function() {
+ module.debug('Initializing dimmer', settings);
+
+ module.bind.events();
+ module.set.dimmable();
+ module.instantiate();
+ },
+
+ instantiate: function() {
+ module.verbose('Storing instance of module', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, instance)
+ ;
+ },
+
+ destroy: function() {
+ module.verbose('Destroying previous module', $dimmer);
+ module.unbind.events();
+ module.remove.variation();
+ $dimmable
+ .off(eventNamespace)
+ ;
+ },
+
+ bind: {
+ events: function() {
+ if(settings.on == 'hover') {
+ $dimmable
+ .on('mouseenter' + eventNamespace, module.show)
+ .on('mouseleave' + eventNamespace, module.hide)
+ ;
+ }
+ else if(settings.on == 'click') {
+ $dimmable
+ .on(clickEvent + eventNamespace, module.toggle)
+ ;
+ }
+ if( module.is.page() ) {
+ module.debug('Setting as a page dimmer', $dimmable);
+ module.set.pageDimmer();
+ }
+
+ if( module.is.closable() ) {
+ module.verbose('Adding dimmer close event', $dimmer);
+ $dimmable
+ .on(clickEvent + eventNamespace, selector.dimmer, module.event.click)
+ ;
+ }
+ }
+ },
+
+ unbind: {
+ events: function() {
+ $module
+ .removeData(moduleNamespace)
+ ;
+ $dimmable
+ .off(eventNamespace)
+ ;
+ }
+ },
+
+ event: {
+ click: function(event) {
+ module.verbose('Determining if event occurred on dimmer', event);
+ if( $dimmer.find(event.target).length === 0 || $(event.target).is(selector.content) ) {
+ module.hide();
+ event.stopImmediatePropagation();
+ }
+ }
+ },
+
+ addContent: function(element) {
+ var
+ $content = $(element)
+ ;
+ module.debug('Add content to dimmer', $content);
+ if($content.parent()[0] !== $dimmer[0]) {
+ $content.detach().appendTo($dimmer);
+ }
+ },
+
+ create: function() {
+ var
+ $element = $( settings.template.dimmer(settings) )
+ ;
+ if(settings.dimmerName) {
+ module.debug('Creating named dimmer', settings.dimmerName);
+ $element.addClass(settings.dimmerName);
+ }
+ $element
+ .appendTo($dimmable)
+ ;
+ return $element;
+ },
+
+ show: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ module.debug('Showing dimmer', $dimmer, settings);
+ module.set.variation();
+ if( (!module.is.dimmed() || module.is.animating()) && module.is.enabled() ) {
+ module.animate.show(callback);
+ settings.onShow.call(element);
+ settings.onChange.call(element);
+ }
+ else {
+ module.debug('Dimmer is already shown or disabled');
+ }
+ },
+
+ hide: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( module.is.dimmed() || module.is.animating() ) {
+ module.debug('Hiding dimmer', $dimmer);
+ module.animate.hide(callback);
+ settings.onHide.call(element);
+ settings.onChange.call(element);
+ }
+ else {
+ module.debug('Dimmer is not visible');
+ }
+ },
+
+ toggle: function() {
+ module.verbose('Toggling dimmer visibility', $dimmer);
+ if( !module.is.dimmed() ) {
+ module.show();
+ }
+ else {
+ if ( module.is.closable() ) {
+ module.hide();
+ }
+ }
+ },
+
+ animate: {
+ show: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) {
+ if(settings.useFlex) {
+ module.debug('Using flex dimmer');
+ module.remove.legacy();
+ }
+ else {
+ module.debug('Using legacy non-flex dimmer');
+ module.set.legacy();
+ }
+ if(settings.opacity !== 'auto') {
+ module.set.opacity();
+ }
+ $dimmer
+ .transition({
+ displayType : settings.useFlex
+ ? 'flex'
+ : 'block',
+ animation : settings.transition + ' in',
+ queue : false,
+ duration : module.get.duration(),
+ useFailSafe : true,
+ onStart : function() {
+ module.set.dimmed();
+ },
+ onComplete : function() {
+ module.set.active();
+ callback();
+ }
+ })
+ ;
+ }
+ else {
+ module.verbose('Showing dimmer animation with javascript');
+ module.set.dimmed();
+ if(settings.opacity == 'auto') {
+ settings.opacity = 0.8;
+ }
+ $dimmer
+ .stop()
+ .css({
+ opacity : 0,
+ width : '100%',
+ height : '100%'
+ })
+ .fadeTo(module.get.duration(), settings.opacity, function() {
+ $dimmer.removeAttr('style');
+ module.set.active();
+ callback();
+ })
+ ;
+ }
+ },
+ hide: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) {
+ module.verbose('Hiding dimmer with css');
+ $dimmer
+ .transition({
+ displayType : settings.useFlex
+ ? 'flex'
+ : 'block',
+ animation : settings.transition + ' out',
+ queue : false,
+ duration : module.get.duration(),
+ useFailSafe : true,
+ onComplete : function() {
+ module.remove.dimmed();
+ module.remove.variation();
+ module.remove.active();
+ callback();
+ }
+ })
+ ;
+ }
+ else {
+ module.verbose('Hiding dimmer with javascript');
+ $dimmer
+ .stop()
+ .fadeOut(module.get.duration(), function() {
+ module.remove.dimmed();
+ module.remove.active();
+ $dimmer.removeAttr('style');
+ callback();
+ })
+ ;
+ }
+ }
+ },
+
+ get: {
+ dimmer: function() {
+ return $dimmer;
+ },
+ duration: function() {
+ if(typeof settings.duration == 'object') {
+ if( module.is.active() ) {
+ return settings.duration.hide;
+ }
+ else {
+ return settings.duration.show;
+ }
+ }
+ return settings.duration;
+ }
+ },
+
+ has: {
+ dimmer: function() {
+ if(settings.dimmerName) {
+ return ($module.find(selector.dimmer).filter('.' + settings.dimmerName).length > 0);
+ }
+ else {
+ return ( $module.find(selector.dimmer).length > 0 );
+ }
+ }
+ },
+
+ is: {
+ active: function() {
+ return $dimmer.hasClass(className.active);
+ },
+ animating: function() {
+ return ( $dimmer.is(':animated') || $dimmer.hasClass(className.animating) );
+ },
+ closable: function() {
+ if(settings.closable == 'auto') {
+ if(settings.on == 'hover') {
+ return false;
+ }
+ return true;
+ }
+ return settings.closable;
+ },
+ dimmer: function() {
+ return $module.hasClass(className.dimmer);
+ },
+ dimmable: function() {
+ return $module.hasClass(className.dimmable);
+ },
+ dimmed: function() {
+ return $dimmable.hasClass(className.dimmed);
+ },
+ disabled: function() {
+ return $dimmable.hasClass(className.disabled);
+ },
+ enabled: function() {
+ return !module.is.disabled();
+ },
+ page: function () {
+ return $dimmable.is('body');
+ },
+ pageDimmer: function() {
+ return $dimmer.hasClass(className.pageDimmer);
+ }
+ },
+
+ can: {
+ show: function() {
+ return !$dimmer.hasClass(className.disabled);
+ }
+ },
+
+ set: {
+ opacity: function(opacity) {
+ var
+ color = $dimmer.css('background-color'),
+ colorArray = color.split(','),
+ isRGB = (colorArray && colorArray.length >= 3)
+ ;
+ opacity = settings.opacity === 0 ? 0 : settings.opacity || opacity;
+ if(isRGB) {
+ colorArray[2] = colorArray[2].replace(')','');
+ colorArray[3] = opacity + ')';
+ color = colorArray.join(',');
+ }
+ else {
+ color = 'rgba(0, 0, 0, ' + opacity + ')';
+ }
+ module.debug('Setting opacity to', opacity);
+ $dimmer.css('background-color', color);
+ },
+ legacy: function() {
+ $dimmer.addClass(className.legacy);
+ },
+ active: function() {
+ $dimmer.addClass(className.active);
+ },
+ dimmable: function() {
+ $dimmable.addClass(className.dimmable);
+ },
+ dimmed: function() {
+ $dimmable.addClass(className.dimmed);
+ },
+ pageDimmer: function() {
+ $dimmer.addClass(className.pageDimmer);
+ },
+ disabled: function() {
+ $dimmer.addClass(className.disabled);
+ },
+ variation: function(variation) {
+ variation = variation || settings.variation;
+ if(variation) {
+ $dimmer.addClass(variation);
+ }
+ }
+ },
+
+ remove: {
+ active: function() {
+ $dimmer
+ .removeClass(className.active)
+ ;
+ },
+ legacy: function() {
+ $dimmer.removeClass(className.legacy);
+ },
+ dimmed: function() {
+ $dimmable.removeClass(className.dimmed);
+ },
+ disabled: function() {
+ $dimmer.removeClass(className.disabled);
+ },
+ variation: function(variation) {
+ variation = variation || settings.variation;
+ if(variation) {
+ $dimmer.removeClass(variation);
+ }
+ }
+ },
+
+ setting: function(name, value) {
+ module.debug('Changing setting', name, value);
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ if($.isPlainObject(settings[name])) {
+ $.extend(true, settings[name], value);
+ }
+ else {
+ settings[name] = value;
+ }
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ 'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if($allModules.length > 1) {
+ title += ' ' + '(' + $allModules.length + ')';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ module.error(error.method, query);
+ return false;
+ }
+ });
+ }
+ if ( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+
+ module.preinitialize();
+
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.initialize();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.initialize();
+ }
+ })
+ ;
+
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : this
+ ;
+};
+
+$.fn.dimmer.settings = {
+
+ name : 'Dimmer',
+ namespace : 'dimmer',
+
+ silent : false,
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ // whether should use flex layout
+ useFlex : true,
+
+ // name to distinguish between multiple dimmers in context
+ dimmerName : false,
+
+ // whether to add a variation type
+ variation : false,
+
+ // whether to bind close events
+ closable : 'auto',
+
+ // whether to use css animations
+ useCSS : true,
+
+ // css animation to use
+ transition : 'fade',
+
+ // event to bind to
+ on : false,
+
+ // overriding opacity value
+ opacity : 'auto',
+
+ // transition durations
+ duration : {
+ show : 500,
+ hide : 500
+ },
+// whether the dynamically created dimmer should have a loader
+ displayLoader: false,
+ loaderText : false,
+ loaderVariation : '',
+
+ onChange : function(){},
+ onShow : function(){},
+ onHide : function(){},
+
+ error : {
+ method : 'The method you called is not defined.'
+ },
+
+ className : {
+ active : 'active',
+ animating : 'animating',
+ dimmable : 'dimmable',
+ dimmed : 'dimmed',
+ dimmer : 'dimmer',
+ disabled : 'disabled',
+ hide : 'hide',
+ legacy : 'legacy',
+ pageDimmer : 'page',
+ show : 'show',
+ loader : 'ui loader'
+ },
+
+ selector: {
+ dimmer : '> .ui.dimmer',
+ content : '.ui.dimmer > .content, .ui.dimmer > .content > .center'
+ },
+
+ template: {
+ dimmer: function(settings) {
+ var d = $('<div/>').addClass('ui dimmer'),l;
+ if(settings.displayLoader) {
+ l = $('<div/>')
+ .addClass(settings.className.loader)
+ .addClass(settings.loaderVariation);
+ if(!!settings.loaderText){
+ l.text(settings.loaderText);
+ l.addClass('text');
+ }
+ d.append(l);
+ }
+ return d;
+ }
+ }
+
+};
+
+})( jQuery, window, document );
+
+/*!
+ * # Fomantic-UI - Dropdown
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.dropdown = function(parameters) {
+ var
+ $allModules = $(this),
+ $document = $(document),
+
+ moduleSelector = $allModules.selector || '',
+
+ hasTouch = ('ontouchstart' in document.documentElement),
+ clickEvent = "click", unstableClickEvent = hasTouch
+ ? 'touchstart'
+ : 'click',
+
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+ returnedValue
+ ;
+
+ $allModules
+ .each(function(elementIndex) {
+ var
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
+ : $.extend({}, $.fn.dropdown.settings),
+
+ className = settings.className,
+ message = settings.message,
+ fields = settings.fields,
+ keys = settings.keys,
+ metadata = settings.metadata,
+ namespace = settings.namespace,
+ regExp = settings.regExp,
+ selector = settings.selector,
+ error = settings.error,
+ templates = settings.templates,
+
+ eventNamespace = '.' + namespace,
+ moduleNamespace = 'module-' + namespace,
+
+ $module = $(this),
+ $context = $(settings.context),
+ $text = $module.find(selector.text),
+ $search = $module.find(selector.search),
+ $sizer = $module.find(selector.sizer),
+ $input = $module.find(selector.input),
+ $icon = $module.find(selector.icon),
+ $clear = $module.find(selector.clearIcon),
+
+ $combo = ($module.prev().find(selector.text).length > 0)
+ ? $module.prev().find(selector.text)
+ : $module.prev(),
+
+ $menu = $module.children(selector.menu),
+ $item = $menu.find(selector.item),
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(),
+
+ activated = false,
+ itemActivated = false,
+ internalChange = false,
+ iconClicked = false,
+ element = this,
+ instance = $module.data(moduleNamespace),
+
+ selectActionActive,
+ initialLoad,
+ pageLostFocus,
+ willRefocus,
+ elementNamespace,
+ id,
+ selectObserver,
+ menuObserver,
+ classObserver,
+ module
+ ;
+
+ module = {
+
+ initialize: function() {
+ module.debug('Initializing dropdown', settings);
+
+ if( module.is.alreadySetup() ) {
+ module.setup.reference();
+ }
+ else {
+ if (settings.ignoreDiacritics && !String.prototype.normalize) {
+ settings.ignoreDiacritics = false;
+ module.error(error.noNormalize, element);
+ }
+
+ module.setup.layout();
+
+ if(settings.values) {
+ module.set.initialLoad();
+ module.change.values(settings.values);
+ module.remove.initialLoad();
+ }
+
+ module.refreshData();
+
+ module.save.defaults();
+ module.restore.selected();
+
+ module.create.id();
+ module.bind.events();
+
+ module.observeChanges();
+ module.instantiate();
+ }
+
+ },
+
+ instantiate: function() {
+ module.verbose('Storing instance of dropdown', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, module)
+ ;
+ },
+
+ destroy: function() {
+ module.verbose('Destroying previous dropdown', $module);
+ module.remove.tabbable();
+ module.remove.active();
+ $menu.transition('stop all');
+ $menu.removeClass(className.visible).addClass(className.hidden);
+ $module
+ .off(eventNamespace)
+ .removeData(moduleNamespace)
+ ;
+ $menu
+ .off(eventNamespace)
+ ;
+ $document
+ .off(elementNamespace)
+ ;
+ module.disconnect.menuObserver();
+ module.disconnect.selectObserver();
+ module.disconnect.classObserver();
+ },
+
+ observeChanges: function() {
+ if('MutationObserver' in window) {
+ selectObserver = new MutationObserver(module.event.select.mutation);
+ menuObserver = new MutationObserver(module.event.menu.mutation);
+ classObserver = new MutationObserver(module.event.class.mutation);
+ module.debug('Setting up mutation observer', selectObserver, menuObserver, classObserver);
+ module.observe.select();
+ module.observe.menu();
+ module.observe.class();
+ }
+ },
+
+ disconnect: {
+ menuObserver: function() {
+ if(menuObserver) {
+ menuObserver.disconnect();
+ }
+ },
+ selectObserver: function() {
+ if(selectObserver) {
+ selectObserver.disconnect();
+ }
+ },
+ classObserver: function() {
+ if(classObserver) {
+ classObserver.disconnect();
+ }
+ }
+ },
+ observe: {
+ select: function() {
+ if(module.has.input() && selectObserver) {
+ selectObserver.observe($module[0], {
+ childList : true,
+ subtree : true
+ });
+ }
+ },
+ menu: function() {
+ if(module.has.menu() && menuObserver) {
+ menuObserver.observe($menu[0], {
+ childList : true,
+ subtree : true
+ });
+ }
+ },
+ class: function() {
+ if(module.has.search() && classObserver) {
+ classObserver.observe($module[0], {
+ attributes : true
+ });
+ }
+ }
+ },
+
+ create: {
+ id: function() {
+ id = (Math.random().toString(16) + '000000000').substr(2, 8);
+ elementNamespace = '.' + id;
+ module.verbose('Creating unique id for element', id);
+ },
+ userChoice: function(values) {
+ var
+ $userChoices,
+ $userChoice,
+ isUserValue,
+ html
+ ;
+ values = values || module.get.userValues();
+ if(!values) {
+ return false;
+ }
+ values = Array.isArray(values)
+ ? values
+ : [values]
+ ;
+ $.each(values, function(index, value) {
+ if(module.get.item(value) === false) {
+ html = settings.templates.addition( module.add.variables(message.addResult, value) );
+ $userChoice = $('<div />')
+ .html(html)
+ .attr('data-' + metadata.value, value)
+ .attr('data-' + metadata.text, value)
+ .addClass(className.addition)
+ .addClass(className.item)
+ ;
+ if(settings.hideAdditions) {
+ $userChoice.addClass(className.hidden);
+ }
+ $userChoices = ($userChoices === undefined)
+ ? $userChoice
+ : $userChoices.add($userChoice)
+ ;
+ module.verbose('Creating user choices for value', value, $userChoice);
+ }
+ });
+ return $userChoices;
+ },
+ userLabels: function(value) {
+ var
+ userValues = module.get.userValues()
+ ;
+ if(userValues) {
+ module.debug('Adding user labels', userValues);
+ $.each(userValues, function(index, value) {
+ module.verbose('Adding custom user value');
+ module.add.label(value, value);
+ });
+ }
+ },
+ menu: function() {
+ $menu = $('<div />')
+ .addClass(className.menu)
+ .appendTo($module)
+ ;
+ },
+ sizer: function() {
+ $sizer = $('<span />')
+ .addClass(className.sizer)
+ .insertAfter($search)
+ ;
+ }
+ },
+
+ search: function(query) {
+ query = (query !== undefined)
+ ? query
+ : module.get.query()
+ ;
+ module.verbose('Searching for query', query);
+ if(module.has.minCharacters(query)) {
+ module.filter(query);
+ }
+ else {
+ module.hide(null,true);
+ }
+ },
+
+ select: {
+ firstUnfiltered: function() {
+ module.verbose('Selecting first non-filtered element');
+ module.remove.selectedItem();
+ $item
+ .not(selector.unselectable)
+ .not(selector.addition + selector.hidden)
+ .eq(0)
+ .addClass(className.selected)
+ ;
+ },
+ nextAvailable: function($selected) {
+ $selected = $selected.eq(0);
+ var
+ $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
+ $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
+ hasNext = ($nextAvailable.length > 0)
+ ;
+ if(hasNext) {
+ module.verbose('Moving selection to', $nextAvailable);
+ $nextAvailable.addClass(className.selected);
+ }
+ else {
+ module.verbose('Moving selection to', $prevAvailable);
+ $prevAvailable.addClass(className.selected);
+ }
+ }
+ },
+
+ setup: {
+ api: function() {
+ var
+ apiSettings = {
+ debug : settings.debug,
+ urlData : {
+ value : module.get.value(),
+ query : module.get.query()
+ },
+ on : false
+ }
+ ;
+ module.verbose('First request, initializing API');
+ $module
+ .api(apiSettings)
+ ;
+ },
+ layout: function() {
+ if( $module.is('select') ) {
+ module.setup.select();
+ module.setup.returnedObject();
+ }
+ if( !module.has.menu() ) {
+ module.create.menu();
+ }
+ if ( module.is.selection() && module.is.clearable() && !module.has.clearItem() ) {
+ module.verbose('Adding clear icon');
+ $clear = $('<i />')
+ .addClass('remove icon')
+ .insertBefore($text)
+ ;
+ }
+ if( module.is.search() && !module.has.search() ) {
+ module.verbose('Adding search input');
+ $search = $('<input />')
+ .addClass(className.search)
+ .prop('autocomplete', 'off')
+ .insertBefore($text)
+ ;
+ }
+ if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
+ module.create.sizer();
+ }
+ if(settings.allowTab) {
+ module.set.tabbable();
+ }
+ },
+ select: function() {
+ var
+ selectValues = module.get.selectValues()
+ ;
+ module.debug('Dropdown initialized on a select', selectValues);
+ if( $module.is('select') ) {
+ $input = $module;
+ }
+ // see if select is placed correctly already
+ if($input.parent(selector.dropdown).length > 0) {
+ module.debug('UI dropdown already exists. Creating dropdown menu only');
+ $module = $input.closest(selector.dropdown);
+ if( !module.has.menu() ) {
+ module.create.menu();
+ }
+ $menu = $module.children(selector.menu);
+ module.setup.menu(selectValues);
+ }
+ else {
+ module.debug('Creating entire dropdown from select');
+ $module = $('<div />')
+ .attr('class', $input.attr('class') )
+ .addClass(className.selection)
+ .addClass(className.dropdown)
+ .html( templates.dropdown(selectValues, fields, settings.preserveHTML, settings.className) )
+ .insertBefore($input)
+ ;
+ if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
+ module.error(error.missingMultiple);
+ $input.prop('multiple', true);
+ }
+ if($input.is('[multiple]')) {
+ module.set.multiple();
+ }
+ if ($input.prop('disabled')) {
+ module.debug('Disabling dropdown');
+ $module.addClass(className.disabled);
+ }
+ $input
+ .removeAttr('required')
+ .removeAttr('class')
+ .detach()
+ .prependTo($module)
+ ;
+ }
+ module.refresh();
+ },
+ menu: function(values) {
+ $menu.html( templates.menu(values, fields,settings.preserveHTML,settings.className));
+ $item = $menu.find(selector.item);
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
+ },
+ reference: function() {
+ module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
+ // replace module reference
+ $module = $module.parent(selector.dropdown);
+ instance = $module.data(moduleNamespace);
+ element = $module.get(0);
+ module.refresh();
+ module.setup.returnedObject();
+ },
+ returnedObject: function() {
+ var
+ $firstModules = $allModules.slice(0, elementIndex),
+ $lastModules = $allModules.slice(elementIndex + 1)
+ ;
+ // adjust all modules to use correct reference
+ $allModules = $firstModules.add($module).add($lastModules);
+ }
+ },
+
+ refresh: function() {
+ module.refreshSelectors();
+ module.refreshData();
+ },
+
+ refreshItems: function() {
+ $item = $menu.find(selector.item);
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
+ },
+
+ refreshSelectors: function() {
+ module.verbose('Refreshing selector cache');
+ $text = $module.find(selector.text);
+ $search = $module.find(selector.search);
+ $input = $module.find(selector.input);
+ $icon = $module.find(selector.icon);
+ $combo = ($module.prev().find(selector.text).length > 0)
+ ? $module.prev().find(selector.text)
+ : $module.prev()
+ ;
+ $menu = $module.children(selector.menu);
+ $item = $menu.find(selector.item);
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
+ },
+
+ refreshData: function() {
+ module.verbose('Refreshing cached metadata');
+ $item
+ .removeData(metadata.text)
+ .removeData(metadata.value)
+ ;
+ },
+
+ clearData: function() {
+ module.verbose('Clearing metadata');
+ $item
+ .removeData(metadata.text)
+ .removeData(metadata.value)
+ ;
+ $module
+ .removeData(metadata.defaultText)
+ .removeData(metadata.defaultValue)
+ .removeData(metadata.placeholderText)
+ ;
+ },
+
+ toggle: function() {
+ module.verbose('Toggling menu visibility');
+ if( !module.is.active() ) {
+ module.show();
+ }
+ else {
+ module.hide();
+ }
+ },
+
+ show: function(callback, preventFocus) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(!module.can.show() && module.is.remote()) {
+ module.debug('No API results retrieved, searching before show');
+ module.queryRemote(module.get.query(), module.show);
+ }
+ if( module.can.show() && !module.is.active() ) {
+ module.debug('Showing dropdown');
+ if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
+ module.remove.message();
+ }
+ if(module.is.allFiltered()) {
+ return true;
+ }
+ if(settings.onShow.call(element) !== false) {
+ module.animate.show(function() {
+ if( module.can.click() ) {
+ module.bind.intent();
+ }
+ if(module.has.search() && !preventFocus) {
+ module.focusSearch();
+ }
+ module.set.visible();
+ callback.call(element);
+ });
+ }
+ }
+ },
+
+ hide: function(callback, preventBlur) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( module.is.active() && !module.is.animatingOutward() ) {
+ module.debug('Hiding dropdown');
+ if(settings.onHide.call(element) !== false) {
+ module.animate.hide(function() {
+ module.remove.visible();
+ // hiding search focus
+ if ( module.is.focusedOnSearch() && preventBlur !== true ) {
+ $search.blur();
+ }
+ callback.call(element);
+ });
+ }
+ } else if( module.can.click() ) {
+ module.unbind.intent();
+ }
+ iconClicked = false;
+ },
+
+ hideOthers: function() {
+ module.verbose('Finding other dropdowns to hide');
+ $allModules
+ .not($module)
+ .has(selector.menu + '.' + className.visible)
+ .dropdown('hide')
+ ;
+ },
+
+ hideMenu: function() {
+ module.verbose('Hiding menu instantaneously');
+ module.remove.active();
+ module.remove.visible();
+ $menu.transition('hide');
+ },
+
+ hideSubMenus: function() {
+ var
+ $subMenus = $menu.children(selector.item).find(selector.menu)
+ ;
+ module.verbose('Hiding sub menus', $subMenus);
+ $subMenus.transition('hide');
+ },
+
+ bind: {
+ events: function() {
+ module.bind.keyboardEvents();
+ module.bind.inputEvents();
+ module.bind.mouseEvents();
+ },
+ keyboardEvents: function() {
+ module.verbose('Binding keyboard events');
+ $module
+ .on('keydown' + eventNamespace, module.event.keydown)
+ ;
+ if( module.has.search() ) {
+ $module
+ .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
+ ;
+ }
+ if( module.is.multiple() ) {
+ $document
+ .on('keydown' + elementNamespace, module.event.document.keydown)
+ ;
+ }
+ },
+ inputEvents: function() {
+ module.verbose('Binding input change events');
+ $module
+ .on('change' + eventNamespace, selector.input, module.event.change)
+ ;
+ },
+ mouseEvents: function() {
+ module.verbose('Binding mouse events');
+ if(module.is.multiple()) {
+ $module
+ .on(clickEvent + eventNamespace, selector.label, module.event.label.click)
+ .on(clickEvent + eventNamespace, selector.remove, module.event.remove.click)
+ ;
+ }
+ if( module.is.searchSelection() ) {
+ $module
+ .on('mousedown' + eventNamespace, module.event.mousedown)
+ .on('mouseup' + eventNamespace, module.event.mouseup)
+ .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown)
+ .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup)
+ .on(clickEvent + eventNamespace, selector.icon, module.event.icon.click)
+ .on(clickEvent + eventNamespace, selector.clearIcon, module.event.clearIcon.click)
+ .on('focus' + eventNamespace, selector.search, module.event.search.focus)
+ .on(clickEvent + eventNamespace, selector.search, module.event.search.focus)
+ .on('blur' + eventNamespace, selector.search, module.event.search.blur)
+ .on(clickEvent + eventNamespace, selector.text, module.event.text.focus)
+ ;
+ if(module.is.multiple()) {
+ $module
+ .on(clickEvent + eventNamespace, module.event.click)
+ ;
+ }
+ }
+ else {
+ if(settings.on == 'click') {
+ $module
+ .on(clickEvent + eventNamespace, selector.icon, module.event.icon.click)
+ .on(clickEvent + eventNamespace, module.event.test.toggle)
+ ;
+ }
+ else if(settings.on == 'hover') {
+ $module
+ .on('mouseenter' + eventNamespace, module.delay.show)
+ .on('mouseleave' + eventNamespace, module.delay.hide)
+ ;
+ }
+ else {
+ $module
+ .on(settings.on + eventNamespace, module.toggle)
+ ;
+ }
+ $module
+ .on('mousedown' + eventNamespace, module.event.mousedown)
+ .on('mouseup' + eventNamespace, module.event.mouseup)
+ .on('focus' + eventNamespace, module.event.focus)
+ .on(clickEvent + eventNamespace, selector.clearIcon, module.event.clearIcon.click)
+ ;
+ if(module.has.menuSearch() ) {
+ $module
+ .on('blur' + eventNamespace, selector.search, module.event.search.blur)
+ ;
+ }
+ else {
+ $module
+ .on('blur' + eventNamespace, module.event.blur)
+ ;
+ }
+ }
+ $menu
+ .on((hasTouch ? 'touchstart' : 'mouseenter') + eventNamespace, selector.item, module.event.item.mouseenter)
+ .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
+ .on('click' + eventNamespace, selector.item, module.event.item.click)
+ ;
+ },
+ intent: function() {
+ module.verbose('Binding hide intent event to document');
+ if(hasTouch) {
+ $document
+ .on('touchstart' + elementNamespace, module.event.test.touch)
+ .on('touchmove' + elementNamespace, module.event.test.touch)
+ ;
+ }
+ $document
+ .on(clickEvent + elementNamespace, module.event.test.hide)
+ ;
+ }
+ },
+
+ unbind: {
+ intent: function() {
+ module.verbose('Removing hide intent event from document');
+ if(hasTouch) {
+ $document
+ .off('touchstart' + elementNamespace)
+ .off('touchmove' + elementNamespace)
+ ;
+ }
+ $document
+ .off(clickEvent + elementNamespace)
+ ;
+ }
+ },
+
+ filter: function(query) {
+ var
+ searchTerm = (query !== undefined)
+ ? query
+ : module.get.query(),
+ afterFiltered = function() {
+ if(module.is.multiple()) {
+ module.filterActive();
+ }
+ if(query || (!query && module.get.activeItem().length == 0)) {
+ module.select.firstUnfiltered();
+ }
+ if( module.has.allResultsFiltered() ) {
+ if( settings.onNoResults.call(element, searchTerm) ) {
+ if(settings.allowAdditions) {
+ if(settings.hideAdditions) {
+ module.verbose('User addition with no menu, setting empty style');
+ module.set.empty();
+ module.hideMenu();
+ }
+ }
+ else {
+ module.verbose('All items filtered, showing message', searchTerm);
+ module.add.message(message.noResults);
+ }
+ }
+ else {
+ module.verbose('All items filtered, hiding dropdown', searchTerm);
+ module.hideMenu();
+ }
+ }
+ else {
+ module.remove.empty();
+ module.remove.message();
+ }
+ if(settings.allowAdditions) {
+ module.add.userSuggestion(module.escape.htmlEntities(query));
+ }
+ if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
+ module.show();
+ }
+ }
+ ;
+ if(settings.useLabels && module.has.maxSelections()) {
+ return;
+ }
+ if(settings.apiSettings) {
+ if( module.can.useAPI() ) {
+ module.queryRemote(searchTerm, function() {
+ if(settings.filterRemoteData) {
+ module.filterItems(searchTerm);
+ }
+ var preSelected = $input.val();
+ if(!Array.isArray(preSelected)) {
+ preSelected = preSelected && preSelected!=="" ? preSelected.split(settings.delimiter) : [];
+ }
+ $.each(preSelected,function(index,value){
+ $item.filter('[data-value="'+value+'"]')
+ .addClass(className.filtered)
+ ;
+ });
+ afterFiltered();
+ });
+ }
+ else {
+ module.error(error.noAPI);
+ }
+ }
+ else {
+ module.filterItems(searchTerm);
+ afterFiltered();
+ }
+ },
+
+ queryRemote: function(query, callback) {
+ var
+ apiSettings = {
+ errorDuration : false,
+ cache : 'local',
+ throttle : settings.throttle,
+ urlData : {
+ query: query
+ },
+ onError: function() {
+ module.add.message(message.serverError);
+ callback();
+ },
+ onFailure: function() {
+ module.add.message(message.serverError);
+ callback();
+ },
+ onSuccess : function(response) {
+ var
+ values = response[fields.remoteValues]
+ ;
+ if (!Array.isArray(values)){
+ values = [];
+ }
+ module.remove.message();
+ var menuConfig = {};
+ menuConfig[fields.values] = values;
+ module.setup.menu(menuConfig);
+
+ if(values.length===0 && !settings.allowAdditions) {
+ module.add.message(message.noResults);
+ }
+ callback();
+ }
+ }
+ ;
+ if( !$module.api('get request') ) {
+ module.setup.api();
+ }
+ apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
+ $module
+ .api('setting', apiSettings)
+ .api('query')
+ ;
+ },
+
+ filterItems: function(query) {
+ var
+ searchTerm = module.remove.diacritics(query !== undefined
+ ? query
+ : module.get.query()
+ ),
+ results = null,
+ escapedTerm = module.escape.string(searchTerm),
+ regExpFlags = (settings.ignoreSearchCase ? 'i' : '') + 'gm',
+ beginsWithRegExp = new RegExp('^' + escapedTerm, regExpFlags)
+ ;
+ // avoid loop if we're matching nothing
+ if( module.has.query() ) {
+ results = [];
+
+ module.verbose('Searching for matching values', searchTerm);
+ $item
+ .each(function(){
+ var
+ $choice = $(this),
+ text,
+ value
+ ;
+ if($choice.hasClass(className.unfilterable)) {
+ results.push(this);
+ return true;
+ }
+ if(settings.match === 'both' || settings.match === 'text') {
+ text = module.remove.diacritics(String(module.get.choiceText($choice, false)));
+ if(text.search(beginsWithRegExp) !== -1) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
+ results.push(this);
+ return true;
+ }
+ }
+ if(settings.match === 'both' || settings.match === 'value') {
+ value = module.remove.diacritics(String(module.get.choiceValue($choice, text)));
+ if(value.search(beginsWithRegExp) !== -1) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
+ results.push(this);
+ return true;
+ }
+ }
+ })
+ ;
+ }
+ module.debug('Showing only matched items', searchTerm);
+ module.remove.filteredItem();
+ if(results) {
+ $item
+ .not(results)
+ .addClass(className.filtered)
+ ;
+ }
+
+ if(!module.has.query()) {
+ $divider
+ .removeClass(className.hidden);
+ } else if(settings.hideDividers === true) {
+ $divider
+ .addClass(className.hidden);
+ } else if(settings.hideDividers === 'empty') {
+ $divider
+ .removeClass(className.hidden)
+ .filter(function() {
+ // First find the last divider in this divider group
+ // Dividers which are direct siblings are considered a group
+ var lastDivider = $(this).nextUntil(selector.item);
+
+ return (lastDivider.length ? lastDivider : $(this))
+ // Count all non-filtered items until the next divider (or end of the dropdown)
+ .nextUntil(selector.divider)
+ .filter(selector.item + ":not(." + className.filtered + ")")
+ // Hide divider if no items are found
+ .length === 0;
+ })
+ .addClass(className.hidden);
+ }
+ },
+
+ fuzzySearch: function(query, term) {
+ var
+ termLength = term.length,
+ queryLength = query.length
+ ;
+ query = (settings.ignoreSearchCase ? query.toLowerCase() : query);
+ term = (settings.ignoreSearchCase ? term.toLowerCase() : term);
+ if(queryLength > termLength) {
+ return false;
+ }
+ if(queryLength === termLength) {
+ return (query === term);
+ }
+ search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
+ var
+ queryCharacter = query.charCodeAt(characterIndex)
+ ;
+ while(nextCharacterIndex < termLength) {
+ if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
+ continue search;
+ }
+ }
+ return false;
+ }
+ return true;
+ },
+ exactSearch: function (query, term) {
+ query = (settings.ignoreSearchCase ? query.toLowerCase() : query);
+ term = (settings.ignoreSearchCase ? term.toLowerCase() : term);
+ return term.indexOf(query) > -1;
+
+ },
+ filterActive: function() {
+ if(settings.useLabels) {
+ $item.filter('.' + className.active)
+ .addClass(className.filtered)
+ ;
+ }
+ },
+
+ focusSearch: function(skipHandler) {
+ if( module.has.search() && !module.is.focusedOnSearch() ) {
+ if(skipHandler) {
+ $module.off('focus' + eventNamespace, selector.search);
+ $search.focus();
+ $module.on('focus' + eventNamespace, selector.search, module.event.search.focus);
+ }
+ else {
+ $search.focus();
+ }
+ }
+ },
+
+ blurSearch: function() {
+ if( module.has.search() ) {
+ $search.blur();
+ }
+ },
+
+ forceSelection: function() {
+ var
+ $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
+ $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0),
+ $selectedItem = ($currentlySelected.length > 0)
+ ? $currentlySelected
+ : $activeItem,
+ hasSelected = ($selectedItem.length > 0)
+ ;
+ if(settings.allowAdditions || (hasSelected && !module.is.multiple())) {
+ module.debug('Forcing partial selection to selected item', $selectedItem);
+ module.event.item.click.call($selectedItem, {}, true);
+ }
+ else {
+ module.remove.searchTerm();
+ }
+ },
+
+ change: {
+ values: function(values) {
+ if(!settings.allowAdditions) {
+ module.clear();
+ }
+ module.debug('Creating dropdown with specified values', values);
+ var menuConfig = {};
+ menuConfig[fields.values] = values;
+ module.setup.menu(menuConfig);
+ $.each(values, function(index, item) {
+ if(item.selected == true) {
+ module.debug('Setting initial selection to', item[fields.value]);
+ module.set.selected(item[fields.value]);
+ if(!module.is.multiple()) {
+ return false;
+ }
+ }
+ });
+
+ if(module.has.selectInput()) {
+ module.disconnect.selectObserver();
+ $input.html('');
+ $input.append('<option disabled selected value></option>');
+ $.each(values, function(index, item) {
+ var
+ value = settings.templates.deQuote(item[fields.value]),
+ name = settings.templates.escape(
+ item[fields.name] || '',
+ settings.preserveHTML
+ )
+ ;
+ $input.append('<option value="' + value + '">' + name + '</option>');
+ });
+ module.observe.select();
+ }
+ }
+ },
+
+ event: {
+ change: function() {
+ if(!internalChange) {
+ module.debug('Input changed, updating selection');
+ module.set.selected();
+ }
+ },
+ focus: function() {
+ if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
+ module.show();
+ }
+ },
+ blur: function(event) {
+ pageLostFocus = (document.activeElement === this);
+ if(!activated && !pageLostFocus) {
+ module.remove.activeLabel();
+ module.hide();
+ }
+ },
+ mousedown: function() {
+ if(module.is.searchSelection()) {
+ // prevent menu hiding on immediate re-focus
+ willRefocus = true;
+ }
+ else {
+ // prevents focus callback from occurring on mousedown
+ activated = true;
+ }
+ },
+ mouseup: function() {
+ if(module.is.searchSelection()) {
+ // prevent menu hiding on immediate re-focus
+ willRefocus = false;
+ }
+ else {
+ activated = false;
+ }
+ },
+ click: function(event) {
+ var
+ $target = $(event.target)
+ ;
+ // focus search
+ if($target.is($module)) {
+ if(!module.is.focusedOnSearch()) {
+ module.focusSearch();
+ }
+ else {
+ module.show();
+ }
+ }
+ },
+ search: {
+ focus: function(event) {
+ activated = true;
+ if(module.is.multiple()) {
+ module.remove.activeLabel();
+ }
+ if(settings.showOnFocus || (event.type !== 'focus' && event.type !== 'focusin')) {
+ module.search();
+ }
+ },
+ blur: function(event) {
+ pageLostFocus = (document.activeElement === this);
+ if(module.is.searchSelection() && !willRefocus) {
+ if(!itemActivated && !pageLostFocus) {
+ if(settings.forceSelection) {
+ module.forceSelection();
+ } else if(!settings.allowAdditions){
+ module.remove.searchTerm();
+ }
+ module.hide();
+ }
+ }
+ willRefocus = false;
+ }
+ },
+ clearIcon: {
+ click: function(event) {
+ module.clear();
+ if(module.is.searchSelection()) {
+ module.remove.searchTerm();
+ }
+ module.hide();
+ event.stopPropagation();
+ }
+ },
+ icon: {
+ click: function(event) {
+ iconClicked=true;
+ if(module.has.search()) {
+ if(!module.is.active()) {
+ if(settings.showOnFocus){
+ module.focusSearch();
+ } else {
+ module.toggle();
+ }
+ } else {
+ module.blurSearch();
+ }
+ } else {
+ module.toggle();
+ }
+ }
+ },
+ text: {
+ focus: function(event) {
+ activated = true;
+ module.focusSearch();
+ }
+ },
+ input: function(event) {
+ if(module.is.multiple() || module.is.searchSelection()) {
+ module.set.filtered();
+ }
+ clearTimeout(module.timer);
+ module.timer = setTimeout(module.search, settings.delay.search);
+ },
+ label: {
+ click: function(event) {
+ var
+ $label = $(this),
+ $labels = $module.find(selector.label),
+ $activeLabels = $labels.filter('.' + className.active),
+ $nextActive = $label.nextAll('.' + className.active),
+ $prevActive = $label.prevAll('.' + className.active),
+ $range = ($nextActive.length > 0)
+ ? $label.nextUntil($nextActive).add($activeLabels).add($label)
+ : $label.prevUntil($prevActive).add($activeLabels).add($label)
+ ;
+ if(event.shiftKey) {
+ $activeLabels.removeClass(className.active);
+ $range.addClass(className.active);
+ }
+ else if(event.ctrlKey) {
+ $label.toggleClass(className.active);
+ }
+ else {
+ $activeLabels.removeClass(className.active);
+ $label.addClass(className.active);
+ }
+ settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
+ }
+ },
+ remove: {
+ click: function() {
+ var
+ $label = $(this).parent()
+ ;
+ if( $label.hasClass(className.active) ) {
+ // remove all selected labels
+ module.remove.activeLabels();
+ }
+ else {
+ // remove this label only
+ module.remove.activeLabels( $label );
+ }
+ }
+ },
+ test: {
+ toggle: function(event) {
+ var
+ toggleBehavior = (module.is.multiple())
+ ? module.show
+ : module.toggle
+ ;
+ if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
+ return;
+ }
+ if( module.determine.eventOnElement(event, toggleBehavior) ) {
+ event.preventDefault();
+ }
+ },
+ touch: function(event) {
+ module.determine.eventOnElement(event, function() {
+ if(event.type == 'touchstart') {
+ module.timer = setTimeout(function() {
+ module.hide();
+ }, settings.delay.touch);
+ }
+ else if(event.type == 'touchmove') {
+ clearTimeout(module.timer);
+ }
+ });
+ event.stopPropagation();
+ },
+ hide: function(event) {
+ if(module.determine.eventInModule(event, module.hide)){
+ if(element.id && $(event.target).attr('for') === element.id){
+ event.preventDefault();
+ }
+ }
+ }
+ },
+ class: {
+ mutation: function(mutations) {
+ mutations.forEach(function(mutation) {
+ if(mutation.attributeName === "class") {
+ module.check.disabled();
+ }
+ });
+ }
+ },
+ select: {
+ mutation: function(mutations) {
+ module.debug('<select> modified, recreating menu');
+ if(module.is.selectMutation(mutations)) {
+ module.disconnect.selectObserver();
+ module.refresh();
+ module.setup.select();
+ module.set.selected();
+ module.observe.select();
+ }
+ }
+ },
+ menu: {
+ mutation: function(mutations) {
+ var
+ mutation = mutations[0],
+ $addedNode = mutation.addedNodes
+ ? $(mutation.addedNodes[0])
+ : $(false),
+ $removedNode = mutation.removedNodes
+ ? $(mutation.removedNodes[0])
+ : $(false),
+ $changedNodes = $addedNode.add($removedNode),
+ isUserAddition = $changedNodes.is(selector.addition) || $changedNodes.closest(selector.addition).length > 0,
+ isMessage = $changedNodes.is(selector.message) || $changedNodes.closest(selector.message).length > 0
+ ;
+ if(isUserAddition || isMessage) {
+ module.debug('Updating item selector cache');
+ module.refreshItems();
+ }
+ else {
+ module.debug('Menu modified, updating selector cache');
+ module.refresh();
+ }
+ },
+ mousedown: function() {
+ itemActivated = true;
+ },
+ mouseup: function() {
+ itemActivated = false;
+ }
+ },
+ item: {
+ mouseenter: function(event) {
+ var
+ $target = $(event.target),
+ $item = $(this),
+ $subMenu = $item.children(selector.menu),
+ $otherMenus = $item.siblings(selector.item).children(selector.menu),
+ hasSubMenu = ($subMenu.length > 0),
+ isBubbledEvent = ($subMenu.find($target).length > 0)
+ ;
+ if( !isBubbledEvent && hasSubMenu ) {
+ clearTimeout(module.itemTimer);
+ module.itemTimer = setTimeout(function() {
+ module.verbose('Showing sub-menu', $subMenu);
+ $.each($otherMenus, function() {
+ module.animate.hide(false, $(this));
+ });
+ module.animate.show(false, $subMenu);
+ }, settings.delay.show);
+ event.preventDefault();
+ }
+ },
+ mouseleave: function(event) {
+ var
+ $subMenu = $(this).children(selector.menu)
+ ;
+ if($subMenu.length > 0) {
+ clearTimeout(module.itemTimer);
+ module.itemTimer = setTimeout(function() {
+ module.verbose('Hiding sub-menu', $subMenu);
+ module.animate.hide(false, $subMenu);
+ }, settings.delay.hide);
+ }
+ },
+ click: function (event, skipRefocus) {
+ var
+ $choice = $(this),
+ $target = (event)
+ ? $(event.target)
+ : $(''),
+ $subMenu = $choice.find(selector.menu),
+ text = module.get.choiceText($choice),
+ value = module.get.choiceValue($choice, text),
+ hasSubMenu = ($subMenu.length > 0),
+ isBubbledEvent = ($subMenu.find($target).length > 0)
+ ;
+ // prevents IE11 bug where menu receives focus even though `tabindex=-1`
+ if (document.activeElement.tagName.toLowerCase() !== 'input') {
+ $(document.activeElement).blur();
+ }
+ if(!isBubbledEvent && (!hasSubMenu || settings.allowCategorySelection)) {
+ if(module.is.searchSelection()) {
+ if(settings.allowAdditions) {
+ module.remove.userAddition();
+ }
+ module.remove.searchTerm();
+ if(!module.is.focusedOnSearch() && !(skipRefocus == true)) {
+ module.focusSearch(true);
+ }
+ }
+ if(!settings.useLabels) {
+ module.remove.filteredItem();
+ module.set.scrollPosition($choice);
+ }
+ module.determine.selectAction.call(this, text, value);
+ }
+ }
+ },
+
+ document: {
+ // label selection should occur even when element has no focus
+ keydown: function(event) {
+ var
+ pressedKey = event.which,
+ isShortcutKey = module.is.inObject(pressedKey, keys)
+ ;
+ if(isShortcutKey) {
+ var
+ $label = $module.find(selector.label),
+ $activeLabel = $label.filter('.' + className.active),
+ activeValue = $activeLabel.data(metadata.value),
+ labelIndex = $label.index($activeLabel),
+ labelCount = $label.length,
+ hasActiveLabel = ($activeLabel.length > 0),
+ hasMultipleActive = ($activeLabel.length > 1),
+ isFirstLabel = (labelIndex === 0),
+ isLastLabel = (labelIndex + 1 == labelCount),
+ isSearch = module.is.searchSelection(),
+ isFocusedOnSearch = module.is.focusedOnSearch(),
+ isFocused = module.is.focused(),
+ caretAtStart = (isFocusedOnSearch && module.get.caretPosition(false) === 0),
+ isSelectedSearch = (caretAtStart && module.get.caretPosition(true) !== 0),
+ $nextLabel
+ ;
+ if(isSearch && !hasActiveLabel && !isFocusedOnSearch) {
+ return;
+ }
+
+ if(pressedKey == keys.leftArrow) {
+ // activate previous label
+ if((isFocused || caretAtStart) && !hasActiveLabel) {
+ module.verbose('Selecting previous label');
+ $label.last().addClass(className.active);
+ }
+ else if(hasActiveLabel) {
+ if(!event.shiftKey) {
+ module.verbose('Selecting previous label');
+ $label.removeClass(className.active);
+ }
+ else {
+ module.verbose('Adding previous label to selection');
+ }
+ if(isFirstLabel && !hasMultipleActive) {
+ $activeLabel.addClass(className.active);
+ }
+ else {
+ $activeLabel.prev(selector.siblingLabel)
+ .addClass(className.active)
+ .end()
+ ;
+ }
+ event.preventDefault();
+ }
+ }
+ else if(pressedKey == keys.rightArrow) {
+ // activate first label
+ if(isFocused && !hasActiveLabel) {
+ $label.first().addClass(className.active);
+ }
+ // activate next label
+ if(hasActiveLabel) {
+ if(!event.shiftKey) {
+ module.verbose('Selecting next label');
+ $label.removeClass(className.active);
+ }
+ else {
+ module.verbose('Adding next label to selection');
+ }
+ if(isLastLabel) {
+ if(isSearch) {
+ if(!isFocusedOnSearch) {
+ module.focusSearch();
+ }
+ else {
+ $label.removeClass(className.active);
+ }
+ }
+ else if(hasMultipleActive) {
+ $activeLabel.next(selector.siblingLabel).addClass(className.active);
+ }
+ else {
+ $activeLabel.addClass(className.active);
+ }
+ }
+ else {
+ $activeLabel.next(selector.siblingLabel).addClass(className.active);
+ }
+ event.preventDefault();
+ }
+ }
+ else if(pressedKey == keys.deleteKey || pressedKey == keys.backspace) {
+ if(hasActiveLabel) {
+ module.verbose('Removing active labels');
+ if(isLastLabel) {
+ if(isSearch && !isFocusedOnSearch) {
+ module.focusSearch();
+ }
+ }
+ $activeLabel.last().next(selector.siblingLabel).addClass(className.active);
+ module.remove.activeLabels($activeLabel);
+ event.preventDefault();
+ }
+ else if(caretAtStart && !isSelectedSearch && !hasActiveLabel && pressedKey == keys.backspace) {
+ module.verbose('Removing last label on input backspace');
+ $activeLabel = $label.last().addClass(className.active);
+ module.remove.activeLabels($activeLabel);
+ }
+ }
+ else {
+ $activeLabel.removeClass(className.active);
+ }
+ }
+ }
+ },
+
+ keydown: function(event) {
+ var
+ pressedKey = event.which,
+ isShortcutKey = module.is.inObject(pressedKey, keys)
+ ;
+ if(isShortcutKey) {
+ var
+ $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
+ $activeItem = $menu.children('.' + className.active).eq(0),
+ $selectedItem = ($currentlySelected.length > 0)
+ ? $currentlySelected
+ : $activeItem,
+ $visibleItems = ($selectedItem.length > 0)
+ ? $selectedItem.siblings(':not(.' + className.filtered +')').addBack()
+ : $menu.children(':not(.' + className.filtered +')'),
+ $subMenu = $selectedItem.children(selector.menu),
+ $parentMenu = $selectedItem.closest(selector.menu),
+ inVisibleMenu = ($parentMenu.hasClass(className.visible) || $parentMenu.hasClass(className.animating) || $parentMenu.parent(selector.menu).length > 0),
+ hasSubMenu = ($subMenu.length> 0),
+ hasSelectedItem = ($selectedItem.length > 0),
+ selectedIsSelectable = ($selectedItem.not(selector.unselectable).length > 0),
+ delimiterPressed = (pressedKey == keys.delimiter && settings.allowAdditions && module.is.multiple()),
+ isAdditionWithoutMenu = (settings.allowAdditions && settings.hideAdditions && (pressedKey == keys.enter || delimiterPressed) && selectedIsSelectable),
+ $nextItem,
+ isSubMenuItem,
+ newIndex
+ ;
+ // allow selection with menu closed
+ if(isAdditionWithoutMenu) {
+ module.verbose('Selecting item from keyboard shortcut', $selectedItem);
+ module.event.item.click.call($selectedItem, event);
+ if(module.is.searchSelection()) {
+ module.remove.searchTerm();
+ }
+ if(module.is.multiple()){
+ event.preventDefault();
+ }
+ }
+
+ // visible menu keyboard shortcuts
+ if( module.is.visible() ) {
+
+ // enter (select or open sub-menu)
+ if(pressedKey == keys.enter || delimiterPressed) {
+ if(pressedKey == keys.enter && hasSelectedItem && hasSubMenu && !settings.allowCategorySelection) {
+ module.verbose('Pressed enter on unselectable category, opening sub menu');
+ pressedKey = keys.rightArrow;
+ }
+ else if(selectedIsSelectable) {
+ module.verbose('Selecting item from keyboard shortcut', $selectedItem);
+ module.event.item.click.call($selectedItem, event);
+ if(module.is.searchSelection()) {
+ module.remove.searchTerm();
+ if(module.is.multiple()) {
+ $search.focus();
+ }
+ }
+ }
+ event.preventDefault();
+ }
+
+ // sub-menu actions
+ if(hasSelectedItem) {
+
+ if(pressedKey == keys.leftArrow) {
+
+ isSubMenuItem = ($parentMenu[0] !== $menu[0]);
+
+ if(isSubMenuItem) {
+ module.verbose('Left key pressed, closing sub-menu');
+ module.animate.hide(false, $parentMenu);
+ $selectedItem
+ .removeClass(className.selected)
+ ;
+ $parentMenu
+ .closest(selector.item)
+ .addClass(className.selected)
+ ;
+ event.preventDefault();
+ }
+ }
+
+ // right arrow (show sub-menu)
+ if(pressedKey == keys.rightArrow) {
+ if(hasSubMenu) {
+ module.verbose('Right key pressed, opening sub-menu');
+ module.animate.show(false, $subMenu);
+ $selectedItem
+ .removeClass(className.selected)
+ ;
+ $subMenu
+ .find(selector.item).eq(0)
+ .addClass(className.selected)
+ ;
+ event.preventDefault();
+ }
+ }
+ }
+
+ // up arrow (traverse menu up)
+ if(pressedKey == keys.upArrow) {
+ $nextItem = (hasSelectedItem && inVisibleMenu)
+ ? $selectedItem.prevAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
+ : $item.eq(0)
+ ;
+ if($visibleItems.index( $nextItem ) < 0) {
+ module.verbose('Up key pressed but reached top of current menu');
+ event.preventDefault();
+ return;
+ }
+ else {
+ module.verbose('Up key pressed, changing active item');
+ $selectedItem
+ .removeClass(className.selected)
+ ;
+ $nextItem
+ .addClass(className.selected)
+ ;
+ module.set.scrollPosition($nextItem);
+ if(settings.selectOnKeydown && module.is.single()) {
+ module.set.selectedItem($nextItem);
+ }
+ }
+ event.preventDefault();
+ }
+
+ // down arrow (traverse menu down)
+ if(pressedKey == keys.downArrow) {
+ $nextItem = (hasSelectedItem && inVisibleMenu)
+ ? $nextItem = $selectedItem.nextAll(selector.item + ':not(' + selector.unselectable + ')').eq(0)
+ : $item.eq(0)
+ ;
+ if($nextItem.length === 0) {
+ module.verbose('Down key pressed but reached bottom of current menu');
+ event.preventDefault();
+ return;
+ }
+ else {
+ module.verbose('Down key pressed, changing active item');
+ $item
+ .removeClass(className.selected)
+ ;
+ $nextItem
+ .addClass(className.selected)
+ ;
+ module.set.scrollPosition($nextItem);
+ if(settings.selectOnKeydown && module.is.single()) {
+ module.set.selectedItem($nextItem);
+ }
+ }
+ event.preventDefault();
+ }
+
+ // page down (show next page)
+ if(pressedKey == keys.pageUp) {
+ module.scrollPage('up');
+ event.preventDefault();
+ }
+ if(pressedKey == keys.pageDown) {
+ module.scrollPage('down');
+ event.preventDefault();
+ }
+
+ // escape (close menu)
+ if(pressedKey == keys.escape) {
+ module.verbose('Escape key pressed, closing dropdown');
+ module.hide();
+ }
+
+ }
+ else {
+ // delimiter key
+ if(delimiterPressed) {
+ event.preventDefault();
+ }
+ // down arrow (open menu)
+ if(pressedKey == keys.downArrow && !module.is.visible()) {
+ module.verbose('Down key pressed, showing dropdown');
+ module.show();
+ event.preventDefault();
+ }
+ }
+ }
+ else {
+ if( !module.has.search() ) {
+ module.set.selectedLetter( String.fromCharCode(pressedKey) );
+ }
+ }
+ }
+ },
+
+ trigger: {
+ change: function() {
+ var
+ inputElement = $input[0]
+ ;
+ if(inputElement) {
+ var events = document.createEvent('HTMLEvents');
+ module.verbose('Triggering native change event');
+ events.initEvent('change', true, false);
+ inputElement.dispatchEvent(events);
+ }
+ }
+ },
+
+ determine: {
+ selectAction: function(text, value) {
+ selectActionActive = true;
+ module.verbose('Determining action', settings.action);
+ if( $.isFunction( module.action[settings.action] ) ) {
+ module.verbose('Triggering preset action', settings.action, text, value);
+ module.action[ settings.action ].call(element, text, value, this);
+ }
+ else if( $.isFunction(settings.action) ) {
+ module.verbose('Triggering user action', settings.action, text, value);
+ settings.action.call(element, text, value, this);
+ }
+ else {
+ module.error(error.action, settings.action);
+ }
+ selectActionActive = false;
+ },
+ eventInModule: function(event, callback) {
+ var
+ $target = $(event.target),
+ inDocument = ($target.closest(document.documentElement).length > 0),
+ inModule = ($target.closest($module).length > 0)
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(inDocument && !inModule) {
+ module.verbose('Triggering event', callback);
+ callback();
+ return true;
+ }
+ else {
+ module.verbose('Event occurred in dropdown, canceling callback');
+ return false;
+ }
+ },
+ eventOnElement: function(event, callback) {
+ var
+ $target = $(event.target),
+ $label = $target.closest(selector.siblingLabel),
+ inVisibleDOM = document.body.contains(event.target),
+ notOnLabel = ($module.find($label).length === 0 || !(module.is.multiple() && settings.useLabels)),
+ notInMenu = ($target.closest($menu).length === 0)
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(inVisibleDOM && notOnLabel && notInMenu) {
+ module.verbose('Triggering event', callback);
+ callback();
+ return true;
+ }
+ else {
+ module.verbose('Event occurred in dropdown menu, canceling callback');
+ return false;
+ }
+ }
+ },
+
+ action: {
+
+ nothing: function() {},
+
+ activate: function(text, value, element) {
+ value = (value !== undefined)
+ ? value
+ : text
+ ;
+ if( module.can.activate( $(element) ) ) {
+ module.set.selected(value, $(element));
+ if(!module.is.multiple()) {
+ module.hideAndClear();
+ }
+ }
+ },
+
+ select: function(text, value, element) {
+ value = (value !== undefined)
+ ? value
+ : text
+ ;
+ if( module.can.activate( $(element) ) ) {
+ module.set.value(value, text, $(element));
+ if(!module.is.multiple()) {
+ module.hideAndClear();
+ }
+ }
+ },
+
+ combo: function(text, value, element) {
+ value = (value !== undefined)
+ ? value
+ : text
+ ;
+ module.set.selected(value, $(element));
+ module.hideAndClear();
+ },
+
+ hide: function(text, value, element) {
+ module.set.value(value, text, $(element));
+ module.hideAndClear();
+ }
+
+ },
+
+ get: {
+ id: function() {
+ return id;
+ },
+ defaultText: function() {
+ return $module.data(metadata.defaultText);
+ },
+ defaultValue: function() {
+ return $module.data(metadata.defaultValue);
+ },
+ placeholderText: function() {
+ if(settings.placeholder != 'auto' && typeof settings.placeholder == 'string') {
+ return settings.placeholder;
+ }
+ return $module.data(metadata.placeholderText) || '';
+ },
+ text: function() {
+ return settings.preserveHTML ? $text.html() : $text.text();
+ },
+ query: function() {
+ return String($search.val()).trim();
+ },
+ searchWidth: function(value) {
+ value = (value !== undefined)
+ ? value
+ : $search.val()
+ ;
+ $sizer.text(value);
+ // prevent rounding issues
+ return Math.ceil( $sizer.width() + 1);
+ },
+ selectionCount: function() {
+ var
+ values = module.get.values(),
+ count
+ ;
+ count = ( module.is.multiple() )
+ ? Array.isArray(values)
+ ? values.length
+ : 0
+ : (module.get.value() !== '')
+ ? 1
+ : 0
+ ;
+ return count;
+ },
+ transition: function($subMenu) {
+ return (settings.transition == 'auto')
+ ? module.is.upward($subMenu)
+ ? 'slide up'
+ : 'slide down'
+ : settings.transition
+ ;
+ },
+ userValues: function() {
+ var
+ values = module.get.values()
+ ;
+ if(!values) {
+ return false;
+ }
+ values = Array.isArray(values)
+ ? values
+ : [values]
+ ;
+ return $.grep(values, function(value) {
+ return (module.get.item(value) === false);
+ });
+ },
+ uniqueArray: function(array) {
+ return $.grep(array, function (value, index) {
+ return $.inArray(value, array) === index;
+ });
+ },
+ caretPosition: function(returnEndPos) {
+ var
+ input = $search.get(0),
+ range,
+ rangeLength
+ ;
+ if(returnEndPos && 'selectionEnd' in input){
+ return input.selectionEnd;
+ }
+ else if(!returnEndPos && 'selectionStart' in input) {
+ return input.selectionStart;
+ }
+ if (document.selection) {
+ input.focus();
+ range = document.selection.createRange();
+ rangeLength = range.text.length;
+ if(returnEndPos) {
+ return rangeLength;
+ }
+ range.moveStart('character', -input.value.length);
+ return range.text.length - rangeLength;
+ }
+ },
+ value: function() {
+ var
+ value = ($input.length > 0)
+ ? $input.val()
+ : $module.data(metadata.value),
+ isEmptyMultiselect = (Array.isArray(value) && value.length === 1 && value[0] === '')
+ ;
+ // prevents placeholder element from being selected when multiple
+ return (value === undefined || isEmptyMultiselect)
+ ? ''
+ : value
+ ;
+ },
+ values: function() {
+ var
+ value = module.get.value()
+ ;
+ if(value === '') {
+ return '';
+ }
+ return ( !module.has.selectInput() && module.is.multiple() )
+ ? (typeof value == 'string') // delimited string
+ ? module.escape.htmlEntities(value).split(settings.delimiter)
+ : ''
+ : value
+ ;
+ },
+ remoteValues: function() {
+ var
+ values = module.get.values(),
+ remoteValues = false
+ ;
+ if(values) {
+ if(typeof values == 'string') {
+ values = [values];
+ }
+ $.each(values, function(index, value) {
+ var
+ name = module.read.remoteData(value)
+ ;
+ module.verbose('Restoring value from session data', name, value);
+ if(name) {
+ if(!remoteValues) {
+ remoteValues = {};
+ }
+ remoteValues[value] = name;
+ }
+ });
+ }
+ return remoteValues;
+ },
+ choiceText: function($choice, preserveHTML) {
+ preserveHTML = (preserveHTML !== undefined)
+ ? preserveHTML
+ : settings.preserveHTML
+ ;
+ if($choice) {
+ if($choice.find(selector.menu).length > 0) {
+ module.verbose('Retrieving text of element with sub-menu');
+ $choice = $choice.clone();
+ $choice.find(selector.menu).remove();
+ $choice.find(selector.menuIcon).remove();
+ }
+ return ($choice.data(metadata.text) !== undefined)
+ ? $choice.data(metadata.text)
+ : (preserveHTML)
+ ? $choice.html().trim()
+ : $choice.text().trim()
+ ;
+ }
+ },
+ choiceValue: function($choice, choiceText) {
+ choiceText = choiceText || module.get.choiceText($choice);
+ if(!$choice) {
+ return false;
+ }
+ return ($choice.data(metadata.value) !== undefined)
+ ? String( $choice.data(metadata.value) )
+ : (typeof choiceText === 'string')
+ ? String(
+ settings.ignoreSearchCase
+ ? choiceText.toLowerCase()
+ : choiceText
+ ).trim()
+ : String(choiceText)
+ ;
+ },
+ inputEvent: function() {
+ var
+ input = $search[0]
+ ;
+ if(input) {
+ return (input.oninput !== undefined)
+ ? 'input'
+ : (input.onpropertychange !== undefined)
+ ? 'propertychange'
+ : 'keyup'
+ ;
+ }
+ return false;
+ },
+ selectValues: function() {
+ var
+ select = {},
+ oldGroup = [],
+ values = []
+ ;
+ $module
+ .find('option')
+ .each(function() {
+ var
+ $option = $(this),
+ name = $option.html(),
+ disabled = $option.attr('disabled'),
+ value = ( $option.attr('value') !== undefined )
+ ? $option.attr('value')
+ : name,
+ text = ( $option.data(metadata.text) !== undefined )
+ ? $option.data(metadata.text)
+ : name,
+ group = $option.parent('optgroup')
+ ;
+ if(settings.placeholder === 'auto' && value === '') {
+ select.placeholder = name;
+ }
+ else {
+ if(group.length !== oldGroup.length || group[0] !== oldGroup[0]) {
+ values.push({
+ type: 'header',
+ divider: settings.headerDivider,
+ name: group.attr('label') || ''
+ });
+ oldGroup = group;
+ }
+ values.push({
+ name : name,
+ value : value,
+ text : text,
+ disabled : disabled
+ });
+ }
+ })
+ ;
+ if(settings.placeholder && settings.placeholder !== 'auto') {
+ module.debug('Setting placeholder value to', settings.placeholder);
+ select.placeholder = settings.placeholder;
+ }
+ if(settings.sortSelect) {
+ if(settings.sortSelect === true) {
+ values.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
+ } else if(settings.sortSelect === 'natural') {
+ values.sort(function(a, b) {
+ return (a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ });
+ } else if($.isFunction(settings.sortSelect)) {
+ values.sort(settings.sortSelect);
+ }
+ select[fields.values] = values;
+ module.debug('Retrieved and sorted values from select', select);
+ }
+ else {
+ select[fields.values] = values;
+ module.debug('Retrieved values from select', select);
+ }
+ return select;
+ },
+ activeItem: function() {
+ return $item.filter('.' + className.active);
+ },
+ selectedItem: function() {
+ var
+ $selectedItem = $item.not(selector.unselectable).filter('.' + className.selected)
+ ;
+ return ($selectedItem.length > 0)
+ ? $selectedItem
+ : $item.eq(0)
+ ;
+ },
+ itemWithAdditions: function(value) {
+ var
+ $items = module.get.item(value),
+ $userItems = module.create.userChoice(value),
+ hasUserItems = ($userItems && $userItems.length > 0)
+ ;
+ if(hasUserItems) {
+ $items = ($items.length > 0)
+ ? $items.add($userItems)
+ : $userItems
+ ;
+ }
+ return $items;
+ },
+ item: function(value, strict) {
+ var
+ $selectedItem = false,
+ shouldSearch,
+ isMultiple
+ ;
+ value = (value !== undefined)
+ ? value
+ : ( module.get.values() !== undefined)
+ ? module.get.values()
+ : module.get.text()
+ ;
+ isMultiple = (module.is.multiple() && Array.isArray(value));
+ shouldSearch = (isMultiple)
+ ? (value.length > 0)
+ : (value !== undefined && value !== null)
+ ;
+ strict = (value === '' || value === false || value === true)
+ ? true
+ : strict || false
+ ;
+ if(shouldSearch) {
+ $item
+ .each(function() {
+ var
+ $choice = $(this),
+ optionText = module.get.choiceText($choice),
+ optionValue = module.get.choiceValue($choice, optionText)
+ ;
+ // safe early exit
+ if(optionValue === null || optionValue === undefined) {
+ return;
+ }
+ if(isMultiple) {
+ if($.inArray(module.escape.htmlEntities(String(optionValue)), value.map(function(v){return String(v);})) !== -1) {
+ $selectedItem = ($selectedItem)
+ ? $selectedItem.add($choice)
+ : $choice
+ ;
+ }
+ }
+ else if(strict) {
+ module.verbose('Ambiguous dropdown value using strict type check', $choice, value);
+ if( optionValue === value) {
+ $selectedItem = $choice;
+ return true;
+ }
+ }
+ else {
+ if(settings.ignoreCase) {
+ optionValue = optionValue.toLowerCase();
+ value = value.toLowerCase();
+ }
+ if(module.escape.htmlEntities(String(optionValue)) === module.escape.htmlEntities(String(value))) {
+ module.verbose('Found select item by value', optionValue, value);
+ $selectedItem = $choice;
+ return true;
+ }
+ }
+ })
+ ;
+ }
+ return $selectedItem;
+ }
+ },
+
+ check: {
+ maxSelections: function(selectionCount) {
+ if(settings.maxSelections) {
+ selectionCount = (selectionCount !== undefined)
+ ? selectionCount
+ : module.get.selectionCount()
+ ;
+ if(selectionCount >= settings.maxSelections) {
+ module.debug('Maximum selection count reached');
+ if(settings.useLabels) {
+ $item.addClass(className.filtered);
+ module.add.message(message.maxSelections);
+ }
+ return true;
+ }
+ else {
+ module.verbose('No longer at maximum selection count');
+ module.remove.message();
+ module.remove.filteredItem();
+ if(module.is.searchSelection()) {
+ module.filterItems();
+ }
+ return false;
+ }
+ }
+ return true;
+ },
+ disabled: function(){
+ $search.attr('tabindex',module.is.disabled() ? -1 : 0);
+ }
+ },
+
+ restore: {
+ defaults: function(preventChangeTrigger) {
+ module.clear(preventChangeTrigger);
+ module.restore.defaultText();
+ module.restore.defaultValue();
+ },
+ defaultText: function() {
+ var
+ defaultText = module.get.defaultText(),
+ placeholderText = module.get.placeholderText
+ ;
+ if(defaultText === placeholderText) {
+ module.debug('Restoring default placeholder text', defaultText);
+ module.set.placeholderText(defaultText);
+ }
+ else {
+ module.debug('Restoring default text', defaultText);
+ module.set.text(defaultText);
+ }
+ },
+ placeholderText: function() {
+ module.set.placeholderText();
+ },
+ defaultValue: function() {
+ var
+ defaultValue = module.get.defaultValue()
+ ;
+ if(defaultValue !== undefined) {
+ module.debug('Restoring default value', defaultValue);
+ if(defaultValue !== '') {
+ module.set.value(defaultValue);
+ module.set.selected();
+ }
+ else {
+ module.remove.activeItem();
+ module.remove.selectedItem();
+ }
+ }
+ },
+ labels: function() {
+ if(settings.allowAdditions) {
+ if(!settings.useLabels) {
+ module.error(error.labels);
+ settings.useLabels = true;
+ }
+ module.debug('Restoring selected values');
+ module.create.userLabels();
+ }
+ module.check.maxSelections();
+ },
+ selected: function() {
+ module.restore.values();
+ if(module.is.multiple()) {
+ module.debug('Restoring previously selected values and labels');
+ module.restore.labels();
+ }
+ else {
+ module.debug('Restoring previously selected values');
+ }
+ },
+ values: function() {
+ // prevents callbacks from occurring on initial load
+ module.set.initialLoad();
+ if(settings.apiSettings && settings.saveRemoteData && module.get.remoteValues()) {
+ module.restore.remoteValues();
+ }
+ else {
+ module.set.selected();
+ }
+ var value = module.get.value();
+ if(value && value !== '' && !(Array.isArray(value) && value.length === 0)) {
+ $input.removeClass(className.noselection);
+ } else {
+ $input.addClass(className.noselection);
+ }
+ module.remove.initialLoad();
+ },
+ remoteValues: function() {
+ var
+ values = module.get.remoteValues()
+ ;
+ module.debug('Recreating selected from session data', values);
+ if(values) {
+ if( module.is.single() ) {
+ $.each(values, function(value, name) {
+ module.set.text(name);
+ });
+ }
+ else {
+ $.each(values, function(value, name) {
+ module.add.label(value, name);
+ });
+ }
+ }
+ }
+ },
+
+ read: {
+ remoteData: function(value) {
+ var
+ name
+ ;
+ if(window.Storage === undefined) {
+ module.error(error.noStorage);
+ return;
+ }
+ name = sessionStorage.getItem(value);
+ return (name !== undefined)
+ ? name
+ : false
+ ;
+ }
+ },
+
+ save: {
+ defaults: function() {
+ module.save.defaultText();
+ module.save.placeholderText();
+ module.save.defaultValue();
+ },
+ defaultValue: function() {
+ var
+ value = module.get.value()
+ ;
+ module.verbose('Saving default value as', value);
+ $module.data(metadata.defaultValue, value);
+ },
+ defaultText: function() {
+ var
+ text = module.get.text()
+ ;
+ module.verbose('Saving default text as', text);
+ $module.data(metadata.defaultText, text);
+ },
+ placeholderText: function() {
+ var
+ text
+ ;
+ if(settings.placeholder !== false && $text.hasClass(className.placeholder)) {
+ text = module.get.text();
+ module.verbose('Saving placeholder text as', text);
+ $module.data(metadata.placeholderText, text);
+ }
+ },
+ remoteData: function(name, value) {
+ if(window.Storage === undefined) {
+ module.error(error.noStorage);
+ return;
+ }
+ module.verbose('Saving remote data to session storage', value, name);
+ sessionStorage.setItem(value, name);
+ }
+ },
+
+ clear: function(preventChangeTrigger) {
+ if(module.is.multiple() && settings.useLabels) {
+ module.remove.labels();
+ }
+ else {
+ module.remove.activeItem();
+ module.remove.selectedItem();
+ module.remove.filteredItem();
+ }
+ module.set.placeholderText();
+ module.clearValue(preventChangeTrigger);
+ },
+
+ clearValue: function(preventChangeTrigger) {
+ module.set.value('', null, null, preventChangeTrigger);
+ },
+
+ scrollPage: function(direction, $selectedItem) {
+ var
+ $currentItem = $selectedItem || module.get.selectedItem(),
+ $menu = $currentItem.closest(selector.menu),
+ menuHeight = $menu.outerHeight(),
+ currentScroll = $menu.scrollTop(),
+ itemHeight = $item.eq(0).outerHeight(),
+ itemsPerPage = Math.floor(menuHeight / itemHeight),
+ maxScroll = $menu.prop('scrollHeight'),
+ newScroll = (direction == 'up')
+ ? currentScroll - (itemHeight * itemsPerPage)
+ : currentScroll + (itemHeight * itemsPerPage),
+ $selectableItem = $item.not(selector.unselectable),
+ isWithinRange,
+ $nextSelectedItem,
+ elementIndex
+ ;
+ elementIndex = (direction == 'up')
+ ? $selectableItem.index($currentItem) - itemsPerPage
+ : $selectableItem.index($currentItem) + itemsPerPage
+ ;
+ isWithinRange = (direction == 'up')
+ ? (elementIndex >= 0)
+ : (elementIndex < $selectableItem.length)
+ ;
+ $nextSelectedItem = (isWithinRange)
+ ? $selectableItem.eq(elementIndex)
+ : (direction == 'up')
+ ? $selectableItem.first()
+ : $selectableItem.last()
+ ;
+ if($nextSelectedItem.length > 0) {
+ module.debug('Scrolling page', direction, $nextSelectedItem);
+ $currentItem
+ .removeClass(className.selected)
+ ;
+ $nextSelectedItem
+ .addClass(className.selected)
+ ;
+ if(settings.selectOnKeydown && module.is.single()) {
+ module.set.selectedItem($nextSelectedItem);
+ }
+ $menu
+ .scrollTop(newScroll)
+ ;
+ }
+ },
+
+ set: {
+ filtered: function() {
+ var
+ isMultiple = module.is.multiple(),
+ isSearch = module.is.searchSelection(),
+ isSearchMultiple = (isMultiple && isSearch),
+ searchValue = (isSearch)
+ ? module.get.query()
+ : '',
+ hasSearchValue = (typeof searchValue === 'string' && searchValue.length > 0),
+ searchWidth = module.get.searchWidth(),
+ valueIsSet = searchValue !== ''
+ ;
+ if(isMultiple && hasSearchValue) {
+ module.verbose('Adjusting input width', searchWidth, settings.glyphWidth);
+ $search.css('width', searchWidth);
+ }
+ if(hasSearchValue || (isSearchMultiple && valueIsSet)) {
+ module.verbose('Hiding placeholder text');
+ $text.addClass(className.filtered);
+ }
+ else if(!isMultiple || (isSearchMultiple && !valueIsSet)) {
+ module.verbose('Showing placeholder text');
+ $text.removeClass(className.filtered);
+ }
+ },
+ empty: function() {
+ $module.addClass(className.empty);
+ },
+ loading: function() {
+ $module.addClass(className.loading);
+ },
+ placeholderText: function(text) {
+ text = text || module.get.placeholderText();
+ module.debug('Setting placeholder text', text);
+ module.set.text(text);
+ $text.addClass(className.placeholder);
+ },
+ tabbable: function() {
+ if( module.is.searchSelection() ) {
+ module.debug('Added tabindex to searchable dropdown');
+ $search
+ .val('')
+ ;
+ module.check.disabled();
+ $menu
+ .attr('tabindex', -1)
+ ;
+ }
+ else {
+ module.debug('Added tabindex to dropdown');
+ if( $module.attr('tabindex') === undefined) {
+ $module
+ .attr('tabindex', 0)
+ ;
+ $menu
+ .attr('tabindex', -1)
+ ;
+ }
+ }
+ },
+ initialLoad: function() {
+ module.verbose('Setting initial load');
+ initialLoad = true;
+ },
+ activeItem: function($item) {
+ if( settings.allowAdditions && $item.filter(selector.addition).length > 0 ) {
+ $item.addClass(className.filtered);
+ }
+ else {
+ $item.addClass(className.active);
+ }
+ },
+ partialSearch: function(text) {
+ var
+ length = module.get.query().length
+ ;
+ $search.val( text.substr(0, length));
+ },
+ scrollPosition: function($item, forceScroll) {
+ var
+ edgeTolerance = 5,
+ $menu,
+ hasActive,
+ offset,
+ itemHeight,
+ itemOffset,
+ menuOffset,
+ menuScroll,
+ menuHeight,
+ abovePage,
+ belowPage
+ ;
+
+ $item = $item || module.get.selectedItem();
+ $menu = $item.closest(selector.menu);
+ hasActive = ($item && $item.length > 0);
+ forceScroll = (forceScroll !== undefined)
+ ? forceScroll
+ : false
+ ;
+ if(module.get.activeItem().length === 0){
+ forceScroll = false;
+ }
+ if($item && $menu.length > 0 && hasActive) {
+ itemOffset = $item.position().top;
+
+ $menu.addClass(className.loading);
+ menuScroll = $menu.scrollTop();
+ menuOffset = $menu.offset().top;
+ itemOffset = $item.offset().top;
+ offset = menuScroll - menuOffset + itemOffset;
+ if(!forceScroll) {
+ menuHeight = $menu.height();
+ belowPage = menuScroll + menuHeight < (offset + edgeTolerance);
+ abovePage = ((offset - edgeTolerance) < menuScroll);
+ }
+ module.debug('Scrolling to active item', offset);
+ if(forceScroll || abovePage || belowPage) {
+ $menu.scrollTop(offset);
+ }
+ $menu.removeClass(className.loading);
+ }
+ },
+ text: function(text) {
+ if(settings.action === 'combo') {
+ module.debug('Changing combo button text', text, $combo);
+ if(settings.preserveHTML) {
+ $combo.html(text);
+ }
+ else {
+ $combo.text(text);
+ }
+ }
+ else if(settings.action === 'activate') {
+ if(text !== module.get.placeholderText()) {
+ $text.removeClass(className.placeholder);
+ }
+ module.debug('Changing text', text, $text);
+ $text
+ .removeClass(className.filtered)
+ ;
+ if(settings.preserveHTML) {
+ $text.html(text);
+ }
+ else {
+ $text.text(text);
+ }
+ }
+ },
+ selectedItem: function($item) {
+ var
+ value = module.get.choiceValue($item),
+ searchText = module.get.choiceText($item, false),
+ text = module.get.choiceText($item, true)
+ ;
+ module.debug('Setting user selection to item', $item);
+ module.remove.activeItem();
+ module.set.partialSearch(searchText);
+ module.set.activeItem($item);
+ module.set.selected(value, $item);
+ module.set.text(text);
+ },
+ selectedLetter: function(letter) {
+ var
+ $selectedItem = $item.filter('.' + className.selected),
+ alreadySelectedLetter = $selectedItem.length > 0 && module.has.firstLetter($selectedItem, letter),
+ $nextValue = false,
+ $nextItem
+ ;
+ // check next of same letter
+ if(alreadySelectedLetter) {
+ $nextItem = $selectedItem.nextAll($item).eq(0);
+ if( module.has.firstLetter($nextItem, letter) ) {
+ $nextValue = $nextItem;
+ }
+ }
+ // check all values
+ if(!$nextValue) {
+ $item
+ .each(function(){
+ if(module.has.firstLetter($(this), letter)) {
+ $nextValue = $(this);
+ return false;
+ }
+ })
+ ;
+ }
+ // set next value
+ if($nextValue) {
+ module.verbose('Scrolling to next value with letter', letter);
+ module.set.scrollPosition($nextValue);
+ $selectedItem.removeClass(className.selected);
+ $nextValue.addClass(className.selected);
+ if(settings.selectOnKeydown && module.is.single()) {
+ module.set.selectedItem($nextValue);
+ }
+ }
+ },
+ direction: function($menu) {
+ if(settings.direction == 'auto') {
+ // reset position, remove upward if it's base menu
+ if (!$menu) {
+ module.remove.upward();
+ } else if (module.is.upward($menu)) {
+ //we need make sure when make assertion openDownward for $menu, $menu does not have upward class
+ module.remove.upward($menu);
+ }
+
+ if(module.can.openDownward($menu)) {
+ module.remove.upward($menu);
+ }
+ else {
+ module.set.upward($menu);
+ }
+ if(!module.is.leftward($menu) && !module.can.openRightward($menu)) {
+ module.set.leftward($menu);
+ }
+ }
+ else if(settings.direction == 'upward') {
+ module.set.upward($menu);
+ }
+ },
+ upward: function($currentMenu) {
+ var $element = $currentMenu || $module;
+ $element.addClass(className.upward);
+ },
+ leftward: function($currentMenu) {
+ var $element = $currentMenu || $menu;
+ $element.addClass(className.leftward);
+ },
+ value: function(value, text, $selected, preventChangeTrigger) {
+ if(value !== undefined && value !== '' && !(Array.isArray(value) && value.length === 0)) {
+ $input.removeClass(className.noselection);
+ } else {
+ $input.addClass(className.noselection);
+ }
+ var
+ escapedValue = module.escape.value(value),
+ hasInput = ($input.length > 0),
+ currentValue = module.get.values(),
+ stringValue = (value !== undefined)
+ ? String(value)
+ : value,
+ newValue
+ ;
+ if(hasInput) {
+ if(!settings.allowReselection && stringValue == currentValue) {
+ module.verbose('Skipping value update already same value', value, currentValue);
+ if(!module.is.initialLoad()) {
+ return;
+ }
+ }
+
+ if( module.is.single() && module.has.selectInput() && module.can.extendSelect() ) {
+ module.debug('Adding user option', value);
+ module.add.optionValue(value);
+ }
+ module.debug('Updating input value', escapedValue, currentValue);
+ internalChange = true;
+ $input
+ .val(escapedValue)
+ ;
+ if(settings.fireOnInit === false && module.is.initialLoad()) {
+ module.debug('Input native change event ignored on initial load');
+ }
+ else if(preventChangeTrigger !== true) {
+ module.trigger.change();
+ }
+ internalChange = false;
+ }
+ else {
+ module.verbose('Storing value in metadata', escapedValue, $input);
+ if(escapedValue !== currentValue) {
+ $module.data(metadata.value, stringValue);
+ }
+ }
+ if(settings.fireOnInit === false && module.is.initialLoad()) {
+ module.verbose('No callback on initial load', settings.onChange);
+ }
+ else if(preventChangeTrigger !== true) {
+ settings.onChange.call(element, value, text, $selected);
+ }
+ },
+ active: function() {
+ $module
+ .addClass(className.active)
+ ;
+ },
+ multiple: function() {
+ $module.addClass(className.multiple);
+ },
+ visible: function() {
+ $module.addClass(className.visible);
+ },
+ exactly: function(value, $selectedItem) {
+ module.debug('Setting selected to exact values');
+ module.clear();
+ module.set.selected(value, $selectedItem);
+ },
+ selected: function(value, $selectedItem) {
+ var
+ isMultiple = module.is.multiple()
+ ;
+ $selectedItem = (settings.allowAdditions)
+ ? $selectedItem || module.get.itemWithAdditions(value)
+ : $selectedItem || module.get.item(value)
+ ;
+ if(!$selectedItem) {
+ return;
+ }
+ module.debug('Setting selected menu item to', $selectedItem);
+ if(module.is.multiple()) {
+ module.remove.searchWidth();
+ }
+ if(module.is.single()) {
+ module.remove.activeItem();
+ module.remove.selectedItem();
+ }
+ else if(settings.useLabels) {
+ module.remove.selectedItem();
+ }
+ // select each item
+ $selectedItem
+ .each(function() {
+ var
+ $selected = $(this),
+ selectedText = module.get.choiceText($selected),
+ selectedValue = module.get.choiceValue($selected, selectedText),
+
+ isFiltered = $selected.hasClass(className.filtered),
+ isActive = $selected.hasClass(className.active),
+ isUserValue = $selected.hasClass(className.addition),
+ shouldAnimate = (isMultiple && $selectedItem.length == 1)
+ ;
+ if(isMultiple) {
+ if(!isActive || isUserValue) {
+ if(settings.apiSettings && settings.saveRemoteData) {
+ module.save.remoteData(selectedText, selectedValue);
+ }
+ if(settings.useLabels) {
+ module.add.label(selectedValue, selectedText, shouldAnimate);
+ module.add.value(selectedValue, selectedText, $selected);
+ module.set.activeItem($selected);
+ module.filterActive();
+ module.select.nextAvailable($selectedItem);
+ }
+ else {
+ module.add.value(selectedValue, selectedText, $selected);
+ module.set.text(module.add.variables(message.count));
+ module.set.activeItem($selected);
+ }
+ }
+ else if(!isFiltered && (settings.useLabels || selectActionActive)) {
+ module.debug('Selected active value, removing label');
+ module.remove.selected(selectedValue);
+ }
+ }
+ else {
+ if(settings.apiSettings && settings.saveRemoteData) {
+ module.save.remoteData(selectedText, selectedValue);
+ }
+ module.set.text(selectedText);
+ module.set.value(selectedValue, selectedText, $selected);
+ $selected
+ .addClass(className.active)
+ .addClass(className.selected)
+ ;
+ }
+ })
+ ;
+ module.remove.searchTerm();
+ }
+ },
+
+ add: {
+ label: function(value, text, shouldAnimate) {
+ var
+ $next = module.is.searchSelection()
+ ? $search
+ : $text,
+ escapedValue = module.escape.value(value),
+ $label
+ ;
+ if(settings.ignoreCase) {
+ escapedValue = escapedValue.toLowerCase();
+ }
+ $label = $('<a />')
+ .addClass(className.label)
+ .attr('data-' + metadata.value, escapedValue)
+ .html(templates.label(escapedValue, text, settings.preserveHTML, settings.className))
+ ;
+ $label = settings.onLabelCreate.call($label, escapedValue, text);
+
+ if(module.has.label(value)) {
+ module.debug('User selection already exists, skipping', escapedValue);
+ return;
+ }
+ if(settings.label.variation) {
+ $label.addClass(settings.label.variation);
+ }
+ if(shouldAnimate === true) {
+ module.debug('Animating in label', $label);
+ $label
+ .addClass(className.hidden)
+ .insertBefore($next)
+ .transition({
+ animation : settings.label.transition,
+ debug : settings.debug,
+ verbose : settings.verbose,
+ duration : settings.label.duration
+ })
+ ;
+ }
+ else {
+ module.debug('Adding selection label', $label);
+ $label
+ .insertBefore($next)
+ ;
+ }
+ },
+ message: function(message) {
+ var
+ $message = $menu.children(selector.message),
+ html = settings.templates.message(module.add.variables(message))
+ ;
+ if($message.length > 0) {
+ $message
+ .html(html)
+ ;
+ }
+ else {
+ $message = $('<div/>')
+ .html(html)
+ .addClass(className.message)
+ .appendTo($menu)
+ ;
+ }
+ },
+ optionValue: function(value) {
+ var
+ escapedValue = module.escape.value(value),
+ $option = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
+ hasOption = ($option.length > 0)
+ ;
+ if(hasOption) {
+ return;
+ }
+ // temporarily disconnect observer
+ module.disconnect.selectObserver();
+ if( module.is.single() ) {
+ module.verbose('Removing previous user addition');
+ $input.find('option.' + className.addition).remove();
+ }
+ $('<option/>')
+ .prop('value', escapedValue)
+ .addClass(className.addition)
+ .html(value)
+ .appendTo($input)
+ ;
+ module.verbose('Adding user addition as an <option>', value);
+ module.observe.select();
+ },
+ userSuggestion: function(value) {
+ var
+ $addition = $menu.children(selector.addition),
+ $existingItem = module.get.item(value),
+ alreadyHasValue = $existingItem && $existingItem.not(selector.addition).length,
+ hasUserSuggestion = $addition.length > 0,
+ html
+ ;
+ if(settings.useLabels && module.has.maxSelections()) {
+ return;
+ }
+ if(value === '' || alreadyHasValue) {
+ $addition.remove();
+ return;
+ }
+ if(hasUserSuggestion) {
+ $addition
+ .data(metadata.value, value)
+ .data(metadata.text, value)
+ .attr('data-' + metadata.value, value)
+ .attr('data-' + metadata.text, value)
+ .removeClass(className.filtered)
+ ;
+ if(!settings.hideAdditions) {
+ html = settings.templates.addition( module.add.variables(message.addResult, value) );
+ $addition
+ .html(html)
+ ;
+ }
+ module.verbose('Replacing user suggestion with new value', $addition);
+ }
+ else {
+ $addition = module.create.userChoice(value);
+ $addition
+ .prependTo($menu)
+ ;
+ module.verbose('Adding item choice to menu corresponding with user choice addition', $addition);
+ }
+ if(!settings.hideAdditions || module.is.allFiltered()) {
+ $addition
+ .addClass(className.selected)
+ .siblings()
+ .removeClass(className.selected)
+ ;
+ }
+ module.refreshItems();
+ },
+ variables: function(message, term) {
+ var
+ hasCount = (message.search('{count}') !== -1),
+ hasMaxCount = (message.search('{maxCount}') !== -1),
+ hasTerm = (message.search('{term}') !== -1),
+ count,
+ query
+ ;
+ module.verbose('Adding templated variables to message', message);
+ if(hasCount) {
+ count = module.get.selectionCount();
+ message = message.replace('{count}', count);
+ }
+ if(hasMaxCount) {
+ count = module.get.selectionCount();
+ message = message.replace('{maxCount}', settings.maxSelections);
+ }
+ if(hasTerm) {
+ query = term || module.get.query();
+ message = message.replace('{term}', query);
+ }
+ return message;
+ },
+ value: function(addedValue, addedText, $selectedItem) {
+ var
+ currentValue = module.get.values(),
+ newValue
+ ;
+ if(module.has.value(addedValue)) {
+ module.debug('Value already selected');
+ return;
+ }
+ if(addedValue === '') {
+ module.debug('Cannot select blank values from multiselect');
+ return;
+ }
+ // extend current array
+ if(Array.isArray(currentValue)) {
+ newValue = currentValue.concat([addedValue]);
+ newValue = module.get.uniqueArray(newValue);
+ }
+ else {
+ newValue = [addedValue];
+ }
+ // add values
+ if( module.has.selectInput() ) {
+ if(module.can.extendSelect()) {
+ module.debug('Adding value to select', addedValue, newValue, $input);
+ module.add.optionValue(addedValue);
+ }
+ }
+ else {
+ newValue = newValue.join(settings.delimiter);
+ module.debug('Setting hidden input to delimited value', newValue, $input);
+ }
+
+ if(settings.fireOnInit === false && module.is.initialLoad()) {
+ module.verbose('Skipping onadd callback on initial load', settings.onAdd);
+ }
+ else {
+ settings.onAdd.call(element, addedValue, addedText, $selectedItem);
+ }
+ module.set.value(newValue, addedText, $selectedItem);
+ module.check.maxSelections();
+ },
+ },
+
+ remove: {
+ active: function() {
+ $module.removeClass(className.active);
+ },
+ activeLabel: function() {
+ $module.find(selector.label).removeClass(className.active);
+ },
+ empty: function() {
+ $module.removeClass(className.empty);
+ },
+ loading: function() {
+ $module.removeClass(className.loading);
+ },
+ initialLoad: function() {
+ initialLoad = false;
+ },
+ upward: function($currentMenu) {
+ var $element = $currentMenu || $module;
+ $element.removeClass(className.upward);
+ },
+ leftward: function($currentMenu) {
+ var $element = $currentMenu || $menu;
+ $element.removeClass(className.leftward);
+ },
+ visible: function() {
+ $module.removeClass(className.visible);
+ },
+ activeItem: function() {
+ $item.removeClass(className.active);
+ },
+ filteredItem: function() {
+ if(settings.useLabels && module.has.maxSelections() ) {
+ return;
+ }
+ if(settings.useLabels && module.is.multiple()) {
+ $item.not('.' + className.active).removeClass(className.filtered);
+ }
+ else {
+ $item.removeClass(className.filtered);
+ }
+ if(settings.hideDividers) {
+ $divider.removeClass(className.hidden);
+ }
+ module.remove.empty();
+ },
+ optionValue: function(value) {
+ var
+ escapedValue = module.escape.value(value),
+ $option = $input.find('option[value="' + module.escape.string(escapedValue) + '"]'),
+ hasOption = ($option.length > 0)
+ ;
+ if(!hasOption || !$option.hasClass(className.addition)) {
+ return;
+ }
+ // temporarily disconnect observer
+ if(selectObserver) {
+ selectObserver.disconnect();
+ module.verbose('Temporarily disconnecting mutation observer');
+ }
+ $option.remove();
+ module.verbose('Removing user addition as an <option>', escapedValue);
+ if(selectObserver) {
+ selectObserver.observe($input[0], {
+ childList : true,
+ subtree : true
+ });
+ }
+ },
+ message: function() {
+ $menu.children(selector.message).remove();
+ },
+ searchWidth: function() {
+ $search.css('width', '');
+ },
+ searchTerm: function() {
+ module.verbose('Cleared search term');
+ $search.val('');
+ module.set.filtered();
+ },
+ userAddition: function() {
+ $item.filter(selector.addition).remove();
+ },
+ selected: function(value, $selectedItem) {
+ $selectedItem = (settings.allowAdditions)
+ ? $selectedItem || module.get.itemWithAdditions(value)
+ : $selectedItem || module.get.item(value)
+ ;
+
+ if(!$selectedItem) {
+ return false;
+ }
+
+ $selectedItem
+ .each(function() {
+ var
+ $selected = $(this),
+ selectedText = module.get.choiceText($selected),
+ selectedValue = module.get.choiceValue($selected, selectedText)
+ ;
+ if(module.is.multiple()) {
+ if(settings.useLabels) {
+ module.remove.value(selectedValue, selectedText, $selected);
+ module.remove.label(selectedValue);
+ }
+ else {
+ module.remove.value(selectedValue, selectedText, $selected);
+ if(module.get.selectionCount() === 0) {
+ module.set.placeholderText();
+ }
+ else {
+ module.set.text(module.add.variables(message.count));
+ }
+ }
+ }
+ else {
+ module.remove.value(selectedValue, selectedText, $selected);
+ }
+ $selected
+ .removeClass(className.filtered)
+ .removeClass(className.active)
+ ;
+ if(settings.useLabels) {
+ $selected.removeClass(className.selected);
+ }
+ })
+ ;
+ },
+ selectedItem: function() {
+ $item.removeClass(className.selected);
+ },
+ value: function(removedValue, removedText, $removedItem) {
+ var
+ values = module.get.values(),
+ newValue
+ ;
+ removedValue = module.escape.htmlEntities(removedValue);
+ if( module.has.selectInput() ) {
+ module.verbose('Input is <select> removing selected option', removedValue);
+ newValue = module.remove.arrayValue(removedValue, values);
+ module.remove.optionValue(removedValue);
+ }
+ else {
+ module.verbose('Removing from delimited values', removedValue);
+ newValue = module.remove.arrayValue(removedValue, values);
+ newValue = newValue.join(settings.delimiter);
+ }
+ if(settings.fireOnInit === false && module.is.initialLoad()) {
+ module.verbose('No callback on initial load', settings.onRemove);
+ }
+ else {
+ settings.onRemove.call(element, removedValue, removedText, $removedItem);
+ }
+ module.set.value(newValue, removedText, $removedItem);
+ module.check.maxSelections();
+ },
+ arrayValue: function(removedValue, values) {
+ if( !Array.isArray(values) ) {
+ values = [values];
+ }
+ values = $.grep(values, function(value){
+ return (removedValue != value);
+ });
+ module.verbose('Removed value from delimited string', removedValue, values);
+ return values;
+ },
+ label: function(value, shouldAnimate) {
+ var
+ $labels = $module.find(selector.label),
+ $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(settings.ignoreCase ? value.toLowerCase() : value) +'"]')
+ ;
+ module.verbose('Removing label', $removedLabel);
+ $removedLabel.remove();
+ },
+ activeLabels: function($activeLabels) {
+ $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active);
+ module.verbose('Removing active label selections', $activeLabels);
+ module.remove.labels($activeLabels);
+ },
+ labels: function($labels) {
+ $labels = $labels || $module.find(selector.label);
+ module.verbose('Removing labels', $labels);
+ $labels
+ .each(function(){
+ var
+ $label = $(this),
+ value = $label.data(metadata.value),
+ stringValue = (value !== undefined)
+ ? String(value)
+ : value,
+ isUserValue = module.is.userValue(stringValue)
+ ;
+ if(settings.onLabelRemove.call($label, value) === false) {
+ module.debug('Label remove callback cancelled removal');
+ return;
+ }
+ module.remove.message();
+ if(isUserValue) {
+ module.remove.value(stringValue);
+ module.remove.label(stringValue);
+ }
+ else {
+ // selected will also remove label
+ module.remove.selected(stringValue);
+ }
+ })
+ ;
+ },
+ tabbable: function() {
+ if( module.is.searchSelection() ) {
+ module.debug('Searchable dropdown initialized');
+ $search
+ .removeAttr('tabindex')
+ ;
+ $menu
+ .removeAttr('tabindex')
+ ;
+ }
+ else {
+ module.debug('Simple selection dropdown initialized');
+ $module
+ .removeAttr('tabindex')
+ ;
+ $menu
+ .removeAttr('tabindex')
+ ;
+ }
+ },
+ diacritics: function(text) {
+ return settings.ignoreDiacritics ? text.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : text;
+ }
+ },
+
+ has: {
+ menuSearch: function() {
+ return (module.has.search() && $search.closest($menu).length > 0);
+ },
+ clearItem: function() {
+ return ($clear.length > 0);
+ },
+ search: function() {
+ return ($search.length > 0);
+ },
+ sizer: function() {
+ return ($sizer.length > 0);
+ },
+ selectInput: function() {
+ return ( $input.is('select') );
+ },
+ minCharacters: function(searchTerm) {
+ if(settings.minCharacters && !iconClicked) {
+ searchTerm = (searchTerm !== undefined)
+ ? String(searchTerm)
+ : String(module.get.query())
+ ;
+ return (searchTerm.length >= settings.minCharacters);
+ }
+ iconClicked=false;
+ return true;
+ },
+ firstLetter: function($item, letter) {
+ var
+ text,
+ firstLetter
+ ;
+ if(!$item || $item.length === 0 || typeof letter !== 'string') {
+ return false;
+ }
+ text = module.get.choiceText($item, false);
+ letter = letter.toLowerCase();
+ firstLetter = String(text).charAt(0).toLowerCase();
+ return (letter == firstLetter);
+ },
+ input: function() {
+ return ($input.length > 0);
+ },
+ items: function() {
+ return ($item.length > 0);
+ },
+ menu: function() {
+ return ($menu.length > 0);
+ },
+ message: function() {
+ return ($menu.children(selector.message).length !== 0);
+ },
+ label: function(value) {
+ var
+ escapedValue = module.escape.value(value),
+ $labels = $module.find(selector.label)
+ ;
+ if(settings.ignoreCase) {
+ escapedValue = escapedValue.toLowerCase();
+ }
+ return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0);
+ },
+ maxSelections: function() {
+ return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections);
+ },
+ allResultsFiltered: function() {
+ var
+ $normalResults = $item.not(selector.addition)
+ ;
+ return ($normalResults.filter(selector.unselectable).length === $normalResults.length);
+ },
+ userSuggestion: function() {
+ return ($menu.children(selector.addition).length > 0);
+ },
+ query: function() {
+ return (module.get.query() !== '');
+ },
+ value: function(value) {
+ return (settings.ignoreCase)
+ ? module.has.valueIgnoringCase(value)
+ : module.has.valueMatchingCase(value)
+ ;
+ },
+ valueMatchingCase: function(value) {
+ var
+ values = module.get.values(),
+ hasValue = Array.isArray(values)
+ ? values && ($.inArray(value, values) !== -1)
+ : (values == value)
+ ;
+ return (hasValue)
+ ? true
+ : false
+ ;
+ },
+ valueIgnoringCase: function(value) {
+ var
+ values = module.get.values(),
+ hasValue = false
+ ;
+ if(!Array.isArray(values)) {
+ values = [values];
+ }
+ $.each(values, function(index, existingValue) {
+ if(String(value).toLowerCase() == String(existingValue).toLowerCase()) {
+ hasValue = true;
+ return false;
+ }
+ });
+ return hasValue;
+ }
+ },
+
+ is: {
+ active: function() {
+ return $module.hasClass(className.active);
+ },
+ animatingInward: function() {
+ return $menu.transition('is inward');
+ },
+ animatingOutward: function() {
+ return $menu.transition('is outward');
+ },
+ bubbledLabelClick: function(event) {
+ return $(event.target).is('select, input') && $module.closest('label').length > 0;
+ },
+ bubbledIconClick: function(event) {
+ return $(event.target).closest($icon).length > 0;
+ },
+ alreadySetup: function() {
+ return ($module.is('select') && $module.parent(selector.dropdown).data(moduleNamespace) !== undefined && $module.prev().length === 0);
+ },
+ animating: function($subMenu) {
+ return ($subMenu)
+ ? $subMenu.transition && $subMenu.transition('is animating')
+ : $menu.transition && $menu.transition('is animating')
+ ;
+ },
+ leftward: function($subMenu) {
+ var $selectedMenu = $subMenu || $menu;
+ return $selectedMenu.hasClass(className.leftward);
+ },
+ clearable: function() {
+ return ($module.hasClass(className.clearable) || settings.clearable);
+ },
+ disabled: function() {
+ return $module.hasClass(className.disabled);
+ },
+ focused: function() {
+ return (document.activeElement === $module[0]);
+ },
+ focusedOnSearch: function() {
+ return (document.activeElement === $search[0]);
+ },
+ allFiltered: function() {
+ return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() );
+ },
+ hidden: function($subMenu) {
+ return !module.is.visible($subMenu);
+ },
+ initialLoad: function() {
+ return initialLoad;
+ },
+ inObject: function(needle, object) {
+ var
+ found = false
+ ;
+ $.each(object, function(index, property) {
+ if(property == needle) {
+ found = true;
+ return true;
+ }
+ });
+ return found;
+ },
+ multiple: function() {
+ return $module.hasClass(className.multiple);
+ },
+ remote: function() {
+ return settings.apiSettings && module.can.useAPI();
+ },
+ single: function() {
+ return !module.is.multiple();
+ },
+ selectMutation: function(mutations) {
+ var
+ selectChanged = false
+ ;
+ $.each(mutations, function(index, mutation) {
+ if($(mutation.target).is('select') || $(mutation.addedNodes).is('select')) {
+ selectChanged = true;
+ return false;
+ }
+ });
+ return selectChanged;
+ },
+ search: function() {
+ return $module.hasClass(className.search);
+ },
+ searchSelection: function() {
+ return ( module.has.search() && $search.parent(selector.dropdown).length === 1 );
+ },
+ selection: function() {
+ return $module.hasClass(className.selection);
+ },
+ userValue: function(value) {
+ return ($.inArray(value, module.get.userValues()) !== -1);
+ },
+ upward: function($menu) {
+ var $element = $menu || $module;
+ return $element.hasClass(className.upward);
+ },
+ visible: function($subMenu) {
+ return ($subMenu)
+ ? $subMenu.hasClass(className.visible)
+ : $menu.hasClass(className.visible)
+ ;
+ },
+ verticallyScrollableContext: function() {
+ var
+ overflowY = ($context.get(0) !== window)
+ ? $context.css('overflow-y')
+ : false
+ ;
+ return (overflowY == 'auto' || overflowY == 'scroll');
+ },
+ horizontallyScrollableContext: function() {
+ var
+ overflowX = ($context.get(0) !== window)
+ ? $context.css('overflow-X')
+ : false
+ ;
+ return (overflowX == 'auto' || overflowX == 'scroll');
+ }
+ },
+
+ can: {
+ activate: function($item) {
+ if(settings.useLabels) {
+ return true;
+ }
+ if(!module.has.maxSelections()) {
+ return true;
+ }
+ if(module.has.maxSelections() && $item.hasClass(className.active)) {
+ return true;
+ }
+ return false;
+ },
+ openDownward: function($subMenu) {
+ var
+ $currentMenu = $subMenu || $menu,
+ canOpenDownward = true,
+ onScreen = {},
+ calculations
+ ;
+ $currentMenu
+ .addClass(className.loading)
+ ;
+ calculations = {
+ context: {
+ offset : ($context.get(0) === window)
+ ? { top: 0, left: 0}
+ : $context.offset(),
+ scrollTop : $context.scrollTop(),
+ height : $context.outerHeight()
+ },
+ menu : {
+ offset: $currentMenu.offset(),
+ height: $currentMenu.outerHeight()
+ }
+ };
+ if(module.is.verticallyScrollableContext()) {
+ calculations.menu.offset.top += calculations.context.scrollTop;
+ }
+ onScreen = {
+ above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.context.offset.top - calculations.menu.height,
+ below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top - calculations.context.offset.top + calculations.menu.height
+ };
+ if(onScreen.below) {
+ module.verbose('Dropdown can fit in context downward', onScreen);
+ canOpenDownward = true;
+ }
+ else if(!onScreen.below && !onScreen.above) {
+ module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen);
+ canOpenDownward = true;
+ }
+ else {
+ module.verbose('Dropdown cannot fit below, opening upward', onScreen);
+ canOpenDownward = false;
+ }
+ $currentMenu.removeClass(className.loading);
+ return canOpenDownward;
+ },
+ openRightward: function($subMenu) {
+ var
+ $currentMenu = $subMenu || $menu,
+ canOpenRightward = true,
+ isOffscreenRight = false,
+ calculations
+ ;
+ $currentMenu
+ .addClass(className.loading)
+ ;
+ calculations = {
+ context: {
+ offset : ($context.get(0) === window)
+ ? { top: 0, left: 0}
+ : $context.offset(),
+ scrollLeft : $context.scrollLeft(),
+ width : $context.outerWidth()
+ },
+ menu: {
+ offset : $currentMenu.offset(),
+ width : $currentMenu.outerWidth()
+ }
+ };
+ if(module.is.horizontallyScrollableContext()) {
+ calculations.menu.offset.left += calculations.context.scrollLeft;
+ }
+ isOffscreenRight = (calculations.menu.offset.left - calculations.context.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width);
+ if(isOffscreenRight) {
+ module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight);
+ canOpenRightward = false;
+ }
+ $currentMenu.removeClass(className.loading);
+ return canOpenRightward;
+ },
+ click: function() {
+ return (hasTouch || settings.on == 'click');
+ },
+ extendSelect: function() {
+ return settings.allowAdditions || settings.apiSettings;
+ },
+ show: function() {
+ return !module.is.disabled() && (module.has.items() || module.has.message());
+ },
+ useAPI: function() {
+ return $.fn.api !== undefined;
+ }
+ },
+
+ animate: {
+ show: function(callback, $subMenu) {
+ var
+ $currentMenu = $subMenu || $menu,
+ start = ($subMenu)
+ ? function() {}
+ : function() {
+ module.hideSubMenus();
+ module.hideOthers();
+ module.set.active();
+ },
+ transition
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ module.verbose('Doing menu show animation', $currentMenu);
+ module.set.direction($subMenu);
+ transition = module.get.transition($subMenu);
+ if( module.is.selection() ) {
+ module.set.scrollPosition(module.get.selectedItem(), true);
+ }
+ if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) {
+ var displayType = $module.hasClass('column') ? 'flex' : false;
+ if(transition == 'none') {
+ start();
+ $currentMenu.transition({
+ displayType: displayType
+ }).transition('show');
+ callback.call(element);
+ }
+ else if($.fn.transition !== undefined && $module.transition('is supported')) {
+ $currentMenu
+ .transition({
+ animation : transition + ' in',
+ debug : settings.debug,
+ verbose : settings.verbose,
+ duration : settings.duration,
+ queue : true,
+ onStart : start,
+ displayType: displayType,
+ onComplete : function() {
+ callback.call(element);
+ }
+ })
+ ;
+ }
+ else {
+ module.error(error.noTransition, transition);
+ }
+ }
+ },
+ hide: function(callback, $subMenu) {
+ var
+ $currentMenu = $subMenu || $menu,
+ start = ($subMenu)
+ ? function() {}
+ : function() {
+ if( module.can.click() ) {
+ module.unbind.intent();
+ }
+ module.remove.active();
+ },
+ transition = module.get.transition($subMenu)
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) {
+ module.verbose('Doing menu hide animation', $currentMenu);
+
+ if(transition == 'none') {
+ start();
+ $currentMenu.transition('hide');
+ callback.call(element);
+ }
+ else if($.fn.transition !== undefined && $module.transition('is supported')) {
+ $currentMenu
+ .transition({
+ animation : transition + ' out',
+ duration : settings.duration,
+ debug : settings.debug,
+ verbose : settings.verbose,
+ queue : false,
+ onStart : start,
+ onComplete : function() {
+ callback.call(element);
+ }
+ })
+ ;
+ }
+ else {
+ module.error(error.transition);
+ }
+ }
+ }
+ },
+
+ hideAndClear: function() {
+ module.remove.searchTerm();
+ if( module.has.maxSelections() ) {
+ return;
+ }
+ if(module.has.search()) {
+ module.hide(function() {
+ module.remove.filteredItem();
+ });
+ }
+ else {
+ module.hide();
+ }
+ },
+
+ delay: {
+ show: function() {
+ module.verbose('Delaying show event to ensure user intent');
+ clearTimeout(module.timer);
+ module.timer = setTimeout(module.show, settings.delay.show);
+ },
+ hide: function() {
+ module.verbose('Delaying hide event to ensure user intent');
+ clearTimeout(module.timer);
+ module.timer = setTimeout(module.hide, settings.delay.hide);
+ }
+ },
+
+ escape: {
+ value: function(value) {
+ var
+ multipleValues = Array.isArray(value),
+ stringValue = (typeof value === 'string'),
+ isUnparsable = (!stringValue && !multipleValues),
+ hasQuotes = (stringValue && value.search(regExp.quote) !== -1),
+ values = []
+ ;
+ if(isUnparsable || !hasQuotes) {
+ return value;
+ }
+ module.debug('Encoding quote values for use in select', value);
+ if(multipleValues) {
+ $.each(value, function(index, value){
+ values.push(value.replace(regExp.quote, '&quot;'));
+ });
+ return values;
+ }
+ return value.replace(regExp.quote, '&quot;');
+ },
+ string: function(text) {
+ text = String(text);
+ return text.replace(regExp.escape, '\\$&');
+ },
+ htmlEntities: function(string) {
+ var
+ badChars = /[<>"'`]/g,
+ shouldEscape = /[&<>"'`]/,
+ escape = {
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
+ },
+ escapedChar = function(chr) {
+ return escape[chr];
+ }
+ ;
+ if(shouldEscape.test(string)) {
+ string = string.replace(/&(?![a-z0-9#]{1,6};)/, "&amp;");
+ return string.replace(badChars, escapedChar);
+ }
+ return string;
+ }
+ },
+
+ setting: function(name, value) {
+ module.debug('Changing setting', name, value);
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ if($.isPlainObject(settings[name])) {
+ $.extend(true, settings[name], value);
+ }
+ else {
+ settings[name] = value;
+ }
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ 'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ module.error(error.method, query);
+ return false;
+ }
+ });
+ }
+ if ( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.initialize();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.initialize();
+ }
+ })
+ ;
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : $allModules
+ ;
+};
+
+$.fn.dropdown.settings = {
+
+ silent : false,
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ on : 'click', // what event should show menu action on item selection
+ action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){})
+
+ values : false, // specify values to use for dropdown
+
+ clearable : false, // whether the value of the dropdown can be cleared
+
+ apiSettings : false,
+ selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used
+ minCharacters : 0, // Minimum characters required to trigger API call
+
+ filterRemoteData : false, // Whether API results should be filtered after being returned for query term
+ saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh
+
+ throttle : 200, // How long to wait after last user input to search remotely
+
+ context : window, // Context to use when determining if on screen
+ direction : 'auto', // Whether dropdown should always open in one direction
+ keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing
+
+ match : 'both', // what to match against with search selection (both, text, or label)
+ fullTextSearch : false, // search anywhere in value (set to 'exact' to require exact matches)
+ ignoreDiacritics : false, // match results also if they contain diacritics of the same base character (for example searching for "a" will also match "á" or "â" or "à", etc...)
+ hideDividers : false, // Whether to hide any divider elements (specified in selector.divider) that are sibling to any items when searched (set to true will hide all dividers, set to 'empty' will hide them when they are not followed by a visible item)
+
+ placeholder : 'auto', // whether to convert blank <select> values to placeholder text
+ preserveHTML : true, // preserve html when selecting value
+ sortSelect : false, // sort selection on init
+
+ forceSelection : true, // force a choice on blur with search selection
+
+ allowAdditions : false, // whether multiple select should allow user added values
+ ignoreCase : false, // whether to consider case sensitivity when creating labels
+ ignoreSearchCase : true, // whether to consider case sensitivity when filtering items
+ hideAdditions : true, // whether or not to hide special message prompting a user they can enter a value
+
+ maxSelections : false, // When set to a number limits the number of selections to this count
+ useLabels : true, // whether multiple select should filter currently active selections from choices
+ delimiter : ',', // when multiselect uses normal <input> the values will be delimited with this character
+
+ showOnFocus : true, // show menu on focus
+ allowReselection : false, // whether current value should trigger callbacks when reselected
+ allowTab : true, // add tabindex to element
+ allowCategorySelection : false, // allow elements with sub-menus to be selected
+
+ fireOnInit : false, // Whether callbacks should fire when initializing dropdown values
+
+ transition : 'auto', // auto transition will slide down or up based on direction
+ duration : 200, // duration of transition
+
+ glyphWidth : 1.037, // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width
+
+ headerDivider : true, // whether option headers should have an additional divider line underneath when converted from <select> <optgroup>
+
+ // label settings on multi-select
+ label: {
+ transition : 'scale',
+ duration : 200,
+ variation : false
+ },
+
+ // delay before event
+ delay : {
+ hide : 300,
+ show : 200,
+ search : 20,
+ touch : 50
+ },
+
+ /* Callbacks */
+ onChange : function(value, text, $selected){},
+ onAdd : function(value, text, $selected){},
+ onRemove : function(value, text, $selected){},
+
+ onLabelSelect : function($selectedLabels){},
+ onLabelCreate : function(value, text) { return $(this); },
+ onLabelRemove : function(value) { return true; },
+ onNoResults : function(searchTerm) { return true; },
+ onShow : function(){},
+ onHide : function(){},
+
+ /* Component */
+ name : 'Dropdown',
+ namespace : 'dropdown',
+
+ message: {
+ addResult : 'Add <b>{term}</b>',
+ count : '{count} selected',
+ maxSelections : 'Max {maxCount} selections',
+ noResults : 'No results found.',
+ serverError : 'There was an error contacting the server'
+ },
+
+ error : {
+ action : 'You called a dropdown action that was not defined',
+ alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown',
+ labels : 'Allowing user additions currently requires the use of labels.',
+ missingMultiple : '<select> requires multiple property to be set to correctly preserve multiple values',
+ method : 'The method you called is not defined.',
+ noAPI : 'The API module is required to load resources remotely',
+ noStorage : 'Saving remote data requires session storage',
+ noTransition : 'This module requires ui transitions <https://github.com/Semantic-Org/UI-Transition>',
+ noNormalize : '"ignoreDiacritics" setting will be ignored. Browser does not support String().normalize(). You may consider including <https://cdn.jsdelivr.net/npm/unorm@1.4.1/lib/unorm.min.js> as a polyfill.'
+ },
+
+ regExp : {
+ escape : /[-[\]{}()*+?.,\\^$|#\s:=@]/g,
+ quote : /"/g
+ },
+
+ metadata : {
+ defaultText : 'defaultText',
+ defaultValue : 'defaultValue',
+ placeholderText : 'placeholder',
+ text : 'text',
+ value : 'value'
+ },
+
+ // property names for remote query
+ fields: {
+ remoteValues : 'results', // grouping for api results
+ values : 'values', // grouping for all dropdown values
+ disabled : 'disabled', // whether value should be disabled
+ name : 'name', // displayed dropdown text
+ value : 'value', // actual dropdown value
+ text : 'text', // displayed text when selected
+ type : 'type', // type of dropdown element
+ image : 'image', // optional image path
+ imageClass : 'imageClass', // optional individual class for image
+ icon : 'icon', // optional icon name
+ iconClass : 'iconClass', // optional individual class for icon (for example to use flag instead)
+ class : 'class', // optional individual class for item/header
+ divider : 'divider' // optional divider append for group headers
+ },
+
+ keys : {
+ backspace : 8,
+ delimiter : 188, // comma
+ deleteKey : 46,
+ enter : 13,
+ escape : 27,
+ pageUp : 33,
+ pageDown : 34,
+ leftArrow : 37,
+ upArrow : 38,
+ rightArrow : 39,
+ downArrow : 40
+ },
+
+ selector : {
+ addition : '.addition',
+ divider : '.divider, .header',
+ dropdown : '.ui.dropdown',
+ hidden : '.hidden',
+ icon : '> .dropdown.icon',
+ input : '> input[type="hidden"], > select',
+ item : '.item',
+ label : '> .label',
+ remove : '> .label > .delete.icon',
+ siblingLabel : '.label',
+ menu : '.menu',
+ message : '.message',
+ menuIcon : '.dropdown.icon',
+ search : 'input.search, .menu > .search > input, .menu input.search',
+ sizer : '> span.sizer',
+ text : '> .text:not(.icon)',
+ unselectable : '.disabled, .filtered',
+ clearIcon : '> .remove.icon'
+ },
+
+ className : {
+ active : 'active',
+ addition : 'addition',
+ animating : 'animating',
+ disabled : 'disabled',
+ empty : 'empty',
+ dropdown : 'ui dropdown',
+ filtered : 'filtered',
+ hidden : 'hidden transition',
+ icon : 'icon',
+ image : 'image',
+ item : 'item',
+ label : 'ui label',
+ loading : 'loading',
+ menu : 'menu',
+ message : 'message',
+ multiple : 'multiple',
+ placeholder : 'default',
+ sizer : 'sizer',
+ search : 'search',
+ selected : 'selected',
+ selection : 'selection',
+ upward : 'upward',
+ leftward : 'left',
+ visible : 'visible',
+ clearable : 'clearable',
+ noselection : 'noselection',
+ delete : 'delete',
+ header : 'header',
+ divider : 'divider',
+ groupIcon : '',
+ unfilterable : 'unfilterable'
+ }
+
+};
+
+/* Templates */
+$.fn.dropdown.settings.templates = {
+ deQuote: function(string) {
+ return String(string).replace(/"/g,"");
+ },
+ escape: function(string, preserveHTML) {
+ if (preserveHTML){
+ return string;
+ }
+ var
+ badChars = /[<>"'`]/g,
+ shouldEscape = /[&<>"'`]/,
+ escape = {
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
+ },
+ escapedChar = function(chr) {
+ return escape[chr];
+ }
+ ;
+ if(shouldEscape.test(string)) {
+ string = string.replace(/&(?![a-z0-9#]{1,6};)/, "&amp;");
+ return string.replace(badChars, escapedChar);
+ }
+ return string;
+ },
+ // generates dropdown from select values
+ dropdown: function(select, fields, preserveHTML, className) {
+ var
+ placeholder = select.placeholder || false,
+ html = '',
+ escape = $.fn.dropdown.settings.templates.escape
+ ;
+ html += '<i class="dropdown icon"></i>';
+ if(placeholder) {
+ html += '<div class="default text">' + escape(placeholder,preserveHTML) + '</div>';
+ }
+ else {
+ html += '<div class="text"></div>';
+ }
+ html += '<div class="'+className.menu+'">';
+ html += $.fn.dropdown.settings.templates.menu(select, fields, preserveHTML,className);
+ html += '</div>';
+ return html;
+ },
+
+ // generates just menu from select
+ menu: function(response, fields, preserveHTML, className) {
+ var
+ values = response[fields.values] || [],
+ html = '',
+ escape = $.fn.dropdown.settings.templates.escape,
+ deQuote = $.fn.dropdown.settings.templates.deQuote
+ ;
+ $.each(values, function(index, option) {
+ var
+ itemType = (option[fields.type])
+ ? option[fields.type]
+ : 'item'
+ ;
+
+ if( itemType === 'item' ) {
+ var
+ maybeText = (option[fields.text])
+ ? ' data-text="' + deQuote(option[fields.text]) + '"'
+ : '',
+ maybeDisabled = (option[fields.disabled])
+ ? className.disabled+' '
+ : ''
+ ;
+ html += '<div class="'+ maybeDisabled + (option[fields.class] ? deQuote(option[fields.class]) : className.item)+'" data-value="' + deQuote(option[fields.value]) + '"' + maybeText + '>';
+ if(option[fields.image]) {
+ html += '<img class="'+(option[fields.imageClass] ? deQuote(option[fields.imageClass]) : className.image)+'" src="' + deQuote(option[fields.image]) + '">';
+ }
+ if(option[fields.icon]) {
+ html += '<i class="'+deQuote(option[fields.icon])+' '+(option[fields.iconClass] ? deQuote(option[fields.iconClass]) : className.icon)+'"></i>';
+ }
+ html += escape(option[fields.name] || '', preserveHTML);
+ html += '</div>';
+ } else if (itemType === 'header') {
+ var groupName = escape(option[fields.name] || '', preserveHTML),
+ groupIcon = option[fields.icon] ? deQuote(option[fields.icon]) : className.groupIcon
+ ;
+ if(groupName !== '' || groupIcon !== '') {
+ html += '<div class="' + (option[fields.class] ? deQuote(option[fields.class]) : className.header) + '">';
+ if (groupIcon !== '') {
+ html += '<i class="' + groupIcon + ' ' + (option[fields.iconClass] ? deQuote(option[fields.iconClass]) : className.icon) + '"></i>';
+ }
+ html += groupName;
+ html += '</div>';
+ }
+ if(option[fields.divider]){
+ html += '<div class="'+className.divider+'"></div>';
+ }
+ }
+ });
+ return html;
+ },
+
+ // generates label for multiselect
+ label: function(value, text, preserveHTML, className) {
+ var
+ escape = $.fn.dropdown.settings.templates.escape;
+ return escape(text,preserveHTML) + '<i class="'+className.delete+' icon"></i>';
+ },
+
+
+ // generates messages like "No results"
+ message: function(message) {
+ return message;
+ },
+
+ // generates user addition to selection menu
+ addition: function(choice) {
+ return choice;
+ }
+
+};
+
+})( jQuery, window, document );
+
+/*!
+ * # Fomantic-UI - Form Validation
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.form = function(parameters) {
+ var
+ $allModules = $(this),
+ moduleSelector = $allModules.selector || '',
+
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ legacyParameters = arguments[1],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+ returnedValue
+ ;
+ $allModules
+ .each(function() {
+ var
+ $module = $(this),
+ element = this,
+
+ formErrors = [],
+ keyHeldDown = false,
+
+ // set at run-time
+ $field,
+ $group,
+ $message,
+ $prompt,
+ $submit,
+ $clear,
+ $reset,
+
+ settings,
+ validation,
+
+ metadata,
+ selector,
+ className,
+ regExp,
+ error,
+
+ namespace,
+ moduleNamespace,
+ eventNamespace,
+
+ submitting = false,
+ dirty = false,
+ history = ['clean', 'clean'],
+
+ instance,
+ module
+ ;
+
+ module = {
+
+ initialize: function() {
+
+ // settings grabbed at run time
+ module.get.settings();
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.instantiate();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.verbose('Initializing form validation', $module, settings);
+ module.bindEvents();
+ module.set.defaults();
+ if (settings.autoCheckRequired) {
+ module.set.autoCheck();
+ }
+ module.instantiate();
+ }
+ },
+
+ instantiate: function() {
+ module.verbose('Storing instance of module', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, module)
+ ;
+ },
+
+ destroy: function() {
+ module.verbose('Destroying previous module', instance);
+ module.removeEvents();
+ $module
+ .removeData(moduleNamespace)
+ ;
+ },
+
+ refresh: function() {
+ module.verbose('Refreshing selector cache');
+ $field = $module.find(selector.field);
+ $group = $module.find(selector.group);
+ $message = $module.find(selector.message);
+ $prompt = $module.find(selector.prompt);
+
+ $submit = $module.find(selector.submit);
+ $clear = $module.find(selector.clear);
+ $reset = $module.find(selector.reset);
+ },
+
+ submit: function() {
+ module.verbose('Submitting form', $module);
+ submitting = true;
+ $module.submit();
+ },
+
+ attachEvents: function(selector, action) {
+ action = action || 'submit';
+ $(selector).on('click' + eventNamespace, function(event) {
+ module[action]();
+ event.preventDefault();
+ });
+ },
+
+ bindEvents: function() {
+ module.verbose('Attaching form events');
+ $module
+ .on('submit' + eventNamespace, module.validate.form)
+ .on('blur' + eventNamespace, selector.field, module.event.field.blur)
+ .on('click' + eventNamespace, selector.submit, module.submit)
+ .on('click' + eventNamespace, selector.reset, module.reset)
+ .on('click' + eventNamespace, selector.clear, module.clear)
+ ;
+ if(settings.keyboardShortcuts) {
+ $module.on('keydown' + eventNamespace, selector.field, module.event.field.keydown);
+ }
+ $field.each(function(index, el) {
+ var
+ $input = $(el),
+ type = $input.prop('type'),
+ inputEvent = module.get.changeEvent(type, $input)
+ ;
+ $input.on(inputEvent + eventNamespace, module.event.field.change);
+ });
+
+ // Dirty events
+ if (settings.preventLeaving) {
+ $(window).on('beforeunload' + eventNamespace, module.event.beforeUnload);
+ }
+
+ $field.on('change click keyup keydown blur', function(e) {
+ $(this).triggerHandler(e.type + ".dirty");
+ });
+
+ $field.on('change.dirty click.dirty keyup.dirty keydown.dirty blur.dirty', module.determine.isDirty);
+
+ $module.on('dirty' + eventNamespace, function(e) {
+ settings.onDirty.call();
+ });
+
+ $module.on('clean' + eventNamespace, function(e) {
+ settings.onClean.call();
+ })
+ },
+
+ clear: function() {
+ $field.each(function (index, el) {
+ var
+ $field = $(el),
+ $element = $field.parent(),
+ $fieldGroup = $field.closest($group),
+ $prompt = $fieldGroup.find(selector.prompt),
+ $calendar = $field.closest(selector.uiCalendar),
+ defaultValue = $field.data(metadata.defaultValue) || '',
+ isCheckbox = $element.is(selector.uiCheckbox),
+ isDropdown = $element.is(selector.uiDropdown) && module.can.useElement('dropdown'),
+ isCalendar = ($calendar.length > 0 && module.can.useElement('calendar')),
+ isErrored = $fieldGroup.hasClass(className.error)
+ ;
+ if(isErrored) {
+ module.verbose('Resetting error on field', $fieldGroup);
+ $fieldGroup.removeClass(className.error);
+ $prompt.remove();
+ }
+ if(isDropdown) {
+ module.verbose('Resetting dropdown value', $element, defaultValue);
+ $element.dropdown('clear', true);
+ }
+ else if(isCheckbox) {
+ $field.prop('checked', false);
+ }
+ else if (isCalendar) {
+ $calendar.calendar('clear');
+ }
+ else {
+ module.verbose('Resetting field value', $field, defaultValue);
+ $field.val('');
+ }
+ });
+ module.remove.states();
+ },
+
+ reset: function() {
+ $field.each(function (index, el) {
+ var
+ $field = $(el),
+ $element = $field.parent(),
+ $fieldGroup = $field.closest($group),
+ $calendar = $field.closest(selector.uiCalendar),
+ $prompt = $fieldGroup.find(selector.prompt),
+ defaultValue = $field.data(metadata.defaultValue),
+ isCheckbox = $element.is(selector.uiCheckbox),
+ isDropdown = $element.is(selector.uiDropdown) && module.can.useElement('dropdown'),
+ isCalendar = ($calendar.length > 0 && module.can.useElement('calendar')),
+ isErrored = $fieldGroup.hasClass(className.error)
+ ;
+ if(defaultValue === undefined) {
+ return;
+ }
+ if(isErrored) {
+ module.verbose('Resetting error on field', $fieldGroup);
+ $fieldGroup.removeClass(className.error);
+ $prompt.remove();
+ }
+ if(isDropdown) {
+ module.verbose('Resetting dropdown value', $element, defaultValue);
+ $element.dropdown('restore defaults', true);
+ }
+ else if(isCheckbox) {
+ module.verbose('Resetting checkbox value', $element, defaultValue);
+ $field.prop('checked', defaultValue);
+ }
+ else if (isCalendar) {
+ $calendar.calendar('set date', defaultValue);
+ }
+ else {
+ module.verbose('Resetting field value', $field, defaultValue);
+ $field.val(defaultValue);
+ }
+ });
+ module.remove.states();
+ },
+
+ determine: {
+ isValid: function() {
+ var
+ allValid = true
+ ;
+ $.each(validation, function(fieldName, field) {
+ if( !( module.validate.field(field, fieldName, true) ) ) {
+ allValid = false;
+ }
+ });
+ return allValid;
+ },
+ isDirty: function(e) {
+ var formIsDirty = false;
+
+ $field.each(function(index, el) {
+ var
+ $el = $(el),
+ isCheckbox = ($el.filter(selector.checkbox).length > 0),
+ isDirty
+ ;
+
+ if (isCheckbox) {
+ isDirty = module.is.checkboxDirty($el);
+ } else {
+ isDirty = module.is.fieldDirty($el);
+ }
+
+ $el.data(settings.metadata.isDirty, isDirty);
+
+ formIsDirty |= isDirty;
+ });
+
+ if (formIsDirty) {
+ module.set.dirty();
+ } else {
+ module.set.clean();
+ }
+
+ if (e && e.namespace === 'dirty') {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ }
+ },
+
+ is: {
+ bracketedRule: function(rule) {
+ return (rule.type && rule.type.match(settings.regExp.bracket));
+ },
+ shorthandFields: function(fields) {
+ var
+ fieldKeys = Object.keys(fields),
+ firstRule = fields[fieldKeys[0]]
+ ;
+ return module.is.shorthandRules(firstRule);
+ },
+ // duck type rule test
+ shorthandRules: function(rules) {
+ return (typeof rules == 'string' || Array.isArray(rules));
+ },
+ empty: function($field) {
+ if(!$field || $field.length === 0) {
+ return true;
+ }
+ else if($field.is(selector.checkbox)) {
+ return !$field.is(':checked');
+ }
+ else {
+ return module.is.blank($field);
+ }
+ },
+ blank: function($field) {
+ return String($field.val()).trim() === '';
+ },
+ valid: function(field, showErrors) {
+ var
+ allValid = true
+ ;
+ if(field) {
+ module.verbose('Checking if field is valid', field);
+ return module.validate.field(validation[field], field, !!showErrors);
+ }
+ else {
+ module.verbose('Checking if form is valid');
+ $.each(validation, function(fieldName, field) {
+ if( !module.is.valid(fieldName, showErrors) ) {
+ allValid = false;
+ }
+ });
+ return allValid;
+ }
+ },
+ dirty: function() {
+ return dirty;
+ },
+ clean: function() {
+ return !dirty;
+ },
+ fieldDirty: function($el) {
+ var initialValue = $el.data(metadata.defaultValue);
+ // Explicitly check for null/undefined here as value may be `false`, so ($el.data(dataInitialValue) || '') would not work
+ if (initialValue == null) { initialValue = ''; }
+ else if(Array.isArray(initialValue)) {
+ initialValue = initialValue.toString();
+ }
+ var currentValue = $el.val();
+ if (currentValue == null) { currentValue = ''; }
+ // multiple select values are returned as arrays which are never equal, so do string conversion first
+ else if(Array.isArray(currentValue)) {
+ currentValue = currentValue.toString();
+ }
+ // Boolean values can be encoded as "true/false" or "True/False" depending on underlying frameworks so we need a case insensitive comparison
+ var boolRegex = /^(true|false)$/i;
+ var isBoolValue = boolRegex.test(initialValue) && boolRegex.test(currentValue);
+ if (isBoolValue) {
+ var regex = new RegExp("^" + initialValue + "$", "i");
+ return !regex.test(currentValue);
+ }
+
+ return currentValue !== initialValue;
+ },
+ checkboxDirty: function($el) {
+ var initialValue = $el.data(metadata.defaultValue);
+ var currentValue = $el.is(":checked");
+
+ return initialValue !== currentValue;
+ },
+ justDirty: function() {
+ return (history[0] === 'dirty');
+ },
+ justClean: function() {
+ return (history[0] === 'clean');
+ }
+ },
+
+ removeEvents: function() {
+ $module.off(eventNamespace);
+ $field.off(eventNamespace);
+ $submit.off(eventNamespace);
+ $field.off(eventNamespace);
+ },
+
+ event: {
+ field: {
+ keydown: function(event) {
+ var
+ $field = $(this),
+ key = event.which,
+ isInput = $field.is(selector.input),
+ isCheckbox = $field.is(selector.checkbox),
+ isInDropdown = ($field.closest(selector.uiDropdown).length > 0),
+ keyCode = {
+ enter : 13,
+ escape : 27
+ }
+ ;
+ if( key == keyCode.escape) {
+ module.verbose('Escape key pressed blurring field');
+ $field
+ .blur()
+ ;
+ }
+ if(!event.ctrlKey && key == keyCode.enter && isInput && !isInDropdown && !isCheckbox) {
+ if(!keyHeldDown) {
+ $field.one('keyup' + eventNamespace, module.event.field.keyup);
+ module.submit();
+ module.debug('Enter pressed on input submitting form');
+ }
+ keyHeldDown = true;
+ }
+ },
+ keyup: function() {
+ keyHeldDown = false;
+ },
+ blur: function(event) {
+ var
+ $field = $(this),
+ $fieldGroup = $field.closest($group),
+ validationRules = module.get.validation($field)
+ ;
+ if( $fieldGroup.hasClass(className.error) ) {
+ module.debug('Revalidating field', $field, validationRules);
+ if(validationRules) {
+ module.validate.field( validationRules );
+ }
+ }
+ else if(settings.on == 'blur') {
+ if(validationRules) {
+ module.validate.field( validationRules );
+ }
+ }
+ },
+ change: function(event) {
+ var
+ $field = $(this),
+ $fieldGroup = $field.closest($group),
+ validationRules = module.get.validation($field)
+ ;
+ if(validationRules && (settings.on == 'change' || ( $fieldGroup.hasClass(className.error) && settings.revalidate) )) {
+ clearTimeout(module.timer);
+ module.timer = setTimeout(function() {
+ module.debug('Revalidating field', $field, module.get.validation($field));
+ module.validate.field( validationRules );
+ if(!settings.inline) {
+ module.validate.form(false,true);
+ }
+ }, settings.delay);
+ }
+ }
+ },
+ beforeUnload: function(event) {
+ if (module.is.dirty() && !submitting) {
+ var event = event || window.event;
+
+ // For modern browsers
+ if (event) {
+ event.returnValue = settings.text.leavingMessage;
+ }
+
+ // For olders...
+ return settings.text.leavingMessage;
+ }
+ }
+
+ },
+
+ get: {
+ ancillaryValue: function(rule) {
+ if(!rule.type || (!rule.value && !module.is.bracketedRule(rule))) {
+ return false;
+ }
+ return (rule.value !== undefined)
+ ? rule.value
+ : rule.type.match(settings.regExp.bracket)[1] + ''
+ ;
+ },
+ ruleName: function(rule) {
+ if( module.is.bracketedRule(rule) ) {
+ return rule.type.replace(rule.type.match(settings.regExp.bracket)[0], '');
+ }
+ return rule.type;
+ },
+ changeEvent: function(type, $input) {
+ if(type == 'checkbox' || type == 'radio' || type == 'hidden' || $input.is('select')) {
+ return 'change';
+ }
+ else {
+ return module.get.inputEvent();
+ }
+ },
+ inputEvent: function() {
+ return (document.createElement('input').oninput !== undefined)
+ ? 'input'
+ : (document.createElement('input').onpropertychange !== undefined)
+ ? 'propertychange'
+ : 'keyup'
+ ;
+ },
+ fieldsFromShorthand: function(fields) {
+ var
+ fullFields = {}
+ ;
+ $.each(fields, function(name, rules) {
+ if(typeof rules == 'string') {
+ rules = [rules];
+ }
+ fullFields[name] = {
+ rules: []
+ };
+ $.each(rules, function(index, rule) {
+ fullFields[name].rules.push({ type: rule });
+ });
+ });
+ return fullFields;
+ },
+ prompt: function(rule, field) {
+ var
+ ruleName = module.get.ruleName(rule),
+ ancillary = module.get.ancillaryValue(rule),
+ $field = module.get.field(field.identifier),
+ value = $field.val(),
+ prompt = $.isFunction(rule.prompt)
+ ? rule.prompt(value)
+ : rule.prompt || settings.prompt[ruleName] || settings.text.unspecifiedRule,
+ requiresValue = (prompt.search('{value}') !== -1),
+ requiresName = (prompt.search('{name}') !== -1),
+ $label,
+ name
+ ;
+ if(requiresValue) {
+ prompt = prompt.replace(/\{value\}/g, $field.val());
+ }
+ if(requiresName) {
+ $label = $field.closest(selector.group).find('label').eq(0);
+ name = ($label.length == 1)
+ ? $label.text()
+ : $field.prop('placeholder') || settings.text.unspecifiedField
+ ;
+ prompt = prompt.replace(/\{name\}/g, name);
+ }
+ prompt = prompt.replace(/\{identifier\}/g, field.identifier);
+ prompt = prompt.replace(/\{ruleValue\}/g, ancillary);
+ if(!rule.prompt) {
+ module.verbose('Using default validation prompt for type', prompt, ruleName);
+ }
+ return prompt;
+ },
+ settings: function() {
+ if($.isPlainObject(parameters)) {
+ var
+ keys = Object.keys(parameters),
+ isLegacySettings = (keys.length > 0)
+ ? (parameters[keys[0]].identifier !== undefined && parameters[keys[0]].rules !== undefined)
+ : false
+ ;
+ if(isLegacySettings) {
+ // 1.x (ducktyped)
+ settings = $.extend(true, {}, $.fn.form.settings, legacyParameters);
+ validation = $.extend({}, $.fn.form.settings.defaults, parameters);
+ module.error(settings.error.oldSyntax, element);
+ module.verbose('Extending settings from legacy parameters', validation, settings);
+ }
+ else {
+ // 2.x
+ if(parameters.fields && module.is.shorthandFields(parameters.fields)) {
+ parameters.fields = module.get.fieldsFromShorthand(parameters.fields);
+ }
+ settings = $.extend(true, {}, $.fn.form.settings, parameters);
+ validation = $.extend({}, $.fn.form.settings.defaults, settings.fields);
+ module.verbose('Extending settings', validation, settings);
+ }
+ }
+ else {
+ settings = $.fn.form.settings;
+ validation = $.fn.form.settings.defaults;
+ module.verbose('Using default form validation', validation, settings);
+ }
+
+ // shorthand
+ namespace = settings.namespace;
+ metadata = settings.metadata;
+ selector = settings.selector;
+ className = settings.className;
+ regExp = settings.regExp;
+ error = settings.error;
+ moduleNamespace = 'module-' + namespace;
+ eventNamespace = '.' + namespace;
+
+ // grab instance
+ instance = $module.data(moduleNamespace);
+
+ // refresh selector cache
+ module.refresh();
+ },
+ field: function(identifier) {
+ module.verbose('Finding field with identifier', identifier);
+ identifier = module.escape.string(identifier);
+ var t;
+ if((t=$field.filter('#' + identifier)).length > 0 ) {
+ return t;
+ }
+ if((t=$field.filter('[name="' + identifier +'"]')).length > 0 ) {
+ return t;
+ }
+ if((t=$field.filter('[name="' + identifier +'[]"]')).length > 0 ) {
+ return t;
+ }
+ if((t=$field.filter('[data-' + metadata.validate + '="'+ identifier +'"]')).length > 0 ) {
+ return t;
+ }
+ return $('<input/>');
+ },
+ fields: function(fields) {
+ var
+ $fields = $()
+ ;
+ $.each(fields, function(index, name) {
+ $fields = $fields.add( module.get.field(name) );
+ });
+ return $fields;
+ },
+ validation: function($field) {
+ var
+ fieldValidation,
+ identifier
+ ;
+ if(!validation) {
+ return false;
+ }
+ $.each(validation, function(fieldName, field) {
+ identifier = field.identifier || fieldName;
+ $.each(module.get.field(identifier), function(index, groupField) {
+ if(groupField == $field[0]) {
+ field.identifier = identifier;
+ fieldValidation = field;
+ return false;
+ }
+ });
+ });
+ return fieldValidation || false;
+ },
+ value: function (field) {
+ var
+ fields = [],
+ results
+ ;
+ fields.push(field);
+ results = module.get.values.call(element, fields);
+ return results[field];
+ },
+ values: function (fields) {
+ var
+ $fields = Array.isArray(fields)
+ ? module.get.fields(fields)
+ : $field,
+ values = {}
+ ;
+ $fields.each(function(index, field) {
+ var
+ $field = $(field),
+ $calendar = $field.closest(selector.uiCalendar),
+ name = $field.prop('name'),
+ value = $field.val(),
+ isCheckbox = $field.is(selector.checkbox),
+ isRadio = $field.is(selector.radio),
+ isMultiple = (name.indexOf('[]') !== -1),
+ isCalendar = ($calendar.length > 0 && module.can.useElement('calendar')),
+ isChecked = (isCheckbox)
+ ? $field.is(':checked')
+ : false
+ ;
+ if(name) {
+ if(isMultiple) {
+ name = name.replace('[]', '');
+ if(!values[name]) {
+ values[name] = [];
+ }
+ if(isCheckbox) {
+ if(isChecked) {
+ values[name].push(value || true);
+ }
+ else {
+ values[name].push(false);
+ }
+ }
+ else {
+ values[name].push(value);
+ }
+ }
+ else {
+ if(isRadio) {
+ if(values[name] === undefined || values[name] === false) {
+ values[name] = (isChecked)
+ ? value || true
+ : false
+ ;
+ }
+ }
+ else if(isCheckbox) {
+ if(isChecked) {
+ values[name] = value || true;
+ }
+ else {
+ values[name] = false;
+ }
+ }
+ else if(isCalendar) {
+ var date = $calendar.calendar('get date');
+
+ if (date !== null) {
+ if (settings.dateHandling == 'date') {
+ values[name] = date;
+ } else if(settings.dateHandling == 'input') {
+ values[name] = $calendar.calendar('get input date')
+ } else if (settings.dateHandling == 'formatter') {
+ var type = $calendar.calendar('setting', 'type');
+
+ switch(type) {
+ case 'date':
+ values[name] = settings.formatter.date(date);
+ break;
+
+ case 'datetime':
+ values[name] = settings.formatter.datetime(date);
+ break;
+
+ case 'time':
+ values[name] = settings.formatter.time(date);
+ break;
+
+ case 'month':
+ values[name] = settings.formatter.month(date);
+ break;
+
+ case 'year':
+ values[name] = settings.formatter.year(date);
+ break;
+
+ default:
+ module.debug('Wrong calendar mode', $calendar, type);
+ values[name] = '';
+ }
+ }
+ } else {
+ values[name] = '';
+ }
+ } else {
+ values[name] = value;
+ }
+ }
+ }
+ });
+ return values;
+ },
+ dirtyFields: function() {
+ return $field.filter(function(index, e) {
+ return $(e).data(metadata.isDirty);
+ });
+ }
+ },
+
+ has: {
+
+ field: function(identifier) {
+ module.verbose('Checking for existence of a field with identifier', identifier);
+ identifier = module.escape.string(identifier);
+ if(typeof identifier !== 'string') {
+ module.error(error.identifier, identifier);
+ }
+ if($field.filter('#' + identifier).length > 0 ) {
+ return true;
+ }
+ else if( $field.filter('[name="' + identifier +'"]').length > 0 ) {
+ return true;
+ }
+ else if( $field.filter('[data-' + metadata.validate + '="'+ identifier +'"]').length > 0 ) {
+ return true;
+ }
+ return false;
+ }
+
+ },
+
+ can: {
+ useElement: function(element){
+ if ($.fn[element] !== undefined) {
+ return true;
+ }
+ module.error(error.noElement.replace('{element}',element));
+ return false;
+ }
+ },
+
+ escape: {
+ string: function(text) {
+ text = String(text);
+ return text.replace(regExp.escape, '\\$&');
+ }
+ },
+
+ add: {
+ // alias
+ rule: function(name, rules) {
+ module.add.field(name, rules);
+ },
+ field: function(name, rules) {
+ // Validation should have at least a standard format
+ if(validation[name] === undefined || validation[name].rules === undefined) {
+ validation[name] = {
+ rules: []
+ };
+ }
+ var
+ newValidation = {
+ rules: []
+ }
+ ;
+ if(module.is.shorthandRules(rules)) {
+ rules = Array.isArray(rules)
+ ? rules
+ : [rules]
+ ;
+ $.each(rules, function(_index, rule) {
+ newValidation.rules.push({ type: rule });
+ });
+ }
+ else {
+ newValidation.rules = rules.rules;
+ }
+ // For each new rule, check if there's not already one with the same type
+ $.each(newValidation.rules, function (_index, rule) {
+ if ($.grep(validation[name].rules, function(item){ return item.type == rule.type; }).length == 0) {
+ validation[name].rules.push(rule);
+ }
+ });
+ module.debug('Adding rules', newValidation.rules, validation);
+ },
+ fields: function(fields) {
+ var
+ newValidation
+ ;
+ if(fields && module.is.shorthandFields(fields)) {
+ newValidation = module.get.fieldsFromShorthand(fields);
+ }
+ else {
+ newValidation = fields;
+ }
+ validation = $.extend({}, validation, newValidation);
+ },
+ prompt: function(identifier, errors, internal) {
+ var
+ $field = module.get.field(identifier),
+ $fieldGroup = $field.closest($group),
+ $prompt = $fieldGroup.children(selector.prompt),
+ promptExists = ($prompt.length !== 0)
+ ;
+ errors = (typeof errors == 'string')
+ ? [errors]
+ : errors
+ ;
+ module.verbose('Adding field error state', identifier);
+ if(!internal) {
+ $fieldGroup
+ .addClass(className.error)
+ ;
+ }
+ if(settings.inline) {
+ if(!promptExists) {
+ $prompt = settings.templates.prompt(errors, className.label);
+ $prompt
+ .appendTo($fieldGroup)
+ ;
+ }
+ $prompt
+ .html(errors[0])
+ ;
+ if(!promptExists) {
+ if(settings.transition && module.can.useElement('transition') && $module.transition('is supported')) {
+ module.verbose('Displaying error with css transition', settings.transition);
+ $prompt.transition(settings.transition + ' in', settings.duration);
+ }
+ else {
+ module.verbose('Displaying error with fallback javascript animation');
+ $prompt
+ .fadeIn(settings.duration)
+ ;
+ }
+ }
+ else {
+ module.verbose('Inline errors are disabled, no inline error added', identifier);
+ }
+ }
+ },
+ errors: function(errors) {
+ module.debug('Adding form error messages', errors);
+ module.set.error();
+ $message
+ .html( settings.templates.error(errors) )
+ ;
+ }
+ },
+
+ remove: {
+ errors: function() {
+ module.debug('Removing form error messages');
+ $message.empty();
+ },
+ states: function() {
+ $module.removeClass(className.error).removeClass(className.success);
+ if(!settings.inline) {
+ module.remove.errors();
+ }
+ module.determine.isDirty();
+ },
+ rule: function(field, rule) {
+ var
+ rules = Array.isArray(rule)
+ ? rule
+ : [rule]
+ ;
+ if(validation[field] === undefined || !Array.isArray(validation[field].rules)) {
+ return;
+ }
+ if(rule === undefined) {
+ module.debug('Removed all rules');
+ validation[field].rules = [];
+ return;
+ }
+ $.each(validation[field].rules, function(index, rule) {
+ if(rule && rules.indexOf(rule.type) !== -1) {
+ module.debug('Removed rule', rule.type);
+ validation[field].rules.splice(index, 1);
+ }
+ });
+ },
+ field: function(field) {
+ var
+ fields = Array.isArray(field)
+ ? field
+ : [field]
+ ;
+ $.each(fields, function(index, field) {
+ module.remove.rule(field);
+ });
+ },
+ // alias
+ rules: function(field, rules) {
+ if(Array.isArray(field)) {
+ $.each(field, function(index, field) {
+ module.remove.rule(field, rules);
+ });
+ }
+ else {
+ module.remove.rule(field, rules);
+ }
+ },
+ fields: function(fields) {
+ module.remove.field(fields);
+ },
+ prompt: function(identifier) {
+ var
+ $field = module.get.field(identifier),
+ $fieldGroup = $field.closest($group),
+ $prompt = $fieldGroup.children(selector.prompt)
+ ;
+ $fieldGroup
+ .removeClass(className.error)
+ ;
+ if(settings.inline && $prompt.is(':visible')) {
+ module.verbose('Removing prompt for field', identifier);
+ if(settings.transition && module.can.useElement('transition') && $module.transition('is supported')) {
+ $prompt.transition(settings.transition + ' out', settings.duration, function() {
+ $prompt.remove();
+ });
+ }
+ else {
+ $prompt
+ .fadeOut(settings.duration, function(){
+ $prompt.remove();
+ })
+ ;
+ }
+ }
+ }
+ },
+
+ set: {
+ success: function() {
+ $module
+ .removeClass(className.error)
+ .addClass(className.success)
+ ;
+ },
+ defaults: function () {
+ $field.each(function (index, el) {
+ var
+ $el = $(el),
+ $parent = $el.parent(),
+ isCheckbox = ($el.filter(selector.checkbox).length > 0),
+ isDropdown = $parent.is(selector.uiDropdown) && module.can.useElement('dropdown'),
+ $calendar = $el.closest(selector.uiCalendar),
+ isCalendar = ($calendar.length > 0 && module.can.useElement('calendar')),
+ value = (isCheckbox)
+ ? $el.is(':checked')
+ : $el.val()
+ ;
+ if (isDropdown) {
+ $parent.dropdown('save defaults');
+ }
+ else if (isCalendar) {
+ $calendar.calendar('refresh');
+ }
+ $el.data(metadata.defaultValue, value);
+ $el.data(metadata.isDirty, false);
+ });
+ },
+ error: function() {
+ $module
+ .removeClass(className.success)
+ .addClass(className.error)
+ ;
+ },
+ value: function (field, value) {
+ var
+ fields = {}
+ ;
+ fields[field] = value;
+ return module.set.values.call(element, fields);
+ },
+ values: function (fields) {
+ if($.isEmptyObject(fields)) {
+ return;
+ }
+ $.each(fields, function(key, value) {
+ var
+ $field = module.get.field(key),
+ $element = $field.parent(),
+ $calendar = $field.closest(selector.uiCalendar),
+ isMultiple = Array.isArray(value),
+ isCheckbox = $element.is(selector.uiCheckbox) && module.can.useElement('checkbox'),
+ isDropdown = $element.is(selector.uiDropdown) && module.can.useElement('dropdown'),
+ isRadio = ($field.is(selector.radio) && isCheckbox),
+ isCalendar = ($calendar.length > 0 && module.can.useElement('calendar')),
+ fieldExists = ($field.length > 0),
+ $multipleField
+ ;
+ if(fieldExists) {
+ if(isMultiple && isCheckbox) {
+ module.verbose('Selecting multiple', value, $field);
+ $element.checkbox('uncheck');
+ $.each(value, function(index, value) {
+ $multipleField = $field.filter('[value="' + value + '"]');
+ $element = $multipleField.parent();
+ if($multipleField.length > 0) {
+ $element.checkbox('check');
+ }
+ });
+ }
+ else if(isRadio) {
+ module.verbose('Selecting radio value', value, $field);
+ $field.filter('[value="' + value + '"]')
+ .parent(selector.uiCheckbox)
+ .checkbox('check')
+ ;
+ }
+ else if(isCheckbox) {
+ module.verbose('Setting checkbox value', value, $element);
+ if(value === true || value === 1) {
+ $element.checkbox('check');
+ }
+ else {
+ $element.checkbox('uncheck');
+ }
+ }
+ else if(isDropdown) {
+ module.verbose('Setting dropdown value', value, $element);
+ $element.dropdown('set selected', value);
+ }
+ else if (isCalendar) {
+ $calendar.calendar('set date',value);
+ }
+ else {
+ module.verbose('Setting field value', value, $field);
+ $field.val(value);
+ }
+ }
+ });
+ },
+ dirty: function() {
+ module.verbose('Setting state dirty');
+ dirty = true;
+ history[0] = history[1];
+ history[1] = 'dirty';
+
+ if (module.is.justClean()) {
+ $module.trigger('dirty');
+ }
+ },
+ clean: function() {
+ module.verbose('Setting state clean');
+ dirty = false;
+ history[0] = history[1];
+ history[1] = 'clean';
+
+ if (module.is.justDirty()) {
+ $module.trigger('clean');
+ }
+ },
+ asClean: function() {
+ module.set.defaults();
+ module.set.clean();
+ },
+ asDirty: function() {
+ module.set.defaults();
+ module.set.dirty();
+ },
+ autoCheck: function() {
+ module.debug('Enabling auto check on required fields');
+ $field.each(function (_index, el) {
+ var
+ $el = $(el),
+ $elGroup = $(el).closest($group),
+ isCheckbox = ($el.filter(selector.checkbox).length > 0),
+ isRequired = $el.prop('required') || $elGroup.hasClass(className.required) || $elGroup.parent().hasClass(className.required),
+ isDisabled = $el.is(':disabled') || $elGroup.hasClass(className.disabled) || $elGroup.parent().hasClass(className.disabled),
+ validation = module.get.validation($el),
+ hasEmptyRule = validation
+ ? $.grep(validation.rules, function(rule) { return rule.type == "empty" }) !== 0
+ : false,
+ identifier = validation.identifier || $el.attr('id') || $el.attr('name') || $el.data(metadata.validate)
+ ;
+ if (isRequired && !isDisabled && !hasEmptyRule && identifier !== undefined) {
+ if (isCheckbox) {
+ module.verbose("Adding 'checked' rule on field", identifier);
+ module.add.rule(identifier, "checked");
+ } else {
+ module.verbose("Adding 'empty' rule on field", identifier);
+ module.add.rule(identifier, "empty");
+ }
+ }
+ });
+ }
+ },
+
+ validate: {
+
+ form: function(event, ignoreCallbacks) {
+ var values = module.get.values();
+
+ // input keydown event will fire submit repeatedly by browser default
+ if(keyHeldDown) {
+ return false;
+ }
+
+ // reset errors
+ formErrors = [];
+ if( module.determine.isValid() ) {
+ module.debug('Form has no validation errors, submitting');
+ module.set.success();
+ if(!settings.inline) {
+ module.remove.errors();
+ }
+ if(ignoreCallbacks !== true) {
+ return settings.onSuccess.call(element, event, values);
+ }
+ }
+ else {
+ module.debug('Form has errors');
+ submitting = false;
+ module.set.error();
+ if(!settings.inline) {
+ module.add.errors(formErrors);
+ }
+ // prevent ajax submit
+ if(event && $module.data('moduleApi') !== undefined) {
+ event.stopImmediatePropagation();
+ }
+ if(ignoreCallbacks !== true) {
+ return settings.onFailure.call(element, formErrors, values);
+ }
+ }
+ },
+
+ // takes a validation object and returns whether field passes validation
+ field: function(field, fieldName, showErrors) {
+ showErrors = (showErrors !== undefined)
+ ? showErrors
+ : true
+ ;
+ if(typeof field == 'string') {
+ module.verbose('Validating field', field);
+ fieldName = field;
+ field = validation[field];
+ }
+ var
+ identifier = field.identifier || fieldName,
+ $field = module.get.field(identifier),
+ $dependsField = (field.depends)
+ ? module.get.field(field.depends)
+ : false,
+ fieldValid = true,
+ fieldErrors = []
+ ;
+ if(!field.identifier) {
+ module.debug('Using field name as identifier', identifier);
+ field.identifier = identifier;
+ }
+ var isDisabled = !$field.filter(':not(:disabled)').length;
+ if(isDisabled) {
+ module.debug('Field is disabled. Skipping', identifier);
+ }
+ else if(field.optional && module.is.blank($field)){
+ module.debug('Field is optional and blank. Skipping', identifier);
+ }
+ else if(field.depends && module.is.empty($dependsField)) {
+ module.debug('Field depends on another value that is not present or empty. Skipping', $dependsField);
+ }
+ else if(field.rules !== undefined) {
+ if(showErrors) {
+ $field.closest($group).removeClass(className.error);
+ }
+ $.each(field.rules, function(index, rule) {
+ if( module.has.field(identifier)) {
+ var invalidFields = module.validate.rule(field, rule,true) || [];
+ if (invalidFields.length>0){
+ module.debug('Field is invalid', identifier, rule.type);
+ fieldErrors.push(module.get.prompt(rule, field));
+ fieldValid = false;
+ if(showErrors){
+ $(invalidFields).closest($group).addClass(className.error);
+ }
+ }
+ }
+ });
+ }
+ if(fieldValid) {
+ if(showErrors) {
+ module.remove.prompt(identifier, fieldErrors);
+ settings.onValid.call($field);
+ }
+ }
+ else {
+ if(showErrors) {
+ formErrors = formErrors.concat(fieldErrors);
+ module.add.prompt(identifier, fieldErrors, true);
+ settings.onInvalid.call($field, fieldErrors);
+ }
+ return false;
+ }
+ return true;
+ },
+
+ // takes validation rule and returns whether field passes rule
+ rule: function(field, rule, internal) {
+ var
+ $field = module.get.field(field.identifier),
+ ancillary = module.get.ancillaryValue(rule),
+ ruleName = module.get.ruleName(rule),
+ ruleFunction = settings.rules[ruleName],
+ invalidFields = [],
+ isCheckbox = $field.is(selector.checkbox),
+ isValid = function(field){
+ var value = (isCheckbox ? $(field).filter(':checked').val() : $(field).val());
+ // cast to string avoiding encoding special values
+ value = (value === undefined || value === '' || value === null)
+ ? ''
+ : (settings.shouldTrim) ? String(value + '').trim() : String(value + '')
+ ;
+ return ruleFunction.call(field, value, ancillary, $module);
+ }
+ ;
+ if( !$.isFunction(ruleFunction) ) {
+ module.error(error.noRule, ruleName);
+ return;
+ }
+ if(isCheckbox) {
+ if (!isValid($field)) {
+ invalidFields = $field;
+ }
+ } else {
+ $.each($field, function (index, field) {
+ if (!isValid(field)) {
+ invalidFields.push(field);
+ }
+ });
+ }
+ return internal ? invalidFields : !(invalidFields.length>0);
+ }
+ },
+
+ setting: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ settings[name] = value;
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ 'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if($allModules.length > 1) {
+ title += ' ' + '(' + $allModules.length + ')';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ return false;
+ }
+ });
+ }
+ if( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+ module.initialize();
+ })
+ ;
+
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : this
+ ;
+};
+
+$.fn.form.settings = {
+
+ name : 'Form',
+ namespace : 'form',
+
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ fields : false,
+
+ keyboardShortcuts : true,
+ on : 'submit',
+ inline : false,
+
+ delay : 200,
+ revalidate : true,
+ shouldTrim : true,
+
+ transition : 'scale',
+ duration : 200,
+
+ autoCheckRequired : false,
+ preventLeaving : false,
+ dateHandling : 'date', // 'date', 'input', 'formatter'
+
+ onValid : function() {},
+ onInvalid : function() {},
+ onSuccess : function() { return true; },
+ onFailure : function() { return false; },
+ onDirty : function() {},
+ onClean : function() {},
+
+ metadata : {
+ defaultValue : 'default',
+ validate : 'validate',
+ isDirty : 'isDirty'
+ },
+
+ regExp: {
+ htmlID : /^[a-zA-Z][\w:.-]*$/g,
+ bracket : /\[(.*)\]/i,
+ decimal : /^\d+\.?\d*$/,
+ email : /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,
+ escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|:,=@]/g,
+ flags : /^\/(.*)\/(.*)?/,
+ integer : /^\-?\d+$/,
+ number : /^\-?\d*(\.\d+)?$/,
+ url : /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/i
+ },
+
+ text: {
+ unspecifiedRule : 'Please enter a valid value',
+ unspecifiedField : 'This field',
+ leavingMessage : 'There are unsaved changes on this page which will be discarded if you continue.'
+ },
+
+ prompt: {
+ empty : '{name} must have a value',
+ checked : '{name} must be checked',
+ email : '{name} must be a valid e-mail',
+ url : '{name} must be a valid url',
+ regExp : '{name} is not formatted correctly',
+ integer : '{name} must be an integer',
+ decimal : '{name} must be a decimal number',
+ number : '{name} must be set to a number',
+ is : '{name} must be "{ruleValue}"',
+ isExactly : '{name} must be exactly "{ruleValue}"',
+ not : '{name} cannot be set to "{ruleValue}"',
+ notExactly : '{name} cannot be set to exactly "{ruleValue}"',
+ contain : '{name} must contain "{ruleValue}"',
+ containExactly : '{name} must contain exactly "{ruleValue}"',
+ doesntContain : '{name} cannot contain "{ruleValue}"',
+ doesntContainExactly : '{name} cannot contain exactly "{ruleValue}"',
+ minLength : '{name} must be at least {ruleValue} characters',
+ length : '{name} must be at least {ruleValue} characters',
+ exactLength : '{name} must be exactly {ruleValue} characters',
+ maxLength : '{name} cannot be longer than {ruleValue} characters',
+ match : '{name} must match {ruleValue} field',
+ different : '{name} must have a different value than {ruleValue} field',
+ creditCard : '{name} must be a valid credit card number',
+ minCount : '{name} must have at least {ruleValue} choices',
+ exactCount : '{name} must have exactly {ruleValue} choices',
+ maxCount : '{name} must have {ruleValue} or less choices'
+ },
+
+ selector : {
+ checkbox : 'input[type="checkbox"], input[type="radio"]',
+ clear : '.clear',
+ field : 'input:not(.search), textarea, select',
+ group : '.field',
+ input : 'input',
+ message : '.error.message',
+ prompt : '.prompt.label',
+ radio : 'input[type="radio"]',
+ reset : '.reset:not([type="reset"])',
+ submit : '.submit:not([type="submit"])',
+ uiCheckbox : '.ui.checkbox',
+ uiDropdown : '.ui.dropdown',
+ uiCalendar : '.ui.calendar'
+ },
+
+ className : {
+ error : 'error',
+ label : 'ui basic red pointing prompt label',
+ pressed : 'down',
+ success : 'success',
+ required : 'required',
+ disabled : 'disabled'
+ },
+
+ error: {
+ identifier : 'You must specify a string identifier for each field',
+ method : 'The method you called is not defined.',
+ noRule : 'There is no rule matching the one you specified',
+ oldSyntax : 'Starting in 2.0 forms now only take a single settings object. Validation settings converted to new syntax automatically.',
+ noElement : 'This module requires ui {element}'
+ },
+
+ templates: {
+
+ // template that produces error message
+ error: function(errors) {
+ var
+ html = '<ul class="list">'
+ ;
+ $.each(errors, function(index, value) {
+ html += '<li>' + value + '</li>';
+ });
+ html += '</ul>';
+ return $(html);
+ },
+
+ // template that produces label
+ prompt: function(errors, labelClasses) {
+ return $('<div/>')
+ .addClass(labelClasses)
+ .html(errors[0])
+ ;
+ }
+ },
+
+ formatter: {
+ date: function(date) {
+ return Intl.DateTimeFormat('en-GB').format(date);
+ },
+ datetime: function(date) {
+ return Intl.DateTimeFormat('en-GB', {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ }).format(date);
+ },
+ time: function(date) {
+ return Intl.DateTimeFormat('en-GB', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ }).format(date);
+ },
+ month: function(date) {
+ return Intl.DateTimeFormat('en-GB', {
+ month: '2-digit',
+ year: 'numeric'
+ }).format(date);
+ },
+ year: function(date) {
+ return Intl.DateTimeFormat('en-GB', {
+ year: 'numeric'
+ }).format(date);
+ }
+ },
+
+ rules: {
+
+ // is not empty or blank string
+ empty: function(value) {
+ return !(value === undefined || '' === value || Array.isArray(value) && value.length === 0);
+ },
+
+ // checkbox checked
+ checked: function() {
+ return ($(this).filter(':checked').length > 0);
+ },
+
+ // is most likely an email
+ email: function(value){
+ return $.fn.form.settings.regExp.email.test(value);
+ },
+
+ // value is most likely url
+ url: function(value) {
+ return $.fn.form.settings.regExp.url.test(value);
+ },
+
+ // matches specified regExp
+ regExp: function(value, regExp) {
+ if(regExp instanceof RegExp) {
+ return value.match(regExp);
+ }
+ var
+ regExpParts = regExp.match($.fn.form.settings.regExp.flags),
+ flags
+ ;
+ // regular expression specified as /baz/gi (flags)
+ if(regExpParts) {
+ regExp = (regExpParts.length >= 2)
+ ? regExpParts[1]
+ : regExp
+ ;
+ flags = (regExpParts.length >= 3)
+ ? regExpParts[2]
+ : ''
+ ;
+ }
+ return value.match( new RegExp(regExp, flags) );
+ },
+
+ // is valid integer or matches range
+ integer: function(value, range) {
+ var
+ intRegExp = $.fn.form.settings.regExp.integer,
+ min,
+ max,
+ parts
+ ;
+ if( !range || ['', '..'].indexOf(range) !== -1) {
+ // do nothing
+ }
+ else if(range.indexOf('..') == -1) {
+ if(intRegExp.test(range)) {
+ min = max = range - 0;
+ }
+ }
+ else {
+ parts = range.split('..', 2);
+ if(intRegExp.test(parts[0])) {
+ min = parts[0] - 0;
+ }
+ if(intRegExp.test(parts[1])) {
+ max = parts[1] - 0;
+ }
+ }
+ return (
+ intRegExp.test(value) &&
+ (min === undefined || value >= min) &&
+ (max === undefined || value <= max)
+ );
+ },
+
+ // is valid number (with decimal)
+ decimal: function(value) {
+ return $.fn.form.settings.regExp.decimal.test(value);
+ },
+
+ // is valid number
+ number: function(value) {
+ return $.fn.form.settings.regExp.number.test(value);
+ },
+
+ // is value (case insensitive)
+ is: function(value, text) {
+ text = (typeof text == 'string')
+ ? text.toLowerCase()
+ : text
+ ;
+ value = (typeof value == 'string')
+ ? value.toLowerCase()
+ : value
+ ;
+ return (value == text);
+ },
+
+ // is value
+ isExactly: function(value, text) {
+ return (value == text);
+ },
+
+ // value is not another value (case insensitive)
+ not: function(value, notValue) {
+ value = (typeof value == 'string')
+ ? value.toLowerCase()
+ : value
+ ;
+ notValue = (typeof notValue == 'string')
+ ? notValue.toLowerCase()
+ : notValue
+ ;
+ return (value != notValue);
+ },
+
+ // value is not another value (case sensitive)
+ notExactly: function(value, notValue) {
+ return (value != notValue);
+ },
+
+ // value contains text (insensitive)
+ contains: function(value, text) {
+ // escape regex characters
+ text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
+ return (value.search( new RegExp(text, 'i') ) !== -1);
+ },
+
+ // value contains text (case sensitive)
+ containsExactly: function(value, text) {
+ // escape regex characters
+ text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
+ return (value.search( new RegExp(text) ) !== -1);
+ },
+
+ // value contains text (insensitive)
+ doesntContain: function(value, text) {
+ // escape regex characters
+ text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
+ return (value.search( new RegExp(text, 'i') ) === -1);
+ },
+
+ // value contains text (case sensitive)
+ doesntContainExactly: function(value, text) {
+ // escape regex characters
+ text = text.replace($.fn.form.settings.regExp.escape, "\\$&");
+ return (value.search( new RegExp(text) ) === -1);
+ },
+
+ // is at least string length
+ minLength: function(value, requiredLength) {
+ return (value !== undefined)
+ ? (value.length >= requiredLength)
+ : false
+ ;
+ },
+
+ // see rls notes for 2.0.6 (this is a duplicate of minLength)
+ length: function(value, requiredLength) {
+ return (value !== undefined)
+ ? (value.length >= requiredLength)
+ : false
+ ;
+ },
+
+ // is exactly length
+ exactLength: function(value, requiredLength) {
+ return (value !== undefined)
+ ? (value.length == requiredLength)
+ : false
+ ;
+ },
+
+ // is less than length
+ maxLength: function(value, maxLength) {
+ return (value !== undefined)
+ ? (value.length <= maxLength)
+ : false
+ ;
+ },
+
+ // matches another field
+ match: function(value, identifier, $module) {
+ var
+ matchingValue,
+ matchingElement
+ ;
+ if((matchingElement = $module.find('[data-validate="'+ identifier +'"]')).length > 0 ) {
+ matchingValue = matchingElement.val();
+ }
+ else if((matchingElement = $module.find('#' + identifier)).length > 0) {
+ matchingValue = matchingElement.val();
+ }
+ else if((matchingElement = $module.find('[name="' + identifier +'"]')).length > 0) {
+ matchingValue = matchingElement.val();
+ }
+ else if((matchingElement = $module.find('[name="' + identifier +'[]"]')).length > 0 ) {
+ matchingValue = matchingElement;
+ }
+ return (matchingValue !== undefined)
+ ? ( value.toString() == matchingValue.toString() )
+ : false
+ ;
+ },
+
+ // different than another field
+ different: function(value, identifier, $module) {
+ // use either id or name of field
+ var
+ matchingValue,
+ matchingElement
+ ;
+ if((matchingElement = $module.find('[data-validate="'+ identifier +'"]')).length > 0 ) {
+ matchingValue = matchingElement.val();
+ }
+ else if((matchingElement = $module.find('#' + identifier)).length > 0) {
+ matchingValue = matchingElement.val();
+ }
+ else if((matchingElement = $module.find('[name="' + identifier +'"]')).length > 0) {
+ matchingValue = matchingElement.val();
+ }
+ else if((matchingElement = $module.find('[name="' + identifier +'[]"]')).length > 0 ) {
+ matchingValue = matchingElement;
+ }
+ return (matchingValue !== undefined)
+ ? ( value.toString() !== matchingValue.toString() )
+ : false
+ ;
+ },
+
+ creditCard: function(cardNumber, cardTypes) {
+ var
+ cards = {
+ visa: {
+ pattern : /^4/,
+ length : [16]
+ },
+ amex: {
+ pattern : /^3[47]/,
+ length : [15]
+ },
+ mastercard: {
+ pattern : /^5[1-5]/,
+ length : [16]
+ },
+ discover: {
+ pattern : /^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)/,
+ length : [16]
+ },
+ unionPay: {
+ pattern : /^(62|88)/,
+ length : [16, 17, 18, 19]
+ },
+ jcb: {
+ pattern : /^35(2[89]|[3-8][0-9])/,
+ length : [16]
+ },
+ maestro: {
+ pattern : /^(5018|5020|5038|6304|6759|676[1-3])/,
+ length : [12, 13, 14, 15, 16, 17, 18, 19]
+ },
+ dinersClub: {
+ pattern : /^(30[0-5]|^36)/,
+ length : [14]
+ },
+ laser: {
+ pattern : /^(6304|670[69]|6771)/,
+ length : [16, 17, 18, 19]
+ },
+ visaElectron: {
+ pattern : /^(4026|417500|4508|4844|491(3|7))/,
+ length : [16]
+ }
+ },
+ valid = {},
+ validCard = false,
+ requiredTypes = (typeof cardTypes == 'string')
+ ? cardTypes.split(',')
+ : false,
+ unionPay,
+ validation
+ ;
+
+ if(typeof cardNumber !== 'string' || cardNumber.length === 0) {
+ return;
+ }
+
+ // allow dashes in card
+ cardNumber = cardNumber.replace(/[\-]/g, '');
+
+ // verify card types
+ if(requiredTypes) {
+ $.each(requiredTypes, function(index, type){
+ // verify each card type
+ validation = cards[type];
+ if(validation) {
+ valid = {
+ length : ($.inArray(cardNumber.length, validation.length) !== -1),
+ pattern : (cardNumber.search(validation.pattern) !== -1)
+ };
+ if(valid.length && valid.pattern) {
+ validCard = true;
+ }
+ }
+ });
+
+ if(!validCard) {
+ return false;
+ }
+ }
+
+ // skip luhn for UnionPay
+ unionPay = {
+ number : ($.inArray(cardNumber.length, cards.unionPay.length) !== -1),
+ pattern : (cardNumber.search(cards.unionPay.pattern) !== -1)
+ };
+ if(unionPay.number && unionPay.pattern) {
+ return true;
+ }
+
+ // verify luhn, adapted from <https://gist.github.com/2134376>
+ var
+ length = cardNumber.length,
+ multiple = 0,
+ producedValue = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
+ ],
+ sum = 0
+ ;
+ while (length--) {
+ sum += producedValue[multiple][parseInt(cardNumber.charAt(length), 10)];
+ multiple ^= 1;
+ }
+ return (sum % 10 === 0 && sum > 0);
+ },
+
+ minCount: function(value, minCount) {
+ if(minCount == 0) {
+ return true;
+ }
+ if(minCount == 1) {
+ return (value !== '');
+ }
+ return (value.split(',').length >= minCount);
+ },
+
+ exactCount: function(value, exactCount) {
+ if(exactCount == 0) {
+ return (value === '');
+ }
+ if(exactCount == 1) {
+ return (value !== '' && value.search(',') === -1);
+ }
+ return (value.split(',').length == exactCount);
+ },
+
+ maxCount: function(value, maxCount) {
+ if(maxCount == 0) {
+ return false;
+ }
+ if(maxCount == 1) {
+ return (value.search(',') === -1);
+ }
+ return (value.split(',').length <= maxCount);
+ }
+ }
+
+};
+
+})( jQuery, window, document );
+
+/*!
+ * # Fomantic-UI - Modal
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.modal = function(parameters) {
+ var
+ $allModules = $(this),
+ $window = $(window),
+ $document = $(document),
+ $body = $('body'),
+
+ moduleSelector = $allModules.selector || '',
+
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+
+ requestAnimationFrame = window.requestAnimationFrame
+ || window.mozRequestAnimationFrame
+ || window.webkitRequestAnimationFrame
+ || window.msRequestAnimationFrame
+ || function(callback) { setTimeout(callback, 0); },
+
+ returnedValue
+ ;
+
+ $allModules
+ .each(function() {
+ var
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.modal.settings, parameters)
+ : $.extend({}, $.fn.modal.settings),
+
+ selector = settings.selector,
+ className = settings.className,
+ namespace = settings.namespace,
+ error = settings.error,
+
+ eventNamespace = '.' + namespace,
+ moduleNamespace = 'module-' + namespace,
+
+ $module = $(this),
+ $context = $(settings.context),
+ $close = $module.find(selector.close),
+
+ $allModals,
+ $otherModals,
+ $focusedElement,
+ $dimmable,
+ $dimmer,
+
+ element = this,
+ instance = $module.data(moduleNamespace),
+
+ ignoreRepeatedEvents = false,
+
+ initialMouseDownInModal,
+ initialMouseDownInScrollbar,
+ initialBodyMargin = '',
+ tempBodyMargin = '',
+
+ elementEventNamespace,
+ id,
+ observer,
+ module
+ ;
+ module = {
+
+ initialize: function() {
+ module.cache = {};
+ module.verbose('Initializing dimmer', $context);
+
+ module.create.id();
+ module.create.dimmer();
+
+ if ( settings.allowMultiple ) {
+ module.create.innerDimmer();
+ }
+ if (!settings.centered){
+ $module.addClass('top aligned');
+ }
+ module.refreshModals();
+
+ module.bind.events();
+ if(settings.observeChanges) {
+ module.observeChanges();
+ }
+ module.instantiate();
+ },
+
+ instantiate: function() {
+ module.verbose('Storing instance of modal');
+ instance = module;
+ $module
+ .data(moduleNamespace, instance)
+ ;
+ },
+
+ create: {
+ dimmer: function() {
+ var
+ defaultSettings = {
+ debug : settings.debug,
+ dimmerName : 'modals'
+ },
+ dimmerSettings = $.extend(true, defaultSettings, settings.dimmerSettings)
+ ;
+ if($.fn.dimmer === undefined) {
+ module.error(error.dimmer);
+ return;
+ }
+ module.debug('Creating dimmer');
+ $dimmable = $context.dimmer(dimmerSettings);
+ if(settings.detachable) {
+ module.verbose('Modal is detachable, moving content into dimmer');
+ $dimmable.dimmer('add content', $module);
+ }
+ else {
+ module.set.undetached();
+ }
+ $dimmer = $dimmable.dimmer('get dimmer');
+ },
+ id: function() {
+ id = (Math.random().toString(16) + '000000000').substr(2, 8);
+ elementEventNamespace = '.' + id;
+ module.verbose('Creating unique id for element', id);
+ },
+ innerDimmer: function() {
+ if ( $module.find(selector.dimmer).length == 0 ) {
+ $module.prepend('<div class="ui inverted dimmer"></div>');
+ }
+ }
+ },
+
+ destroy: function() {
+ if (observer) {
+ observer.disconnect();
+ }
+ module.verbose('Destroying previous modal');
+ $module
+ .removeData(moduleNamespace)
+ .off(eventNamespace)
+ ;
+ $window.off(elementEventNamespace);
+ $dimmer.off(elementEventNamespace);
+ $close.off(eventNamespace);
+ $context.dimmer('destroy');
+ },
+
+ observeChanges: function() {
+ if('MutationObserver' in window) {
+ observer = new MutationObserver(function(mutations) {
+ module.debug('DOM tree modified, refreshing');
+ module.refresh();
+ });
+ observer.observe(element, {
+ childList : true,
+ subtree : true
+ });
+ module.debug('Setting up mutation observer', observer);
+ }
+ },
+
+ refresh: function() {
+ module.remove.scrolling();
+ module.cacheSizes();
+ if(!module.can.useFlex()) {
+ module.set.modalOffset();
+ }
+ module.set.screenHeight();
+ module.set.type();
+ },
+
+ refreshModals: function() {
+ $otherModals = $module.siblings(selector.modal);
+ $allModals = $otherModals.add($module);
+ },
+
+ attachEvents: function(selector, event) {
+ var
+ $toggle = $(selector)
+ ;
+ event = $.isFunction(module[event])
+ ? module[event]
+ : module.toggle
+ ;
+ if($toggle.length > 0) {
+ module.debug('Attaching modal events to element', selector, event);
+ $toggle
+ .off(eventNamespace)
+ .on('click' + eventNamespace, event)
+ ;
+ }
+ else {
+ module.error(error.notFound, selector);
+ }
+ },
+
+ bind: {
+ events: function() {
+ module.verbose('Attaching events');
+ $module
+ .on('click' + eventNamespace, selector.close, module.event.close)
+ .on('click' + eventNamespace, selector.approve, module.event.approve)
+ .on('click' + eventNamespace, selector.deny, module.event.deny)
+ ;
+ $window
+ .on('resize' + elementEventNamespace, module.event.resize)
+ ;
+ },
+ scrollLock: function() {
+ // touch events default to passive, due to changes in chrome to optimize mobile perf
+ $dimmable.get(0).addEventListener('touchmove', module.event.preventScroll, { passive: false });
+ }
+ },
+
+ unbind: {
+ scrollLock: function() {
+ $dimmable.get(0).removeEventListener('touchmove', module.event.preventScroll, { passive: false });
+ }
+ },
+
+ get: {
+ id: function() {
+ return (Math.random().toString(16) + '000000000').substr(2, 8);
+ }
+ },
+
+ event: {
+ approve: function() {
+ if(ignoreRepeatedEvents || settings.onApprove.call(element, $(this)) === false) {
+ module.verbose('Approve callback returned false cancelling hide');
+ return;
+ }
+ ignoreRepeatedEvents = true;
+ module.hide(function() {
+ ignoreRepeatedEvents = false;
+ });
+ },
+ preventScroll: function(event) {
+ if(event.target.className.indexOf('dimmer') !== -1) {
+ event.preventDefault();
+ }
+ },
+ deny: function() {
+ if(ignoreRepeatedEvents || settings.onDeny.call(element, $(this)) === false) {
+ module.verbose('Deny callback returned false cancelling hide');
+ return;
+ }
+ ignoreRepeatedEvents = true;
+ module.hide(function() {
+ ignoreRepeatedEvents = false;
+ });
+ },
+ close: function() {
+ module.hide();
+ },
+ mousedown: function(event) {
+ var
+ $target = $(event.target),
+ isRtl = module.is.rtl();
+ ;
+ initialMouseDownInModal = ($target.closest(selector.modal).length > 0);
+ if(initialMouseDownInModal) {
+ module.verbose('Mouse down event registered inside the modal');
+ }
+ initialMouseDownInScrollbar = module.is.scrolling() && ((!isRtl && $(window).outerWidth() - settings.scrollbarWidth <= event.clientX) || (isRtl && settings.scrollbarWidth >= event.clientX));
+ if(initialMouseDownInScrollbar) {
+ module.verbose('Mouse down event registered inside the scrollbar');
+ }
+ },
+ mouseup: function(event) {
+ if(!settings.closable) {
+ module.verbose('Dimmer clicked but closable setting is disabled');
+ return;
+ }
+ if(initialMouseDownInModal) {
+ module.debug('Dimmer clicked but mouse down was initially registered inside the modal');
+ return;
+ }
+ if(initialMouseDownInScrollbar){
+ module.debug('Dimmer clicked but mouse down was initially registered inside the scrollbar');
+ return;
+ }
+ var
+ $target = $(event.target),
+ isInModal = ($target.closest(selector.modal).length > 0),
+ isInDOM = $.contains(document.documentElement, event.target)
+ ;
+ if(!isInModal && isInDOM && module.is.active() && $module.hasClass(className.front) ) {
+ module.debug('Dimmer clicked, hiding all modals');
+ if(settings.allowMultiple) {
+ if(!module.hideAll()) {
+ return;
+ }
+ }
+ else if(!module.hide()){
+ return;
+ }
+ module.remove.clickaway();
+ }
+ },
+ debounce: function(method, delay) {
+ clearTimeout(module.timer);
+ module.timer = setTimeout(method, delay);
+ },
+ keyboard: function(event) {
+ var
+ keyCode = event.which,
+ escapeKey = 27
+ ;
+ if(keyCode == escapeKey) {
+ if(settings.closable) {
+ module.debug('Escape key pressed hiding modal');
+ if ( $module.hasClass(className.front) ) {
+ module.hide();
+ }
+ }
+ else {
+ module.debug('Escape key pressed, but closable is set to false');
+ }
+ event.preventDefault();
+ }
+ },
+ resize: function() {
+ if( $dimmable.dimmer('is active') && ( module.is.animating() || module.is.active() ) ) {
+ requestAnimationFrame(module.refresh);
+ }
+ }
+ },
+
+ toggle: function() {
+ if( module.is.active() || module.is.animating() ) {
+ module.hide();
+ }
+ else {
+ module.show();
+ }
+ },
+
+ show: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ module.refreshModals();
+ module.set.dimmerSettings();
+ module.set.dimmerStyles();
+
+ module.showModal(callback);
+ },
+
+ hide: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ module.refreshModals();
+ return module.hideModal(callback);
+ },
+
+ showModal: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( module.is.animating() || !module.is.active() ) {
+ module.showDimmer();
+ module.cacheSizes();
+ module.set.bodyMargin();
+ if(module.can.useFlex()) {
+ module.remove.legacy();
+ }
+ else {
+ module.set.legacy();
+ module.set.modalOffset();
+ module.debug('Using non-flex legacy modal positioning.');
+ }
+ module.set.screenHeight();
+ module.set.type();
+ module.set.clickaway();
+
+ if( !settings.allowMultiple && module.others.active() ) {
+ module.hideOthers(module.showModal);
+ }
+ else {
+ ignoreRepeatedEvents = false;
+ if( settings.allowMultiple ) {
+ if ( module.others.active() ) {
+ $otherModals.filter('.' + className.active).find(selector.dimmer).addClass('active');
+ }
+
+ if ( settings.detachable ) {
+ $module.detach().appendTo($dimmer);
+ }
+ }
+ settings.onShow.call(element);
+ if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
+ module.debug('Showing modal with css animations');
+ $module
+ .transition({
+ debug : settings.debug,
+ animation : settings.transition + ' in',
+ queue : settings.queue,
+ duration : settings.duration,
+ useFailSafe : true,
+ onComplete : function() {
+ settings.onVisible.apply(element);
+ if(settings.keyboardShortcuts) {
+ module.add.keyboardShortcuts();
+ }
+ module.save.focus();
+ module.set.active();
+ if(settings.autofocus) {
+ module.set.autofocus();
+ }
+ callback();
+ }
+ })
+ ;
+ }
+ else {
+ module.error(error.noTransition);
+ }
+ }
+ }
+ else {
+ module.debug('Modal is already visible');
+ }
+ },
+
+ hideModal: function(callback, keepDimmed, hideOthersToo) {
+ var
+ $previousModal = $otherModals.filter('.' + className.active).last()
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ module.debug('Hiding modal');
+ if(settings.onHide.call(element, $(this)) === false) {
+ module.verbose('Hide callback returned false cancelling hide');
+ ignoreRepeatedEvents = false;
+ return false;
+ }
+
+ if( module.is.animating() || module.is.active() ) {
+ if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
+ module.remove.active();
+ $module
+ .transition({
+ debug : settings.debug,
+ animation : settings.transition + ' out',
+ queue : settings.queue,
+ duration : settings.duration,
+ useFailSafe : true,
+ onStart : function() {
+ if(!module.others.active() && !module.others.animating() && !keepDimmed) {
+ module.hideDimmer();
+ }
+ if( settings.keyboardShortcuts && !module.others.active() ) {
+ module.remove.keyboardShortcuts();
+ }
+ },
+ onComplete : function() {
+ module.unbind.scrollLock();
+ if ( settings.allowMultiple ) {
+ $previousModal.addClass(className.front);
+ $module.removeClass(className.front);
+
+ if ( hideOthersToo ) {
+ $allModals.find(selector.dimmer).removeClass('active');
+ }
+ else {
+ $previousModal.find(selector.dimmer).removeClass('active');
+ }
+ }
+ settings.onHidden.call(element);
+ module.remove.dimmerStyles();
+ module.restore.focus();
+ callback();
+ }
+ })
+ ;
+ }
+ else {
+ module.error(error.noTransition);
+ }
+ }
+ },
+
+ showDimmer: function() {
+ if($dimmable.dimmer('is animating') || !$dimmable.dimmer('is active') ) {
+ module.save.bodyMargin();
+ module.debug('Showing dimmer');
+ $dimmable.dimmer('show');
+ }
+ else {
+ module.debug('Dimmer already visible');
+ }
+ },
+
+ hideDimmer: function() {
+ if( $dimmable.dimmer('is animating') || ($dimmable.dimmer('is active')) ) {
+ module.unbind.scrollLock();
+ $dimmable.dimmer('hide', function() {
+ module.restore.bodyMargin();
+ module.remove.clickaway();
+ module.remove.screenHeight();
+ });
+ }
+ else {
+ module.debug('Dimmer is not visible cannot hide');
+ return;
+ }
+ },
+
+ hideAll: function(callback) {
+ var
+ $visibleModals = $allModals.filter('.' + className.active + ', .' + className.animating)
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( $visibleModals.length > 0 ) {
+ module.debug('Hiding all visible modals');
+ var hideOk = true;
+//check in reverse order trying to hide most top displayed modal first
+ $($visibleModals.get().reverse()).each(function(index,element){
+ if(hideOk){
+ hideOk = $(element).modal('hide modal', callback, false, true);
+ }
+ });
+ if(hideOk) {
+ module.hideDimmer();
+ }
+ return hideOk;
+ }
+ },
+
+ hideOthers: function(callback) {
+ var
+ $visibleModals = $otherModals.filter('.' + className.active + ', .' + className.animating)
+ ;
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( $visibleModals.length > 0 ) {
+ module.debug('Hiding other modals', $otherModals);
+ $visibleModals
+ .modal('hide modal', callback, true)
+ ;
+ }
+ },
+
+ others: {
+ active: function() {
+ return ($otherModals.filter('.' + className.active).length > 0);
+ },
+ animating: function() {
+ return ($otherModals.filter('.' + className.animating).length > 0);
+ }
+ },
+
+
+ add: {
+ keyboardShortcuts: function() {
+ module.verbose('Adding keyboard shortcuts');
+ $document
+ .on('keyup' + eventNamespace, module.event.keyboard)
+ ;
+ }
+ },
+
+ save: {
+ focus: function() {
+ var
+ $activeElement = $(document.activeElement),
+ inCurrentModal = $activeElement.closest($module).length > 0
+ ;
+ if(!inCurrentModal) {
+ $focusedElement = $(document.activeElement).blur();
+ }
+ },
+ bodyMargin: function() {
+ initialBodyMargin = $body.css('margin-'+(module.can.leftBodyScrollbar() ? 'left':'right'));
+ var bodyMarginRightPixel = parseInt(initialBodyMargin.replace(/[^\d.]/g, '')),
+ bodyScrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+ tempBodyMargin = bodyMarginRightPixel + bodyScrollbarWidth;
+ }
+ },
+
+ restore: {
+ focus: function() {
+ if($focusedElement && $focusedElement.length > 0 && settings.restoreFocus) {
+ $focusedElement.focus();
+ }
+ },
+ bodyMargin: function() {
+ var position = module.can.leftBodyScrollbar() ? 'left':'right';
+ $body.css('margin-'+position, initialBodyMargin);
+ $body.find(selector.bodyFixed.replace('right',position)).css('padding-'+position, initialBodyMargin);
+ }
+ },
+
+ remove: {
+ active: function() {
+ $module.removeClass(className.active);
+ },
+ legacy: function() {
+ $module.removeClass(className.legacy);
+ },
+ clickaway: function() {
+ if (!settings.detachable) {
+ $module
+ .off('mousedown' + elementEventNamespace)
+ ;
+ }
+ $dimmer
+ .off('mousedown' + elementEventNamespace)
+ ;
+ $dimmer
+ .off('mouseup' + elementEventNamespace)
+ ;
+ },
+ dimmerStyles: function() {
+ $dimmer.removeClass(className.inverted);
+ $dimmable.removeClass(className.blurring);
+ },
+ bodyStyle: function() {
+ if($body.attr('style') === '') {
+ module.verbose('Removing style attribute');
+ $body.removeAttr('style');
+ }
+ },
+ screenHeight: function() {
+ module.debug('Removing page height');
+ $body
+ .css('height', '')
+ ;
+ },
+ keyboardShortcuts: function() {
+ module.verbose('Removing keyboard shortcuts');
+ $document
+ .off('keyup' + eventNamespace)
+ ;
+ },
+ scrolling: function() {
+ $dimmable.removeClass(className.scrolling);
+ $module.removeClass(className.scrolling);
+ }
+ },
+
+ cacheSizes: function() {
+ $module.addClass(className.loading);
+ var
+ scrollHeight = $module.prop('scrollHeight'),
+ modalWidth = $module.outerWidth(),
+ modalHeight = $module.outerHeight()
+ ;
+ if(module.cache.pageHeight === undefined || modalHeight !== 0) {
+ $.extend(module.cache, {
+ pageHeight : $(document).outerHeight(),
+ width : modalWidth,
+ height : modalHeight + settings.offset,
+ scrollHeight : scrollHeight + settings.offset,
+ contextHeight : (settings.context == 'body')
+ ? $(window).height()
+ : $dimmable.height(),
+ });
+ module.cache.topOffset = -(module.cache.height / 2);
+ }
+ $module.removeClass(className.loading);
+ module.debug('Caching modal and container sizes', module.cache);
+ },
+
+ can: {
+ leftBodyScrollbar: function(){
+ if(module.cache.leftBodyScrollbar === undefined) {
+ module.cache.leftBodyScrollbar = module.is.rtl() && ((module.is.iframe && !module.is.firefox()) || module.is.safari() || module.is.edge() || module.is.ie());
+ }
+ return module.cache.leftBodyScrollbar;
+ },
+ useFlex: function() {
+ if (settings.useFlex === 'auto') {
+ return settings.detachable && !module.is.ie();
+ }
+ if(settings.useFlex && module.is.ie()) {
+ module.debug('useFlex true is not supported in IE');
+ } else if(settings.useFlex && !settings.detachable) {
+ module.debug('useFlex true in combination with detachable false is not supported');
+ }
+ return settings.useFlex;
+ },
+ fit: function() {
+ var
+ contextHeight = module.cache.contextHeight,
+ verticalCenter = module.cache.contextHeight / 2,
+ topOffset = module.cache.topOffset,
+ scrollHeight = module.cache.scrollHeight,
+ height = module.cache.height,
+ paddingHeight = settings.padding,
+ startPosition = (verticalCenter + topOffset)
+ ;
+ return (scrollHeight > height)
+ ? (startPosition + scrollHeight + paddingHeight < contextHeight)
+ : (height + (paddingHeight * 2) < contextHeight)
+ ;
+ }
+ },
+
+ is: {
+ active: function() {
+ return $module.hasClass(className.active);
+ },
+ ie: function() {
+ if(module.cache.isIE === undefined) {
+ var
+ isIE11 = (!(window.ActiveXObject) && 'ActiveXObject' in window),
+ isIE = ('ActiveXObject' in window)
+ ;
+ module.cache.isIE = (isIE11 || isIE);
+ }
+ return module.cache.isIE;
+ },
+ animating: function() {
+ return $module.transition('is supported')
+ ? $module.transition('is animating')
+ : $module.is(':visible')
+ ;
+ },
+ scrolling: function() {
+ return $dimmable.hasClass(className.scrolling);
+ },
+ modernBrowser: function() {
+ // appName for IE11 reports 'Netscape' can no longer use
+ return !(window.ActiveXObject || 'ActiveXObject' in window);
+ },
+ rtl: function() {
+ if(module.cache.isRTL === undefined) {
+ module.cache.isRTL = $body.attr('dir') === 'rtl' || $body.css('direction') === 'rtl';
+ }
+ return module.cache.isRTL;
+ },
+ safari: function() {
+ if(module.cache.isSafari === undefined) {
+ module.cache.isSafari = /constructor/i.test(window.HTMLElement) || !!window.ApplePaySession;
+ }
+ return module.cache.isSafari;
+ },
+ edge: function(){
+ if(module.cache.isEdge === undefined) {
+ module.cache.isEdge = !!window.setImmediate && !module.is.ie();
+ }
+ return module.cache.isEdge;
+ },
+ firefox: function(){
+ if(module.cache.isFirefox === undefined) {
+ module.cache.isFirefox = !!window.InstallTrigger;
+ }
+ return module.cache.isFirefox;
+ },
+ iframe: function() {
+ return !(self === top);
+ }
+ },
+
+ set: {
+ autofocus: function() {
+ var
+ $inputs = $module.find('[tabindex], :input').filter(':visible').filter(function() {
+ return $(this).closest('.disabled').length === 0;
+ }),
+ $autofocus = $inputs.filter('[autofocus]'),
+ $input = ($autofocus.length > 0)
+ ? $autofocus.first()
+ : $inputs.first()
+ ;
+ if($input.length > 0) {
+ $input.focus();
+ }
+ },
+ bodyMargin: function() {
+ var position = module.can.leftBodyScrollbar() ? 'left':'right';
+ if(settings.detachable || module.can.fit()) {
+ $body.css('margin-'+position, tempBodyMargin + 'px');
+ }
+ $body.find(selector.bodyFixed.replace('right',position)).css('padding-'+position, tempBodyMargin + 'px');
+ },
+ clickaway: function() {
+ if (!settings.detachable) {
+ $module
+ .on('mousedown' + elementEventNamespace, module.event.mousedown)
+ ;
+ }
+ $dimmer
+ .on('mousedown' + elementEventNamespace, module.event.mousedown)
+ ;
+ $dimmer
+ .on('mouseup' + elementEventNamespace, module.event.mouseup)
+ ;
+ },
+ dimmerSettings: function() {
+ if($.fn.dimmer === undefined) {
+ module.error(error.dimmer);
+ return;
+ }
+ var
+ defaultSettings = {
+ debug : settings.debug,
+ dimmerName : 'modals',
+ closable : 'auto',
+ useFlex : module.can.useFlex(),
+ duration : {
+ show : settings.duration,
+ hide : settings.duration
+ }
+ },
+ dimmerSettings = $.extend(true, defaultSettings, settings.dimmerSettings)
+ ;
+ if(settings.inverted) {
+ dimmerSettings.variation = (dimmerSettings.variation !== undefined)
+ ? dimmerSettings.variation + ' inverted'
+ : 'inverted'
+ ;
+ }
+ $context.dimmer('setting', dimmerSettings);
+ },
+ dimmerStyles: function() {
+ if(settings.inverted) {
+ $dimmer.addClass(className.inverted);
+ }
+ else {
+ $dimmer.removeClass(className.inverted);
+ }
+ if(settings.blurring) {
+ $dimmable.addClass(className.blurring);
+ }
+ else {
+ $dimmable.removeClass(className.blurring);
+ }
+ },
+ modalOffset: function() {
+ if (!settings.detachable) {
+ var canFit = module.can.fit();
+ $module
+ .css({
+ top: (!$module.hasClass('aligned') && canFit)
+ ? $(document).scrollTop() + (module.cache.contextHeight - module.cache.height) / 2
+ : !canFit || $module.hasClass('top')
+ ? $(document).scrollTop() + settings.padding
+ : $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding),
+ marginLeft: -(module.cache.width / 2)
+ })
+ ;
+ } else {
+ $module
+ .css({
+ marginTop: (!$module.hasClass('aligned') && module.can.fit())
+ ? -(module.cache.height / 2)
+ : settings.padding / 2,
+ marginLeft: -(module.cache.width / 2)
+ })
+ ;
+ }
+ module.verbose('Setting modal offset for legacy mode');
+ },
+ screenHeight: function() {
+ if( module.can.fit() ) {
+ $body.css('height', '');
+ }
+ else if(!$module.hasClass('bottom')) {
+ module.debug('Modal is taller than page content, resizing page height');
+ $body
+ .css('height', module.cache.height + (settings.padding * 2) )
+ ;
+ }
+ },
+ active: function() {
+ $module.addClass(className.active + ' ' + className.front);
+ $otherModals.filter('.' + className.active).removeClass(className.front);
+ },
+ scrolling: function() {
+ $dimmable.addClass(className.scrolling);
+ $module.addClass(className.scrolling);
+ module.unbind.scrollLock();
+ },
+ legacy: function() {
+ $module.addClass(className.legacy);
+ },
+ type: function() {
+ if(module.can.fit()) {
+ module.verbose('Modal fits on screen');
+ if(!module.others.active() && !module.others.animating()) {
+ module.remove.scrolling();
+ module.bind.scrollLock();
+ }
+ }
+ else if (!$module.hasClass('bottom')){
+ module.verbose('Modal cannot fit on screen setting to scrolling');
+ module.set.scrolling();
+ } else {
+ module.verbose('Bottom aligned modal not fitting on screen is unsupported for scrolling');
+ }
+ },
+ undetached: function() {
+ $dimmable.addClass(className.undetached);
+ }
+ },
+
+ setting: function(name, value) {
+ module.debug('Changing setting', name, value);
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ if($.isPlainObject(settings[name])) {
+ $.extend(true, settings[name], value);
+ }
+ else {
+ settings[name] = value;
+ }
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ 'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ return false;
+ }
+ });
+ }
+ if ( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.initialize();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.initialize();
+ }
+ })
+ ;
+
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : this
+ ;
+};
+
+$.fn.modal.settings = {
+
+ name : 'Modal',
+ namespace : 'modal',
+
+ useFlex : 'auto',
+ offset : 0,
+
+ silent : false,
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ observeChanges : false,
+
+ allowMultiple : false,
+ detachable : true,
+ closable : true,
+ autofocus : true,
+ restoreFocus : true,
+
+ inverted : false,
+ blurring : false,
+
+ centered : true,
+
+ dimmerSettings : {
+ closable : false,
+ useCSS : true
+ },
+
+ // whether to use keyboard shortcuts
+ keyboardShortcuts: true,
+
+ context : 'body',
+
+ queue : false,
+ duration : 500,
+ transition : 'scale',
+
+ // padding with edge of page
+ padding : 50,
+ scrollbarWidth: 10,
+
+ // called before show animation
+ onShow : function(){},
+
+ // called after show animation
+ onVisible : function(){},
+
+ // called before hide animation
+ onHide : function(){ return true; },
+
+ // called after hide animation
+ onHidden : function(){},
+
+ // called after approve selector match
+ onApprove : function(){ return true; },
+
+ // called after deny selector match
+ onDeny : function(){ return true; },
+
+ selector : {
+ close : '> .close',
+ approve : '.actions .positive, .actions .approve, .actions .ok',
+ deny : '.actions .negative, .actions .deny, .actions .cancel',
+ modal : '.ui.modal',
+ dimmer : '> .ui.dimmer',
+ bodyFixed: '> .ui.fixed.menu, > .ui.right.toast-container, > .ui.right.sidebar'
+ },
+ error : {
+ dimmer : 'UI Dimmer, a required component is not included in this page',
+ method : 'The method you called is not defined.',
+ notFound : 'The element you specified could not be found'
+ },
+ className : {
+ active : 'active',
+ animating : 'animating',
+ blurring : 'blurring',
+ inverted : 'inverted',
+ legacy : 'legacy',
+ loading : 'loading',
+ scrolling : 'scrolling',
+ undetached : 'undetached',
+ front : 'front'
+ }
+};
+
+
+})( jQuery, window, document );
+
+/*!
+ * # Fomantic-UI - Search
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.search = function(parameters) {
+ var
+ $allModules = $(this),
+ moduleSelector = $allModules.selector || '',
+
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+ returnedValue
+ ;
+ $(this)
+ .each(function() {
+ var
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.search.settings, parameters)
+ : $.extend({}, $.fn.search.settings),
+
+ className = settings.className,
+ metadata = settings.metadata,
+ regExp = settings.regExp,
+ fields = settings.fields,
+ selector = settings.selector,
+ error = settings.error,
+ namespace = settings.namespace,
+
+ eventNamespace = '.' + namespace,
+ moduleNamespace = namespace + '-module',
+
+ $module = $(this),
+ $prompt = $module.find(selector.prompt),
+ $searchButton = $module.find(selector.searchButton),
+ $results = $module.find(selector.results),
+ $result = $module.find(selector.result),
+ $category = $module.find(selector.category),
+
+ element = this,
+ instance = $module.data(moduleNamespace),
+
+ disabledBubbled = false,
+ resultsDismissed = false,
+
+ module
+ ;
+
+ module = {
+
+ initialize: function() {
+ module.verbose('Initializing module');
+ module.get.settings();
+ module.determine.searchFields();
+ module.bind.events();
+ module.set.type();
+ module.create.results();
+ module.instantiate();
+ },
+ instantiate: function() {
+ module.verbose('Storing instance of module', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, module)
+ ;
+ },
+ destroy: function() {
+ module.verbose('Destroying instance');
+ $module
+ .off(eventNamespace)
+ .removeData(moduleNamespace)
+ ;
+ },
+
+ refresh: function() {
+ module.debug('Refreshing selector cache');
+ $prompt = $module.find(selector.prompt);
+ $searchButton = $module.find(selector.searchButton);
+ $category = $module.find(selector.category);
+ $results = $module.find(selector.results);
+ $result = $module.find(selector.result);
+ },
+
+ refreshResults: function() {
+ $results = $module.find(selector.results);
+ $result = $module.find(selector.result);
+ },
+
+ bind: {
+ events: function() {
+ module.verbose('Binding events to search');
+ if(settings.automatic) {
+ $module
+ .on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input)
+ ;
+ $prompt
+ .attr('autocomplete', 'off')
+ ;
+ }
+ $module
+ // prompt
+ .on('focus' + eventNamespace, selector.prompt, module.event.focus)
+ .on('blur' + eventNamespace, selector.prompt, module.event.blur)
+ .on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard)
+ // search button
+ .on('click' + eventNamespace, selector.searchButton, module.query)
+ // results
+ .on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown)
+ .on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup)
+ .on('click' + eventNamespace, selector.result, module.event.result.click)
+ ;
+ }
+ },
+
+ determine: {
+ searchFields: function() {
+ // this makes sure $.extend does not add specified search fields to default fields
+ // this is the only setting which should not extend defaults
+ if(parameters && parameters.searchFields !== undefined) {
+ settings.searchFields = parameters.searchFields;
+ }
+ }
+ },
+
+ event: {
+ input: function() {
+ if(settings.searchDelay) {
+ clearTimeout(module.timer);
+ module.timer = setTimeout(function() {
+ if(module.is.focused()) {
+ module.query();
+ }
+ }, settings.searchDelay);
+ }
+ else {
+ module.query();
+ }
+ },
+ focus: function() {
+ module.set.focus();
+ if(settings.searchOnFocus && module.has.minimumCharacters() ) {
+ module.query(function() {
+ if(module.can.show() ) {
+ module.showResults();
+ }
+ });
+ }
+ },
+ blur: function(event) {
+ var
+ pageLostFocus = (document.activeElement === this),
+ callback = function() {
+ module.cancel.query();
+ module.remove.focus();
+ module.timer = setTimeout(module.hideResults, settings.hideDelay);
+ }
+ ;
+ if(pageLostFocus) {
+ return;
+ }
+ resultsDismissed = false;
+ if(module.resultsClicked) {
+ module.debug('Determining if user action caused search to close');
+ $module
+ .one('click.close' + eventNamespace, selector.results, function(event) {
+ if(module.is.inMessage(event) || disabledBubbled) {
+ $prompt.focus();
+ return;
+ }
+ disabledBubbled = false;
+ if( !module.is.animating() && !module.is.hidden()) {
+ callback();
+ }
+ })
+ ;
+ }
+ else {
+ module.debug('Input blurred without user action, closing results');
+ callback();
+ }
+ },
+ result: {
+ mousedown: function() {
+ module.resultsClicked = true;
+ },
+ mouseup: function() {
+ module.resultsClicked = false;
+ },
+ click: function(event) {
+ module.debug('Search result selected');
+ var
+ $result = $(this),
+ $title = $result.find(selector.title).eq(0),
+ $link = $result.is('a[href]')
+ ? $result
+ : $result.find('a[href]').eq(0),
+ href = $link.attr('href') || false,
+ target = $link.attr('target') || false,
+ // title is used for result lookup
+ value = ($title.length > 0)
+ ? $title.text()
+ : false,
+ results = module.get.results(),
+ result = $result.data(metadata.result) || module.get.result(value, results)
+ ;
+ if(value) {
+ module.set.value(value);
+ }
+ if( $.isFunction(settings.onSelect) ) {
+ if(settings.onSelect.call(element, result, results) === false) {
+ module.debug('Custom onSelect callback cancelled default select action');
+ disabledBubbled = true;
+ return;
+ }
+ }
+ module.hideResults();
+ if(href) {
+ event.preventDefault();
+ module.verbose('Opening search link found in result', $link);
+ if(target == '_blank' || event.ctrlKey) {
+ window.open(href);
+ }
+ else {
+ window.location.href = (href);
+ }
+ }
+ }
+ }
+ },
+ ensureVisible: function ensureVisible($el) {
+ var elTop, elBottom, resultsScrollTop, resultsHeight;
+
+ elTop = $el.position().top;
+ elBottom = elTop + $el.outerHeight(true);
+
+ resultsScrollTop = $results.scrollTop();
+ resultsHeight = $results.height()
+ parseInt($results.css('paddingTop'), 0) +
+ parseInt($results.css('paddingBottom'), 0);
+
+ if (elTop < 0) {
+ $results.scrollTop(resultsScrollTop + elTop);
+ }
+
+ else if (resultsHeight < elBottom) {
+ $results.scrollTop(resultsScrollTop + (elBottom - resultsHeight));
+ }
+ },
+ handleKeyboard: function(event) {
+ var
+ // force selector refresh
+ $result = $module.find(selector.result),
+ $category = $module.find(selector.category),
+ $activeResult = $result.filter('.' + className.active),
+ currentIndex = $result.index( $activeResult ),
+ resultSize = $result.length,
+ hasActiveResult = $activeResult.length > 0,
+
+ keyCode = event.which,
+ keys = {
+ backspace : 8,
+ enter : 13,
+ escape : 27,
+ upArrow : 38,
+ downArrow : 40
+ },
+ newIndex
+ ;
+ // search shortcuts
+ if(keyCode == keys.escape) {
+ module.verbose('Escape key pressed, blurring search field');
+ module.hideResults();
+ resultsDismissed = true;
+ }
+ if( module.is.visible() ) {
+ if(keyCode == keys.enter) {
+ module.verbose('Enter key pressed, selecting active result');
+ if( $result.filter('.' + className.active).length > 0 ) {
+ module.event.result.click.call($result.filter('.' + className.active), event);
+ event.preventDefault();
+ return false;
+ }
+ }
+ else if(keyCode == keys.upArrow && hasActiveResult) {
+ module.verbose('Up key pressed, changing active result');
+ newIndex = (currentIndex - 1 < 0)
+ ? currentIndex
+ : currentIndex - 1
+ ;
+ $category
+ .removeClass(className.active)
+ ;
+ $result
+ .removeClass(className.active)
+ .eq(newIndex)
+ .addClass(className.active)
+ .closest($category)
+ .addClass(className.active)
+ ;
+ module.ensureVisible($result.eq(newIndex));
+ event.preventDefault();
+ }
+ else if(keyCode == keys.downArrow) {
+ module.verbose('Down key pressed, changing active result');
+ newIndex = (currentIndex + 1 >= resultSize)
+ ? currentIndex
+ : currentIndex + 1
+ ;
+ $category
+ .removeClass(className.active)
+ ;
+ $result
+ .removeClass(className.active)
+ .eq(newIndex)
+ .addClass(className.active)
+ .closest($category)
+ .addClass(className.active)
+ ;
+ module.ensureVisible($result.eq(newIndex));
+ event.preventDefault();
+ }
+ }
+ else {
+ // query shortcuts
+ if(keyCode == keys.enter) {
+ module.verbose('Enter key pressed, executing query');
+ module.query();
+ module.set.buttonPressed();
+ $prompt.one('keyup', module.remove.buttonFocus);
+ }
+ }
+ },
+
+ setup: {
+ api: function(searchTerm, callback) {
+ var
+ apiSettings = {
+ debug : settings.debug,
+ on : false,
+ cache : settings.cache,
+ action : 'search',
+ urlData : {
+ query : searchTerm
+ },
+ onSuccess : function(response) {
+ module.parse.response.call(element, response, searchTerm);
+ callback();
+ },
+ onFailure : function() {
+ module.displayMessage(error.serverError);
+ callback();
+ },
+ onAbort : function(response) {
+ },
+ onError : module.error
+ }
+ ;
+ $.extend(true, apiSettings, settings.apiSettings);
+ module.verbose('Setting up API request', apiSettings);
+ $module.api(apiSettings);
+ }
+ },
+
+ can: {
+ useAPI: function() {
+ return $.fn.api !== undefined;
+ },
+ show: function() {
+ return module.is.focused() && !module.is.visible() && !module.is.empty();
+ },
+ transition: function() {
+ return settings.transition && $.fn.transition !== undefined && $module.transition('is supported');
+ }
+ },
+
+ is: {
+ animating: function() {
+ return $results.hasClass(className.animating);
+ },
+ hidden: function() {
+ return $results.hasClass(className.hidden);
+ },
+ inMessage: function(event) {
+ if(!event.target) {
+ return;
+ }
+ var
+ $target = $(event.target),
+ isInDOM = $.contains(document.documentElement, event.target)
+ ;
+ return (isInDOM && $target.closest(selector.message).length > 0);
+ },
+ empty: function() {
+ return ($results.html() === '');
+ },
+ visible: function() {
+ return ($results.filter(':visible').length > 0);
+ },
+ focused: function() {
+ return ($prompt.filter(':focus').length > 0);
+ }
+ },
+
+ get: {
+ settings: function() {
+ if($.isPlainObject(parameters) && parameters.searchFullText) {
+ settings.fullTextSearch = parameters.searchFullText;
+ module.error(settings.error.oldSearchSyntax, element);
+ }
+ if (settings.ignoreDiacritics && !String.prototype.normalize) {
+ settings.ignoreDiacritics = false;
+ module.error(error.noNormalize, element);
+ }
+ },
+ inputEvent: function() {
+ var
+ prompt = $prompt[0],
+ inputEvent = (prompt !== undefined && prompt.oninput !== undefined)
+ ? 'input'
+ : (prompt !== undefined && prompt.onpropertychange !== undefined)
+ ? 'propertychange'
+ : 'keyup'
+ ;
+ return inputEvent;
+ },
+ value: function() {
+ return $prompt.val();
+ },
+ results: function() {
+ var
+ results = $module.data(metadata.results)
+ ;
+ return results;
+ },
+ result: function(value, results) {
+ var
+ result = false
+ ;
+ value = (value !== undefined)
+ ? value
+ : module.get.value()
+ ;
+ results = (results !== undefined)
+ ? results
+ : module.get.results()
+ ;
+ if(settings.type === 'category') {
+ module.debug('Finding result that matches', value);
+ $.each(results, function(index, category) {
+ if(Array.isArray(category.results)) {
+ result = module.search.object(value, category.results)[0];
+ // don't continue searching if a result is found
+ if(result) {
+ return false;
+ }
+ }
+ });
+ }
+ else {
+ module.debug('Finding result in results object', value);
+ result = module.search.object(value, results)[0];
+ }
+ return result || false;
+ },
+ },
+
+ select: {
+ firstResult: function() {
+ module.verbose('Selecting first result');
+ $result.first().addClass(className.active);
+ }
+ },
+
+ set: {
+ focus: function() {
+ $module.addClass(className.focus);
+ },
+ loading: function() {
+ $module.addClass(className.loading);
+ },
+ value: function(value) {
+ module.verbose('Setting search input value', value);
+ $prompt
+ .val(value)
+ ;
+ },
+ type: function(type) {
+ type = type || settings.type;
+ if(settings.type == 'category') {
+ $module.addClass(settings.type);
+ }
+ },
+ buttonPressed: function() {
+ $searchButton.addClass(className.pressed);
+ }
+ },
+
+ remove: {
+ loading: function() {
+ $module.removeClass(className.loading);
+ },
+ focus: function() {
+ $module.removeClass(className.focus);
+ },
+ buttonPressed: function() {
+ $searchButton.removeClass(className.pressed);
+ },
+ diacritics: function(text) {
+ return settings.ignoreDiacritics ? text.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : text;
+ }
+ },
+
+ query: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ var
+ searchTerm = module.get.value(),
+ cache = module.read.cache(searchTerm)
+ ;
+ callback = callback || function() {};
+ if( module.has.minimumCharacters() ) {
+ if(cache) {
+ module.debug('Reading result from cache', searchTerm);
+ module.save.results(cache.results);
+ module.addResults(cache.html);
+ module.inject.id(cache.results);
+ callback();
+ }
+ else {
+ module.debug('Querying for', searchTerm);
+ if($.isPlainObject(settings.source) || Array.isArray(settings.source)) {
+ module.search.local(searchTerm);
+ callback();
+ }
+ else if( module.can.useAPI() ) {
+ module.search.remote(searchTerm, callback);
+ }
+ else {
+ module.error(error.source);
+ callback();
+ }
+ }
+ settings.onSearchQuery.call(element, searchTerm);
+ }
+ else {
+ module.hideResults();
+ }
+ },
+
+ search: {
+ local: function(searchTerm) {
+ var
+ results = module.search.object(searchTerm, settings.source),
+ searchHTML
+ ;
+ module.set.loading();
+ module.save.results(results);
+ module.debug('Returned full local search results', results);
+ if(settings.maxResults > 0) {
+ module.debug('Using specified max results', results);
+ results = results.slice(0, settings.maxResults);
+ }
+ if(settings.type == 'category') {
+ results = module.create.categoryResults(results);
+ }
+ searchHTML = module.generateResults({
+ results: results
+ });
+ module.remove.loading();
+ module.addResults(searchHTML);
+ module.inject.id(results);
+ module.write.cache(searchTerm, {
+ html : searchHTML,
+ results : results
+ });
+ },
+ remote: function(searchTerm, callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if($module.api('is loading')) {
+ $module.api('abort');
+ }
+ module.setup.api(searchTerm, callback);
+ $module
+ .api('query')
+ ;
+ },
+ object: function(searchTerm, source, searchFields) {
+ searchTerm = module.remove.diacritics(String(searchTerm));
+ var
+ results = [],
+ exactResults = [],
+ fuzzyResults = [],
+ searchExp = searchTerm.replace(regExp.escape, '\\$&'),
+ matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'),
+
+ // avoid duplicates when pushing results
+ addResult = function(array, result) {
+ var
+ notResult = ($.inArray(result, results) == -1),
+ notFuzzyResult = ($.inArray(result, fuzzyResults) == -1),
+ notExactResults = ($.inArray(result, exactResults) == -1)
+ ;
+ if(notResult && notFuzzyResult && notExactResults) {
+ array.push(result);
+ }
+ }
+ ;
+ source = source || settings.source;
+ searchFields = (searchFields !== undefined)
+ ? searchFields
+ : settings.searchFields
+ ;
+
+ // search fields should be array to loop correctly
+ if(!Array.isArray(searchFields)) {
+ searchFields = [searchFields];
+ }
+
+ // exit conditions if no source
+ if(source === undefined || source === false) {
+ module.error(error.source);
+ return [];
+ }
+ // iterate through search fields looking for matches
+ $.each(searchFields, function(index, field) {
+ $.each(source, function(label, content) {
+ var
+ fieldExists = (typeof content[field] == 'string') || (typeof content[field] == 'number')
+ ;
+ if(fieldExists) {
+ var text;
+ if (typeof content[field] === 'string'){
+ text = module.remove.diacritics(content[field]);
+ } else {
+ text = content[field].toString();
+ }
+ if( text.search(matchRegExp) !== -1) {
+ // content starts with value (first in results)
+ addResult(results, content);
+ }
+ else if(settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text) ) {
+ // content fuzzy matches (last in results)
+ addResult(exactResults, content);
+ }
+ else if(settings.fullTextSearch == true && module.fuzzySearch(searchTerm, text) ) {
+ // content fuzzy matches (last in results)
+ addResult(fuzzyResults, content);
+ }
+ }
+ });
+ });
+ $.merge(exactResults, fuzzyResults);
+ $.merge(results, exactResults);
+ return results;
+ }
+ },
+ exactSearch: function (query, term) {
+ query = query.toLowerCase();
+ term = term.toLowerCase();
+ return term.indexOf(query) > -1;
+ },
+ fuzzySearch: function(query, term) {
+ var
+ termLength = term.length,
+ queryLength = query.length
+ ;
+ if(typeof query !== 'string') {
+ return false;
+ }
+ query = query.toLowerCase();
+ term = term.toLowerCase();
+ if(queryLength > termLength) {
+ return false;
+ }
+ if(queryLength === termLength) {
+ return (query === term);
+ }
+ search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
+ var
+ queryCharacter = query.charCodeAt(characterIndex)
+ ;
+ while(nextCharacterIndex < termLength) {
+ if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
+ continue search;
+ }
+ }
+ return false;
+ }
+ return true;
+ },
+
+ parse: {
+ response: function(response, searchTerm) {
+ if(Array.isArray(response)){
+ var o={};
+ o[fields.results]=response;
+ response = o;
+ }
+ var
+ searchHTML = module.generateResults(response)
+ ;
+ module.verbose('Parsing server response', response);
+ if(response !== undefined) {
+ if(searchTerm !== undefined && response[fields.results] !== undefined) {
+ module.addResults(searchHTML);
+ module.inject.id(response[fields.results]);
+ module.write.cache(searchTerm, {
+ html : searchHTML,
+ results : response[fields.results]
+ });
+ module.save.results(response[fields.results]);
+ }
+ }
+ }
+ },
+
+ cancel: {
+ query: function() {
+ if( module.can.useAPI() ) {
+ $module.api('abort');
+ }
+ }
+ },
+
+ has: {
+ minimumCharacters: function() {
+ var
+ searchTerm = module.get.value(),
+ numCharacters = searchTerm.length
+ ;
+ return (numCharacters >= settings.minCharacters);
+ },
+ results: function() {
+ if($results.length === 0) {
+ return false;
+ }
+ var
+ html = $results.html()
+ ;
+ return html != '';
+ }
+ },
+
+ clear: {
+ cache: function(value) {
+ var
+ cache = $module.data(metadata.cache)
+ ;
+ if(!value) {
+ module.debug('Clearing cache', value);
+ $module.removeData(metadata.cache);
+ }
+ else if(value && cache && cache[value]) {
+ module.debug('Removing value from cache', value);
+ delete cache[value];
+ $module.data(metadata.cache, cache);
+ }
+ }
+ },
+
+ read: {
+ cache: function(name) {
+ var
+ cache = $module.data(metadata.cache)
+ ;
+ if(settings.cache) {
+ module.verbose('Checking cache for generated html for query', name);
+ return (typeof cache == 'object') && (cache[name] !== undefined)
+ ? cache[name]
+ : false
+ ;
+ }
+ return false;
+ }
+ },
+
+ create: {
+ categoryResults: function(results) {
+ var
+ categoryResults = {}
+ ;
+ $.each(results, function(index, result) {
+ if(!result.category) {
+ return;
+ }
+ if(categoryResults[result.category] === undefined) {
+ module.verbose('Creating new category of results', result.category);
+ categoryResults[result.category] = {
+ name : result.category,
+ results : [result]
+ };
+ }
+ else {
+ categoryResults[result.category].results.push(result);
+ }
+ });
+ return categoryResults;
+ },
+ id: function(resultIndex, categoryIndex) {
+ var
+ resultID = (resultIndex + 1), // not zero indexed
+ letterID,
+ id
+ ;
+ if(categoryIndex !== undefined) {
+ // start char code for "A"
+ letterID = String.fromCharCode(97 + categoryIndex);
+ id = letterID + resultID;
+ module.verbose('Creating category result id', id);
+ }
+ else {
+ id = resultID;
+ module.verbose('Creating result id', id);
+ }
+ return id;
+ },
+ results: function() {
+ if($results.length === 0) {
+ $results = $('<div />')
+ .addClass(className.results)
+ .appendTo($module)
+ ;
+ }
+ }
+ },
+
+ inject: {
+ result: function(result, resultIndex, categoryIndex) {
+ module.verbose('Injecting result into results');
+ var
+ $selectedResult = (categoryIndex !== undefined)
+ ? $results
+ .children().eq(categoryIndex)
+ .children(selector.results)
+ .first()
+ .children(selector.result)
+ .eq(resultIndex)
+ : $results
+ .children(selector.result).eq(resultIndex)
+ ;
+ module.verbose('Injecting results metadata', $selectedResult);
+ $selectedResult
+ .data(metadata.result, result)
+ ;
+ },
+ id: function(results) {
+ module.debug('Injecting unique ids into results');
+ var
+ // since results may be object, we must use counters
+ categoryIndex = 0,
+ resultIndex = 0
+ ;
+ if(settings.type === 'category') {
+ // iterate through each category result
+ $.each(results, function(index, category) {
+ if(category.results.length > 0){
+ resultIndex = 0;
+ $.each(category.results, function(index, result) {
+ if(result.id === undefined) {
+ result.id = module.create.id(resultIndex, categoryIndex);
+ }
+ module.inject.result(result, resultIndex, categoryIndex);
+ resultIndex++;
+ });
+ categoryIndex++;
+ }
+ });
+ }
+ else {
+ // top level
+ $.each(results, function(index, result) {
+ if(result.id === undefined) {
+ result.id = module.create.id(resultIndex);
+ }
+ module.inject.result(result, resultIndex);
+ resultIndex++;
+ });
+ }
+ return results;
+ }
+ },
+
+ save: {
+ results: function(results) {
+ module.verbose('Saving current search results to metadata', results);
+ $module.data(metadata.results, results);
+ }
+ },
+
+ write: {
+ cache: function(name, value) {
+ var
+ cache = ($module.data(metadata.cache) !== undefined)
+ ? $module.data(metadata.cache)
+ : {}
+ ;
+ if(settings.cache) {
+ module.verbose('Writing generated html to cache', name, value);
+ cache[name] = value;
+ $module
+ .data(metadata.cache, cache)
+ ;
+ }
+ }
+ },
+
+ addResults: function(html) {
+ if( $.isFunction(settings.onResultsAdd) ) {
+ if( settings.onResultsAdd.call($results, html) === false ) {
+ module.debug('onResultsAdd callback cancelled default action');
+ return false;
+ }
+ }
+ if(html) {
+ $results
+ .html(html)
+ ;
+ module.refreshResults();
+ if(settings.selectFirstResult) {
+ module.select.firstResult();
+ }
+ module.showResults();
+ }
+ else {
+ module.hideResults(function() {
+ $results.empty();
+ });
+ }
+ },
+
+ showResults: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(resultsDismissed) {
+ return;
+ }
+ if(!module.is.visible() && module.has.results()) {
+ if( module.can.transition() ) {
+ module.debug('Showing results with css animations');
+ $results
+ .transition({
+ animation : settings.transition + ' in',
+ debug : settings.debug,
+ verbose : settings.verbose,
+ duration : settings.duration,
+ onShow : function() {
+ var $firstResult = $module.find(selector.result).eq(0);
+ if($firstResult.length > 0) {
+ module.ensureVisible($firstResult);
+ }
+ },
+ onComplete : function() {
+ callback();
+ },
+ queue : true
+ })
+ ;
+ }
+ else {
+ module.debug('Showing results with javascript');
+ $results
+ .stop()
+ .fadeIn(settings.duration, settings.easing)
+ ;
+ }
+ settings.onResultsOpen.call($results);
+ }
+ },
+ hideResults: function(callback) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( module.is.visible() ) {
+ if( module.can.transition() ) {
+ module.debug('Hiding results with css animations');
+ $results
+ .transition({
+ animation : settings.transition + ' out',
+ debug : settings.debug,
+ verbose : settings.verbose,
+ duration : settings.duration,
+ onComplete : function() {
+ callback();
+ },
+ queue : true
+ })
+ ;
+ }
+ else {
+ module.debug('Hiding results with javascript');
+ $results
+ .stop()
+ .fadeOut(settings.duration, settings.easing)
+ ;
+ }
+ settings.onResultsClose.call($results);
+ }
+ },
+
+ generateResults: function(response) {
+ module.debug('Generating html from response', response);
+ var
+ template = settings.templates[settings.type],
+ isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])),
+ isProperArray = (Array.isArray(response[fields.results]) && response[fields.results].length > 0),
+ html = ''
+ ;
+ if(isProperObject || isProperArray ) {
+ if(settings.maxResults > 0) {
+ if(isProperObject) {
+ if(settings.type == 'standard') {
+ module.error(error.maxResults);
+ }
+ }
+ else {
+ response[fields.results] = response[fields.results].slice(0, settings.maxResults);
+ }
+ }
+ if($.isFunction(template)) {
+ html = template(response, fields, settings.preserveHTML);
+ }
+ else {
+ module.error(error.noTemplate, false);
+ }
+ }
+ else if(settings.showNoResults) {
+ html = module.displayMessage(error.noResults, 'empty', error.noResultsHeader);
+ }
+ settings.onResults.call(element, response);
+ return html;
+ },
+
+ displayMessage: function(text, type, header) {
+ type = type || 'standard';
+ module.debug('Displaying message', text, type, header);
+ module.addResults( settings.templates.message(text, type, header) );
+ return settings.templates.message(text, type, header);
+ },
+
+ setting: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ settings[name] = value;
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ 'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if($allModules.length > 1) {
+ title += ' ' + '(' + $allModules.length + ')';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ return false;
+ }
+ });
+ }
+ if( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.initialize();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.initialize();
+ }
+
+ })
+ ;
+
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : this
+ ;
+};
+
+$.fn.search.settings = {
+
+ name : 'Search',
+ namespace : 'search',
+
+ silent : false,
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ // template to use (specified in settings.templates)
+ type : 'standard',
+
+ // minimum characters required to search
+ minCharacters : 1,
+
+ // whether to select first result after searching automatically
+ selectFirstResult : false,
+
+ // API config
+ apiSettings : false,
+
+ // object to search
+ source : false,
+
+ // Whether search should query current term on focus
+ searchOnFocus : true,
+
+ // fields to search
+ searchFields : [
+ 'id',
+ 'title',
+ 'description'
+ ],
+
+ // field to display in standard results template
+ displayField : '',
+
+ // search anywhere in value (set to 'exact' to require exact matches
+ fullTextSearch : 'exact',
+
+ // match results also if they contain diacritics of the same base character (for example searching for "a" will also match "á" or "â" or "à", etc...)
+ ignoreDiacritics : false,
+
+ // whether to add events to prompt automatically
+ automatic : true,
+
+ // delay before hiding menu after blur
+ hideDelay : 0,
+
+ // delay before searching
+ searchDelay : 200,
+
+ // maximum results returned from search
+ maxResults : 7,
+
+ // whether to store lookups in local cache
+ cache : true,
+
+ // whether no results errors should be shown
+ showNoResults : true,
+
+ // preserve possible html of resultset values
+ preserveHTML : true,
+
+ // transition settings
+ transition : 'scale',
+ duration : 200,
+ easing : 'easeOutExpo',
+
+ // callbacks
+ onSelect : false,
+ onResultsAdd : false,
+
+ onSearchQuery : function(query){},
+ onResults : function(response){},
+
+ onResultsOpen : function(){},
+ onResultsClose : function(){},
+
+ className: {
+ animating : 'animating',
+ active : 'active',
+ empty : 'empty',
+ focus : 'focus',
+ hidden : 'hidden',
+ loading : 'loading',
+ results : 'results',
+ pressed : 'down'
+ },
+
+ error : {
+ source : 'Cannot search. No source used, and Semantic API module was not included',
+ noResultsHeader : 'No Results',
+ noResults : 'Your search returned no results',
+ logging : 'Error in debug logging, exiting.',
+ noEndpoint : 'No search endpoint was specified',
+ noTemplate : 'A valid template name was not specified.',
+ oldSearchSyntax : 'searchFullText setting has been renamed fullTextSearch for consistency, please adjust your settings.',
+ serverError : 'There was an issue querying the server.',
+ maxResults : 'Results must be an array to use maxResults setting',
+ method : 'The method you called is not defined.',
+ noNormalize : '"ignoreDiacritics" setting will be ignored. Browser does not support String().normalize(). You may consider including <https://cdn.jsdelivr.net/npm/unorm@1.4.1/lib/unorm.min.js> as a polyfill.'
+ },
+
+ metadata: {
+ cache : 'cache',
+ results : 'results',
+ result : 'result'
+ },
+
+ regExp: {
+ escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
+ beginsWith : '(?:\s|^)'
+ },
+
+ // maps api response attributes to internal representation
+ fields: {
+ categories : 'results', // array of categories (category view)
+ categoryName : 'name', // name of category (category view)
+ categoryResults : 'results', // array of results (category view)
+ description : 'description', // result description
+ image : 'image', // result image
+ price : 'price', // result price
+ results : 'results', // array of results (standard)
+ title : 'title', // result title
+ url : 'url', // result url
+ action : 'action', // "view more" object name
+ actionText : 'text', // "view more" text
+ actionURL : 'url' // "view more" url
+ },
+
+ selector : {
+ prompt : '.prompt',
+ searchButton : '.search.button',
+ results : '.results',
+ message : '.results > .message',
+ category : '.category',
+ result : '.result',
+ title : '.title, .name'
+ },
+
+ templates: {
+ escape: function(string, preserveHTML) {
+ if (preserveHTML){
+ return string;
+ }
+ var
+ badChars = /[<>"'`]/g,
+ shouldEscape = /[&<>"'`]/,
+ escape = {
+ "<": "&lt;",
+ ">": "&gt;",
+ '"': "&quot;",
+ "'": "&#x27;",
+ "`": "&#x60;"
+ },
+ escapedChar = function(chr) {
+ return escape[chr];
+ }
+ ;
+ if(shouldEscape.test(string)) {
+ string = string.replace(/&(?![a-z0-9#]{1,6};)/, "&amp;");
+ return string.replace(badChars, escapedChar);
+ }
+ return string;
+ },
+ message: function(message, type, header) {
+ var
+ html = ''
+ ;
+ if(message !== undefined && type !== undefined) {
+ html += ''
+ + '<div class="message ' + type + '">'
+ ;
+ if(header) {
+ html += ''
+ + '<div class="header">' + header + '</div>'
+ ;
+ }
+ html += ' <div class="description">' + message + '</div>';
+ html += '</div>';
+ }
+ return html;
+ },
+ category: function(response, fields, preserveHTML) {
+ var
+ html = '',
+ escape = $.fn.search.settings.templates.escape
+ ;
+ if(response[fields.categoryResults] !== undefined) {
+
+ // each category
+ $.each(response[fields.categoryResults], function(index, category) {
+ if(category[fields.results] !== undefined && category.results.length > 0) {
+
+ html += '<div class="category">';
+
+ if(category[fields.categoryName] !== undefined) {
+ html += '<div class="name">' + escape(category[fields.categoryName], preserveHTML) + '</div>';
+ }
+
+ // each item inside category
+ html += '<div class="results">';
+ $.each(category.results, function(index, result) {
+ if(result[fields.url]) {
+ html += '<a class="result" href="' + result[fields.url].replace(/"/g,"") + '">';
+ }
+ else {
+ html += '<a class="result">';
+ }
+ if(result[fields.image] !== undefined) {
+ html += ''
+ + '<div class="image">'
+ + ' <img src="' + result[fields.image].replace(/"/g,"") + '">'
+ + '</div>'
+ ;
+ }
+ html += '<div class="content">';
+ if(result[fields.price] !== undefined) {
+ html += '<div class="price">' + escape(result[fields.price], preserveHTML) + '</div>';
+ }
+ if(result[fields.title] !== undefined) {
+ html += '<div class="title">' + escape(result[fields.title], preserveHTML) + '</div>';
+ }
+ if(result[fields.description] !== undefined) {
+ html += '<div class="description">' + escape(result[fields.description], preserveHTML) + '</div>';
+ }
+ html += ''
+ + '</div>'
+ ;
+ html += '</a>';
+ });
+ html += '</div>';
+ html += ''
+ + '</div>'
+ ;
+ }
+ });
+ if(response[fields.action]) {
+ if(fields.actionURL === false) {
+ html += ''
+ + '<div class="action">'
+ + escape(response[fields.action][fields.actionText], preserveHTML)
+ + '</div>';
+ } else {
+ html += ''
+ + '<a href="' + response[fields.action][fields.actionURL].replace(/"/g,"") + '" class="action">'
+ + escape(response[fields.action][fields.actionText], preserveHTML)
+ + '</a>';
+ }
+ }
+ return html;
+ }
+ return false;
+ },
+ standard: function(response, fields, preserveHTML) {
+ var
+ html = '',
+ escape = $.fn.search.settings.templates.escape
+ ;
+ if(response[fields.results] !== undefined) {
+
+ // each result
+ $.each(response[fields.results], function(index, result) {
+ if(result[fields.url]) {
+ html += '<a class="result" href="' + result[fields.url].replace(/"/g,"") + '">';
+ }
+ else {
+ html += '<a class="result">';
+ }
+ if(result[fields.image] !== undefined) {
+ html += ''
+ + '<div class="image">'
+ + ' <img src="' + result[fields.image].replace(/"/g,"") + '">'
+ + '</div>'
+ ;
+ }
+ html += '<div class="content">';
+ if(result[fields.price] !== undefined) {
+ html += '<div class="price">' + escape(result[fields.price], preserveHTML) + '</div>';
+ }
+ if(result[fields.title] !== undefined) {
+ html += '<div class="title">' + escape(result[fields.title], preserveHTML) + '</div>';
+ }
+ if(result[fields.description] !== undefined) {
+ html += '<div class="description">' + escape(result[fields.description], preserveHTML) + '</div>';
+ }
+ html += ''
+ + '</div>'
+ ;
+ html += '</a>';
+ });
+ if(response[fields.action]) {
+ if(fields.actionURL === false) {
+ html += ''
+ + '<div class="action">'
+ + escape(response[fields.action][fields.actionText], preserveHTML)
+ + '</div>';
+ } else {
+ html += ''
+ + '<a href="' + response[fields.action][fields.actionURL].replace(/"/g,"") + '" class="action">'
+ + escape(response[fields.action][fields.actionText], preserveHTML)
+ + '</a>';
+ }
+ }
+ return html;
+ }
+ return false;
+ }
+ }
+};
+
+})( jQuery, window, document );
+
+/*!
+ * # Fomantic-UI - Tab
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isWindow = $.isWindow || function(obj) {
+ return obj != null && obj === obj.window;
+};
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.tab = function(parameters) {
+
+ var
+ // use window context if none specified
+ $allModules = $.isFunction(this)
+ ? $(window)
+ : $(this),
+
+ moduleSelector = $allModules.selector || '',
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+
+ initializedHistory = false,
+ returnedValue
+ ;
+
+ $allModules
+ .each(function() {
+ var
+
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.tab.settings, parameters)
+ : $.extend({}, $.fn.tab.settings),
+
+ className = settings.className,
+ metadata = settings.metadata,
+ selector = settings.selector,
+ error = settings.error,
+ regExp = settings.regExp,
+
+ eventNamespace = '.' + settings.namespace,
+ moduleNamespace = 'module-' + settings.namespace,
+
+ $module = $(this),
+ $context,
+ $tabs,
+
+ cache = {},
+ firstLoad = true,
+ recursionDepth = 0,
+ element = this,
+ instance = $module.data(moduleNamespace),
+
+ activeTabPath,
+ parameterArray,
+ module,
+
+ historyEvent
+
+ ;
+
+ module = {
+
+ initialize: function() {
+ module.debug('Initializing tab menu item', $module);
+ module.fix.callbacks();
+ module.determineTabs();
+
+ module.debug('Determining tabs', settings.context, $tabs);
+ // set up automatic routing
+ if(settings.auto) {
+ module.set.auto();
+ }
+ module.bind.events();
+
+ if(settings.history && !initializedHistory) {
+ module.initializeHistory();
+ initializedHistory = true;
+ }
+
+ if(settings.autoTabActivation && instance === undefined && module.determine.activeTab() == null) {
+ module.debug('No active tab detected, setting first tab active', module.get.initialPath());
+ module.changeTab(settings.autoTabActivation === true ? module.get.initialPath() : settings.autoTabActivation);
+ };
+
+ module.instantiate();
+ },
+
+ instantiate: function () {
+ module.verbose('Storing instance of module', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, module)
+ ;
+ },
+
+ destroy: function() {
+ module.debug('Destroying tabs', $module);
+ $module
+ .removeData(moduleNamespace)
+ .off(eventNamespace)
+ ;
+ },
+
+ bind: {
+ events: function() {
+ // if using $.tab don't add events
+ if( !$.isWindow( element ) ) {
+ module.debug('Attaching tab activation events to element', $module);
+ $module
+ .on('click' + eventNamespace, module.event.click)
+ ;
+ }
+ }
+ },
+
+ determineTabs: function() {
+ var
+ $reference
+ ;
+
+ // determine tab context
+ if(settings.context === 'parent') {
+ if($module.closest(selector.ui).length > 0) {
+ $reference = $module.closest(selector.ui);
+ module.verbose('Using closest UI element as parent', $reference);
+ }
+ else {
+ $reference = $module;
+ }
+ $context = $reference.parent();
+ module.verbose('Determined parent element for creating context', $context);
+ }
+ else if(settings.context) {
+ $context = $(settings.context);
+ module.verbose('Using selector for tab context', settings.context, $context);
+ }
+ else {
+ $context = $('body');
+ }
+ // find tabs
+ if(settings.childrenOnly) {
+ $tabs = $context.children(selector.tabs);
+ module.debug('Searching tab context children for tabs', $context, $tabs);
+ }
+ else {
+ $tabs = $context.find(selector.tabs);
+ module.debug('Searching tab context for tabs', $context, $tabs);
+ }
+ },
+
+ fix: {
+ callbacks: function() {
+ if( $.isPlainObject(parameters) && (parameters.onTabLoad || parameters.onTabInit) ) {
+ if(parameters.onTabLoad) {
+ parameters.onLoad = parameters.onTabLoad;
+ delete parameters.onTabLoad;
+ module.error(error.legacyLoad, parameters.onLoad);
+ }
+ if(parameters.onTabInit) {
+ parameters.onFirstLoad = parameters.onTabInit;
+ delete parameters.onTabInit;
+ module.error(error.legacyInit, parameters.onFirstLoad);
+ }
+ settings = $.extend(true, {}, $.fn.tab.settings, parameters);
+ }
+ }
+ },
+
+ initializeHistory: function() {
+ module.debug('Initializing page state');
+ if( $.address === undefined ) {
+ module.error(error.state);
+ return false;
+ }
+ else {
+ if(settings.historyType == 'state') {
+ module.debug('Using HTML5 to manage state');
+ if(settings.path !== false) {
+ $.address
+ .history(true)
+ .state(settings.path)
+ ;
+ }
+ else {
+ module.error(error.path);
+ return false;
+ }
+ }
+ $.address
+ .bind('change', module.event.history.change)
+ ;
+ }
+ },
+
+ event: {
+ click: function(event) {
+ var
+ tabPath = $(this).data(metadata.tab)
+ ;
+ if(tabPath !== undefined) {
+ if(settings.history) {
+ module.verbose('Updating page state', event);
+ $.address.value(tabPath);
+ }
+ else {
+ module.verbose('Changing tab', event);
+ module.changeTab(tabPath);
+ }
+ event.preventDefault();
+ }
+ else {
+ module.debug('No tab specified');
+ }
+ },
+ history: {
+ change: function(event) {
+ var
+ tabPath = event.pathNames.join('/') || module.get.initialPath(),
+ pageTitle = settings.templates.determineTitle(tabPath) || false
+ ;
+ module.performance.display();
+ module.debug('History change event', tabPath, event);
+ historyEvent = event;
+ if(tabPath !== undefined) {
+ module.changeTab(tabPath);
+ }
+ if(pageTitle) {
+ $.address.title(pageTitle);
+ }
+ }
+ }
+ },
+
+ refresh: function() {
+ if(activeTabPath) {
+ module.debug('Refreshing tab', activeTabPath);
+ module.changeTab(activeTabPath);
+ }
+ },
+
+ cache: {
+
+ read: function(cacheKey) {
+ return (cacheKey !== undefined)
+ ? cache[cacheKey]
+ : false
+ ;
+ },
+ add: function(cacheKey, content) {
+ cacheKey = cacheKey || activeTabPath;
+ module.debug('Adding cached content for', cacheKey);
+ cache[cacheKey] = content;
+ },
+ remove: function(cacheKey) {
+ cacheKey = cacheKey || activeTabPath;
+ module.debug('Removing cached content for', cacheKey);
+ delete cache[cacheKey];
+ }
+ },
+
+ escape: {
+ string: function(text) {
+ text = String(text);
+ return text.replace(regExp.escape, '\\$&');
+ }
+ },
+
+ set: {
+ auto: function() {
+ var
+ url = (typeof settings.path == 'string')
+ ? settings.path.replace(/\/$/, '') + '/{$tab}'
+ : '/{$tab}'
+ ;
+ module.verbose('Setting up automatic tab retrieval from server', url);
+ if($.isPlainObject(settings.apiSettings)) {
+ settings.apiSettings.url = url;
+ }
+ else {
+ settings.apiSettings = {
+ url: url
+ };
+ }
+ },
+ loading: function(tabPath) {
+ var
+ $tab = module.get.tabElement(tabPath),
+ isLoading = $tab.hasClass(className.loading)
+ ;
+ if(!isLoading) {
+ module.verbose('Setting loading state for', $tab);
+ $tab
+ .addClass(className.loading)
+ .siblings($tabs)
+ .removeClass(className.active + ' ' + className.loading)
+ ;
+ if($tab.length > 0) {
+ settings.onRequest.call($tab[0], tabPath);
+ }
+ }
+ },
+ state: function(state) {
+ $.address.value(state);
+ }
+ },
+
+ changeTab: function(tabPath) {
+ var
+ pushStateAvailable = (window.history && window.history.pushState),
+ shouldIgnoreLoad = (pushStateAvailable && settings.ignoreFirstLoad && firstLoad),
+ remoteContent = (settings.auto || $.isPlainObject(settings.apiSettings) ),
+ // only add default path if not remote content
+ pathArray = (remoteContent && !shouldIgnoreLoad)
+ ? module.utilities.pathToArray(tabPath)
+ : module.get.defaultPathArray(tabPath)
+ ;
+ tabPath = module.utilities.arrayToPath(pathArray);
+ $.each(pathArray, function(index, tab) {
+ var
+ currentPathArray = pathArray.slice(0, index + 1),
+ currentPath = module.utilities.arrayToPath(currentPathArray),
+
+ isTab = module.is.tab(currentPath),
+ isLastIndex = (index + 1 == pathArray.length),
+
+ $tab = module.get.tabElement(currentPath),
+ $anchor,
+ nextPathArray,
+ nextPath,
+ isLastTab
+ ;
+ module.verbose('Looking for tab', tab);
+ if(isTab) {
+ module.verbose('Tab was found', tab);
+ // scope up
+ activeTabPath = currentPath;
+ parameterArray = module.utilities.filterArray(pathArray, currentPathArray);
+
+ if(isLastIndex) {
+ isLastTab = true;
+ }
+ else {
+ nextPathArray = pathArray.slice(0, index + 2);
+ nextPath = module.utilities.arrayToPath(nextPathArray);
+ isLastTab = ( !module.is.tab(nextPath) );
+ if(isLastTab) {
+ module.verbose('Tab parameters found', nextPathArray);
+ }
+ }
+ if(isLastTab && remoteContent) {
+ if(!shouldIgnoreLoad) {
+ module.activate.navigation(currentPath);
+ module.fetch.content(currentPath, tabPath);
+ }
+ else {
+ module.debug('Ignoring remote content on first tab load', currentPath);
+ firstLoad = false;
+ module.cache.add(tabPath, $tab.html());
+ module.activate.all(currentPath);
+ settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent);
+ settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent);
+ }
+ return false;
+ }
+ else {
+ module.debug('Opened local tab', currentPath);
+ module.activate.all(currentPath);
+ if( !module.cache.read(currentPath) ) {
+ module.cache.add(currentPath, true);
+ module.debug('First time tab loaded calling tab init');
+ settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent);
+ }
+ settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent);
+ }
+
+ }
+ else if(tabPath.search('/') == -1 && tabPath !== '') {
+ // look for in page anchor
+ tabPath = module.escape.string(tabPath);
+ $anchor = $('#' + tabPath + ', a[name="' + tabPath + '"]');
+ currentPath = $anchor.closest('[data-tab]').data(metadata.tab);
+ $tab = module.get.tabElement(currentPath);
+ // if anchor exists use parent tab
+ if($anchor && $anchor.length > 0 && currentPath) {
+ module.debug('Anchor link used, opening parent tab', $tab, $anchor);
+ if( !$tab.hasClass(className.active) ) {
+ setTimeout(function() {
+ module.scrollTo($anchor);
+ }, 0);
+ }
+ module.activate.all(currentPath);
+ if( !module.cache.read(currentPath) ) {
+ module.cache.add(currentPath, true);
+ module.debug('First time tab loaded calling tab init');
+ settings.onFirstLoad.call($tab[0], currentPath, parameterArray, historyEvent);
+ }
+ settings.onLoad.call($tab[0], currentPath, parameterArray, historyEvent);
+ return false;
+ }
+ }
+ else {
+ module.error(error.missingTab, $module, $context, currentPath);
+ return false;
+ }
+ });
+ },
+
+ scrollTo: function($element) {
+ var
+ scrollOffset = ($element && $element.length > 0)
+ ? $element.offset().top
+ : false
+ ;
+ if(scrollOffset !== false) {
+ module.debug('Forcing scroll to an in-page link in a hidden tab', scrollOffset, $element);
+ $(document).scrollTop(scrollOffset);
+ }
+ },
+
+ update: {
+ content: function(tabPath, html, evaluateScripts) {
+ var
+ $tab = module.get.tabElement(tabPath),
+ tab = $tab[0]
+ ;
+ evaluateScripts = (evaluateScripts !== undefined)
+ ? evaluateScripts
+ : settings.evaluateScripts
+ ;
+ if(typeof settings.cacheType == 'string' && settings.cacheType.toLowerCase() == 'dom' && typeof html !== 'string') {
+ $tab
+ .empty()
+ .append($(html).clone(true))
+ ;
+ }
+ else {
+ if(evaluateScripts) {
+ module.debug('Updating HTML and evaluating inline scripts', tabPath, html);
+ $tab.html(html);
+ }
+ else {
+ module.debug('Updating HTML', tabPath, html);
+ tab.innerHTML = html;
+ }
+ }
+ }
+ },
+
+ fetch: {
+
+ content: function(tabPath, fullTabPath) {
+ var
+ $tab = module.get.tabElement(tabPath),
+ apiSettings = {
+ dataType : 'html',
+ encodeParameters : false,
+ on : 'now',
+ cache : settings.alwaysRefresh,
+ headers : {
+ 'X-Remote': true
+ },
+ onSuccess : function(response) {
+ if(settings.cacheType == 'response') {
+ module.cache.add(fullTabPath, response);
+ }
+ module.update.content(tabPath, response);
+ if(tabPath == activeTabPath) {
+ module.debug('Content loaded', tabPath);
+ module.activate.tab(tabPath);
+ }
+ else {
+ module.debug('Content loaded in background', tabPath);
+ }
+ settings.onFirstLoad.call($tab[0], tabPath, parameterArray, historyEvent);
+ settings.onLoad.call($tab[0], tabPath, parameterArray, historyEvent);
+
+ if(settings.loadOnce) {
+ module.cache.add(fullTabPath, true);
+ }
+ else if(typeof settings.cacheType == 'string' && settings.cacheType.toLowerCase() == 'dom' && $tab.children().length > 0) {
+ setTimeout(function() {
+ var
+ $clone = $tab.children().clone(true)
+ ;
+ $clone = $clone.not('script');
+ module.cache.add(fullTabPath, $clone);
+ }, 0);
+ }
+ else {
+ module.cache.add(fullTabPath, $tab.html());
+ }
+ },
+ urlData: {
+ tab: fullTabPath
+ }
+ },
+ request = $tab.api('get request') || false,
+ existingRequest = ( request && request.state() === 'pending' ),
+ requestSettings,
+ cachedContent
+ ;
+
+ fullTabPath = fullTabPath || tabPath;
+ cachedContent = module.cache.read(fullTabPath);
+
+
+ if(settings.cache && cachedContent) {
+ module.activate.tab(tabPath);
+ module.debug('Adding cached content', fullTabPath);
+ if(!settings.loadOnce) {
+ if(settings.evaluateScripts == 'once') {
+ module.update.content(tabPath, cachedContent, false);
+ }
+ else {
+ module.update.content(tabPath, cachedContent);
+ }
+ }
+ settings.onLoad.call($tab[0], tabPath, parameterArray, historyEvent);
+ }
+ else if(existingRequest) {
+ module.set.loading(tabPath);
+ module.debug('Content is already loading', fullTabPath);
+ }
+ else if($.api !== undefined) {
+ requestSettings = $.extend(true, {}, settings.apiSettings, apiSettings);
+ module.debug('Retrieving remote content', fullTabPath, requestSettings);
+ module.set.loading(tabPath);
+ $tab.api(requestSettings);
+ }
+ else {
+ module.error(error.api);
+ }
+ }
+ },
+
+ activate: {
+ all: function(tabPath) {
+ module.activate.tab(tabPath);
+ module.activate.navigation(tabPath);
+ },
+ tab: function(tabPath) {
+ var
+ $tab = module.get.tabElement(tabPath),
+ $deactiveTabs = (settings.deactivate == 'siblings')
+ ? $tab.siblings($tabs)
+ : $tabs.not($tab),
+ isActive = $tab.hasClass(className.active)
+ ;
+ module.verbose('Showing tab content for', $tab);
+ if(!isActive) {
+ $tab
+ .addClass(className.active)
+ ;
+ $deactiveTabs
+ .removeClass(className.active + ' ' + className.loading)
+ ;
+ if($tab.length > 0) {
+ settings.onVisible.call($tab[0], tabPath);
+ }
+ }
+ },
+ navigation: function(tabPath) {
+ var
+ $navigation = module.get.navElement(tabPath),
+ $deactiveNavigation = (settings.deactivate == 'siblings')
+ ? $navigation.siblings($allModules)
+ : $allModules.not($navigation),
+ isActive = $navigation.hasClass(className.active)
+ ;
+ module.verbose('Activating tab navigation for', $navigation, tabPath);
+ if(!isActive) {
+ $navigation
+ .addClass(className.active)
+ ;
+ $deactiveNavigation
+ .removeClass(className.active + ' ' + className.loading)
+ ;
+ }
+ }
+ },
+
+ deactivate: {
+ all: function() {
+ module.deactivate.navigation();
+ module.deactivate.tabs();
+ },
+ navigation: function() {
+ $allModules
+ .removeClass(className.active)
+ ;
+ },
+ tabs: function() {
+ $tabs
+ .removeClass(className.active + ' ' + className.loading)
+ ;
+ }
+ },
+
+ is: {
+ tab: function(tabName) {
+ return (tabName !== undefined)
+ ? ( module.get.tabElement(tabName).length > 0 )
+ : false
+ ;
+ }
+ },
+
+ get: {
+ initialPath: function() {
+ return $allModules.eq(0).data(metadata.tab) || $tabs.eq(0).data(metadata.tab);
+ },
+ path: function() {
+ return $.address.value();
+ },
+ // adds default tabs to tab path
+ defaultPathArray: function(tabPath) {
+ return module.utilities.pathToArray( module.get.defaultPath(tabPath) );
+ },
+ defaultPath: function(tabPath) {
+ var
+ $defaultNav = $allModules.filter('[data-' + metadata.tab + '^="' + module.escape.string(tabPath) + '/"]').eq(0),
+ defaultTab = $defaultNav.data(metadata.tab) || false
+ ;
+ if( defaultTab ) {
+ module.debug('Found default tab', defaultTab);
+ if(recursionDepth < settings.maxDepth) {
+ recursionDepth++;
+ return module.get.defaultPath(defaultTab);
+ }
+ module.error(error.recursion);
+ }
+ else {
+ module.debug('No default tabs found for', tabPath, $tabs);
+ }
+ recursionDepth = 0;
+ return tabPath;
+ },
+ navElement: function(tabPath) {
+ tabPath = tabPath || activeTabPath;
+ return $allModules.filter('[data-' + metadata.tab + '="' + module.escape.string(tabPath) + '"]');
+ },
+ tabElement: function(tabPath) {
+ var
+ $fullPathTab,
+ $simplePathTab,
+ tabPathArray,
+ lastTab
+ ;
+ tabPath = tabPath || activeTabPath;
+ tabPathArray = module.utilities.pathToArray(tabPath);
+ lastTab = module.utilities.last(tabPathArray);
+ $fullPathTab = $tabs.filter('[data-' + metadata.tab + '="' + module.escape.string(tabPath) + '"]');
+ $simplePathTab = $tabs.filter('[data-' + metadata.tab + '="' + module.escape.string(lastTab) + '"]');
+ return ($fullPathTab.length > 0)
+ ? $fullPathTab
+ : $simplePathTab
+ ;
+ },
+ tab: function() {
+ return activeTabPath;
+ }
+ },
+
+ determine: {
+ activeTab: function() {
+ var activeTab = null;
+
+ $tabs.each(function(_index, tab) {
+ var $tab = $(tab);
+
+ if( $tab.hasClass(className.active) ) {
+ var
+ tabPath = $(this).data(metadata.tab),
+ $anchor = $allModules.filter('[data-' + metadata.tab + '="' + module.escape.string(tabPath) + '"]')
+ ;
+
+ if( $anchor.hasClass(className.active) ) {
+ activeTab = tabPath;
+ }
+ }
+ });
+
+ return activeTab;
+ }
+ },
+
+ utilities: {
+ filterArray: function(keepArray, removeArray) {
+ return $.grep(keepArray, function(keepValue) {
+ return ( $.inArray(keepValue, removeArray) == -1);
+ });
+ },
+ last: function(array) {
+ return Array.isArray(array)
+ ? array[ array.length - 1]
+ : false
+ ;
+ },
+ pathToArray: function(pathName) {
+ if(pathName === undefined) {
+ pathName = activeTabPath;
+ }
+ return typeof pathName == 'string'
+ ? pathName.split('/')
+ : [pathName]
+ ;
+ },
+ arrayToPath: function(pathArray) {
+ return Array.isArray(pathArray)
+ ? pathArray.join('/')
+ : false
+ ;
+ }
+ },
+
+ setting: function(name, value) {
+ module.debug('Changing setting', name, value);
+ if( $.isPlainObject(name) ) {
+ $.extend(true, settings, name);
+ }
+ else if(value !== undefined) {
+ if($.isPlainObject(settings[name])) {
+ $.extend(true, settings[name], value);
+ }
+ else {
+ settings[name] = value;
+ }
+ }
+ else {
+ return settings[name];
+ }
+ },
+ internal: function(name, value) {
+ if( $.isPlainObject(name) ) {
+ $.extend(true, module, name);
+ }
+ else if(value !== undefined) {
+ module[name] = value;
+ }
+ else {
+ return module[name];
+ }
+ },
+ debug: function() {
+ if(!settings.silent && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.debug.apply(console, arguments);
+ }
+ }
+ },
+ verbose: function() {
+ if(!settings.silent && settings.verbose && settings.debug) {
+ if(settings.performance) {
+ module.performance.log(arguments);
+ }
+ else {
+ module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
+ module.verbose.apply(console, arguments);
+ }
+ }
+ },
+ error: function() {
+ if(!settings.silent) {
+ module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
+ module.error.apply(console, arguments);
+ }
+ },
+ performance: {
+ log: function(message) {
+ var
+ currentTime,
+ executionTime,
+ previousTime
+ ;
+ if(settings.performance) {
+ currentTime = new Date().getTime();
+ previousTime = time || currentTime;
+ executionTime = currentTime - previousTime;
+ time = currentTime;
+ performance.push({
+ 'Name' : message[0],
+ 'Arguments' : [].slice.call(message, 1) || '',
+ 'Element' : element,
+ 'Execution Time' : executionTime
+ });
+ }
+ clearTimeout(module.performance.timer);
+ module.performance.timer = setTimeout(module.performance.display, 500);
+ },
+ display: function() {
+ var
+ title = settings.name + ':',
+ totalTime = 0
+ ;
+ time = false;
+ clearTimeout(module.performance.timer);
+ $.each(performance, function(index, data) {
+ totalTime += data['Execution Time'];
+ });
+ title += ' ' + totalTime + 'ms';
+ if(moduleSelector) {
+ title += ' \'' + moduleSelector + '\'';
+ }
+ if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
+ console.groupCollapsed(title);
+ if(console.table) {
+ console.table(performance);
+ }
+ else {
+ $.each(performance, function(index, data) {
+ console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
+ });
+ }
+ console.groupEnd();
+ }
+ performance = [];
+ }
+ },
+ invoke: function(query, passedArguments, context) {
+ var
+ object = instance,
+ maxDepth,
+ found,
+ response
+ ;
+ passedArguments = passedArguments || queryArguments;
+ context = element || context;
+ if(typeof query == 'string' && object !== undefined) {
+ query = query.split(/[\. ]/);
+ maxDepth = query.length - 1;
+ $.each(query, function(depth, value) {
+ var camelCaseValue = (depth != maxDepth)
+ ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
+ : query
+ ;
+ if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
+ object = object[camelCaseValue];
+ }
+ else if( object[camelCaseValue] !== undefined ) {
+ found = object[camelCaseValue];
+ return false;
+ }
+ else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
+ object = object[value];
+ }
+ else if( object[value] !== undefined ) {
+ found = object[value];
+ return false;
+ }
+ else {
+ module.error(error.method, query);
+ return false;
+ }
+ });
+ }
+ if ( $.isFunction( found ) ) {
+ response = found.apply(context, passedArguments);
+ }
+ else if(found !== undefined) {
+ response = found;
+ }
+ if(Array.isArray(returnedValue)) {
+ returnedValue.push(response);
+ }
+ else if(returnedValue !== undefined) {
+ returnedValue = [returnedValue, response];
+ }
+ else if(response !== undefined) {
+ returnedValue = response;
+ }
+ return found;
+ }
+ };
+ if(methodInvoked) {
+ if(instance === undefined) {
+ module.initialize();
+ }
+ module.invoke(query);
+ }
+ else {
+ if(instance !== undefined) {
+ instance.invoke('destroy');
+ }
+ module.initialize();
+ }
+ })
+ ;
+ return (returnedValue !== undefined)
+ ? returnedValue
+ : this
+ ;
+
+};
+
+// shortcut for tabbed content with no defined navigation
+$.tab = function() {
+ $(window).tab.apply(this, arguments);
+};
+
+$.fn.tab.settings = {
+
+ name : 'Tab',
+ namespace : 'tab',
+
+ silent : false,
+ debug : false,
+ verbose : false,
+ performance : true,
+
+ auto : false, // uses pjax style endpoints fetching content from same url with remote-content headers
+ history : false, // use browser history
+ historyType : 'hash', // #/ or html5 state
+ path : false, // base path of url
+
+ context : false, // specify a context that tabs must appear inside
+ childrenOnly : false, // use only tabs that are children of context
+ maxDepth : 25, // max depth a tab can be nested
+
+ deactivate : 'siblings', // whether tabs should deactivate sibling menu elements or all elements initialized together
+
+ alwaysRefresh : false, // load tab content new every tab click
+ cache : true, // cache the content requests to pull locally
+ loadOnce : false, // Whether tab data should only be loaded once when using remote content
+ cacheType : 'response', // Whether to cache exact response, or to html cache contents after scripts execute
+ ignoreFirstLoad : false, // don't load remote content on first load
+
+ apiSettings : false, // settings for api call
+ evaluateScripts : 'once', // whether inline scripts should be parsed (true/false/once). Once will not re-evaluate on cached content
+ autoTabActivation: true, // whether a non existing active tab will auto activate the first available tab
+
+ onFirstLoad : function(tabPath, parameterArray, historyEvent) {}, // called first time loaded
+ onLoad : function(tabPath, parameterArray, historyEvent) {}, // called on every load
+ onVisible : function(tabPath, parameterArray, historyEvent) {}, // called every time tab visible
+ onRequest : function(tabPath, parameterArray, historyEvent) {}, // called ever time a tab beings loading remote content
+
+ templates : {
+ determineTitle: function(tabArray) {} // returns page title for path
+ },
+
+ error: {
+ api : 'You attempted to load content without API module',
+ method : 'The method you called is not defined',
+ missingTab : 'Activated tab cannot be found. Tabs are case-sensitive.',
+ noContent : 'The tab you specified is missing a content url.',
+ path : 'History enabled, but no path was specified',
+ recursion : 'Max recursive depth reached',
+ legacyInit : 'onTabInit has been renamed to onFirstLoad in 2.0, please adjust your code.',
+ legacyLoad : 'onTabLoad has been renamed to onLoad in 2.0. Please adjust your code',
+ state : 'History requires Asual\'s Address library <https://github.com/asual/jquery-address>'
+ },
+
+ regExp : {
+ escape : /[-[\]{}()*+?.,\\^$|#\s:=@]/g
+ },
+
+ metadata : {
+ tab : 'tab',
+ loaded : 'loaded',
+ promise: 'promise'
+ },
+
+ className : {
+ loading : 'loading',
+ active : 'active'
+ },
+
+ selector : {
+ tabs : '.ui.tab',
+ ui : '.ui'
+ }
+
+};
+
+})( jQuery, window, document );
diff --git a/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2 b/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
new file mode 100644
index 0000000..978a681
--- /dev/null
+++ b/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
Binary files differ
diff --git a/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2 b/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
new file mode 100644
index 0000000..7e0118e
--- /dev/null
+++ b/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
Binary files differ
diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json
new file mode 100644
index 0000000..0b9dc6e
--- /dev/null
+++ b/web_src/fomantic/package-lock.json
@@ -0,0 +1,8742 @@
+{
+ "name": "fomantic",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "fomantic-ui": "2.8.7"
+ }
+ },
+ "node_modules/@choojs/findup": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz",
+ "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^2.15.1"
+ },
+ "bin": {
+ "findup": "bin/findup.js"
+ }
+ },
+ "node_modules/@choojs/findup/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
+ "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.0.3"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
+ "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/auth-token": "^5.0.0",
+ "@octokit/graphql": "^8.0.0",
+ "@octokit/request": "^9.0.0",
+ "@octokit/request-error": "^6.0.1",
+ "@octokit/types": "^13.0.0",
+ "before-after-hook": "^3.0.2",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/auth-token": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz",
+ "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/endpoint": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
+ "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
+ "version": "22.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
+ "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/request": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
+ "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/endpoint": "^10.0.0",
+ "@octokit/request-error": "^6.0.1",
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/request-error": {
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
+ "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/types": "^13.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/types": {
+ "version": "13.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.0.tgz",
+ "integrity": "sha512-CrooV/vKCXqwLa+osmHLIMUb87brpgUqlqkPGc6iE2wCkUvTrHiXFMhAKoDDaAAYJrtKtrFTgSQTg5nObBEaew==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/openapi-types": "^22.2.0"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/before-after-hook": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
+ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
+ "license": "Apache-2.0",
+ "peer": true
+ },
+ "node_modules/@octokit/core/node_modules/universal-user-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+ "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "6.0.12",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
+ "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/endpoint/node_modules/universal-user-agent": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
+ "license": "ISC"
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
+ "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/request": "^9.0.0",
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
+ "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
+ "version": "22.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
+ "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/request": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
+ "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/endpoint": "^10.0.0",
+ "@octokit/request-error": "^6.0.1",
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/request-error": {
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
+ "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/types": "^13.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/types": {
+ "version": "13.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.0.tgz",
+ "integrity": "sha512-CrooV/vKCXqwLa+osmHLIMUb87brpgUqlqkPGc6iE2wCkUvTrHiXFMhAKoDDaAAYJrtKtrFTgSQTg5nObBEaew==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@octokit/openapi-types": "^22.2.0"
+ }
+ },
+ "node_modules/@octokit/graphql/node_modules/universal-user-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
+ "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==",
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "12.11.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
+ "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz",
+ "integrity": "sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^2.0.1"
+ }
+ },
+ "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz",
+ "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">= 8"
+ }
+ },
+ "node_modules/@octokit/plugin-request-log": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
+ "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz",
+ "integrity": "sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^2.0.1",
+ "deprecation": "^2.3.1"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz",
+ "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">= 8"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
+ "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^6.0.1",
+ "@octokit/request-error": "^2.1.0",
+ "@octokit/types": "^6.16.1",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz",
+ "integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^2.0.0",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/@octokit/request-error/node_modules/@octokit/types": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz",
+ "integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">= 8"
+ }
+ },
+ "node_modules/@octokit/request/node_modules/@octokit/request-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
+ "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/@octokit/request/node_modules/universal-user-agent": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
+ "license": "ISC"
+ },
+ "node_modules/@octokit/rest": {
+ "version": "16.43.2",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz",
+ "integrity": "sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^2.4.0",
+ "@octokit/plugin-paginate-rest": "^1.1.1",
+ "@octokit/plugin-request-log": "^1.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "2.4.0",
+ "@octokit/request": "^5.2.0",
+ "@octokit/request-error": "^1.0.2",
+ "atob-lite": "^2.0.0",
+ "before-after-hook": "^2.0.0",
+ "btoa-lite": "^1.0.0",
+ "deprecation": "^2.0.0",
+ "lodash.get": "^4.4.2",
+ "lodash.set": "^4.3.2",
+ "lodash.uniq": "^4.5.0",
+ "octokit-pagination-methods": "^1.1.0",
+ "once": "^1.4.0",
+ "universal-user-agent": "^4.0.0"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "6.41.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
+ "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^12.11.0"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "license": "MIT"
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@types/expect": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz",
+ "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.7.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
+ "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/vinyl": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz",
+ "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/expect": "^1.20.4",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/accord": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/accord/-/accord-0.29.0.tgz",
+ "integrity": "sha512-3OOR92FTc2p5/EcOzPcXp+Cbo+3C15nV9RXHlOUBCBpHhcB+0frbSNR9ehED/o7sTcyGVtqGJpguToEdlXhD0w==",
+ "license": "MIT",
+ "dependencies": {
+ "convert-source-map": "^1.5.0",
+ "glob": "^7.0.5",
+ "indx": "^0.2.3",
+ "lodash.clone": "^4.3.2",
+ "lodash.defaults": "^4.0.1",
+ "lodash.flatten": "^4.2.0",
+ "lodash.merge": "^4.4.0",
+ "lodash.partialright": "^4.1.4",
+ "lodash.pick": "^4.2.1",
+ "lodash.uniq": "^4.3.0",
+ "resolve": "^1.5.0",
+ "semver": "^5.3.0",
+ "uglify-js": "^2.8.22",
+ "when": "^3.7.8"
+ }
+ },
+ "node_modules/accord/node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "license": "MIT"
+ },
+ "node_modules/align-text": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+ "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2",
+ "longest": "^1.0.1",
+ "repeat-string": "^1.5.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/align-text/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
+ "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-wrap": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-cyan": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz",
+ "integrity": "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-wrap": "0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+ "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ansi-gray": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
+ "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-wrap": "0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-red": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz",
+ "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-wrap": "0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ansi-wrap": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
+ "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/any-shell-escape": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/any-shell-escape/-/any-shell-escape-0.1.1.tgz",
+ "integrity": "sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "license": "ISC",
+ "dependencies": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ }
+ },
+ "node_modules/anymatch/node_modules/normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "remove-trailing-separator": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/append-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz",
+ "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/archy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
+ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-filter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz",
+ "integrity": "sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==",
+ "license": "MIT",
+ "dependencies": {
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-map": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz",
+ "integrity": "sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==",
+ "license": "MIT",
+ "dependencies": {
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-differ": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
+ "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-initial": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz",
+ "integrity": "sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==",
+ "license": "MIT",
+ "dependencies": {
+ "array-slice": "^1.0.0",
+ "is-number": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-initial/node_modules/is-number": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+ "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-last": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz",
+ "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-last/node_modules/is-number": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+ "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-sort": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz",
+ "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==",
+ "license": "MIT",
+ "dependencies": {
+ "default-compare": "^1.0.0",
+ "get-value": "^2.0.6",
+ "kind-of": "^5.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
+ "license": "MIT",
+ "dependencies": {
+ "array-uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==",
+ "license": "MIT"
+ },
+ "node_modules/async-done": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz",
+ "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.2",
+ "process-nextick-args": "^2.0.0",
+ "stream-exhaust": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/async-each": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz",
+ "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/async-settle": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz",
+ "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==",
+ "license": "MIT",
+ "dependencies": {
+ "async-done": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/atob-lite": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
+ "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "9.8.8",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
+ "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.12.0",
+ "caniuse-lite": "^1.0.30001109",
+ "normalize-range": "^0.1.2",
+ "num2fraction": "^1.2.2",
+ "picocolors": "^0.2.1",
+ "postcss": "^7.0.32",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ }
+ },
+ "node_modules/bach": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
+ "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-filter": "^1.1.1",
+ "arr-flatten": "^1.0.1",
+ "arr-map": "^2.0.0",
+ "array-each": "^1.0.0",
+ "array-initial": "^1.0.0",
+ "array-last": "^1.1.1",
+ "async-done": "^1.2.2",
+ "async-settle": "^1.0.0",
+ "now-and-later": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "license": "MIT",
+ "dependencies": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/base/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/beeper": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz",
+ "integrity": "sha512-3vqtKL1N45I5dV0RdssXZG7X6pCqQrWPNOlBPZPrd+QkE2HEhR57Z04m0KtpbsZH73j+a3F8UD1TQnn+ExTvIA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/better-console": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/better-console/-/better-console-1.0.1.tgz",
+ "integrity": "sha512-M/azU25cj3ZHbMSoXEroDfzcolfUvM03PZw5EEBk9T3tqdIYfMXrIkEKb9q8OZMC8Hic8Q9l8jk6TZq9cyRrcw==",
+ "license": "BSD",
+ "dependencies": {
+ "chalk": "^1.1.3",
+ "cli-table": "~0.3.1"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/binaryextensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz",
+ "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ },
+ "funding": {
+ "url": "https://bevry.me/fund"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
+ "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001663",
+ "electron-to-chromium": "^1.5.28",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/btoa-lite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
+ "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==",
+ "license": "MIT"
+ },
+ "node_modules/buffer-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz",
+ "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+ "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001664",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz",
+ "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/center-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+ "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==",
+ "license": "MIT",
+ "dependencies": {
+ "align-text": "^0.1.3",
+ "lazy-cache": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "license": "MIT"
+ },
+ "node_modules/chokidar": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
+ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ },
+ "optionalDependencies": {
+ "fsevents": "^1.2.7"
+ }
+ },
+ "node_modules/class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/class-utils/node_modules/is-descriptor": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+ "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/clean-css": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
+ "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "~0.6.0"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cli-table": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz",
+ "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==",
+ "dependencies": {
+ "colors": "1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.2.0"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
+ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
+ "license": "ISC"
+ },
+ "node_modules/cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
+ "license": "MIT",
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
+ "license": "MIT",
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-buffer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz",
+ "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/clone-stats": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz",
+ "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==",
+ "license": "MIT"
+ },
+ "node_modules/cloneable-readable": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz",
+ "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "process-nextick-args": "^2.0.0",
+ "readable-stream": "^2.3.5"
+ }
+ },
+ "node_modules/code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/collection-map": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
+ "integrity": "sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-map": "^2.0.2",
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==",
+ "license": "MIT",
+ "dependencies": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "license": "MIT"
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/colors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
+ "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "engines": [
+ "node >= 0.8"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/concat-with-sourcemaps": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz",
+ "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==",
+ "license": "ISC",
+ "dependencies": {
+ "source-map": "^0.6.1"
+ }
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/copy-anything": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
+ "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^3.14.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/copy-props": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz",
+ "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==",
+ "license": "MIT",
+ "dependencies": {
+ "each-props": "^1.3.2",
+ "is-plain-object": "^5.0.0"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/css": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+ "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "source-map": "^0.6.1",
+ "source-map-resolve": "^0.5.2",
+ "urix": "^0.1.0"
+ }
+ },
+ "node_modules/d": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+ "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+ "license": "ISC",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/dateformat": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
+ "integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decode-uri-component": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/deep-assign": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz",
+ "integrity": "sha512-iAL1PDjxqhANx86VhUjK0HSb4bozMfJUK64rxdrlWPCgMv7rBvP6AFySY69e+k8JAtPHNWoTsQT5OJvE+Jgpjg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz",
+ "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^5.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-resolution": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz",
+ "integrity": "sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
+ "integrity": "sha512-7yjqSoVSlJzA4t/VUwazuEagGeANEKB3f/aNI//06pfKgwoCb7f6Q1gETN1sZzYaj6chTQ0AhIwDiPdfOjko4A==",
+ "license": "MIT",
+ "dependencies": {
+ "globby": "^6.1.0",
+ "is-path-cwd": "^1.0.0",
+ "is-path-in-cwd": "^1.0.0",
+ "p-map": "^1.1.1",
+ "pify": "^3.0.0",
+ "rimraf": "^2.2.8"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
+ "license": "ISC"
+ },
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-indent": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
+ "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz",
+ "integrity": "sha512-1zEb73vemXFpUmfh3fsta4YHz3lwebxXvaWmPbFv9apujQBWDnkrPDLXLQs1gZo4RCWMDsT89r0Pf/z8/02TGA==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/duplexer2": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+ "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==",
+ "license": "BSD",
+ "dependencies": {
+ "readable-stream": "~1.1.9"
+ }
+ },
+ "node_modules/duplexer2/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+ "license": "MIT"
+ },
+ "node_modules/duplexer2/node_modules/readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "node_modules/duplexer2/node_modules/string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
+ "license": "MIT"
+ },
+ "node_modules/duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "node_modules/each-props": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz",
+ "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.1",
+ "object.defaults": "^1.1.0"
+ }
+ },
+ "node_modules/each-props/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+ "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/editorconfig/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/editorconfig/node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/editorconfig/node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.29",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz",
+ "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==",
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/errno": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es5-ext": {
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+ "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.2",
+ "ext": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/es6-weak-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+ "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.46",
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
+ "node_modules/execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-brackets/node_modules/is-descriptor": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+ "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "license": "MIT",
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+ "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+ "license": "ISC",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "license": "MIT",
+ "dependencies": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/extglob/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fancy-log": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz",
+ "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-gray": "^0.1.1",
+ "color-support": "^1.1.3",
+ "parse-node-version": "^1.0.0",
+ "time-stamp": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz",
+ "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==",
+ "license": "MIT"
+ },
+ "node_modules/figures": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+ "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/findup-sync": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
+ "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/fined/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/first-chunk-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz",
+ "integrity": "sha512-X8Z+b/0L4lToKYq+lwnKqi9X/Zek0NibLpsJgVsSxpoYq7JtiCtRb5HqKVEjEw/qAb/4AKKRLOwwKHlWNpm2Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flush-write-stream": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
+ }
+ },
+ "node_modules/fomantic-ui": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/fomantic-ui/-/fomantic-ui-2.8.7.tgz",
+ "integrity": "sha512-u22d28Z+U8mduTIM50MYzBGRz7CXYjGs2fUY6KO8N3enE8OAatDOXV4Mb/Xvj/ck5aNE6er6XJNK1fFWXt/u/w==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/rest": "^16.16.0",
+ "better-console": "1.0.1",
+ "del": "^3.0.0",
+ "extend": "^3.0.2",
+ "gulp": "^4.0.0",
+ "gulp-autoprefixer": "^6.0.0",
+ "gulp-chmod": "^2.0.0",
+ "gulp-clean-css": "^3.10.0",
+ "gulp-clone": "^2.0.1",
+ "gulp-concat": "^2.6.1",
+ "gulp-concat-css": "^3.1.0",
+ "gulp-concat-filenames": "^1.2.0",
+ "gulp-copy": "^4.0.0",
+ "gulp-debug": "^4.0.0",
+ "gulp-dedupe": "0.0.2",
+ "gulp-flatten": "^0.4.0",
+ "gulp-git": "^2.9.0",
+ "gulp-header": "^2.0.5",
+ "gulp-if": "^2.0.2",
+ "gulp-json-editor": "^2.4.3",
+ "gulp-less": "^4.0.1",
+ "gulp-notify": "^3.0.0",
+ "gulp-plumber": "^1.1.0",
+ "gulp-print": "^5.0.0",
+ "gulp-rename": "^1.4.0",
+ "gulp-replace": "^1.0.0",
+ "gulp-rtlcss": "^1.3.0",
+ "gulp-tap": "^1.0.1",
+ "gulp-uglify": "^3.0.1",
+ "inquirer": "^6.2.1",
+ "jquery": "^3.4.0",
+ "less": "^3.7.0",
+ "map-stream": "^0.1.0",
+ "merge-stream": "^2.0.0",
+ "mkdirp": "^0.5.1",
+ "normalize-path": "^3.0.0",
+ "replace-ext": "^1.0.0",
+ "require-dot-file": "^0.4.0",
+ "wrench-sui": "^0.0.3",
+ "yamljs": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=10.15.3",
+ "npm": ">=6.4.1"
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==",
+ "license": "MIT",
+ "dependencies": {
+ "for-in": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/foreground-child/node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/foreground-child/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/foreground-child/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fork-stream": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz",
+ "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==",
+ "license": "BSD"
+ },
+ "node_modules/fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==",
+ "license": "MIT",
+ "dependencies": {
+ "map-cache": "^0.2.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fs-mkdirp-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz",
+ "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.11",
+ "through2": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/fs-mkdirp-stream/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
+ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
+ "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "license": "ISC"
+ },
+ "node_modules/get-imports": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-imports/-/get-imports-1.0.0.tgz",
+ "integrity": "sha512-9FjKG2Os+o/EuOIh3B/LNMbU2FWPGHVy/gs9TJpytK95IPl7lLqiu+VAU7JX6VZimqdmpLemgsGMdQWdKvqYGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "array-uniq": "^1.0.1",
+ "import-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-own-enumerable-property-symbols": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+ "license": "ISC"
+ },
+ "node_modules/get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-stream/node_modules/pump": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "node_modules/glob-parent/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/glob-stream": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
+ "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==",
+ "license": "MIT",
+ "dependencies": {
+ "extend": "^3.0.0",
+ "glob": "^7.1.1",
+ "glob-parent": "^3.1.0",
+ "is-negated-glob": "^1.0.0",
+ "ordered-read-streams": "^1.0.0",
+ "pumpify": "^1.3.5",
+ "readable-stream": "^2.1.5",
+ "remove-trailing-separator": "^1.0.1",
+ "to-absolute-glob": "^2.0.0",
+ "unique-stream": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/glob-watcher": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz",
+ "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "^2.0.0",
+ "async-done": "^1.2.0",
+ "chokidar": "^2.0.0",
+ "is-negated-glob": "^1.0.0",
+ "just-debounce": "^1.0.0",
+ "normalize-path": "^3.0.0",
+ "object.defaults": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "license": "MIT",
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+ "license": "MIT",
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/globby": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+ "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==",
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^1.0.1",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/globby/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/glogg": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz",
+ "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==",
+ "license": "MIT",
+ "dependencies": {
+ "sparkles": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/growly": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
+ "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==",
+ "license": "MIT"
+ },
+ "node_modules/gulp": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
+ "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==",
+ "license": "MIT",
+ "dependencies": {
+ "glob-watcher": "^5.0.3",
+ "gulp-cli": "^2.2.0",
+ "undertaker": "^1.2.1",
+ "vinyl-fs": "^3.0.0"
+ },
+ "bin": {
+ "gulp": "bin/gulp.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/gulp-autoprefixer": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/gulp-autoprefixer/-/gulp-autoprefixer-6.1.0.tgz",
+ "integrity": "sha512-Ti/BUFe+ekhbDJfspZIMiOsOvw51KhI9EncsDfK7NaxjqRm+v4xS9v99kPxEoiDavpWqQWvG8Y6xT1mMlB3aXA==",
+ "license": "MIT",
+ "dependencies": {
+ "autoprefixer": "^9.5.1",
+ "fancy-log": "^1.3.2",
+ "plugin-error": "^1.0.1",
+ "postcss": "^7.0.2",
+ "through2": "^3.0.1",
+ "vinyl-sourcemaps-apply": "^0.2.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gulp-chmod": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz",
+ "integrity": "sha512-ttOK11mugzcy6D5CQD8rXqS7M4Ecoo64bDNhRXT9Yok9ztAcOeIK8hsv7LlV1eFS4iSQKZETvEZC5Kt/sH74sw==",
+ "license": "MIT",
+ "dependencies": {
+ "deep-assign": "^1.0.0",
+ "stat-mode": "^0.2.0",
+ "through2": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gulp-chmod/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-clean-css": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-3.10.0.tgz",
+ "integrity": "sha512-7Isf9Y690o/Q5MVjEylH1H7L8WeZ89woW7DnhD5unTintOdZb67KdOayRgp9trUFo+f9UyJtuatV42e/+kghPg==",
+ "license": "MIT",
+ "dependencies": {
+ "clean-css": "4.2.1",
+ "plugin-error": "1.0.1",
+ "through2": "2.0.3",
+ "vinyl-sourcemaps-apply": "0.2.1"
+ }
+ },
+ "node_modules/gulp-clean-css/node_modules/through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-cli": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz",
+ "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^1.0.1",
+ "archy": "^1.0.0",
+ "array-sort": "^1.0.0",
+ "color-support": "^1.1.3",
+ "concat-stream": "^1.6.0",
+ "copy-props": "^2.0.1",
+ "fancy-log": "^1.3.2",
+ "gulplog": "^1.0.0",
+ "interpret": "^1.4.0",
+ "isobject": "^3.0.1",
+ "liftoff": "^3.1.0",
+ "matchdep": "^2.0.0",
+ "mute-stdout": "^1.0.0",
+ "pretty-hrtime": "^1.0.0",
+ "replace-homedir": "^1.0.0",
+ "semver-greatest-satisfied-range": "^1.1.0",
+ "v8flags": "^3.2.0",
+ "yargs": "^7.1.0"
+ },
+ "bin": {
+ "gulp": "bin/gulp.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/gulp-clone": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/gulp-clone/-/gulp-clone-2.0.1.tgz",
+ "integrity": "sha512-SLg/KsHBbinR/pCX3PF5l1YlR28hLp0X+bcpf77PtMJ6zvAQ5kRjtCPV5Wt1wHXsXWZN0eTUZ15R8ZYpi/CdCA==",
+ "dependencies": {
+ "plugin-error": "^0.1.2",
+ "through2": "^2.0.3"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-clone/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-concat": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz",
+ "integrity": "sha512-a2scActrQrDBpBbR3WUZGyGS1JEPLg5PZJdIa7/Bi3GuKAmPYDK6SFhy/NZq5R8KsKKFvtfR0fakbUCcKGCCjg==",
+ "license": "MIT",
+ "dependencies": {
+ "concat-with-sourcemaps": "^1.0.0",
+ "through2": "^2.0.0",
+ "vinyl": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/gulp-concat-css": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/gulp-concat-css/-/gulp-concat-css-3.1.0.tgz",
+ "integrity": "sha512-iLTBPS+cutlgLyK3bp9DMts+WuS8n2mQpjzQ7p/ZVQc8FO5fvpN+ntg9U6jsuNvPeuii82aKm8XeOzF0nUK+TA==",
+ "dependencies": {
+ "lodash.defaults": "^3.0.0",
+ "parse-import": "^2.0.0",
+ "plugin-error": "^0.1.2",
+ "rework": "~1.0.0",
+ "rework-import": "^2.0.0",
+ "rework-plugin-url": "^1.0.1",
+ "through2": "~1.1.1",
+ "vinyl": "^2.1.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+ "license": "MIT"
+ },
+ "node_modules/gulp-concat-css/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "node_modules/gulp-concat-css/node_modules/string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
+ "license": "MIT"
+ },
+ "node_modules/gulp-concat-css/node_modules/through2": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz",
+ "integrity": "sha512-zEbpaeSMHxczpTzO1KkMHjBC1enTA68ojeaZGG4toqdASpb9t4xUZaYFBq2/9OHo5nTGFVSYd4c910OR+6wxbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": ">=1.1.13-1 <1.2.0-0",
+ "xtend": ">=4.0.0 <4.1.0-0"
+ }
+ },
+ "node_modules/gulp-concat-filenames": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gulp-concat-filenames/-/gulp-concat-filenames-1.2.0.tgz",
+ "integrity": "sha512-2wHcntxftYa2kiv5QOaniSNQuRf1axHGqkyXhRoCBXAVvwzrUp++qW9GNSAdvb3h+7m8yC8Fu25guuaDU+1WaA==",
+ "dependencies": {
+ "gulp-util": "3.x.x",
+ "through": "2.x.x"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/gulp-concat/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-copy": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/gulp-copy/-/gulp-copy-4.0.1.tgz",
+ "integrity": "sha512-UbdAwmEiVNNv55KAiUYWOP6Za7h8JPHNNyekNx8Gyc5XRlpUzTrlEclps939nOeiDPsd6jUtT2LmfavJirbZQg==",
+ "license": "MIT",
+ "dependencies": {
+ "gulp": "^4.0.0",
+ "plugin-error": "^0.1.2",
+ "through2": "^2.0.3"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-copy/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-debug": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/gulp-debug/-/gulp-debug-4.0.0.tgz",
+ "integrity": "sha512-cn/GhMD2nVZCVxAl5vWao4/dcoZ8wUJ8w3oqTvQaGDmC1vT7swNOEbhQTWJp+/otKePT64aENcqAQXDcdj5H1g==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^2.3.0",
+ "fancy-log": "^1.3.2",
+ "plur": "^3.0.0",
+ "stringify-object": "^3.0.0",
+ "through2": "^2.0.0",
+ "tildify": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "gulp": ">=4"
+ }
+ },
+ "node_modules/gulp-debug/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gulp-debug/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gulp-debug/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gulp-debug/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-dedupe": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/gulp-dedupe/-/gulp-dedupe-0.0.2.tgz",
+ "integrity": "sha512-Y+FZmAVHUYDgJiGneLXY2sCErvcY89sskjGQILhh5YvNGZq5M+pKsY54K0MyquZGxj2g10ZDVM5vQnEP7yUrVA==",
+ "license": "MIT",
+ "dependencies": {
+ "colors": "~1.0.2",
+ "diff": "~1.0.8",
+ "gulp-util": "~3.0.1",
+ "lodash.defaults": "~2.4.1",
+ "through": "~2.3.6"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/gulp-dedupe/node_modules/lodash.defaults": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz",
+ "integrity": "sha512-5wTIPWwGGr07JFysAZB8+7JB2NjJKXDIwogSaRX5zED85zyUAQwtOqUk8AsJkkigUcL3akbHYXd5+BPtTGQPZw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._objecttypes": "~2.4.1",
+ "lodash.keys": "~2.4.1"
+ }
+ },
+ "node_modules/gulp-dedupe/node_modules/lodash.keys": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz",
+ "integrity": "sha512-ZpJhwvUXHSNL5wYd1RM6CUa2ZuqorG9ngoJ9Ix5Cce+uX7I5O/E06FCJdhSZ33b5dVyeQDnIlWH7B2s5uByZ7g==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._isnative": "~2.4.1",
+ "lodash._shimkeys": "~2.4.1",
+ "lodash.isobject": "~2.4.1"
+ }
+ },
+ "node_modules/gulp-flatten": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/gulp-flatten/-/gulp-flatten-0.4.0.tgz",
+ "integrity": "sha512-eg4spVTAiv1xXmugyaCxWne1oPtNG0UHEtABx5W8ScLiqAYceyYm6GYA36x0Qh8KOIXmAZV97L2aYGnKREG3Sg==",
+ "license": "MIT",
+ "dependencies": {
+ "plugin-error": "^0.1.2",
+ "through2": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-flatten/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-git": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/gulp-git/-/gulp-git-2.11.0.tgz",
+ "integrity": "sha512-7YOcwin7sr68weYhBNOtZia3LZOGZWXgGcxxcxCi2hjljTgysOhH9mLTH2hdG5YLcuAFNg7mMbb2xIRfYsaQZw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-shell-escape": "^0.1.1",
+ "fancy-log": "^1.3.2",
+ "lodash": "^4.17.21",
+ "plugin-error": "^1.0.1",
+ "require-dir": "^1.0.0",
+ "strip-bom-stream": "^3.0.0",
+ "vinyl": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.9.0"
+ }
+ },
+ "node_modules/gulp-header": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-2.0.9.tgz",
+ "integrity": "sha512-LMGiBx+qH8giwrOuuZXSGvswcIUh0OiioNkUpLhNyvaC6/Ga8X6cfAeme2L5PqsbXMhL8o8b/OmVqIQdxprhcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "concat-with-sourcemaps": "^1.1.0",
+ "lodash.template": "^4.5.0",
+ "map-stream": "0.0.7",
+ "through2": "^2.0.0"
+ }
+ },
+ "node_modules/gulp-header/node_modules/map-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
+ "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==",
+ "license": "MIT"
+ },
+ "node_modules/gulp-header/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-if": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz",
+ "integrity": "sha512-tV0UfXkZodpFq6CYxEqH8tqLQgN6yR9qOhpEEN3O6N5Hfqk3fFLcbAavSex5EqnmoQjyaZ/zvgwclvlTI1KGfw==",
+ "license": "MIT",
+ "dependencies": {
+ "gulp-match": "^1.0.3",
+ "ternary-stream": "^2.0.1",
+ "through2": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/gulp-if/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-json-editor": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.6.0.tgz",
+ "integrity": "sha512-Ni0ZUpNrhesHiTlHQth/Nv1rXCn0LUicEvzA5XuGy186C4PVeNoRjfuAIQrbmt3scKv8dgGbCs0hd77ScTw7hA==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.3.1",
+ "detect-indent": "^6.1.0",
+ "js-beautify": "^1.14.11",
+ "plugin-error": "^2.0.1",
+ "through2": "^4.0.2"
+ }
+ },
+ "node_modules/gulp-json-editor/node_modules/plugin-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz",
+ "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/gulp-json-editor/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gulp-json-editor/node_modules/through2": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
+ "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "3"
+ }
+ },
+ "node_modules/gulp-less": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-4.0.1.tgz",
+ "integrity": "sha512-hmM2k0FfQp7Ptm3ZaqO2CkMX3hqpiIOn4OHtuSsCeFym63F7oWlEua5v6u1cIjVUKYsVIs9zPg9vbqTEb/udpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accord": "^0.29.0",
+ "less": "2.6.x || ^3.7.1",
+ "object-assign": "^4.0.1",
+ "plugin-error": "^0.1.2",
+ "replace-ext": "^1.0.0",
+ "through2": "^2.0.0",
+ "vinyl-sourcemaps-apply": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-less/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-match": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.1.0.tgz",
+ "integrity": "sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minimatch": "^3.0.3"
+ }
+ },
+ "node_modules/gulp-notify": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/gulp-notify/-/gulp-notify-3.2.0.tgz",
+ "integrity": "sha512-qEocs1UVoDKKUjfsxJNMNwkRla0PbsyJwsqNNXpzYWsLQ29LhxRMY3wnTGZcc4hMHtalnvah/Dwlwb4NijH/0A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^1.0.1",
+ "fancy-log": "^1.3.2",
+ "lodash.template": "^4.4.0",
+ "node-notifier": "^5.2.1",
+ "node.extend": "^2.0.0",
+ "plugin-error": "^0.1.2",
+ "through2": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=0.8.0",
+ "npm": ">=1.2.10"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-notify/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-plumber": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.1.tgz",
+ "integrity": "sha512-mctAi9msEAG7XzW5ytDVZ9PxWMzzi1pS2rBH7lA095DhMa6KEXjm+St0GOCc567pJKJ/oCvosVAZEpAey0q2eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^1.1.3",
+ "fancy-log": "^1.3.2",
+ "plugin-error": "^0.1.2",
+ "through2": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=0.10",
+ "npm": ">=1.2.10"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/arr-diff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
+ "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "array-slice": "^0.2.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/arr-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
+ "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/array-slice": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+ "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/extend-shallow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
+ "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/kind-of": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
+ "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/plugin-error": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
+ "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-cyan": "^0.1.1",
+ "ansi-red": "^0.1.1",
+ "arr-diff": "^1.0.1",
+ "arr-union": "^2.0.1",
+ "extend-shallow": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-plumber/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-print": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/gulp-print/-/gulp-print-5.0.2.tgz",
+ "integrity": "sha512-iIpHMzC/b3gFvVXOfP9Jk94SWGIsDLVNUrxULRleQev+08ug07mh84b1AOlW6QDQdmInQiqDFqJN1UvhU2nXdg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^3.2.4",
+ "fancy-log": "^1.3.3",
+ "map-stream": "0.0.7",
+ "vinyl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gulp-print/node_modules/ansi-colors": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
+ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gulp-print/node_modules/map-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
+ "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==",
+ "license": "MIT"
+ },
+ "node_modules/gulp-rename": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz",
+ "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gulp-replace": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz",
+ "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/vinyl": "^2.0.4",
+ "istextorbinary": "^3.0.0",
+ "replacestream": "^4.0.3",
+ "yargs-parser": ">=5.0.0-security.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/gulp-rtlcss": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/gulp-rtlcss/-/gulp-rtlcss-1.4.2.tgz",
+ "integrity": "sha512-wd807z/xq4XKtSwgrEetbx/aPoI5gV0yWV2rNqEBRwe2cJvNKLDsYR9A968c3gZtaKRMGAue5g3pHn40R+GWSA==",
+ "license": "MIT",
+ "dependencies": {
+ "plugin-error": "^1.0.1",
+ "rtlcss": "^2.4.0",
+ "through2": "^2.0.5",
+ "vinyl-sourcemaps-apply": "^0.2.1"
+ }
+ },
+ "node_modules/gulp-rtlcss/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-tap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gulp-tap/-/gulp-tap-1.0.1.tgz",
+ "integrity": "sha512-VpCARRSyr+WP16JGnoIg98/AcmyQjOwCpQgYoE35CWTdEMSbpgtAIK2fndqv2yY7aXstW27v3ZNBs0Ltb0Zkbg==",
+ "license": "MIT",
+ "dependencies": {
+ "through2": "^2.0.3"
+ }
+ },
+ "node_modules/gulp-tap/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-uglify": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gulp-uglify/-/gulp-uglify-3.0.2.tgz",
+ "integrity": "sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg==",
+ "license": "MIT",
+ "dependencies": {
+ "array-each": "^1.0.1",
+ "extend-shallow": "^3.0.2",
+ "gulplog": "^1.0.0",
+ "has-gulplog": "^0.1.0",
+ "isobject": "^3.0.1",
+ "make-error-cause": "^1.1.1",
+ "safe-buffer": "^5.1.2",
+ "through2": "^2.0.0",
+ "uglify-js": "^3.0.5",
+ "vinyl-sourcemaps-apply": "^0.2.0"
+ }
+ },
+ "node_modules/gulp-uglify/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-uglify/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-uglify/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-uglify/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-uglify/node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/gulp-util": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz",
+ "integrity": "sha512-q5oWPc12lwSFS9h/4VIjG+1NuNDlJ48ywV2JKItY4Ycc/n1fXJeYPVQsfu5ZrhQi7FGSDBalwUCLar/GyHXKGw==",
+ "deprecated": "gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5",
+ "license": "MIT",
+ "dependencies": {
+ "array-differ": "^1.0.0",
+ "array-uniq": "^1.0.2",
+ "beeper": "^1.0.0",
+ "chalk": "^1.0.0",
+ "dateformat": "^2.0.0",
+ "fancy-log": "^1.1.0",
+ "gulplog": "^1.0.0",
+ "has-gulplog": "^0.1.0",
+ "lodash._reescape": "^3.0.0",
+ "lodash._reevaluate": "^3.0.0",
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.template": "^3.0.0",
+ "minimist": "^1.1.0",
+ "multipipe": "^0.1.2",
+ "object-assign": "^3.0.0",
+ "replace-ext": "0.0.1",
+ "through2": "^2.0.0",
+ "vinyl": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/gulp-util/node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/gulp-util/node_modules/clone-stats": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz",
+ "integrity": "sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==",
+ "license": "MIT"
+ },
+ "node_modules/gulp-util/node_modules/lodash.template": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
+ "integrity": "sha512-0B4Y53I0OgHUJkt+7RmlDFWKjVAI/YUpWNiL9GQz5ORDr4ttgfQGo+phBWKFLJbBdtOwgMuUkdOHOnPg45jKmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._basecopy": "^3.0.0",
+ "lodash._basetostring": "^3.0.0",
+ "lodash._basevalues": "^3.0.0",
+ "lodash._isiterateecall": "^3.0.0",
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.escape": "^3.0.0",
+ "lodash.keys": "^3.0.0",
+ "lodash.restparam": "^3.0.0",
+ "lodash.templatesettings": "^3.0.0"
+ }
+ },
+ "node_modules/gulp-util/node_modules/lodash.templatesettings": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz",
+ "integrity": "sha512-TcrlEr31tDYnWkHFWDCV3dHYroKEXpJZ2YJYvJdhN+y4AkWMDZ5I4I8XDtUKqSAyG81N7w+I1mFEJtcED+tGqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.escape": "^3.0.0"
+ }
+ },
+ "node_modules/gulp-util/node_modules/object-assign": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+ "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gulp-util/node_modules/replace-ext": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz",
+ "integrity": "sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gulp-util/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/gulp-util/node_modules/vinyl": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
+ "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==",
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.0",
+ "clone-stats": "^0.0.1",
+ "replace-ext": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 0.9"
+ }
+ },
+ "node_modules/gulplog": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz",
+ "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==",
+ "license": "MIT",
+ "dependencies": {
+ "glogg": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-gulplog": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz",
+ "integrity": "sha512-+F4GzLjwHNNDEAJW2DC1xXfEoPkRDmUdJ7CBYw4MpqtDwOnqdImJl7GWlpqx+Wko6//J8uKTnIe4wZSv7yCqmw==",
+ "license": "MIT",
+ "dependencies": {
+ "sparkles": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==",
+ "license": "MIT",
+ "dependencies": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-values/node_modules/kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "license": "MIT",
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "license": "ISC"
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/import-regex/-/import-regex-1.1.0.tgz",
+ "integrity": "sha512-EblpleIyIdATUKj8ovFojUHyToxgjeKXQgTHZBGZ4cEkbtV21BlO1PSrzZQ6Fei2fgk7uhDeEx656yvPhlRGeA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/indx": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/indx/-/indx-0.2.3.tgz",
+ "integrity": "sha512-SEM+Px+Ghr3fZ+i9BNvUIZJ4UhojFuf+sT7x3cl2/ElL7NXne1A/m29VYzWTTypdOgDnWfoKNewIuPA6y+NMyQ==",
+ "license": "MIT"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/inquirer": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
+ "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^3.2.0",
+ "chalk": "^2.4.2",
+ "cli-cursor": "^2.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^2.0.0",
+ "lodash": "^4.17.12",
+ "mute-stream": "0.0.7",
+ "run-async": "^2.2.0",
+ "rxjs": "^6.4.0",
+ "string-width": "^2.1.0",
+ "strip-ansi": "^5.1.0",
+ "through": "^2.3.6"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/ansi-regex": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/inquirer/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/inquirer/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/inquirer/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/inquirer/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
+ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/invert-kv": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+ "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ip-regex": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz",
+ "integrity": "sha512-HjpCHTuxbR/6jWJroc/VN+npo5j0T4Vv2TAI5qdEHQx7hsL767MeccGFSsLtF694EiZKTSEqgoeU6DtGFCcuqQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/irregular-plurals": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz",
+ "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz",
+ "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-accessor-descriptor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
+ "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "license": "MIT"
+ },
+ "node_modules/is-core-module": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
+ "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-descriptor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz",
+ "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-descriptor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz",
+ "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-negated-glob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
+ "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-path-cwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+ "integrity": "sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-path-in-cwd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+ "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-path-inside": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+ "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-is-inside": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-unc-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "license": "MIT",
+ "dependencies": {
+ "unc-path-regex": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==",
+ "license": "MIT"
+ },
+ "node_modules/is-valid-glob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
+ "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
+ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
+ "license": "MIT"
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istextorbinary": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz",
+ "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "binaryextensions": "^2.2.0",
+ "textextensions": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://bevry.me/fund"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jquery": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
+ "license": "MIT"
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
+ "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
+ "license": "MIT",
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.3.3",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.0"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-beautify/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/js-beautify/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/js-beautify/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "license": "MIT"
+ },
+ "node_modules/just-debounce": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz",
+ "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==",
+ "license": "MIT"
+ },
+ "node_modules/kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/last-run": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz",
+ "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "default-resolution": "^2.0.0",
+ "es6-weak-map": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/lazy-cache": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+ "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lcid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+ "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==",
+ "license": "MIT",
+ "dependencies": {
+ "invert-kv": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lead": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz",
+ "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==",
+ "license": "MIT",
+ "dependencies": {
+ "flush-write-stream": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/less": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/less/-/less-3.13.1.tgz",
+ "integrity": "sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "copy-anything": "^2.0.1",
+ "tslib": "^1.10.0"
+ },
+ "bin": {
+ "lessc": "bin/lessc"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "optionalDependencies": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
+ "mime": "^1.4.1",
+ "native-request": "^1.0.5",
+ "source-map": "~0.6.0"
+ }
+ },
+ "node_modules/liftoff": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
+ "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==",
+ "license": "MIT",
+ "dependencies": {
+ "extend": "^3.0.0",
+ "findup-sync": "^3.0.0",
+ "fined": "^1.0.1",
+ "flagged-respawn": "^1.0.0",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.0",
+ "rechoir": "^0.6.2",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/liftoff/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "strip-bom": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/load-json-file/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._baseassign": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
+ "integrity": "sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._basecopy": "^3.0.0",
+ "lodash.keys": "^3.0.0"
+ }
+ },
+ "node_modules/lodash._basecopy": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+ "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._basetostring": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz",
+ "integrity": "sha512-mTzAr1aNAv/i7W43vOR/uD/aJ4ngbtsRaCubp2BfZhlGU/eORUjg/7F6X0orNMdv33JOrdgGybtvMN/po3EWrA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._basevalues": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz",
+ "integrity": "sha512-H94wl5P13uEqlCg7OcNNhMQ8KvWSIyqXzOPusRgHC9DK3o54P6P3xtbXlVbRABG4q5gSmp7EDdJ0MSuW9HX6Mg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._bindcallback": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz",
+ "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._createassigner": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz",
+ "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._bindcallback": "^3.0.0",
+ "lodash._isiterateecall": "^3.0.0",
+ "lodash.restparam": "^3.0.0"
+ }
+ },
+ "node_modules/lodash._getnative": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+ "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._isiterateecall": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
+ "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._isnative": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz",
+ "integrity": "sha512-BOlKGKNHhCHswGOWtmVb5zBygyxN7EmTuzVOSQI6QSoGhG+kvv71gICFS1TBpnqvT1n53txK8CDK3u5D2/GZxQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._objecttypes": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz",
+ "integrity": "sha512-XpqGh1e7hhkOzftBfWE7zt+Yn9mVHFkDhicVttvKLsoCMLVVL+xTQjfjB4X4vtznauxv0QZ5ZAeqjvat0dh62Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._reescape": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz",
+ "integrity": "sha512-Sjlavm5y+FUVIF3vF3B75GyXrzsfYV8Dlv3L4mEpuB9leg8N6yf/7rU06iLPx9fY0Mv3khVp9p7Dx0mGV6V5OQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._reevaluate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz",
+ "integrity": "sha512-OrPwdDc65iJiBeUe5n/LIjd7Viy99bKwDdk7Z5ljfZg0uFRFlfQaCy9tZ4YMAag9WAZmlVpe1iZrkIMMSMHD3w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._reinterpolate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
+ "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._root": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
+ "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash._shimkeys": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz",
+ "integrity": "sha512-lBrglYxLD/6KAJ8IEa5Lg+YHgNAL7FyKqXg4XOUI+Du/vtniLs1ZqS+yHNKPkK54waAgkdUnDOYaWf+rv4B+AA==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._objecttypes": "~2.4.1"
+ }
+ },
+ "node_modules/lodash.assign": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz",
+ "integrity": "sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._baseassign": "^3.0.0",
+ "lodash._createassigner": "^3.0.0",
+ "lodash.keys": "^3.0.0"
+ }
+ },
+ "node_modules/lodash.clone": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
+ "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.defaults": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz",
+ "integrity": "sha512-X7135IXFQt5JDFnYxOVAzVz+kFvwDn3N8DJYf+nrz/mMWEuSu7+OL6rWqsk3+VR1T4TejFCSu5isBJOLSID2bg==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.assign": "^3.0.0",
+ "lodash.restparam": "^3.0.0"
+ }
+ },
+ "node_modules/lodash.escape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
+ "integrity": "sha512-n1PZMXgaaDWZDSvuNZ/8XOcYO2hOKDqZel5adtR30VKQAtoWs/5AOeFA0vPV8moiPzlqe7F4cP2tzpFewQyelQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._root": "^3.0.0"
+ }
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isarguments": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+ "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isarray": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+ "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isobject": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz",
+ "integrity": "sha512-sTebg2a1PoicYEZXD5PBdQcTlIJ6hUslrlWr7iV0O7n+i4596s2NQ9I5CaZ5FbXSfya/9WQsrYLANUJv9paYVA==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._objecttypes": "~2.4.1"
+ }
+ },
+ "node_modules/lodash.keys": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+ "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._getnative": "^3.0.0",
+ "lodash.isarguments": "^3.0.0",
+ "lodash.isarray": "^3.0.0"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.partialright": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/lodash.partialright/-/lodash.partialright-4.2.1.tgz",
+ "integrity": "sha512-yebmPMQZH7i4El6SdJTW9rn8irWl8VTcsmiWqm/I4sY8/ZjbSo0Z512HL6soeAu3mh5rhx5uIIo6kYJOQXbCxw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.pick": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+ "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.restparam": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
+ "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.set": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+ "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.template": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
+ "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._reinterpolate": "^3.0.0",
+ "lodash.templatesettings": "^4.0.0"
+ }
+ },
+ "node_modules/lodash.templatesettings": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz",
+ "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash._reinterpolate": "^3.0.0"
+ }
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
+ "license": "MIT"
+ },
+ "node_modules/longest": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+ "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/macos-release": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz",
+ "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/make-dir/node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "license": "ISC"
+ },
+ "node_modules/make-error-cause": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-1.2.2.tgz",
+ "integrity": "sha512-4TO2Y3HkBnis4c0dxhAgD/jprySYLACf7nwN6V0HAHDx59g12WlRpUmFy1bRHamjGUEEBrEvCq6SUpsEE2lhUg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "make-error": "^1.2.0"
+ }
+ },
+ "node_modules/make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/make-iterator/node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-stream": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
+ },
+ "node_modules/map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==",
+ "license": "MIT",
+ "dependencies": {
+ "object-visit": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/matchdep": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
+ "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==",
+ "license": "MIT",
+ "dependencies": {
+ "findup-sync": "^2.0.0",
+ "micromatch": "^3.0.4",
+ "resolve": "^1.4.0",
+ "stack-trace": "0.0.10"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/matchdep/node_modules/findup-sync": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+ "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/matchdep/node_modules/is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/micromatch/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/micromatch/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/micromatch/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/micromatch/node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mixin-deep": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+ "license": "MIT",
+ "dependencies": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mixin-deep/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mixin-deep/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/multipipe": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
+ "integrity": "sha512-7ZxrUybYv9NonoXgwoOqtStIu18D1c3eFZj27hqgf5kBrBF8Q+tE8V0MW8dKM5QLkQPh1JhhbKgHLY9kifov4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "duplexer2": "0.0.2"
+ }
+ },
+ "node_modules/mute-stdout": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz",
+ "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+ "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==",
+ "license": "ISC"
+ },
+ "node_modules/nan": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
+ "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nanomatch/node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/native-request": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.1.2.tgz",
+ "integrity": "sha512-/etjwrK0J4Ebbcnt35VMWnfiUX/B04uwGJxyJInagxDqf2z5drSt/lsOvEMWGYunz1kaLZAFrV4NDAbOoDKvAQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
+ "license": "ISC"
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-notifier": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.5.tgz",
+ "integrity": "sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "growly": "^1.3.0",
+ "is-wsl": "^1.1.0",
+ "semver": "^5.5.0",
+ "shellwords": "^0.1.1",
+ "which": "^1.3.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+ "license": "MIT"
+ },
+ "node_modules/node.extend": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.3.tgz",
+ "integrity": "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw==",
+ "license": "(MIT OR GPL-2.0)",
+ "dependencies": {
+ "hasown": "^2.0.0",
+ "is": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/now-and-later": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz",
+ "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.3.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/num2fraction": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
+ "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==",
+ "license": "MIT"
+ },
+ "node_modules/number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-copy/node_modules/is-descriptor": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+ "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-copy/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==",
+ "license": "MIT",
+ "dependencies": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==",
+ "license": "MIT",
+ "dependencies": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.reduce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz",
+ "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==",
+ "license": "MIT",
+ "dependencies": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/octokit-pagination-methods": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
+ "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==",
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+ "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ordered-read-streams": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz",
+ "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-locale": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==",
+ "license": "MIT",
+ "dependencies": {
+ "lcid": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-name": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
+ "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
+ "license": "MIT",
+ "dependencies": {
+ "macos-release": "^2.2.0",
+ "windows-release": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
+ "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/parse-import": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-import/-/parse-import-2.0.0.tgz",
+ "integrity": "sha512-c59vdx1LiQT+majNKMyfFLrNMAVS9U1bychTv3CEuxbKspgnVTrzLRtgtfCWyAmTuFAxQVSJFasVv8svJLksIg==",
+ "license": "MIT",
+ "dependencies": {
+ "get-imports": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "error-ex": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-node-version": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==",
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==",
+ "license": "MIT",
+ "dependencies": {
+ "path-root-regex": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-type/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
+ "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
+ "license": "ISC"
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "pinkie": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/plugin-error": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
+ "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^1.0.1",
+ "arr-diff": "^4.0.0",
+ "arr-union": "^3.1.0",
+ "extend-shallow": "^3.0.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/plugin-error/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/plugin-error/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/plugin-error/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/plur": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz",
+ "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==",
+ "license": "MIT",
+ "dependencies": {
+ "irregular-plurals": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "7.0.39",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
+ "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "license": "ISC"
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pump": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+ "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/pumpify": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+ "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+ "license": "MIT",
+ "dependencies": {
+ "duplexify": "^3.6.0",
+ "inherits": "^2.0.3",
+ "pump": "^2.0.0"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+ "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "load-json-file": "^1.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-pkg-up": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+ "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^1.0.0",
+ "read-pkg": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+ "dependencies": {
+ "resolve": "^1.1.6"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-not/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-not/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/regex-not/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/remove-bom-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz",
+ "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5",
+ "is-utf8": "^0.2.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/remove-bom-stream": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz",
+ "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==",
+ "license": "MIT",
+ "dependencies": {
+ "remove-bom-buffer": "^3.0.0",
+ "safe-buffer": "^5.1.0",
+ "through2": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/remove-bom-stream/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==",
+ "license": "ISC"
+ },
+ "node_modules/repeat-element": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
+ "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/replace-ext": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz",
+ "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/replace-homedir": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz",
+ "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==",
+ "license": "MIT",
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1",
+ "is-absolute": "^1.0.0",
+ "remove-trailing-separator": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/replacestream": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz",
+ "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.3",
+ "object-assign": "^4.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/require-dir": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/require-dir/-/require-dir-1.2.0.tgz",
+ "integrity": "sha512-LY85DTSu+heYgDqq/mK+7zFHWkttVNRXC9NKcKGyuGLdlsfbjEPrIEYdCVrx6hqnJb+xSu3Lzaoo8VnmOhhjNA==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-dot-file": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/require-dot-file/-/require-dot-file-0.4.0.tgz",
+ "integrity": "sha512-pMe/T7+uFi2NMYsxuQtTh9n/UKD13HAHeDOk7KuP2pr7aKi5aMhvkbGD4IeoJKjy+3vdIUy8ggXYWzlZTL5FWA==",
+ "license": "MIT"
+ },
+ "node_modules/require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==",
+ "license": "ISC"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+ "license": "MIT",
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-options": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz",
+ "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==",
+ "license": "MIT",
+ "dependencies": {
+ "value-or-function": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==",
+ "deprecated": "https://github.com/lydell/resolve-url#deprecated",
+ "license": "MIT"
+ },
+ "node_modules/restore-cursor": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+ "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^2.0.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/rework": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
+ "integrity": "sha512-eEjL8FdkdsxApd0yWVZgBGzfCQiT8yqSc2H1p4jpZpQdtz7ohETiDMoje5PlM8I9WgkqkreVxFUKYOiJdVWDXw==",
+ "dependencies": {
+ "convert-source-map": "^0.3.3",
+ "css": "^2.0.0"
+ }
+ },
+ "node_modules/rework-import": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/rework-import/-/rework-import-2.1.0.tgz",
+ "integrity": "sha512-ufvoQX6cDhrqYc8ZXvJ+6FqimwyI4qn8cH1ypAiS9Mn41iVPN/9RGwRvscBtUEkHA09w8voTIakRJKslgWcTEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "css": "^2.0.0",
+ "globby": "^2.0.0",
+ "parse-import": "^2.0.0",
+ "url-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rework-import/node_modules/glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/rework-import/node_modules/globby": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-2.1.0.tgz",
+ "integrity": "sha512-CqRID2dMaN4Zi9PANiQHhmKaGu7ZASehBLnaDogjR9L3L1EqAGFhflafT0IrSN/zm9xFk+KMTXZCN8pUYOiO/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^1.0.1",
+ "async": "^1.2.1",
+ "glob": "^5.0.3",
+ "object-assign": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rework-import/node_modules/object-assign": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+ "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rework-plugin-function": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/rework-plugin-function/-/rework-plugin-function-1.0.2.tgz",
+ "integrity": "sha512-kyIphbC2Kuc3iFz1CSAQ5zmt4o/IHquhO+uG0kK0FQTjs4Z5eAxrqmrv3rZMR1KXa77SesaW9KwKyfbYoLMEqw==",
+ "license": "MIT",
+ "dependencies": {
+ "rework-visit": "^1.0.0"
+ }
+ },
+ "node_modules/rework-plugin-url": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/rework-plugin-url/-/rework-plugin-url-1.1.0.tgz",
+ "integrity": "sha512-qlAhbJKfEK59jAPQppIn8bNXffW1INlaJZaXdX/ZLs/CzZSnn38Y0wESQ3tjOwRsDbPEUHN2XJ3ZgueDaaCC0A==",
+ "license": "MIT",
+ "dependencies": {
+ "rework-plugin-function": "^1.0.0"
+ }
+ },
+ "node_modules/rework-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz",
+ "integrity": "sha512-W6V2fix7nCLUYX1v6eGPrBOZlc03/faqzP4sUxMAJMBMOPYhfV/RyLegTufn5gJKaOITyi+gvf0LXDZ9NzkHnQ==",
+ "license": "MIT"
+ },
+ "node_modules/rework/node_modules/convert-source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+ "integrity": "sha512-+4nRk0k3oEpwUB7/CalD7xE2z4VmtEnnq0GO2IPTkrooTrAhEsWvuLF5iWP1dXrwluki/azwXV1ve7gtYuPldg==",
+ "license": "MIT"
+ },
+ "node_modules/right-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+ "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==",
+ "license": "MIT",
+ "dependencies": {
+ "align-text": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/rtlcss": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.6.2.tgz",
+ "integrity": "sha512-06LFAr+GAPo+BvaynsXRfoYTJvSaWRyOhURCQ7aeI1MKph9meM222F+Zkt3bDamyHHJuGi3VPtiRkpyswmQbGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@choojs/findup": "^0.2.1",
+ "chalk": "^2.4.2",
+ "mkdirp": "^0.5.1",
+ "postcss": "^6.0.23",
+ "strip-json-comments": "^2.0.0"
+ },
+ "bin": {
+ "rtlcss": "bin/rtlcss.js"
+ }
+ },
+ "node_modules/rtlcss/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rtlcss/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rtlcss/node_modules/postcss": {
+ "version": "6.0.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+ "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^2.4.1",
+ "source-map": "^0.6.1",
+ "supports-color": "^5.4.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/rtlcss/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==",
+ "license": "MIT",
+ "dependencies": {
+ "ret": "~0.1.10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/semver-greatest-satisfied-range": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz",
+ "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==",
+ "license": "MIT",
+ "dependencies": {
+ "sver-compat": "^1.5.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-value": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/set-value/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shellwords": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
+ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
+ "license": "MIT"
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "license": "MIT",
+ "dependencies": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-node/node_modules/define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon-util/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/snapdragon/node_modules/is-descriptor": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+ "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/snapdragon/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-resolve": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+ "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
+ "license": "MIT",
+ "dependencies": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "node_modules/source-map-url": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
+ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
+ "deprecated": "See https://github.com/lydell/source-map-url#deprecated",
+ "license": "MIT"
+ },
+ "node_modules/sparkles": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz",
+ "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.20",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz",
+ "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/stat-mode": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz",
+ "integrity": "sha512-o+7DC0OM5Jt3+gratXXqfXf62V/CBoqQbT7Kp7jCxTYW2PLOB2/ZSGIfm9T5/QZe1Vw1MCbu6DoB6JnhVtxcJw==",
+ "license": "MIT"
+ },
+ "node_modules/static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==",
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-descriptor": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-extend/node_modules/is-descriptor": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz",
+ "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-accessor-descriptor": "^1.0.1",
+ "is-data-descriptor": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/stream-exhaust": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz",
+ "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==",
+ "license": "MIT"
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+ "license": "MIT"
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/stringify-object": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
+ "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "get-own-enumerable-property-symbols": "^3.0.0",
+ "is-obj": "^1.0.1",
+ "is-regexp": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-utf8": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-bom-buf": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz",
+ "integrity": "sha512-1sUIL1jck0T1mhOLP2c696BIznzT525Lkub+n4jjMHjhjhoAQA6Ye659DxdlZBr0aLDMQoTxKIpnlqxgtwjsuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-utf8": "^0.2.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-bom-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-3.0.0.tgz",
+ "integrity": "sha512-2di6sulSHfspbuEJHwwF6vzwijA4uaKsKYtviRQsJsOdxxb6yexiDcZFQ5oY10J50YxmCdHn/1sQmxDKbrGOVw==",
+ "license": "MIT",
+ "dependencies": {
+ "first-chunk-stream": "^2.0.0",
+ "strip-bom-buf": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sver-compat": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz",
+ "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==",
+ "license": "MIT",
+ "dependencies": {
+ "es6-iterator": "^2.0.1",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/ternary-stream": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-2.1.1.tgz",
+ "integrity": "sha512-j6ei9hxSoyGlqTmoMjOm+QNvUKDOIY6bNl4Uh1lhBvl6yjPW2iLqxDUYyfDPZknQ4KdRziFl+ec99iT4l7g0cw==",
+ "license": "MIT",
+ "dependencies": {
+ "duplexify": "^3.5.0",
+ "fork-stream": "^0.0.4",
+ "merge-stream": "^1.0.0",
+ "through2": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/ternary-stream/node_modules/merge-stream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
+ "integrity": "sha512-e6RM36aegd4f+r8BZCcYXlO2P3H6xbUM6ktL2Xmf45GAOit9bI4z6/3VU7JwllVO1L7u0UDSg/EhzQ5lmMLolA==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "node_modules/ternary-stream/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/textextensions": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz",
+ "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://bevry.me/fund"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "license": "MIT"
+ },
+ "node_modules/through2": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
+ "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "2 || 3"
+ }
+ },
+ "node_modules/through2-filter": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz",
+ "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==",
+ "license": "MIT",
+ "dependencies": {
+ "through2": "~2.0.0",
+ "xtend": "~4.0.0"
+ }
+ },
+ "node_modules/through2-filter/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/tildify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz",
+ "integrity": "sha512-Y9q1GaV/BO65Z9Yf4NOGMuwt3SGdptkZBnaaKfTQakrDyCLiuO1Kc5wxW4xLdsjzunRtqtOdhekiUFmZbklwYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "os-homedir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/time-stamp": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
+ "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "license": "MIT",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/to-absolute-glob": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
+ "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-absolute": "^1.0.0",
+ "is-negated-glob": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==",
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-object-path/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "license": "MIT",
+ "dependencies": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-through": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz",
+ "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "through2": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/to-through/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+ "license": "ISC"
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "source-map": "~0.5.1",
+ "yargs": "~3.10.0"
+ },
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ },
+ "optionalDependencies": {
+ "uglify-to-browserify": "~1.0.0"
+ }
+ },
+ "node_modules/uglify-js/node_modules/camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/uglify-js/node_modules/cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==",
+ "license": "ISC",
+ "dependencies": {
+ "center-align": "^0.1.1",
+ "right-align": "^0.1.1",
+ "wordwrap": "0.0.2"
+ }
+ },
+ "node_modules/uglify-js/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/uglify-js/node_modules/yargs": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==",
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^1.0.2",
+ "cliui": "^2.1.0",
+ "decamelize": "^1.0.0",
+ "window-size": "0.1.0"
+ }
+ },
+ "node_modules/uglify-to-browserify": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+ "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/undertaker": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz",
+ "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-flatten": "^1.0.1",
+ "arr-map": "^2.0.0",
+ "bach": "^1.0.0",
+ "collection-map": "^1.0.0",
+ "es6-weak-map": "^2.0.1",
+ "fast-levenshtein": "^1.0.0",
+ "last-run": "^1.1.0",
+ "object.defaults": "^1.0.0",
+ "object.reduce": "^1.0.0",
+ "undertaker-registry": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/undertaker-registry": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz",
+ "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "license": "MIT"
+ },
+ "node_modules/union-value": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unique-stream": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
+ "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==",
+ "license": "MIT",
+ "dependencies": {
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "through2-filter": "^3.0.0"
+ }
+ },
+ "node_modules/universal-user-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz",
+ "integrity": "sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==",
+ "license": "ISC",
+ "dependencies": {
+ "os-name": "^3.1.0"
+ }
+ },
+ "node_modules/unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-value/node_modules/isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==",
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/unset-value/node_modules/has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+ "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/update-browserslist-db/node_modules/picocolors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+ "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "license": "ISC"
+ },
+ "node_modules/urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==",
+ "deprecated": "Please see https://github.com/lydell/urix#deprecated",
+ "license": "MIT"
+ },
+ "node_modules/url-regex": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz",
+ "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-regex": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "license": "MIT",
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/value-or-function": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz",
+ "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vinyl": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz",
+ "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==",
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^2.1.1",
+ "clone-buffer": "^1.0.0",
+ "clone-stats": "^1.0.0",
+ "cloneable-readable": "^1.0.0",
+ "remove-trailing-separator": "^1.0.1",
+ "replace-ext": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vinyl-fs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz",
+ "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==",
+ "license": "MIT",
+ "dependencies": {
+ "fs-mkdirp-stream": "^1.0.0",
+ "glob-stream": "^6.1.0",
+ "graceful-fs": "^4.0.0",
+ "is-valid-glob": "^1.0.0",
+ "lazystream": "^1.0.0",
+ "lead": "^1.0.0",
+ "object.assign": "^4.0.4",
+ "pumpify": "^1.3.5",
+ "readable-stream": "^2.3.3",
+ "remove-bom-buffer": "^3.0.0",
+ "remove-bom-stream": "^1.2.0",
+ "resolve-options": "^1.1.0",
+ "through2": "^2.0.0",
+ "to-through": "^2.0.0",
+ "value-or-function": "^3.0.0",
+ "vinyl": "^2.0.0",
+ "vinyl-sourcemap": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vinyl-fs/node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/vinyl-sourcemap": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz",
+ "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==",
+ "license": "MIT",
+ "dependencies": {
+ "append-buffer": "^1.0.2",
+ "convert-source-map": "^1.5.0",
+ "graceful-fs": "^4.1.6",
+ "normalize-path": "^2.1.1",
+ "now-and-later": "^2.0.0",
+ "remove-bom-buffer": "^3.0.0",
+ "vinyl": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vinyl-sourcemap/node_modules/normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "remove-trailing-separator": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vinyl-sourcemaps-apply": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz",
+ "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==",
+ "license": "ISC",
+ "dependencies": {
+ "source-map": "^0.5.1"
+ }
+ },
+ "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/when": {
+ "version": "3.7.8",
+ "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz",
+ "integrity": "sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+ "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==",
+ "license": "ISC"
+ },
+ "node_modules/window-size": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
+ "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/windows-release": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz",
+ "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==",
+ "license": "MIT",
+ "dependencies": {
+ "execa": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==",
+ "license": "MIT/X11",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==",
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
+ "license": "MIT",
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
+ "license": "MIT",
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/wrench-sui": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wrench-sui/-/wrench-sui-0.0.3.tgz",
+ "integrity": "sha512-Y6qzMpcMG9akKnIdUsKzEF/Ht0KQJBP8ETkZj3FcGe93NC71e940WZUP1y+j+hc8Ecx9TyX0GvAWC4yymA88yA==",
+ "engines": {
+ "node": ">=0.1.97"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
+ "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==",
+ "license": "ISC"
+ },
+ "node_modules/yamljs": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz",
+ "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "glob": "^7.0.5"
+ },
+ "bin": {
+ "json2yaml": "bin/json2yaml",
+ "yaml2json": "bin/yaml2json"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz",
+ "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==",
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^3.0.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^1.4.0",
+ "read-pkg-up": "^1.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^1.0.2",
+ "which-module": "^1.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^5.0.1"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz",
+ "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^3.0.0",
+ "object.assign": "^4.1.0"
+ }
+ },
+ "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
+ "license": "MIT",
+ "dependencies": {
+ "number-is-nan": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
+ "license": "MIT",
+ "dependencies": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ }
+ }
+}
diff --git a/web_src/fomantic/package.json b/web_src/fomantic/package.json
new file mode 100644
index 0000000..c031c07
--- /dev/null
+++ b/web_src/fomantic/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "fomantic-ui": "2.8.7"
+ }
+}
diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json
new file mode 100644
index 0000000..5db57bc
--- /dev/null
+++ b/web_src/fomantic/semantic.json
@@ -0,0 +1,34 @@
+{
+ "version": "2.8.7",
+ "base": "node_modules/fomantic-ui",
+ "paths": {
+ "source": {
+ "config": "src/theme.config",
+ "definitions": "src/definitions/",
+ "site": "src/site/",
+ "themes": "src/themes/"
+ },
+ "output": {
+ "packaged": "../../build/",
+ "uncompressed": "../../build/components/",
+ "compressed": "../../build/components/",
+ "themes": "../../build/themes/"
+ },
+ "clean": "../../build/"
+ },
+ "permission": false,
+ "autoInstall": false,
+ "rtl": false,
+ "admin": false,
+ "components": [
+ "api",
+ "button",
+ "dimmer",
+ "dropdown",
+ "form",
+ "menu",
+ "modal",
+ "search",
+ "tab"
+ ]
+}
diff --git a/web_src/fomantic/theme.config.less b/web_src/fomantic/theme.config.less
new file mode 100644
index 0000000..b923994
--- /dev/null
+++ b/web_src/fomantic/theme.config.less
@@ -0,0 +1,103 @@
+/*
+
+████████╗██╗ ██╗███████╗███╗ ███╗███████╗███████╗
+╚══██╔══╝██║ ██║██╔════╝████╗ ████║██╔════╝██╔════╝
+ ██║ ███████║█████╗ ██╔████╔██║█████╗ ███████╗
+ ██║ ██╔══██║██╔══╝ ██║╚██╔╝██║██╔══╝ ╚════██║
+ ██║ ██║ ██║███████╗██║ ╚═╝ ██║███████╗███████║
+ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝
+
+*/
+
+/*******************************
+ Theme Selection
+*******************************/
+
+/* To override a theme for an individual element
+ specify theme name below
+*/
+
+/* Global */
+@site : 'default';
+@reset : 'default';
+
+/* Elements */
+@button : 'default';
+@container : 'default';
+@divider : 'default';
+@emoji : 'default';
+@flag : 'default';
+@header : 'default';
+@icon : 'default';
+@image : 'default';
+@input : 'default';
+@label : 'default';
+@list : 'default';
+@loader : 'default';
+@placeholder: 'default';
+@rail : 'default';
+@reveal : 'default';
+@segment : 'default';
+@step : 'default';
+@text : 'default';
+
+/* Collections */
+@breadcrumb : 'default';
+@form : 'default';
+@grid : 'default';
+@menu : 'default';
+@message : 'default';
+@table : 'default';
+
+/* Modules */
+@calendar : 'default';
+@checkbox : 'default';
+@dimmer : 'default';
+@dropdown : 'default';
+@embed : 'default';
+@modal : 'default';
+@nag : 'default';
+@popup : 'default';
+@progress : 'default';
+@slider : 'default';
+@rating : 'default';
+@search : 'default';
+@shape : 'default';
+@sidebar : 'default';
+@sticky : 'default';
+@tab : 'default';
+@toast : 'default';
+@transition : 'default';
+
+/* Views */
+@ad : 'default';
+@card : 'default';
+@comment : 'default';
+@feed : 'default';
+@item : 'default';
+@statistic : 'default';
+
+/*******************************
+ Folders
+*******************************/
+
+/* Path to theme packages */
+@themesFolder : 'themes';
+
+/* Path to site override folder */
+@siteFolder : '_site/';
+
+
+/*******************************
+ Import Theme
+*******************************/
+
+@import (multiple) "theme.less";
+
+/*******************************
+ Theme Overrides
+*******************************/
+
+@importGoogleFonts : false;
+
+/* End Config */
diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js
new file mode 100644
index 0000000..c707979
--- /dev/null
+++ b/web_src/js/bootstrap.js
@@ -0,0 +1,109 @@
+// DO NOT IMPORT window.config HERE!
+// to make sure the error handler always works, we should never import `window.config`, because
+// some user's custom template breaks it.
+
+// This sets up the URL prefix used in webpack's chunk loading.
+// This file must be imported before any lazy-loading is being attempted.
+__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
+
+// Ignore external and some known internal errors that we are unable to currently fix.
+function shouldIgnoreError(err) {
+ const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin));
+
+ if (!(err instanceof Error)) return false;
+ // If the error stack trace does not include the base URL of our script assets, it likely came
+ // from a browser extension or inline script. Ignore these errors.
+ if (!err.stack?.includes(assetBaseUrl)) return true;
+ // Ignore some known internal errors that we are unable to currently fix (eg via Monaco).
+ const ignorePatterns = [
+ '/assets/js/monaco.', // https://codeberg.org/forgejo/forgejo/issues/3638 , https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496
+ ];
+ for (const pattern of ignorePatterns) {
+ if (err.stack?.includes(pattern)) return true;
+ }
+ return false;
+}
+
+const filteredErrors = new Set([
+ 'getModifierState is not a function', // https://github.com/microsoft/monaco-editor/issues/4325
+]);
+
+export function showGlobalErrorMessage(msg) {
+ const pageContent = document.querySelector('.page-content');
+ if (!pageContent) return;
+
+ for (const filteredError of filteredErrors) {
+ if (msg.includes(filteredError)) return;
+ }
+
+ // compact the message to a data attribute to avoid too many duplicated messages
+ const msgCompact = msg.replace(/\W/g, '').trim();
+ let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
+ if (!msgDiv) {
+ const el = document.createElement('div');
+ el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`;
+ msgDiv = el.childNodes[0];
+ }
+ // merge duplicated messages into "the message (count)" format
+ const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1;
+ msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
+ msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
+ msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
+ pageContent.prepend(msgDiv);
+}
+
+/**
+ * @param {ErrorEvent|PromiseRejectionEvent} event - Event
+ * @param {string} event.message - Only present on ErrorEvent
+ * @param {string} event.error - Only present on ErrorEvent
+ * @param {string} event.type - Only present on ErrorEvent
+ * @param {string} event.filename - Only present on ErrorEvent
+ * @param {number} event.lineno - Only present on ErrorEvent
+ * @param {number} event.colno - Only present on ErrorEvent
+ * @param {string} event.reason - Only present on PromiseRejectionEvent
+ * @param {number} event.promise - Only present on PromiseRejectionEvent
+ */
+function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}) {
+ const err = error ?? reason;
+ const {runModeIsProd} = window.config ?? {};
+
+ // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a
+ // non-critical event from the browser. We log them but don't show them to users. Examples:
+ // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
+ // - https://github.com/mozilla-mobile/firefox-ios/issues/10817
+ // - https://github.com/go-gitea/gitea/issues/20240
+ if (!err) {
+ if (message) console.error(new Error(message));
+ if (runModeIsProd) return;
+ }
+
+ // In production do not display errors that should be ignored.
+ if (runModeIsProd && shouldIgnoreError(err)) return;
+
+ let msg = err?.message ?? message;
+ if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
+ const dot = msg.endsWith('.') ? '' : '.';
+ const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type;
+ showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`);
+}
+
+function initGlobalErrorHandler() {
+ if (window._globalHandlerErrors?._inited) {
+ showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`);
+ return;
+ }
+ if (!window.config) {
+ showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
+ }
+ // we added an event handler for window error at the very beginning of <script> of page head the
+ // handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before
+ // this init then in this init, we can collect all error events and show them.
+ for (const e of window._globalHandlerErrors || []) {
+ processWindowErrorEvent(e);
+ }
+ // then, change _globalHandlerErrors to an object with push method, to process further error
+ // events directly
+ window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)};
+}
+
+initGlobalErrorHandler();
diff --git a/web_src/js/bootstrap.test.js b/web_src/js/bootstrap.test.js
new file mode 100644
index 0000000..a6b901b
--- /dev/null
+++ b/web_src/js/bootstrap.test.js
@@ -0,0 +1,12 @@
+import {showGlobalErrorMessage} from './bootstrap.js';
+
+test('showGlobalErrorMessage', () => {
+ document.body.innerHTML = '<div class="page-content"></div>';
+ showGlobalErrorMessage('test msg 1');
+ showGlobalErrorMessage('test msg 2');
+ showGlobalErrorMessage('test msg 1'); // duplicated
+
+ expect(document.body.innerHTML).toContain('>test msg 1 (2)<');
+ expect(document.body.innerHTML).toContain('>test msg 2<');
+ expect(document.querySelectorAll('.js-global-error').length).toEqual(2);
+});
diff --git a/web_src/js/components/.eslintrc.yaml b/web_src/js/components/.eslintrc.yaml
new file mode 100644
index 0000000..0d23344
--- /dev/null
+++ b/web_src/js/components/.eslintrc.yaml
@@ -0,0 +1,21 @@
+plugins:
+ - eslint-plugin-vue
+ - eslint-plugin-vue-scoped-css
+
+extends:
+ - ../../../.eslintrc.yaml
+ - plugin:vue/vue3-recommended
+ - plugin:vue-scoped-css/vue3-recommended
+
+parserOptions:
+ sourceType: module
+ ecmaVersion: latest
+
+env:
+ browser: true
+
+rules:
+ vue/attributes-order: [0]
+ vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}]
+ vue/max-attributes-per-line: [0]
+ vue-scoped-css/enforce-style-type: [0]
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
new file mode 100644
index 0000000..7ada543
--- /dev/null
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -0,0 +1,39 @@
+<!-- This vue should be kept the same as templates/repo/actions/status.tmpl
+ Please also update the template file above if this vue is modified.
+ action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown
+-->
+<script>
+import {SvgIcon} from '../svg.js';
+
+export default {
+ components: {SvgIcon},
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ size: {
+ type: Number,
+ default: 16,
+ },
+ className: {
+ type: String,
+ default: '',
+ },
+ localeStatus: {
+ type: String,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status">
+ <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
+ <SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
+ <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
+ <SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'blocked'"/>
+ <SvgIcon name="octicon-meter" class="text yellow" :size="size" :class-name="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
+ <SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else-if="['failure', 'cancelled', 'unknown'].includes(status)"/>
+ </span>
+</template>
diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue
new file mode 100644
index 0000000..be0841e
--- /dev/null
+++ b/web_src/js/components/ActivityHeatmap.vue
@@ -0,0 +1,83 @@
+<script>
+import {CalendarHeatmap} from 'vue3-calendar-heatmap';
+
+export default {
+ components: {CalendarHeatmap},
+ props: {
+ values: {
+ type: Array,
+ default: () => [],
+ },
+ locale: {
+ type: Object,
+ default: () => {},
+ },
+ },
+ data: () => ({
+ colorRange: [
+ 'var(--color-secondary-alpha-60)',
+ 'var(--color-secondary-alpha-60)',
+ 'var(--color-primary-light-4)',
+ 'var(--color-primary-light-2)',
+ 'var(--color-primary)',
+ 'var(--color-primary-dark-2)',
+ 'var(--color-primary-dark-4)',
+ ],
+ endDate: new Date(),
+ }),
+ mounted() {
+ // work around issue with first legend color being rendered twice and legend cut off
+ const legend = document.querySelector('.vch__external-legend-wrapper');
+ legend.setAttribute('viewBox', '12 0 80 10');
+ legend.style.marginRight = '-12px';
+ },
+ methods: {
+ handleDayClick(e) {
+ // Reset filter if same date is clicked
+ const params = new URLSearchParams(document.location.search);
+ const queryDate = params.get('date');
+ // Timezone has to be stripped because toISOString() converts to UTC
+ const clickedDate = new Date(e.date - (e.date.getTimezoneOffset() * 60000)).toISOString().substring(0, 10);
+
+ if (queryDate && queryDate === clickedDate) {
+ params.delete('date');
+ } else {
+ params.set('date', clickedDate);
+ }
+
+ params.delete('page');
+
+ const newSearch = params.toString();
+ window.location.search = newSearch.length ? `?${newSearch}` : '';
+ },
+ },
+};
+</script>
+<template>
+ <div class="total-contributions">
+ {{ locale.contributions_in_the_last_12_months }}
+ </div>
+ <calendar-heatmap
+ :locale="locale"
+ :no-data-text="locale.contributions_zero"
+ :tooltip-formatter="
+ (v) =>
+ locale.contributions_format
+ .replace(
+ '{contributions}',
+ `<b>${v.count} ${
+ v.count === 1
+ ? locale.contributions_one
+ : locale.contributions_few
+ }</b>`
+ )
+ .replace('{month}', locale.months[v.date.getMonth()])
+ .replace('{day}', v.date.getDate())
+ .replace('{year}', v.date.getFullYear())
+ "
+ :end-date="endDate"
+ :values="values"
+ :range-color="colorRange"
+ @day-click="handleDayClick($event)"
+ />
+</template>
diff --git a/web_src/js/components/ContextPopup.test.js b/web_src/js/components/ContextPopup.test.js
new file mode 100644
index 0000000..2726567
--- /dev/null
+++ b/web_src/js/components/ContextPopup.test.js
@@ -0,0 +1,163 @@
+import {flushPromises, mount} from '@vue/test-utils';
+import ContextPopup from './ContextPopup.vue';
+
+async function assertPopup(popupData, expectedIconColor, expectedIcon) {
+ const date = new Date('2024-07-13T22:00:00Z');
+
+ vi.spyOn(global, 'fetch').mockResolvedValue({
+ json: vi.fn().mockResolvedValue({
+ ok: true,
+ created_at: date.toISOString(),
+ repository: {full_name: 'user2/repo1'},
+ ...popupData,
+ }),
+ ok: true,
+ });
+
+ const popup = mount(ContextPopup);
+ popup.vm.$el.dispatchEvent(new CustomEvent('ce-load-context-popup', {
+ detail: {owner: 'user2', repo: 'repo1', index: popupData.number},
+ }));
+ await flushPromises();
+
+ expect(popup.get('p:nth-of-type(1)').text()).toEqual(`user2/repo1 on ${date.toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})}`);
+ expect(popup.get('p:nth-of-type(2)').text()).toEqual(`${popupData.title} #${popupData.number}`);
+ expect(popup.get('p:nth-of-type(3)').text()).toEqual(popupData.body);
+
+ expect(popup.get('svg').classes()).toContain(expectedIcon);
+ expect(popup.get('svg').classes()).toContain(expectedIconColor);
+
+ for (const l of popupData.labels) {
+ expect(popup.findAll('.ui.label').map((x) => x.text())).toContain(l.name);
+ }
+}
+
+test('renders an open issue popup', async () => {
+ await assertPopup({
+ title: 'Open Issue',
+ body: 'Open Issue Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'open',
+ pull_request: null,
+ }, 'green', 'octicon-issue-opened');
+});
+
+test('renders a closed issue popup', async () => {
+ await assertPopup({
+ title: 'Closed Issue',
+ body: 'Closed Issue Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'closed',
+ pull_request: null,
+ }, 'red', 'octicon-issue-closed');
+});
+
+test('renders an open PR popup', async () => {
+ await assertPopup({
+ title: 'Open PR',
+ body: 'Open PR Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'open',
+ pull_request: {merged: false, draft: false},
+ }, 'green', 'octicon-git-pull-request');
+});
+
+test('renders an open WIP PR popup', async () => {
+ await assertPopup({
+ title: 'WIP: Open PR',
+ body: 'WIP Open PR Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'open',
+ pull_request: {merged: false, draft: true},
+ }, 'grey', 'octicon-git-pull-request-draft');
+});
+
+test('renders a closed PR popup', async () => {
+ await assertPopup({
+ title: 'Closed PR',
+ body: 'Closed PR Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'closed',
+ pull_request: {merged: false, draft: false},
+ }, 'red', 'octicon-git-pull-request-closed');
+});
+
+test('renders a closed WIP PR popup', async () => {
+ await assertPopup({
+ title: 'WIP: Closed PR',
+ body: 'WIP Closed PR Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'closed',
+ pull_request: {merged: false, draft: true},
+ }, 'red', 'octicon-git-pull-request-closed');
+});
+
+test('renders a merged PR popup', async () => {
+ await assertPopup({
+ title: 'Merged PR',
+ body: 'Merged PR Body',
+ number: 1,
+ labels: [{color: 'd21b1fff', name: 'Bug'}, {color: 'aaff00', name: 'Confirmed'}],
+ state: 'closed',
+ pull_request: {merged: true, draft: false},
+ }, 'purple', 'octicon-git-merge');
+});
+
+test('renders an issue popup with escaped HTML', async () => {
+ const evil = '<script class="evil">alert("evil message");</script>';
+
+ vi.spyOn(global, 'fetch').mockResolvedValue({
+ json: vi.fn().mockResolvedValue({
+ ok: true,
+ created_at: '2024-07-13T22:00:00Z',
+ repository: {full_name: evil},
+ title: evil,
+ body: evil,
+ labels: [{color: '000666', name: evil}],
+ state: 'open',
+ pull_request: null,
+ }),
+ ok: true,
+ });
+
+ const popup = mount(ContextPopup);
+ popup.vm.$el.dispatchEvent(new CustomEvent('ce-load-context-popup', {
+ detail: {owner: evil, repo: evil, index: 1},
+ }));
+ await flushPromises();
+
+ expect(() => popup.get('.evil')).toThrowError();
+ expect(popup.get('p:nth-of-type(1)').text()).toContain(evil);
+ expect(popup.get('p:nth-of-type(2)').text()).toContain(evil);
+ expect(popup.get('p:nth-of-type(3)').text()).toContain(evil);
+});
+
+test('renders an issue popup with emojis', async () => {
+ vi.spyOn(global, 'fetch').mockResolvedValue({
+ json: vi.fn().mockResolvedValue({
+ ok: true,
+ created_at: '2024-07-13T22:00:00Z',
+ repository: {full_name: 'user2/repo1'},
+ title: 'Title',
+ body: 'Body',
+ labels: [{color: '000666', name: 'Tag :+1:'}],
+ state: 'open',
+ pull_request: null,
+ }),
+ ok: true,
+ });
+
+ const popup = mount(ContextPopup);
+ popup.vm.$el.dispatchEvent(new CustomEvent('ce-load-context-popup', {
+ detail: {owner: 'user2', repo: 'repo1', index: 1},
+ }));
+ await flushPromises();
+
+ expect(popup.get('.ui.label').text()).toEqual('Tag 👍');
+});
diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
new file mode 100644
index 0000000..752a9a1
--- /dev/null
+++ b/web_src/js/components/ContextPopup.vue
@@ -0,0 +1,130 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {contrastColor} from '../utils/color.js';
+import {GET} from '../modules/fetch.js';
+import {emojiHTML} from '../features/emoji.js';
+import {htmlEscape} from 'escape-goat';
+
+const {appSubUrl, i18n} = window.config;
+
+export default {
+ components: {SvgIcon},
+ data: () => ({
+ loading: false,
+ issue: null,
+ i18nErrorOccurred: i18n.error_occurred,
+ i18nErrorMessage: null,
+ }),
+ computed: {
+ createdAt() {
+ return new Date(this.issue.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'});
+ },
+
+ body() {
+ const body = this.issue.body.replace(/\n+/g, ' ');
+ if (body.length > 85) {
+ return `${body.substring(0, 85)}…`;
+ }
+ return body;
+ },
+
+ icon() {
+ if (this.issue.pull_request !== null) {
+ if (this.issue.pull_request.merged === true) {
+ return 'octicon-git-merge'; // Merged PR
+ }
+
+ if (this.issue.state === 'closed') {
+ return 'octicon-git-pull-request-closed'; // Closed PR
+ }
+
+ if (this.issue.pull_request.draft === true) {
+ return 'octicon-git-pull-request-draft'; // WIP PR
+ }
+
+ return 'octicon-git-pull-request'; // Open PR
+ }
+
+ if (this.issue.state === 'closed') {
+ return 'octicon-issue-closed'; // Closed issue
+ }
+
+ return 'octicon-issue-opened'; // Open issue
+ },
+
+ color() {
+ if (this.issue.pull_request !== null) {
+ if (this.issue.pull_request.merged === true) {
+ return 'purple'; // Merged PR
+ }
+
+ if (this.issue.pull_request.draft === true && this.issue.state === 'open') {
+ return 'grey'; // WIP PR
+ }
+ }
+
+ if (this.issue.state === 'closed') {
+ return 'red'; // Closed issue
+ }
+
+ return 'green'; // Open issue
+ },
+
+ labels() {
+ return this.issue.labels.map((label) => ({
+ name: htmlEscape(label.name).replaceAll(/:[-+\w]+:/g, (emoji) => {
+ return emojiHTML(emoji.substring(1, emoji.length - 1));
+ }),
+ color: `#${label.color}`,
+ textColor: contrastColor(`#${label.color}`),
+ }));
+ },
+ },
+ mounted() {
+ this.$refs.root.addEventListener('ce-load-context-popup', (e) => {
+ const data = e.detail;
+ if (!this.loading && this.issue === null) {
+ this.load(data);
+ }
+ });
+ },
+ methods: {
+ async load(data) {
+ this.loading = true;
+ this.i18nErrorMessage = null;
+
+ try {
+ const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`);
+ const respJson = await response.json();
+ if (!response.ok) {
+ this.i18nErrorMessage = respJson.message ?? i18n.network_error;
+ return;
+ }
+ this.issue = respJson;
+ } catch {
+ this.i18nErrorMessage = i18n.network_error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div ref="root">
+ <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
+ <div v-if="!loading && issue !== null" id="issue-info-popup">
+ <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
+ <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
+ <p>{{ body }}</p>
+ <div class="labels-list">
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-for="label in labels" :key="label.name" class="ui label" :style="{ color: label.textColor, backgroundColor: label.color }" v-html="label.name"/>
+ </div>
+ </div>
+ <div v-if="!loading && issue === null">
+ <p><small>{{ i18nErrorOccurred }}</small></p>
+ <p>{{ i18nErrorMessage }}</p>
+ </div>
+ </div>
+</template>
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
new file mode 100644
index 0000000..e587413
--- /dev/null
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -0,0 +1,537 @@
+<script>
+import {createApp, nextTick} from 'vue';
+import $ from 'jquery';
+import {SvgIcon} from '../svg.js';
+import {GET} from '../modules/fetch.js';
+
+const {appSubUrl, assetUrlPrefix, pageData} = window.config;
+
+// make sure this matches templates/repo/commit_status.tmpl
+const commitStatus = {
+ pending: {name: 'octicon-dot-fill', color: 'yellow'},
+ success: {name: 'octicon-check', color: 'green'},
+ error: {name: 'gitea-exclamation', color: 'red'},
+ failure: {name: 'octicon-x', color: 'red'},
+ warning: {name: 'gitea-exclamation', color: 'yellow'},
+};
+
+const sfc = {
+ components: {SvgIcon},
+ data() {
+ const params = new URLSearchParams(window.location.search);
+ const tab = params.get('repo-search-tab') || 'repos';
+ const reposFilter = params.get('repo-search-filter') || 'all';
+ const privateFilter = params.get('repo-search-private') || 'both';
+ const archivedFilter = params.get('repo-search-archived') || 'unarchived';
+ const searchQuery = params.get('repo-search-query') || '';
+ const page = Number(params.get('repo-search-page')) || 1;
+
+ return {
+ tab,
+ repos: [],
+ reposTotalCount: 0,
+ reposFilter,
+ archivedFilter,
+ privateFilter,
+ page,
+ finalPage: 1,
+ searchQuery,
+ isLoading: false,
+ staticPrefix: assetUrlPrefix,
+ counts: {},
+ repoTypes: {
+ all: {
+ searchMode: '',
+ },
+ forks: {
+ searchMode: 'fork',
+ },
+ mirrors: {
+ searchMode: 'mirror',
+ },
+ sources: {
+ searchMode: 'source',
+ },
+ collaborative: {
+ searchMode: 'collaborative',
+ },
+ },
+ textArchivedFilterTitles: {},
+ textPrivateFilterTitles: {},
+
+ organizations: [],
+ isOrganization: true,
+ canCreateOrganization: false,
+ organizationsTotalCount: 0,
+ organizationId: 0,
+
+ subUrl: appSubUrl,
+ ...pageData.dashboardRepoList,
+ activeIndex: -1, // don't select anything at load, first cursor down will select
+ };
+ },
+
+ computed: {
+ showMoreReposLink() {
+ return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
+ },
+ searchURL() {
+ return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
+ }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
+ }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
+ }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
+ }`;
+ },
+ repoTypeCount() {
+ return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
+ },
+ checkboxArchivedFilterTitle() {
+ return this.textArchivedFilterTitles[this.archivedFilter];
+ },
+ checkboxArchivedFilterProps() {
+ return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'};
+ },
+ checkboxPrivateFilterTitle() {
+ return this.textPrivateFilterTitles[this.privateFilter];
+ },
+ checkboxPrivateFilterProps() {
+ return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'};
+ },
+ },
+
+ mounted() {
+ const el = document.getElementById('dashboard-repo-list');
+ this.changeReposFilter(this.reposFilter);
+ $(el).find('.dropdown').dropdown();
+ nextTick(() => {
+ this.$refs.search.focus();
+ });
+
+ this.textArchivedFilterTitles = {
+ 'archived': this.textShowOnlyArchived,
+ 'unarchived': this.textShowOnlyUnarchived,
+ 'both': this.textShowBothArchivedUnarchived,
+ };
+
+ this.textPrivateFilterTitles = {
+ 'private': this.textShowOnlyPrivate,
+ 'public': this.textShowOnlyPublic,
+ 'both': this.textShowBothPrivatePublic,
+ };
+ },
+
+ methods: {
+ changeTab(t) {
+ this.tab = t;
+ this.updateHistory();
+ },
+
+ changeReposFilter(filter) {
+ this.reposFilter = filter;
+ this.repos = [];
+ this.page = 1;
+ this.counts[`${filter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
+ this.searchRepos();
+ },
+
+ updateHistory() {
+ const params = new URLSearchParams(window.location.search);
+
+ if (this.tab === 'repos') {
+ params.delete('repo-search-tab');
+ } else {
+ params.set('repo-search-tab', this.tab);
+ }
+
+ if (this.reposFilter === 'all') {
+ params.delete('repo-search-filter');
+ } else {
+ params.set('repo-search-filter', this.reposFilter);
+ }
+
+ if (this.privateFilter === 'both') {
+ params.delete('repo-search-private');
+ } else {
+ params.set('repo-search-private', this.privateFilter);
+ }
+
+ if (this.archivedFilter === 'unarchived') {
+ params.delete('repo-search-archived');
+ } else {
+ params.set('repo-search-archived', this.archivedFilter);
+ }
+
+ if (this.searchQuery === '') {
+ params.delete('repo-search-query');
+ } else {
+ params.set('repo-search-query', this.searchQuery);
+ }
+
+ if (this.page === 1) {
+ params.delete('repo-search-page');
+ } else {
+ params.set('repo-search-page', `${this.page}`);
+ }
+
+ const queryString = params.toString();
+ if (queryString) {
+ window.history.replaceState({}, '', `?${queryString}`);
+ } else {
+ window.history.replaceState({}, '', window.location.pathname);
+ }
+ },
+
+ toggleArchivedFilter() {
+ if (this.archivedFilter === 'unarchived') {
+ this.archivedFilter = 'archived';
+ } else if (this.archivedFilter === 'archived') {
+ this.archivedFilter = 'both';
+ } else { // including both
+ this.archivedFilter = 'unarchived';
+ }
+ this.page = 1;
+ this.repos = [];
+ this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
+ this.searchRepos();
+ },
+
+ togglePrivateFilter() {
+ if (this.privateFilter === 'both') {
+ this.privateFilter = 'public';
+ } else if (this.privateFilter === 'public') {
+ this.privateFilter = 'private';
+ } else { // including private
+ this.privateFilter = 'both';
+ }
+ this.page = 1;
+ this.repos = [];
+ this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
+ this.searchRepos();
+ },
+
+ changePage(page) {
+ this.page = page;
+ if (this.page > this.finalPage) {
+ this.page = this.finalPage;
+ }
+ if (this.page < 1) {
+ this.page = 1;
+ }
+ this.repos = [];
+ this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
+ this.searchRepos();
+ },
+
+ async searchRepos() {
+ this.isLoading = true;
+
+ const searchedMode = this.repoTypes[this.reposFilter].searchMode;
+ const searchedURL = this.searchURL;
+ const searchedQuery = this.searchQuery;
+
+ let response, json;
+ try {
+ if (!this.reposTotalCount) {
+ const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
+ response = await GET(totalCountSearchURL);
+ this.reposTotalCount = response.headers.get('X-Total-Count') ?? '?';
+ }
+
+ response = await GET(searchedURL);
+ json = await response.json();
+ } catch {
+ if (searchedURL === this.searchURL) {
+ this.isLoading = false;
+ }
+ return;
+ }
+
+ if (searchedURL === this.searchURL) {
+ this.repos = json.data.map((webSearchRepo) => {
+ return {
+ ...webSearchRepo.repository,
+ latest_commit_status: webSearchRepo.latest_commit_status,
+ locale_latest_commit_status: webSearchRepo.locale_latest_commit_status,
+ };
+ });
+ const count = response.headers.get('X-Total-Count');
+ if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
+ this.reposTotalCount = count;
+ }
+ this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = count;
+ this.finalPage = Math.ceil(count / this.searchLimit);
+ this.updateHistory();
+ this.isLoading = false;
+ }
+ },
+
+ repoIcon(repo) {
+ if (repo.fork) {
+ return 'octicon-repo-forked';
+ } else if (repo.mirror) {
+ return 'octicon-mirror';
+ } else if (repo.template) {
+ return `octicon-repo-template`;
+ } else if (repo.private) {
+ return 'octicon-lock';
+ } else if (repo.internal) {
+ return 'octicon-repo';
+ }
+ return 'octicon-repo';
+ },
+
+ statusIcon(status) {
+ return commitStatus[status].name;
+ },
+
+ statusColor(status) {
+ return commitStatus[status].color;
+ },
+
+ reposFilterKeyControl(e) {
+ switch (e.key) {
+ case 'Enter':
+ document.querySelector('.repo-owner-name-list li.active a')?.click();
+ break;
+ case 'ArrowUp':
+ if (this.activeIndex > 0) {
+ this.activeIndex--;
+ } else if (this.page > 1) {
+ this.changePage(this.page - 1);
+ this.activeIndex = this.searchLimit - 1;
+ }
+ break;
+ case 'ArrowDown':
+ if (this.activeIndex < this.repos.length - 1) {
+ this.activeIndex++;
+ } else if (this.page < this.finalPage) {
+ this.activeIndex = 0;
+ this.changePage(this.page + 1);
+ }
+ break;
+ case 'ArrowRight':
+ if (this.page < this.finalPage) {
+ this.changePage(this.page + 1);
+ }
+ break;
+ case 'ArrowLeft':
+ if (this.page > 1) {
+ this.changePage(this.page - 1);
+ }
+ break;
+ }
+ if (this.activeIndex === -1 || this.activeIndex > this.repos.length - 1) {
+ this.activeIndex = 0;
+ }
+ },
+ },
+};
+
+export function initDashboardRepoList() {
+ const el = document.getElementById('dashboard-repo-list');
+ if (el) {
+ createApp(sfc).mount(el);
+ }
+}
+
+export default sfc; // activate the IDE's Vue plugin
+</script>
+<template>
+ <div>
+ <div v-if="!isOrganization" class="ui secondary stackable menu tabs-with-labels">
+ <a :class="{item: true, active: tab === 'repos'}" @click="changeTab('repos')">{{ textMyRepos }} <span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span></a>
+ <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textMyOrgs }} <span class="ui grey label tw-ml-2">{{ organizationsTotalCount }}</span></a>
+ </div>
+ <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
+ <h4 v-if="isOrganization" class="ui top attached tw-mt-4 tw-flex tw-items-center">
+ <div class="tw-flex-1 tw-flex tw-items-center">
+ {{ textMyRepos }}
+ <span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
+ </div>
+ </h4>
+ <div class="ui top attached segment repos-search">
+ <div class="ui small fluid action left icon input">
+ <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
+ <i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
+ <div class="ui dropdown icon button" :title="textFilter">
+ <svg-icon name="octicon-filter" :size="16"/>
+ <div class="menu">
+ <a class="item" @click="toggleArchivedFilter()">
+ <div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle">
+ <!--the "hidden" is necessary to make the checkbox work without Fomantic UI js,
+ otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
+ <input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps">
+ <label>
+ <svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/>
+ {{ textShowArchived }}
+ </label>
+ </div>
+ </a>
+ <a class="item" @click="togglePrivateFilter()">
+ <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
+ <input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps">
+ <label>
+ <svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/>
+ {{ textShowPrivate }}
+ </label>
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ <overflow-menu class="ui secondary pointing tabular borderless menu repos-filter">
+ <div class="overflow-menu-items tw-justify-center">
+ <a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
+ {{ textAll }}
+ <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+ </a>
+ <a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
+ {{ textSources }}
+ <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+ </a>
+ <a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
+ {{ textForks }}
+ <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+ </a>
+ <a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
+ {{ textMirrors }}
+ <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+ </a>
+ <a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
+ {{ textCollaborative }}
+ <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
+ </a>
+ </div>
+ </overflow-menu>
+ </div>
+ <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
+ <ul class="repo-owner-name-list">
+ <li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
+ <a class="repo-list-link muted" :href="repo.link">
+ <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/>
+ <div class="text truncate">{{ repo.full_name }}</div>
+ <div v-if="repo.archived">
+ <svg-icon name="octicon-archive" :size="16"/>
+ </div>
+ </a>
+ <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status" :href="repo.latest_commit_status.TargetURL" :data-tooltip-content="repo.locale_latest_commit_status">
+ <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
+ <svg-icon :name="statusIcon(repo.latest_commit_status.State)" :class-name="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status.State)" :size="16"/>
+ </a>
+ </li>
+ </ul>
+ <div v-if="showMoreReposLink" class="tw-text-center">
+ <div class="divider tw-my-0"/>
+ <div class="ui borderless pagination menu narrow tw-my-2">
+ <a
+ class="item navigation tw-py-1" :class="{'disabled': page === 1}"
+ @click="changePage(1)" :title="textFirstPage"
+ >
+ <svg-icon name="gitea-double-chevron-left" :size="16" class-name="tw-mr-1"/>
+ </a>
+ <a
+ class="item navigation tw-py-1" :class="{'disabled': page === 1}"
+ @click="changePage(page - 1)" :title="textPreviousPage"
+ >
+ <svg-icon name="octicon-chevron-left" :size="16" clsas-name="tw-mr-1"/>
+ </a>
+ <a class="active item tw-py-1">{{ page }}</a>
+ <a
+ class="item navigation" :class="{'disabled': page === finalPage}"
+ @click="changePage(page + 1)" :title="textNextPage"
+ >
+ <svg-icon name="octicon-chevron-right" :size="16" class-name="tw-ml-1"/>
+ </a>
+ <a
+ class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
+ @click="changePage(finalPage)" :title="textLastPage"
+ >
+ <svg-icon name="gitea-double-chevron-right" :size="16" class-name="tw-ml-1"/>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
+ <div v-if="organizations.length" class="ui attached table segment tw-rounded">
+ <ul class="repo-owner-name-list">
+ <li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
+ <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
+ <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
+ <div class="text truncate">{{ org.name }}</div>
+ <div><!-- div to prevent underline of label on hover -->
+ <span class="ui tiny basic label" v-if="org.org_visibility !== 'public'">
+ {{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }}
+ </span>
+ </div>
+ </a>
+ <div class="text light grey tw-flex tw-items-center tw-ml-2">
+ {{ org.num_repos }}
+ <svg-icon name="octicon-repo" :size="16" class-name="tw-ml-1 tw-mt-0.5"/>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+<style scoped>
+ul {
+ list-style: none;
+ margin: 0;
+ padding-left: 0;
+}
+
+ul li {
+ padding: 0 10px;
+}
+
+ul li:not(:last-child) {
+ border-bottom: 1px solid var(--color-secondary);
+}
+
+.repos-search {
+ padding-bottom: 0 !important;
+}
+
+.repos-filter {
+ padding-top: 0 !important;
+ margin-top: 0 !important;
+ border-bottom-width: 0 !important;
+ margin-bottom: 2px !important;
+}
+
+.repos-filter .item {
+ padding-left: 6px !important;
+ padding-right: 6px !important;
+}
+
+.repo-list-link {
+ min-width: 0; /* for text truncation */
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 0.5rem;
+}
+
+.repo-list-link .svg {
+ color: var(--color-text-light-2);
+}
+
+.repo-list-icon {
+ min-width: 16px;
+ margin-right: 2px;
+}
+
+/* octicon-mirror has no padding inside the SVG */
+.repo-list-icon.octicon-mirror {
+ width: 14px;
+ min-width: 14px;
+ margin-left: 1px;
+ margin-right: 3px;
+}
+
+.repo-owner-name-list li.active {
+ background: var(--color-hover);
+}
+</style>
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
new file mode 100644
index 0000000..3f6e4e4
--- /dev/null
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -0,0 +1,306 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {GET} from '../modules/fetch.js';
+
+export default {
+ components: {SvgIcon},
+ data: () => {
+ const el = document.getElementById('diff-commit-select');
+ return {
+ menuVisible: false,
+ isLoading: false,
+ locale: {
+ filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
+ },
+ commits: [],
+ hoverActivated: false,
+ lastReviewCommitSha: null,
+ };
+ },
+ computed: {
+ commitsSinceLastReview() {
+ if (this.lastReviewCommitSha) {
+ return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1;
+ }
+ return 0;
+ },
+ queryParams() {
+ return this.$el.parentNode.getAttribute('data-queryparams');
+ },
+ issueLink() {
+ return this.$el.parentNode.getAttribute('data-issuelink');
+ },
+ },
+ mounted() {
+ document.body.addEventListener('click', this.onBodyClick);
+ this.$el.addEventListener('keydown', this.onKeyDown);
+ this.$el.addEventListener('keyup', this.onKeyUp);
+ },
+ unmounted() {
+ document.body.removeEventListener('click', this.onBodyClick);
+ this.$el.removeEventListener('keydown', this.onKeyDown);
+ this.$el.removeEventListener('keyup', this.onKeyUp);
+ },
+ methods: {
+ onBodyClick(event) {
+ // close this menu on click outside of this element when the dropdown is currently visible opened
+ if (this.$el.contains(event.target)) return;
+ if (this.menuVisible) {
+ this.toggleMenu();
+ }
+ },
+ onKeyDown(event) {
+ if (!this.menuVisible) return;
+ const item = document.activeElement;
+ if (!this.$el.contains(item)) return;
+ switch (event.key) {
+ case 'ArrowDown': // select next element
+ event.preventDefault();
+ this.focusElem(item.nextElementSibling, item);
+ break;
+ case 'ArrowUp': // select previous element
+ event.preventDefault();
+ this.focusElem(item.previousElementSibling, item);
+ break;
+ case 'Escape': // close menu
+ event.preventDefault();
+ item.tabIndex = -1;
+ this.toggleMenu();
+ break;
+ }
+ },
+ onKeyUp(event) {
+ if (!this.menuVisible) return;
+ const item = document.activeElement;
+ if (!this.$el.contains(item)) return;
+ if (event.key === 'Shift' && this.hoverActivated) {
+ // shift is not pressed anymore -> deactivate hovering and reset hovered and selected
+ this.hoverActivated = false;
+ for (const commit of this.commits) {
+ commit.hovered = false;
+ commit.selected = false;
+ }
+ }
+ },
+ highlight(commit) {
+ if (!this.hoverActivated) return;
+ const indexSelected = this.commits.findIndex((x) => x.selected);
+ const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id);
+ for (const [idx, commit] of this.commits.entries()) {
+ commit.hovered = Math.min(indexSelected, indexCurrentElem) <= idx && idx <= Math.max(indexSelected, indexCurrentElem);
+ }
+ },
+ /** Focus given element */
+ focusElem(elem, prevElem) {
+ if (elem) {
+ elem.tabIndex = 0;
+ if (prevElem) prevElem.tabIndex = -1;
+ elem.focus();
+ }
+ },
+ /** Opens our menu, loads commits before opening */
+ async toggleMenu() {
+ this.menuVisible = !this.menuVisible;
+ // load our commits when the menu is not yet visible (it'll be toggled after loading)
+ // and we got no commits
+ if (!this.commits.length && this.menuVisible && !this.isLoading) {
+ this.isLoading = true;
+ try {
+ await this.fetchCommits();
+ } finally {
+ this.isLoading = false;
+ }
+ }
+ // set correct tabindex to allow easier navigation
+ this.$nextTick(() => {
+ const expandBtn = this.$el.querySelector('#diff-commit-list-expand');
+ const showAllChanges = this.$el.querySelector('#diff-commit-list-show-all');
+ if (this.menuVisible) {
+ this.focusElem(showAllChanges, expandBtn);
+ } else {
+ this.focusElem(expandBtn, showAllChanges);
+ }
+ });
+ },
+ /** Load the commits to show in this dropdown */
+ async fetchCommits() {
+ const resp = await GET(`${this.issueLink}/commits/list`);
+ const results = await resp.json();
+ this.commits.push(...results.commits.map((x) => {
+ x.hovered = false;
+ return x;
+ }));
+ this.commits.reverse();
+ this.lastReviewCommitSha = results.last_review_commit_sha || null;
+ if (this.lastReviewCommitSha && !this.commits.some((x) => x.id === this.lastReviewCommitSha)) {
+ // the lastReviewCommit is not available (probably due to a force push)
+ // reset the last review commit sha
+ this.lastReviewCommitSha = null;
+ }
+ Object.assign(this.locale, results.locale);
+ },
+ showAllChanges() {
+ window.location = `${this.issueLink}/files${this.queryParams}`;
+ },
+ /** Called when user clicks on since last review */
+ changesSinceLastReviewClick() {
+ window.location = `${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`;
+ },
+ /** Clicking on a single commit opens this specific commit */
+ commitClicked(commitId, newWindow = false) {
+ const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`;
+ if (newWindow) {
+ window.open(url);
+ } else {
+ window.location = url;
+ }
+ },
+ /**
+ * When a commit is clicked with shift this enables the range
+ * selection. Second click (with shift) defines the end of the
+ * range. This opens the diff of this range
+ * Exception: first commit is the first commit of this PR. Then
+ * the diff from beginning of PR up to the second clicked commit is
+ * opened
+ */
+ commitClickedShift(commit) {
+ this.hoverActivated = !this.hoverActivated;
+ commit.selected = true;
+ // Second click -> determine our range and open links accordingly
+ if (!this.hoverActivated) {
+ // find all selected commits and generate a link
+ if (this.commits[0].selected) {
+ // first commit is selected - generate a short url with only target sha
+ const lastCommitIdx = this.commits.findLastIndex((x) => x.selected);
+ if (lastCommitIdx === this.commits.length - 1) {
+ // user selected all commits - just show the normal diff page
+ window.location = `${this.issueLink}/files${this.queryParams}`;
+ } else {
+ window.location = `${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`;
+ }
+ } else {
+ const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id;
+ const end = this.commits.findLast((x) => x.selected).id;
+ window.location = `${this.issueLink}/files/${start}..${end}${this.queryParams}`;
+ }
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div class="ui scrolling dropdown custom">
+ <button
+ class="ui basic button"
+ id="diff-commit-list-expand"
+ @click.stop="toggleMenu()"
+ :data-tooltip-content="locale.filter_changes_by_commit"
+ aria-haspopup="true"
+ aria-controls="diff-commit-selector-menu"
+ :aria-label="locale.filter_changes_by_commit"
+ aria-activedescendant="diff-commit-list-show-all"
+ >
+ <svg-icon name="octicon-git-commit"/>
+ </button>
+ <div class="menu left transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
+ <div class="loading-indicator is-loading" v-if="isLoading"/>
+ <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
+ <div class="gt-ellipsis">
+ {{ locale.show_all_commits }}
+ </div>
+ <div class="gt-ellipsis text light-2 tw-mb-0">
+ {{ locale.stats_num_commits }}
+ </div>
+ </div>
+ <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review -->
+ <div
+ v-if="lastReviewCommitSha != null" role="menuitem"
+ class="vertical item"
+ :class="{disabled: !commitsSinceLastReview}"
+ @keydown.enter="changesSinceLastReviewClick()"
+ @click="changesSinceLastReviewClick()"
+ >
+ <div class="gt-ellipsis">
+ {{ locale.show_changes_since_your_last_review }}
+ </div>
+ <div class="gt-ellipsis text light-2">
+ {{ commitsSinceLastReview }} commits
+ </div>
+ </div>
+ <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
+ <template v-for="commit in commits" :key="commit.id">
+ <div
+ class="vertical item" role="menuitem"
+ :class="{selection: commit.selected, hovered: commit.hovered}"
+ @keydown.enter.exact="commitClicked(commit.id)"
+ @keydown.enter.shift.exact="commitClickedShift(commit)"
+ @mouseover.shift="highlight(commit)"
+ @click.exact="commitClicked(commit.id)"
+ @click.ctrl.exact="commitClicked(commit.id, true)"
+ @click.meta.exact="commitClicked(commit.id, true)"
+ @click.shift.exact.stop.prevent="commitClickedShift(commit)"
+ >
+ <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1">
+ <div class="gt-ellipsis commit-list-summary">
+ {{ commit.summary }}
+ </div>
+ <div class="gt-ellipsis text light-2">
+ {{ commit.committer_or_author_name }}
+ <span class="text right">
+ <!-- TODO: make this respect the PreferredTimestampTense setting -->
+ <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
+ </span>
+ </div>
+ </div>
+ <div class="tw-font-mono">
+ {{ commit.short_sha }}
+ </div>
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
+<style scoped>
+ .hovered:not(.selection) {
+ background-color: var(--color-small-accent) !important;
+ }
+ .selection {
+ background-color: var(--color-accent) !important;
+ }
+
+ .info {
+ display: inline-block;
+ padding: 7px 14px !important;
+ line-height: 1.4;
+ width: 100%;
+ }
+
+ #diff-commit-selector-menu {
+ overflow-x: hidden;
+ max-height: 450px;
+ }
+
+ #diff-commit-selector-menu .loading-indicator {
+ height: 200px;
+ width: 350px;
+ }
+
+ #diff-commit-selector-menu .item,
+ #diff-commit-selector-menu .info {
+ display: flex !important;
+ flex-direction: row;
+ line-height: 1.4;
+ padding: 7px 14px !important;
+ border-top: 1px solid var(--color-secondary) !important;
+ gap: 0.25em;
+ }
+
+ #diff-commit-selector-menu .item:focus {
+ color: var(--color-text);
+ background: var(--color-hover);
+ }
+
+ #diff-commit-selector-menu .commit-list-summary {
+ max-width: min(380px, 96vw);
+ }
+</style>
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
new file mode 100644
index 0000000..916780d
--- /dev/null
+++ b/web_src/js/components/DiffFileList.vue
@@ -0,0 +1,58 @@
+<script>
+import {loadMoreFiles} from '../features/repo-diff.js';
+import {diffTreeStore} from '../modules/stores.js';
+
+export default {
+ data: () => {
+ return {store: diffTreeStore()};
+ },
+ mounted() {
+ document.getElementById('show-file-list-btn').addEventListener('click', this.toggleFileList);
+ },
+ unmounted() {
+ document.getElementById('show-file-list-btn').removeEventListener('click', this.toggleFileList);
+ },
+ methods: {
+ toggleFileList() {
+ this.store.fileListIsVisible = !this.store.fileListIsVisible;
+ },
+ diffTypeToString(pType) {
+ const diffTypes = {
+ 1: 'add',
+ 2: 'modify',
+ 3: 'del',
+ 4: 'rename',
+ 5: 'copy',
+ };
+ return diffTypes[pType];
+ },
+ diffStatsWidth(adds, dels) {
+ return `${adds / (adds + dels) * 100}%`;
+ },
+ loadMoreData() {
+ loadMoreFiles(this.store.linkLoadMore);
+ },
+ },
+};
+</script>
+<template>
+ <ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible">
+ <li v-for="file in store.files" :key="file.NameHash">
+ <div class="tw-font-semibold tw-flex tw-items-center pull-right">
+ <span v-if="file.IsBin" class="tw-ml-0.5 tw-mr-2">{{ store.binaryFileMessage }}</span>
+ {{ file.IsBin ? '' : file.Addition + file.Deletion }}
+ <span v-if="!file.IsBin" class="diff-stats-bar tw-mx-2" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)">
+ <div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }"/>
+ </span>
+ </div>
+ <!-- todo finish all file status, now modify, add, delete and rename -->
+ <span :class="['status', diffTypeToString(file.Type)]" :data-tooltip-content="diffTypeToString(file.Type)">&nbsp;</span>
+ <a class="file tw-font-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a>
+ </li>
+ <li v-if="store.isIncomplete" class="tw-pt-1">
+ <span class="file tw-flex tw-items-center tw-justify-between">{{ store.tooManyFilesMessage }}
+ <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
+ </span>
+ </li>
+ </ol>
+</template>
diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
new file mode 100644
index 0000000..cddfee1
--- /dev/null
+++ b/web_src/js/components/DiffFileTree.vue
@@ -0,0 +1,144 @@
+<script>
+import DiffFileTreeItem from './DiffFileTreeItem.vue';
+import {loadMoreFiles} from '../features/repo-diff.js';
+import {toggleElem} from '../utils/dom.js';
+import {diffTreeStore} from '../modules/stores.js';
+import {setFileFolding} from '../features/file-fold.js';
+
+const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
+
+export default {
+ components: {DiffFileTreeItem},
+ data: () => {
+ return {store: diffTreeStore()};
+ },
+ computed: {
+ fileTree() {
+ const result = [];
+ for (const file of this.store.files) {
+ // Split file into directories
+ const splits = file.Name.split('/');
+ let index = 0;
+ let parent = null;
+ let isFile = false;
+ for (const split of splits) {
+ index += 1;
+ // reached the end
+ if (index === splits.length) {
+ isFile = true;
+ }
+ let newParent = {
+ name: split,
+ children: [],
+ isFile,
+ };
+
+ if (isFile === true) {
+ newParent.file = file;
+ }
+
+ if (parent) {
+ // check if the folder already exists
+ const existingFolder = parent.children.find(
+ (x) => x.name === split,
+ );
+ if (existingFolder) {
+ newParent = existingFolder;
+ } else {
+ parent.children.push(newParent);
+ }
+ } else {
+ const existingFolder = result.find((x) => x.name === split);
+ if (existingFolder) {
+ newParent = existingFolder;
+ } else {
+ result.push(newParent);
+ }
+ }
+ parent = newParent;
+ }
+ }
+ const mergeChildIfOnlyOneDir = (entries) => {
+ for (const entry of entries) {
+ if (entry.children) {
+ mergeChildIfOnlyOneDir(entry.children);
+ }
+ if (entry.children.length === 1 && entry.children[0].isFile === false) {
+ // Merge it to the parent
+ entry.name = `${entry.name}/${entry.children[0].name}`;
+ entry.children = entry.children[0].children;
+ }
+ }
+ };
+ // Merge folders with just a folder as children in order to
+ // reduce the depth of our tree.
+ mergeChildIfOnlyOneDir(result);
+ return result;
+ },
+ },
+ mounted() {
+ // Default to true if unset
+ this.store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
+ document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', this.toggleVisibility);
+
+ this.hashChangeListener = () => {
+ this.store.selectedItem = window.location.hash;
+ this.expandSelectedFile();
+ };
+ this.hashChangeListener();
+ window.addEventListener('hashchange', this.hashChangeListener);
+ },
+ unmounted() {
+ document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', this.toggleVisibility);
+ window.removeEventListener('hashchange', this.hashChangeListener);
+ },
+ methods: {
+ expandSelectedFile() {
+ // expand file if the selected file is folded
+ if (this.store.selectedItem) {
+ const box = document.querySelector(this.store.selectedItem);
+ const folded = box?.getAttribute('data-folded') === 'true';
+ if (folded) setFileFolding(box, box.querySelector('.fold-file'), false);
+ }
+ },
+ toggleVisibility() {
+ this.updateVisibility(!this.store.fileTreeIsVisible);
+ },
+ updateVisibility(visible) {
+ this.store.fileTreeIsVisible = visible;
+ localStorage.setItem(LOCAL_STORAGE_KEY, this.store.fileTreeIsVisible);
+ this.updateState(this.store.fileTreeIsVisible);
+ },
+ updateState(visible) {
+ const btn = document.querySelector('.diff-toggle-file-tree-button');
+ const [toShow, toHide] = btn.querySelectorAll('.icon');
+ const tree = document.getElementById('diff-file-tree');
+ const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text');
+ btn.setAttribute('data-tooltip-content', newTooltip);
+ toggleElem(tree, visible);
+ toggleElem(toShow, !visible);
+ toggleElem(toHide, visible);
+ },
+ loadMoreData() {
+ loadMoreFiles(this.store.linkLoadMore);
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
+ <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
+ <DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item"/>
+ <div v-if="store.isIncomplete" class="tw-pt-1">
+ <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a>
+ </div>
+ </div>
+</template>
+<style scoped>
+.diff-file-tree-items {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ margin-right: .5rem;
+}
+</style>
diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue
new file mode 100644
index 0000000..0f6e543
--- /dev/null
+++ b/web_src/js/components/DiffFileTreeItem.vue
@@ -0,0 +1,97 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {diffTreeStore} from '../modules/stores.js';
+
+export default {
+ components: {SvgIcon},
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ },
+ data: () => ({
+ store: diffTreeStore(),
+ collapsed: false,
+ }),
+ methods: {
+ getIconForDiffType(pType) {
+ const diffTypes = {
+ 1: {name: 'octicon-diff-added', classes: ['text', 'green']},
+ 2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']},
+ 3: {name: 'octicon-diff-removed', classes: ['text', 'red']},
+ 4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']},
+ 5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok
+ };
+ return diffTypes[pType];
+ },
+ },
+};
+</script>
+<template>
+ <!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"-->
+ <a
+ v-if="item.isFile" class="item-file"
+ :class="{'selected': store.selectedItem === '#diff-' + item.file.NameHash, 'viewed': item.file.IsViewed}"
+ :title="item.name" :href="'#diff-' + item.file.NameHash"
+ >
+ <!-- file -->
+ <SvgIcon name="octicon-file"/>
+ <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span>
+ <SvgIcon :name="getIconForDiffType(item.file.Type).name" :class="getIconForDiffType(item.file.Type).classes"/>
+ </a>
+ <div v-else class="item-directory" :title="item.name" @click.stop="collapsed = !collapsed">
+ <!-- directory -->
+ <SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
+ <SvgIcon class="text primary" name="octicon-file-directory-fill"/>
+ <span class="gt-ellipsis">{{ item.name }}</span>
+ </div>
+
+ <div v-if="item.children?.length" v-show="!collapsed" class="sub-items">
+ <DiffFileTreeItem v-for="childItem in item.children" :key="childItem.name" :item="childItem"/>
+ </div>
+</template>
+<style scoped>
+a, a:hover {
+ text-decoration: none;
+ color: var(--color-text);
+}
+
+.sub-items {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ margin-left: 13px;
+ border-left: 1px solid var(--color-secondary);
+}
+
+.sub-items .item-file {
+ padding-left: 18px;
+}
+
+.item-file.selected {
+ color: var(--color-text);
+ background: var(--color-active);
+ border-radius: 4px;
+}
+
+.item-file.viewed {
+ color: var(--color-text-light-3);
+}
+
+.item-file,
+.item-directory {
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+ padding: 3px 6px;
+}
+
+.item-file:hover,
+.item-directory:hover {
+ color: var(--color-text);
+ background: var(--color-hover);
+ border-radius: 4px;
+ cursor: pointer;
+}
+</style>
diff --git a/web_src/js/components/PullRequestMergeForm.test.js b/web_src/js/components/PullRequestMergeForm.test.js
new file mode 100644
index 0000000..7b856e0
--- /dev/null
+++ b/web_src/js/components/PullRequestMergeForm.test.js
@@ -0,0 +1,34 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+import {flushPromises, mount} from '@vue/test-utils';
+import PullRequestMergeForm from './PullRequestMergeForm.vue';
+
+async function renderMergeForm(branchName) {
+ window.config.pageData.pullRequestMergeForm = {
+ textDeleteBranch: `Delete branch "${branchName}"`,
+ textDoMerge: 'Merge',
+ defaultMergeStyle: 'merge',
+ isPullBranchDeletable: true,
+ canMergeNow: true,
+ mergeStyles: [{
+ 'name': 'merge',
+ 'allowed': true,
+ 'textDoMerge': 'Merge',
+ 'mergeTitleFieldText': 'Merge PR',
+ 'mergeMessageFieldText': 'Description',
+ 'hideAutoMerge': 'Hide this message',
+ }],
+ };
+ const mergeform = mount(PullRequestMergeForm);
+ mergeform.get('.merge-button').trigger('click');
+ await flushPromises();
+ return mergeform;
+}
+
+test('renders escaped branch name', async () => {
+ let mergeform = await renderMergeForm('<b>evil</b>');
+ expect(mergeform.get('label[for="delete-branch-after-merge"]').text()).toBe('Delete branch "<b>evil</b>"');
+
+ mergeform = await renderMergeForm('<script class="evil">alert("evil message");</script>');
+ expect(mergeform.get('label[for="delete-branch-after-merge"]').text()).toBe('Delete branch "<script class="evil">alert("evil message");</script>"');
+});
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
new file mode 100644
index 0000000..bd0901a
--- /dev/null
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -0,0 +1,252 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {toggleElem} from '../utils/dom.js';
+
+const {csrfToken, pageData} = window.config;
+
+export default {
+ components: {SvgIcon},
+ data: () => ({
+ csrfToken,
+ mergeForm: pageData.pullRequestMergeForm,
+
+ mergeTitleFieldValue: '',
+ mergeMessageFieldValue: '',
+ deleteBranchAfterMerge: false,
+ autoMergeWhenSucceed: false,
+
+ mergeStyle: '',
+ mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles
+ hideMergeMessageTexts: false,
+ textDoMerge: '',
+ mergeTitleFieldText: '',
+ mergeMessageFieldText: '',
+ hideAutoMerge: false,
+ },
+ mergeStyleAllowedCount: 0,
+
+ showMergeStyleMenu: false,
+ showActionForm: false,
+ }),
+ computed: {
+ mergeButtonStyleClass() {
+ if (this.mergeForm.allOverridableChecksOk) return 'primary';
+ return this.autoMergeWhenSucceed ? 'primary' : 'red';
+ },
+ forceMerge() {
+ return this.mergeForm.canMergeNow && !this.mergeForm.allOverridableChecksOk;
+ },
+ },
+ watch: {
+ mergeStyle(val) {
+ this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val);
+ for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
+ toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
+ }
+ },
+ },
+ created() {
+ this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
+
+ let mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed && e.name === this.mergeForm.defaultMergeStyle)?.name;
+ if (!mergeStyle) mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name;
+ this.switchMergeStyle(mergeStyle, !this.mergeForm.canMergeNow);
+ },
+ mounted() {
+ document.addEventListener('mouseup', this.hideMergeStyleMenu);
+ },
+ unmounted() {
+ document.removeEventListener('mouseup', this.hideMergeStyleMenu);
+ },
+ methods: {
+ hideMergeStyleMenu() {
+ this.showMergeStyleMenu = false;
+ },
+ toggleActionForm(show) {
+ this.showActionForm = show;
+ if (!show) return;
+ this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge;
+ this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText;
+ this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText;
+ },
+ switchMergeStyle(name, autoMerge = false) {
+ this.mergeStyle = name;
+ this.autoMergeWhenSucceed = autoMerge;
+ },
+ clearMergeMessage() {
+ this.mergeMessageFieldValue = this.mergeForm.defaultMergeMessage;
+ },
+ },
+};
+</script>
+<template>
+ <!--
+ if this component is shown, either the user is an admin (can do a merge without checks), or they are a writer who has the permission to do a merge
+ if the user is a writer and can't do a merge now (canMergeNow==false), then only show the Auto Merge for them
+ How to test the UI manually:
+ * Method 1: manually set some variables in pull.tmpl, eg: {{$notAllOverridableChecksOk = true}} {{$canMergeNow = false}}
+ * Method 2: make a protected branch, then set state=pending/success :
+ curl -X POST ${root_url}/api/v1/repos/${owner}/${repo}/statuses/${sha} \
+ -H "accept: application/json" -H "authorization: Basic $base64_auth" -H "Content-Type: application/json" \
+ -d '{"context": "test/context", "description": "description", "state": "${state}", "target_url": "http://localhost"}'
+ -->
+ <div>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"/>
+
+ <!-- another similar form is in pull.tmpl (manual merge)-->
+ <form class="ui form form-fetch-action" v-if="showActionForm" :action="mergeForm.baseLink+'/merge'" method="post">
+ <input type="hidden" name="_csrf" :value="csrfToken">
+ <input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID">
+ <input type="hidden" name="merge_when_checks_succeed" v-model="autoMergeWhenSucceed">
+ <input type="hidden" name="force_merge" v-model="forceMerge">
+
+ <template v-if="!mergeStyleDetail.hideMergeMessageTexts">
+ <div class="field">
+ <input type="text" name="merge_title_field" v-model="mergeTitleFieldValue">
+ </div>
+ <div class="field">
+ <textarea name="merge_message_field" rows="5" :placeholder="mergeForm.mergeMessageFieldPlaceHolder" v-model="mergeMessageFieldValue"/>
+ <template v-if="mergeMessageFieldValue !== mergeForm.defaultMergeMessage">
+ <button @click.prevent="clearMergeMessage" class="btn tw-mt-1 tw-p-1 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint">
+ {{ mergeForm.textClearMergeMessage }}
+ </button>
+ </template>
+ </div>
+ </template>
+
+ <div class="field" v-if="mergeStyle === 'manually-merged'">
+ <input type="text" name="merge_commit_id" :placeholder="mergeForm.textMergeCommitId">
+ </div>
+
+ <button class="ui button" :class="mergeButtonStyleClass" type="submit" name="do" :value="mergeStyle">
+ {{ mergeStyleDetail.textDoMerge }}
+ <template v-if="autoMergeWhenSucceed">
+ {{ mergeForm.textAutoMergeButtonWhenSucceed }}
+ </template>
+ </button>
+
+ <button class="ui button merge-cancel" @click="toggleActionForm(false)">
+ {{ mergeForm.textCancel }}
+ </button>
+
+ <div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
+ <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
+ <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
+ </div>
+ </form>
+
+ <div v-if="!showActionForm" class="tw-flex">
+ <!-- the merge button -->
+ <div class="ui buttons merge-button" :class="[mergeForm.emptyCommit ? 'grey' : mergeForm.allOverridableChecksOk ? 'primary' : 'red']" @click="toggleActionForm(true)">
+ <button class="ui button">
+ <svg-icon name="octicon-git-merge"/>
+ <span class="button-text">
+ {{ mergeStyleDetail.textDoMerge }}
+ <template v-if="autoMergeWhenSucceed">
+ {{ mergeForm.textAutoMergeButtonWhenSucceed }}
+ </template>
+ </span>
+ </button>
+ <div class="ui dropdown icon button" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1">
+ <svg-icon name="octicon-triangle-down" :size="14"/>
+ <div class="menu" :class="{'show':showMergeStyleMenu}">
+ <template v-for="msd in mergeForm.mergeStyles">
+ <!-- if can merge now, show one action "merge now", and an action "auto merge when succeed" -->
+ <div class="item" v-if="msd.allowed && mergeForm.canMergeNow" :key="msd.name" @click.stop="switchMergeStyle(msd.name)">
+ <div class="action-text">
+ {{ msd.textDoMerge }}
+ </div>
+ <div v-if="!msd.hideAutoMerge" class="auto-merge-small" @click.stop="switchMergeStyle(msd.name, true)">
+ <svg-icon name="octicon-clock" :size="14"/>
+ <div class="auto-merge-tip">
+ {{ mergeForm.textAutoMergeWhenSucceed }}
+ </div>
+ </div>
+ </div>
+
+ <!-- if can NOT merge now, only show one action "auto merge when succeed" -->
+ <div class="item" v-if="msd.allowed && !mergeForm.canMergeNow && !msd.hideAutoMerge" :key="msd.name" @click.stop="switchMergeStyle(msd.name, true)">
+ <div class="action-text">
+ {{ msd.textDoMerge }} {{ mergeForm.textAutoMergeButtonWhenSucceed }}
+ </div>
+ </div>
+ </template>
+ </div>
+ </div>
+ </div>
+
+ <!-- the cancel auto merge button -->
+ <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="tw-ml-4">
+ <input type="hidden" name="_csrf" :value="csrfToken">
+ <button class="ui button">
+ {{ mergeForm.textAutoMergeCancelSchedule }}
+ </button>
+ </form>
+ </div>
+ </div>
+</template>
+<style scoped>
+/* to keep UI the same, at the moment we are still using some Fomantic UI styles, but we do not use their scripts, so we need to fine tune some styles */
+.ui.dropdown .menu.show {
+ display: block;
+}
+.ui.checkbox label {
+ cursor: pointer;
+}
+
+/* make the dropdown list left-aligned */
+.ui.merge-button {
+ position: relative;
+}
+.ui.merge-button .ui.dropdown {
+ position: static;
+}
+.ui.merge-button > .ui.dropdown:last-child > .menu:not(.left) {
+ left: 0;
+ right: auto;
+}
+.ui.merge-button .ui.dropdown .menu > .item {
+ display: flex;
+ align-items: stretch;
+ padding: 0 !important; /* polluted by semantic.css: .ui.dropdown .menu > .item { !important } */
+}
+
+/* merge style list item */
+.action-text {
+ padding: 0.8rem;
+ flex: 1
+}
+
+.auto-merge-small {
+ width: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+.auto-merge-small .auto-merge-tip {
+ display: none;
+ left: 38px;
+ top: -1px;
+ bottom: -1px;
+ position: absolute;
+ align-items: center;
+ color: var(--color-info-text);
+ background-color: var(--color-info-bg);
+ border: 1px solid var(--color-info-border);
+ border-left: none;
+ padding-right: 1rem;
+}
+
+.auto-merge-small:hover {
+ color: var(--color-info-text);
+ background-color: var(--color-info-bg);
+ border: 1px solid var(--color-info-border);
+}
+
+.auto-merge-small:hover .auto-merge-tip {
+ display: flex;
+}
+
+</style>
diff --git a/web_src/js/components/RepoActionView.test.js b/web_src/js/components/RepoActionView.test.js
new file mode 100644
index 0000000..8c4e150
--- /dev/null
+++ b/web_src/js/components/RepoActionView.test.js
@@ -0,0 +1,105 @@
+import {mount, flushPromises} from '@vue/test-utils';
+import RepoActionView from './RepoActionView.vue';
+
+test('processes ##[group] and ##[endgroup]', async () => {
+ Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
+ vi.spyOn(global, 'fetch').mockImplementation((url, opts) => {
+ const artifacts_value = {
+ artifacts: [],
+ };
+ const stepsLog_value = [
+ {
+ step: 0,
+ cursor: 0,
+ lines: [
+ {index: 1, message: '##[group]Test group', timestamp: 0},
+ {index: 2, message: 'A test line', timestamp: 0},
+ {index: 3, message: '##[endgroup]', timestamp: 0},
+ {index: 4, message: 'A line outside the group', timestamp: 0},
+ ],
+ },
+ ];
+ const jobs_value = {
+ state: {
+ run: {
+ status: 'success',
+ commit: {
+ pusher: {},
+ },
+ },
+ currentJob: {
+ steps: [
+ {
+ summary: 'Test Job',
+ duration: '1s',
+ status: 'success',
+ },
+ ],
+ },
+ },
+ logs: {
+ stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [],
+ },
+ };
+
+ return Promise.resolve({
+ ok: true,
+ json: vi.fn().mockResolvedValue(
+ url.endsWith('/artifacts') ? artifacts_value : jobs_value,
+ ),
+ });
+ });
+
+ const wrapper = mount(RepoActionView, {
+ props: {
+ jobIndex: '1',
+ locale: {
+ approve: '',
+ cancel: '',
+ rerun: '',
+ artifactsTitle: '',
+ areYouSure: '',
+ confirmDeleteArtifact: '',
+ rerun_all: '',
+ showTimeStamps: '',
+ showLogSeconds: '',
+ showFullScreen: '',
+ downloadLogs: '',
+ status: {
+ unknown: '',
+ waiting: '',
+ running: '',
+ success: '',
+ failure: '',
+ cancelled: '',
+ skipped: '',
+ blocked: '',
+ },
+ },
+ },
+ });
+ await flushPromises();
+ await wrapper.get('.job-step-summary').trigger('click');
+ await flushPromises();
+
+ // Test if header was loaded correctly
+ expect(wrapper.get('.step-summary-msg').text()).toEqual('Test Job');
+
+ // Check if 3 lines where rendered
+ expect(wrapper.findAll('.job-log-line').length).toEqual(3);
+
+ // Check if line 1 contains the group header
+ expect(wrapper.get('.job-log-line:nth-of-type(1) > details.log-msg').text()).toEqual('Test group');
+
+ // Check if right after the header line exists a log list
+ expect(wrapper.find('.job-log-line:nth-of-type(1) + .job-log-list.hidden').exists()).toBe(true);
+
+ // Check if inside the loglist exist exactly one log line
+ expect(wrapper.findAll('.job-log-list > .job-log-line').length).toEqual(1);
+
+ // Check if inside the loglist is an logline with our second logline
+ expect(wrapper.get('.job-log-list > .job-log-line > .log-msg').text()).toEqual('A test line');
+
+ // Check if after the log list exists another log line
+ expect(wrapper.get('.job-log-list + .job-log-line > .log-msg').text()).toEqual('A line outside the group');
+});
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
new file mode 100644
index 0000000..4e8e18e
--- /dev/null
+++ b/web_src/js/components/RepoActionView.vue
@@ -0,0 +1,905 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import ActionRunStatus from './ActionRunStatus.vue';
+import {createApp} from 'vue';
+import {toggleElem} from '../utils/dom.js';
+import {formatDatetime} from '../utils/time.js';
+import {renderAnsi} from '../render/ansi.js';
+import {GET, POST, DELETE} from '../modules/fetch.js';
+
+const sfc = {
+ name: 'RepoActionView',
+ components: {
+ SvgIcon,
+ ActionRunStatus,
+ },
+ props: {
+ runIndex: String,
+ jobIndex: String,
+ actionsURL: String,
+ workflowName: String,
+ workflowURL: String,
+ locale: Object,
+ },
+
+ data() {
+ return {
+ // internal state
+ loading: false,
+ intervalID: null,
+ currentJobStepsStates: [],
+ artifacts: [],
+ onHoverRerunIndex: -1,
+ menuVisible: false,
+ isFullScreen: false,
+ timeVisible: {
+ 'log-time-stamp': false,
+ 'log-time-seconds': false,
+ },
+
+ // provided by backend
+ run: {
+ link: '',
+ title: '',
+ status: '',
+ canCancel: false,
+ canApprove: false,
+ canRerun: false,
+ done: false,
+ jobs: [
+ // {
+ // id: 0,
+ // name: '',
+ // status: '',
+ // canRerun: false,
+ // duration: '',
+ // },
+ ],
+ commit: {
+ localeCommit: '',
+ localePushedBy: '',
+ localeWorkflow: '',
+ shortSHA: '',
+ link: '',
+ pusher: {
+ displayName: '',
+ link: '',
+ },
+ branch: {
+ name: '',
+ link: '',
+ },
+ },
+ },
+ currentJob: {
+ title: '',
+ detail: '',
+ steps: [
+ // {
+ // summary: '',
+ // duration: '',
+ // status: '',
+ // }
+ ],
+ },
+ };
+ },
+
+ async mounted() {
+ // load job data and then auto-reload periodically
+ // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
+ await this.loadJob();
+ this.intervalID = setInterval(this.loadJob, 1000);
+ document.body.addEventListener('click', this.closeDropdown);
+ this.hashChangeListener();
+ window.addEventListener('hashchange', this.hashChangeListener);
+ },
+
+ beforeUnmount() {
+ document.body.removeEventListener('click', this.closeDropdown);
+ window.removeEventListener('hashchange', this.hashChangeListener);
+ },
+
+ unmounted() {
+ // clear the interval timer when the component is unmounted
+ // even our page is rendered once, not spa style
+ if (this.intervalID) {
+ clearInterval(this.intervalID);
+ this.intervalID = null;
+ }
+ },
+
+ methods: {
+ // show/hide the step logs for a step
+ toggleStepLogs(idx) {
+ this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded;
+ if (this.currentJobStepsStates[idx].expanded) {
+ this.loadJob(); // try to load the data immediately instead of waiting for next timer interval
+ }
+ },
+ // cancel a run
+ cancelRun() {
+ POST(`${this.run.link}/cancel`);
+ },
+ // approve a run
+ approveRun() {
+ POST(`${this.run.link}/approve`);
+ },
+ // show/hide the step logs for a group
+ toggleGroupLogs(event) {
+ const line = event.target.parentElement;
+ const list = line.nextSibling;
+ if (event.newState === 'open') {
+ list.classList.remove('hidden');
+ } else {
+ list.classList.add('hidden');
+ }
+ },
+
+ createLogLine(line, startTime, stepIndex, group) {
+ const div = document.createElement('div');
+ div.classList.add('job-log-line');
+ div.setAttribute('id', `jobstep-${stepIndex}-${line.index}`);
+ div._jobLogTime = line.timestamp;
+
+ const lineNumber = document.createElement('a');
+ lineNumber.classList.add('line-num', 'muted');
+ lineNumber.textContent = line.index;
+ lineNumber.setAttribute('href', `#jobstep-${stepIndex}-${line.index}`);
+ div.append(lineNumber);
+
+ // for "Show timestamps"
+ const logTimeStamp = document.createElement('span');
+ logTimeStamp.className = 'log-time-stamp';
+ const date = new Date(parseFloat(line.timestamp * 1000));
+ const timeStamp = formatDatetime(date);
+ logTimeStamp.textContent = timeStamp;
+ toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']);
+ // for "Show seconds"
+ const logTimeSeconds = document.createElement('span');
+ logTimeSeconds.className = 'log-time-seconds';
+ const seconds = Math.floor(parseFloat(line.timestamp) - parseFloat(startTime));
+ logTimeSeconds.textContent = `${seconds}s`;
+ toggleElem(logTimeSeconds, this.timeVisible['log-time-seconds']);
+
+ let logMessage = document.createElement('span');
+ logMessage.innerHTML = renderAnsi(line.message);
+ if (group.isHeader) {
+ const details = document.createElement('details');
+ details.addEventListener('toggle', this.toggleGroupLogs);
+ const summary = document.createElement('summary');
+ summary.append(logMessage);
+ details.append(summary);
+ logMessage = details;
+ }
+ logMessage.className = 'log-msg';
+ logMessage.style.paddingLeft = `${group.depth}em`;
+
+ div.append(logTimeStamp);
+ div.append(logMessage);
+ div.append(logTimeSeconds);
+
+ return div;
+ },
+
+ appendLogs(stepIndex, logLines, startTime) {
+ const groupStack = [];
+ const container = this.$refs.logs[stepIndex];
+ for (const line of logLines) {
+ const el = groupStack.length > 0 ? groupStack[groupStack.length - 1] : container;
+ const group = {
+ depth: groupStack.length,
+ isHeader: false,
+ };
+ if (line.message.startsWith('##[group]')) {
+ group.isHeader = true;
+
+ const logLine = this.createLogLine(
+ {
+ ...line,
+ message: line.message.substring(9),
+ },
+ startTime, stepIndex, group,
+ );
+ logLine.setAttribute('data-group', group.index);
+ el.append(logLine);
+
+ const list = document.createElement('div');
+ list.classList.add('job-log-list');
+ list.classList.add('hidden');
+ list.setAttribute('data-group', group.index);
+ groupStack.push(list);
+ el.append(list);
+ } else if (line.message.startsWith('##[endgroup]')) {
+ groupStack.pop();
+ } else {
+ el.append(this.createLogLine(line, startTime, stepIndex, group));
+ }
+ }
+ },
+
+ async fetchArtifacts() {
+ const resp = await GET(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
+ return await resp.json();
+ },
+
+ async deleteArtifact(name) {
+ if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return;
+ await DELETE(`${this.run.link}/artifacts/${name}`);
+ await this.loadJob();
+ },
+
+ async fetchJob() {
+ const logCursors = this.currentJobStepsStates.map((it, idx) => {
+ // cursor is used to indicate the last position of the logs
+ // it's only used by backend, frontend just reads it and passes it back, it and can be any type.
+ // for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
+ return {step: idx, cursor: it.cursor, expanded: it.expanded};
+ });
+ const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, {
+ data: {logCursors},
+ });
+ return await resp.json();
+ },
+
+ async loadJob() {
+ if (this.loading) return;
+ try {
+ this.loading = true;
+
+ let job, artifacts;
+ try {
+ [job, artifacts] = await Promise.all([
+ this.fetchJob(),
+ this.fetchArtifacts(), // refresh artifacts if upload-artifact step done
+ ]);
+ } catch (err) {
+ if (err instanceof TypeError) return; // avoid network error while unloading page
+ throw err;
+ }
+
+ this.artifacts = artifacts['artifacts'] || [];
+
+ // save the state to Vue data, then the UI will be updated
+ this.run = job.state.run;
+ this.currentJob = job.state.currentJob;
+
+ // sync the currentJobStepsStates to store the job step states
+ for (let i = 0; i < this.currentJob.steps.length; i++) {
+ if (!this.currentJobStepsStates[i]) {
+ // initial states for job steps
+ this.currentJobStepsStates[i] = {cursor: null, expanded: false};
+ }
+ }
+ // append logs to the UI
+ for (const logs of job.logs.stepsLog) {
+ // save the cursor, it will be passed to backend next time
+ this.currentJobStepsStates[logs.step].cursor = logs.cursor;
+ this.appendLogs(logs.step, logs.lines, logs.started);
+ }
+
+ if (this.run.done && this.intervalID) {
+ clearInterval(this.intervalID);
+ this.intervalID = null;
+ }
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ isDone(status) {
+ return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
+ },
+
+ isExpandable(status) {
+ return ['success', 'running', 'failure', 'cancelled'].includes(status);
+ },
+
+ closeDropdown() {
+ if (this.menuVisible) this.menuVisible = false;
+ },
+
+ toggleTimeDisplay(type) {
+ this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`];
+ for (const el of this.$refs.steps.querySelectorAll(`.log-time-${type}`)) {
+ toggleElem(el, this.timeVisible[`log-time-${type}`]);
+ }
+ },
+
+ toggleFullScreen() {
+ this.isFullScreen = !this.isFullScreen;
+ const fullScreenEl = document.querySelector('.action-view-right');
+ const outerEl = document.querySelector('.full.height');
+ const actionBodyEl = document.querySelector('.action-view-body');
+ const headerEl = document.querySelector('#navbar');
+ const contentEl = document.querySelector('.page-content.repository');
+ const footerEl = document.querySelector('.page-footer');
+ toggleElem(headerEl, !this.isFullScreen);
+ toggleElem(contentEl, !this.isFullScreen);
+ toggleElem(footerEl, !this.isFullScreen);
+ // move .action-view-right to new parent
+ if (this.isFullScreen) {
+ outerEl.append(fullScreenEl);
+ } else {
+ actionBodyEl.append(fullScreenEl);
+ }
+ },
+ async hashChangeListener() {
+ const selectedLogStep = window.location.hash;
+ if (!selectedLogStep) return;
+ const [_, step, _line] = selectedLogStep.split('-');
+ if (!this.currentJobStepsStates[step]) return;
+ if (!this.currentJobStepsStates[step].expanded && this.currentJobStepsStates[step].cursor === null) {
+ this.currentJobStepsStates[step].expanded = true;
+ // need to await for load job if the step log is loaded for the first time
+ // so logline can be selected by querySelector
+ await this.loadJob();
+ }
+ const logLine = this.$refs.steps.querySelector(selectedLogStep);
+ if (!logLine) return;
+ logLine.querySelector('.line-num').click();
+ },
+ },
+};
+
+export default sfc;
+
+export function initRepositoryActionView() {
+ const el = document.getElementById('repo-action-view');
+ if (!el) return;
+
+ // TODO: the parent element's full height doesn't work well now,
+ // but we can not pollute the global style at the moment, only fix the height problem for pages with this component
+ const parentFullHeight = document.querySelector('body > div.full.height');
+ if (parentFullHeight) parentFullHeight.style.paddingBottom = '0';
+
+ const view = createApp(sfc, {
+ runIndex: el.getAttribute('data-run-index'),
+ jobIndex: el.getAttribute('data-job-index'),
+ actionsURL: el.getAttribute('data-actions-url'),
+ workflowName: el.getAttribute('data-workflow-name'),
+ workflowURL: el.getAttribute('data-workflow-url'),
+ locale: {
+ approve: el.getAttribute('data-locale-approve'),
+ cancel: el.getAttribute('data-locale-cancel'),
+ rerun: el.getAttribute('data-locale-rerun'),
+ artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
+ areYouSure: el.getAttribute('data-locale-are-you-sure'),
+ confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
+ rerun_all: el.getAttribute('data-locale-rerun-all'),
+ showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
+ showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
+ showFullScreen: el.getAttribute('data-locale-show-full-screen'),
+ downloadLogs: el.getAttribute('data-locale-download-logs'),
+ status: {
+ unknown: el.getAttribute('data-locale-status-unknown'),
+ waiting: el.getAttribute('data-locale-status-waiting'),
+ running: el.getAttribute('data-locale-status-running'),
+ success: el.getAttribute('data-locale-status-success'),
+ failure: el.getAttribute('data-locale-status-failure'),
+ cancelled: el.getAttribute('data-locale-status-cancelled'),
+ skipped: el.getAttribute('data-locale-status-skipped'),
+ blocked: el.getAttribute('data-locale-status-blocked'),
+ },
+ },
+ });
+ view.mount(el);
+}
+</script>
+<template>
+ <div class="ui container action-view-container">
+ <div class="action-view-header">
+ <div class="action-info-summary">
+ <div class="action-info-summary-title">
+ <ActionRunStatus :locale-status="locale.status[run.status]" :status="run.status" :size="20"/>
+ <h2 class="action-info-summary-title-text">
+ {{ run.title }}
+ </h2>
+ </div>
+ <button class="ui basic small compact button primary" @click="approveRun()" v-if="run.canApprove">
+ {{ locale.approve }}
+ </button>
+ <button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
+ {{ locale.cancel }}
+ </button>
+ <button class="ui basic small compact button tw-mr-0 tw-whitespace-nowrap link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
+ {{ locale.rerun_all }}
+ </button>
+ </div>
+ <div class="action-summary">
+ {{ run.commit.localeCommit }}
+ <a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
+ {{ run.commit.localePushedBy }}
+ <a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
+ <span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
+ <a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a>
+ </span>
+ </div>
+ <div class="action-summary">
+ {{ run.commit.localeWorkflow }}
+ <a class="muted" :href="workflowURL">{{ workflowName }}</a>
+ </div>
+ </div>
+ <div class="action-view-body">
+ <div class="action-view-left">
+ <div class="job-group-section">
+ <div class="job-brief-list">
+ <a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="parseInt(jobIndex) === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id" @mouseenter="onHoverRerunIndex = job.id" @mouseleave="onHoverRerunIndex = -1">
+ <div class="job-brief-item-left">
+ <ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
+ <span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
+ </div>
+ <span class="job-brief-item-right">
+ <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/>
+ <span class="step-summary-duration">{{ job.duration }}</span>
+ </span>
+ </a>
+ </div>
+ </div>
+ <div class="job-artifacts" v-if="artifacts.length > 0">
+ <div class="job-artifacts-title">
+ {{ locale.artifactsTitle }}
+ </div>
+ <ul class="job-artifacts-list">
+ <li class="job-artifacts-item" v-for="artifact in artifacts" :key="artifact.name">
+ <a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
+ <SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }}
+ </a>
+ <a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete">
+ <SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="action-view-right">
+ <div class="job-info-header">
+ <div class="job-info-header-left gt-ellipsis">
+ <h3 class="job-info-header-title gt-ellipsis">
+ {{ currentJob.title }}
+ </h3>
+ <p class="job-info-header-detail">
+ {{ currentJob.detail }}
+ </p>
+ </div>
+ <div class="job-info-header-right">
+ <div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible">
+ <button class="btn gt-interact-bg tw-p-2">
+ <SvgIcon name="octicon-gear" :size="18"/>
+ </button>
+ <div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
+ <a class="item" tabindex="0" @click="toggleTimeDisplay('seconds')" @keyup.space="toggleTimeDisplay('seconds')" @keyup.enter="toggleTimeDisplay('seconds')">
+ <i class="icon"><SvgIcon :name="timeVisible['log-time-seconds'] ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
+ {{ locale.showLogSeconds }}
+ </a>
+ <a class="item" tabindex="0" @click="toggleTimeDisplay('stamp')" @keyup.space="toggleTimeDisplay('stamp')" @keyup.enter="toggleTimeDisplay('stamp')">
+ <i class="icon"><SvgIcon :name="timeVisible['log-time-stamp'] ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
+ {{ locale.showTimeStamps }}
+ </a>
+ <a class="item" tabindex="0" @click="toggleFullScreen()" @keyup.space="toggleFullScreen()" @keyup.enter="toggleFullScreen()">
+ <i class="icon"><SvgIcon :name="isFullScreen ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
+ {{ locale.showFullScreen }}
+ </a>
+ <div class="divider"/>
+ <a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
+ <i class="icon"><SvgIcon name="octicon-download"/></i>
+ {{ locale.downloadLogs }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
+ <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
+ <div class="job-step-summary" tabindex="0" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" @keyup.enter.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" @keyup.space.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
+ <!-- If the job is done and the job step log is loaded for the first time, show the loading icon
+ currentJobStepsStates[i].cursor === null means the log is loaded for the first time
+ -->
+ <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+ <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"/>
+ <ActionRunStatus :status="jobStep.status" class="tw-mr-2"/>
+
+ <span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span>
+ <span class="step-summary-duration">{{ jobStep.duration }}</span>
+ </div>
+
+ <!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM,
+ use native DOM elements for "log line" to improve performance, Vue is not suitable for managing so many reactive elements. -->
+ <div class="job-step-logs" ref="logs" v-show="currentJobStepsStates[i].expanded"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<style scoped>
+.action-view-body {
+ padding-top: 12px;
+ padding-bottom: 12px;
+ display: flex;
+ gap: 12px;
+}
+
+/* ================ */
+/* action view header */
+
+.action-view-header {
+ margin-top: 8px;
+}
+
+.action-info-summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.action-info-summary-title {
+ display: flex;
+}
+
+.action-info-summary-title-text {
+ font-size: 20px;
+ margin: 0 0 0 8px;
+ flex: 1;
+ overflow-wrap: anywhere;
+}
+
+.action-summary {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin-left: 28px;
+}
+
+@media (max-width: 767.98px) {
+ .action-commit-summary {
+ margin-left: 0;
+ margin-top: 8px;
+ }
+}
+
+/* ================ */
+/* action view left */
+
+.action-view-left {
+ width: 30%;
+ max-width: 400px;
+ position: sticky;
+ top: 12px;
+ max-height: 100vh;
+ overflow-y: auto;
+ background: var(--color-body);
+ z-index: 2; /* above .job-info-header */
+}
+
+@media (max-width: 767.98px) {
+ .action-view-left {
+ position: static; /* can not sticky because multiple jobs would overlap into right view */
+ }
+}
+
+.job-artifacts-title {
+ font-size: 18px;
+ margin-top: 16px;
+ padding: 16px 10px 0 20px;
+ border-top: 1px solid var(--color-secondary);
+}
+
+.job-artifacts-item {
+ margin: 5px 0;
+ padding: 6px;
+ display: flex;
+ justify-content: space-between;
+}
+
+.job-artifacts-list {
+ padding-left: 12px;
+ list-style: none;
+}
+
+.job-artifacts-icon {
+ padding-right: 3px;
+}
+
+.job-brief-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.job-brief-item {
+ padding: 10px;
+ border-radius: var(--border-radius);
+ text-decoration: none;
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+ color: var(--color-text);
+}
+
+.job-brief-item:hover {
+ background-color: var(--color-hover);
+}
+
+.job-brief-item.selected {
+ font-weight: var(--font-weight-bold);
+ background-color: var(--color-active);
+}
+
+.job-brief-item:first-of-type {
+ margin-top: 0;
+}
+
+.job-brief-item .job-brief-rerun {
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.job-brief-item .job-brief-rerun:hover {
+ transform: scale(130%);
+}
+
+.job-brief-item .job-brief-item-left {
+ display: flex;
+ width: 100%;
+ min-width: 0;
+}
+
+.job-brief-item .job-brief-item-left span {
+ display: flex;
+ align-items: center;
+}
+
+.job-brief-item .job-brief-item-left .job-brief-name {
+ display: block;
+ width: 70%;
+}
+
+.job-brief-item .job-brief-item-right {
+ display: flex;
+ align-items: center;
+}
+
+/* ================ */
+/* action view right */
+
+.action-view-right {
+ flex: 1;
+ color: var(--color-console-fg-subtle);
+ max-height: 100%;
+ width: 70%;
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--color-console-border);
+ border-radius: var(--border-radius);
+ background: var(--color-console-bg);
+ align-self: flex-start;
+}
+
+/* begin fomantic button overrides */
+
+.action-view-right .ui.button,
+.action-view-right .ui.button:focus {
+ background: transparent;
+ color: var(--color-console-fg-subtle);
+}
+
+.action-view-right .ui.button:hover {
+ background: var(--color-console-hover-bg);
+ color: var(--color-console-fg);
+}
+
+.action-view-right .ui.button:active {
+ background: var(--color-console-active-bg);
+ color: var(--color-console-fg);
+}
+
+/* end fomantic button overrides */
+
+/* begin fomantic dropdown menu overrides */
+
+.action-view-right .ui.dropdown .menu {
+ background: var(--color-console-menu-bg);
+ border-color: var(--color-console-menu-border);
+}
+
+.action-view-right .ui.dropdown .menu > .item {
+ color: var(--color-console-fg);
+}
+
+.action-view-right .ui.dropdown .menu > .item:hover {
+ color: var(--color-console-fg);
+ background: var(--color-console-hover-bg);
+}
+
+.action-view-right .ui.dropdown .menu > .item:active {
+ color: var(--color-console-fg);
+ background: var(--color-console-active-bg);
+}
+
+.action-view-right .ui.dropdown .menu > .divider {
+ border-top-color: var(--color-console-menu-border);
+}
+
+.action-view-right .ui.pointing.dropdown > .menu:not(.hidden)::after {
+ background: var(--color-console-menu-bg);
+ box-shadow: -1px -1px 0 0 var(--color-console-menu-border);
+}
+
+/* end fomantic dropdown menu overrides */
+
+.job-info-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 12px;
+ position: sticky;
+ top: 0;
+ height: 60px;
+ z-index: 1; /* above .job-step-container */
+ background: var(--color-console-bg);
+ border-radius: 3px;
+}
+
+.job-info-header:has(+ .job-step-container) {
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+.job-info-header .job-info-header-title {
+ color: var(--color-console-fg);
+ font-size: 16px;
+ margin: 0;
+}
+
+.job-info-header .job-info-header-detail {
+ color: var(--color-console-fg-subtle);
+ font-size: 12px;
+}
+
+.job-info-header-left {
+ flex: 1;
+}
+
+.job-step-container {
+ max-height: 100%;
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
+ border-top: 1px solid var(--color-console-border);
+ z-index: 0;
+}
+
+.job-step-container .job-step-summary {
+ padding: 5px 10px;
+ display: flex;
+ align-items: center;
+ border-radius: var(--border-radius);
+}
+
+.job-step-container .job-step-summary.step-expandable {
+ cursor: pointer;
+}
+
+.job-step-container .job-step-summary.step-expandable:hover {
+ color: var(--color-console-fg);
+ background: var(--color-console-hover-bg);
+}
+
+.job-step-container .job-step-summary .step-summary-msg {
+ flex: 1;
+}
+
+.job-step-container .job-step-summary .step-summary-duration {
+ margin-left: 16px;
+}
+
+.job-step-container .job-step-summary.selected {
+ color: var(--color-console-fg);
+ background-color: var(--color-console-active-bg);
+ position: sticky;
+ top: 60px;
+}
+
+@media (max-width: 767.98px) {
+ .action-view-body {
+ flex-direction: column;
+ }
+ .action-view-left, .action-view-right {
+ width: 100%;
+ }
+ .action-view-left {
+ max-width: none;
+ }
+}
+</style>
+
+<style>
+/* some elements are not managed by vue, so we need to use global style */
+.job-status-rotate {
+ animation: job-status-rotate-keyframes 1s linear infinite;
+}
+
+@keyframes job-status-rotate-keyframes {
+ 100% {
+ transform: rotate(-360deg);
+ }
+}
+
+.job-step-section {
+ margin: 10px;
+}
+
+.job-step-section .job-step-logs {
+ font-family: var(--fonts-monospace);
+ margin: 8px 0;
+ font-size: 12px;
+}
+
+.job-step-section .job-step-logs .job-log-line {
+ display: flex;
+}
+
+.job-log-line:hover,
+.job-log-line:target {
+ background-color: var(--color-console-hover-bg);
+}
+
+.job-log-line:target {
+ scroll-margin-top: 95px;
+}
+
+/* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */
+.job-log-line .line-num, .log-time-seconds {
+ width: 48px;
+ color: var(--color-text-light-3);
+ text-align: right;
+ user-select: none;
+}
+
+.job-log-line:target > .line-num {
+ color: var(--color-primary);
+ text-decoration: underline;
+}
+
+.log-time-seconds {
+ padding-right: 2px;
+}
+
+.job-log-line .log-time,
+.log-time-stamp {
+ color: var(--color-text-light-3);
+ margin-left: 10px;
+ white-space: nowrap;
+}
+
+.job-step-section .job-step-logs .job-log-line .log-msg {
+ flex: 1;
+ word-break: break-all;
+ white-space: break-spaces;
+ margin-left: 10px;
+ overflow-wrap: anywhere;
+}
+
+/* selectors here are intentionally exact to only match fullscreen */
+
+.full.height > .action-view-right {
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ border-radius: 0;
+}
+
+.full.height > .action-view-right > .job-info-header {
+ border-radius: 0;
+}
+
+.full.height > .action-view-right > .job-step-container {
+ height: calc(100% - 60px);
+ border-radius: 0;
+}
+
+.job-log-list.hidden {
+ display: none;
+}
+</style>
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
new file mode 100644
index 0000000..3752bc5
--- /dev/null
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -0,0 +1,164 @@
+<script>
+import {Bar} from 'vue-chartjs';
+import {
+ Chart,
+ Tooltip,
+ BarElement,
+ CategoryScale,
+ LinearScale,
+} from 'chart.js';
+import {chartJsColors} from '../utils/color.js';
+import {createApp} from 'vue';
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Tooltip,
+);
+
+const sfc = {
+ components: {Bar},
+ props: {
+ locale: {
+ type: Object,
+ required: true,
+ },
+ },
+ data: () => ({
+ colors: {
+ barColor: 'green',
+ },
+
+ // possible keys:
+ // * avatar_link: (...)
+ // * commits: (...)
+ // * home_link: (...)
+ // * login: (...)
+ // * name: (...)
+ activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [],
+ i18nCommitActivity: this,
+ }),
+ methods: {
+ graphPoints() {
+ return {
+ datasets: [{
+ label: this.locale.commitActivity,
+ data: this.activityTopAuthors.map((item) => item.commits),
+ backgroundColor: this.colors.barColor,
+ barThickness: 40,
+ borderWidth: 0,
+ tension: 0.3,
+ }],
+ labels: this.activityTopAuthors.map((item) => item.name),
+ };
+ },
+ getOptions() {
+ return {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: true,
+ scales: {
+ x: {
+ type: 'category',
+ grid: {
+ display: false,
+ },
+ ticks: {
+ // Disable the drawing of the labels on the x-asis and force them all
+ // of them to be 'shown', this avoids them being internally skipped
+ // for some data points. We rely on the internally generated ticks
+ // to know where to draw our own ticks. Set rotation to 90 degree
+ // and disable autoSkip. autoSkip is disabled to ensure no ticks are
+ // skipped and rotation is set to avoid messing with the width of the chart.
+ color: 'transparent',
+ minRotation: 90,
+ maxRotation: 90,
+ autoSkip: false,
+ },
+ },
+ y: {
+ ticks: {
+ stepSize: 1,
+ },
+ },
+ },
+ };
+ },
+ },
+ mounted() {
+ const refStyle = window.getComputedStyle(this.$refs.style);
+ this.colors.barColor = refStyle.backgroundColor;
+
+ for (const item of this.activityTopAuthors) {
+ const img = new Image();
+ img.src = item.avatar_link;
+ item.avatar_img = img;
+ }
+
+ Chart.register({
+ id: 'image_label',
+ afterDraw: (chart) => {
+ const xAxis = chart.boxes[0];
+ const yAxis = chart.boxes[1];
+ for (const [index] of xAxis.ticks.entries()) {
+ const x = xAxis.getPixelForTick(index);
+ const img = this.activityTopAuthors[index].avatar_img;
+
+ chart.ctx.save();
+ chart.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, x - 10, yAxis.bottom + 10, 20, 20);
+ chart.ctx.restore();
+ }
+ },
+ beforeEvent: (chart, args) => {
+ const event = args.event;
+ if (event.type !== 'mousemove' && event.type !== 'click') return;
+
+ const yAxis = chart.boxes[1];
+ if (event.y < yAxis.bottom + 10 || event.y > yAxis.bottom + 30) {
+ chart.canvas.style.cursor = '';
+ return;
+ }
+
+ const xAxis = chart.boxes[0];
+ const pointIdx = xAxis.ticks.findIndex((_, index) => {
+ const x = xAxis.getPixelForTick(index);
+ return event.x >= x - 10 && event.x <= x + 10;
+ });
+
+ if (pointIdx === -1) {
+ chart.canvas.style.cursor = '';
+ return;
+ }
+
+ chart.canvas.style.cursor = 'pointer';
+ if (event.type === 'click' && this.activityTopAuthors[pointIdx].home_link) {
+ window.location.href = this.activityTopAuthors[pointIdx].home_link;
+ }
+ },
+ });
+ },
+};
+
+export function initRepoActivityTopAuthorsChart() {
+ const el = document.getElementById('repo-activity-top-authors-chart');
+ if (el) {
+ createApp(sfc, {
+ locale: {
+ commitActivity: el.getAttribute('data-locale-commit-activity'),
+ },
+ }).mount(el);
+ }
+}
+
+export default sfc; // activate the IDE's Vue plugin
+</script>
+<template>
+ <div>
+ <div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/>
+ <Bar height="150px" :data="graphPoints()" :options="getOptions()"/>
+ </div>
+</template>
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
new file mode 100644
index 0000000..bfba203
--- /dev/null
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -0,0 +1,357 @@
+<script>
+import {createApp, nextTick} from 'vue';
+import $ from 'jquery';
+import {SvgIcon} from '../svg.js';
+import {pathEscapeSegments} from '../utils/url.js';
+import {showErrorToast} from '../modules/toast.js';
+import {GET} from '../modules/fetch.js';
+
+const sfc = {
+ components: {SvgIcon},
+
+ // no `data()`, at the moment, the `data()` is provided by the init code, which is not ideal and should be fixed in the future
+
+ computed: {
+ filteredItems() {
+ const items = this.items.filter((item) => {
+ return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) &&
+ (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase()));
+ });
+
+ // TODO: fix this anti-pattern: side-effects-in-computed-properties
+ this.active = !items.length && this.showCreateNewBranch ? 0 : -1;
+ return items;
+ },
+ showNoResults() {
+ return !this.filteredItems.length && !this.showCreateNewBranch;
+ },
+ showCreateNewBranch() {
+ if (this.disableCreateBranch || !this.searchTerm) {
+ return false;
+ }
+ return !this.items.filter((item) => {
+ return item.name.toLowerCase() === this.searchTerm.toLowerCase();
+ }).length;
+ },
+ formActionUrl() {
+ return `${this.repoLink}/branches/_new/${this.branchNameSubURL}`;
+ },
+ shouldCreateTag() {
+ return this.mode === 'tags';
+ },
+ },
+
+ watch: {
+ menuVisible(visible) {
+ if (visible) {
+ this.focusSearchField();
+ this.fetchBranchesOrTags();
+ }
+ },
+ },
+
+ beforeMount() {
+ if (this.viewType === 'tree') {
+ this.isViewTree = true;
+ this.refNameText = this.commitIdShort;
+ } else if (this.viewType === 'tag') {
+ this.isViewTag = true;
+ this.refNameText = this.tagName;
+ } else {
+ this.isViewBranch = true;
+ this.refNameText = this.branchName;
+ }
+
+ document.body.addEventListener('click', (event) => {
+ if (this.$el.contains(event.target)) return;
+ if (this.menuVisible) {
+ this.menuVisible = false;
+ }
+ });
+ },
+ methods: {
+ selectItem(item) {
+ const prev = this.getSelected();
+ if (prev !== null) {
+ prev.selected = false;
+ }
+ item.selected = true;
+ const url = (item.tag) ? this.tagURLPrefix + item.url + this.tagURLSuffix : this.branchURLPrefix + item.url + this.branchURLSuffix;
+ if (!this.branchForm) {
+ window.location.href = url;
+ } else {
+ this.isViewTree = false;
+ this.isViewTag = false;
+ this.isViewBranch = false;
+ this.$refs.dropdownRefName.textContent = item.name;
+ if (this.setAction) {
+ document.getElementById(this.branchForm)?.setAttribute('action', url);
+ } else {
+ $(`#${this.branchForm} input[name="refURL"]`).val(url);
+ }
+ $(`#${this.branchForm} input[name="ref"]`).val(item.name);
+ if (item.tag) {
+ this.isViewTag = true;
+ $(`#${this.branchForm} input[name="refType"]`).val('tag');
+ } else {
+ this.isViewBranch = true;
+ $(`#${this.branchForm} input[name="refType"]`).val('branch');
+ }
+ if (this.submitForm) {
+ $(`#${this.branchForm}`).trigger('submit');
+ }
+ this.menuVisible = false;
+ }
+ },
+ createNewBranch() {
+ if (!this.showCreateNewBranch) return;
+ $(this.$refs.newBranchForm).trigger('submit');
+ },
+ focusSearchField() {
+ nextTick(() => {
+ this.$refs.searchField.focus();
+ });
+ },
+ getSelected() {
+ for (let i = 0, j = this.items.length; i < j; ++i) {
+ if (this.items[i].selected) return this.items[i];
+ }
+ return null;
+ },
+ getSelectedIndexInFiltered() {
+ for (let i = 0, j = this.filteredItems.length; i < j; ++i) {
+ if (this.filteredItems[i].selected) return i;
+ }
+ return -1;
+ },
+ scrollToActive() {
+ let el = this.$refs[`listItem${this.active}`]; // eslint-disable-line no-jquery/variable-pattern
+ if (!el || !el.length) return;
+ if (Array.isArray(el)) {
+ el = el[0];
+ }
+
+ const cont = this.$refs.scrollContainer;
+ if (el.offsetTop < cont.scrollTop) {
+ cont.scrollTop = el.offsetTop;
+ } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) {
+ cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight;
+ }
+ },
+ keydown(event) {
+ if (event.keyCode === 40) { // arrow down
+ event.preventDefault();
+
+ if (this.active === -1) {
+ this.active = this.getSelectedIndexInFiltered();
+ }
+
+ if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) {
+ return;
+ }
+ this.active++;
+ this.scrollToActive();
+ } else if (event.keyCode === 38) { // arrow up
+ event.preventDefault();
+
+ if (this.active === -1) {
+ this.active = this.getSelectedIndexInFiltered();
+ }
+
+ if (this.active <= 0) {
+ return;
+ }
+ this.active--;
+ this.scrollToActive();
+ } else if (event.keyCode === 13) { // enter
+ event.preventDefault();
+
+ if (this.active >= this.filteredItems.length) {
+ this.createNewBranch();
+ } else if (this.active >= 0) {
+ this.selectItem(this.filteredItems[this.active]);
+ }
+ } else if (event.keyCode === 27) { // escape
+ event.preventDefault();
+ this.menuVisible = false;
+ }
+ },
+ handleTabSwitch(mode) {
+ if (this.isLoading) return;
+ this.mode = mode;
+ this.focusSearchField();
+ this.fetchBranchesOrTags();
+ },
+ async fetchBranchesOrTags() {
+ if (!['branches', 'tags'].includes(this.mode) || this.isLoading) return;
+ // only fetch when branch/tag list has not been initialized
+ if (this.hasListInitialized[this.mode] ||
+ (this.mode === 'branches' && !this.showBranchesInDropdown) ||
+ (this.mode === 'tags' && this.noTag)
+ ) {
+ return;
+ }
+ this.isLoading = true;
+ try {
+ const resp = await GET(`${this.repoLink}/${this.mode}/list`);
+ const {results} = await resp.json();
+ for (const result of results) {
+ let selected = false;
+ if (this.mode === 'branches') {
+ selected = result === this.defaultSelectedRefName;
+ } else {
+ selected = result === (this.release ? this.release.tagName : this.defaultSelectedRefName);
+ }
+ this.items.push({name: result, url: pathEscapeSegments(result), branch: this.mode === 'branches', tag: this.mode === 'tags', selected});
+ }
+ this.hasListInitialized[this.mode] = true;
+ } catch (e) {
+ showErrorToast(`Network error when fetching ${this.mode}, error: ${e}`);
+ } finally {
+ this.isLoading = false;
+ }
+ },
+ },
+};
+
+export function initRepoBranchTagSelector(selector) {
+ for (const [elIndex, elRoot] of document.querySelectorAll(selector).entries()) {
+ const data = {
+ csrfToken: window.config.csrfToken,
+ items: [],
+ searchTerm: '',
+ refNameText: '',
+ menuVisible: false,
+ release: null,
+
+ isViewTag: false,
+ isViewBranch: false,
+ isViewTree: false,
+
+ active: 0,
+ isLoading: false,
+ // This means whether branch list/tag list has initialized
+ hasListInitialized: {
+ 'branches': false,
+ 'tags': false,
+ },
+ ...window.config.pageData.branchDropdownDataList[elIndex],
+ };
+
+ const comp = {...sfc, data() { return data }};
+ createApp(comp).mount(elRoot);
+ }
+}
+
+export default sfc; // activate IDE's Vue plugin
+</script>
+<template>
+ <div class="ui dropdown custom">
+ <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+ <span class="text tw-flex tw-items-center tw-mr-1 gt-ellipsis">
+ <template v-if="release">{{ textReleaseCompare }}</template>
+ <template v-else>
+ <svg-icon v-if="isViewTag" name="octicon-tag"/>
+ <svg-icon v-else name="octicon-git-branch"/>
+ <strong ref="dropdownRefName" class="tw-ml-2 tw-inline-block gt-ellipsis">{{ refNameText }}</strong>
+ </template>
+ </span>
+ <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
+ </button>
+ <div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak>
+ <div class="ui icon search input">
+ <i class="icon"><svg-icon name="octicon-filter" :size="16"/></i>
+ <input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder">
+ </div>
+ <div v-if="showBranchesInDropdown" class="branch-tag-tab">
+ <a class="branch-tag-item muted" :class="{active: mode === 'branches'}" href="#" @click="handleTabSwitch('branches')">
+ <svg-icon name="octicon-git-branch" :size="16" class-name="tw-mr-1"/>{{ textBranches }}
+ </a>
+ <a v-if="!noTag" class="branch-tag-item muted" :class="{active: mode === 'tags'}" href="#" @click="handleTabSwitch('tags')">
+ <svg-icon name="octicon-tag" :size="16" class-name="tw-mr-1"/>{{ textTags }}
+ </a>
+ </div>
+ <div class="branch-tag-divider"/>
+ <div class="scrolling menu" ref="scrollContainer">
+ <svg-icon name="octicon-rss" symbol-id="svg-symbol-octicon-rss"/>
+ <div class="loading-indicator is-loading" v-if="isLoading"/>
+ <div v-for="(item, index) in filteredItems" :key="item.name" class="item" :class="{selected: item.selected, active: active === index}" @click="selectItem(item)" :ref="'listItem' + index">
+ {{ item.name }}
+ <div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'">
+ {{ textDefaultBranchLabel }}
+ </div>
+ <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon tw-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
+ <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here -->
+ <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg>
+ </a>
+ </div>
+ <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length">
+ <a href="#" @click="createNewBranch()">
+ <div v-show="shouldCreateTag">
+ <i class="reference tags icon"/>
+ <span v-text="textCreateTag.replace('%s', searchTerm)"/>
+ </div>
+ <div v-show="!shouldCreateTag">
+ <svg-icon name="octicon-git-branch"/>
+ <span v-text="textCreateBranch.replace('%s', searchTerm)"/>
+ </div>
+ <div class="text small">
+ <span v-if="isViewBranch || release">{{ textCreateBranchFrom.replace('%s', branchName) }}</span>
+ <span v-else-if="isViewTag">{{ textCreateBranchFrom.replace('%s', tagName) }}</span>
+ <span v-else>{{ textCreateBranchFrom.replace('%s', commitIdShort) }}</span>
+ </div>
+ </a>
+ <form ref="newBranchForm" :action="formActionUrl" method="post">
+ <input type="hidden" name="_csrf" :value="csrfToken">
+ <input type="hidden" name="new_branch_name" v-model="searchTerm">
+ <input type="hidden" name="create_tag" v-model="shouldCreateTag">
+ <input type="hidden" name="current_path" v-model="treePath" v-if="treePath">
+ </form>
+ </div>
+ </div>
+ <div class="message" v-if="showNoResults && !isLoading">
+ {{ noResults }}
+ </div>
+ </div>
+ </div>
+</template>
+<style scoped>
+.branch-tag-tab {
+ padding: 0 10px;
+}
+
+.branch-tag-item {
+ display: inline-block;
+ padding: 10px;
+ border: 1px solid transparent;
+ border-bottom: none;
+}
+
+.branch-tag-item.active {
+ border-color: var(--color-secondary);
+ background: var(--color-menu);
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+}
+
+.branch-tag-divider {
+ margin-top: -1px !important;
+ border-top: 1px solid var(--color-secondary);
+}
+
+.scrolling.menu {
+ border-top: none !important;
+}
+
+.menu .item .rss-icon {
+ display: none; /* only show RSS icon on hover */
+}
+
+.menu .item:hover .rss-icon {
+ display: inline-block;
+}
+
+.scrolling.menu .loading-indicator {
+ height: 4em;
+}
+</style>
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
new file mode 100644
index 0000000..1d40d6d
--- /dev/null
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -0,0 +1,172 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+ Chart,
+ Legend,
+ LinearScale,
+ TimeScale,
+ PointElement,
+ LineElement,
+ Filler,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Line as ChartLine} from 'vue-chartjs';
+import {
+ startDaysBetween,
+ firstStartDateAfterDate,
+ fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+ TimeScale,
+ LinearScale,
+ Legend,
+ PointElement,
+ LineElement,
+ Filler,
+);
+
+export default {
+ components: {ChartLine, SvgIcon},
+ props: {
+ locale: {
+ type: Object,
+ required: true,
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ errorText: '',
+ repoLink: pageData.repoLink || [],
+ data: [],
+ }),
+ mounted() {
+ this.fetchGraphData();
+ },
+ methods: {
+ async fetchGraphData() {
+ this.isLoading = true;
+ try {
+ let response;
+ do {
+ response = await GET(`${this.repoLink}/activity/code-frequency/data`);
+ if (response.status === 202) {
+ await sleep(1000); // wait for 1 second before retrying
+ }
+ } while (response.status === 202);
+ if (response.ok) {
+ this.data = await response.json();
+ const weekValues = Object.values(this.data);
+ const start = weekValues[0].week;
+ const end = firstStartDateAfterDate(new Date());
+ const startDays = startDaysBetween(start, end);
+ this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
+ this.errorText = '';
+ } else {
+ this.errorText = response.statusText;
+ }
+ } catch (err) {
+ this.errorText = err.message;
+ } finally {
+ this.isLoading = false;
+ }
+ },
+
+ toGraphData(data) {
+ return {
+ datasets: [
+ {
+ data: data.map((i) => ({x: i.week, y: i.additions})),
+ pointRadius: 0,
+ pointHitRadius: 0,
+ fill: true,
+ label: 'Additions',
+ backgroundColor: chartJsColors['additions'],
+ borderWidth: 0,
+ tension: 0.3,
+ },
+ {
+ data: data.map((i) => ({x: i.week, y: -i.deletions})),
+ pointRadius: 0,
+ pointHitRadius: 0,
+ fill: true,
+ label: 'Deletions',
+ backgroundColor: chartJsColors['deletions'],
+ borderWidth: 0,
+ tension: 0.3,
+ },
+ ],
+ };
+ },
+
+ getOptions() {
+ return {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: true,
+ plugins: {
+ legend: {
+ display: true,
+ },
+ },
+ scales: {
+ x: {
+ type: 'time',
+ grid: {
+ display: false,
+ },
+ time: {
+ minUnit: 'month',
+ },
+ ticks: {
+ maxRotation: 0,
+ maxTicksLimit: 12,
+ },
+ },
+ y: {
+ ticks: {
+ maxTicksLimit: 6,
+ },
+ },
+ },
+ };
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="ui header tw-flex tw-items-center tw-justify-between">
+ {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
+ </div>
+ <div class="tw-flex ui segment main-graph">
+ <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
+ <div v-if="isLoading">
+ <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+ {{ locale.loadingInfo }}
+ </div>
+ <div v-else class="text red">
+ <SvgIcon name="octicon-x-circle-fill"/>
+ {{ errorText }}
+ </div>
+ </div>
+ <ChartLine
+ v-memo="data" v-if="data.length !== 0"
+ :data="toGraphData(data)" :options="getOptions()"
+ />
+ </div>
+ </div>
+</template>
+<style scoped>
+.main-graph {
+ height: 440px;
+}
+</style>
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
new file mode 100644
index 0000000..dec2599
--- /dev/null
+++ b/web_src/js/components/RepoContributors.vue
@@ -0,0 +1,431 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+ Chart,
+ Title,
+ BarElement,
+ LinearScale,
+ TimeScale,
+ PointElement,
+ LineElement,
+ Filler,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import {Line as ChartLine} from 'vue-chartjs';
+import {
+ startDaysBetween,
+ firstStartDateAfterDate,
+ fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+import $ from 'jquery';
+
+const customEventListener = {
+ id: 'customEventListener',
+ afterEvent: (chart, args, opts) => {
+ // event will be replayed from chart.update when reset zoom,
+ // so we need to check whether args.replay is true to avoid call loops
+ if (args.event.type === 'dblclick' && opts.chartType === 'main' && !args.replay) {
+ chart.resetZoom();
+ opts.instance.updateOtherCharts(args.event, true);
+ }
+ },
+};
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+ TimeScale,
+ LinearScale,
+ BarElement,
+ Title,
+ PointElement,
+ LineElement,
+ Filler,
+ zoomPlugin,
+ customEventListener,
+);
+
+export default {
+ components: {ChartLine, SvgIcon},
+ props: {
+ locale: {
+ type: Object,
+ required: true,
+ },
+ repoLink: {
+ type: String,
+ required: true,
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ errorText: '',
+ totalStats: {},
+ sortedContributors: {},
+ type: 'commits',
+ contributorsStats: [],
+ xAxisStart: null,
+ xAxisEnd: null,
+ xAxisMin: null,
+ xAxisMax: null,
+ }),
+ mounted() {
+ this.fetchGraphData();
+
+ $('#repo-contributors').dropdown({
+ onChange: (val) => {
+ this.xAxisMin = this.xAxisStart;
+ this.xAxisMax = this.xAxisEnd;
+ this.type = val;
+ this.sortContributors();
+ },
+ });
+ },
+ methods: {
+ sortContributors() {
+ const contributors = this.filterContributorWeeksByDateRange();
+ const criteria = `total_${this.type}`;
+ this.sortedContributors = Object.values(contributors)
+ .filter((contributor) => contributor[criteria] !== 0)
+ .sort((a, b) => a[criteria] > b[criteria] ? -1 : a[criteria] === b[criteria] ? 0 : 1)
+ .slice(0, 100);
+ },
+
+ async fetchGraphData() {
+ this.isLoading = true;
+ try {
+ let response;
+ do {
+ response = await GET(`${this.repoLink}/activity/contributors/data`);
+ if (response.status === 202) {
+ await sleep(1000); // wait for 1 second before retrying
+ }
+ } while (response.status === 202);
+ if (response.ok) {
+ const data = await response.json();
+ const {total, ...rest} = data;
+ // below line might be deleted if we are sure go produces map always sorted by keys
+ total.weeks = Object.fromEntries(Object.entries(total.weeks).sort());
+
+ const weekValues = Object.values(total.weeks);
+ this.xAxisStart = weekValues[0].week;
+ this.xAxisEnd = firstStartDateAfterDate(new Date());
+ const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
+ total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
+ this.xAxisMin = this.xAxisStart;
+ this.xAxisMax = this.xAxisEnd;
+ this.contributorsStats = {};
+ for (const [email, user] of Object.entries(rest)) {
+ user.weeks = fillEmptyStartDaysWithZeroes(startDays, user.weeks);
+ this.contributorsStats[email] = user;
+ }
+ this.sortContributors();
+ this.totalStats = total;
+ this.errorText = '';
+ } else {
+ this.errorText = response.statusText;
+ }
+ } catch (err) {
+ this.errorText = err.message;
+ } finally {
+ this.isLoading = false;
+ }
+ },
+
+ filterContributorWeeksByDateRange() {
+ const filteredData = {};
+ const data = this.contributorsStats;
+ for (const key of Object.keys(data)) {
+ const user = data[key];
+ user.total_commits = 0;
+ user.total_additions = 0;
+ user.total_deletions = 0;
+ user.max_contribution_type = 0;
+ const filteredWeeks = user.weeks.filter((week) => {
+ const oneWeek = 7 * 24 * 60 * 60 * 1000;
+ if (week.week >= this.xAxisMin - oneWeek && week.week <= this.xAxisMax + oneWeek) {
+ user.total_commits += week.commits;
+ user.total_additions += week.additions;
+ user.total_deletions += week.deletions;
+ if (week[this.type] > user.max_contribution_type) {
+ user.max_contribution_type = week[this.type];
+ }
+ return true;
+ }
+ return false;
+ });
+ // this line is required. See https://github.com/sahinakkaya/gitea/pull/3#discussion_r1396495722
+ // for details.
+ user.max_contribution_type += 1;
+
+ filteredData[key] = {...user, weeks: filteredWeeks};
+ }
+
+ return filteredData;
+ },
+
+ maxMainGraph() {
+ // This method calculates maximum value for Y value of the main graph. If the number
+ // of maximum contributions for selected contribution type is 15.955 it is probably
+ // better to round it up to 20.000.This method is responsible for doing that.
+ // Normally, chartjs handles this automatically, but it will resize the graph when you
+ // zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
+ const maxValue = Math.max(
+ ...this.totalStats.weeks.map((o) => o[this.type]),
+ );
+ const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
+ if (coefficient % 1 === 0) return maxValue;
+ return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
+ },
+
+ maxContributorGraph() {
+ // Similar to maxMainGraph method this method calculates maximum value for Y value
+ // for contributors' graph. If I let chartjs do this for me, it will choose different
+ // maxY value for each contributors' graph which again makes it harder to compare.
+ const maxValue = Math.max(
+ ...this.sortedContributors.map((c) => c.max_contribution_type),
+ );
+ const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
+ if (coefficient % 1 === 0) return maxValue;
+ return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
+ },
+
+ toGraphData(data) {
+ return {
+ datasets: [
+ {
+ data: data.map((i) => ({x: i.week, y: i[this.type]})),
+ pointRadius: 0,
+ pointHitRadius: 0,
+ fill: 'start',
+ backgroundColor: chartJsColors[this.type],
+ borderWidth: 0,
+ tension: 0.3,
+ },
+ ],
+ };
+ },
+
+ updateOtherCharts(event, reset) {
+ const minVal = event.chart.options.scales.x.min;
+ const maxVal = event.chart.options.scales.x.max;
+ if (reset) {
+ this.xAxisMin = this.xAxisStart;
+ this.xAxisMax = this.xAxisEnd;
+ this.sortContributors();
+ } else if (minVal) {
+ this.xAxisMin = minVal;
+ this.xAxisMax = maxVal;
+ this.sortContributors();
+ }
+ },
+
+ getOptions(type) {
+ return {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false,
+ events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove', 'dblclick'],
+ plugins: {
+ title: {
+ display: type === 'main',
+ text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
+ position: 'top',
+ align: 'center',
+ },
+ customEventListener: {
+ chartType: type,
+ instance: this,
+ },
+ zoom: {
+ pan: {
+ enabled: true,
+ modifierKey: 'shift',
+ mode: 'x',
+ threshold: 20,
+ onPanComplete: this.updateOtherCharts,
+ },
+ limits: {
+ x: {
+ // Check https://www.chartjs.org/chartjs-plugin-zoom/latest/guide/options.html#scale-limits
+ // to know what each option means
+ min: 'original',
+ max: 'original',
+
+ // number of milliseconds in 2 weeks. Minimum x range will be 2 weeks when you zoom on the graph
+ minRange: 2 * 7 * 24 * 60 * 60 * 1000,
+ },
+ },
+ zoom: {
+ drag: {
+ enabled: type === 'main',
+ },
+ pinch: {
+ enabled: type === 'main',
+ },
+ mode: 'x',
+ onZoomComplete: this.updateOtherCharts,
+ },
+ },
+ },
+ scales: {
+ x: {
+ min: this.xAxisMin,
+ max: this.xAxisMax,
+ type: 'time',
+ grid: {
+ display: false,
+ },
+ time: {
+ minUnit: 'month',
+ },
+ ticks: {
+ maxRotation: 0,
+ maxTicksLimit: type === 'main' ? 12 : 6,
+ },
+ },
+ y: {
+ min: 0,
+ max: type === 'main' ? this.maxMainGraph() : this.maxContributorGraph(),
+ ticks: {
+ maxTicksLimit: type === 'main' ? 6 : 4,
+ },
+ },
+ },
+ };
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="ui header tw-flex tw-items-center tw-justify-between">
+ <div>
+ <relative-time
+ v-if="xAxisMin > 0"
+ format="datetime"
+ year="numeric"
+ month="short"
+ day="numeric"
+ weekday=""
+ :datetime="new Date(xAxisMin)"
+ >
+ {{ new Date(xAxisMin) }}
+ </relative-time>
+ {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "-" }}
+ <relative-time
+ v-if="xAxisMax > 0"
+ format="datetime"
+ year="numeric"
+ month="short"
+ day="numeric"
+ weekday=""
+ :datetime="new Date(xAxisMax)"
+ >
+ {{ new Date(xAxisMax) }}
+ </relative-time>
+ </div>
+ <div>
+ <!-- Contribution type -->
+ <div class="ui dropdown jump" id="repo-contributors">
+ <div class="ui basic compact button">
+ <span class="not-mobile">{{ locale.filterLabel }}</span> <strong>{{ locale.contributionType[type] }}</strong>
+ <svg-icon name="octicon-triangle-down" :size="14"/>
+ </div>
+ <div class="menu">
+ <div :class="['item', {'selected': type === 'commits'}]" data-value="commits">
+ {{ locale.contributionType.commits }}
+ </div>
+ <div :class="['item', {'selected': type === 'additions'}]" data-value="additions">
+ {{ locale.contributionType.additions }}
+ </div>
+ <div :class="['item', {'selected': type === 'deletions'}]" data-value="deletions">
+ {{ locale.contributionType.deletions }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="tw-flex ui segment main-graph">
+ <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
+ <div v-if="isLoading">
+ <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+ {{ locale.loadingInfo }}
+ </div>
+ <div v-else class="text red">
+ <SvgIcon name="octicon-x-circle-fill"/>
+ {{ errorText }}
+ </div>
+ </div>
+ <ChartLine
+ v-memo="[totalStats.weeks, type]" v-if="Object.keys(totalStats).length !== 0"
+ :data="toGraphData(totalStats.weeks)" :options="getOptions('main')"
+ />
+ </div>
+ <div class="contributor-grid">
+ <div
+ v-for="(contributor, index) in sortedContributors"
+ :key="index"
+ v-memo="[sortedContributors, type]"
+ >
+ <div class="ui top attached header tw-flex tw-flex-1">
+ <b class="ui right">#{{ index + 1 }}</b>
+ <a :href="contributor.home_link">
+ <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link">
+ </a>
+ <div class="tw-ml-2">
+ <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
+ <h4 v-else class="contributor-name">
+ {{ contributor.name }}
+ </h4>
+ <p class="tw-text-12 tw-flex tw-gap-1">
+ <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
+ <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
+ <strong v-if="contributor.total_deletions" class="text red">
+ {{ contributor.total_deletions.toLocaleString() }}--</strong>
+ </p>
+ </div>
+ </div>
+ <div class="ui attached segment">
+ <div>
+ <ChartLine
+ :data="toGraphData(contributor.weeks)"
+ :options="getOptions('contributor')"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<style scoped>
+.main-graph {
+ height: 260px;
+ padding-top: 2px;
+}
+
+.contributor-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+}
+
+.contributor-grid > * {
+ min-width: 0;
+}
+
+@media (max-width: 991.98px) {
+ .contributor-grid {
+ grid-template-columns: repeat(1, 1fr);
+ }
+}
+
+.contributor-name {
+ margin-bottom: 0;
+}
+</style>
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
new file mode 100644
index 0000000..8759978
--- /dev/null
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -0,0 +1,149 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+ Chart,
+ Tooltip,
+ BarElement,
+ LinearScale,
+ TimeScale,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Bar} from 'vue-chartjs';
+import {
+ startDaysBetween,
+ firstStartDateAfterDate,
+ fillEmptyStartDaysWithZeroes,
+} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
+import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+ TimeScale,
+ LinearScale,
+ BarElement,
+ Tooltip,
+);
+
+export default {
+ components: {Bar, SvgIcon},
+ props: {
+ locale: {
+ type: Object,
+ required: true,
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ errorText: '',
+ repoLink: pageData.repoLink || [],
+ data: [],
+ }),
+ mounted() {
+ this.fetchGraphData();
+ },
+ methods: {
+ async fetchGraphData() {
+ this.isLoading = true;
+ try {
+ let response;
+ do {
+ response = await GET(`${this.repoLink}/activity/recent-commits/data`);
+ if (response.status === 202) {
+ await sleep(1000); // wait for 1 second before retrying
+ }
+ } while (response.status === 202);
+ if (response.ok) {
+ const data = await response.json();
+ const start = Object.values(data)[0].week;
+ const end = firstStartDateAfterDate(new Date());
+ const startDays = startDaysBetween(start, end);
+ this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
+ this.errorText = '';
+ } else {
+ this.errorText = response.statusText;
+ }
+ } catch (err) {
+ this.errorText = err.message;
+ } finally {
+ this.isLoading = false;
+ }
+ },
+
+ toGraphData(data) {
+ return {
+ datasets: [
+ {
+ data: data.map((i) => ({x: i.week, y: i.commits})),
+ label: 'Commits',
+ backgroundColor: chartJsColors['commits'],
+ borderWidth: 0,
+ tension: 0.3,
+ },
+ ],
+ };
+ },
+
+ getOptions() {
+ return {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: true,
+ scales: {
+ x: {
+ type: 'time',
+ grid: {
+ display: false,
+ },
+ time: {
+ minUnit: 'week',
+ },
+ ticks: {
+ maxRotation: 0,
+ maxTicksLimit: 52,
+ },
+ },
+ y: {
+ ticks: {
+ maxTicksLimit: 6,
+ },
+ },
+ },
+ };
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="ui header tw-flex tw-items-center tw-justify-between">
+ {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
+ </div>
+ <div class="tw-flex ui segment main-graph">
+ <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
+ <div v-if="isLoading">
+ <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/>
+ {{ locale.loadingInfo }}
+ </div>
+ <div v-else class="text red">
+ <SvgIcon name="octicon-x-circle-fill"/>
+ {{ errorText }}
+ </div>
+ </div>
+ <Bar
+ v-memo="data" v-if="data.length !== 0"
+ :data="toGraphData(data)" :options="getOptions()"
+ />
+ </div>
+ </div>
+</template>
+<style scoped>
+.main-graph {
+ height: 250px;
+}
+</style>
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
new file mode 100644
index 0000000..925e531
--- /dev/null
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -0,0 +1,115 @@
+<script>
+import {createApp} from 'vue';
+import {hideElem, showElem} from '../utils/dom.js';
+
+const sfc = {
+ props: {
+ isAdmin: {
+ type: Boolean,
+ required: true,
+ },
+ noAccessLabel: {
+ type: String,
+ required: true,
+ },
+ readLabel: {
+ type: String,
+ required: true,
+ },
+ writeLabel: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ categories() {
+ const categories = [
+ 'activitypub',
+ ];
+ if (this.isAdmin) {
+ categories.push('admin');
+ }
+ categories.push(
+ 'issue',
+ 'misc',
+ 'notification',
+ 'organization',
+ 'package',
+ 'repository',
+ 'user');
+ return categories;
+ },
+ },
+
+ mounted() {
+ document.getElementById('scoped-access-submit').addEventListener('click', this.onClickSubmit);
+ },
+
+ unmounted() {
+ document.getElementById('scoped-access-submit').removeEventListener('click', this.onClickSubmit);
+ },
+
+ methods: {
+ onClickSubmit(e) {
+ e.preventDefault();
+
+ const warningEl = document.getElementById('scoped-access-warning');
+ // check that at least one scope has been selected
+ for (const el of document.getElementsByClassName('access-token-select')) {
+ if (el.value) {
+ // Hide the error if it was visible from previous attempt.
+ hideElem(warningEl);
+ // Submit the form.
+ document.getElementById('scoped-access-form').submit();
+ // Don't show the warning.
+ return;
+ }
+ }
+ // no scopes selected, show validation error
+ showElem(warningEl);
+ },
+ },
+};
+
+export default sfc;
+
+/**
+ * Initialize category toggle sections
+ */
+export function initScopedAccessTokenCategories() {
+ for (const el of document.getElementsByClassName('scoped-access-token')) {
+ createApp(sfc, {
+ isAdmin: el.getAttribute('data-is-admin') === 'true',
+ noAccessLabel: el.getAttribute('data-no-access-label'),
+ readLabel: el.getAttribute('data-read-label'),
+ writeLabel: el.getAttribute('data-write-label'),
+ }).mount(el);
+ }
+}
+
+</script>
+<template>
+ <div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category">
+ <label class="category-label" :for="'access-token-scope-' + category">
+ {{ category }}
+ </label>
+ <div class="gitea-select">
+ <select
+ class="ui selection access-token-select"
+ name="scope"
+ :id="'access-token-scope-' + category"
+ >
+ <option value="">
+ {{ noAccessLabel }}
+ </option>
+ <option :value="'read:' + category">
+ {{ readLabel }}
+ </option>
+ <option :value="'write:' + category">
+ {{ writeLabel }}
+ </option>
+ </select>
+ </div>
+ </div>
+</template>
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
new file mode 100644
index 0000000..1a5bd6e
--- /dev/null
+++ b/web_src/js/features/admin/common.js
@@ -0,0 +1,258 @@
+import $ from 'jquery';
+import {checkAppUrl} from '../common-global.js';
+import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
+import {POST} from '../../modules/fetch.js';
+
+const {appSubUrl} = window.config;
+
+function onSecurityProtocolChange() {
+ if (Number(document.getElementById('security_protocol')?.value) > 0) {
+ showElem('.has-tls');
+ } else {
+ hideElem('.has-tls');
+ }
+}
+
+export function initAdminCommon() {
+ if (!document.querySelector('.page-content.admin')) return;
+
+ // check whether appUrl(ROOT_URL) is correct, if not, show an error message
+ checkAppUrl();
+
+ // New user
+ if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) {
+ document.getElementById('login_type')?.addEventListener('change', function () {
+ if (this.value?.substring(0, 1) === '0') {
+ document.getElementById('user_name')?.removeAttribute('disabled');
+ document.getElementById('login_name')?.removeAttribute('required');
+ hideElem('.non-local');
+ showElem('.local');
+ document.getElementById('user_name')?.focus();
+
+ if (this.getAttribute('data-password') === 'required') {
+ document.getElementById('password')?.setAttribute('required', 'required');
+ }
+ } else {
+ if (document.querySelector('.admin.edit.user')) {
+ document.getElementById('user_name')?.setAttribute('disabled', 'disabled');
+ }
+ document.getElementById('login_name')?.setAttribute('required', 'required');
+ showElem('.non-local');
+ hideElem('.local');
+ document.getElementById('login_name')?.focus();
+
+ document.getElementById('password')?.removeAttribute('required');
+ }
+ });
+ }
+
+ function onUsePagedSearchChange() {
+ const searchPageSizeElements = document.querySelectorAll('.search-page-size');
+ if (document.getElementById('use_paged_search').checked) {
+ showElem('.search-page-size');
+ for (const el of searchPageSizeElements) {
+ el.querySelector('input')?.setAttribute('required', 'required');
+ }
+ } else {
+ hideElem('.search-page-size');
+ for (const el of searchPageSizeElements) {
+ el.querySelector('input')?.removeAttribute('required');
+ }
+ }
+ }
+
+ function onOAuth2Change(applyDefaultValues) {
+ hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url');
+ for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) {
+ input.removeAttribute('required');
+ }
+
+ const provider = document.getElementById('oauth2_provider')?.value;
+ switch (provider) {
+ case 'openidConnect':
+ for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input')) {
+ input.setAttribute('required', 'required');
+ }
+ showElem('.open_id_connect_auto_discovery_url');
+ break;
+ default: {
+ const customURLSettings = document.getElementById(`${provider}_customURLSettings`);
+ if (!customURLSettings) break;
+ const customURLRequired = (customURLSettings.getAttribute('data-required') === 'true');
+ document.getElementById('oauth2_use_custom_url').checked = customURLRequired;
+ if (customURLRequired || customURLSettings.getAttribute('data-available') === 'true') {
+ showElem('.oauth2_use_custom_url');
+ }
+ }
+ }
+ onOAuth2UseCustomURLChange(applyDefaultValues);
+ }
+
+ function onOAuth2UseCustomURLChange(applyDefaultValues) {
+ const provider = document.getElementById('oauth2_provider')?.value;
+ hideElem('.oauth2_use_custom_url_field');
+ for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) {
+ input.removeAttribute('required');
+ }
+
+ if (document.getElementById('oauth2_use_custom_url')?.checked) {
+ for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
+ const customInput = document.getElementById(`${provider}_${custom}`);
+ if (!customInput) continue;
+ if (applyDefaultValues) {
+ document.getElementById(`oauth2_${custom}`).value = customInput.value;
+ }
+ if (customInput.getAttribute('data-available') === 'true') {
+ for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
+ input.setAttribute('required', 'required');
+ }
+ showElem(`.oauth2_${custom}`);
+ }
+ }
+ }
+ }
+
+ function onEnableLdapGroupsChange() {
+ toggleElem(document.getElementById('ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
+ }
+
+ // New authentication
+ if (document.querySelector('.admin.new.authentication')) {
+ document.getElementById('auth_type')?.addEventListener('change', function () {
+ hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
+
+ for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) {
+ input.removeAttribute('required');
+ }
+
+ document.querySelector('.binddnrequired')?.classList.remove('required');
+
+ const authType = this.value;
+ switch (authType) {
+ case '2': // LDAP
+ showElem('.ldap');
+ for (const input of document.querySelectorAll('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
+ input.setAttribute('required', 'required');
+ }
+ document.querySelector('.binddnrequired')?.classList.add('required');
+ break;
+ case '3': // SMTP
+ showElem('.smtp');
+ showElem('.has-tls');
+ for (const input of document.querySelectorAll('.smtp div.required input, .has-tls')) {
+ input.setAttribute('required', 'required');
+ }
+ break;
+ case '4': // PAM
+ showElem('.pam');
+ for (const input of document.querySelectorAll('.pam input')) {
+ input.setAttribute('required', 'required');
+ }
+ break;
+ case '5': // LDAP
+ showElem('.dldap');
+ for (const input of document.querySelectorAll('.dldap div.required:not(.ldap) input')) {
+ input.setAttribute('required', 'required');
+ }
+ break;
+ case '6': // OAuth2
+ showElem('.oauth2');
+ for (const input of document.querySelectorAll('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) {
+ input.setAttribute('required', 'required');
+ }
+ onOAuth2Change(true);
+ break;
+ case '7': // SSPI
+ showElem('.sspi');
+ for (const input of document.querySelectorAll('.sspi div.required input')) {
+ input.setAttribute('required', 'required');
+ }
+ break;
+ }
+ if (authType === '2' || authType === '5') {
+ onSecurityProtocolChange();
+ onEnableLdapGroupsChange();
+ }
+ if (authType === '2') {
+ onUsePagedSearchChange();
+ }
+ });
+ $('#auth_type').trigger('change');
+ document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
+ document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
+ document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+ document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
+ $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
+ }
+ // Edit authentication
+ if (document.querySelector('.admin.edit.authentication')) {
+ const authType = document.getElementById('auth_type')?.value;
+ if (authType === '2' || authType === '5') {
+ document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
+ $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
+ onEnableLdapGroupsChange();
+ if (authType === '2') {
+ document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
+ }
+ } else if (authType === '6') {
+ document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+ document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
+ onOAuth2Change(false);
+ }
+ }
+
+ if (document.querySelector('.admin.authentication')) {
+ $('#auth_name').on('input', function () {
+ // appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash.
+ document.getElementById('oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`;
+ }).trigger('input');
+ }
+
+ // Notice
+ if (document.querySelector('.admin.notice')) {
+ const detailModal = document.getElementById('detail-modal');
+
+ // Attach view detail modals
+ $('.view-detail').on('click', function () {
+ const description = this.closest('tr').querySelector('.notice-description').textContent;
+ detailModal.querySelector('.content pre').textContent = description;
+ $(detailModal).modal('show');
+ return false;
+ });
+
+ // Select actions
+ const checkboxes = document.querySelectorAll('.select.table .ui.checkbox input');
+
+ $('.select.action').on('click', function () {
+ switch ($(this).data('action')) {
+ case 'select-all':
+ for (const checkbox of checkboxes) {
+ checkbox.checked = true;
+ }
+ break;
+ case 'deselect-all':
+ for (const checkbox of checkboxes) {
+ checkbox.checked = false;
+ }
+ break;
+ case 'inverse':
+ for (const checkbox of checkboxes) {
+ checkbox.checked = !checkbox.checked;
+ }
+ break;
+ }
+ });
+ document.getElementById('delete-selection')?.addEventListener('click', async function (e) {
+ e.preventDefault();
+ this.classList.add('is-loading', 'disabled');
+ const data = new FormData();
+ for (const checkbox of checkboxes) {
+ if (checkbox.checked) {
+ data.append('ids[]', checkbox.closest('.ui.checkbox').getAttribute('data-id'));
+ }
+ }
+ await POST(this.getAttribute('data-link'), {data});
+ window.location.href = this.getAttribute('data-redirect');
+ });
+ }
+}
diff --git a/web_src/js/features/admin/config.js b/web_src/js/features/admin/config.js
new file mode 100644
index 0000000..c382342
--- /dev/null
+++ b/web_src/js/features/admin/config.js
@@ -0,0 +1,24 @@
+import {showTemporaryTooltip} from '../../modules/tippy.js';
+import {POST} from '../../modules/fetch.js';
+
+const {appSubUrl} = window.config;
+
+export function initAdminConfigs() {
+ const elAdminConfig = document.querySelector('.page-content.admin.config');
+ if (!elAdminConfig) return;
+
+ for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) {
+ el.addEventListener('change', async () => {
+ try {
+ const resp = await POST(`${appSubUrl}/admin/config`, {
+ data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: el.checked}),
+ });
+ const json = await resp.json();
+ if (json.errorMessage) throw new Error(json.errorMessage);
+ } catch (ex) {
+ showTemporaryTooltip(el, ex.toString());
+ el.checked = !el.checked;
+ }
+ });
+ }
+}
diff --git a/web_src/js/features/admin/emails.js b/web_src/js/features/admin/emails.js
new file mode 100644
index 0000000..46fafa7
--- /dev/null
+++ b/web_src/js/features/admin/emails.js
@@ -0,0 +1,14 @@
+import $ from 'jquery';
+
+export function initAdminEmails() {
+ function linkEmailAction(e) {
+ const $this = $(this);
+ $('#form-uid').val($this.data('uid'));
+ $('#form-email').val($this.data('email'));
+ $('#form-primary').val($this.data('primary'));
+ $('#form-activate').val($this.data('activate'));
+ $('#change-email-modal').modal('show');
+ e.preventDefault();
+ }
+ $('.link-email-action').on('click', linkEmailAction);
+}
diff --git a/web_src/js/features/admin/users.js b/web_src/js/features/admin/users.js
new file mode 100644
index 0000000..7cac603
--- /dev/null
+++ b/web_src/js/features/admin/users.js
@@ -0,0 +1,39 @@
+export function initAdminUserListSearchForm() {
+ const searchForm = window.config.pageData.adminUserListSearchForm;
+ if (!searchForm) return;
+
+ const form = document.querySelector('#user-list-search-form');
+ if (!form) return;
+
+ for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) {
+ button.classList.add('active');
+ }
+
+ if (searchForm.StatusFilterMap) {
+ for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) {
+ if (!v) continue;
+ for (const input of form.querySelectorAll(`input[name="status_filter[${k}]"][value="${v}"]`)) {
+ input.checked = true;
+ }
+ }
+ }
+
+ for (const radio of form.querySelectorAll('input[type=radio]')) {
+ radio.addEventListener('click', () => {
+ form.submit();
+ });
+ }
+
+ const resetButtons = form.querySelectorAll('.j-reset-status-filter');
+ for (const button of resetButtons) {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ for (const input of form.querySelectorAll('input[type=radio]')) {
+ if (input.name.startsWith('status_filter[')) {
+ input.checked = false;
+ }
+ }
+ form.submit();
+ });
+ }
+}
diff --git a/web_src/js/features/autofocus-end.js b/web_src/js/features/autofocus-end.js
new file mode 100644
index 0000000..da71ce9
--- /dev/null
+++ b/web_src/js/features/autofocus-end.js
@@ -0,0 +1,6 @@
+export function initAutoFocusEnd() {
+ for (const el of document.querySelectorAll('.js-autofocus-end')) {
+ el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
+ el.setSelectionRange(el.value.length, el.value.length);
+ }
+}
diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js
new file mode 100644
index 0000000..c803a50
--- /dev/null
+++ b/web_src/js/features/captcha.js
@@ -0,0 +1,51 @@
+import {isDarkTheme} from '../utils.js';
+
+export async function initCaptcha() {
+ const captchaEl = document.querySelector('#captcha');
+ if (!captchaEl) return;
+
+ const siteKey = captchaEl.getAttribute('data-sitekey');
+ const isDark = isDarkTheme();
+
+ const params = {
+ sitekey: siteKey,
+ theme: isDark ? 'dark' : 'light',
+ };
+
+ switch (captchaEl.getAttribute('data-captcha-type')) {
+ case 'g-recaptcha': {
+ if (window.grecaptcha) {
+ window.grecaptcha.ready(() => {
+ window.grecaptcha.render(captchaEl, params);
+ });
+ }
+ break;
+ }
+ case 'cf-turnstile': {
+ if (window.turnstile) {
+ window.turnstile.render(captchaEl, params);
+ }
+ break;
+ }
+ case 'h-captcha': {
+ if (window.hcaptcha) {
+ window.hcaptcha.render(captchaEl, params);
+ }
+ break;
+ }
+ case 'm-captcha': {
+ const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
+ mCaptcha.INPUT_NAME = 'm-captcha-response';
+ const instanceURL = captchaEl.getAttribute('data-instance-url');
+
+ mCaptcha.default({
+ siteKey: {
+ instanceUrl: new URL(instanceURL),
+ key: siteKey,
+ },
+ });
+ break;
+ }
+ default:
+ }
+}
diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js
new file mode 100644
index 0000000..7e26bff
--- /dev/null
+++ b/web_src/js/features/citation.js
@@ -0,0 +1,50 @@
+import $ from 'jquery';
+import {getCurrentLocale} from '../utils.js';
+
+const {pageData} = window.config;
+
+async function initInputCitationValue(inputContent) {
+ const [{Cite, plugins}] = await Promise.all([
+ import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'),
+ import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'),
+ import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'),
+ ]);
+ const {citationFileContent} = pageData;
+ const config = plugins.config.get('@bibtex');
+ config.constants.fieldTypes.doi = ['field', 'literal'];
+ config.constants.fieldTypes.version = ['field', 'literal'];
+ const citationFormatter = new Cite(citationFileContent);
+ const lang = getCurrentLocale() || 'en-US';
+ const bibtexOutput = citationFormatter.format('bibtex', {lang});
+ inputContent.value = bibtexOutput;
+}
+
+export async function initCitationFileCopyContent() {
+ if (!pageData.citationFileContent) return;
+
+ const inputContent = document.getElementById('citation-copy-content');
+
+ if (!inputContent) return;
+
+ document.getElementById('cite-repo-button')?.addEventListener('click', async (e) => {
+ const dropdownBtn = e.target.closest('.ui.dropdown.button');
+ dropdownBtn.classList.add('is-loading');
+
+ try {
+ try {
+ await initInputCitationValue(inputContent);
+ } catch (e) {
+ console.error(`initCitationFileCopyContent error: ${e}`, e);
+ return;
+ }
+
+ inputContent.addEventListener('click', () => {
+ inputContent.select();
+ });
+ } finally {
+ dropdownBtn.classList.remove('is-loading');
+ }
+
+ $('#cite-repo-modal').modal('show');
+ });
+}
diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js
new file mode 100644
index 0000000..daf7e2a
--- /dev/null
+++ b/web_src/js/features/clipboard.js
@@ -0,0 +1,32 @@
+import {showTemporaryTooltip} from '../modules/tippy.js';
+import {toAbsoluteUrl} from '../utils.js';
+import {clippie} from 'clippie';
+
+const {copy_success, copy_error} = window.config.i18n;
+
+// Enable clipboard copy from HTML attributes. These properties are supported:
+// - data-clipboard-text: Direct text to copy
+// - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
+// - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
+export function initGlobalCopyToClipboardListener() {
+ document.addEventListener('click', async (e) => {
+ const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]');
+ if (!target) return;
+
+ e.preventDefault();
+
+ let text = target.getAttribute('data-clipboard-text');
+ if (!text) {
+ text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value;
+ }
+
+ if (text && target.getAttribute('data-clipboard-text-type') === 'url') {
+ text = toAbsoluteUrl(text);
+ }
+
+ if (text) {
+ const success = await clippie(text);
+ showTemporaryTooltip(target, success ? copy_success : copy_error);
+ }
+ });
+}
diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js
new file mode 100644
index 0000000..47e1539
--- /dev/null
+++ b/web_src/js/features/code-frequency.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoCodeFrequency() {
+ const el = document.getElementById('repo-code-frequency-chart');
+ if (!el) return;
+
+ const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue');
+ try {
+ const View = createApp(RepoCodeFrequency, {
+ locale: {
+ loadingTitle: el.getAttribute('data-locale-loading-title'),
+ loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+ loadingInfo: el.getAttribute('data-locale-loading-info'),
+ },
+ });
+ View.mount(el);
+ } catch (err) {
+ console.error('RepoCodeFrequency failed to load', err);
+ el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+ }
+}
diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js
new file mode 100644
index 0000000..07a686f
--- /dev/null
+++ b/web_src/js/features/codeeditor.js
@@ -0,0 +1,191 @@
+import tinycolor from 'tinycolor2';
+import {basename, extname, isObject, isDarkTheme} from '../utils.js';
+import {onInputDebounce} from '../utils/dom.js';
+
+const languagesByFilename = {};
+const languagesByExt = {};
+
+const baseOptions = {
+ fontFamily: 'var(--fonts-monospace)',
+ fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242
+ guides: {bracketPairs: false, indentation: false},
+ links: false,
+ minimap: {enabled: false},
+ occurrencesHighlight: 'off',
+ overviewRulerLanes: 0,
+ renderLineHighlight: 'all',
+ renderLineHighlightOnlyWhenFocus: true,
+ rulers: false,
+ scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6},
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+};
+
+function getEditorconfig(input) {
+ try {
+ return JSON.parse(input.getAttribute('data-editorconfig'));
+ } catch {
+ return null;
+ }
+}
+
+function initLanguages(monaco) {
+ for (const {filenames, extensions, id} of monaco.languages.getLanguages()) {
+ for (const filename of filenames || []) {
+ languagesByFilename[filename] = id;
+ }
+ for (const extension of extensions || []) {
+ languagesByExt[extension] = id;
+ }
+ }
+}
+
+function getLanguage(filename) {
+ return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext';
+}
+
+function updateEditor(monaco, editor, filename, lineWrapExts) {
+ editor.updateOptions(getFileBasedOptions(filename, lineWrapExts));
+ const model = editor.getModel();
+ const language = model.getLanguageId();
+ const newLanguage = getLanguage(filename);
+ if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage);
+}
+
+// export editor for customization - https://github.com/go-gitea/gitea/issues/10409
+function exportEditor(editor) {
+ if (!window.codeEditors) window.codeEditors = [];
+ if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor);
+}
+
+export async function createMonaco(textarea, filename, editorOpts) {
+ const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor');
+
+ initLanguages(monaco);
+ let {language, ...other} = editorOpts;
+ if (!language) language = getLanguage(filename);
+
+ const container = document.createElement('div');
+ container.className = 'monaco-editor-container';
+ textarea.parentNode.append(container);
+
+ // https://github.com/microsoft/monaco-editor/issues/2427
+ // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
+ const styles = window.getComputedStyle(document.documentElement);
+ const getColor = (name) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
+
+ monaco.editor.defineTheme('gitea', {
+ base: isDarkTheme() ? 'vs-dark' : 'vs',
+ inherit: true,
+ rules: [
+ {
+ background: getColor('--color-code-bg'),
+ },
+ ],
+ colors: {
+ 'editor.background': getColor('--color-code-bg'),
+ 'editor.foreground': getColor('--color-text'),
+ 'editor.inactiveSelectionBackground': getColor('--color-primary-light-4'),
+ 'editor.lineHighlightBackground': getColor('--color-editor-line-highlight'),
+ 'editor.selectionBackground': getColor('--color-primary-light-3'),
+ 'editor.selectionForeground': getColor('--color-primary-light-3'),
+ 'editorLineNumber.background': getColor('--color-code-bg'),
+ 'editorLineNumber.foreground': getColor('--color-secondary-dark-6'),
+ 'editorWidget.background': getColor('--color-body'),
+ 'editorWidget.border': getColor('--color-secondary'),
+ 'input.background': getColor('--color-input-background'),
+ 'input.border': getColor('--color-input-border'),
+ 'input.foreground': getColor('--color-input-text'),
+ 'scrollbar.shadow': getColor('--color-shadow'),
+ 'progressBar.background': getColor('--color-primary'),
+ },
+ });
+
+ const editor = monaco.editor.create(container, {
+ value: textarea.value,
+ theme: 'gitea',
+ language,
+ ...other,
+ });
+
+ monaco.editor.addKeybindingRules([
+ {keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion
+ ]);
+
+ const model = editor.getModel();
+ model.onDidChangeContent(() => {
+ textarea.value = editor.getValue({preserveBOM: true});
+ textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure
+ });
+
+ exportEditor(editor);
+
+ const loading = document.querySelector('.editor-loading');
+ if (loading) loading.remove();
+
+ return {monaco, editor};
+}
+
+function getFileBasedOptions(filename, lineWrapExts) {
+ return {
+ wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off',
+ };
+}
+
+function togglePreviewDisplay(previewable) {
+ const previewTab = document.querySelector('a[data-tab="preview"]');
+ if (!previewTab) return;
+
+ if (previewable) {
+ const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`);
+ previewTab.setAttribute('data-url', newUrl);
+ previewTab.style.display = '';
+ } else {
+ previewTab.style.display = 'none';
+ // If the "preview" tab was active, user changes the filename to a non-previewable one,
+ // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
+ if (previewTab.classList.contains('active')) {
+ const writeTab = document.querySelector('a[data-tab="write"]');
+ writeTab.click();
+ }
+ }
+}
+
+export async function createCodeEditor(textarea, filenameInput) {
+ const filename = basename(filenameInput.value);
+ const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(','));
+ const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(',');
+ const previewable = previewableExts.has(extname(filename));
+ const editorConfig = getEditorconfig(filenameInput);
+
+ togglePreviewDisplay(previewable);
+
+ const {monaco, editor} = await createMonaco(textarea, filename, {
+ ...baseOptions,
+ ...getFileBasedOptions(filenameInput.value, lineWrapExts),
+ ...getEditorConfigOptions(editorConfig),
+ });
+
+ filenameInput.addEventListener('input', onInputDebounce(() => {
+ const filename = filenameInput.value;
+ const previewable = previewableExts.has(extname(filename));
+ togglePreviewDisplay(previewable);
+ updateEditor(monaco, editor, filename, lineWrapExts);
+ }));
+
+ return editor;
+}
+
+function getEditorConfigOptions(ec) {
+ if (!isObject(ec)) return {};
+
+ const opts = {};
+ opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec);
+ if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size);
+ if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize;
+ if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)];
+ opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true;
+ opts.insertSpaces = ec.indent_style === 'space';
+ opts.useTabStops = ec.indent_style === 'tab';
+ return opts;
+}
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
new file mode 100644
index 0000000..6d00d90
--- /dev/null
+++ b/web_src/js/features/colorpicker.js
@@ -0,0 +1,66 @@
+import {createTippy} from '../modules/tippy.js';
+
+export async function initColorPickers() {
+ const els = document.getElementsByClassName('js-color-picker-input');
+ if (!els.length) return;
+
+ await Promise.all([
+ import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
+ import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
+ ]);
+
+ for (const el of els) {
+ initPicker(el);
+ }
+}
+
+function updateSquare(el, newValue) {
+ el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
+}
+
+function updatePicker(el, newValue) {
+ el.setAttribute('color', newValue);
+}
+
+function initPicker(el) {
+ const input = el.querySelector('input');
+
+ const square = document.createElement('div');
+ square.classList.add('preview-square');
+ updateSquare(square, input.value);
+ el.append(square);
+
+ const picker = document.createElement('hex-color-picker');
+ picker.addEventListener('color-changed', (e) => {
+ input.value = e.detail.value;
+ input.focus();
+ updateSquare(square, e.detail.value);
+ });
+
+ input.addEventListener('input', (e) => {
+ updateSquare(square, e.target.value);
+ updatePicker(picker, e.target.value);
+ });
+
+ createTippy(input, {
+ trigger: 'focus click',
+ theme: 'bare',
+ hideOnClick: true,
+ content: picker,
+ placement: 'bottom-start',
+ interactive: true,
+ onShow() {
+ updatePicker(picker, input.value);
+ },
+ });
+
+ // init precolors
+ for (const colorEl of el.querySelectorAll('.precolors .color')) {
+ colorEl.addEventListener('click', (e) => {
+ const newValue = e.target.getAttribute('data-color-hex');
+ input.value = newValue;
+ input.dispatchEvent(new Event('input', {bubbles: true}));
+ updateSquare(square, newValue);
+ });
+ }
+}
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
new file mode 100644
index 0000000..5a304d9
--- /dev/null
+++ b/web_src/js/features/common-global.js
@@ -0,0 +1,463 @@
+import $ from 'jquery';
+import '../vendor/jquery.are-you-sure.js';
+import {clippie} from 'clippie';
+import {createDropzone} from './dropzone.js';
+import {showGlobalErrorMessage} from '../bootstrap.js';
+import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
+import {svg} from '../svg.js';
+import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter} from '../utils/dom.js';
+import {htmlEscape} from 'escape-goat';
+import {showTemporaryTooltip} from '../modules/tippy.js';
+import {confirmModal} from './comp/ConfirmModal.js';
+import {showErrorToast} from '../modules/toast.js';
+import {request, POST, GET} from '../modules/fetch.js';
+import '../htmx.js';
+
+const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
+
+export function initGlobalFormDirtyLeaveConfirm() {
+ // Warn users that try to leave a page after entering data into a form.
+ // Except on sign-in pages, and for forms marked as 'ignore-dirty'.
+ if (!$('.user.signin').length) {
+ $('form:not(.ignore-dirty)').areYouSure();
+ }
+}
+
+export function initHeadNavbarContentToggle() {
+ const navbar = document.getElementById('navbar');
+ const btn = document.getElementById('navbar-expand-toggle');
+ if (!navbar || !btn) return;
+
+ btn.addEventListener('click', () => {
+ const isExpanded = btn.classList.contains('active');
+ navbar.classList.toggle('navbar-menu-open', !isExpanded);
+ btn.classList.toggle('active', !isExpanded);
+ });
+}
+
+export function initFootLanguageMenu() {
+ async function linkLanguageAction() {
+ const $this = $(this);
+ await GET($this.data('url'));
+ window.location.reload();
+ }
+
+ $('.language-menu a[lang]').on('click', linkLanguageAction);
+}
+
+export function initGlobalEnterQuickSubmit() {
+ $(document).on('keydown', '.js-quick-submit', (e) => {
+ if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter')) {
+ handleGlobalEnterQuickSubmit(e.target);
+ return false;
+ }
+ });
+}
+
+export function initGlobalButtonClickOnEnter() {
+ $(document).on('keypress', 'div.ui.button,span.ui.button', (e) => {
+ if (e.code === ' ' || e.code === 'Enter') {
+ $(e.target).trigger('click');
+ e.preventDefault();
+ }
+ });
+}
+
+// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
+// more details are in the backend's fetch-redirect handler
+function fetchActionDoRedirect(redirect) {
+ const form = document.createElement('form');
+ const input = document.createElement('input');
+ form.method = 'post';
+ form.action = `${appSubUrl}/-/fetch-redirect`;
+ input.type = 'hidden';
+ input.name = 'redirect';
+ input.value = redirect;
+ form.append(input);
+ document.body.append(form);
+ form.submit();
+}
+
+async function fetchActionDoRequest(actionElem, url, opt) {
+ try {
+ const resp = await request(url, opt);
+ if (resp.status === 200) {
+ let {redirect} = await resp.json();
+ redirect = redirect || actionElem.getAttribute('data-redirect');
+ actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading
+ if (redirect) {
+ fetchActionDoRedirect(redirect);
+ } else {
+ window.location.reload();
+ }
+ return;
+ } else if (resp.status >= 400 && resp.status < 500) {
+ const data = await resp.json();
+ // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
+ // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
+ if (data.errorMessage) {
+ showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
+ } else {
+ showErrorToast(`server error: ${resp.status}`);
+ }
+ } else {
+ showErrorToast(`server error: ${resp.status}`);
+ }
+ } catch (e) {
+ if (e.name !== 'AbortError') {
+ console.error('error when doRequest', e);
+ showErrorToast(`${i18n.network_error} ${e}`);
+ }
+ }
+ actionElem.classList.remove('is-loading', 'loading-icon-2px');
+}
+
+async function formFetchAction(e) {
+ if (!e.target.classList.contains('form-fetch-action')) return;
+
+ e.preventDefault();
+ const formEl = e.target;
+ if (formEl.classList.contains('is-loading')) return;
+
+ formEl.classList.add('is-loading');
+ if (formEl.clientHeight < 50) {
+ formEl.classList.add('loading-icon-2px');
+ }
+
+ const formMethod = formEl.getAttribute('method') || 'get';
+ const formActionUrl = formEl.getAttribute('action');
+ const formData = new FormData(formEl);
+ const formSubmitter = submitEventSubmitter(e);
+ const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
+ if (submitterName) {
+ formData.append(submitterName, submitterValue || '');
+ }
+
+ let reqUrl = formActionUrl;
+ const reqOpt = {method: formMethod.toUpperCase()};
+ if (formMethod.toLowerCase() === 'get') {
+ const params = new URLSearchParams();
+ for (const [key, value] of formData) {
+ params.append(key, value.toString());
+ }
+ const pos = reqUrl.indexOf('?');
+ if (pos !== -1) {
+ reqUrl = reqUrl.slice(0, pos);
+ }
+ reqUrl += `?${params.toString()}`;
+ } else {
+ reqOpt.body = formData;
+ }
+
+ await fetchActionDoRequest(formEl, reqUrl, reqOpt);
+}
+
+export function initGlobalCommon() {
+ // Semantic UI modules.
+ const $uiDropdowns = $('.ui.dropdown');
+
+ // do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
+ $uiDropdowns.filter(':not(.custom)').dropdown();
+
+ // The "jump" means this dropdown is mainly used for "menu" purpose,
+ // clicking an item will jump to somewhere else or trigger an action/function.
+ // When a dropdown is used for non-refresh actions with tippy,
+ // it must have this "jump" class to hide the tippy when dropdown is closed.
+ $uiDropdowns.filter('.jump').dropdown({
+ action: 'hide',
+ onShow() {
+ // hide associated tooltip while dropdown is open
+ this._tippy?.hide();
+ this._tippy?.disable();
+ },
+ onHide() {
+ this._tippy?.enable();
+
+ // hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
+ setTimeout(() => {
+ const $dropdown = $(this);
+ if ($dropdown.dropdown('is hidden')) {
+ $(this).find('.menu > .item').each((_, item) => {
+ item._tippy?.hide();
+ });
+ }
+ }, 2000);
+ },
+ });
+
+ // Special popup-directions, prevent Fomantic from guessing the popup direction.
+ // With default "direction: auto", if the viewport height is small, Fomantic would show the popup upward,
+ // if the dropdown is at the beginning of the page, then the top part would be clipped by the window view.
+ // eg: Issue List "Sort" dropdown
+ // But we can not set "direction: downward" for all dropdowns, because there is a bug in dropdown menu positioning when calculating the "left" position,
+ // which would make some dropdown popups slightly shift out of the right viewport edge in some cases.
+ // eg: the "Create New Repo" menu on the navbar.
+ $uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward');
+ $uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward');
+
+ $('.tabular.menu .item').tab();
+
+ initSubmitEventPolyfill();
+ document.addEventListener('submit', formFetchAction);
+ document.addEventListener('click', linkAction);
+}
+
+export function initGlobalDropzone() {
+ for (const el of document.querySelectorAll('.dropzone')) {
+ initDropzone(el);
+ }
+}
+
+export function initDropzone(el) {
+ const $dropzone = $(el);
+ const _promise = createDropzone(el, {
+ url: $dropzone.data('upload-url'),
+ headers: {'X-Csrf-Token': csrfToken},
+ maxFiles: $dropzone.data('max-file'),
+ maxFilesize: $dropzone.data('max-size'),
+ acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
+ addRemoveLinks: true,
+ dictDefaultMessage: $dropzone.data('default-message'),
+ dictInvalidFileType: $dropzone.data('invalid-input-type'),
+ dictFileTooBig: $dropzone.data('file-too-big'),
+ dictRemoveFile: $dropzone.data('remove-file'),
+ timeout: 0,
+ thumbnailMethod: 'contain',
+ thumbnailWidth: 480,
+ thumbnailHeight: 480,
+ init() {
+ this.on('success', (file, data) => {
+ file.uuid = data.uuid;
+ const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
+ $dropzone.find('.files').append($input);
+ // Create a "Copy Link" element, to conveniently copy the image
+ // or file link as Markdown to the clipboard
+ const copyLinkElement = document.createElement('div');
+ copyLinkElement.className = 'tw-text-center';
+ // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
+ copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
+ copyLinkElement.addEventListener('click', async (e) => {
+ e.preventDefault();
+ let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
+ if (file.type.startsWith('image/')) {
+ fileMarkdown = `!${fileMarkdown}`;
+ } else if (file.type.startsWith('video/')) {
+ fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`;
+ }
+ const success = await clippie(fileMarkdown);
+ showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
+ });
+ file.previewTemplate.append(copyLinkElement);
+ });
+ this.on('removedfile', (file) => {
+ $(`#${file.uuid}`).remove();
+ if ($dropzone.data('remove-url')) {
+ POST($dropzone.data('remove-url'), {
+ data: new URLSearchParams({file: file.uuid}),
+ });
+ }
+ });
+ this.on('error', function (file, message) {
+ showErrorToast(message);
+ this.removeFile(file);
+ });
+ },
+ });
+}
+
+async function linkAction(e) {
+ // A "link-action" can post AJAX request to its "data-url"
+ // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
+ // If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
+ const el = e.target.closest('.link-action');
+ if (!el) return;
+
+ e.preventDefault();
+ const url = el.getAttribute('data-url');
+ const doRequest = async () => {
+ el.disabled = true;
+ await fetchActionDoRequest(el, url, {method: 'POST'});
+ el.disabled = false;
+ };
+
+ const modalConfirmContent = htmlEscape(el.getAttribute('data-modal-confirm') || '');
+ if (!modalConfirmContent) {
+ await doRequest();
+ return;
+ }
+
+ const isRisky = el.classList.contains('red') || el.classList.contains('yellow') || el.classList.contains('orange') || el.classList.contains('negative');
+ if (await confirmModal({content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'primary'})) {
+ await doRequest();
+ }
+}
+
+export function initGlobalLinkActions() {
+ function showDeletePopup(e) {
+ e.preventDefault();
+ const $this = $(this || e.target);
+ const dataArray = $this.data();
+ let filter = '';
+ if ($this[0].getAttribute('data-modal-id')) {
+ filter += `#${$this[0].getAttribute('data-modal-id')}`;
+ }
+
+ const $dialog = $(`.delete.modal${filter}`);
+ $dialog.find('.name').text($this.data('name'));
+ for (const [key, value] of Object.entries(dataArray)) {
+ if (key && key.startsWith('data')) {
+ $dialog.find(`.${key}`).text(value);
+ }
+ }
+
+ $dialog.modal({
+ closable: false,
+ onApprove: async () => {
+ if ($this.data('type') === 'form') {
+ $($this.data('form')).trigger('submit');
+ return;
+ }
+ if ($this[0].getAttribute('hx-confirm')) {
+ e.detail.issueRequest(true);
+ return;
+ }
+ const postData = new FormData();
+ for (const [key, value] of Object.entries(dataArray)) {
+ if (key && key.startsWith('data')) {
+ postData.append(key.slice(4), value);
+ }
+ if (key === 'id') {
+ postData.append('id', value);
+ }
+ }
+
+ const response = await POST($this.data('url'), {data: postData});
+ if (response.ok) {
+ const data = await response.json();
+ window.location.href = data.redirect;
+ }
+ },
+ }).modal('show');
+ }
+
+ // Helpers.
+ $('.delete-button').on('click', showDeletePopup);
+
+ document.addEventListener('htmx:confirm', (e) => {
+ e.preventDefault();
+ // htmx:confirm is triggered for every HTMX request, even those that don't
+ // have the `hx-confirm` attribute specified. To avoid opening modals for
+ // those elements, check if 'e.detail.question' is empty, which contains the
+ // value of the `hx-confirm` attribute.
+ if (!e.detail.question) {
+ e.detail.issueRequest(true);
+ } else {
+ showDeletePopup(e);
+ }
+ });
+}
+
+function initGlobalShowModal() {
+ // A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
+ // Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
+ // * First, try to query '#target'
+ // * Then, try to query '.target'
+ // * Then, try to query 'target' as HTML tag
+ // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
+ $('.show-modal').on('click', function (e) {
+ e.preventDefault();
+ const modalSelector = this.getAttribute('data-modal');
+ const $modal = $(modalSelector);
+ if (!$modal.length) {
+ throw new Error('no modal for this action');
+ }
+ const modalAttrPrefix = 'data-modal-';
+ for (const attrib of this.attributes) {
+ if (!attrib.name.startsWith(modalAttrPrefix)) {
+ continue;
+ }
+
+ const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
+ const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
+ // try to find target by: "#target" -> ".target" -> "target tag"
+ let $attrTarget = $modal.find(`#${attrTargetName}`);
+ if (!$attrTarget.length) $attrTarget = $modal.find(`.${attrTargetName}`);
+ if (!$attrTarget.length) $attrTarget = $modal.find(`${attrTargetName}`);
+ if (!$attrTarget.length) continue; // TODO: show errors in dev mode to remind developers that there is a bug
+
+ if (attrTargetAttr) {
+ $attrTarget[0][attrTargetAttr] = attrib.value;
+ } else if ($attrTarget[0].matches('input, textarea')) {
+ $attrTarget.val(attrib.value); // FIXME: add more supports like checkbox
+ } else {
+ $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
+ }
+ }
+
+ $modal.modal('setting', {
+ onApprove: () => {
+ // "form-fetch-action" can handle network errors gracefully,
+ // so keep the modal dialog to make users can re-submit the form if anything wrong happens.
+ if ($modal.find('.form-fetch-action').length) return false;
+ },
+ }).modal('show');
+ });
+}
+
+export function initGlobalButtons() {
+ // There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
+ // However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
+ // There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
+ $(document).on('click', 'form button.ui.cancel.button', (e) => {
+ e.preventDefault();
+ });
+
+ $('.show-panel').on('click', function (e) {
+ // a '.show-panel' element can show a panel, by `data-panel="selector"`
+ // if it has "toggle" class, it toggles the panel
+ e.preventDefault();
+ const sel = this.getAttribute('data-panel');
+ if (this.classList.contains('toggle')) {
+ toggleElem(sel);
+ } else {
+ showElem(sel);
+ }
+ });
+
+ $('.hide-panel').on('click', function (e) {
+ // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
+ e.preventDefault();
+ let sel = this.getAttribute('data-panel');
+ if (sel) {
+ hideElem($(sel));
+ return;
+ }
+ sel = this.getAttribute('data-panel-closest');
+ if (sel) {
+ hideElem($(this).closest(sel));
+ return;
+ }
+ // should never happen, otherwise there is a bug in code
+ showErrorToast('Nothing to hide');
+ });
+
+ initGlobalShowModal();
+}
+
+/**
+ * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
+ * * Cross-origin API request without correct cookie
+ * * Incorrect href in <a>
+ * * ...
+ * So we check whether current URL starts with AppUrl(ROOT_URL).
+ * If they don't match, show a warning to users.
+ */
+export function checkAppUrl() {
+ const curUrl = window.location.href;
+ // some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
+ if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) {
+ return;
+ }
+ showGlobalErrorMessage(`Your ROOT_URL in app.ini is "${appUrl}", it's unlikely matching the site you are visiting.
+Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`);
+}
diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js
new file mode 100644
index 0000000..0c0f6c5
--- /dev/null
+++ b/web_src/js/features/common-issue-list.js
@@ -0,0 +1,68 @@
+import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
+
+const {appSubUrl} = window.config;
+const reIssueIndex = /^(\d+)$/; // eg: "123"
+const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
+const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}"
+
+// if the searchText can be parsed to an "issue goto link", return the link, otherwise return empty string
+export function parseIssueListQuickGotoLink(repoLink, searchText) {
+ searchText = searchText.trim();
+ let targetUrl = '';
+ if (repoLink) {
+ // try to parse it in current repo
+ if (reIssueIndex.test(searchText)) {
+ targetUrl = `${repoLink}/issues/${searchText}`;
+ } else if (reIssueSharpIndex.test(searchText)) {
+ targetUrl = `${repoLink}/issues/${searchText.substr(1)}`;
+ }
+ } else {
+ // try to parse it for a global search (eg: "owner/repo#123")
+ const matchIssueOwnerRepoIndex = searchText.match(reIssueOwnerRepoIndex);
+ if (matchIssueOwnerRepoIndex) {
+ const [_, owner, repo, index] = matchIssueOwnerRepoIndex;
+ targetUrl = `${appSubUrl}/${owner}/${repo}/issues/${index}`;
+ }
+ }
+ return targetUrl;
+}
+
+export function initCommonIssueListQuickGoto() {
+ const goto = document.getElementById('issue-list-quick-goto');
+ if (!goto) return;
+
+ const form = goto.closest('form');
+ const input = form.querySelector('input[name=q]');
+ const repoLink = goto.getAttribute('data-repo-link');
+
+ form.addEventListener('submit', (e) => {
+ // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
+ let doQuickGoto = !isElemHidden(goto);
+ const submitter = submitEventSubmitter(e);
+ if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false;
+ if (!doQuickGoto) return;
+
+ // if there is a goto button, use its link
+ e.preventDefault();
+ window.location.href = goto.getAttribute('data-issue-goto-link');
+ });
+
+ const onInput = async () => {
+ const searchText = input.value;
+ // try to check whether the parsed goto link is valid
+ let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
+ if (targetUrl) {
+ const res = await GET(`${targetUrl}/info`);
+ if (res.status !== 200) targetUrl = '';
+ }
+ // if the input value has changed, then ignore the result
+ if (input.value !== searchText) return;
+
+ toggleElem(goto, Boolean(targetUrl));
+ goto.setAttribute('data-issue-goto-link', targetUrl);
+ };
+
+ input.addEventListener('input', onInputDebounce(onInput));
+ onInput();
+}
diff --git a/web_src/js/features/common-issue-list.test.js b/web_src/js/features/common-issue-list.test.js
new file mode 100644
index 0000000..da7ea64
--- /dev/null
+++ b/web_src/js/features/common-issue-list.test.js
@@ -0,0 +1,16 @@
+import {parseIssueListQuickGotoLink} from './common-issue-list.js';
+
+test('parseIssueListQuickGotoLink', () => {
+ expect(parseIssueListQuickGotoLink('/link', '')).toEqual('');
+ expect(parseIssueListQuickGotoLink('/link', 'abc')).toEqual('');
+ expect(parseIssueListQuickGotoLink('/link', '123')).toEqual('/link/issues/123');
+ expect(parseIssueListQuickGotoLink('/link', '#123')).toEqual('/link/issues/123');
+ expect(parseIssueListQuickGotoLink('/link', 'owner/repo#123')).toEqual('');
+
+ expect(parseIssueListQuickGotoLink('', '')).toEqual('');
+ expect(parseIssueListQuickGotoLink('', 'abc')).toEqual('');
+ expect(parseIssueListQuickGotoLink('', '123')).toEqual('');
+ expect(parseIssueListQuickGotoLink('', '#123')).toEqual('');
+ expect(parseIssueListQuickGotoLink('', 'owner/repo#')).toEqual('');
+ expect(parseIssueListQuickGotoLink('', 'owner/repo#123')).toEqual('/owner/repo/issues/123');
+});
diff --git a/web_src/js/features/common-organization.js b/web_src/js/features/common-organization.js
new file mode 100644
index 0000000..442714a
--- /dev/null
+++ b/web_src/js/features/common-organization.js
@@ -0,0 +1,16 @@
+import {initCompLabelEdit} from './comp/LabelEdit.js';
+import {toggleElem} from '../utils/dom.js';
+
+export function initCommonOrganization() {
+ if (!document.querySelectorAll('.organization').length) {
+ return;
+ }
+
+ document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () {
+ const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase();
+ toggleElem('#org-name-change-prompt', nameChanged);
+ });
+
+ // Labels
+ initCompLabelEdit('.organization.settings.labels');
+}
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
new file mode 100644
index 0000000..70e92de
--- /dev/null
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -0,0 +1,413 @@
+import '@github/markdown-toolbar-element';
+import '@github/text-expander-element';
+import $ from 'jquery';
+import {attachTribute} from '../tribute.js';
+import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js';
+import {initEasyMDEPaste, initTextareaPaste} from './Paste.js';
+import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
+import {renderPreviewPanelContent} from '../repo-editor.js';
+import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
+import {initTextExpander} from './TextExpander.js';
+import {showErrorToast} from '../../modules/toast.js';
+import {POST} from '../../modules/fetch.js';
+
+let elementIdCounter = 0;
+
+/**
+ * validate if the given textarea is non-empty.
+ * @param {HTMLElement} textarea - The textarea element to be validated.
+ * @returns {boolean} returns true if validation succeeded.
+ */
+export function validateTextareaNonEmpty(textarea) {
+ // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation.
+ // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert.
+ if (!textarea.value) {
+ if (isElemVisible(textarea)) {
+ textarea.required = true;
+ const form = textarea.closest('form');
+ form?.reportValidity();
+ } else {
+ // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places.
+ showErrorToast('Require non-empty content');
+ }
+ return false;
+ }
+ return true;
+}
+
+class ComboMarkdownEditor {
+ constructor(container, options = {}) {
+ container._giteaComboMarkdownEditor = this;
+ this.options = options;
+ this.container = container;
+ }
+
+ async init() {
+ this.prepareEasyMDEToolbarActions();
+ this.setupContainer();
+ this.setupTab();
+ this.setupDropzone();
+ this.setupTextarea();
+
+ await this.switchToUserPreference();
+ }
+
+ applyEditorHeights(el, heights) {
+ if (!heights) return;
+ if (heights.minHeight) el.style.minHeight = heights.minHeight;
+ if (heights.height) el.style.height = heights.height;
+ if (heights.maxHeight) el.style.maxHeight = heights.maxHeight;
+ }
+
+ setupContainer() {
+ initTextExpander(this.container.querySelector('text-expander'));
+ this.container.addEventListener('ce-editor-content-changed', (e) => this.options?.onContentChanged?.(this, e));
+ }
+
+ setupTextarea() {
+ this.textarea = this.container.querySelector('.markdown-text-editor');
+ this.textarea._giteaComboMarkdownEditor = this;
+ this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
+ this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e));
+ this.applyEditorHeights(this.textarea, this.options.editorHeights);
+
+ if (this.textarea.getAttribute('data-disable-autosize') !== 'true') {
+ this.textareaAutosize = autosize(this.textarea, {viewportMarginBottom: 130});
+ }
+
+ this.textareaMarkdownToolbar = this.container.querySelector('markdown-toolbar');
+ this.textareaMarkdownToolbar.setAttribute('for', this.textarea.id);
+ for (const el of this.textareaMarkdownToolbar.querySelectorAll('.markdown-toolbar-button')) {
+ // upstream bug: The role code is never executed in base MarkdownButtonElement https://github.com/github/markdown-toolbar-element/issues/70
+ el.setAttribute('role', 'button');
+ // the editor usually is in a form, so the buttons should have "type=button", avoiding conflicting with the form's submit.
+ if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button');
+ }
+ this.textareaMarkdownToolbar.querySelector('button[data-md-action="indent"]')?.addEventListener('click', () => {
+ this.indentSelection(false);
+ });
+ this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => {
+ this.indentSelection(true);
+ });
+
+ this.textarea.addEventListener('keydown', (e) => {
+ if (e.shiftKey) {
+ e.target._shiftDown = true;
+ }
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.altKey) {
+ if (!this.breakLine()) return; // Nothing changed, let the default handler work.
+ this.options?.onContentChanged?.(this, e);
+ e.preventDefault();
+ }
+ });
+ this.textarea.addEventListener('keyup', (e) => {
+ if (!e.shiftKey) {
+ e.target._shiftDown = false;
+ }
+ });
+
+ const monospaceButton = this.container.querySelector('.markdown-switch-monospace');
+ const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true';
+ const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text');
+ monospaceButton.setAttribute('data-tooltip-content', monospaceText);
+ monospaceButton.setAttribute('aria-checked', String(monospaceEnabled));
+
+ monospaceButton?.addEventListener('click', (e) => {
+ e.preventDefault();
+ const enabled = localStorage?.getItem('markdown-editor-monospace') !== 'true';
+ localStorage.setItem('markdown-editor-monospace', String(enabled));
+ this.textarea.classList.toggle('tw-font-mono', enabled);
+ const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text');
+ monospaceButton.setAttribute('data-tooltip-content', text);
+ monospaceButton.setAttribute('aria-checked', String(enabled));
+ });
+
+ const easymdeButton = this.container.querySelector('.markdown-switch-easymde');
+ easymdeButton?.addEventListener('click', async (e) => {
+ e.preventDefault();
+ this.userPreferredEditor = 'easymde';
+ await this.switchToEasyMDE();
+ });
+
+ if (this.dropzone) {
+ initTextareaPaste(this.textarea, this.dropzone);
+ }
+ }
+
+ setupDropzone() {
+ const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
+ if (dropzoneParentContainer) {
+ this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
+ }
+ }
+
+ setupTab() {
+ const $container = $(this.container);
+ const tabs = $container[0].querySelectorAll('.tabular.menu > .item');
+
+ // Fomantic Tab requires the "data-tab" to be globally unique.
+ // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
+ const tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer');
+ const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer');
+ tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
+ tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
+ const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]');
+ const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]');
+ panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`);
+ panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`);
+ elementIdCounter++;
+
+ tabEditor.addEventListener('click', () => {
+ requestAnimationFrame(() => {
+ this.focus();
+ });
+ });
+
+ $(tabs).tab();
+
+ this.previewUrl = tabPreviewer.getAttribute('data-preview-url');
+ this.previewContext = tabPreviewer.getAttribute('data-preview-context');
+ this.previewMode = this.options.previewMode ?? 'comment';
+ this.previewWiki = this.options.previewWiki ?? false;
+ tabPreviewer.addEventListener('click', async () => {
+ const formData = new FormData();
+ formData.append('mode', this.previewMode);
+ formData.append('context', this.previewContext);
+ formData.append('text', this.value());
+ formData.append('wiki', this.previewWiki);
+ const response = await POST(this.previewUrl, {data: formData});
+ const data = await response.text();
+ renderPreviewPanelContent($(panelPreviewer), data);
+ });
+ }
+
+ prepareEasyMDEToolbarActions() {
+ this.easyMDEToolbarDefault = [
+ 'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3',
+ 'heading-bigger', 'heading-smaller', '|', 'code', 'quote', '|', 'gitea-checkbox-empty',
+ 'gitea-checkbox-checked', '|', 'unordered-list', 'ordered-list', '|', 'link', 'image',
+ 'table', 'horizontal-rule', '|', 'gitea-switch-to-textarea',
+ ];
+ }
+
+ parseEasyMDEToolbar(EasyMDE, actions) {
+ this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(EasyMDE, this);
+ const processed = [];
+ for (const action of actions) {
+ const actionButton = this.easyMDEToolbarActions[action];
+ if (!actionButton) throw new Error(`Unknown EasyMDE toolbar action ${action}`);
+ processed.push(actionButton);
+ }
+ return processed;
+ }
+
+ async switchToUserPreference() {
+ if (this.userPreferredEditor === 'easymde') {
+ await this.switchToEasyMDE();
+ } else {
+ this.switchToTextarea();
+ }
+ }
+
+ switchToTextarea() {
+ if (!this.easyMDE) return;
+ showElem(this.textareaMarkdownToolbar);
+ if (this.easyMDE) {
+ this.easyMDE.toTextArea();
+ this.easyMDE = null;
+ }
+ }
+
+ async switchToEasyMDE() {
+ if (this.easyMDE) return;
+ // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles.
+ const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
+ const easyMDEOpt = {
+ autoDownloadFontAwesome: false,
+ element: this.textarea,
+ forceSync: true,
+ renderingConfig: {singleLineBreaks: false},
+ indentWithTabs: false,
+ tabSize: 4,
+ spellChecker: false,
+ inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
+ nativeSpellcheck: true,
+ ...this.options.easyMDEOptions,
+ };
+ easyMDEOpt.toolbar = this.parseEasyMDEToolbar(EasyMDE, easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault);
+
+ this.easyMDE = new EasyMDE(easyMDEOpt);
+ this.easyMDE.codemirror.on('change', (...args) => {this.options?.onContentChanged?.(this, ...args)});
+ this.easyMDE.codemirror.setOption('extraKeys', {
+ 'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
+ 'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
+ Enter: (cm) => {
+ const tributeContainer = document.querySelector('.tribute-container');
+ if (!tributeContainer || tributeContainer.style.display === 'none') {
+ cm.execCommand('newlineAndIndent');
+ }
+ },
+ Up: (cm) => {
+ const tributeContainer = document.querySelector('.tribute-container');
+ if (!tributeContainer || tributeContainer.style.display === 'none') {
+ return cm.execCommand('goLineUp');
+ }
+ },
+ Down: (cm) => {
+ const tributeContainer = document.querySelector('.tribute-container');
+ if (!tributeContainer || tributeContainer.style.display === 'none') {
+ return cm.execCommand('goLineDown');
+ }
+ },
+ });
+ this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights);
+ await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
+ initEasyMDEPaste(this.easyMDE, this.dropzone);
+ hideElem(this.textareaMarkdownToolbar);
+ }
+
+ value(v = undefined) {
+ if (v === undefined) {
+ if (this.easyMDE) {
+ return this.easyMDE.value();
+ }
+ return this.textarea.value;
+ }
+
+ if (this.easyMDE) {
+ this.easyMDE.value(v);
+ } else {
+ this.textarea.value = v;
+ }
+ this.textareaAutosize?.resizeToFit();
+ }
+
+ focus() {
+ if (this.easyMDE) {
+ this.easyMDE.codemirror.focus();
+ } else {
+ this.textarea.focus();
+ }
+ }
+
+ moveCursorToEnd() {
+ this.textarea.focus();
+ this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
+ if (this.easyMDE) {
+ this.easyMDE.codemirror.focus();
+ this.easyMDE.codemirror.setCursor(this.easyMDE.codemirror.lineCount(), 0);
+ }
+ }
+
+ indentSelection(unindent) {
+ // Indent with 4 spaces, unindent 4 spaces or fewer or a lost tab.
+ const indentPrefix = ' ';
+ const unindentRegex = /^( {1,4}|\t)/;
+
+ // Indent all lines that are included in the selection, partially or whole, while preserving the original selection at the end.
+ const lines = this.textarea.value.split('\n');
+ const changedLines = [];
+ // The current selection or cursor position.
+ const [start, end] = [this.textarea.selectionStart, this.textarea.selectionEnd];
+ // The range containing whole lines that will effectively be replaced.
+ let [editStart, editEnd] = [start, end];
+ // The range that needs to be re-selected to match previous selection.
+ let [newStart, newEnd] = [start, end];
+ // The start and end position of the current line (where end points to the newline or EOF)
+ let [lineStart, lineEnd] = [0, 0];
+
+ for (const line of lines) {
+ lineEnd = lineStart + line.length + 1;
+ if (lineEnd <= start) {
+ lineStart = lineEnd;
+ continue;
+ }
+
+ const updated = unindent ? line.replace(unindentRegex, '') : indentPrefix + line;
+ changedLines.push(updated);
+ const move = updated.length - line.length;
+
+ if (start >= lineStart && start < lineEnd) {
+ editStart = lineStart;
+ newStart = Math.max(start + move, lineStart);
+ }
+
+ newEnd += move;
+ editEnd = lineEnd - 1;
+ lineStart = lineEnd;
+ if (lineStart > end) break;
+ }
+
+ // Update changed lines whole.
+ const text = changedLines.join('\n');
+ this.textarea.focus();
+ this.textarea.setSelectionRange(editStart, editEnd);
+ if (!document.execCommand('insertText', false, text)) {
+ // execCommand is deprecated, but setRangeText (and any other direct value modifications) erases the native undo history.
+ // So only fall back to it if execCommand fails.
+ this.textarea.setRangeText(text);
+ }
+
+ // Set selection to (effectively) be the same as before.
+ this.textarea.setSelectionRange(newStart, Math.max(newStart, newEnd));
+ }
+
+ breakLine() {
+ const [start, end] = [this.textarea.selectionStart, this.textarea.selectionEnd];
+
+ // Do nothing if a range is selected
+ if (start !== end) return false;
+
+ const value = this.textarea.value;
+ // Find the beginning of the current line.
+ const lineStart = Math.max(0, value.lastIndexOf('\n', start - 1) + 1);
+ // Find the end and extract the line.
+ const lineEnd = value.indexOf('\n', start);
+ const line = value.slice(lineStart, lineEnd < 0 ? value.length : lineEnd);
+ // Match any whitespace at the start + any repeatable prefix + exactly one space after.
+ const prefix = line.match(/^\s*((\d+)[.)]\s|[-*+]\s+(\[[ x]\]\s?)?|(>\s+)+)?/);
+
+ // Defer to browser if we can't do anything more useful, or if the cursor is inside the prefix.
+ if (!prefix || !prefix[0].length || lineStart + prefix[0].length > start) return false;
+
+ // Insert newline + prefix.
+ let text = `\n${prefix[0]}`;
+ // Increment a number if present. (perhaps detecting repeating 1. and not doing that then would be a good idea)
+ const num = text.match(/\d+/);
+ if (num) text = text.replace(num[0], Number(num[0]) + 1);
+ text = text.replace('[x]', '[ ]');
+
+ if (!document.execCommand('insertText', false, text)) {
+ this.textarea.setRangeText(text);
+ }
+
+ return true;
+ }
+
+ get userPreferredEditor() {
+ return window.localStorage.getItem(`markdown-editor-${this.options.useScene ?? 'default'}`);
+ }
+ set userPreferredEditor(s) {
+ window.localStorage.setItem(`markdown-editor-${this.options.useScene ?? 'default'}`, s);
+ }
+}
+
+export function getComboMarkdownEditor(el) {
+ if (el instanceof $) el = el[0];
+ return el?._giteaComboMarkdownEditor;
+}
+
+export async function initComboMarkdownEditor(container, options = {}) {
+ if (container instanceof $) {
+ if (container.length !== 1) {
+ throw new Error('initComboMarkdownEditor: container must be a single element');
+ }
+ container = container[0];
+ }
+ if (!container) {
+ throw new Error('initComboMarkdownEditor: container is null');
+ }
+ const editor = new ComboMarkdownEditor(container, options);
+ await editor.init();
+ return editor;
+}
diff --git a/web_src/js/features/comp/ConfirmModal.js b/web_src/js/features/comp/ConfirmModal.js
new file mode 100644
index 0000000..e64996a
--- /dev/null
+++ b/web_src/js/features/comp/ConfirmModal.js
@@ -0,0 +1,30 @@
+import $ from 'jquery';
+import {svg} from '../../svg.js';
+import {htmlEscape} from 'escape-goat';
+
+const {i18n} = window.config;
+
+export async function confirmModal(opts = {content: '', buttonColor: 'primary'}) {
+ return new Promise((resolve) => {
+ const $modal = $(`
+<div class="ui g-modal-confirm modal">
+ <div class="content">${htmlEscape(opts.content)}</div>
+ <div class="actions">
+ <button class="ui cancel button">${svg('octicon-x')} ${i18n.modal_cancel}</button>
+ <button class="ui ${opts.buttonColor || 'primary'} ok button">${svg('octicon-check')} ${i18n.modal_confirm}</button>
+ </div>
+</div>
+`);
+
+ $modal.appendTo(document.body);
+ $modal.modal({
+ onApprove() {
+ resolve(true);
+ },
+ onHidden() {
+ $modal.remove();
+ resolve(false);
+ },
+ }).modal('show');
+ });
+}
diff --git a/web_src/js/features/comp/EasyMDEToolbarActions.js b/web_src/js/features/comp/EasyMDEToolbarActions.js
new file mode 100644
index 0000000..35abb87
--- /dev/null
+++ b/web_src/js/features/comp/EasyMDEToolbarActions.js
@@ -0,0 +1,152 @@
+import {svg} from '../../svg.js';
+
+export function easyMDEToolbarActions(EasyMDE, editor) {
+ const actions = {
+ '|': '|',
+ 'heading-1': {
+ action: EasyMDE.toggleHeading1,
+ icon: svg('octicon-heading'),
+ title: 'Heading 1',
+ },
+ 'heading-2': {
+ action: EasyMDE.toggleHeading2,
+ icon: svg('octicon-heading'),
+ title: 'Heading 2',
+ },
+ 'heading-3': {
+ action: EasyMDE.toggleHeading3,
+ icon: svg('octicon-heading'),
+ title: 'Heading 3',
+ },
+ 'heading-smaller': {
+ action: EasyMDE.toggleHeadingSmaller,
+ icon: svg('octicon-heading'),
+ title: 'Decrease Heading',
+ },
+ 'heading-bigger': {
+ action: EasyMDE.toggleHeadingBigger,
+ icon: svg('octicon-heading'),
+ title: 'Increase Heading',
+ },
+ 'bold': {
+ action: EasyMDE.toggleBold,
+ icon: svg('octicon-bold'),
+ title: 'Bold',
+ },
+ 'italic': {
+ action: EasyMDE.toggleItalic,
+ icon: svg('octicon-italic'),
+ title: 'Italic',
+ },
+ 'strikethrough': {
+ action: EasyMDE.toggleStrikethrough,
+ icon: svg('octicon-strikethrough'),
+ title: 'Strikethrough',
+ },
+ 'quote': {
+ action: EasyMDE.toggleBlockquote,
+ icon: svg('octicon-quote'),
+ title: 'Quote',
+ },
+ 'code': {
+ action: EasyMDE.toggleCodeBlock,
+ icon: svg('octicon-code'),
+ title: 'Code',
+ },
+ 'link': {
+ action: EasyMDE.drawLink,
+ icon: svg('octicon-link'),
+ title: 'Link',
+ },
+ 'unordered-list': {
+ action: EasyMDE.toggleUnorderedList,
+ icon: svg('octicon-list-unordered'),
+ title: 'Unordered List',
+ },
+ 'ordered-list': {
+ action: EasyMDE.toggleOrderedList,
+ icon: svg('octicon-list-ordered'),
+ title: 'Ordered List',
+ },
+ 'image': {
+ action: EasyMDE.drawImage,
+ icon: svg('octicon-image'),
+ title: 'Image',
+ },
+ 'table': {
+ action: EasyMDE.drawTable,
+ icon: svg('octicon-table'),
+ title: 'Table',
+ },
+ 'horizontal-rule': {
+ action: EasyMDE.drawHorizontalRule,
+ icon: svg('octicon-horizontal-rule'),
+ title: 'Horizontal Rule',
+ },
+ 'preview': {
+ action: EasyMDE.togglePreview,
+ icon: svg('octicon-eye'),
+ title: 'Preview',
+ },
+ 'fullscreen': {
+ action: EasyMDE.toggleFullScreen,
+ icon: svg('octicon-screen-full'),
+ title: 'Fullscreen',
+ },
+ 'side-by-side': {
+ action: EasyMDE.toggleSideBySide,
+ icon: svg('octicon-columns'),
+ title: 'Side by Side',
+ },
+
+ // Forgejo custom actions
+ 'gitea-checkbox-empty': {
+ action(e) {
+ const cm = e.codemirror;
+ cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`);
+ cm.focus();
+ },
+ icon: svg('gitea-empty-checkbox'),
+ title: 'Add Checkbox (empty)',
+ },
+ 'gitea-checkbox-checked': {
+ action(e) {
+ const cm = e.codemirror;
+ cm.replaceSelection(`\n- [x] ${cm.getSelection()}`);
+ cm.focus();
+ },
+ icon: svg('octicon-checkbox'),
+ title: 'Add Checkbox (checked)',
+ },
+ 'gitea-switch-to-textarea': {
+ action: () => {
+ editor.userPreferredEditor = 'textarea';
+ editor.switchToTextarea();
+ },
+ icon: svg('octicon-arrow-switch'),
+ title: 'Revert to simple textarea',
+ },
+ 'gitea-code-inline': {
+ action(e) {
+ const cm = e.codemirror;
+ const selection = cm.getSelection();
+ cm.replaceSelection(`\`${selection}\``);
+ if (!selection) {
+ const cursorPos = cm.getCursor();
+ cm.setCursor(cursorPos.line, cursorPos.ch - 1);
+ }
+ cm.focus();
+ },
+ icon: svg('octicon-chevron-right'),
+ title: 'Add Inline Code',
+ },
+ };
+
+ for (const [key, value] of Object.entries(actions)) {
+ if (typeof value !== 'string') {
+ value.name = key;
+ }
+ }
+
+ return actions;
+}
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
new file mode 100644
index 0000000..2cc75cc
--- /dev/null
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -0,0 +1,96 @@
+import $ from 'jquery';
+
+function isExclusiveScopeName(name) {
+ return /.*[^/]\/[^/].*/.test(name);
+}
+
+function updateExclusiveLabelEdit(form) {
+ const nameInput = document.querySelector(`${form} .label-name-input`);
+ const exclusiveField = document.querySelector(`${form} .label-exclusive-input-field`);
+ const exclusiveCheckbox = document.querySelector(`${form} .label-exclusive-input`);
+ const exclusiveWarning = document.querySelector(`${form} .label-exclusive-warning`);
+
+ if (isExclusiveScopeName(nameInput.value)) {
+ exclusiveField?.classList.remove('muted');
+ exclusiveField?.removeAttribute('aria-disabled');
+ if (exclusiveCheckbox.checked && exclusiveCheckbox.getAttribute('data-exclusive-warn')) {
+ exclusiveWarning?.classList.remove('tw-hidden');
+ } else {
+ exclusiveWarning?.classList.add('tw-hidden');
+ }
+ } else {
+ exclusiveField?.classList.add('muted');
+ exclusiveField?.setAttribute('aria-disabled', 'true');
+ exclusiveWarning?.classList.add('tw-hidden');
+ }
+}
+
+export function initCompLabelEdit(selector) {
+ if (!$(selector).length) return;
+
+ // Create label
+ $('.new-label.button').on('click', () => {
+ updateExclusiveLabelEdit('.new-label');
+ $('.new-label.modal').modal({
+ onApprove() {
+ const form = document.querySelector('.new-label.form');
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return false;
+ }
+ $('.new-label.form').trigger('submit');
+ },
+ }).modal('show');
+ return false;
+ });
+
+ // Edit label
+ $('.edit-label-button').on('click', function () {
+ $('#label-modal-id').val($(this).data('id'));
+
+ const $nameInput = $('.edit-label .label-name-input');
+ $nameInput.val($(this).data('title'));
+
+ const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
+ $isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived');
+
+ const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
+ $exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive');
+ // Warn when label was previously not exclusive and used in issues
+ $exclusiveCheckbox.data('exclusive-warn',
+ $(this).data('num-issues') > 0 &&
+ (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
+ updateExclusiveLabelEdit('.edit-label');
+
+ $('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
+
+ const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
+ colorInput.value = this.getAttribute('data-color');
+ colorInput.dispatchEvent(new Event('input', {bubbles: true}));
+
+ $('.edit-label.modal').modal({
+ onApprove() {
+ const form = document.querySelector('.edit-label.form');
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return false;
+ }
+ $('.edit-label.form').trigger('submit');
+ },
+ }).modal('show');
+ return false;
+ });
+
+ $('.new-label .label-name-input').on('input', () => {
+ updateExclusiveLabelEdit('.new-label');
+ });
+ $('.new-label .label-exclusive-input').on('change', () => {
+ updateExclusiveLabelEdit('.new-label');
+ });
+ $('.edit-label .label-name-input').on('input', () => {
+ updateExclusiveLabelEdit('.edit-label');
+ });
+ $('.edit-label .label-exclusive-input').on('change', () => {
+ updateExclusiveLabelEdit('.edit-label');
+ });
+}
diff --git a/web_src/js/features/comp/Paste.js b/web_src/js/features/comp/Paste.js
new file mode 100644
index 0000000..7e4ecbb
--- /dev/null
+++ b/web_src/js/features/comp/Paste.js
@@ -0,0 +1,144 @@
+import {POST} from '../../modules/fetch.js';
+import {getPastedContent, replaceTextareaSelection} from '../../utils/dom.js';
+import {isUrl} from '../../utils/url.js';
+
+async function uploadFile(file, uploadUrl) {
+ const formData = new FormData();
+ formData.append('file', file, file.name);
+
+ const res = await POST(uploadUrl, {data: formData});
+ return await res.json();
+}
+
+function triggerEditorContentChanged(target) {
+ target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
+}
+
+class TextareaEditor {
+ constructor(editor) {
+ this.editor = editor;
+ }
+
+ insertPlaceholder(value) {
+ const editor = this.editor;
+ const startPos = editor.selectionStart;
+ const endPos = editor.selectionEnd;
+ editor.value = editor.value.substring(0, startPos) + value + editor.value.substring(endPos);
+ editor.selectionStart = startPos;
+ editor.selectionEnd = startPos + value.length;
+ editor.focus();
+ triggerEditorContentChanged(editor);
+ }
+
+ replacePlaceholder(oldVal, newVal) {
+ const editor = this.editor;
+ const startPos = editor.selectionStart;
+ const endPos = editor.selectionEnd;
+ if (editor.value.substring(startPos, endPos) === oldVal) {
+ editor.value = editor.value.substring(0, startPos) + newVal + editor.value.substring(endPos);
+ editor.selectionEnd = startPos + newVal.length;
+ } else {
+ editor.value = editor.value.replace(oldVal, newVal);
+ editor.selectionEnd -= oldVal.length;
+ editor.selectionEnd += newVal.length;
+ }
+ editor.selectionStart = editor.selectionEnd;
+ editor.focus();
+ triggerEditorContentChanged(editor);
+ }
+}
+
+class CodeMirrorEditor {
+ constructor(editor) {
+ this.editor = editor;
+ }
+
+ insertPlaceholder(value) {
+ const editor = this.editor;
+ const startPoint = editor.getCursor('start');
+ const endPoint = editor.getCursor('end');
+ editor.replaceSelection(value);
+ endPoint.ch = startPoint.ch + value.length;
+ editor.setSelection(startPoint, endPoint);
+ editor.focus();
+ triggerEditorContentChanged(editor.getTextArea());
+ }
+
+ replacePlaceholder(oldVal, newVal) {
+ const editor = this.editor;
+ const endPoint = editor.getCursor('end');
+ if (editor.getSelection() === oldVal) {
+ editor.replaceSelection(newVal);
+ } else {
+ editor.setValue(editor.getValue().replace(oldVal, newVal));
+ }
+ endPoint.ch -= oldVal.length;
+ endPoint.ch += newVal.length;
+ editor.setSelection(endPoint, endPoint);
+ editor.focus();
+ triggerEditorContentChanged(editor.getTextArea());
+ }
+}
+
+async function handleClipboardImages(editor, dropzone, images, e) {
+ const uploadUrl = dropzone.getAttribute('data-upload-url');
+ const filesContainer = dropzone.querySelector('.files');
+
+ if (!dropzone || !uploadUrl || !filesContainer || !images.length) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ for (const img of images) {
+ const name = img.name.slice(0, img.name.lastIndexOf('.'));
+
+ const placeholder = `![${name}](uploading ...)`;
+ editor.insertPlaceholder(placeholder);
+
+ const {uuid} = await uploadFile(img, uploadUrl);
+
+ const url = `/attachments/${uuid}`;
+ const text = `![${name}](${url})`;
+ editor.replacePlaceholder(placeholder, text);
+
+ const input = document.createElement('input');
+ input.setAttribute('name', 'files');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('id', uuid);
+ input.value = uuid;
+ filesContainer.append(input);
+ }
+}
+
+function handleClipboardText(textarea, text, e) {
+ // when pasting links over selected text, turn it into [text](link), except when shift key is held
+ const {value, selectionStart, selectionEnd, _shiftDown} = textarea;
+ if (_shiftDown) return;
+ const selectedText = value.substring(selectionStart, selectionEnd);
+ const trimmedText = text.trim();
+ if (selectedText && isUrl(trimmedText) && !isUrl(selectedText)) {
+ e.stopPropagation();
+ e.preventDefault();
+ replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`);
+ }
+}
+
+export function initEasyMDEPaste(easyMDE, dropzone) {
+ easyMDE.codemirror.on('paste', (_, e) => {
+ const {images} = getPastedContent(e);
+ if (images.length) {
+ handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e);
+ }
+ });
+}
+
+export function initTextareaPaste(textarea, dropzone) {
+ textarea.addEventListener('paste', (e) => {
+ const {images, text} = getPastedContent(e);
+ if (images.length) {
+ handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
+ } else if (text) {
+ handleClipboardText(textarea, text, e);
+ }
+ });
+}
diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js
new file mode 100644
index 0000000..e6d7080
--- /dev/null
+++ b/web_src/js/features/comp/QuickSubmit.js
@@ -0,0 +1,17 @@
+export function handleGlobalEnterQuickSubmit(target) {
+ const form = target.closest('form');
+ if (form) {
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return;
+ }
+
+ // here use the event to trigger the submit event (instead of calling `submit()` method directly)
+ // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
+ form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
+ } else {
+ // if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
+ // the 'ce-' prefix means this is a CustomEvent
+ target.dispatchEvent(new CustomEvent('ce-quick-submit', {bubbles: true}));
+ }
+}
diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js
new file mode 100644
index 0000000..fd4601f
--- /dev/null
+++ b/web_src/js/features/comp/ReactionSelector.js
@@ -0,0 +1,38 @@
+import $ from 'jquery';
+import {POST} from '../../modules/fetch.js';
+
+export function initCompReactionSelector($parent) {
+ $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {
+ e.preventDefault();
+
+ if (this.classList.contains('disabled')) return;
+
+ const actionUrl = this.closest('[data-action-url]')?.getAttribute('data-action-url');
+ const reactionContent = this.getAttribute('data-reaction-content');
+ const hasReacted = this.closest('.comment')?.querySelector(`.ui.segment.reactions a[data-reaction-content="${reactionContent}"]`)?.getAttribute('data-has-reacted') === 'true';
+
+ const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
+ data: new URLSearchParams({content: reactionContent}),
+ });
+
+ const data = await res.json();
+ if (data && (data.html || data.empty)) {
+ const $content = $(this).closest('.content');
+ let $react = $content.find('.segment.reactions');
+ if ((!data.empty || data.html === '') && $react.length > 0) {
+ $react.remove();
+ }
+ if (!data.empty) {
+ const $attachments = $content.find('.segment.bottom:first');
+ $react = $(data.html);
+ if ($attachments.length > 0) {
+ $react.insertBefore($attachments);
+ } else {
+ $react.appendTo($content);
+ }
+ $react.find('.dropdown').dropdown();
+ initCompReactionSelector($react);
+ }
+ }
+ });
+}
diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js
new file mode 100644
index 0000000..081c474
--- /dev/null
+++ b/web_src/js/features/comp/SearchUserBox.js
@@ -0,0 +1,51 @@
+import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
+
+const {appSubUrl} = window.config;
+const looksLikeEmailAddressCheck = /^\S+@\S+$/;
+
+export function initCompSearchUserBox() {
+ const searchUserBox = document.getElementById('search-user-box');
+ if (!searchUserBox) return;
+
+ const $searchUserBox = $(searchUserBox);
+ const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
+ const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
+ $searchUserBox.search({
+ minCharacters: 2,
+ apiSettings: {
+ url: `${appSubUrl}/user/search?active=1&q={query}`,
+ onResponse(response) {
+ const items = [];
+ const searchQuery = $searchUserBox.find('input').val();
+ const searchQueryUppercase = searchQuery.toUpperCase();
+ $.each(response.data, (_i, item) => {
+ const resultItem = {
+ title: item.login,
+ image: item.avatar_url,
+ };
+ if (item.full_name) {
+ resultItem.description = htmlEscape(item.full_name);
+ }
+ if (searchQueryUppercase === item.login.toUpperCase()) {
+ items.unshift(resultItem);
+ } else {
+ items.push(resultItem);
+ }
+ });
+
+ if (allowEmailInput && !items.length && looksLikeEmailAddressCheck.test(searchQuery)) {
+ const resultItem = {
+ title: searchQuery,
+ description: allowEmailDescription,
+ };
+ items.push(resultItem);
+ }
+
+ return {results: items};
+ },
+ },
+ searchFields: ['login', 'full_name'],
+ showNoResults: false,
+ });
+}
diff --git a/web_src/js/features/comp/TextExpander.js b/web_src/js/features/comp/TextExpander.js
new file mode 100644
index 0000000..128a2dd
--- /dev/null
+++ b/web_src/js/features/comp/TextExpander.js
@@ -0,0 +1,61 @@
+import {matchEmoji, matchMention} from '../../utils/match.js';
+import {emojiString} from '../emoji.js';
+
+export function initTextExpander(expander) {
+ expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
+ if (key === ':') {
+ const matches = matchEmoji(text);
+ if (!matches.length) return provide({matched: false});
+
+ const ul = document.createElement('ul');
+ ul.classList.add('suggestions');
+ for (const name of matches) {
+ const emoji = emojiString(name);
+ const li = document.createElement('li');
+ li.setAttribute('role', 'option');
+ li.setAttribute('data-value', emoji);
+ li.textContent = `${emoji} ${name}`;
+ ul.append(li);
+ }
+
+ provide({matched: true, fragment: ul});
+ } else if (key === '@') {
+ const matches = matchMention(text);
+ if (!matches.length) return provide({matched: false});
+
+ const ul = document.createElement('ul');
+ ul.classList.add('suggestions');
+ for (const {value, name, fullname, avatar} of matches) {
+ const li = document.createElement('li');
+ li.setAttribute('role', 'option');
+ li.setAttribute('data-value', `${key}${value}`);
+
+ const img = document.createElement('img');
+ img.src = avatar;
+ li.append(img);
+
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = name;
+ li.append(nameSpan);
+
+ if (fullname && fullname.toLowerCase() !== name) {
+ const fullnameSpan = document.createElement('span');
+ fullnameSpan.classList.add('fullname');
+ fullnameSpan.textContent = fullname;
+ li.append(fullnameSpan);
+ }
+
+ ul.append(li);
+ }
+
+ provide({matched: true, fragment: ul});
+ }
+ });
+ expander?.addEventListener('text-expander-value', ({detail}) => {
+ if (detail?.item) {
+ // add a space after @mentions as it's likely the user wants one
+ const suffix = detail.key === '@' ? ' ' : '';
+ detail.value = `${detail.item.getAttribute('data-value')}${suffix}`;
+ }
+ });
+}
diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js
new file mode 100644
index 0000000..522daa1
--- /dev/null
+++ b/web_src/js/features/comp/WebHookEditor.js
@@ -0,0 +1,28 @@
+import {POST} from '../../modules/fetch.js';
+import {toggleElem} from '../../utils/dom.js';
+
+export function initCompWebHookEditor() {
+ if (!document.querySelectorAll('.new.webhook').length) {
+ return;
+ }
+
+ // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field
+ const httpMethodInput = document.getElementById('http_method');
+ if (httpMethodInput) {
+ const updateContentType = function () {
+ const visible = httpMethodInput.value === 'POST';
+ toggleElem(document.getElementById('content_type').closest('.field'), visible);
+ };
+ updateContentType();
+ httpMethodInput.addEventListener('change', updateContentType);
+ }
+
+ // Test delivery
+ document.getElementById('test-delivery')?.addEventListener('click', async function () {
+ this.classList.add('is-loading', 'disabled');
+ await POST(this.getAttribute('data-link'));
+ setTimeout(() => {
+ window.location.href = this.getAttribute('data-redirect');
+ }, 5000);
+ });
+}
diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
new file mode 100644
index 0000000..ce90f3e
--- /dev/null
+++ b/web_src/js/features/contextpopup.js
@@ -0,0 +1,43 @@
+import {createApp} from 'vue';
+import ContextPopup from '../components/ContextPopup.vue';
+import {parseIssueHref} from '../utils.js';
+import {createTippy} from '../modules/tippy.js';
+
+export function initContextPopups() {
+ const refIssues = document.querySelectorAll('.ref-issue');
+ attachRefIssueContextPopup(refIssues);
+}
+
+export function attachRefIssueContextPopup(refIssues) {
+ for (const refIssue of refIssues) {
+ if (refIssue.classList.contains('ref-external-issue')) {
+ return;
+ }
+
+ const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
+ if (!owner) return;
+
+ const el = document.createElement('div');
+ refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
+
+ const view = createApp(ContextPopup);
+
+ try {
+ view.mount(el);
+ } catch (err) {
+ console.error(err);
+ el.textContent = 'ContextPopup failed to load';
+ }
+
+ createTippy(refIssue, {
+ content: el,
+ placement: 'top-start',
+ interactive: true,
+ role: 'dialog',
+ interactiveBorder: 5,
+ onShow: () => {
+ el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
+ },
+ });
+ }
+}
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
new file mode 100644
index 0000000..79b3389
--- /dev/null
+++ b/web_src/js/features/contributors.js
@@ -0,0 +1,29 @@
+import {createApp} from 'vue';
+
+export async function initRepoContributors() {
+ const el = document.getElementById('repo-contributors-chart');
+ if (!el) return;
+
+ const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
+ try {
+ const View = createApp(RepoContributors, {
+ repoLink: el.getAttribute('data-repo-link'),
+ locale: {
+ filterLabel: el.getAttribute('data-locale-filter-label'),
+ contributionType: {
+ commits: el.getAttribute('data-locale-contribution-type-commits'),
+ additions: el.getAttribute('data-locale-contribution-type-additions'),
+ deletions: el.getAttribute('data-locale-contribution-type-deletions'),
+ },
+
+ loadingTitle: el.getAttribute('data-locale-loading-title'),
+ loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+ loadingInfo: el.getAttribute('data-locale-loading-info'),
+ },
+ });
+ View.mount(el);
+ } catch (err) {
+ console.error('RepoContributors failed to load', err);
+ el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+ }
+}
diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js
new file mode 100644
index 0000000..03efe00
--- /dev/null
+++ b/web_src/js/features/copycontent.js
@@ -0,0 +1,56 @@
+import {clippie} from 'clippie';
+import {showTemporaryTooltip} from '../modules/tippy.js';
+import {convertImage} from '../utils.js';
+import {GET} from '../modules/fetch.js';
+
+const {i18n} = window.config;
+
+export function initCopyContent() {
+ const btn = document.getElementById('copy-content');
+ if (!btn || btn.classList.contains('disabled')) return;
+
+ btn.addEventListener('click', async () => {
+ if (btn.classList.contains('is-loading')) return;
+ let content;
+ let isRasterImage = false;
+ const link = btn.getAttribute('data-link');
+
+ // when data-link is present, we perform a fetch. this is either because
+ // the text to copy is not in the DOM or it is an image which should be
+ // fetched to copy in full resolution
+ if (link) {
+ btn.classList.add('is-loading', 'loading-icon-2px');
+ try {
+ const res = await GET(link, {credentials: 'include', redirect: 'follow'});
+ const contentType = res.headers.get('content-type');
+
+ if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {
+ isRasterImage = true;
+ content = await res.blob();
+ } else {
+ content = await res.text();
+ }
+ } catch {
+ return showTemporaryTooltip(btn, i18n.copy_error);
+ } finally {
+ btn.classList.remove('is-loading', 'loading-icon-2px');
+ }
+ } else { // text, read from DOM
+ const lineEls = document.querySelectorAll('.file-view .lines-code');
+ content = Array.from(lineEls, (el) => el.textContent).join('');
+ }
+
+ // try copy original first, if that fails and it's an image, convert it to png
+ const success = await clippie(content);
+ if (success) {
+ showTemporaryTooltip(btn, i18n.copy_success);
+ } else {
+ if (isRasterImage) {
+ const success = await clippie(await convertImage(content, 'image/png'));
+ showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error);
+ } else {
+ showTemporaryTooltip(btn, i18n.copy_error);
+ }
+ }
+ });
+}
diff --git a/web_src/js/features/dropzone.js b/web_src/js/features/dropzone.js
new file mode 100644
index 0000000..e7b8a9d
--- /dev/null
+++ b/web_src/js/features/dropzone.js
@@ -0,0 +1,7 @@
+export async function createDropzone(el, opts) {
+ const [{Dropzone}] = await Promise.all([
+ import(/* webpackChunkName: "dropzone" */'dropzone'),
+ import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
+ ]);
+ return new Dropzone(el, opts);
+}
diff --git a/web_src/js/features/emoji.js b/web_src/js/features/emoji.js
new file mode 100644
index 0000000..032a3ef
--- /dev/null
+++ b/web_src/js/features/emoji.js
@@ -0,0 +1,38 @@
+import emojis from '../../../assets/emoji.json';
+
+const {assetUrlPrefix, customEmojis} = window.config;
+
+const tempMap = {...customEmojis};
+for (const {emoji, aliases} of emojis) {
+ for (const alias of aliases || []) {
+ tempMap[alias] = emoji;
+ }
+}
+
+export const emojiKeys = Object.keys(tempMap).sort((a, b) => {
+ if (a === '+1' || a === '-1') return -1;
+ if (b === '+1' || b === '-1') return 1;
+ return a.localeCompare(b);
+});
+
+const emojiMap = {};
+for (const key of emojiKeys) {
+ emojiMap[key] = tempMap[key];
+}
+
+// retrieve HTML for given emoji name
+export function emojiHTML(name) {
+ let inner;
+ if (Object.hasOwn(customEmojis, name)) {
+ inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`;
+ } else {
+ inner = emojiString(name);
+ }
+
+ return `<span class="emoji" title=":${name}:">${inner}</span>`;
+}
+
+// retrieve string for given emoji name
+export function emojiString(name) {
+ return emojiMap[name] || `:${name}:`;
+}
diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js
new file mode 100644
index 0000000..62581cf
--- /dev/null
+++ b/web_src/js/features/eventsource.sharedworker.js
@@ -0,0 +1,141 @@
+const sourcesByUrl = {};
+const sourcesByPort = {};
+
+class Source {
+ constructor(url) {
+ this.url = url;
+ this.eventSource = new EventSource(url);
+ this.listening = {};
+ this.clients = [];
+ this.listen('open');
+ this.listen('close');
+ this.listen('logout');
+ this.listen('notification-count');
+ this.listen('stopwatches');
+ this.listen('error');
+ }
+
+ register(port) {
+ if (this.clients.includes(port)) return;
+
+ this.clients.push(port);
+
+ port.postMessage({
+ type: 'status',
+ message: `registered to ${this.url}`,
+ });
+ }
+
+ deregister(port) {
+ const portIdx = this.clients.indexOf(port);
+ if (portIdx < 0) {
+ return this.clients.length;
+ }
+ this.clients.splice(portIdx, 1);
+ return this.clients.length;
+ }
+
+ close() {
+ if (!this.eventSource) return;
+
+ this.eventSource.close();
+ this.eventSource = null;
+ }
+
+ listen(eventType) {
+ if (this.listening[eventType]) return;
+ this.listening[eventType] = true;
+ this.eventSource.addEventListener(eventType, (event) => {
+ this.notifyClients({
+ type: eventType,
+ data: event.data,
+ });
+ });
+ }
+
+ notifyClients(event) {
+ for (const client of this.clients) {
+ client.postMessage(event);
+ }
+ }
+
+ status(port) {
+ port.postMessage({
+ type: 'status',
+ message: `url: ${this.url} readyState: ${this.eventSource.readyState}`,
+ });
+ }
+}
+
+self.addEventListener('connect', (e) => {
+ for (const port of e.ports) {
+ port.addEventListener('message', (event) => {
+ if (!self.EventSource) {
+ // some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope.
+ // this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller,
+ // in case the caller would like to use a fallback method to do its work.
+ port.postMessage({type: 'no-event-source'});
+ return;
+ }
+ if (event.data.type === 'start') {
+ const url = event.data.url;
+ if (sourcesByUrl[url]) {
+ // we have a Source registered to this url
+ const source = sourcesByUrl[url];
+ source.register(port);
+ sourcesByPort[port] = source;
+ return;
+ }
+ let source = sourcesByPort[port];
+ if (source) {
+ if (source.eventSource && source.url === url) return;
+
+ // How this has happened I don't understand...
+ // deregister from that source
+ const count = source.deregister(port);
+ // Clean-up
+ if (count === 0) {
+ source.close();
+ sourcesByUrl[source.url] = null;
+ }
+ }
+ // Create a new Source
+ source = new Source(url);
+ source.register(port);
+ sourcesByUrl[url] = source;
+ sourcesByPort[port] = source;
+ } else if (event.data.type === 'listen') {
+ const source = sourcesByPort[port];
+ source.listen(event.data.eventType);
+ } else if (event.data.type === 'close') {
+ const source = sourcesByPort[port];
+
+ if (!source) return;
+
+ const count = source.deregister(port);
+ if (count === 0) {
+ source.close();
+ sourcesByUrl[source.url] = null;
+ sourcesByPort[port] = null;
+ }
+ } else if (event.data.type === 'status') {
+ const source = sourcesByPort[port];
+ if (!source) {
+ port.postMessage({
+ type: 'status',
+ message: 'not connected',
+ });
+ return;
+ }
+ source.status(port);
+ } else {
+ // just send it back
+ port.postMessage({
+ type: 'error',
+ message: `received but don't know how to handle: ${event.data}`,
+ });
+ }
+ });
+ port.start();
+ }
+});
diff --git a/web_src/js/features/file-fold.js b/web_src/js/features/file-fold.js
new file mode 100644
index 0000000..3efefaf
--- /dev/null
+++ b/web_src/js/features/file-fold.js
@@ -0,0 +1,19 @@
+import {svg} from '../svg.js';
+
+// Hides the file if newFold is true, and shows it otherwise. The actual hiding is performed using CSS.
+//
+// The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class.
+// The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class.
+//
+export function setFileFolding(fileContentBox, foldArrow, newFold) {
+ foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18);
+ fileContentBox.setAttribute('data-folded', newFold);
+ if (newFold && fileContentBox.getBoundingClientRect().top < 0) {
+ fileContentBox.scrollIntoView();
+ }
+}
+
+// Like `setFileFolding`, except that it automatically inverts the current file folding state.
+export function invertFileFolding(fileContentBox, foldArrow) {
+ setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true');
+}
diff --git a/web_src/js/features/heatmap.js b/web_src/js/features/heatmap.js
new file mode 100644
index 0000000..688f06c
--- /dev/null
+++ b/web_src/js/features/heatmap.js
@@ -0,0 +1,40 @@
+import {createApp} from 'vue';
+import ActivityHeatmap from '../components/ActivityHeatmap.vue';
+import {translateMonth, translateDay} from '../utils.js';
+
+export function initHeatmap() {
+ const el = document.getElementById('user-heatmap');
+ if (!el) return;
+
+ try {
+ const heatmap = {};
+ for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) {
+ // Convert to user timezone and sum contributions by date
+ const dateStr = new Date(timestamp * 1000).toDateString();
+ heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
+ }
+
+ const values = Object.keys(heatmap).map((v) => {
+ return {date: new Date(v), count: heatmap[v]};
+ });
+
+ const locale = {
+ months: new Array(12).fill().map((_, idx) => translateMonth(idx)),
+ days: new Array(7).fill().map((_, idx) => translateDay(idx)),
+ contributions_in_the_last_12_months: el.getAttribute('data-locale-total-contributions'),
+ contributions_zero: el.getAttribute('data-locale-contributions-zero'),
+ contributions_format: el.getAttribute('data-locale-contributions-format'),
+ contributions_one: el.getAttribute('data-locale-contributions-one'),
+ contributions_few: el.getAttribute('data-locale-contributions-few'),
+ more: el.getAttribute('data-locale-more'),
+ less: el.getAttribute('data-locale-less'),
+ };
+
+ const View = createApp(ActivityHeatmap, {values, locale});
+ View.mount(el);
+ el.classList.remove('is-loading');
+ } catch (err) {
+ console.error('Heatmap failed to load', err);
+ el.textContent = 'Heatmap failed to load';
+ }
+}
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
new file mode 100644
index 0000000..d1b139f
--- /dev/null
+++ b/web_src/js/features/imagediff.js
@@ -0,0 +1,271 @@
+import $ from 'jquery';
+import {GET} from '../modules/fetch.js';
+import {hideElem, loadElem, queryElemChildren} from '../utils/dom.js';
+import {parseDom} from '../utils.js';
+
+function getDefaultSvgBoundsIfUndefined(text, src) {
+ const DefaultSize = 300;
+ const MaxSize = 99999;
+
+ const svgDoc = parseDom(text, 'image/svg+xml');
+ const svg = svgDoc.documentElement;
+ const width = svg?.width?.baseVal;
+ const height = svg?.height?.baseVal;
+ if (width === undefined || height === undefined) {
+ return null; // in case some svg is invalid or doesn't have the width/height
+ }
+ if (width.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE || height.unitType === SVGLength.SVG_LENGTHTYPE_PERCENTAGE) {
+ const img = new Image();
+ img.src = src;
+ if (img.width > 1 && img.width < MaxSize && img.height > 1 && img.height < MaxSize) {
+ return {
+ width: img.width,
+ height: img.height,
+ };
+ }
+ if (svg.hasAttribute('viewBox')) {
+ const viewBox = svg.viewBox.baseVal;
+ return {
+ width: DefaultSize,
+ height: DefaultSize * viewBox.width / viewBox.height,
+ };
+ }
+ return {
+ width: DefaultSize,
+ height: DefaultSize,
+ };
+ }
+ return null;
+}
+
+function createContext(imageAfter, imageBefore) {
+ const sizeAfter = {
+ width: imageAfter?.width || 0,
+ height: imageAfter?.height || 0,
+ };
+ const sizeBefore = {
+ width: imageBefore?.width || 0,
+ height: imageBefore?.height || 0,
+ };
+ const maxSize = {
+ width: Math.max(sizeBefore.width, sizeAfter.width),
+ height: Math.max(sizeBefore.height, sizeAfter.height),
+ };
+
+ return {
+ imageAfter,
+ imageBefore,
+ sizeAfter,
+ sizeBefore,
+ maxSize,
+ ratio: [
+ Math.floor(maxSize.width - sizeAfter.width) / 2,
+ Math.floor(maxSize.height - sizeAfter.height) / 2,
+ Math.floor(maxSize.width - sizeBefore.width) / 2,
+ Math.floor(maxSize.height - sizeBefore.height) / 2,
+ ],
+ };
+}
+
+export function initImageDiff() {
+ $('.image-diff:not([data-image-diff-loaded])').each(async function() {
+ const $container = $(this);
+ this.setAttribute('data-image-diff-loaded', 'true');
+
+ // the container may be hidden by "viewed" checkbox, so use the parent's width for reference
+ const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100);
+
+ const imageInfos = [{
+ path: this.getAttribute('data-path-after'),
+ mime: this.getAttribute('data-mime-after'),
+ $images: $container.find('img.image-after'), // matches 3 <img>
+ $boundsInfo: $container.find('.bounds-info-after'),
+ }, {
+ path: this.getAttribute('data-path-before'),
+ mime: this.getAttribute('data-mime-before'),
+ $images: $container.find('img.image-before'), // matches 3 <img>
+ $boundsInfo: $container.find('.bounds-info-before'),
+ }];
+
+ await Promise.all(imageInfos.map(async (info) => {
+ const [success] = await Promise.all(Array.from(info.$images, (img) => {
+ return loadElem(img, info.path);
+ }));
+ // only the first images is associated with $boundsInfo
+ if (!success) info.$boundsInfo.text('(image error)');
+ if (info.mime === 'image/svg+xml') {
+ const resp = await GET(info.path);
+ const text = await resp.text();
+ const bounds = getDefaultSvgBoundsIfUndefined(text, info.path);
+ if (bounds) {
+ info.$images.each(function() {
+ this.setAttribute('width', bounds.width);
+ this.setAttribute('height', bounds.height);
+ });
+ hideElem(info.$boundsInfo);
+ }
+ }
+ }));
+
+ const $imagesAfter = imageInfos[0].$images;
+ const $imagesBefore = imageInfos[1].$images;
+
+ initSideBySide(this, createContext($imagesAfter[0], $imagesBefore[0]));
+ if ($imagesAfter.length > 0 && $imagesBefore.length > 0) {
+ initSwipe(createContext($imagesAfter[1], $imagesBefore[1]));
+ initOverlay(createContext($imagesAfter[2], $imagesBefore[2]));
+ }
+
+ queryElemChildren(this, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
+
+ function initSideBySide(container, sizes) {
+ let factor = 1;
+ if (sizes.maxSize.width > (diffContainerWidth - 24) / 2) {
+ factor = (diffContainerWidth - 24) / 2 / sizes.maxSize.width;
+ }
+
+ const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth;
+ const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight;
+ if (sizes.imageAfter) {
+ const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width');
+ if (boundsInfoAfterWidth) {
+ boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`;
+ boundsInfoAfterWidth.classList.toggle('green', widthChanged);
+ }
+ const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height');
+ if (boundsInfoAfterHeight) {
+ boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`;
+ boundsInfoAfterHeight.classList.toggle('green', heightChanged);
+ }
+ }
+
+ if (sizes.imageBefore) {
+ const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width');
+ if (boundsInfoBeforeWidth) {
+ boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`;
+ boundsInfoBeforeWidth.classList.toggle('red', widthChanged);
+ }
+ const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height');
+ if (boundsInfoBeforeHeight) {
+ boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`;
+ boundsInfoBeforeHeight.classList.add('red', heightChanged);
+ }
+ }
+
+ if (sizes.imageAfter) {
+ const container = sizes.imageAfter.parentNode;
+ sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
+ sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
+ container.style.margin = '10px auto';
+ container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
+ container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
+ }
+
+ if (sizes.imageBefore) {
+ const container = sizes.imageBefore.parentNode;
+ sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
+ sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
+ container.style.margin = '10px auto';
+ container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
+ container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
+ }
+ }
+
+ function initSwipe(sizes) {
+ let factor = 1;
+ if (sizes.maxSize.width > diffContainerWidth - 12) {
+ factor = (diffContainerWidth - 12) / sizes.maxSize.width;
+ }
+
+ if (sizes.imageAfter) {
+ const container = sizes.imageAfter.parentNode;
+ const swipeFrame = container.parentNode;
+ sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
+ sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
+ container.style.margin = `0px ${sizes.ratio[0] * factor}px`;
+ container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
+ container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
+ swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`;
+ swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
+ }
+
+ if (sizes.imageBefore) {
+ const container = sizes.imageBefore.parentNode;
+ const swipeFrame = container.parentNode;
+ sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
+ sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
+ container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
+ container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
+ container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
+ swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
+ swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
+ }
+
+ // extra height for inner "position: absolute" elements
+ const swipe = $container.find('.diff-swipe')[0];
+ if (swipe) {
+ swipe.style.width = `${sizes.maxSize.width * factor + 2}px`;
+ swipe.style.height = `${sizes.maxSize.height * factor + 30}px`;
+ }
+
+ $container.find('.swipe-bar').on('mousedown', function(e) {
+ e.preventDefault();
+
+ const $swipeBar = $(this);
+ const $swipeFrame = $swipeBar.parent();
+ const width = $swipeFrame.width() - $swipeBar.width() - 2;
+
+ $(document).on('mousemove.diff-swipe', (e2) => {
+ e2.preventDefault();
+
+ const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width));
+ $swipeBar[0].style.left = `${value}px`;
+ $container.find('.swipe-container')[0].style.width = `${$swipeFrame.width() - value}px`;
+
+ $(document).on('mouseup.diff-swipe', () => {
+ $(document).off('.diff-swipe');
+ });
+ });
+ });
+ }
+
+ function initOverlay(sizes) {
+ let factor = 1;
+ if (sizes.maxSize.width > diffContainerWidth - 12) {
+ factor = (diffContainerWidth - 12) / sizes.maxSize.width;
+ }
+
+ if (sizes.imageAfter) {
+ const container = sizes.imageAfter.parentNode;
+ sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
+ sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
+ container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`;
+ container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
+ container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
+ }
+
+ if (sizes.imageBefore) {
+ const container = sizes.imageBefore.parentNode;
+ const overlayFrame = container.parentNode;
+ sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
+ sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
+ container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
+ container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
+ container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
+
+ // some inner elements are `position: absolute`, so the container's height must be large enough
+ overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
+ overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
+ }
+
+ const rangeInput = $container[0].querySelector('input[type="range"]');
+ function updateOpacity() {
+ if (sizes.imageAfter) {
+ sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`;
+ }
+ }
+ rangeInput?.addEventListener('input', updateOpacity);
+ updateOpacity();
+ }
+ });
+}
diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js
new file mode 100644
index 0000000..897f5fb
--- /dev/null
+++ b/web_src/js/features/install.js
@@ -0,0 +1,119 @@
+import {hideElem, showElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
+
+export function initInstall() {
+ const page = document.querySelector('.page-content.install');
+ if (!page) {
+ return;
+ }
+ if (page.classList.contains('post-install')) {
+ initPostInstall();
+ } else {
+ initPreInstall();
+ }
+}
+function initPreInstall() {
+ const defaultDbUser = 'forgejo';
+ const defaultDbName = 'forgejo';
+
+ const defaultDbHosts = {
+ mysql: '127.0.0.1:3306',
+ postgres: '127.0.0.1:5432',
+ };
+
+ const dbHost = document.getElementById('db_host');
+ const dbUser = document.getElementById('db_user');
+ const dbName = document.getElementById('db_name');
+
+ // Database type change detection.
+ document.getElementById('db_type').addEventListener('change', function () {
+ const dbType = this.value;
+ hideElem('div[data-db-setting-for]');
+ showElem(`div[data-db-setting-for=${dbType}]`);
+
+ if (dbType !== 'sqlite3') {
+ // for most remote database servers
+ showElem('div[data-db-setting-for=common-host]');
+ const lastDbHost = dbHost.value;
+ const isDbHostDefault = !lastDbHost || Object.values(defaultDbHosts).includes(lastDbHost);
+ if (isDbHostDefault) {
+ dbHost.value = defaultDbHosts[dbType] ?? '';
+ }
+ if (!dbUser.value && !dbName.value) {
+ dbUser.value = defaultDbUser;
+ dbName.value = defaultDbName;
+ }
+ } // else: for SQLite3, the default path is always prepared by backend code (setting)
+ });
+ document.getElementById('db_type').dispatchEvent(new Event('change'));
+
+ const appUrl = document.getElementById('app_url');
+ if (appUrl.value.includes('://localhost')) {
+ appUrl.value = window.location.href;
+ }
+
+ const domain = document.getElementById('domain');
+ if (domain.value.trim() === 'localhost') {
+ domain.value = window.location.hostname;
+ }
+
+ // TODO: better handling of exclusive relations.
+ document.querySelector('#offline-mode input').addEventListener('change', function () {
+ if (this.checked) {
+ document.querySelector('#disable-gravatar input').checked = true;
+ document.querySelector('#federated-avatar-lookup input').checked = false;
+ }
+ });
+ document.querySelector('#disable-gravatar input').addEventListener('change', function () {
+ if (this.checked) {
+ document.querySelector('#federated-avatar-lookup input').checked = false;
+ } else {
+ document.querySelector('#offline-mode input').checked = false;
+ }
+ });
+ document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () {
+ if (this.checked) {
+ document.querySelector('#disable-gravatar input').checked = false;
+ document.querySelector('#offline-mode input').checked = false;
+ }
+ });
+ document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
+ if (this.checked) {
+ if (!document.querySelector('#disable-registration input').checked) {
+ document.querySelector('#enable-openid-signup input').checked = true;
+ }
+ } else {
+ document.querySelector('#enable-openid-signup input').checked = false;
+ }
+ });
+ document.querySelector('#disable-registration input').addEventListener('change', function () {
+ if (this.checked) {
+ document.querySelector('#enable-captcha input').checked = false;
+ document.querySelector('#enable-openid-signup input').checked = false;
+ } else {
+ document.querySelector('#enable-openid-signup input').checked = true;
+ }
+ });
+ document.querySelector('#enable-captcha input').addEventListener('change', function () {
+ if (this.checked) {
+ document.querySelector('#disable-registration input').checked = false;
+ }
+ });
+}
+
+function initPostInstall() {
+ const el = document.getElementById('goto-user-login');
+ if (!el) return;
+
+ const targetUrl = el.getAttribute('href');
+ let tid = setInterval(async () => {
+ try {
+ const resp = await GET(targetUrl);
+ if (tid && resp.status === 200) {
+ clearInterval(tid);
+ tid = null;
+ window.location.href = targetUrl;
+ }
+ } catch {}
+ }, 1000);
+}
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
new file mode 100644
index 0000000..b57df9c
--- /dev/null
+++ b/web_src/js/features/notification.js
@@ -0,0 +1,192 @@
+import $ from 'jquery';
+import {GET} from '../modules/fetch.js';
+import {toggleElem} from '../utils/dom.js';
+
+const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
+let notificationSequenceNumber = 0;
+
+export function initNotificationsTable() {
+ const table = document.getElementById('notification_table');
+ if (!table) return;
+
+ // when page restores from bfcache, delete previously clicked items
+ window.addEventListener('pageshow', (e) => {
+ if (e.persisted) { // page was restored from bfcache
+ const table = document.getElementById('notification_table');
+ const unreadCountEl = document.querySelector('.notifications-unread-count');
+ let unreadCount = parseInt(unreadCountEl.textContent);
+ for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) {
+ item.remove();
+ unreadCount -= 1;
+ }
+ unreadCountEl.textContent = unreadCount;
+ }
+ });
+
+ // mark clicked unread links for deletion on bfcache restore
+ for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) {
+ link.addEventListener('click', (e) => {
+ e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
+ });
+ }
+}
+
+async function receiveUpdateCount(event) {
+ try {
+ const data = JSON.parse(event.data);
+
+ for (const count of document.querySelectorAll('.notification_count')) {
+ count.classList.toggle('tw-hidden', data.Count === 0);
+ count.textContent = `${data.Count}`;
+ }
+ await updateNotificationTable();
+ } catch (error) {
+ console.error(error, event);
+ }
+}
+
+export function initNotificationCount() {
+ const $notificationCount = $('.notification_count');
+
+ if (!$notificationCount.length) {
+ return;
+ }
+
+ let usingPeriodicPoller = false;
+ const startPeriodicPoller = (timeout, lastCount) => {
+ if (timeout <= 0 || !Number.isFinite(timeout)) return;
+ usingPeriodicPoller = true;
+ lastCount = lastCount ?? $notificationCount.text();
+ setTimeout(async () => {
+ await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
+ }, timeout);
+ };
+
+ if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
+ // Try to connect to the event source via the shared worker first
+ const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker');
+ worker.addEventListener('error', (event) => {
+ console.error('worker error', event);
+ });
+ worker.port.addEventListener('messageerror', () => {
+ console.error('unable to deserialize message');
+ });
+ worker.port.postMessage({
+ type: 'start',
+ url: `${window.location.origin}${appSubUrl}/user/events`,
+ });
+ worker.port.addEventListener('message', (event) => {
+ if (!event.data || !event.data.type) {
+ console.error('unknown worker message event', event);
+ return;
+ }
+ if (event.data.type === 'notification-count') {
+ const _promise = receiveUpdateCount(event.data);
+ } else if (event.data.type === 'no-event-source') {
+ // browser doesn't support EventSource, falling back to periodic poller
+ if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
+ } else if (event.data.type === 'error') {
+ console.error('worker port event error', event.data);
+ } else if (event.data.type === 'logout') {
+ if (event.data.data !== 'here') {
+ return;
+ }
+ worker.port.postMessage({
+ type: 'close',
+ });
+ worker.port.close();
+ window.location.href = `${window.location.origin}${appSubUrl}/`;
+ } else if (event.data.type === 'close') {
+ worker.port.postMessage({
+ type: 'close',
+ });
+ worker.port.close();
+ }
+ });
+ worker.port.addEventListener('error', (e) => {
+ console.error('worker port error', e);
+ });
+ worker.port.start();
+ window.addEventListener('beforeunload', () => {
+ worker.port.postMessage({
+ type: 'close',
+ });
+ worker.port.close();
+ });
+
+ return;
+ }
+
+ startPeriodicPoller(notificationSettings.MinTimeout);
+}
+
+async function updateNotificationCountWithCallback(callback, timeout, lastCount) {
+ const currentCount = $('.notification_count').text();
+ if (lastCount !== currentCount) {
+ callback(notificationSettings.MinTimeout, currentCount);
+ return;
+ }
+
+ const newCount = await updateNotificationCount();
+ let needsUpdate = false;
+
+ if (lastCount !== newCount) {
+ needsUpdate = true;
+ timeout = notificationSettings.MinTimeout;
+ } else if (timeout < notificationSettings.MaxTimeout) {
+ timeout += notificationSettings.TimeoutStep;
+ }
+
+ callback(timeout, newCount);
+ if (needsUpdate) {
+ await updateNotificationTable();
+ }
+}
+
+async function updateNotificationTable() {
+ const notificationDiv = document.getElementById('notification_div');
+ if (notificationDiv) {
+ try {
+ const params = new URLSearchParams(window.location.search);
+ params.set('div-only', true);
+ params.set('sequence-number', ++notificationSequenceNumber);
+ const url = `${appSubUrl}/notifications?${params.toString()}`;
+ const response = await GET(url);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch notification table');
+ }
+
+ const data = await response.text();
+ if ($(data).data('sequence-number') === notificationSequenceNumber) {
+ notificationDiv.outerHTML = data;
+ initNotificationsTable();
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+}
+
+async function updateNotificationCount() {
+ try {
+ const response = await GET(`${appSubUrl}/notifications/new`);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch notification count');
+ }
+
+ const data = await response.json();
+
+ toggleElem('.notification_count', data.new !== 0);
+
+ for (const el of document.getElementsByClassName('notification_count')) {
+ el.textContent = `${data.new}`;
+ }
+
+ return `${data.new}`;
+ } catch (error) {
+ console.error(error);
+ return '0';
+ }
+}
diff --git a/web_src/js/features/org-team.js b/web_src/js/features/org-team.js
new file mode 100644
index 0000000..9b059b3
--- /dev/null
+++ b/web_src/js/features/org-team.js
@@ -0,0 +1,26 @@
+import $ from 'jquery';
+
+const {appSubUrl} = window.config;
+
+export function initOrgTeamSearchRepoBox() {
+ const $searchRepoBox = $('#search-repo-box');
+ $searchRepoBox.search({
+ minCharacters: 2,
+ apiSettings: {
+ url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`,
+ onResponse(response) {
+ const items = [];
+ $.each(response.data, (_i, item) => {
+ items.push({
+ title: item.repository.full_name.split('/')[1],
+ description: item.repository.full_name,
+ });
+ });
+
+ return {results: items};
+ },
+ },
+ searchFields: ['full_name'],
+ showNoResults: false,
+ });
+}
diff --git a/web_src/js/features/pull-view-file.js b/web_src/js/features/pull-view-file.js
new file mode 100644
index 0000000..2472e5a
--- /dev/null
+++ b/web_src/js/features/pull-view-file.js
@@ -0,0 +1,96 @@
+import {diffTreeStore} from '../modules/stores.js';
+import {setFileFolding} from './file-fold.js';
+import {POST} from '../modules/fetch.js';
+
+const {pageData} = window.config;
+const prReview = pageData.prReview || {};
+const viewedStyleClass = 'viewed-file-checked-form';
+const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found
+const expandFilesBtnSelector = '#expand-files-btn';
+const collapseFilesBtnSelector = '#collapse-files-btn';
+
+// Refreshes the summary of viewed files if present
+// The data used will be window.config.pageData.prReview.numberOf{Viewed}Files
+function refreshViewedFilesSummary() {
+ const viewedFilesProgress = document.getElementById('viewed-files-summary');
+ viewedFilesProgress?.setAttribute('value', prReview.numberOfViewedFiles);
+ const summaryLabel = document.getElementById('viewed-files-summary-label');
+ if (summaryLabel) summaryLabel.innerHTML = summaryLabel.getAttribute('data-text-changed-template')
+ .replace('%[1]d', prReview.numberOfViewedFiles)
+ .replace('%[2]d', prReview.numberOfFiles);
+}
+
+// Explicitly recounts how many files the user has currently reviewed by counting the number of checked "viewed" checkboxes
+// Additionally, the viewed files summary will be updated if it exists
+export function countAndUpdateViewedFiles() {
+ // The number of files is constant, but the number of viewed files can change because files can be loaded dynamically
+ prReview.numberOfViewedFiles = document.querySelectorAll(`${viewedCheckboxSelector} > input[type=checkbox][checked]`).length;
+ refreshViewedFilesSummary();
+}
+
+// Initializes a listener for all children of the given html element
+// (for example 'document' in the most basic case)
+// to watch for changes of viewed-file checkboxes
+export function initViewedCheckboxListenerFor() {
+ for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) {
+ // To prevent double addition of listeners
+ form.setAttribute('data-has-viewed-checkbox-listener', true);
+
+ // The checkbox consists of a div containing the real checkbox with its label and the CSRF token,
+ // hence the actual checkbox first has to be found
+ const checkbox = form.querySelector('input[type=checkbox]');
+ checkbox.addEventListener('input', function() {
+ // Mark the file as viewed visually - will especially change the background
+ if (this.checked) {
+ form.classList.add(viewedStyleClass);
+ checkbox.setAttribute('checked', '');
+ prReview.numberOfViewedFiles++;
+ } else {
+ form.classList.remove(viewedStyleClass);
+ checkbox.removeAttribute('checked');
+ prReview.numberOfViewedFiles--;
+ }
+
+ // Update viewed-files summary and remove "has changed" label if present
+ refreshViewedFilesSummary();
+ const hasChangedLabel = form.parentNode.querySelector('.changed-since-last-review');
+ hasChangedLabel?.remove();
+
+ const fileName = checkbox.getAttribute('name');
+
+ // check if the file is in our difftreestore and if we find it -> change the IsViewed status
+ const fileInPageData = diffTreeStore().files.find((x) => x.Name === fileName);
+ if (fileInPageData) {
+ fileInPageData.IsViewed = this.checked;
+ }
+
+ // Unfortunately, actual forms cause too many problems, hence another approach is needed
+ const files = {};
+ files[fileName] = this.checked;
+ const data = {files};
+ const headCommitSHA = form.getAttribute('data-headcommit');
+ if (headCommitSHA) data.headCommitSHA = headCommitSHA;
+ POST(form.getAttribute('data-link'), {data});
+
+ // Fold the file accordingly
+ const parentBox = form.closest('.diff-file-header');
+ setFileFolding(parentBox.closest('.file-content'), parentBox.querySelector('.fold-file'), this.checked);
+ });
+ }
+}
+
+export function initExpandAndCollapseFilesButton() {
+ // expand btn
+ document.querySelector(expandFilesBtnSelector)?.addEventListener('click', () => {
+ for (const box of document.querySelectorAll('.file-content[data-folded="true"]')) {
+ setFileFolding(box, box.querySelector('.fold-file'), false);
+ }
+ });
+ // collapse btn, need to exclude the div of “show more”
+ document.querySelector(collapseFilesBtnSelector)?.addEventListener('click', () => {
+ for (const box of document.querySelectorAll('.file-content:not([data-folded="true"])')) {
+ if (box.getAttribute('id') === 'diff-incomplete') continue;
+ setFileFolding(box, box.querySelector('.fold-file'), true);
+ }
+ });
+}
diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js
new file mode 100644
index 0000000..030c251
--- /dev/null
+++ b/web_src/js/features/recent-commits.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoRecentCommits() {
+ const el = document.getElementById('repo-recent-commits-chart');
+ if (!el) return;
+
+ const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
+ try {
+ const View = createApp(RepoRecentCommits, {
+ locale: {
+ loadingTitle: el.getAttribute('data-locale-loading-title'),
+ loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+ loadingInfo: el.getAttribute('data-locale-loading-info'),
+ },
+ });
+ View.mount(el);
+ } catch (err) {
+ console.error('RepoRecentCommits failed to load', err);
+ el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+ }
+}
diff --git a/web_src/js/features/repo-branch.js b/web_src/js/features/repo-branch.js
new file mode 100644
index 0000000..b9ffc61
--- /dev/null
+++ b/web_src/js/features/repo-branch.js
@@ -0,0 +1,42 @@
+import $ from 'jquery';
+import {toggleElem} from '../utils/dom.js';
+
+export function initRepoBranchButton() {
+ initRepoCreateBranchButton();
+ initRepoRenameBranchButton();
+}
+
+function initRepoCreateBranchButton() {
+ // 2 pages share this code, one is the branch list page, the other is the commit view page: create branch/tag from current commit (dirty code)
+ for (const el of document.querySelectorAll('.show-create-branch-modal')) {
+ el.addEventListener('click', () => {
+ const modalFormName = el.getAttribute('data-modal-form') || '#create-branch-form';
+ const modalForm = document.querySelector(modalFormName);
+ if (!modalForm) return;
+ modalForm.action = `${modalForm.getAttribute('data-base-action')}${el.getAttribute('data-branch-from-urlcomponent')}`;
+
+ const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
+ document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from');
+
+ $(el.getAttribute('data-modal')).modal('show');
+ });
+ }
+}
+
+function initRepoRenameBranchButton() {
+ for (const el of document.querySelectorAll('.show-rename-branch-modal')) {
+ el.addEventListener('click', () => {
+ const target = el.getAttribute('data-modal');
+ const modal = document.querySelector(target);
+ const oldBranchName = el.getAttribute('data-old-branch-name');
+ modal.querySelector('input[name=from]').value = oldBranchName;
+
+ // display the warning that the branch which is chosen is the default branch
+ const warn = modal.querySelector('.default-branch-warning');
+ toggleElem(warn, el.getAttribute('data-is-default-branch') === 'true');
+
+ const text = modal.querySelector('[data-rename-branch-to]');
+ text.textContent = text.getAttribute('data-rename-branch-to').replace('%s', oldBranchName);
+ });
+ }
+}
diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js
new file mode 100644
index 0000000..794cc38
--- /dev/null
+++ b/web_src/js/features/repo-code.js
@@ -0,0 +1,195 @@
+import $ from 'jquery';
+import {svg} from '../svg.js';
+import {invertFileFolding} from './file-fold.js';
+import {createTippy} from '../modules/tippy.js';
+import {clippie} from 'clippie';
+import {toAbsoluteUrl} from '../utils.js';
+
+export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/;
+export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/;
+
+function changeHash(hash) {
+ if (window.history.pushState) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.location.hash = hash;
+ }
+}
+
+function isBlame() {
+ return Boolean(document.querySelector('div.blame'));
+}
+
+function getLineEls() {
+ return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`);
+}
+
+function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
+ for (const el of $linesEls) {
+ el.closest('tr').classList.remove('active');
+ }
+
+ // add hashchange to permalink
+ const refInNewIssue = document.querySelector('a.ref-in-new-issue');
+ const copyPermalink = document.querySelector('a.copy-line-permalink');
+ const viewGitBlame = document.querySelector('a.view_git_blame');
+
+ const updateIssueHref = function (anchor) {
+ if (!refInNewIssue) return;
+ const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new');
+ const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link');
+ const issueContent = `${toAbsoluteUrl(urlParamBodyLink)}#${anchor}`; // the default content for issue body
+ refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
+ };
+
+ const updateViewGitBlameFragment = function (anchor) {
+ if (!viewGitBlame) return;
+ let href = viewGitBlame.getAttribute('href');
+ href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
+ if (anchor.length !== 0) {
+ href = `${href}#${anchor}`;
+ }
+ viewGitBlame.setAttribute('href', href);
+ };
+
+ const updateCopyPermalinkUrl = function (anchor) {
+ if (!copyPermalink) return;
+ let link = copyPermalink.getAttribute('data-url');
+ link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
+ copyPermalink.setAttribute('data-url', link);
+ };
+
+ if ($selectionStartEls) {
+ let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1));
+ let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1));
+ let c;
+ if (a !== b) {
+ if (a > b) {
+ c = a;
+ a = b;
+ b = c;
+ }
+ const classes = [];
+ for (let i = a; i <= b; i++) {
+ classes.push(`[rel=L${i}]`);
+ }
+ $linesEls.filter(classes.join(',')).each(function () {
+ this.closest('tr').classList.add('active');
+ });
+ changeHash(`#L${a}-L${b}`);
+
+ updateIssueHref(`L${a}-L${b}`);
+ updateViewGitBlameFragment(`L${a}-L${b}`);
+ updateCopyPermalinkUrl(`L${a}-L${b}`);
+ return;
+ }
+ }
+ $selectionEndEl[0].closest('tr').classList.add('active');
+ changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`);
+
+ updateIssueHref($selectionEndEl[0].getAttribute('rel'));
+ updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel'));
+ updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel'));
+}
+
+function showLineButton() {
+ const menu = document.querySelector('.code-line-menu');
+ if (!menu) return;
+
+ // remove all other line buttons
+ for (const el of document.querySelectorAll('.code-line-button')) {
+ el.remove();
+ }
+
+ // find active row and add button
+ const tr = document.querySelector('.code-view tr.active');
+ const td = tr.querySelector('td.lines-num');
+ const btn = document.createElement('button');
+ btn.classList.add('code-line-button', 'ui', 'basic', 'button');
+ btn.innerHTML = svg('octicon-kebab-horizontal');
+ td.prepend(btn);
+
+ // put a copy of the menu back into DOM for the next click
+ btn.closest('.code-view').append(menu.cloneNode(true));
+
+ createTippy(btn, {
+ trigger: 'click',
+ hideOnClick: true,
+ content: menu,
+ placement: 'right-start',
+ interactive: true,
+ onShow: (tippy) => {
+ tippy.popper.addEventListener('click', () => {
+ tippy.hide();
+ }, {once: true});
+ },
+ });
+}
+
+export function initRepoCodeView() {
+ if ($('.code-view .lines-num').length > 0) {
+ $(document).on('click', '.lines-num span', function (e) {
+ const linesEls = getLineEls();
+ const selectedEls = Array.from(linesEls).filter((el) => {
+ return el.matches(`[rel=${this.getAttribute('id')}]`);
+ });
+
+ let from;
+ if (e.shiftKey) {
+ from = Array.from(linesEls).filter((el) => {
+ return el.closest('tr').classList.contains('active');
+ });
+ }
+ selectRange($(linesEls), $(selectedEls), from ? $(from) : null);
+
+ if (window.getSelection) {
+ window.getSelection().removeAllRanges();
+ } else {
+ document.selection.empty();
+ }
+
+ showLineButton();
+ });
+
+ $(window).on('hashchange', () => {
+ let m = window.location.hash.match(rangeAnchorRegex);
+ const $linesEls = $(getLineEls());
+ let $first;
+ if (m) {
+ $first = $linesEls.filter(`[rel=${m[1]}]`);
+ if ($first.length) {
+ const $last = $linesEls.filter(`[rel=${m[2]}]`);
+ selectRange($linesEls, $first, $last.length ? $last : $linesEls.last());
+
+ // show code view menu marker (don't show in blame page)
+ if (!isBlame()) {
+ showLineButton();
+ }
+
+ $('html, body').scrollTop($first.offset().top - 200);
+ return;
+ }
+ }
+ m = window.location.hash.match(singleAnchorRegex);
+ if (m) {
+ $first = $linesEls.filter(`[rel=L${m[2]}]`);
+ if ($first.length) {
+ selectRange($linesEls, $first);
+
+ // show code view menu marker (don't show in blame page)
+ if (!isBlame()) {
+ showLineButton();
+ }
+
+ $('html, body').scrollTop($first.offset().top - 200);
+ }
+ }
+ }).trigger('hashchange');
+ }
+ $(document).on('click', '.fold-file', ({currentTarget}) => {
+ invertFileFolding(currentTarget.closest('.file-content'), currentTarget);
+ });
+ $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => {
+ await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url')));
+ });
+}
diff --git a/web_src/js/features/repo-code.test.js b/web_src/js/features/repo-code.test.js
new file mode 100644
index 0000000..0e0062a
--- /dev/null
+++ b/web_src/js/features/repo-code.test.js
@@ -0,0 +1,17 @@
+import {singleAnchorRegex, rangeAnchorRegex} from './repo-code.js';
+
+test('singleAnchorRegex', () => {
+ expect(singleAnchorRegex.test('#L0')).toEqual(false);
+ expect(singleAnchorRegex.test('#L1')).toEqual(true);
+ expect(singleAnchorRegex.test('#L01')).toEqual(false);
+ expect(singleAnchorRegex.test('#n0')).toEqual(false);
+ expect(singleAnchorRegex.test('#n1')).toEqual(true);
+ expect(singleAnchorRegex.test('#n01')).toEqual(false);
+});
+
+test('rangeAnchorRegex', () => {
+ expect(rangeAnchorRegex.test('#L0-L10')).toEqual(false);
+ expect(rangeAnchorRegex.test('#L1-L10')).toEqual(true);
+ expect(rangeAnchorRegex.test('#L01-L10')).toEqual(false);
+ expect(rangeAnchorRegex.test('#L1-L01')).toEqual(false);
+});
diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js
new file mode 100644
index 0000000..f61ea08
--- /dev/null
+++ b/web_src/js/features/repo-commit.js
@@ -0,0 +1,27 @@
+import {createTippy} from '../modules/tippy.js';
+import {toggleElem} from '../utils/dom.js';
+
+export function initRepoEllipsisButton() {
+ for (const button of document.querySelectorAll('.js-toggle-commit-body')) {
+ button.addEventListener('click', function (e) {
+ e.preventDefault();
+ const expanded = this.getAttribute('aria-expanded') === 'true';
+ toggleElem(this.parentElement.querySelector('.commit-body'));
+ this.setAttribute('aria-expanded', String(!expanded));
+ });
+ }
+}
+
+export function initCommitStatuses() {
+ for (const element of document.querySelectorAll('[data-tippy="commit-statuses"]')) {
+ const top = document.querySelector('.repository.file.list') || document.querySelector('.repository.diff');
+
+ createTippy(element, {
+ content: element.nextElementSibling,
+ placement: top ? 'top-start' : 'bottom-start',
+ interactive: true,
+ role: 'dialog',
+ theme: 'box-with-header',
+ });
+ }
+}
diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js
new file mode 100644
index 0000000..88aa93d
--- /dev/null
+++ b/web_src/js/features/repo-common.js
@@ -0,0 +1,83 @@
+import $ from 'jquery';
+import {hideElem, queryElems, showElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
+import {sleep} from '../utils.js';
+
+async function onDownloadArchive(e) {
+ e.preventDefault();
+ // there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list
+ const el = e.target.closest('a.archive-link[href]');
+ const targetLoading = el.closest('.ui.dropdown') ?? el;
+ targetLoading.classList.add('is-loading', 'loading-icon-2px');
+ try {
+ for (let tryCount = 0; ;tryCount++) {
+ const response = await POST(el.href);
+ if (!response.ok) throw new Error(`Invalid server response: ${response.status}`);
+
+ const data = await response.json();
+ if (data.complete) break;
+ await sleep(Math.min((tryCount + 1) * 750, 2000));
+ }
+ window.location.href = el.href; // the archive is ready, start real downloading
+ } catch (e) {
+ console.error(e);
+ showErrorToast(`Failed to download the archive: ${e}`, {duration: 2500});
+ } finally {
+ targetLoading.classList.remove('is-loading', 'loading-icon-2px');
+ }
+}
+
+export function initRepoArchiveLinks() {
+ queryElems('a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive));
+}
+
+export function initRepoCloneLink() {
+ const $repoCloneSsh = $('#repo-clone-ssh');
+ const $repoCloneHttps = $('#repo-clone-https');
+ const $inputLink = $('#repo-clone-url');
+
+ if ((!$repoCloneSsh.length && !$repoCloneHttps.length) || !$inputLink.length) {
+ return;
+ }
+
+ $repoCloneSsh.on('click', () => {
+ localStorage.setItem('repo-clone-protocol', 'ssh');
+ window.updateCloneStates();
+ });
+ $repoCloneHttps.on('click', () => {
+ localStorage.setItem('repo-clone-protocol', 'https');
+ window.updateCloneStates();
+ });
+
+ $inputLink.on('focus', () => {
+ $inputLink.trigger('select');
+ });
+}
+
+export function initRepoCommonBranchOrTagDropdown(selector) {
+ $(selector).each(function () {
+ const $dropdown = $(this);
+ $dropdown.find('.reference.column').on('click', function () {
+ hideElem($dropdown.find('.scrolling.reference-list-menu'));
+ showElem($($(this).data('target')));
+ return false;
+ });
+ });
+}
+
+export function initRepoCommonFilterSearchDropdown(selector) {
+ const $dropdown = $(selector);
+ if (!$dropdown.length) return;
+
+ $dropdown.dropdown({
+ fullTextSearch: 'exact',
+ selectOnKeydown: false,
+ onChange(_text, _value, $choice) {
+ if ($choice[0].getAttribute('data-url')) {
+ window.location.href = $choice[0].getAttribute('data-url');
+ }
+ },
+ message: {noResults: $dropdown[0].getAttribute('data-no-results')},
+ });
+}
diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js
new file mode 100644
index 0000000..aa7fc38
--- /dev/null
+++ b/web_src/js/features/repo-diff-commit.js
@@ -0,0 +1,53 @@
+import {hideElem, showElem, toggleElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
+
+async function loadBranchesAndTags(area, loadingButton) {
+ loadingButton.classList.add('disabled');
+ try {
+ const res = await GET(loadingButton.getAttribute('data-fetch-url'));
+ const data = await res.json();
+ hideElem(loadingButton);
+ addTags(area, data.tags);
+ addBranches(area, data.branches, data.default_branch);
+ showElem(area.querySelectorAll('.branch-and-tag-detail'));
+ } finally {
+ loadingButton.classList.remove('disabled');
+ }
+}
+
+function addTags(area, tags) {
+ const tagArea = area.querySelector('.tag-area');
+ toggleElem(tagArea.parentElement, tags.length > 0);
+ for (const tag of tags) {
+ addLink(tagArea, tag.web_link, tag.name);
+ }
+}
+
+function addBranches(area, branches, defaultBranch) {
+ const defaultBranchTooltip = area.getAttribute('data-text-default-branch-tooltip');
+ const branchArea = area.querySelector('.branch-area');
+ toggleElem(branchArea.parentElement, branches.length > 0);
+ for (const branch of branches) {
+ const tooltip = defaultBranch === branch.name ? defaultBranchTooltip : null;
+ addLink(branchArea, branch.web_link, branch.name, tooltip);
+ }
+}
+
+function addLink(parent, href, text, tooltip) {
+ const link = document.createElement('a');
+ link.classList.add('muted', 'tw-px-1');
+ link.href = href;
+ link.textContent = text;
+ if (tooltip) {
+ link.classList.add('tw-border', 'tw-border-secondary', 'tw-rounded');
+ link.setAttribute('data-tooltip-content', tooltip);
+ }
+ parent.append(link);
+}
+
+export function initRepoDiffCommitBranchesAndTags() {
+ for (const area of document.querySelectorAll('.branch-and-tag-area')) {
+ const btn = area.querySelector('.load-branches-and-tags');
+ btn.addEventListener('click', () => loadBranchesAndTags(area, btn));
+ }
+}
diff --git a/web_src/js/features/repo-diff-commitselect.js b/web_src/js/features/repo-diff-commitselect.js
new file mode 100644
index 0000000..ebac64e
--- /dev/null
+++ b/web_src/js/features/repo-diff-commitselect.js
@@ -0,0 +1,10 @@
+import {createApp} from 'vue';
+import DiffCommitSelector from '../components/DiffCommitSelector.vue';
+
+export function initDiffCommitSelect() {
+ const el = document.getElementById('diff-commit-select');
+ if (!el) return;
+
+ const commitSelect = createApp(DiffCommitSelector);
+ commitSelect.mount(el);
+}
diff --git a/web_src/js/features/repo-diff-filetree.js b/web_src/js/features/repo-diff-filetree.js
new file mode 100644
index 0000000..5dd2c42
--- /dev/null
+++ b/web_src/js/features/repo-diff-filetree.js
@@ -0,0 +1,17 @@
+import {createApp} from 'vue';
+import DiffFileTree from '../components/DiffFileTree.vue';
+import DiffFileList from '../components/DiffFileList.vue';
+
+export function initDiffFileTree() {
+ const el = document.getElementById('diff-file-tree');
+ if (!el) return;
+
+ const fileTreeView = createApp(DiffFileTree);
+ fileTreeView.mount(el);
+
+ const fileListElement = document.getElementById('diff-file-list');
+ if (!fileListElement) return;
+
+ const fileListView = createApp(DiffFileList);
+ fileListView.mount(fileListElement);
+}
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
new file mode 100644
index 0000000..e5723af
--- /dev/null
+++ b/web_src/js/features/repo-diff.js
@@ -0,0 +1,232 @@
+import $ from 'jquery';
+import {initCompReactionSelector} from './comp/ReactionSelector.js';
+import {initRepoIssueContentHistory} from './repo-issue-content.js';
+import {initDiffFileTree} from './repo-diff-filetree.js';
+import {initDiffCommitSelect} from './repo-diff-commitselect.js';
+import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
+import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
+import {initImageDiff} from './imagediff.js';
+import {showErrorToast} from '../modules/toast.js';
+import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
+import {POST, GET} from '../modules/fetch.js';
+
+const {pageData, i18n} = window.config;
+
+function initRepoDiffReviewButton() {
+ const reviewBox = document.getElementById('review-box');
+ if (!reviewBox) return;
+
+ const counter = reviewBox.querySelector('.review-comments-counter');
+ if (!counter) return;
+
+ $(document).on('click', 'button[name="pending_review"]', (e) => {
+ const $form = $(e.target).closest('form');
+ // Watch for the form's submit event.
+ $form.on('submit', () => {
+ const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
+ counter.setAttribute('data-pending-comment-number', num);
+ counter.textContent = num;
+
+ reviewBox.classList.remove('pulse');
+ requestAnimationFrame(() => {
+ reviewBox.classList.add('pulse');
+ });
+ });
+ });
+}
+
+function initRepoDiffFileViewToggle() {
+ $('.file-view-toggle').on('click', function () {
+ for (const el of queryElemSiblings(this)) {
+ el.classList.remove('active');
+ }
+ this.classList.add('active');
+
+ const target = document.querySelector(this.getAttribute('data-toggle-selector'));
+ if (!target) return;
+
+ hideElem(queryElemSiblings(target));
+ showElem(target);
+ });
+}
+
+function initRepoDiffConversationForm() {
+ $(document).on('submit', '.conversation-holder form', async (e) => {
+ e.preventDefault();
+
+ const $form = $(e.target);
+ const textArea = e.target.querySelector('textarea');
+ if (!validateTextareaNonEmpty(textArea)) {
+ return;
+ }
+
+ if (e.target.classList.contains('is-loading')) return;
+ try {
+ e.target.classList.add('is-loading');
+ const formData = new FormData($form[0]);
+
+ // If the form is submitted by a button, append the button's name and value to the form data.
+ // originalEvent can be undefined, such as an event that's caused by Ctrl+Enter, in that case
+ // sent the event itself.
+ const submitter = submitEventSubmitter(e.originalEvent ?? e);
+ const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit');
+ if (isSubmittedByButton && submitter.name) {
+ formData.append(submitter.name, submitter.value);
+ }
+
+ const response = await POST(e.target.getAttribute('action'), {data: formData});
+ const $newConversationHolder = $(await response.text());
+ const {path, side, idx} = $newConversationHolder.data();
+
+ $form.closest('.conversation-holder').replaceWith($newConversationHolder);
+ let selector;
+ if ($form.closest('tr').data('line-type') === 'same') {
+ selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`;
+ } else {
+ selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`;
+ }
+ for (const el of document.querySelectorAll(selector)) {
+ el.classList.add('tw-invisible');
+ }
+ $newConversationHolder.find('.dropdown').dropdown();
+ initCompReactionSelector($newConversationHolder);
+ } catch { // here the caught error might be a jQuery AJAX error (thrown by await $.post), which is not good to use for error message handling
+ console.error('error when submitting conversation', e);
+ showErrorToast(i18n.network_error);
+ } finally {
+ e.target.classList.remove('is-loading');
+ }
+ });
+
+ $(document).on('click', '.resolve-conversation', async function (e) {
+ e.preventDefault();
+ const comment_id = $(this).data('comment-id');
+ const origin = $(this).data('origin');
+ const action = $(this).data('action');
+ const url = $(this).data('update-url');
+
+ try {
+ const response = await POST(url, {data: new URLSearchParams({origin, action, comment_id})});
+ const data = await response.text();
+
+ if ($(this).closest('.conversation-holder').length) {
+ const $conversation = $(data);
+ $(this).closest('.conversation-holder').replaceWith($conversation);
+ $conversation.find('.dropdown').dropdown();
+ initCompReactionSelector($conversation);
+ } else {
+ window.location.reload();
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ }
+ });
+}
+
+export function initRepoDiffConversationNav() {
+ // Previous/Next code review conversation
+ $(document).on('click', '.previous-conversation', (e) => {
+ const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
+ const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
+ const index = $conversations.index($conversation);
+ const previousIndex = index > 0 ? index - 1 : $conversations.length - 1;
+ const $previousConversation = $conversations.eq(previousIndex);
+ const anchor = $previousConversation.find('.comment').first()[0].getAttribute('id');
+ window.location.href = `#${anchor}`;
+ });
+ $(document).on('click', '.next-conversation', (e) => {
+ const $conversation = $(e.currentTarget).closest('.comment-code-cloud');
+ const $conversations = $('.comment-code-cloud:not(.tw-hidden)');
+ const index = $conversations.index($conversation);
+ const nextIndex = index < $conversations.length - 1 ? index + 1 : 0;
+ const $nextConversation = $conversations.eq(nextIndex);
+ const anchor = $nextConversation.find('.comment').first()[0].getAttribute('id');
+ window.location.href = `#${anchor}`;
+ });
+}
+
+// Will be called when the show more (files) button has been pressed
+function onShowMoreFiles() {
+ initRepoIssueContentHistory();
+ initViewedCheckboxListenerFor();
+ countAndUpdateViewedFiles();
+ initImageDiff();
+}
+
+export async function loadMoreFiles(url) {
+ const target = document.querySelector('a#diff-show-more-files');
+ if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
+ return;
+ }
+
+ pageData.diffFileInfo.isLoadingNewData = true;
+ target?.classList.add('disabled');
+
+ try {
+ const response = await GET(url);
+ const resp = await response.text();
+ const $resp = $(resp);
+ // the response is a full HTML page, we need to extract the relevant contents:
+ // 1. append the newly loaded file list items to the existing list
+ $('#diff-incomplete').replaceWith($resp.find('#diff-file-boxes').children());
+ // 2. re-execute the script to append the newly loaded items to the JS variables to refresh the DiffFileTree
+ $('body').append($resp.find('script#diff-data-script'));
+
+ onShowMoreFiles();
+ } catch (error) {
+ console.error('Error:', error);
+ showErrorToast('An error occurred while loading more files.');
+ } finally {
+ target?.classList.remove('disabled');
+ pageData.diffFileInfo.isLoadingNewData = false;
+ }
+}
+
+function initRepoDiffShowMore() {
+ $(document).on('click', 'a#diff-show-more-files', (e) => {
+ e.preventDefault();
+
+ const linkLoadMore = e.target.getAttribute('data-href');
+ loadMoreFiles(linkLoadMore);
+ });
+
+ $(document).on('click', 'a.diff-load-button', async (e) => {
+ e.preventDefault();
+ const $target = $(e.target);
+
+ if (e.target.classList.contains('disabled')) {
+ return;
+ }
+
+ e.target.classList.add('disabled');
+
+ const url = $target.data('href');
+
+ try {
+ const response = await GET(url);
+ const resp = await response.text();
+
+ if (!resp) {
+ return;
+ }
+ $target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children());
+ onShowMoreFiles();
+ } catch (error) {
+ console.error('Error:', error);
+ } finally {
+ e.target.classList.remove('disabled');
+ }
+ });
+}
+
+export function initRepoDiffView() {
+ initRepoDiffConversationForm();
+ if (!$('#diff-file-list').length) return;
+ initDiffFileTree();
+ initDiffCommitSelect();
+ initRepoDiffShowMore();
+ initRepoDiffReviewButton();
+ initRepoDiffFileViewToggle();
+ initViewedCheckboxListenerFor();
+ initExpandAndCollapseFilesButton();
+}
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
new file mode 100644
index 0000000..faf8ba1
--- /dev/null
+++ b/web_src/js/features/repo-editor.js
@@ -0,0 +1,203 @@
+import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
+import {createCodeEditor} from './codeeditor.js';
+import {hideElem, showElem, createElementFromHTML} from '../utils/dom.js';
+import {initMarkupContent} from '../markup/content.js';
+import {attachRefIssueContextPopup} from './contextpopup.js';
+import {POST} from '../modules/fetch.js';
+
+function initEditPreviewTab($form) {
+ const $tabMenu = $form.find('.tabular.menu');
+ $tabMenu.find('.item').tab();
+ const $previewTab = $tabMenu.find(
+ `.item[data-tab="${$tabMenu.data('preview')}"]`,
+ );
+ if ($previewTab.length) {
+ $previewTab.on('click', async function () {
+ const $this = $(this);
+ let context = `${$this.data('context')}/`;
+ const mode = $this.data('markup-mode') || 'comment';
+ const $treePathEl = $form.find('input#tree_path');
+ if ($treePathEl.length > 0) {
+ context += $treePathEl.val();
+ }
+ context = context.substring(0, context.lastIndexOf('/'));
+
+ const formData = new FormData();
+ formData.append('mode', mode);
+ formData.append('context', context);
+ formData.append(
+ 'text',
+ $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
+ );
+ formData.append('file_path', $treePathEl.val());
+ try {
+ const response = await POST($this.data('url'), {data: formData});
+ const data = await response.text();
+ const $previewPanel = $form.find(
+ `.tab[data-tab="${$tabMenu.data('preview')}"]`,
+ );
+ renderPreviewPanelContent($previewPanel, data);
+ } catch (error) {
+ console.error('Error:', error);
+ }
+ });
+ }
+}
+
+function initEditorForm() {
+ const $form = $('.repository .edit.form');
+ if (!$form) return;
+ initEditPreviewTab($form);
+}
+
+function getCursorPosition($e) {
+ const el = $e.get(0);
+ let pos = 0;
+ if ('selectionStart' in el) {
+ pos = el.selectionStart;
+ } else if ('selection' in document) {
+ el.focus();
+ const Sel = document.selection.createRange();
+ const SelLength = document.selection.createRange().text.length;
+ Sel.moveStart('character', -el.value.length);
+ pos = Sel.text.length - SelLength;
+ }
+ return pos;
+}
+
+export function initRepoEditor() {
+ initEditorForm();
+
+ $('.js-quick-pull-choice-option').on('change', function () {
+ if ($(this).val() === 'commit-to-new-branch') {
+ showElem('.quick-pull-branch-name');
+ document.querySelector('.quick-pull-branch-name input').required = true;
+ } else {
+ hideElem('.quick-pull-branch-name');
+ document.querySelector('.quick-pull-branch-name input').required = false;
+ }
+ $('#commit-button').text(this.getAttribute('button_text'));
+ });
+
+ const joinTreePath = ($fileNameEl) => {
+ const parts = [];
+ $('.breadcrumb span.section').each(function () {
+ const $element = $(this);
+ if ($element.find('a').length) {
+ parts.push($element.find('a').text());
+ } else {
+ parts.push($element.text());
+ }
+ });
+ if ($fileNameEl.val()) parts.push($fileNameEl.val());
+ $('#tree_path').val(parts.join('/'));
+ };
+
+ const $editFilename = $('#file-name');
+ $editFilename.on('input', function () {
+ const parts = $(this).val().split('/');
+
+ if (parts.length > 1) {
+ for (let i = 0; i < parts.length; ++i) {
+ const value = parts[i];
+ if (i < parts.length - 1) {
+ if (value.length) {
+ $editFilename[0].before(
+ createElementFromHTML(
+ `<span class="section"><a href="#">${htmlEscape(value)}</a></span>`,
+ ),
+ );
+ $editFilename[0].before(
+ createElementFromHTML(`<div class="breadcrumb-divider">/</div>`),
+ );
+ }
+ } else {
+ $(this).val(value);
+ }
+ this.setSelectionRange(0, 0);
+ }
+ }
+
+ joinTreePath($(this));
+ });
+
+ $editFilename.on('keydown', function (e) {
+ const $section = $('.breadcrumb span.section');
+
+ // Jump back to last directory once the filename is empty
+ if (
+ e.code === 'Backspace' &&
+ getCursorPosition($(this)) === 0 &&
+ $section.length > 0
+ ) {
+ e.preventDefault();
+ const $divider = $('.breadcrumb .breadcrumb-divider');
+ const value = $section.last().find('a').text();
+ $(this).val(value + $(this).val());
+ this.setSelectionRange(value.length, value.length);
+ $section.last().remove();
+ $divider.last().remove();
+ joinTreePath($(this));
+ }
+ });
+
+ const $editArea = $('.repository.editor textarea#edit_area');
+ if (!$editArea.length) return;
+
+ (async () => {
+ const editor = await createCodeEditor($editArea[0], $editFilename[0]);
+
+ // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
+ // to enable or disable the commit button
+ const commitButton = document.getElementById('commit-button');
+ const $editForm = $('.ui.edit.form');
+ const dirtyFileClass = 'dirty-file';
+
+ // Disabling the button at the start
+ if ($('input[name="page_has_posted"]').val() !== 'true') {
+ commitButton.disabled = true;
+ }
+
+ // Registering a custom listener for the file path and the file content
+ $editForm.areYouSure({
+ silent: true,
+ dirtyClass: dirtyFileClass,
+ fieldSelector: ':input:not(.commit-form-wrapper :input)',
+ change($form) {
+ const dirty = $form[0]?.classList.contains(dirtyFileClass);
+ commitButton.disabled = !dirty;
+ },
+ });
+
+ // Update the editor from query params, if available,
+ // only after the dirtyFileClass initialization
+ const params = new URLSearchParams(window.location.search);
+ const value = params.get('value');
+ if (value) {
+ editor.setValue(value);
+ }
+
+ commitButton?.addEventListener('click', (e) => {
+ // A modal which asks if an empty file should be committed
+ if (!$editArea.val()) {
+ $('#edit-empty-content-modal')
+ .modal({
+ onApprove() {
+ $('.edit.form').trigger('submit');
+ },
+ })
+ .modal('show');
+ e.preventDefault();
+ }
+ });
+ })();
+}
+
+export function renderPreviewPanelContent($panelPreviewer, data) {
+ $panelPreviewer.html(data);
+ initMarkupContent();
+
+ const $refIssues = $panelPreviewer.find('p .ref-issue');
+ attachRefIssueContextPopup($refIssues);
+}
diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
new file mode 100644
index 0000000..cff5068
--- /dev/null
+++ b/web_src/js/features/repo-findfile.js
@@ -0,0 +1,117 @@
+import {svg} from '../svg.js';
+import {toggleElem} from '../utils/dom.js';
+import {pathEscapeSegments} from '../utils/url.js';
+import {GET} from '../modules/fetch.js';
+
+const threshold = 50;
+let files = [];
+let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult;
+
+// return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...]
+// res[even] is unmatched, res[odd] is matched, see unit tests for examples
+// argument subLower must be a lower-cased string.
+export function strSubMatch(full, subLower) {
+ const res = [''];
+ let i = 0, j = 0;
+ const fullLower = full.toLowerCase();
+ while (i < subLower.length && j < fullLower.length) {
+ if (subLower[i] === fullLower[j]) {
+ if (res.length % 2 !== 0) res.push('');
+ res[res.length - 1] += full[j];
+ j++;
+ i++;
+ } else {
+ if (res.length % 2 === 0) res.push('');
+ res[res.length - 1] += full[j];
+ j++;
+ }
+ }
+ if (i !== subLower.length) {
+ // if the sub string doesn't match the full, only return the full as unmatched.
+ return [full];
+ }
+ if (j < full.length) {
+ // append remaining chars from full to result as unmatched
+ if (res.length % 2 === 0) res.push('');
+ res[res.length - 1] += full.substring(j);
+ }
+ return res;
+}
+
+export function calcMatchedWeight(matchResult) {
+ let weight = 0;
+ for (let i = 0; i < matchResult.length; i++) {
+ if (i % 2 === 1) { // matches are on odd indices, see strSubMatch
+ // use a function f(x+x) > f(x) + f(x) to make the longer matched string has higher weight.
+ weight += matchResult[i].length * matchResult[i].length;
+ }
+ }
+ return weight;
+}
+
+export function filterRepoFilesWeighted(files, filter) {
+ let filterResult = [];
+ if (filter) {
+ const filterLower = filter.toLowerCase();
+ // TODO: for large repo, this loop could be slow, maybe there could be one more limit:
+ // ... && filterResult.length < threshold * 20, wait for more feedbacks
+ for (let i = 0; i < files.length; i++) {
+ const res = strSubMatch(files[i], filterLower);
+ if (res.length > 1) { // length==1 means unmatched, >1 means having matched sub strings
+ filterResult.push({matchResult: res, matchWeight: calcMatchedWeight(res)});
+ }
+ }
+ filterResult.sort((a, b) => b.matchWeight - a.matchWeight);
+ filterResult = filterResult.slice(0, threshold);
+ } else {
+ for (let i = 0; i < files.length && i < threshold; i++) {
+ filterResult.push({matchResult: [files[i]], matchWeight: 0});
+ }
+ }
+ return filterResult;
+}
+
+function filterRepoFiles(filter) {
+ const treeLink = repoFindFileInput.getAttribute('data-url-tree-link');
+ repoFindFileTableBody.innerHTML = '';
+
+ const filterResult = filterRepoFilesWeighted(files, filter);
+
+ toggleElem(repoFindFileNoResult, !filterResult.length);
+ for (const r of filterResult) {
+ const row = document.createElement('tr');
+ const cell = document.createElement('td');
+ const a = document.createElement('a');
+ a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`);
+ a.innerHTML = svg('octicon-file', 16, 'tw-mr-2');
+ row.append(cell);
+ cell.append(a);
+ for (const [index, part] of r.matchResult.entries()) {
+ const span = document.createElement('span');
+ // safely escape by using textContent
+ span.textContent = part;
+ // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
+ // the matchResult[odd] is matched and highlighted to red.
+ if (index % 2 === 1) span.classList.add('ui', 'text', 'red');
+ a.append(span);
+ }
+ repoFindFileTableBody.append(row);
+ }
+}
+
+async function loadRepoFiles() {
+ const response = await GET(repoFindFileInput.getAttribute('data-url-data-link'));
+ files = await response.json();
+ filterRepoFiles(repoFindFileInput.value);
+}
+
+export function initFindFileInRepo() {
+ repoFindFileInput = document.getElementById('repo-file-find-input');
+ if (!repoFindFileInput) return;
+
+ repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody');
+ repoFindFileNoResult = document.getElementById('repo-find-file-no-result');
+ repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value));
+
+ loadRepoFiles();
+}
diff --git a/web_src/js/features/repo-findfile.test.js b/web_src/js/features/repo-findfile.test.js
new file mode 100644
index 0000000..2d96ed4
--- /dev/null
+++ b/web_src/js/features/repo-findfile.test.js
@@ -0,0 +1,34 @@
+import {strSubMatch, calcMatchedWeight, filterRepoFilesWeighted} from './repo-findfile.js';
+
+describe('Repo Find Files', () => {
+ test('strSubMatch', () => {
+ expect(strSubMatch('abc', '')).toEqual(['abc']);
+ expect(strSubMatch('abc', 'a')).toEqual(['', 'a', 'bc']);
+ expect(strSubMatch('abc', 'b')).toEqual(['a', 'b', 'c']);
+ expect(strSubMatch('abc', 'c')).toEqual(['ab', 'c']);
+ expect(strSubMatch('abc', 'ac')).toEqual(['', 'a', 'b', 'c']);
+ expect(strSubMatch('abc', 'z')).toEqual(['abc']);
+ expect(strSubMatch('abc', 'az')).toEqual(['abc']);
+
+ expect(strSubMatch('ABc', 'ac')).toEqual(['', 'A', 'B', 'c']);
+ expect(strSubMatch('abC', 'ac')).toEqual(['', 'a', 'b', 'C']);
+
+ expect(strSubMatch('aabbcc', 'abc')).toEqual(['', 'a', 'a', 'b', 'b', 'c', 'c']);
+ expect(strSubMatch('the/directory', 'hedir')).toEqual(['t', 'he', '/', 'dir', 'ectory']);
+ });
+
+ test('calcMatchedWeight', () => {
+ expect(calcMatchedWeight(['a', 'b', 'c', 'd']) < calcMatchedWeight(['a', 'bc', 'c'])).toBeTruthy();
+ });
+
+ test('filterRepoFilesWeighted', () => {
+ // the first matched result should always be the "word.txt"
+ let res = filterRepoFilesWeighted(['word.txt', 'we-got-result.dat'], 'word');
+ expect(res).toHaveLength(2);
+ expect(res[0].matchResult).toEqual(['', 'word', '.txt']);
+
+ res = filterRepoFilesWeighted(['we-got-result.dat', 'word.txt'], 'word');
+ expect(res).toHaveLength(2);
+ expect(res[0].matchResult).toEqual(['', 'word', '.txt']);
+ });
+});
diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
new file mode 100644
index 0000000..689b6f1
--- /dev/null
+++ b/web_src/js/features/repo-graph.js
@@ -0,0 +1,155 @@
+import $ from 'jquery';
+import {hideElem, showElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
+
+export function initRepoGraphGit() {
+ const graphContainer = document.getElementById('git-graph-container');
+ if (!graphContainer) return;
+
+ document.getElementById('flow-color-monochrome')?.addEventListener('click', () => {
+ document.getElementById('flow-color-monochrome').classList.add('active');
+ document.getElementById('flow-color-colored')?.classList.remove('active');
+ graphContainer.classList.remove('colored');
+ graphContainer.classList.add('monochrome');
+ const params = new URLSearchParams(window.location.search);
+ params.set('mode', 'monochrome');
+ const queryString = params.toString();
+ if (queryString) {
+ window.history.replaceState({}, '', `?${queryString}`);
+ } else {
+ window.history.replaceState({}, '', window.location.pathname);
+ }
+ for (const link of document.querySelectorAll('.pagination a')) {
+ const href = link.getAttribute('href');
+ if (!href) continue;
+ const url = new URL(href, window.location);
+ const params = url.searchParams;
+ params.set('mode', 'monochrome');
+ url.search = `?${params.toString()}`;
+ link.setAttribute('href', url.href);
+ }
+ });
+
+ document.getElementById('flow-color-colored')?.addEventListener('click', () => {
+ document.getElementById('flow-color-colored').classList.add('active');
+ document.getElementById('flow-color-monochrome')?.classList.remove('active');
+ graphContainer.classList.add('colored');
+ graphContainer.classList.remove('monochrome');
+ for (const link of document.querySelectorAll('.pagination a')) {
+ const href = link.getAttribute('href');
+ if (!href) continue;
+ const url = new URL(href, window.location);
+ const params = url.searchParams;
+ params.delete('mode');
+ url.search = `?${params.toString()}`;
+ link.setAttribute('href', url.href);
+ }
+ const params = new URLSearchParams(window.location.search);
+ params.delete('mode');
+ const queryString = params.toString();
+ if (queryString) {
+ window.history.replaceState({}, '', `?${queryString}`);
+ } else {
+ window.history.replaceState({}, '', window.location.pathname);
+ }
+ });
+ const url = new URL(window.location);
+ const params = url.searchParams;
+ const updateGraph = () => {
+ const queryString = params.toString();
+ const ajaxUrl = new URL(url);
+ ajaxUrl.searchParams.set('div-only', 'true');
+ window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname);
+ document.getElementById('pagination').innerHTML = '';
+ hideElem('#rel-container');
+ hideElem('#rev-container');
+ showElem('#loading-indicator');
+ (async () => {
+ const response = await GET(String(ajaxUrl));
+ const html = await response.text();
+ const div = document.createElement('div');
+ div.innerHTML = html;
+ document.getElementById('pagination').innerHTML = div.querySelector('#pagination').innerHTML;
+ document.getElementById('rel-container').innerHTML = div.querySelector('#rel-container').innerHTML;
+ document.getElementById('rev-container').innerHTML = div.querySelector('#rev-container').innerHTML;
+ hideElem('#loading-indicator');
+ showElem('#rel-container');
+ showElem('#rev-container');
+ })();
+ };
+ const dropdownSelected = params.getAll('branch');
+ if (params.has('hide-pr-refs') && params.get('hide-pr-refs') === 'true') {
+ dropdownSelected.splice(0, 0, '...flow-hide-pr-refs');
+ }
+
+ const flowSelectRefsDropdown = document.getElementById('flow-select-refs-dropdown');
+ $(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
+ $(flowSelectRefsDropdown).dropdown({
+ clearable: true,
+ fullTextSeach: 'exact',
+ onRemove(toRemove) {
+ if (toRemove === '...flow-hide-pr-refs') {
+ params.delete('hide-pr-refs');
+ } else {
+ const branches = params.getAll('branch');
+ params.delete('branch');
+ for (const branch of branches) {
+ if (branch !== toRemove) {
+ params.append('branch', branch);
+ }
+ }
+ }
+ updateGraph();
+ },
+ onAdd(toAdd) {
+ if (toAdd === '...flow-hide-pr-refs') {
+ params.set('hide-pr-refs', true);
+ } else {
+ params.append('branch', toAdd);
+ }
+ updateGraph();
+ },
+ });
+
+ graphContainer.addEventListener('mouseenter', (e) => {
+ if (e.target.matches('#rev-list li')) {
+ const flow = e.target.getAttribute('data-flow');
+ if (flow === '0') return;
+ document.getElementById(`flow-${flow}`)?.classList.add('highlight');
+ e.target.classList.add('hover');
+ for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+ item.classList.add('highlight');
+ }
+ } else if (e.target.matches('#rel-container .flow-group')) {
+ e.target.classList.add('highlight');
+ const flow = e.target.getAttribute('data-flow');
+ for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+ item.classList.add('highlight');
+ }
+ } else if (e.target.matches('#rel-container .flow-commit')) {
+ const rev = e.target.getAttribute('data-rev');
+ document.querySelector(`#rev-list li#commit-${rev}`)?.classList.add('hover');
+ }
+ });
+
+ graphContainer.addEventListener('mouseleave', (e) => {
+ if (e.target.matches('#rev-list li')) {
+ const flow = e.target.getAttribute('data-flow');
+ if (flow === '0') return;
+ document.getElementById(`flow-${flow}`)?.classList.remove('highlight');
+ e.target.classList.remove('hover');
+ for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+ item.classList.remove('highlight');
+ }
+ } else if (e.target.matches('#rel-container .flow-group')) {
+ e.target.classList.remove('highlight');
+ const flow = e.target.getAttribute('data-flow');
+ for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) {
+ item.classList.remove('highlight');
+ }
+ } else if (e.target.matches('#rel-container .flow-commit')) {
+ const rev = e.target.getAttribute('data-rev');
+ document.querySelector(`#rev-list li#commit-${rev}`)?.classList.remove('hover');
+ }
+ });
+}
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
new file mode 100644
index 0000000..6a5bce8
--- /dev/null
+++ b/web_src/js/features/repo-home.js
@@ -0,0 +1,147 @@
+import $ from 'jquery';
+import {stripTags} from '../utils.js';
+import {hideElem, queryElemChildren, showElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
+
+const {appSubUrl} = window.config;
+
+export function initRepoTopicBar() {
+ const mgrBtn = document.getElementById('manage_topic');
+ if (!mgrBtn) return;
+
+ const editDiv = document.getElementById('topic_edit');
+ const viewDiv = document.getElementById('repo-topics');
+ const topicDropdown = editDiv.querySelector('.ui.dropdown');
+ let lastErrorToast;
+
+ mgrBtn.addEventListener('click', () => {
+ hideElem(viewDiv);
+ showElem(editDiv);
+ topicDropdown.querySelector('input.search').focus();
+ });
+
+ document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
+ lastErrorToast?.hideToast();
+ hideElem(editDiv);
+ showElem(viewDiv);
+ mgrBtn.focus();
+ });
+
+ document.getElementById('save_topic').addEventListener('click', async (e) => {
+ lastErrorToast?.hideToast();
+ const topics = editDiv.querySelector('input[name=topics]').value;
+
+ const data = new FormData();
+ data.append('topics', topics);
+
+ const response = await POST(e.target.getAttribute('data-link'), {data});
+
+ if (response.ok) {
+ const responseData = await response.json();
+ if (responseData.status === 'ok') {
+ queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove());
+ if (topics.length) {
+ const topicArray = topics.split(',');
+ topicArray.sort();
+ for (const topic of topicArray) {
+ // it should match the code in repo/home.tmpl
+ const link = document.createElement('a');
+ link.classList.add('repo-topic', 'ui', 'large', 'label');
+ link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
+ link.textContent = topic;
+ mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
+ }
+ }
+ hideElem(editDiv);
+ showElem(viewDiv);
+ }
+ } else if (response.status === 422) {
+ // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
+ const responseData = await response.json();
+ lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
+ if (responseData.invalidTopics.length > 0) {
+ const {invalidTopics} = responseData;
+ const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label');
+ for (const [index, value] of topics.split(',').entries()) {
+ if (invalidTopics.includes(value)) {
+ topicLabels[index].classList.remove('green');
+ topicLabels[index].classList.add('red');
+ }
+ }
+ }
+ }
+ });
+
+ $(topicDropdown).dropdown({
+ allowAdditions: true,
+ forceSelection: false,
+ fullTextSearch: 'exact',
+ fields: {name: 'description', value: 'data-value'},
+ saveRemoteData: false,
+ label: {
+ transition: 'horizontal flip',
+ duration: 200,
+ variation: false,
+ },
+ apiSettings: {
+ url: `${appSubUrl}/explore/topics/search?q={query}`,
+ throttle: 500,
+ cache: false,
+ onResponse(res) {
+ const formattedResponse = {
+ success: false,
+ results: [],
+ };
+ const query = stripTags(this.urlData.query.trim());
+ let found_query = false;
+ const current_topics = [];
+ for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) {
+ current_topics.push(el.getAttribute('data-value'));
+ }
+
+ if (res.topics) {
+ let found = false;
+ for (let i = 0; i < res.topics.length; i++) {
+ // skip currently added tags
+ if (current_topics.includes(res.topics[i].topic_name)) {
+ continue;
+ }
+
+ if (res.topics[i].topic_name.toLowerCase() === query.toLowerCase()) {
+ found_query = true;
+ }
+ formattedResponse.results.push({description: res.topics[i].topic_name, 'data-value': res.topics[i].topic_name});
+ found = true;
+ }
+ formattedResponse.success = found;
+ }
+
+ if (query.length > 0 && !found_query) {
+ formattedResponse.success = true;
+ formattedResponse.results.unshift({description: query, 'data-value': query});
+ } else if (query.length > 0 && found_query) {
+ formattedResponse.results.sort((a, b) => {
+ if (a.description.toLowerCase() === query.toLowerCase()) return -1;
+ if (b.description.toLowerCase() === query.toLowerCase()) return 1;
+ if (a.description > b.description) return -1;
+ if (a.description < b.description) return 1;
+ return 0;
+ });
+ }
+
+ return formattedResponse;
+ },
+ },
+ onLabelCreate(value) {
+ value = value.toLowerCase().trim();
+ this.attr('data-value', value).contents().first().replaceWith(value);
+ return $(this);
+ },
+ onAdd(addedValue, _addedText, $addedChoice) {
+ addedValue = addedValue.toLowerCase().trim();
+ $addedChoice[0].setAttribute('data-value', addedValue);
+ $addedChoice[0].setAttribute('data-text', addedValue);
+ },
+ });
+}
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
new file mode 100644
index 0000000..cef2f49
--- /dev/null
+++ b/web_src/js/features/repo-issue-content.js
@@ -0,0 +1,154 @@
+import $ from 'jquery';
+import {svg} from '../svg.js';
+import {showErrorToast} from '../modules/toast.js';
+import {GET, POST} from '../modules/fetch.js';
+import {showElem} from '../utils/dom.js';
+
+const {appSubUrl} = window.config;
+let i18nTextEdited;
+let i18nTextOptions;
+let i18nTextDeleteFromHistory;
+let i18nTextDeleteFromHistoryConfirm;
+
+function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleHtml) {
+ let $dialog = $('.content-history-detail-dialog');
+ if ($dialog.length) return;
+
+ $dialog = $(`
+<div class="ui modal content-history-detail-dialog">
+ ${svg('octicon-x', 16, 'close icon inside')}
+ <div class="header tw-flex tw-items-center tw-justify-between">
+ <div>${itemTitleHtml}</div>
+ <div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden">
+ ${i18nTextOptions}
+ ${svg('octicon-triangle-down', 14, 'dropdown icon')}
+ <div class="menu">
+ <div class="item red text" data-option-item="delete">${i18nTextDeleteFromHistory}</div>
+ </div>
+ </div>
+ </div>
+ <div class="comment-diff-data is-loading"></div>
+</div>`);
+ $dialog.appendTo($('body'));
+ $dialog.find('.dialog-header-options').dropdown({
+ showOnFocus: false,
+ allowReselection: true,
+ async onChange(_value, _text, $item) {
+ const optionItem = $item.data('option-item');
+ if (optionItem === 'delete') {
+ if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
+ try {
+ const params = new URLSearchParams();
+ params.append('comment_id', commentId);
+ params.append('history_id', historyId);
+
+ const response = await POST(`${issueBaseUrl}/content-history/soft-delete?${params.toString()}`);
+ const resp = await response.json();
+
+ if (resp.ok) {
+ $dialog.modal('hide');
+ } else {
+ showErrorToast(resp.message);
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ showErrorToast('An error occurred while deleting the history.');
+ }
+ }
+ } else { // required by eslint
+ showErrorToast(`unknown option item: ${optionItem}`);
+ }
+ },
+ onHide() {
+ $(this).dropdown('clear', true);
+ },
+ });
+ $dialog.modal({
+ async onShow() {
+ try {
+ const params = new URLSearchParams();
+ params.append('comment_id', commentId);
+ params.append('history_id', historyId);
+
+ const url = `${issueBaseUrl}/content-history/detail?${params.toString()}`;
+ const response = await GET(url);
+ const resp = await response.json();
+
+ const commentDiffData = $dialog.find('.comment-diff-data')[0];
+ commentDiffData?.classList.remove('is-loading');
+ commentDiffData.innerHTML = resp.diffHtml;
+ // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden.
+ if (resp.canSoftDelete) {
+ showElem($dialog.find('.dialog-header-options'));
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ }
+ },
+ onHidden() {
+ $dialog.remove();
+ },
+ }).modal('show');
+}
+
+function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
+ const $headerLeft = $item.find('.comment-header-left');
+ const menuHtml = `
+ <div class="ui dropdown interact-fg content-history-menu" data-comment-id="${commentId}">
+ &bull; ${i18nTextEdited}${svg('octicon-triangle-down', 14, 'dropdown icon')}
+ <div class="menu">
+ </div>
+ </div>`;
+
+ $headerLeft.find(`.content-history-menu`).remove();
+ $headerLeft.append($(menuHtml));
+ $headerLeft.find('.dropdown').dropdown({
+ action: 'hide',
+ apiSettings: {
+ cache: false,
+ url: `${issueBaseUrl}/content-history/list?comment_id=${commentId}`,
+ },
+ saveRemoteData: false,
+ onHide() {
+ $(this).dropdown('change values', null);
+ },
+ onChange(value, itemHtml, $item) {
+ if (value && !$item.find('[data-history-is-deleted=1]').length) {
+ showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml);
+ }
+ },
+ });
+}
+
+export async function initRepoIssueContentHistory() {
+ const issueIndex = $('#issueIndex').val();
+ if (!issueIndex) return;
+
+ const $itemIssue = $('.repository.issue .timeline-item.comment.first'); // issue(PR) main content
+ const $comments = $('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments
+ if (!$itemIssue.length && !$comments.length) return;
+
+ const repoLink = $('#repolink').val();
+ const issueBaseUrl = `${appSubUrl}/${repoLink}/issues/${issueIndex}`;
+
+ try {
+ const response = await GET(`${issueBaseUrl}/content-history/overview`);
+ const resp = await response.json();
+
+ i18nTextEdited = resp.i18n.textEdited;
+ i18nTextDeleteFromHistory = resp.i18n.textDeleteFromHistory;
+ i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm;
+ i18nTextOptions = resp.i18n.textOptions;
+
+ if (resp.editedHistoryCountMap[0] && $itemIssue.length) {
+ showContentHistoryMenu(issueBaseUrl, $itemIssue, '0');
+ }
+ for (const [commentId, _editedCount] of Object.entries(resp.editedHistoryCountMap)) {
+ if (commentId === '0') continue;
+ const $itemComment = $(`#issuecomment-${commentId}`);
+ showContentHistoryMenu(issueBaseUrl, $itemComment, commentId);
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ }
+}
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
new file mode 100644
index 0000000..92f058c
--- /dev/null
+++ b/web_src/js/features/repo-issue-list.js
@@ -0,0 +1,245 @@
+import $ from 'jquery';
+import {updateIssuesMeta} from './repo-issue.js';
+import {toggleElem, hideElem, isElemHidden} from '../utils/dom.js';
+import {htmlEscape} from 'escape-goat';
+import {confirmModal} from './comp/ConfirmModal.js';
+import {showErrorToast} from '../modules/toast.js';
+import {createSortable} from '../modules/sortable.js';
+import {DELETE, POST} from '../modules/fetch.js';
+import {parseDom} from '../utils.js';
+
+function initRepoIssueListCheckboxes() {
+ const issueSelectAll = document.querySelector('.issue-checkbox-all');
+ if (!issueSelectAll) return; // logged out state
+ const issueCheckboxes = document.querySelectorAll('.issue-checkbox');
+
+ const syncIssueSelectionState = () => {
+ const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
+ const anyChecked = Boolean(checkedCheckboxes.length);
+ const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length;
+
+ if (allChecked) {
+ issueSelectAll.checked = true;
+ issueSelectAll.indeterminate = false;
+ } else if (anyChecked) {
+ issueSelectAll.checked = false;
+ issueSelectAll.indeterminate = true;
+ } else {
+ issueSelectAll.checked = false;
+ issueSelectAll.indeterminate = false;
+ }
+ // if any issue is selected, show the action panel, otherwise show the filter panel
+ toggleElem($('#issue-filters'), !anyChecked);
+ toggleElem($('#issue-actions'), anyChecked);
+ // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
+ const panels = document.querySelectorAll('#issue-filters, #issue-actions');
+ const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
+ const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left');
+ toolbarLeft.prepend(issueSelectAll);
+ };
+
+ for (const el of issueCheckboxes) {
+ el.addEventListener('change', syncIssueSelectionState);
+ }
+
+ issueSelectAll.addEventListener('change', () => {
+ for (const el of issueCheckboxes) {
+ el.checked = issueSelectAll.checked;
+ }
+ syncIssueSelectionState();
+ });
+
+ $('.issue-action').on('click', async function (e) {
+ e.preventDefault();
+
+ const url = this.getAttribute('data-url');
+ let action = this.getAttribute('data-action');
+ let elementId = this.getAttribute('data-element-id');
+ let issueIDs = [];
+ for (const el of document.querySelectorAll('.issue-checkbox:checked')) {
+ issueIDs.push(el.getAttribute('data-issue-id'));
+ }
+ issueIDs = issueIDs.join(',');
+ if (!issueIDs) return;
+
+ // for assignee
+ if (elementId === '0' && url.endsWith('/assignee')) {
+ elementId = '';
+ action = 'clear';
+ }
+
+ // for toggle
+ if (action === 'toggle' && e.altKey) {
+ action = 'toggle-alt';
+ }
+
+ // for delete
+ if (action === 'delete') {
+ const confirmText = e.target.getAttribute('data-action-delete-confirm');
+ if (!await confirmModal({content: confirmText, buttonColor: 'orange'})) {
+ return;
+ }
+ }
+
+ try {
+ await updateIssuesMeta(url, action, issueIDs, elementId);
+ window.location.reload();
+ } catch (err) {
+ showErrorToast(err.responseJSON?.error ?? err.message);
+ }
+ });
+}
+
+function initRepoIssueListAuthorDropdown() {
+ const $searchDropdown = $('.user-remote-search');
+ if (!$searchDropdown.length) return;
+
+ let searchUrl = $searchDropdown[0].getAttribute('data-search-url');
+ const actionJumpUrl = $searchDropdown[0].getAttribute('data-action-jump-url');
+ const selectedUserId = $searchDropdown[0].getAttribute('data-selected-user-id');
+ if (!searchUrl.includes('?')) searchUrl += '?';
+
+ $searchDropdown.dropdown('setting', {
+ fullTextSearch: true,
+ selectOnKeydown: false,
+ apiSettings: {
+ cache: false,
+ url: `${searchUrl}&q={query}`,
+ onResponse(resp) {
+ // the content is provided by backend IssuePosters handler
+ const processedResults = []; // to be used by dropdown to generate menu items
+ for (const item of resp.results) {
+ let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
+ if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`;
+ processedResults.push({value: item.user_id, name: html});
+ }
+ resp.results = processedResults;
+ return resp;
+ },
+ },
+ action: (_text, value) => {
+ window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value));
+ },
+ onShow: () => {
+ $searchDropdown.dropdown('filter', ' '); // trigger a search on first show
+ },
+ });
+
+ // we want to generate the dropdown menu items by ourselves, replace its internal setup functions
+ const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')};
+ const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
+ $searchDropdown.dropdown('internal', 'setup', dropdownSetup);
+ dropdownSetup.menu = function (values) {
+ const menu = $searchDropdown.find('> .menu')[0];
+ // remove old dynamic items
+ for (const el of menu.querySelectorAll(':scope > .dynamic-item')) {
+ el.remove();
+ }
+
+ const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className'));
+ if (newMenuHtml) {
+ const newMenuItems = parseDom(newMenuHtml, 'text/html').querySelectorAll('body > div');
+ for (const newMenuItem of newMenuItems) {
+ newMenuItem.classList.add('dynamic-item');
+ }
+ const div = document.createElement('div');
+ div.classList.add('divider', 'dynamic-item');
+ menu.append(div, ...newMenuItems);
+ }
+ $searchDropdown.dropdown('refresh');
+ // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
+ setTimeout(() => {
+ for (const el of menu.querySelectorAll('.item.active, .item.selected')) {
+ el.classList.remove('active', 'selected');
+ }
+ menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected');
+ }, 0);
+ };
+}
+
+function initPinRemoveButton() {
+ for (const button of document.getElementsByClassName('issue-card-unpin')) {
+ button.addEventListener('click', async (event) => {
+ const el = event.currentTarget;
+ const id = Number(el.getAttribute('data-issue-id'));
+
+ // Send the unpin request
+ const response = await DELETE(el.getAttribute('data-unpin-url'));
+ if (response.ok) {
+ // Delete the tooltip
+ el._tippy.destroy();
+ // Remove the Card
+ el.closest(`div.issue-card[data-issue-id="${id}"]`).remove();
+ }
+ });
+ }
+}
+
+async function pinMoveEnd(e) {
+ const url = e.item.getAttribute('data-move-url');
+ const id = Number(e.item.getAttribute('data-issue-id'));
+ await POST(url, {data: {id, position: e.newIndex + 1}});
+}
+
+async function initIssuePinSort() {
+ const pinDiv = document.getElementById('issue-pins');
+
+ if (pinDiv === null) return;
+
+ // If the User is not a Repo Admin, we don't need to proceed
+ if (!pinDiv.hasAttribute('data-is-repo-admin')) return;
+
+ initPinRemoveButton();
+
+ // If only one issue pinned, we don't need to make this Sortable
+ if (pinDiv.children.length < 2) return;
+
+ createSortable(pinDiv, {
+ group: 'shared',
+ onEnd: pinMoveEnd,
+ });
+}
+
+function initArchivedLabelFilter() {
+ const archivedLabelEl = document.querySelector('#archived-filter-checkbox');
+ if (!archivedLabelEl) {
+ return;
+ }
+
+ const url = new URL(window.location.href);
+ const archivedLabels = document.querySelectorAll('[data-is-archived]');
+
+ if (!archivedLabels.length) {
+ hideElem('.archived-label-filter');
+ return;
+ }
+ const selectedLabels = (url.searchParams.get('labels') || '')
+ .split(',')
+ .map((id) => id < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve
+
+ const archivedElToggle = () => {
+ for (const label of archivedLabels) {
+ const id = label.getAttribute('data-label-id');
+ toggleElem(label, archivedLabelEl.checked || selectedLabels.includes(id));
+ }
+ };
+
+ archivedElToggle();
+ archivedLabelEl.addEventListener('change', () => {
+ archivedElToggle();
+ if (archivedLabelEl.checked) {
+ url.searchParams.set('archived', 'true');
+ } else {
+ url.searchParams.delete('archived');
+ }
+ window.location.href = url.href;
+ });
+}
+
+export function initRepoIssueList() {
+ if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list').length) return;
+ initRepoIssueListCheckboxes();
+ initRepoIssueListAuthorDropdown();
+ initIssuePinSort();
+ initArchivedLabelFilter();
+}
diff --git a/web_src/js/features/repo-issue-pr-form.js b/web_src/js/features/repo-issue-pr-form.js
new file mode 100644
index 0000000..7b26e64
--- /dev/null
+++ b/web_src/js/features/repo-issue-pr-form.js
@@ -0,0 +1,10 @@
+import {createApp} from 'vue';
+import PullRequestMergeForm from '../components/PullRequestMergeForm.vue';
+
+export function initRepoPullRequestMergeForm() {
+ const el = document.getElementById('pull-request-merge-form');
+ if (!el) return;
+
+ const view = createApp(PullRequestMergeForm);
+ view.mount(el);
+}
diff --git a/web_src/js/features/repo-issue-pr-status.js b/web_src/js/features/repo-issue-pr-status.js
new file mode 100644
index 0000000..7890b9c
--- /dev/null
+++ b/web_src/js/features/repo-issue-pr-status.js
@@ -0,0 +1,10 @@
+export function initRepoPullRequestCommitStatus() {
+ for (const btn of document.querySelectorAll('.commit-status-hide-checks')) {
+ const panel = btn.closest('.commit-status-panel');
+ const list = panel.querySelector('.commit-status-list');
+ btn.addEventListener('click', () => {
+ list.style.maxHeight = list.style.maxHeight ? '' : '0px'; // toggle
+ btn.textContent = btn.getAttribute(list.style.maxHeight ? 'data-show-all' : 'data-hide-all');
+ });
+ }
+}
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
new file mode 100644
index 0000000..9938d53
--- /dev/null
+++ b/web_src/js/features/repo-issue.js
@@ -0,0 +1,797 @@
+import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
+import {showTemporaryTooltip, createTippy} from '../modules/tippy.js';
+import {hideElem, showElem, toggleElem} from '../utils/dom.js';
+import {setFileFolding} from './file-fold.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+import {toAbsoluteUrl} from '../utils.js';
+import {initDropzone} from './common-global.js';
+import {POST, GET} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
+import {emojiHTML} from './emoji.js';
+
+const {appSubUrl} = window.config;
+
+// if there are draft comments, confirm before reloading, to avoid losing comments
+export function reloadConfirmDraftComment() {
+ const commentTextareas = [
+ document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'),
+ document.querySelector('#comment-form textarea'),
+ ];
+ for (const textarea of commentTextareas) {
+ // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
+ // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
+ if (textarea && textarea.value.trim().length > 10) {
+ textarea.parentElement.scrollIntoView();
+ if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
+ return;
+ }
+ break;
+ }
+ }
+ window.location.reload();
+}
+
+export function initRepoIssueTimeTracking() {
+ $(document).on('click', '.issue-add-time', () => {
+ $('.issue-start-time-modal').modal({
+ duration: 200,
+ onApprove() {
+ $('#add_time_manual_form').trigger('submit');
+ },
+ }).modal('show');
+ $('.issue-start-time-modal input').on('keydown', (e) => {
+ if ((e.keyCode || e.key) === 13) {
+ $('#add_time_manual_form').trigger('submit');
+ }
+ });
+ });
+ $(document).on('click', '.issue-start-time, .issue-stop-time', () => {
+ $('#toggle_stopwatch_form').trigger('submit');
+ });
+ $(document).on('click', '.issue-cancel-time', () => {
+ $('#cancel_stopwatch_form').trigger('submit');
+ });
+ $(document).on('click', 'button.issue-delete-time', function () {
+ const sel = `.issue-delete-time-modal[data-id="${$(this).data('id')}"]`;
+ $(sel).modal({
+ duration: 200,
+ onApprove() {
+ $(`${sel} form`).trigger('submit');
+ },
+ }).modal('show');
+ });
+}
+
+async function updateDeadline(deadlineString) {
+ hideElem('#deadline-err-invalid-date');
+ document.getElementById('deadline-loader')?.classList.add('is-loading');
+
+ let realDeadline = null;
+ if (deadlineString !== '') {
+ const newDate = Date.parse(deadlineString);
+
+ if (Number.isNaN(newDate)) {
+ document.getElementById('deadline-loader')?.classList.remove('is-loading');
+ showElem('#deadline-err-invalid-date');
+ return false;
+ }
+ realDeadline = new Date(newDate);
+ }
+
+ try {
+ const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), {
+ data: {due_date: realDeadline},
+ });
+
+ if (response.ok) {
+ window.location.reload();
+ } else {
+ throw new Error('Invalid response');
+ }
+ } catch (error) {
+ console.error(error);
+ document.getElementById('deadline-loader').classList.remove('is-loading');
+ showElem('#deadline-err-invalid-date');
+ }
+}
+
+export function initRepoIssueDue() {
+ $(document).on('click', '.issue-due-edit', () => {
+ toggleElem('#deadlineForm');
+ });
+ $(document).on('click', '.issue-due-remove', () => {
+ updateDeadline('');
+ });
+ $(document).on('submit', '.issue-due-form', () => {
+ updateDeadline($('#deadlineDate').val());
+ return false;
+ });
+}
+
+/**
+ * @param {HTMLElement} item
+ */
+function excludeLabel(item) {
+ const href = item.getAttribute('href');
+ const id = item.getAttribute('data-label-id');
+
+ const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
+ const newStr = 'labels=$1-$2$3&';
+
+ window.location = href.replace(new RegExp(regStr), newStr);
+}
+
+export function initRepoIssueSidebarList() {
+ const repolink = $('#repolink').val();
+ const repoId = $('#repoId').val();
+ const crossRepoSearch = $('#crossRepoSearch').val();
+ const tp = $('#type').val();
+ let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`;
+ if (crossRepoSearch === 'true') {
+ issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
+ }
+ $('#new-dependency-drop-list')
+ .dropdown({
+ apiSettings: {
+ url: issueSearchUrl,
+ onResponse(response) {
+ const filteredResponse = {success: true, results: []};
+ const currIssueId = $('#new-dependency-drop-list').data('issue-id');
+ // Parse the response from the api to work with our dropdown
+ $.each(response, (_i, issue) => {
+ // Don't list current issue in the dependency list.
+ if (issue.id === currIssueId) {
+ return;
+ }
+ filteredResponse.results.push({
+ name: `#${issue.number} ${issueTitleHTML(htmlEscape(issue.title))
+ }<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
+ value: issue.id,
+ });
+ });
+ return filteredResponse;
+ },
+ cache: false,
+ },
+
+ fullTextSearch: true,
+ });
+
+ $('.menu a.label-filter-item').each(function () {
+ $(this).on('click', function (e) {
+ if (e.altKey) {
+ e.preventDefault();
+ excludeLabel(this);
+ }
+ });
+ });
+
+ $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
+ if (e.altKey && e.keyCode === 13) {
+ const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
+ if (selectedItem) {
+ excludeLabel(selectedItem);
+ }
+ }
+ });
+ $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
+}
+
+export function initRepoIssueCommentDelete() {
+ // Delete comment
+ document.addEventListener('click', async (e) => {
+ if (!e.target.matches('.delete-comment')) return;
+ e.preventDefault();
+
+ const deleteButton = e.target;
+ if (window.confirm(deleteButton.getAttribute('data-locale'))) {
+ try {
+ const response = await POST(deleteButton.getAttribute('data-url'));
+ if (!response.ok) throw new Error('Failed to delete comment');
+
+ const conversationHolder = deleteButton.closest('.conversation-holder');
+ const parentTimelineItem = deleteButton.closest('.timeline-item');
+ const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
+
+ // Check if this was a pending comment.
+ if (conversationHolder?.querySelector('.pending-label')) {
+ const counter = document.querySelector('#review-box .review-comments-counter');
+ let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
+ num = Math.max(num, 0);
+ counter.setAttribute('data-pending-comment-number', num);
+ counter.textContent = String(num);
+ }
+
+ document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove();
+
+ if (conversationHolder && !conversationHolder.querySelector('.comment')) {
+ const path = conversationHolder.getAttribute('data-path');
+ const side = conversationHolder.getAttribute('data-side');
+ const idx = conversationHolder.getAttribute('data-idx');
+ const lineType = conversationHolder.closest('tr').getAttribute('data-line-type');
+
+ if (lineType === 'same') {
+ document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible');
+ } else {
+ document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible');
+ }
+
+ conversationHolder.remove();
+ }
+
+ // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
+ if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
+ const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
+ timelineAvatar?.classList.remove('timeline-avatar-offset');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ });
+}
+
+export function initRepoIssueDependencyDelete() {
+ // Delete Issue dependency
+ $(document).on('click', '.delete-dependency-button', (e) => {
+ const id = e.currentTarget.getAttribute('data-id');
+ const type = e.currentTarget.getAttribute('data-type');
+
+ $('.remove-dependency').modal({
+ closable: false,
+ duration: 200,
+ onApprove: () => {
+ $('#removeDependencyID').val(id);
+ $('#dependencyType').val(type);
+ $('#removeDependencyForm').trigger('submit');
+ },
+ }).modal('show');
+ });
+}
+
+export function initRepoIssueCodeCommentCancel() {
+ // Cancel inline code comment
+ document.addEventListener('click', (e) => {
+ if (!e.target.matches('.cancel-code-comment')) return;
+
+ const form = e.target.closest('form');
+ if (form?.classList.contains('comment-form')) {
+ hideElem(form);
+ showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply'));
+ } else {
+ form.closest('.comment-code-cloud')?.remove();
+ }
+ });
+}
+
+export function initRepoPullRequestUpdate() {
+ // Pull Request update button
+ const pullUpdateButton = document.querySelector('.update-button > button');
+ if (!pullUpdateButton) return;
+
+ pullUpdateButton.addEventListener('click', async function (e) {
+ e.preventDefault();
+ const redirect = this.getAttribute('data-redirect');
+ this.classList.add('is-loading');
+ let response;
+ try {
+ response = await POST(this.getAttribute('data-do'));
+ } catch (error) {
+ console.error(error);
+ } finally {
+ this.classList.remove('is-loading');
+ }
+ let data;
+ try {
+ data = await response?.json(); // the response is probably not a JSON
+ } catch (error) {
+ console.error(error);
+ }
+ if (data?.redirect) {
+ window.location.href = data.redirect;
+ } else if (redirect) {
+ window.location.href = redirect;
+ } else {
+ window.location.reload();
+ }
+ });
+
+ $('.update-button > .dropdown').dropdown({
+ onChange(_text, _value, $choice) {
+ const url = $choice[0].getAttribute('data-do');
+ if (url) {
+ const buttonText = pullUpdateButton.querySelector('.button-text');
+ if (buttonText) {
+ buttonText.textContent = $choice.text();
+ }
+ pullUpdateButton.setAttribute('data-do', url);
+ }
+ },
+ });
+}
+
+export function initRepoPullRequestAllowMaintainerEdit() {
+ const wrapper = document.getElementById('allow-edits-from-maintainers');
+ if (!wrapper) return;
+ const checkbox = wrapper.querySelector('input[type="checkbox"]');
+ checkbox.addEventListener('input', async () => {
+ const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
+ wrapper.classList.add('is-loading');
+ try {
+ const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})});
+ if (!resp.ok) {
+ throw new Error('Failed to update maintainer edit permission');
+ }
+ const data = await resp.json();
+ checkbox.checked = data.allow_maintainer_edit;
+ } catch (error) {
+ checkbox.checked = !checkbox.checked;
+ console.error(error);
+ showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
+ } finally {
+ wrapper.classList.remove('is-loading');
+ }
+ });
+}
+
+export function initRepoIssueReferenceRepositorySearch() {
+ $('.issue_reference_repository_search')
+ .dropdown({
+ apiSettings: {
+ url: `${appSubUrl}/repo/search?q={query}&limit=20`,
+ onResponse(response) {
+ const filteredResponse = {success: true, results: []};
+ $.each(response.data, (_r, repo) => {
+ filteredResponse.results.push({
+ name: htmlEscape(repo.repository.full_name),
+ value: repo.repository.full_name,
+ });
+ });
+ return filteredResponse;
+ },
+ cache: false,
+ },
+ onChange(_value, _text, $choice) {
+ const $form = $choice.closest('form');
+ if (!$form.length) return;
+
+ $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
+ },
+ fullTextSearch: true,
+ });
+}
+
+export function initRepoIssueWipTitle() {
+ $('.title_wip_desc > a').on('click', (e) => {
+ e.preventDefault();
+
+ const $issueTitle = $('#issue_title');
+ $issueTitle.trigger('focus');
+ const value = $issueTitle.val().trim().toUpperCase();
+
+ const wipPrefixes = $('.title_wip_desc').data('wip-prefixes');
+ for (const prefix of wipPrefixes) {
+ if (value.startsWith(prefix.toUpperCase())) {
+ return;
+ }
+ }
+
+ $issueTitle.val(`${wipPrefixes[0]} ${$issueTitle.val()}`);
+ });
+}
+
+export async function updateIssuesMeta(url, action, issue_ids, id) {
+ try {
+ const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
+ if (!response.ok) {
+ throw new Error('Failed to update issues meta');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+export function initRepoIssueComments() {
+ if (!$('.repository.view.issue .timeline').length) return;
+
+ $('.re-request-review').on('click', async function (e) {
+ e.preventDefault();
+ const url = this.getAttribute('data-update-url');
+ const issueId = this.getAttribute('data-issue-id');
+ const id = this.getAttribute('data-id');
+ const isChecked = this.classList.contains('checked');
+
+ await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
+ window.location.reload();
+ });
+
+ document.addEventListener('click', (e) => {
+ const urlTarget = document.querySelector(':target');
+ if (!urlTarget) return;
+
+ const urlTargetId = urlTarget.id;
+ if (!urlTargetId) return;
+
+ if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
+
+ if (!e.target.closest(`#${urlTargetId}`)) {
+ const scrollPosition = $(window).scrollTop();
+ window.location.hash = '';
+ $(window).scrollTop(scrollPosition);
+ window.history.pushState(null, null, ' ');
+ }
+ });
+}
+
+export async function handleReply($el) {
+ hideElem($el);
+ const $form = $el.closest('.comment-code-cloud').find('.comment-form');
+ showElem($form);
+
+ const $textarea = $form.find('textarea');
+ let editor = getComboMarkdownEditor($textarea);
+ if (!editor) {
+ // FIXME: the initialization of the dropzone is not consistent.
+ // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
+ // When the form is submitted and partially reload, none of them is initialized.
+ const dropzone = $form.find('.dropzone')[0];
+ if (!dropzone.dropzone) initDropzone(dropzone);
+ editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
+ }
+ editor.focus();
+ return editor;
+}
+
+export function initRepoPullRequestReview() {
+ if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) {
+ // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing
+ if (window.history.scrollRestoration !== 'manual') {
+ window.history.scrollRestoration = 'manual';
+ }
+ const commentDiv = document.querySelector(window.location.hash);
+ if (commentDiv) {
+ // get the name of the parent id
+ const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
+ if (groupID && groupID.startsWith('code-comments-')) {
+ const id = groupID.slice(14);
+ const ancestorDiffBox = commentDiv.closest('.diff-file-box');
+ // on pages like conversation, there is no diff header
+ const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
+
+ // offset is for scrolling
+ let offset = 30;
+ if (diffHeader) {
+ offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
+ }
+
+ hideElem(`#show-outdated-${id}`);
+ showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
+ // if the comment box is folded, expand it
+ if (ancestorDiffBox?.getAttribute('data-folded') === 'true') {
+ setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
+ }
+
+ window.scrollTo({
+ top: $(commentDiv).offset().top - offset,
+ behavior: 'instant',
+ });
+ }
+ }
+ } else if (window.history.scrollRestoration === 'manual') {
+ // reset scrollRestoration to 'auto' if there is no hash in url and we set it to 'manual' before
+ window.history.scrollRestoration = 'auto';
+ }
+
+ $(document).on('click', '.show-outdated', function (e) {
+ e.preventDefault();
+ const id = this.getAttribute('data-comment');
+ hideElem(this);
+ showElem(`#code-comments-${id}`);
+ showElem(`#code-preview-${id}`);
+ showElem(`#hide-outdated-${id}`);
+ });
+
+ $(document).on('click', '.hide-outdated', function (e) {
+ e.preventDefault();
+ const id = this.getAttribute('data-comment');
+ hideElem(this);
+ hideElem(`#code-comments-${id}`);
+ hideElem(`#code-preview-${id}`);
+ showElem(`#show-outdated-${id}`);
+ });
+
+ $(document).on('click', 'button.comment-form-reply', async function (e) {
+ e.preventDefault();
+ await handleReply($(this));
+ });
+
+ const $reviewBox = $('.review-box-panel');
+ if ($reviewBox.length === 1) {
+ const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor'));
+ }
+
+ // The following part is only for diff views
+ if (!$('.repository.pull.diff').length) return;
+
+ const $reviewBtn = $('.js-btn-review');
+ const $panel = $reviewBtn.parent().find('.review-box-panel');
+ const $closeBtn = $panel.find('.close');
+
+ if ($reviewBtn.length && $panel.length) {
+ const tippy = createTippy($reviewBtn[0], {
+ content: $panel[0],
+ placement: 'bottom',
+ trigger: 'click',
+ maxWidth: 'none',
+ interactive: true,
+ hideOnClick: true,
+ });
+
+ $closeBtn.on('click', (e) => {
+ e.preventDefault();
+ tippy.hide();
+ });
+ }
+
+ $(document).on('click', '.add-code-comment', async function (e) {
+ if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
+ e.preventDefault();
+
+ const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
+ const side = this.getAttribute('data-side');
+ const idx = this.getAttribute('data-idx');
+ const path = this.closest('[data-path]')?.getAttribute('data-path');
+ const tr = this.closest('tr');
+ const lineType = tr.getAttribute('data-line-type');
+
+ const ntr = tr.nextElementSibling;
+ let $ntr = $(ntr);
+ if (!ntr?.classList.contains('add-comment')) {
+ $ntr = $(`
+ <tr class="add-comment" data-line-type="${lineType}">
+ ${isSplit ? `
+ <td class="add-comment-left" colspan="4"></td>
+ <td class="add-comment-right" colspan="4"></td>
+ ` : `
+ <td class="add-comment-left add-comment-right" colspan="5"></td>
+ `}
+ </tr>`);
+ $(tr).after($ntr);
+ }
+
+ const $td = $ntr.find(`.add-comment-${side}`);
+ const $commentCloud = $td.find('.comment-code-cloud');
+ if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
+ try {
+ const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
+ const html = await response.text();
+ $td.html(html);
+ $td.find("input[name='line']").val(idx);
+ $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
+ $td.find("input[name='path']").val(path);
+
+ initDropzone($td.find('.dropzone')[0]);
+ const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
+ editor.focus();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ });
+}
+
+export function initRepoIssueReferenceIssue() {
+ // Reference issue
+ $(document).on('click', '.reference-issue', function (event) {
+ const $this = $(this);
+ const content = $(`#${$this.data('target')}`).text();
+ const poster = $this.data('poster-username');
+ const reference = toAbsoluteUrl($this.data('reference'));
+ const $modal = $($this.data('modal'));
+ $modal.find('textarea[name="content"]').val(`${content}\n\n_Originally posted by @${poster} in ${reference}_`);
+ $modal.modal('show');
+
+ event.preventDefault();
+ });
+}
+
+export function initRepoIssueWipToggle() {
+ // Toggle WIP
+ $('.toggle-wip a, .toggle-wip button').on('click', async (e) => {
+ e.preventDefault();
+ const toggleWip = e.currentTarget.closest('.toggle-wip');
+ const title = toggleWip.getAttribute('data-title');
+ const wipPrefixes = JSON.parse(toggleWip.getAttribute('data-wip-prefixes'));
+ const updateUrl = toggleWip.getAttribute('data-update-url');
+ const prefix = wipPrefixes.find((prefix) => title.startsWith(prefix));
+
+ try {
+ const params = new URLSearchParams();
+ params.append('title', prefix !== undefined ? title.slice(prefix.length).trim() : `${wipPrefixes[0].trim()} ${title}`);
+
+ const response = await POST(updateUrl, {data: params});
+ if (!response.ok) {
+ throw new Error('Failed to toggle WIP status');
+ }
+ window.location.reload();
+ } catch (error) {
+ console.error(error);
+ }
+ });
+}
+
+export function initRepoIssueTitleEdit() {
+ const issueTitleDisplay = document.querySelector('#issue-title-display');
+ const issueTitleEditor = document.querySelector('#issue-title-editor');
+ if (!issueTitleEditor) return;
+
+ const issueTitleInput = issueTitleEditor.querySelector('input');
+ const oldTitle = issueTitleInput.getAttribute('data-old-title');
+ issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
+ hideElem(issueTitleDisplay);
+ hideElem('#pull-desc-display');
+ showElem(issueTitleEditor);
+ showElem('#pull-desc-editor');
+ if (!issueTitleInput.value.trim()) {
+ issueTitleInput.value = oldTitle;
+ }
+ issueTitleInput.focus();
+ });
+ issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
+ hideElem(issueTitleEditor);
+ hideElem('#pull-desc-editor');
+ showElem(issueTitleDisplay);
+ showElem('#pull-desc-display');
+ });
+
+ const pullDescEditor = document.querySelector('#pull-desc-editor'); // it may not exist for a merged PR
+ const prTargetUpdateUrl = pullDescEditor?.getAttribute('data-target-update-url');
+
+ const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
+ const saveAndRefresh = async () => {
+ const newTitle = issueTitleInput.value.trim();
+ try {
+ if (newTitle && newTitle !== oldTitle) {
+ const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
+ if (!resp.ok) {
+ throw new Error(`Failed to update issue title: ${resp.statusText}`);
+ }
+ }
+ if (prTargetUpdateUrl) {
+ const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
+ const oldTargetBranch = document.querySelector('#branch_target').textContent;
+ if (newTargetBranch !== oldTargetBranch) {
+ const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
+ if (!resp.ok) {
+ throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
+ }
+ }
+ }
+ window.location.reload();
+ } catch (error) {
+ console.error(error);
+ showErrorToast(error.message);
+ }
+ };
+ editSaveButton.addEventListener('click', saveAndRefresh);
+ issueTitleEditor.querySelector('input').addEventListener('ce-quick-submit', saveAndRefresh);
+}
+
+export function initRepoIssueBranchSelect() {
+ document.querySelector('#branch-select')?.addEventListener('click', (e) => {
+ const el = e.target.closest('.item[data-branch]');
+ if (!el) return;
+ const pullTargetBranch = document.querySelector('#pull-target-branch');
+ const baseName = pullTargetBranch.getAttribute('data-basename');
+ const branchNameNew = el.getAttribute('data-branch');
+ const branchNameOld = pullTargetBranch.getAttribute('data-branch');
+ pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
+ pullTargetBranch.setAttribute('data-branch', branchNameNew);
+ });
+}
+
+export function initRepoIssueAssignMe() {
+ // Assign to me button
+ document.querySelector('.ui.assignees.list .item.no-select .select-assign-me')
+ ?.addEventListener('click', (e) => {
+ e.preventDefault();
+ const selectMe = e.target;
+ const noSelect = selectMe.parentElement;
+ const selectorList = document.querySelector('.ui.select-assignees .menu');
+
+ if (selectMe.getAttribute('data-action') === 'update') {
+ (async () => {
+ await updateIssuesMeta(
+ selectMe.getAttribute('data-update-url'),
+ selectMe.getAttribute('data-action'),
+ selectMe.getAttribute('data-issue-id'),
+ selectMe.getAttribute('data-id'),
+ );
+ reloadConfirmDraftComment();
+ })();
+ } else {
+ for (const item of selectorList.querySelectorAll('.item')) {
+ if (item.getAttribute('data-id') === selectMe.getAttribute('data-id')) {
+ item.classList.add('checked');
+ item.querySelector('.octicon-check').classList.remove('tw-invisible');
+ }
+ }
+ document.querySelector(selectMe.getAttribute('data-id-selector')).classList.remove('tw-hidden');
+ noSelect.classList.add('tw-hidden');
+ document.querySelector(selectorList.getAttribute('data-id')).value = selectMe.getAttribute('data-id');
+ return false;
+ }
+ });
+}
+
+export function initSingleCommentEditor($commentForm) {
+ // pages:
+ // * normal new issue/pr page, no status-button
+ // * issue/pr view page, with comment form, has status-button
+ const opts = {};
+ const statusButton = document.getElementById('status-button');
+ if (statusButton) {
+ opts.onContentChanged = (editor) => {
+ const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status');
+ statusButton.textContent = statusText;
+ };
+ }
+ initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
+}
+
+export function initIssueTemplateCommentEditors($commentForm) {
+ // pages:
+ // * new issue with issue template
+ const $comboFields = $commentForm.find('.combo-editor-dropzone');
+
+ const initCombo = async ($combo) => {
+ const $dropzoneContainer = $combo.find('.form-field-dropzone');
+ const $formField = $combo.find('.form-field-real');
+ const $markdownEditor = $combo.find('.combo-markdown-editor');
+
+ const editor = await initComboMarkdownEditor($markdownEditor, {
+ onContentChanged: (editor) => {
+ $formField.val(editor.value());
+ },
+ });
+
+ $formField.on('focus', async () => {
+ // deactivate all markdown editors
+ showElem($commentForm.find('.combo-editor-dropzone .form-field-real'));
+ hideElem($commentForm.find('.combo-editor-dropzone .combo-markdown-editor'));
+ hideElem($commentForm.find('.combo-editor-dropzone .form-field-dropzone'));
+
+ // activate this markdown editor
+ hideElem($formField);
+ showElem($markdownEditor);
+ showElem($dropzoneContainer);
+
+ await editor.switchToUserPreference();
+ editor.focus();
+ });
+ };
+
+ for (const el of $comboFields) {
+ initCombo($(el));
+ }
+}
+
+// This function used to show and hide archived label on issue/pr
+// page in the sidebar where we select the labels
+// If we have any archived label tagged to issue and pr. We will show that
+// archived label with checked classed otherwise we will hide it
+// with the help of this function.
+// This function runs globally.
+export function initArchivedLabelHandler() {
+ if (!document.querySelector('.archived-label-hint')) return;
+ for (const label of document.querySelectorAll('[data-is-archived]')) {
+ toggleElem(label, label.classList.contains('checked'));
+ }
+}
+
+// Render the issue's title. It converts emojis and code blocks syntax into their respective HTML equivalent.
+export function issueTitleHTML(title) {
+ return title.replaceAll(/:[-+\w]+:/g, (emoji) => emojiHTML(emoji.substring(1, emoji.length - 1)))
+ .replaceAll(/`[^`]+`/g, (code) => `<code class="inline-code-block">${code.substring(1, code.length - 1)}</code>`);
+}
diff --git a/web_src/js/features/repo-issue.test.js b/web_src/js/features/repo-issue.test.js
new file mode 100644
index 0000000..8c9734b
--- /dev/null
+++ b/web_src/js/features/repo-issue.test.js
@@ -0,0 +1,24 @@
+import {vi} from 'vitest';
+
+import {issueTitleHTML} from './repo-issue.js';
+
+// monaco-editor does not have any exports fields, which trips up vitest
+vi.mock('./comp/ComboMarkdownEditor.js', () => ({}));
+// jQuery is missing
+vi.mock('./common-global.js', () => ({}));
+
+test('Convert issue title to html', () => {
+ expect(issueTitleHTML('')).toEqual('');
+ expect(issueTitleHTML('issue title')).toEqual('issue title');
+
+ const expected_thumbs_up = `<span class="emoji" title=":+1:">👍</span>`;
+ expect(issueTitleHTML(':+1:')).toEqual(expected_thumbs_up);
+ expect(issueTitleHTML(':invalid emoji:')).toEqual(':invalid emoji:');
+
+ const expected_code_block = `<code class="inline-code-block">code</code>`;
+ expect(issueTitleHTML('`code`')).toEqual(expected_code_block);
+ expect(issueTitleHTML('`invalid code')).toEqual('`invalid code');
+ expect(issueTitleHTML('invalid code`')).toEqual('invalid code`');
+
+ expect(issueTitleHTML('issue title :+1: `code`')).toEqual(`issue title ${expected_thumbs_up} ${expected_code_block}`);
+});
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
new file mode 100644
index 0000000..73aaa45
--- /dev/null
+++ b/web_src/js/features/repo-legacy.js
@@ -0,0 +1,610 @@
+import $ from 'jquery';
+import {
+ initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
+ initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
+ initRepoIssueTitleEdit, initRepoIssueWipToggle,
+ initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
+ initRepoIssueAssignMe, reloadConfirmDraftComment,
+} from './repo-issue.js';
+import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
+import {svg} from '../svg.js';
+import {htmlEscape} from 'escape-goat';
+import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
+import {
+ initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
+} from './repo-common.js';
+import {initCitationFileCopyContent} from './citation.js';
+import {initCompLabelEdit} from './comp/LabelEdit.js';
+import {initRepoDiffConversationNav} from './repo-diff.js';
+import {createDropzone} from './dropzone.js';
+import {showErrorToast} from '../modules/toast.js';
+import {initCommentContent, initMarkupContent} from '../markup/content.js';
+import {initCompReactionSelector} from './comp/ReactionSelector.js';
+import {initRepoSettingBranches} from './repo-settings.js';
+import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
+import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js';
+import {hideElem, showElem} from '../utils/dom.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+import {attachRefIssueContextPopup} from './contextpopup.js';
+import {POST, GET} from '../modules/fetch.js';
+
+const {csrfToken} = window.config;
+
+export function initRepoCommentForm() {
+ const $commentForm = $('.comment.form');
+ if (!$commentForm.length) return;
+
+ if ($commentForm.find('.field.combo-editor-dropzone').length) {
+ // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
+ initIssueTemplateCommentEditors($commentForm);
+ } else if ($commentForm.find('.combo-markdown-editor').length) {
+ // it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message
+ initSingleCommentEditor($commentForm);
+ }
+
+ function initBranchSelector() {
+ const $selectBranch = $('.ui.select-branch');
+ const $branchMenu = $selectBranch.find('.reference-list-menu');
+ const $isNewIssue = $branchMenu[0]?.classList.contains('new-issue');
+ $branchMenu.find('.item:not(.no-select)').on('click', async function () {
+ const selectedValue = $(this).data('id');
+ const editMode = $('#editing_mode').val();
+ $($(this).data('id-selector')).val(selectedValue);
+ if ($isNewIssue) {
+ $selectBranch.find('.ui .branch-name').text($(this).data('name'));
+ return;
+ }
+
+ if (editMode === 'true') {
+ const form = document.getElementById('update_issueref_form');
+ const params = new URLSearchParams();
+ params.append('ref', selectedValue);
+ try {
+ await POST(form.getAttribute('action'), {data: params});
+ window.location.reload();
+ } catch (error) {
+ console.error(error);
+ }
+ } else if (editMode === '') {
+ $selectBranch.find('.ui .branch-name').text(selectedValue);
+ }
+ });
+ $selectBranch.find('.reference.column').on('click', function () {
+ hideElem($selectBranch.find('.scrolling.reference-list-menu'));
+ $selectBranch.find('.reference .text').removeClass('black');
+ showElem($($(this).data('target')));
+ $(this).find('.text').addClass('black');
+ return false;
+ });
+ }
+
+ initBranchSelector();
+
+ // List submits
+ function initListSubmits(selector, outerSelector) {
+ const $list = $(`.ui.${outerSelector}.list`);
+ const $noSelect = $list.find('.no-select');
+ const $listMenu = $(`.${selector} .menu`);
+ let hasUpdateAction = $listMenu.data('action') === 'update';
+ const items = {};
+
+ $(`.${selector}`).dropdown({
+ 'action': 'nothing', // do not hide the menu if user presses Enter
+ fullTextSearch: 'exact',
+ async onHide() {
+ hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
+ if (hasUpdateAction) {
+ // TODO: Add batch functionality and make this 1 network request.
+ const itemEntries = Object.entries(items);
+ for (const [elementId, item] of itemEntries) {
+ await updateIssuesMeta(
+ item['update-url'],
+ item.action,
+ item['issue-id'],
+ elementId,
+ );
+ }
+ if (itemEntries.length) {
+ reloadConfirmDraftComment();
+ }
+ }
+ },
+ });
+
+ $listMenu.find('.item:not(.no-select)').on('click', function (e) {
+ e.preventDefault();
+ if (this.classList.contains('ban-change')) {
+ return false;
+ }
+
+ hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
+
+ const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment
+ const scope = this.getAttribute('data-scope');
+
+ $(this).parent().find('.item').each(function () {
+ if (scope) {
+ // Enable only clicked item for scoped labels
+ if (this.getAttribute('data-scope') !== scope) {
+ return true;
+ }
+ if (this !== clickedItem && !this.classList.contains('checked')) {
+ return true;
+ }
+ } else if (this !== clickedItem) {
+ // Toggle for other labels
+ return true;
+ }
+
+ if (this.classList.contains('checked')) {
+ $(this).removeClass('checked');
+ $(this).find('.octicon-check').addClass('tw-invisible');
+ if (hasUpdateAction) {
+ if (!($(this).data('id') in items)) {
+ items[$(this).data('id')] = {
+ 'update-url': $listMenu.data('update-url'),
+ action: 'detach',
+ 'issue-id': $listMenu.data('issue-id'),
+ };
+ } else {
+ delete items[$(this).data('id')];
+ }
+ }
+ } else {
+ $(this).addClass('checked');
+ $(this).find('.octicon-check').removeClass('tw-invisible');
+ if (hasUpdateAction) {
+ if (!($(this).data('id') in items)) {
+ items[$(this).data('id')] = {
+ 'update-url': $listMenu.data('update-url'),
+ action: 'attach',
+ 'issue-id': $listMenu.data('issue-id'),
+ };
+ } else {
+ delete items[$(this).data('id')];
+ }
+ }
+ }
+ });
+
+ // TODO: Which thing should be done for choosing review requests
+ // to make chosen items be shown on time here?
+ if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
+ return false;
+ }
+
+ const listIds = [];
+ $(this).parent().find('.item').each(function () {
+ if (this.classList.contains('checked')) {
+ listIds.push($(this).data('id'));
+ $($(this).data('id-selector')).removeClass('tw-hidden');
+ } else {
+ $($(this).data('id-selector')).addClass('tw-hidden');
+ }
+ });
+ if (!listIds.length) {
+ $noSelect.removeClass('tw-hidden');
+ } else {
+ $noSelect.addClass('tw-hidden');
+ }
+ $($(this).parent().data('id')).val(listIds.join(','));
+ return false;
+ });
+ $listMenu.find('.no-select.item').on('click', function (e) {
+ e.preventDefault();
+ if (hasUpdateAction) {
+ (async () => {
+ await updateIssuesMeta(
+ $listMenu.data('update-url'),
+ 'clear',
+ $listMenu.data('issue-id'),
+ '',
+ );
+ reloadConfirmDraftComment();
+ })();
+ }
+
+ $(this).parent().find('.item').each(function () {
+ $(this).removeClass('checked');
+ $(this).find('.octicon-check').addClass('tw-invisible');
+ });
+
+ if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') {
+ return false;
+ }
+
+ $list.find('.item').each(function () {
+ $(this).addClass('tw-hidden');
+ });
+ $noSelect.removeClass('tw-hidden');
+ $($(this).parent().data('id')).val('');
+ });
+ }
+
+ // Init labels and assignees
+ initListSubmits('select-label', 'labels');
+ initListSubmits('select-assignees', 'assignees');
+ initRepoIssueAssignMe();
+ initListSubmits('select-assignees-modify', 'assignees');
+ initListSubmits('select-reviewers-modify', 'assignees');
+
+ function selectItem(select_id, input_id) {
+ const $menu = $(`${select_id} .menu`);
+ const $list = $(`.ui${select_id}.list`);
+ const hasUpdateAction = $menu.data('action') === 'update';
+
+ $menu.find('.item:not(.no-select)').on('click', function () {
+ $(this).parent().find('.item').each(function () {
+ $(this).removeClass('selected active');
+ });
+
+ $(this).addClass('selected active');
+ if (hasUpdateAction) {
+ (async () => {
+ await updateIssuesMeta(
+ $menu.data('update-url'),
+ '',
+ $menu.data('issue-id'),
+ $(this).data('id'),
+ );
+ reloadConfirmDraftComment();
+ })();
+ }
+
+ let icon = '';
+ if (input_id === '#milestone_id') {
+ icon = svg('octicon-milestone', 18, 'tw-mr-2');
+ } else if (input_id === '#project_id') {
+ icon = svg('octicon-project', 18, 'tw-mr-2');
+ } else if (input_id === '#assignee_ids') {
+ icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`;
+ }
+
+ $list.find('.selected').html(`
+ <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
+ ${icon}
+ ${htmlEscape($(this).text())}
+ </a>
+ `);
+
+ $(`.ui${select_id}.list .no-select`).addClass('tw-hidden');
+ $(input_id).val($(this).data('id'));
+ });
+ $menu.find('.no-select.item').on('click', function () {
+ $(this).parent().find('.item:not(.no-select)').each(function () {
+ $(this).removeClass('selected active');
+ });
+
+ if (hasUpdateAction) {
+ (async () => {
+ await updateIssuesMeta(
+ $menu.data('update-url'),
+ '',
+ $menu.data('issue-id'),
+ $(this).data('id'),
+ );
+ reloadConfirmDraftComment();
+ })();
+ }
+
+ $list.find('.selected').html('');
+ $list.find('.no-select').removeClass('tw-hidden');
+ $(input_id).val('');
+ });
+ }
+
+ // Milestone, Assignee, Project
+ selectItem('.select-project', '#project_id');
+ selectItem('.select-milestone', '#milestone_id');
+ selectItem('.select-assignee', '#assignee_ids');
+}
+
+async function onEditContent(event) {
+ event.preventDefault();
+
+ const segment = this.closest('.header').nextElementSibling;
+ const editContentZone = segment.querySelector('.edit-content-zone');
+ const renderContent = segment.querySelector('.render-content');
+ const rawContent = segment.querySelector('.raw-content');
+
+ let comboMarkdownEditor;
+
+ /**
+ * @param {HTMLElement} dropzone
+ */
+ const setupDropzone = async (dropzone) => {
+ if (!dropzone) return null;
+
+ let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
+ let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
+ const dz = await createDropzone(dropzone, {
+ url: dropzone.getAttribute('data-upload-url'),
+ headers: {'X-Csrf-Token': csrfToken},
+ maxFiles: dropzone.getAttribute('data-max-file'),
+ maxFilesize: dropzone.getAttribute('data-max-size'),
+ acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'),
+ addRemoveLinks: true,
+ dictDefaultMessage: dropzone.getAttribute('data-default-message'),
+ dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'),
+ dictFileTooBig: dropzone.getAttribute('data-file-too-big'),
+ dictRemoveFile: dropzone.getAttribute('data-remove-file'),
+ timeout: 0,
+ thumbnailMethod: 'contain',
+ thumbnailWidth: 480,
+ thumbnailHeight: 480,
+ init() {
+ this.on('success', (file, data) => {
+ file.uuid = data.uuid;
+ fileUuidDict[file.uuid] = {submitted: false};
+ const input = document.createElement('input');
+ input.id = data.uuid;
+ input.name = 'files';
+ input.type = 'hidden';
+ input.value = data.uuid;
+ dropzone.querySelector('.files').append(input);
+ });
+ this.on('removedfile', async (file) => {
+ document.getElementById(file.uuid)?.remove();
+ if (disableRemovedfileEvent) return;
+ if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
+ try {
+ await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ });
+ this.on('submit', () => {
+ for (const fileUuid of Object.keys(fileUuidDict)) {
+ fileUuidDict[fileUuid].submitted = true;
+ }
+ });
+ this.on('reload', async () => {
+ try {
+ const response = await GET(editContentZone.getAttribute('data-attachment-url'));
+ const data = await response.json();
+ // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server
+ disableRemovedfileEvent = true;
+ dz.removeAllFiles(true);
+ dropzone.querySelector('.files').innerHTML = '';
+ for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove();
+ fileUuidDict = {};
+ disableRemovedfileEvent = false;
+
+ for (const attachment of data) {
+ const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`;
+ dz.emit('addedfile', attachment);
+ dz.emit('thumbnail', attachment, imgSrc);
+ dz.emit('complete', attachment);
+ fileUuidDict[attachment.uuid] = {submitted: true};
+ dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%';
+ const input = document.createElement('input');
+ input.id = attachment.uuid;
+ input.name = 'files';
+ input.type = 'hidden';
+ input.value = attachment.uuid;
+ dropzone.querySelector('.files').append(input);
+ }
+ if (!dropzone.querySelector('.dz-preview')) {
+ dropzone.classList.remove('dz-started');
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ });
+ },
+ });
+ dz.emit('reload');
+ return dz;
+ };
+
+ const cancelAndReset = (e) => {
+ e.preventDefault();
+ showElem(renderContent);
+ hideElem(editContentZone);
+ comboMarkdownEditor.attachedDropzoneInst?.emit('reload');
+ };
+
+ const saveAndRefresh = async (e) => {
+ e.preventDefault();
+ showElem(renderContent);
+ hideElem(editContentZone);
+ const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst;
+ try {
+ const params = new URLSearchParams({
+ content: comboMarkdownEditor.value(),
+ context: editContentZone.getAttribute('data-context'),
+ content_version: editContentZone.getAttribute('data-content-version'),
+ });
+ for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
+
+ const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
+ const data = await response.json();
+ if (response.status === 400) {
+ showErrorToast(data.errorMessage);
+ return;
+ }
+ editContentZone.setAttribute('data-content-version', data.contentVersion);
+ if (!data.content) {
+ renderContent.innerHTML = document.getElementById('no-content').innerHTML;
+ rawContent.textContent = '';
+ } else {
+ renderContent.innerHTML = data.content;
+ rawContent.textContent = comboMarkdownEditor.value();
+ const refIssues = renderContent.querySelectorAll('p .ref-issue');
+ attachRefIssueContextPopup(refIssues);
+ }
+ const content = segment;
+ if (!content.querySelector('.dropzone-attachments')) {
+ if (data.attachments !== '') {
+ content.insertAdjacentHTML('beforeend', data.attachments);
+ }
+ } else if (data.attachments === '') {
+ content.querySelector('.dropzone-attachments').remove();
+ } else {
+ content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
+ }
+ dropzoneInst?.emit('submit');
+ dropzoneInst?.emit('reload');
+ initMarkupContent();
+ initCommentContent();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+ if (!comboMarkdownEditor) {
+ editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
+ comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
+ comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
+ editContentZone.addEventListener('ce-quick-submit', saveAndRefresh);
+ editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
+ editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
+ } else {
+ const tabEditor = editContentZone.querySelector('.combo-markdown-editor').querySelector('.tabular.menu > a[data-tab-for=markdown-writer]');
+ tabEditor?.click();
+ }
+
+ // Show write/preview tab and copy raw content as needed
+ showElem(editContentZone);
+ hideElem(renderContent);
+ if (!comboMarkdownEditor.value()) {
+ comboMarkdownEditor.value(rawContent.textContent);
+ }
+ comboMarkdownEditor.focus();
+}
+
+export function initRepository() {
+ if (!$('.page-content.repository').length) return;
+
+ initRepoBranchTagSelector('.js-branch-tag-selector');
+
+ // Options
+ if ($('.repository.settings.options').length > 0) {
+ // Enable or select internal/external wiki system and issue tracker.
+ $('.enable-system').on('change', function () {
+ if (this.checked) {
+ $($(this).data('target')).removeClass('disabled');
+ if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
+ } else {
+ $($(this).data('target')).addClass('disabled');
+ if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
+ }
+ });
+ $('.enable-system-radio').on('change', function () {
+ if (this.value === 'false') {
+ $($(this).data('target')).addClass('disabled');
+ if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
+ } else if (this.value === 'true') {
+ $($(this).data('target')).removeClass('disabled');
+ if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled');
+ }
+ });
+ const $trackerIssueStyleRadios = $('.js-tracker-issue-style');
+ $trackerIssueStyleRadios.on('change input', () => {
+ const checkedVal = $trackerIssueStyleRadios.filter(':checked').val();
+ $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp');
+ });
+ }
+
+ // Labels
+ initCompLabelEdit('.repository.labels');
+
+ // Milestones
+ if ($('.repository.new.milestone').length > 0) {
+ $('#clear-date').on('click', () => {
+ $('#deadline').val('');
+ return false;
+ });
+ }
+
+ // Repo Creation
+ if ($('.repository.new.repo').length > 0) {
+ $('input[name="gitignores"], input[name="license"]').on('change', () => {
+ const gitignores = $('input[name="gitignores"]').val();
+ const license = $('input[name="license"]').val();
+ if (gitignores || license) {
+ document.querySelector('input[name="auto_init"]').checked = true;
+ }
+ });
+ }
+
+ // Compare or pull request
+ const $repoDiff = $('.repository.diff');
+ if ($repoDiff.length) {
+ initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown');
+ initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
+ }
+
+ initRepoCloneLink();
+ initCitationFileCopyContent();
+ initRepoSettingBranches();
+
+ // Issues
+ if ($('.repository.view.issue').length > 0) {
+ initRepoIssueCommentEdit();
+
+ initRepoIssueBranchSelect();
+ initRepoIssueTitleEdit();
+ initRepoIssueWipToggle();
+ initRepoIssueComments();
+
+ initRepoDiffConversationNav();
+ initRepoIssueReferenceIssue();
+
+ initRepoIssueCommentDelete();
+ initRepoIssueDependencyDelete();
+ initRepoIssueCodeCommentCancel();
+ initRepoPullRequestUpdate();
+ initCompReactionSelector($(document));
+
+ initRepoPullRequestMergeForm();
+ initRepoPullRequestCommitStatus();
+ }
+
+ // Pull request
+ const $repoComparePull = $('.repository.compare.pull');
+ if ($repoComparePull.length > 0) {
+ // show pull request form
+ $repoComparePull.find('button.show-form').on('click', function (e) {
+ e.preventDefault();
+ hideElem($(this).parent());
+
+ const $form = $repoComparePull.find('.pullrequest-form');
+ showElem($form);
+ });
+ }
+
+ initUnicodeEscapeButton();
+}
+
+function initRepoIssueCommentEdit() {
+ // Edit issue or comment content
+ $(document).on('click', '.edit-content', onEditContent);
+
+ // Quote reply
+ $(document).on('click', '.quote-reply', async function (event) {
+ event.preventDefault();
+ const target = $(this).data('target');
+ const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
+ const content = `> ${quote}\n\n`;
+ let editor;
+ if (this.classList.contains('quote-reply-diff')) {
+ const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
+ editor = await handleReply($replyBtn);
+ } else {
+ // for normal issue/comment page
+ editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
+ }
+ if (editor) {
+ if (editor.value()) {
+ editor.value(`${editor.value()}\n\n${content}`);
+ } else {
+ editor.value(content);
+ }
+ editor.focus();
+ editor.moveCursorToEnd();
+ }
+ });
+}
diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js
new file mode 100644
index 0000000..fc42ce8
--- /dev/null
+++ b/web_src/js/features/repo-migrate.js
@@ -0,0 +1,64 @@
+import {hideElem, showElem} from '../utils/dom.js';
+import {GET, POST} from '../modules/fetch.js';
+
+const {appSubUrl} = window.config;
+
+export function initRepoMigrationStatusChecker() {
+ const repoMigrating = document.getElementById('repo_migrating');
+ if (!repoMigrating) return;
+
+ document.getElementById('repo_migrating_retry')?.addEventListener('click', doMigrationRetry);
+
+ const task = repoMigrating.getAttribute('data-migrating-task-id');
+
+ // returns true if the refresh still needs to be called after a while
+ const refresh = async () => {
+ const res = await GET(`${appSubUrl}/user/task/${task}`);
+ if (res.url.endsWith('/login')) return false; // stop refreshing if redirected to login
+ if (res.status !== 200) return true; // continue to refresh if network error occurs
+
+ const data = await res.json();
+
+ // for all status
+ if (data.message) {
+ document.getElementById('repo_migrating_progress_message').textContent = data.message;
+ }
+
+ // TaskStatusFinished
+ if (data.status === 4) {
+ window.location.reload();
+ return false;
+ }
+
+ // TaskStatusFailed
+ if (data.status === 3) {
+ hideElem('#repo_migrating_progress');
+ hideElem('#repo_migrating');
+ showElem('#repo_migrating_retry');
+ showElem('#repo_migrating_failed');
+ showElem('#repo_migrating_failed_image');
+ document.getElementById('repo_migrating_failed_error').textContent = data.message;
+ return false;
+ }
+
+ return true; // continue to refresh
+ };
+
+ const syncTaskStatus = async () => {
+ let doNextRefresh = true;
+ try {
+ doNextRefresh = await refresh();
+ } finally {
+ if (doNextRefresh) {
+ setTimeout(syncTaskStatus, 2000);
+ }
+ }
+ };
+
+ syncTaskStatus(); // no await
+}
+
+async function doMigrationRetry(e) {
+ await POST(e.target.getAttribute('data-migrating-task-retry-url'));
+ window.location.reload();
+}
diff --git a/web_src/js/features/repo-migration.js b/web_src/js/features/repo-migration.js
new file mode 100644
index 0000000..59e282e
--- /dev/null
+++ b/web_src/js/features/repo-migration.js
@@ -0,0 +1,69 @@
+import {hideElem, showElem, toggleElem} from '../utils/dom.js';
+
+const service = document.getElementById('service_type');
+const user = document.getElementById('auth_username');
+const pass = document.getElementById('auth_password');
+const token = document.getElementById('auth_token');
+const mirror = document.getElementById('mirror');
+const lfs = document.getElementById('lfs');
+const lfsSettings = document.getElementById('lfs_settings');
+const lfsEndpoint = document.getElementById('lfs_endpoint');
+const items = document.querySelectorAll('#migrate_items input[type=checkbox]');
+
+export function initRepoMigration() {
+ checkAuth();
+ setLFSSettingsVisibility();
+
+ user?.addEventListener('input', () => {checkItems(false)});
+ pass?.addEventListener('input', () => {checkItems(false)});
+ token?.addEventListener('input', () => {checkItems(true)});
+ mirror?.addEventListener('change', () => {checkItems(true)});
+ document.getElementById('lfs_settings_show')?.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ showElem(lfsEndpoint);
+ });
+ lfs?.addEventListener('change', setLFSSettingsVisibility);
+
+ const cloneAddr = document.getElementById('clone_addr');
+ cloneAddr?.addEventListener('change', () => {
+ const repoName = document.getElementById('repo_name');
+ if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
+ repoName.value = cloneAddr.value.match(/^(.*\/)?((.+?)(\.git)?)$/)[3];
+ }
+ });
+}
+
+function checkAuth() {
+ if (!service) return;
+ const serviceType = Number(service.value);
+
+ checkItems(serviceType !== 1);
+}
+
+function checkItems(tokenAuth) {
+ let enableItems;
+ if (tokenAuth) {
+ enableItems = token?.value !== '';
+ } else {
+ enableItems = user?.value !== '' || pass?.value !== '';
+ }
+ if (enableItems && Number(service?.value) > 1) {
+ if (mirror?.checked) {
+ for (const item of items) {
+ item.disabled = item.name !== 'wiki';
+ }
+ return;
+ }
+ for (const item of items) item.disabled = false;
+ } else {
+ for (const item of items) item.disabled = true;
+ }
+}
+
+function setLFSSettingsVisibility() {
+ if (!lfs) return;
+ const visible = lfs.checked;
+ toggleElem(lfsSettings, visible);
+ hideElem(lfsEndpoint);
+}
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
new file mode 100644
index 0000000..a1cc4b3
--- /dev/null
+++ b/web_src/js/features/repo-projects.js
@@ -0,0 +1,188 @@
+import $ from 'jquery';
+import {contrastColor} from '../utils/color.js';
+import {createSortable} from '../modules/sortable.js';
+import {POST, DELETE, PUT} from '../modules/fetch.js';
+
+function updateIssueCount(cards) {
+ const parent = cards.parentElement;
+ const cnt = parent.getElementsByClassName('issue-card').length;
+ parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt;
+}
+
+async function createNewColumn(url, columnTitle, projectColorInput) {
+ try {
+ await POST(url, {
+ data: {
+ title: columnTitle.val(),
+ color: projectColorInput.val(),
+ },
+ });
+ } catch (error) {
+ console.error(error);
+ } finally {
+ columnTitle.closest('form').removeClass('dirty');
+ window.location.reload();
+ }
+}
+
+async function moveIssue({item, from, to, oldIndex}) {
+ const columnCards = to.getElementsByClassName('issue-card');
+ updateIssueCount(from);
+ updateIssueCount(to);
+
+ const columnSorting = {
+ issues: Array.from(columnCards, (card, i) => ({
+ issueID: parseInt(card.getAttribute('data-issue')),
+ sorting: i,
+ })),
+ };
+
+ try {
+ await POST(`${to.getAttribute('data-url')}/move`, {
+ data: columnSorting,
+ });
+ } catch (error) {
+ console.error(error);
+ from.insertBefore(item, from.children[oldIndex]);
+ }
+}
+
+async function initRepoProjectSortable() {
+ const els = document.querySelectorAll('#project-board > .board.sortable');
+ if (!els.length) return;
+
+ // the HTML layout is: #project-board > .board > .project-column .cards > .issue-card
+ const mainBoard = els[0];
+ let boardColumns = mainBoard.getElementsByClassName('project-column');
+ createSortable(mainBoard, {
+ group: 'project-column',
+ draggable: '.project-column',
+ handle: '.project-column-header',
+ delayOnTouchOnly: true,
+ delay: 500,
+ onSort: async () => {
+ boardColumns = mainBoard.getElementsByClassName('project-column');
+
+ const columnSorting = {
+ columns: Array.from(boardColumns, (column, i) => ({
+ columnID: parseInt(column.getAttribute('data-id')),
+ sorting: i,
+ })),
+ };
+
+ try {
+ await POST(mainBoard.getAttribute('data-url'), {
+ data: columnSorting,
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ },
+ });
+
+ for (const boardColumn of boardColumns) {
+ const boardCardList = boardColumn.getElementsByClassName('cards')[0];
+ createSortable(boardCardList, {
+ group: 'shared',
+ onAdd: moveIssue,
+ onUpdate: moveIssue,
+ delayOnTouchOnly: true,
+ delay: 500,
+ });
+ }
+}
+
+export function initRepoProject() {
+ if (!document.querySelector('.repository.projects')) {
+ return;
+ }
+
+ const _promise = initRepoProjectSortable();
+
+ for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
+ const projectHeader = modal.closest('.project-column-header');
+ const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
+ const projectTitleInput = modal.querySelector('.project-column-title-input');
+ const projectColorInput = modal.querySelector('#new_project_column_color');
+ const boardColumn = modal.closest('.project-column');
+ modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
+ e.preventDefault();
+ try {
+ await PUT(this.getAttribute('data-url'), {
+ data: {
+ title: projectTitleInput?.value,
+ color: projectColorInput?.value,
+ },
+ });
+ } catch (error) {
+ console.error(error);
+ } finally {
+ projectTitleLabel.textContent = projectTitleInput?.value;
+ projectTitleInput.closest('form')?.classList.remove('dirty');
+ const dividers = boardColumn.querySelectorAll(':scope > .divider');
+ if (projectColorInput.value) {
+ const color = contrastColor(projectColorInput.value);
+ boardColumn.style.setProperty('background', projectColorInput.value, 'important');
+ boardColumn.style.setProperty('color', color, 'important');
+ for (const divider of dividers) {
+ divider.style.setProperty('color', color);
+ }
+ } else {
+ boardColumn.style.removeProperty('background');
+ boardColumn.style.removeProperty('color');
+ for (const divider of dividers) {
+ divider.style.removeProperty('color');
+ }
+ }
+ $('.ui.modal').modal('hide');
+ }
+ });
+ }
+
+ $('.default-project-column-modal').each(function () {
+ const $boardColumn = $(this).closest('.project-column');
+ const $showButton = $($boardColumn).find('.default-project-column-show');
+ const $commitButton = $(this).find('.actions > .ok.button');
+
+ $($commitButton).on('click', async (e) => {
+ e.preventDefault();
+
+ try {
+ await POST($($showButton).data('url'));
+ } catch (error) {
+ console.error(error);
+ } finally {
+ window.location.reload();
+ }
+ });
+ });
+
+ $('.show-delete-project-column-modal').each(function () {
+ const $deleteColumnModal = $(`${this.getAttribute('data-modal')}`);
+ const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button');
+ const deleteUrl = this.getAttribute('data-url');
+
+ $deleteColumnButton.on('click', async (e) => {
+ e.preventDefault();
+
+ try {
+ await DELETE(deleteUrl);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ window.location.reload();
+ }
+ });
+ });
+
+ $('#new_project_column_submit').on('click', (e) => {
+ e.preventDefault();
+ const $columnTitle = $('#new_project_column');
+ const $projectColorInput = $('#new_project_column_color_picker');
+ if (!$columnTitle.val()) {
+ return;
+ }
+ const url = e.target.getAttribute('data-url');
+ createNewColumn(url, $columnTitle, $projectColorInput);
+ });
+}
diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
new file mode 100644
index 0000000..0db9b8a
--- /dev/null
+++ b/web_src/js/features/repo-release.js
@@ -0,0 +1,95 @@
+import {hideElem, showElem} from '../utils/dom.js';
+import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+
+export function initRepoRelease() {
+ for (const el of document.querySelectorAll('.remove-rel-attach')) {
+ el.addEventListener('click', (e) => {
+ const uuid = e.target.getAttribute('data-uuid');
+ const id = e.target.getAttribute('data-id');
+ document.querySelector(`input[name='attachment-del-${uuid}']`).value =
+ 'true';
+ hideElem(`#attachment-${id}`);
+ });
+ }
+}
+
+export function initRepoReleaseNew() {
+ if (!document.querySelector('.repository.new.release')) return;
+
+ initTagNameEditor();
+ initRepoReleaseEditor();
+ initAddExternalLinkButton();
+}
+
+function initTagNameEditor() {
+ const el = document.getElementById('tag-name-editor');
+ if (!el) return;
+
+ const existingTags = JSON.parse(el.getAttribute('data-existing-tags'));
+ if (!Array.isArray(existingTags)) return;
+
+ const defaultTagHelperText = el.getAttribute('data-tag-helper');
+ const newTagHelperText = el.getAttribute('data-tag-helper-new');
+ const existingTagHelperText = el.getAttribute('data-tag-helper-existing');
+
+ document.getElementById('tag-name').addEventListener('keyup', (e) => {
+ const value = e.target.value;
+ const tagHelper = document.getElementById('tag-helper');
+ if (existingTags.includes(value)) {
+ // If the tag already exists, hide the target branch selector.
+ hideElem('#tag-target-selector');
+ tagHelper.textContent = existingTagHelperText;
+ } else {
+ showElem('#tag-target-selector');
+ tagHelper.textContent = value ? newTagHelperText : defaultTagHelperText;
+ }
+ });
+}
+
+function initRepoReleaseEditor() {
+ const editor = document.querySelector(
+ '.repository.new.release .combo-markdown-editor',
+ );
+ if (!editor) {
+ return;
+ }
+ initComboMarkdownEditor(editor);
+}
+
+let newAttachmentCount = 0;
+
+function initAddExternalLinkButton() {
+ const addExternalLinkButton = document.getElementById('add-external-link');
+ if (!addExternalLinkButton) return;
+
+ addExternalLinkButton.addEventListener('click', () => {
+ newAttachmentCount += 1;
+ const attachmentTemplate = document.getElementById('attachment-template');
+
+ const newAttachment = attachmentTemplate.cloneNode(true);
+ newAttachment.id = `attachment-N${newAttachmentCount}`;
+ newAttachment.classList.remove('tw-hidden');
+
+ const attachmentName = newAttachment.querySelector(
+ 'input[name="attachment-template-new-name"]',
+ );
+ attachmentName.name = `attachment-new-name-${newAttachmentCount}`;
+ attachmentName.required = true;
+
+ const attachmentExtUrl = newAttachment.querySelector(
+ 'input[name="attachment-template-new-exturl"]',
+ );
+ attachmentExtUrl.name = `attachment-new-exturl-${newAttachmentCount}`;
+ attachmentExtUrl.required = true;
+
+ const attachmentDel = newAttachment.querySelector('.remove-rel-attach');
+ attachmentDel.addEventListener('click', () => {
+ newAttachment.remove();
+ });
+
+ attachmentTemplate.parentNode.insertBefore(
+ newAttachment,
+ attachmentTemplate,
+ );
+ });
+}
diff --git a/web_src/js/features/repo-search.js b/web_src/js/features/repo-search.js
new file mode 100644
index 0000000..185f611
--- /dev/null
+++ b/web_src/js/features/repo-search.js
@@ -0,0 +1,22 @@
+export function initRepositorySearch() {
+ const repositorySearchForm = document.querySelector('#repo-search-form');
+ if (!repositorySearchForm) return;
+
+ repositorySearchForm.addEventListener('change', (e) => {
+ e.preventDefault();
+
+ const formData = new FormData(repositorySearchForm);
+ const params = new URLSearchParams(formData);
+
+ if (e.target.name === 'clear-filter') {
+ params.delete('archived');
+ params.delete('fork');
+ params.delete('mirror');
+ params.delete('template');
+ params.delete('private');
+ }
+
+ params.delete('clear-filter');
+ window.location.search = params.toString();
+ });
+}
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
new file mode 100644
index 0000000..52c5de2
--- /dev/null
+++ b/web_src/js/features/repo-settings.js
@@ -0,0 +1,120 @@
+import $ from 'jquery';
+import {minimatch} from 'minimatch';
+import {createMonaco} from './codeeditor.js';
+import {onInputDebounce, toggleElem} from '../utils/dom.js';
+import {POST} from '../modules/fetch.js';
+
+const {appSubUrl, csrfToken} = window.config;
+
+export function initRepoSettingsCollaboration() {
+ // Change collaborator access mode
+ $('.page-content.repository .ui.dropdown.access-mode').each((_, el) => {
+ const $dropdown = $(el);
+ const $text = $dropdown.find('> .text');
+ $dropdown.dropdown({
+ async action(_text, value) {
+ const lastValue = el.getAttribute('data-last-value');
+ try {
+ el.setAttribute('data-last-value', value);
+ $dropdown.dropdown('hide');
+ const data = new FormData();
+ data.append('uid', el.getAttribute('data-uid'));
+ data.append('mode', value);
+ await POST(el.getAttribute('data-url'), {data});
+ } catch {
+ $text.text('(error)'); // prevent from misleading users when error occurs
+ el.setAttribute('data-last-value', lastValue);
+ }
+ },
+ onChange(_value, text, _$choice) {
+ $text.text(text); // update the text when using keyboard navigating
+ },
+ onHide() {
+ // set to the really selected value, defer to next tick to make sure `action` has finished its work because the calling order might be onHide -> action
+ setTimeout(() => {
+ const $item = $dropdown.dropdown('get item', el.getAttribute('data-last-value'));
+ if ($item) {
+ $dropdown.dropdown('set selected', el.getAttribute('data-last-value'));
+ } else {
+ $text.text('(none)'); // prevent from misleading users when the access mode is undefined
+ }
+ }, 0);
+ },
+ });
+ });
+}
+
+export function initRepoSettingSearchTeamBox() {
+ const searchTeamBox = document.getElementById('search-team-box');
+ if (!searchTeamBox) return;
+
+ $(searchTeamBox).search({
+ minCharacters: 2,
+ apiSettings: {
+ url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`,
+ headers: {'X-Csrf-Token': csrfToken},
+ onResponse(response) {
+ const items = [];
+ $.each(response.data, (_i, item) => {
+ items.push({
+ title: item.name,
+ description: `${item.permission} access`, // TODO: translate this string
+ });
+ });
+
+ return {results: items};
+ },
+ },
+ searchFields: ['name', 'description'],
+ showNoResults: false,
+ });
+}
+
+export function initRepoSettingGitHook() {
+ if (!$('.edit.githook').length) return;
+ const filename = document.querySelector('.hook-filename').textContent;
+ const _promise = createMonaco($('#content')[0], filename, {language: 'shell'});
+}
+
+export function initRepoSettingBranches() {
+ if (!document.querySelector('.repository.settings.branches')) return;
+
+ for (const el of document.getElementsByClassName('toggle-target-enabled')) {
+ el.addEventListener('change', function () {
+ const target = document.querySelector(this.getAttribute('data-target'));
+ target?.classList.toggle('disabled', !this.checked);
+ });
+ }
+
+ for (const el of document.getElementsByClassName('toggle-target-disabled')) {
+ el.addEventListener('change', function () {
+ const target = document.querySelector(this.getAttribute('data-target'));
+ if (this.checked) target?.classList.add('disabled'); // only disable, do not auto enable
+ });
+ }
+
+ document.getElementById('dismiss_stale_approvals')?.addEventListener('change', function () {
+ document.getElementById('ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked);
+ });
+
+ // show the `Matched` mark for the status checks that match the pattern
+ const markMatchedStatusChecks = () => {
+ const patterns = (document.getElementById('status_check_contexts').value || '').split(/[\r\n]+/);
+ const validPatterns = patterns.map((item) => item.trim()).filter(Boolean);
+ const marks = document.getElementsByClassName('status-check-matched-mark');
+
+ for (const el of marks) {
+ let matched = false;
+ const statusCheck = el.getAttribute('data-status-check');
+ for (const pattern of validPatterns) {
+ if (minimatch(statusCheck, pattern)) {
+ matched = true;
+ break;
+ }
+ }
+ toggleElem(el, matched);
+ }
+ };
+ markMatchedStatusChecks();
+ document.getElementById('status_check_contexts').addEventListener('input', onInputDebounce(markMatchedStatusChecks));
+}
diff --git a/web_src/js/features/repo-template.js b/web_src/js/features/repo-template.js
new file mode 100644
index 0000000..5f63e8b
--- /dev/null
+++ b/web_src/js/features/repo-template.js
@@ -0,0 +1,51 @@
+import $ from 'jquery';
+import {htmlEscape} from 'escape-goat';
+import {hideElem, showElem} from '../utils/dom.js';
+
+const {appSubUrl} = window.config;
+
+export function initRepoTemplateSearch() {
+ const $repoTemplate = $('#repo_template');
+ const checkTemplate = function () {
+ const $templateUnits = $('#template_units');
+ const $nonTemplate = $('#non_template');
+ if ($repoTemplate.val() !== '' && $repoTemplate.val() !== '0') {
+ showElem($templateUnits);
+ hideElem($nonTemplate);
+ } else {
+ hideElem($templateUnits);
+ showElem($nonTemplate);
+ }
+ };
+ $repoTemplate.on('change', checkTemplate);
+ checkTemplate();
+
+ const changeOwner = function () {
+ $('#repo_template_search')
+ .dropdown({
+ apiSettings: {
+ url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`,
+ onResponse(response) {
+ const filteredResponse = {success: true, results: []};
+ filteredResponse.results.push({
+ name: '',
+ value: '',
+ });
+ // Parse the response from the api to work with our dropdown
+ $.each(response.data, (_r, repo) => {
+ filteredResponse.results.push({
+ name: htmlEscape(repo.repository.full_name),
+ value: repo.repository.id,
+ });
+ });
+ return filteredResponse;
+ },
+ cache: false,
+ },
+
+ fullTextSearch: true,
+ });
+ };
+ $('#uid').on('change', changeOwner);
+ changeOwner();
+}
diff --git a/web_src/js/features/repo-unicode-escape.js b/web_src/js/features/repo-unicode-escape.js
new file mode 100644
index 0000000..9f0c745
--- /dev/null
+++ b/web_src/js/features/repo-unicode-escape.js
@@ -0,0 +1,27 @@
+import {hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.js';
+
+export function initUnicodeEscapeButton() {
+ document.addEventListener('click', (e) => {
+ const btn = e.target.closest('.escape-button, .unescape-button, .toggle-escape-button');
+ if (!btn) return;
+
+ e.preventDefault();
+
+ const fileContent = btn.closest('.file-content, .non-diff-file-content, .file-preview-box');
+ const fileView = fileContent?.querySelectorAll('.file-code, .file-view, .file-preview');
+ if (btn.matches('.escape-button')) {
+ for (const el of fileView) el.classList.add('unicode-escaped');
+ hideElem(btn);
+ showElem(queryElemSiblings(btn, '.unescape-button'));
+ } else if (btn.matches('.unescape-button')) {
+ for (const el of fileView) el.classList.remove('unicode-escaped');
+ hideElem(btn);
+ showElem(queryElemSiblings(btn, '.escape-button'));
+ } else if (btn.matches('.toggle-escape-button')) {
+ const isEscaped = fileView[0]?.classList.contains('unicode-escaped');
+ for (const el of fileView) el.classList.toggle('unicode-escaped', !isEscaped);
+ toggleElem(fileContent.querySelectorAll('.unescape-button'), !isEscaped);
+ toggleElem(fileContent.querySelectorAll('.escape-button'), isEscaped);
+ }
+ });
+}
diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js
new file mode 100644
index 0000000..03a2c68
--- /dev/null
+++ b/web_src/js/features/repo-wiki.js
@@ -0,0 +1,89 @@
+import {initMarkupContent} from '../markup/content.js';
+import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
+import {fomanticMobileScreen} from '../modules/fomantic.js';
+import {POST} from '../modules/fetch.js';
+
+async function initRepoWikiFormEditor() {
+ const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea');
+ if (!editArea) return;
+
+ const form = document.querySelector('.repository.wiki.new .ui.form');
+ const editorContainer = form.querySelector('.combo-markdown-editor');
+ let editor;
+
+ let renderRequesting = false;
+ let lastContent;
+ const renderEasyMDEPreview = async function () {
+ if (renderRequesting) return;
+
+ const previewFull = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active');
+ const previewSide = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active-side');
+ const previewTarget = previewSide || previewFull;
+ const newContent = editArea.value;
+ if (editor && previewTarget && lastContent !== newContent) {
+ renderRequesting = true;
+ const formData = new FormData();
+ formData.append('mode', editor.previewMode);
+ formData.append('context', editor.previewContext);
+ formData.append('text', newContent);
+ formData.append('wiki', editor.previewWiki);
+ try {
+ const response = await POST(editor.previewUrl, {data: formData});
+ const data = await response.text();
+ lastContent = newContent;
+ previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`;
+ initMarkupContent();
+ } catch (error) {
+ console.error('Error rendering preview:', error);
+ } finally {
+ renderRequesting = false;
+ setTimeout(renderEasyMDEPreview, 1000);
+ }
+ } else {
+ setTimeout(renderEasyMDEPreview, 1000);
+ }
+ };
+ renderEasyMDEPreview();
+
+ editor = await initComboMarkdownEditor(editorContainer, {
+ useScene: 'wiki',
+ // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
+ // And another benefit is that we only need to write the style once for both editors.
+ // TODO: Move height style to CSS after EasyMDE removal.
+ editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
+ previewMode: 'gfm',
+ previewWiki: true,
+ easyMDEOptions: {
+ previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
+ toolbar: ['bold', 'italic', 'strikethrough', '|',
+ 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
+ 'gitea-code-inline', 'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|',
+ 'unordered-list', 'ordered-list', '|',
+ 'link', 'image', 'table', 'horizontal-rule', '|',
+ 'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
+ ],
+ },
+ });
+
+ form.addEventListener('submit', (e) => {
+ if (!validateTextareaNonEmpty(editArea)) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+}
+
+function collapseWikiTocForMobile(collapse) {
+ if (collapse) {
+ document.querySelector('.wiki-content-toc details')?.removeAttribute('open');
+ }
+}
+
+export function initRepoWikiForm() {
+ if (!document.querySelector('.page-content.repository.wiki')) return;
+
+ fomanticMobileScreen.addEventListener('change', (e) => collapseWikiTocForMobile(e.matches));
+ collapseWikiTocForMobile(fomanticMobileScreen.matches);
+
+ initRepoWikiFormEditor();
+}
diff --git a/web_src/js/features/sshkey-helper.js b/web_src/js/features/sshkey-helper.js
new file mode 100644
index 0000000..3960eef
--- /dev/null
+++ b/web_src/js/features/sshkey-helper.js
@@ -0,0 +1,10 @@
+export function initSshKeyFormParser() {
+ // Parse SSH Key
+ document.getElementById('ssh-key-content')?.addEventListener('input', function () {
+ const arrays = this.value.split(' ');
+ const title = document.getElementById('ssh-key-title');
+ if (!title.value && arrays.length === 3 && arrays[2] !== '') {
+ title.value = arrays[2];
+ }
+ });
+}
diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
new file mode 100644
index 0000000..d070f52
--- /dev/null
+++ b/web_src/js/features/stopwatch.js
@@ -0,0 +1,167 @@
+import prettyMilliseconds from 'pretty-ms';
+import {createTippy} from '../modules/tippy.js';
+import {GET} from '../modules/fetch.js';
+import {hideElem, showElem} from '../utils/dom.js';
+
+const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
+
+export function initStopwatch() {
+ if (!enableTimeTracking) {
+ return;
+ }
+
+ const stopwatchEl = document.querySelector('.active-stopwatch-trigger');
+ const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
+
+ if (!stopwatchEl || !stopwatchPopup) {
+ return;
+ }
+
+ stopwatchEl.removeAttribute('href'); // intended for noscript mode only
+
+ createTippy(stopwatchEl, {
+ content: stopwatchPopup,
+ placement: 'bottom-end',
+ trigger: 'click',
+ maxWidth: 'none',
+ interactive: true,
+ hideOnClick: true,
+ });
+
+ // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
+ const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
+ if (currSeconds) {
+ updateStopwatchTime(currSeconds);
+ }
+
+ let usingPeriodicPoller = false;
+ const startPeriodicPoller = (timeout) => {
+ if (timeout <= 0 || !Number.isFinite(timeout)) return;
+ usingPeriodicPoller = true;
+ setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
+ };
+
+ // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller
+ if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
+ // Try to connect to the event source via the shared worker first
+ const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker');
+ worker.addEventListener('error', (event) => {
+ console.error('worker error', event);
+ });
+ worker.port.addEventListener('messageerror', () => {
+ console.error('unable to deserialize message');
+ });
+ worker.port.postMessage({
+ type: 'start',
+ url: `${window.location.origin}${appSubUrl}/user/events`,
+ });
+ worker.port.addEventListener('message', (event) => {
+ if (!event.data || !event.data.type) {
+ console.error('unknown worker message event', event);
+ return;
+ }
+ if (event.data.type === 'stopwatches') {
+ updateStopwatchData(JSON.parse(event.data.data));
+ } else if (event.data.type === 'no-event-source') {
+ // browser doesn't support EventSource, falling back to periodic poller
+ if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
+ } else if (event.data.type === 'error') {
+ console.error('worker port event error', event.data);
+ } else if (event.data.type === 'logout') {
+ if (event.data.data !== 'here') {
+ return;
+ }
+ worker.port.postMessage({
+ type: 'close',
+ });
+ worker.port.close();
+ window.location.href = `${window.location.origin}${appSubUrl}/`;
+ } else if (event.data.type === 'close') {
+ worker.port.postMessage({
+ type: 'close',
+ });
+ worker.port.close();
+ }
+ });
+ worker.port.addEventListener('error', (e) => {
+ console.error('worker port error', e);
+ });
+ worker.port.start();
+ window.addEventListener('beforeunload', () => {
+ worker.port.postMessage({
+ type: 'close',
+ });
+ worker.port.close();
+ });
+
+ return;
+ }
+
+ startPeriodicPoller(notificationSettings.MinTimeout);
+}
+
+async function updateStopwatchWithCallback(callback, timeout) {
+ const isSet = await updateStopwatch();
+
+ if (!isSet) {
+ timeout = notificationSettings.MinTimeout;
+ } else if (timeout < notificationSettings.MaxTimeout) {
+ timeout += notificationSettings.TimeoutStep;
+ }
+
+ callback(timeout);
+}
+
+async function updateStopwatch() {
+ const response = await GET(`${appSubUrl}/user/stopwatches`);
+ if (!response.ok) {
+ console.error('Failed to fetch stopwatch data');
+ return false;
+ }
+ const data = await response.json();
+ return updateStopwatchData(data);
+}
+
+function updateStopwatchData(data) {
+ const watch = data[0];
+ const btnEl = document.querySelector('.active-stopwatch-trigger');
+ if (!watch) {
+ clearStopwatchTimer();
+ hideElem(btnEl);
+ } else {
+ const {repo_owner_name, repo_name, issue_index, seconds} = watch;
+ const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
+ document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
+ document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/toggle`);
+ document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
+ const stopwatchIssue = document.querySelector('.stopwatch-issue');
+ if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
+ updateStopwatchTime(seconds);
+ showElem(btnEl);
+ }
+ return Boolean(data.length);
+}
+
+let updateTimeIntervalId = null; // holds setInterval id when active
+function clearStopwatchTimer() {
+ if (updateTimeIntervalId !== null) {
+ clearInterval(updateTimeIntervalId);
+ updateTimeIntervalId = null;
+ }
+}
+function updateStopwatchTime(seconds) {
+ const secs = parseInt(seconds);
+ if (!Number.isFinite(secs)) return;
+
+ clearStopwatchTimer();
+ const stopwatch = document.querySelector('.stopwatch-time');
+ // TODO: replace with <relative-time> similar to how system status up time is shown
+ const start = Date.now();
+ const updateUi = () => {
+ const delta = Date.now() - start;
+ const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
+ if (stopwatch) stopwatch.textContent = dur;
+ };
+ updateUi();
+ updateTimeIntervalId = setInterval(updateUi, 1000);
+}
diff --git a/web_src/js/features/tablesort.js b/web_src/js/features/tablesort.js
new file mode 100644
index 0000000..436fe0a
--- /dev/null
+++ b/web_src/js/features/tablesort.js
@@ -0,0 +1,22 @@
+export function initTableSort() {
+ for (const header of document.querySelectorAll('th[data-sortt-asc]') || []) {
+ const sorttAsc = header.getAttribute('data-sortt-asc');
+ const sorttDesc = header.getAttribute('data-sortt-desc');
+ const sorttDefault = header.getAttribute('data-sortt-default');
+ header.addEventListener('click', () => {
+ tableSort(sorttAsc, sorttDesc, sorttDefault);
+ });
+ }
+}
+
+function tableSort(normSort, revSort, isDefault) {
+ if (!normSort) return false;
+ if (!revSort) revSort = '';
+
+ const url = new URL(window.location);
+ let urlSort = url.searchParams.get('sort');
+ if (!urlSort && isDefault) urlSort = normSort;
+
+ url.searchParams.set('sort', urlSort !== normSort ? normSort : revSort);
+ window.location.replace(url.href);
+}
diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js
new file mode 100644
index 0000000..02cd484
--- /dev/null
+++ b/web_src/js/features/tribute.js
@@ -0,0 +1,57 @@
+import {emojiKeys, emojiHTML, emojiString} from './emoji.js';
+import {htmlEscape} from 'escape-goat';
+
+function makeCollections({mentions, emoji}) {
+ const collections = [];
+
+ if (emoji) {
+ collections.push({
+ trigger: ':',
+ requireLeadingSpace: true,
+ values: (query, cb) => {
+ const matches = [];
+ for (const name of emojiKeys) {
+ if (name.includes(query)) {
+ matches.push(name);
+ if (matches.length > 5) break;
+ }
+ }
+ cb(matches);
+ },
+ lookup: (item) => item,
+ selectTemplate: (item) => {
+ if (item === undefined) return null;
+ return emojiString(item.original);
+ },
+ menuItemTemplate: (item) => {
+ return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`;
+ },
+ });
+ }
+
+ if (mentions) {
+ collections.push({
+ values: window.config.mentionValues ?? [],
+ requireLeadingSpace: true,
+ menuItemTemplate: (item) => {
+ return `
+ <div class="tribute-item">
+ <img src="${htmlEscape(item.original.avatar)}" class="tw-mr-2"/>
+ <span class="name">${htmlEscape(item.original.name)}</span>
+ ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
+ </div>
+ `;
+ },
+ });
+ }
+
+ return collections;
+}
+
+export async function attachTribute(element, {mentions, emoji} = {}) {
+ const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
+ const collections = makeCollections({mentions, emoji});
+ const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
+ tribute.attach(element);
+ return tribute;
+}
diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js
new file mode 100644
index 0000000..6dfbb4d
--- /dev/null
+++ b/web_src/js/features/user-auth-webauthn.js
@@ -0,0 +1,194 @@
+import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
+import {showElem} from '../utils/dom.js';
+import {GET, POST} from '../modules/fetch.js';
+
+const {appSubUrl} = window.config;
+
+export async function initUserAuthWebAuthn() {
+ const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
+ if (!elPrompt) {
+ return;
+ }
+
+ if (!detectWebAuthnSupport()) {
+ return;
+ }
+
+ const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
+ if (res.status !== 200) {
+ webAuthnError('unknown');
+ return;
+ }
+ const options = await res.json();
+ options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
+ for (const cred of options.publicKey.allowCredentials) {
+ cred.id = decodeURLEncodedBase64(cred.id);
+ }
+ try {
+ const credential = await navigator.credentials.get({
+ publicKey: options.publicKey,
+ });
+ await verifyAssertion(credential);
+ } catch (err) {
+ if (!options.publicKey.extensions?.appid) {
+ webAuthnError('general', err.message);
+ return;
+ }
+ delete options.publicKey.extensions.appid;
+ try {
+ const credential = await navigator.credentials.get({
+ publicKey: options.publicKey,
+ });
+ await verifyAssertion(credential);
+ } catch (err) {
+ webAuthnError('general', err.message);
+ }
+ }
+}
+
+async function verifyAssertion(assertedCredential) {
+ // Move data into Arrays in case it is super long
+ const authData = new Uint8Array(assertedCredential.response.authenticatorData);
+ const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
+ const rawId = new Uint8Array(assertedCredential.rawId);
+ const sig = new Uint8Array(assertedCredential.response.signature);
+ const userHandle = new Uint8Array(assertedCredential.response.userHandle);
+
+ const res = await POST(`${appSubUrl}/user/webauthn/assertion`, {
+ data: {
+ id: assertedCredential.id,
+ rawId: encodeURLEncodedBase64(rawId),
+ type: assertedCredential.type,
+ clientExtensionResults: assertedCredential.getClientExtensionResults(),
+ response: {
+ authenticatorData: encodeURLEncodedBase64(authData),
+ clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
+ signature: encodeURLEncodedBase64(sig),
+ userHandle: encodeURLEncodedBase64(userHandle),
+ },
+ },
+ });
+ if (res.status === 500) {
+ webAuthnError('unknown');
+ return;
+ } else if (res.status !== 200) {
+ webAuthnError('unable-to-process');
+ return;
+ }
+ const reply = await res.json();
+
+ window.location.href = reply?.redirect ?? `${appSubUrl}/`;
+}
+
+async function webauthnRegistered(newCredential) {
+ const attestationObject = new Uint8Array(newCredential.response.attestationObject);
+ const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
+ const rawId = new Uint8Array(newCredential.rawId);
+
+ const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, {
+ data: {
+ id: newCredential.id,
+ rawId: encodeURLEncodedBase64(rawId),
+ type: newCredential.type,
+ response: {
+ attestationObject: encodeURLEncodedBase64(attestationObject),
+ clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
+ },
+ },
+ });
+
+ if (res.status === 409) {
+ webAuthnError('duplicated');
+ return;
+ } else if (res.status !== 201) {
+ webAuthnError('unknown');
+ return;
+ }
+
+ window.location.reload();
+}
+
+function webAuthnError(errorType, message) {
+ const elErrorMsg = document.getElementById(`webauthn-error-msg`);
+
+ if (errorType === 'general') {
+ elErrorMsg.textContent = message || 'unknown error';
+ } else {
+ const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
+ if (elTypedError) {
+ elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`;
+ } else {
+ elErrorMsg.textContent = `unknown error type: ${errorType}${message ? ` ${message}` : ''}`;
+ }
+ }
+
+ showElem('#webauthn-error');
+}
+
+function detectWebAuthnSupport() {
+ if (!window.isSecureContext) {
+ webAuthnError('insecure');
+ return false;
+ }
+
+ if (typeof window.PublicKeyCredential !== 'function') {
+ webAuthnError('browser');
+ return false;
+ }
+
+ return true;
+}
+
+export function initUserAuthWebAuthnRegister() {
+ const elRegister = document.getElementById('register-webauthn');
+ if (!elRegister) {
+ return;
+ }
+ if (!detectWebAuthnSupport()) {
+ elRegister.disabled = true;
+ return;
+ }
+ elRegister.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await webAuthnRegisterRequest();
+ });
+}
+
+async function webAuthnRegisterRequest() {
+ const elNickname = document.getElementById('nickname');
+
+ const formData = new FormData();
+ formData.append('name', elNickname.value);
+
+ const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
+ data: formData,
+ });
+
+ if (res.status === 409) {
+ webAuthnError('duplicated');
+ return;
+ } else if (res.status !== 200) {
+ webAuthnError('unknown');
+ return;
+ }
+
+ const options = await res.json();
+ elNickname.closest('div.field').classList.remove('error');
+
+ options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
+ options.publicKey.user.id = decodeURLEncodedBase64(options.publicKey.user.id);
+ if (options.publicKey.excludeCredentials) {
+ for (const cred of options.publicKey.excludeCredentials) {
+ cred.id = decodeURLEncodedBase64(cred.id);
+ }
+ }
+
+ try {
+ const credential = await navigator.credentials.create({
+ publicKey: options.publicKey,
+ });
+ await webauthnRegistered(credential);
+ } catch (err) {
+ webAuthnError('unknown', err);
+ }
+}
diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
new file mode 100644
index 0000000..a871ac4
--- /dev/null
+++ b/web_src/js/features/user-auth.js
@@ -0,0 +1,22 @@
+import {checkAppUrl} from './common-global.js';
+
+export function initUserAuthOauth2() {
+ const outer = document.getElementById('oauth2-login-navigator');
+ if (!outer) return;
+ const inner = document.getElementById('oauth2-login-navigator-inner');
+
+ checkAppUrl();
+
+ for (const link of outer.querySelectorAll('.oauth-login-link')) {
+ link.addEventListener('click', () => {
+ inner.classList.add('tw-invisible');
+ outer.classList.add('is-loading');
+ setTimeout(() => {
+ // recover previous content to let user try again
+ // usually redirection will be performed before this action
+ outer.classList.remove('is-loading');
+ inner.classList.remove('tw-invisible');
+ }, 5000);
+ });
+ }
+}
diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js
new file mode 100644
index 0000000..717ef94
--- /dev/null
+++ b/web_src/js/features/user-settings.js
@@ -0,0 +1,63 @@
+import {hideElem, showElem} from '../utils/dom.js';
+
+function onPronounsDropdownUpdate() {
+ const pronounsCustom = document.getElementById('pronouns-custom');
+ const pronounsDropdown = document.getElementById('pronouns-dropdown');
+ const pronounsInput = pronounsDropdown.querySelector('input');
+ // must be kept in sync with `routers/web/user/setting/profile.go`
+ const isCustom = !(
+ pronounsInput.value === '' ||
+ pronounsInput.value === 'he/him' ||
+ pronounsInput.value === 'she/her' ||
+ pronounsInput.value === 'they/them' ||
+ pronounsInput.value === 'it/its' ||
+ pronounsInput.value === 'any pronouns'
+ );
+ if (isCustom) {
+ if (pronounsInput.value === '!') {
+ pronounsCustom.value = '';
+ } else {
+ pronounsCustom.value = pronounsInput.value;
+ }
+ pronounsCustom.style.display = '';
+ } else {
+ pronounsCustom.style.display = 'none';
+ }
+}
+function onPronounsCustomUpdate() {
+ const pronounsCustom = document.getElementById('pronouns-custom');
+ const pronounsInput = document.querySelector('#pronouns-dropdown input');
+ pronounsInput.value = pronounsCustom.value;
+}
+
+export function initUserSettings() {
+ if (!document.querySelectorAll('.user.settings.profile').length) return;
+
+ const usernameInput = document.getElementById('username');
+ if (!usernameInput) return;
+ usernameInput.addEventListener('input', function () {
+ const prompt = document.getElementById('name-change-prompt');
+ const promptRedirect = document.getElementById('name-change-redirect-prompt');
+ if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) {
+ showElem(prompt);
+ showElem(promptRedirect);
+ } else {
+ hideElem(prompt);
+ hideElem(promptRedirect);
+ }
+ });
+
+ const pronounsDropdown = document.getElementById('pronouns-dropdown');
+ const pronounsCustom = document.getElementById('pronouns-custom');
+ const pronounsInput = pronounsDropdown.querySelector('input');
+
+ // If JS is disabled, the page will show the custom input, as the dropdown requires JS to work.
+ // JS progressively enhances the input by adding a dropdown, but it works regardless.
+ pronounsCustom.removeAttribute('name');
+ pronounsInput.setAttribute('name', 'pronouns');
+ pronounsDropdown.style.display = '';
+
+ onPronounsDropdownUpdate();
+ pronounsInput.addEventListener('change', onPronounsDropdownUpdate);
+ pronounsCustom.addEventListener('input', onPronounsCustomUpdate);
+}
diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js
new file mode 100644
index 0000000..5ca3018
--- /dev/null
+++ b/web_src/js/htmx.js
@@ -0,0 +1,21 @@
+import * as htmx from 'htmx.org';
+import {showErrorToast} from './modules/toast.js';
+
+// https://github.com/bigskysoftware/idiomorph#htmx
+import 'idiomorph/dist/idiomorph-ext.js';
+
+// https://htmx.org/reference/#config
+htmx.config.requestClass = 'is-loading';
+htmx.config.scrollIntoViewOnBoost = false;
+
+// https://htmx.org/events/#htmx:sendError
+document.body.addEventListener('htmx:sendError', (event) => {
+ // TODO: add translations
+ showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`);
+});
+
+// https://htmx.org/events/#htmx:responseError
+document.body.addEventListener('htmx:responseError', (event) => {
+ // TODO: add translations
+ showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`);
+});
diff --git a/web_src/js/index.js b/web_src/js/index.js
new file mode 100644
index 0000000..77014a7
--- /dev/null
+++ b/web_src/js/index.js
@@ -0,0 +1,190 @@
+// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors
+import './bootstrap.js';
+
+import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
+import {initScopedAccessTokenCategories} from './components/ScopedAccessTokenSelector.vue';
+import {initDashboardRepoList} from './components/DashboardRepoList.vue';
+
+import {initGlobalCopyToClipboardListener} from './features/clipboard.js';
+import {initContextPopups} from './features/contextpopup.js';
+import {initRepoGraphGit} from './features/repo-graph.js';
+import {initHeatmap} from './features/heatmap.js';
+import {initImageDiff} from './features/imagediff.js';
+import {initRepoMigration} from './features/repo-migration.js';
+import {initRepoProject} from './features/repo-projects.js';
+import {initTableSort} from './features/tablesort.js';
+import {initAutoFocusEnd} from './features/autofocus-end.js';
+import {initAdminUserListSearchForm} from './features/admin/users.js';
+import {initAdminConfigs} from './features/admin/config.js';
+import {initMarkupAnchors} from './markup/anchors.js';
+import {initNotificationCount, initNotificationsTable} from './features/notification.js';
+import {initRepoIssueContentHistory} from './features/repo-issue-content.js';
+import {initStopwatch} from './features/stopwatch.js';
+import {initFindFileInRepo} from './features/repo-findfile.js';
+import {initCommentContent, initMarkupContent} from './markup/content.js';
+import {initPdfViewer} from './render/pdf.js';
+
+import {initUserAuthOauth2} from './features/user-auth.js';
+import {
+ initRepoIssueDue,
+ initRepoIssueReferenceRepositorySearch,
+ initRepoIssueTimeTracking,
+ initRepoIssueWipTitle,
+ initRepoPullRequestAllowMaintainerEdit,
+ initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
+} from './features/repo-issue.js';
+import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js';
+import {
+ initFootLanguageMenu,
+ initGlobalButtonClickOnEnter,
+ initGlobalButtons,
+ initGlobalCommon,
+ initGlobalDropzone,
+ initGlobalEnterQuickSubmit,
+ initGlobalFormDirtyLeaveConfirm,
+ initGlobalLinkActions,
+ initHeadNavbarContentToggle,
+} from './features/common-global.js';
+import {initRepoTopicBar} from './features/repo-home.js';
+import {initAdminEmails} from './features/admin/emails.js';
+import {initAdminCommon} from './features/admin/common.js';
+import {initRepoTemplateSearch} from './features/repo-template.js';
+import {initRepoCodeView} from './features/repo-code.js';
+import {initSshKeyFormParser} from './features/sshkey-helper.js';
+import {initUserSettings} from './features/user-settings.js';
+import {initRepoArchiveLinks} from './features/repo-common.js';
+import {initRepoMigrationStatusChecker} from './features/repo-migrate.js';
+import {
+ initRepoSettingGitHook,
+ initRepoSettingsCollaboration,
+ initRepoSettingSearchTeamBox,
+} from './features/repo-settings.js';
+import {initRepoDiffView} from './features/repo-diff.js';
+import {initOrgTeamSearchRepoBox} from './features/org-team.js';
+import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.js';
+import {initRepoRelease, initRepoReleaseNew} from './features/repo-release.js';
+import {initRepoEditor} from './features/repo-editor.js';
+import {initCompSearchUserBox} from './features/comp/SearchUserBox.js';
+import {initInstall} from './features/install.js';
+import {initCompWebHookEditor} from './features/comp/WebHookEditor.js';
+import {initRepoBranchButton} from './features/repo-branch.js';
+import {initCommonOrganization} from './features/common-organization.js';
+import {initRepoWikiForm} from './features/repo-wiki.js';
+import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
+import {initCopyContent} from './features/copycontent.js';
+import {initCaptcha} from './features/captcha.js';
+import {initRepositoryActionView} from './components/RepoActionView.vue';
+import {initGlobalTooltips} from './modules/tippy.js';
+import {initGiteaFomantic} from './modules/fomantic.js';
+import {onDomReady} from './utils/dom.js';
+import {initRepoIssueList} from './features/repo-issue-list.js';
+import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
+import {initRepoContributors} from './features/contributors.js';
+import {initRepoCodeFrequency} from './features/code-frequency.js';
+import {initRepoRecentCommits} from './features/recent-commits.js';
+import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
+import {initDirAuto} from './modules/dirauto.js';
+import {initRepositorySearch} from './features/repo-search.js';
+import {initColorPickers} from './features/colorpicker.js';
+
+// Init Gitea's Fomantic settings
+initGiteaFomantic();
+initDirAuto();
+
+onDomReady(() => {
+ initGlobalCommon();
+
+ initGlobalTooltips();
+ initGlobalButtonClickOnEnter();
+ initGlobalButtons();
+ initGlobalCopyToClipboardListener();
+ initGlobalDropzone();
+ initGlobalEnterQuickSubmit();
+ initGlobalFormDirtyLeaveConfirm();
+ initGlobalLinkActions();
+
+ initCommonOrganization();
+ initCommonIssueListQuickGoto();
+
+ initCompSearchUserBox();
+ initCompWebHookEditor();
+
+ initInstall();
+
+ initHeadNavbarContentToggle();
+ initFootLanguageMenu();
+
+ initCommentContent();
+ initContextPopups();
+ initHeatmap();
+ initImageDiff();
+ initMarkupAnchors();
+ initMarkupContent();
+ initSshKeyFormParser();
+ initStopwatch();
+ initTableSort();
+ initAutoFocusEnd();
+ initFindFileInRepo();
+ initCopyContent();
+
+ initAdminCommon();
+ initAdminEmails();
+ initAdminUserListSearchForm();
+ initAdminConfigs();
+
+ initDashboardRepoList();
+
+ initNotificationCount();
+ initNotificationsTable();
+
+ initOrgTeamSearchRepoBox();
+
+ initRepoActivityTopAuthorsChart();
+ initRepoArchiveLinks();
+ initRepoBranchButton();
+ initRepoCodeView();
+ initRepoCommentForm();
+ initRepoEllipsisButton();
+ initRepoDiffCommitBranchesAndTags();
+ initRepoEditor();
+ initRepoGraphGit();
+ initRepoIssueContentHistory();
+ initRepoIssueDue();
+ initRepoIssueList();
+ initRepoIssueSidebarList();
+ initArchivedLabelHandler();
+ initRepoIssueReferenceRepositorySearch();
+ initRepoIssueTimeTracking();
+ initRepoIssueWipTitle();
+ initRepoMigration();
+ initRepoMigrationStatusChecker();
+ initRepoProject();
+ initRepoPullRequestAllowMaintainerEdit();
+ initRepoPullRequestReview();
+ initRepoRelease();
+ initRepoReleaseNew();
+ initRepoSettingGitHook();
+ initRepoSettingSearchTeamBox();
+ initRepoSettingsCollaboration();
+ initRepoTemplateSearch();
+ initRepoTopicBar();
+ initRepoWikiForm();
+ initRepository();
+ initRepositoryActionView();
+ initRepositorySearch();
+ initRepoContributors();
+ initRepoCodeFrequency();
+ initRepoRecentCommits();
+
+ initCommitStatuses();
+ initCaptcha();
+
+ initUserAuthOauth2();
+ initUserAuthWebAuthn();
+ initUserAuthWebAuthnRegister();
+ initUserSettings();
+ initRepoDiffView();
+ initPdfViewer();
+ initScopedAccessTokenCategories();
+ initColorPickers();
+});
diff --git a/web_src/js/jquery.js b/web_src/js/jquery.js
new file mode 100644
index 0000000..6b21998
--- /dev/null
+++ b/web_src/js/jquery.js
@@ -0,0 +1,3 @@
+import $ from 'jquery';
+
+window.$ = window.jQuery = $; // eslint-disable-line no-jquery/variable-pattern
diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
new file mode 100644
index 0000000..0e2c927
--- /dev/null
+++ b/web_src/js/markup/anchors.js
@@ -0,0 +1,70 @@
+import {svg} from '../svg.js';
+
+const addPrefix = (str) => `user-content-${str}`;
+const removePrefix = (str) => str.replace(/^user-content-/, '');
+const hasPrefix = (str) => str.startsWith('user-content-');
+
+// scroll to anchor while respecting the `user-content` prefix that exists on the target
+function scrollToAnchor(encodedId) {
+ if (!encodedId) return;
+ const id = decodeURIComponent(encodedId);
+ const prefixedId = addPrefix(id);
+ let el = document.getElementById(prefixedId);
+
+ // check for matching user-generated `a[name]`
+ if (!el) {
+ const nameAnchors = document.getElementsByName(prefixedId);
+ if (nameAnchors.length) {
+ el = nameAnchors[0];
+ }
+ }
+
+ // compat for links with old 'user-content-' prefixed hashes
+ if (!el && hasPrefix(id)) {
+ return document.getElementById(id)?.scrollIntoView();
+ }
+
+ el?.scrollIntoView();
+}
+
+export function initMarkupAnchors() {
+ const markupEls = document.querySelectorAll('.markup');
+ if (!markupEls.length) return;
+
+ for (const markupEl of markupEls) {
+ // create link icons for markup headings, the resulting link href will remove `user-content-`
+ for (const heading of markupEl.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
+ const a = document.createElement('a');
+ a.classList.add('anchor');
+ a.setAttribute('href', `#${encodeURIComponent(removePrefix(heading.id))}`);
+ a.innerHTML = svg('octicon-link');
+ heading.prepend(a);
+ }
+
+ // remove `user-content-` prefix from links so they don't show in url bar when clicked
+ for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
+ const href = a.getAttribute('href');
+ if (!href.startsWith('#user-content-')) continue;
+ a.setAttribute('href', `#${removePrefix(href.substring(1))}`);
+ }
+
+ // add `user-content-` prefix to user-generated `a[name]` link targets
+ // TODO: this prefix should be added in backend instead
+ for (const a of markupEl.querySelectorAll('a[name]')) {
+ const name = a.getAttribute('name');
+ if (!name) continue;
+ a.setAttribute('name', addPrefix(a.name));
+ }
+
+ for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
+ a.addEventListener('click', (e) => {
+ scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1));
+ });
+ }
+ }
+
+ // scroll to anchor unless the browser has already scrolled somewhere during page load
+ if (!document.querySelector(':target')) {
+ scrollToAnchor(window.location.hash?.substring(1));
+ }
+}
diff --git a/web_src/js/markup/asciicast.js b/web_src/js/markup/asciicast.js
new file mode 100644
index 0000000..97b1874
--- /dev/null
+++ b/web_src/js/markup/asciicast.js
@@ -0,0 +1,17 @@
+export async function renderAsciicast() {
+ const els = document.querySelectorAll('.asciinema-player-container');
+ if (!els.length) return;
+
+ const [player] = await Promise.all([
+ import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
+ import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'),
+ ]);
+
+ for (const el of els) {
+ player.create(el.getAttribute('data-asciinema-player-src'), el, {
+ // poster (a preview frame) to display until the playback is started.
+ // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
+ poster: 'npt:1:0:0',
+ });
+ }
+}
diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
new file mode 100644
index 0000000..078d741
--- /dev/null
+++ b/web_src/js/markup/codecopy.js
@@ -0,0 +1,21 @@
+import {svg} from '../svg.js';
+
+export function makeCodeCopyButton() {
+ const button = document.createElement('button');
+ button.classList.add('code-copy', 'ui', 'button');
+ button.innerHTML = svg('octicon-copy');
+ return button;
+}
+
+export function renderCodeCopy() {
+ const els = document.querySelectorAll('.markup .code-block code');
+ if (!els.length) return;
+
+ for (const el of els) {
+ if (!el.textContent) continue;
+ const btn = makeCodeCopyButton();
+ // remove final trailing newline introduced during HTML rendering
+ btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
+ el.after(btn);
+ }
+}
diff --git a/web_src/js/markup/common.js b/web_src/js/markup/common.js
new file mode 100644
index 0000000..aff4a32
--- /dev/null
+++ b/web_src/js/markup/common.js
@@ -0,0 +1,8 @@
+export function displayError(el, err) {
+ el.classList.remove('is-loading');
+ const errorNode = document.createElement('pre');
+ errorNode.setAttribute('class', 'ui message error markup-block-error');
+ errorNode.textContent = err.str || err.message || String(err);
+ el.before(errorNode);
+ el.setAttribute('data-render-done', 'true');
+}
diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js
new file mode 100644
index 0000000..1d29dc0
--- /dev/null
+++ b/web_src/js/markup/content.js
@@ -0,0 +1,18 @@
+import {renderMermaid} from './mermaid.js';
+import {renderMath} from './math.js';
+import {renderCodeCopy} from './codecopy.js';
+import {renderAsciicast} from './asciicast.js';
+import {initMarkupTasklist} from './tasklist.js';
+
+// code that runs for all markup content
+export function initMarkupContent() {
+ renderMermaid();
+ renderMath();
+ renderCodeCopy();
+ renderAsciicast();
+}
+
+// code that only runs for comments
+export function initCommentContent() {
+ initMarkupTasklist();
+}
diff --git a/web_src/js/markup/math.js b/web_src/js/markup/math.js
new file mode 100644
index 0000000..872e50a
--- /dev/null
+++ b/web_src/js/markup/math.js
@@ -0,0 +1,47 @@
+import {displayError} from './common.js';
+
+function targetElement(el) {
+ // The target element is either the current element if it has the
+ // `is-loading` class or the pre that contains it
+ return el.classList.contains('is-loading') ? el : el.closest('pre');
+}
+
+export async function renderMath() {
+ const els = document.querySelectorAll('.markup code.language-math');
+ if (!els.length) return;
+
+ const [{default: katex}] = await Promise.all([
+ import(/* webpackChunkName: "katex" */'katex'),
+ import(/* webpackChunkName: "katex" */'katex/dist/katex.css'),
+ ]);
+
+ const MAX_CHARS = 1000;
+ const MAX_SIZE = 25;
+ const MAX_EXPAND = 1000;
+
+ for (const el of els) {
+ const target = targetElement(el);
+ if (target.hasAttribute('data-render-done')) continue;
+ const source = el.textContent;
+
+ if (source.length > MAX_CHARS) {
+ displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`));
+ continue;
+ }
+
+ const displayMode = el.classList.contains('display');
+ const nodeName = displayMode ? 'p' : 'span';
+
+ try {
+ const tempEl = document.createElement(nodeName);
+ katex.render(source, tempEl, {
+ maxSize: MAX_SIZE,
+ maxExpand: MAX_EXPAND,
+ displayMode,
+ });
+ target.replaceWith(tempEl);
+ } catch (error) {
+ displayError(target, error);
+ }
+ }
+}
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
new file mode 100644
index 0000000..0549fb3
--- /dev/null
+++ b/web_src/js/markup/mermaid.js
@@ -0,0 +1,74 @@
+import {isDarkTheme} from '../utils.js';
+import {makeCodeCopyButton} from './codecopy.js';
+import {displayError} from './common.js';
+
+const {mermaidMaxSourceCharacters} = window.config;
+
+// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
+const iframeCss = `:root {color-scheme: normal}
+body {margin: 0; padding: 0; overflow: hidden}
+#mermaid {display: block; margin: 0 auto}
+blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
+
+export async function renderMermaid() {
+ const els = document.querySelectorAll('.markup code.language-mermaid');
+ if (!els.length) return;
+
+ const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
+
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: isDarkTheme() ? 'dark' : 'neutral',
+ securityLevel: 'strict',
+ });
+
+ for (const el of els) {
+ const pre = el.closest('pre');
+ if (pre.hasAttribute('data-render-done')) continue;
+
+ const source = el.textContent;
+ if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
+ displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
+ continue;
+ }
+
+ try {
+ await mermaid.parse(source);
+ } catch (err) {
+ displayError(pre, err);
+ continue;
+ }
+
+ try {
+ // can't use bindFunctions here because we can't cross the iframe boundary. This
+ // means js-based interactions won't work but they aren't intended to work either
+ const {svg} = await mermaid.render('mermaid', source);
+
+ const iframe = document.createElement('iframe');
+ iframe.classList.add('markup-render', 'tw-invisible');
+ iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
+
+ const mermaidBlock = document.createElement('div');
+ mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
+ mermaidBlock.append(iframe);
+
+ const btn = makeCodeCopyButton();
+ btn.setAttribute('data-clipboard-text', source);
+ mermaidBlock.append(btn);
+
+ iframe.addEventListener('load', () => {
+ pre.replaceWith(mermaidBlock);
+ mermaidBlock.classList.remove('tw-hidden');
+ iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
+ setTimeout(() => { // avoid flash of iframe background
+ mermaidBlock.classList.remove('is-loading');
+ iframe.classList.remove('tw-invisible');
+ }, 0);
+ });
+
+ document.body.append(mermaidBlock);
+ } catch (err) {
+ displayError(pre, err);
+ }
+ }
+}
diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js
new file mode 100644
index 0000000..375810d
--- /dev/null
+++ b/web_src/js/markup/tasklist.js
@@ -0,0 +1,90 @@
+import {POST} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
+
+const preventListener = (e) => e.preventDefault();
+
+/**
+ * Attaches `input` handlers to markdown rendered tasklist checkboxes in comments.
+ *
+ * When a checkbox value changes, the corresponding [ ] or [x] in the markdown string
+ * is set accordingly and sent to the server. On success it updates the raw-content on
+ * error it resets the checkbox to its original value.
+ */
+export function initMarkupTasklist() {
+ for (const el of document.querySelectorAll(`.markup[data-can-edit=true]`) || []) {
+ const container = el.parentNode;
+ const checkboxes = el.querySelectorAll(`.task-list-item input[type=checkbox]`);
+
+ for (const checkbox of checkboxes) {
+ if (checkbox.hasAttribute('data-editable')) {
+ return;
+ }
+
+ checkbox.setAttribute('data-editable', 'true');
+ checkbox.addEventListener('input', async () => {
+ const checkboxCharacter = checkbox.checked ? 'x' : ' ';
+ const position = parseInt(checkbox.getAttribute('data-source-position')) + 1;
+
+ const rawContent = container.querySelector('.raw-content');
+ const oldContent = rawContent.textContent;
+
+ const encoder = new TextEncoder();
+ const buffer = encoder.encode(oldContent);
+ // Indexes may fall off the ends and return undefined.
+ if (buffer[position - 1] !== '['.codePointAt(0) ||
+ buffer[position] !== ' '.codePointAt(0) && buffer[position] !== 'x'.codePointAt(0) && buffer[position] !== 'X'.codePointAt(0) ||
+ buffer[position + 1] !== ']'.codePointAt(0)) {
+ // Position is probably wrong. Revert and don't allow change.
+ checkbox.checked = !checkbox.checked;
+ throw new Error(`Expected position to be space, x or X and surrounded by brackets, but it's not: position=${position}`);
+ }
+ buffer.set(encoder.encode(checkboxCharacter), position);
+ const newContent = new TextDecoder().decode(buffer);
+
+ if (newContent === oldContent) {
+ return;
+ }
+
+ // Prevent further inputs until the request is done. This does not use the
+ // `disabled` attribute because it causes the border to flash on click.
+ for (const checkbox of checkboxes) {
+ checkbox.addEventListener('click', preventListener);
+ }
+
+ try {
+ const editContentZone = container.querySelector('.edit-content-zone');
+ const updateUrl = editContentZone.getAttribute('data-update-url');
+ const context = editContentZone.getAttribute('data-context');
+ const contentVersion = editContentZone.getAttribute('data-content-version');
+
+ const requestBody = new FormData();
+ requestBody.append('ignore_attachments', 'true');
+ requestBody.append('content', newContent);
+ requestBody.append('context', context);
+ requestBody.append('content_version', contentVersion);
+ const response = await POST(updateUrl, {data: requestBody});
+ const data = await response.json();
+ if (response.status === 400) {
+ showErrorToast(data.errorMessage);
+ return;
+ }
+ editContentZone.setAttribute('data-content-version', data.contentVersion);
+ rawContent.textContent = newContent;
+ } catch (err) {
+ checkbox.checked = !checkbox.checked;
+ console.error(err);
+ }
+
+ // Enable input on checkboxes again
+ for (const checkbox of checkboxes) {
+ checkbox.removeEventListener('click', preventListener);
+ }
+ });
+ }
+
+ // Enable the checkboxes as they are initially disabled by the markdown renderer
+ for (const checkbox of checkboxes) {
+ checkbox.disabled = false;
+ }
+ }
+}
diff --git a/web_src/js/modules/dirauto.js b/web_src/js/modules/dirauto.js
new file mode 100644
index 0000000..cd90f81
--- /dev/null
+++ b/web_src/js/modules/dirauto.js
@@ -0,0 +1,40 @@
+import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+
+// for performance considerations, it only uses performant syntax
+function attachDirAuto(el) {
+ if (el.type !== 'hidden' &&
+ el.type !== 'checkbox' &&
+ el.type !== 'radio' &&
+ el.type !== 'range' &&
+ el.type !== 'color') {
+ el.dir = 'auto';
+ }
+}
+
+export function initDirAuto() {
+ const observer = new MutationObserver((mutationList) => {
+ const len = mutationList.length;
+ for (let i = 0; i < len; i++) {
+ const mutation = mutationList[i];
+ const len = mutation.addedNodes.length;
+ for (let i = 0; i < len; i++) {
+ const addedNode = mutation.addedNodes[i];
+ if (!isDocumentFragmentOrElementNode(addedNode)) continue;
+ if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode);
+ const children = addedNode.querySelectorAll('input, textarea');
+ const len = children.length;
+ for (let childIdx = 0; childIdx < len; childIdx++) {
+ attachDirAuto(children[childIdx]);
+ }
+ }
+ }
+ });
+
+ const docNodes = document.querySelectorAll('input, textarea');
+ const len = docNodes.length;
+ for (let i = 0; i < len; i++) {
+ attachDirAuto(docNodes[i]);
+ }
+
+ observer.observe(document, {subtree: true, childList: true});
+}
diff --git a/web_src/js/modules/fetch.js b/web_src/js/modules/fetch.js
new file mode 100644
index 0000000..2191a8d
--- /dev/null
+++ b/web_src/js/modules/fetch.js
@@ -0,0 +1,41 @@
+import {isObject} from '../utils.js';
+
+const {csrfToken} = window.config;
+
+// safe HTTP methods that don't need a csrf token
+const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
+
+// fetch wrapper, use below method name functions and the `data` option to pass in data
+// which will automatically set an appropriate headers. For json content, only object
+// and array types are currently supported.
+export function request(url, {method = 'GET', data, headers = {}, ...other} = {}) {
+ let body, contentType;
+ if (data instanceof FormData || data instanceof URLSearchParams) {
+ body = data;
+ } else if (isObject(data) || Array.isArray(data)) {
+ contentType = 'application/json';
+ body = JSON.stringify(data);
+ }
+
+ const headersMerged = new Headers({
+ ...(!safeMethods.has(method) && {'x-csrf-token': csrfToken}),
+ ...(contentType && {'content-type': contentType}),
+ });
+
+ for (const [name, value] of Object.entries(headers)) {
+ headersMerged.set(name, value);
+ }
+
+ return fetch(url, {
+ method,
+ headers: headersMerged,
+ ...other,
+ ...(body && {body}),
+ });
+}
+
+export const GET = (url, opts) => request(url, {method: 'GET', ...opts});
+export const POST = (url, opts) => request(url, {method: 'POST', ...opts});
+export const PATCH = (url, opts) => request(url, {method: 'PATCH', ...opts});
+export const PUT = (url, opts) => request(url, {method: 'PUT', ...opts});
+export const DELETE = (url, opts) => request(url, {method: 'DELETE', ...opts});
diff --git a/web_src/js/modules/fetch.test.js b/web_src/js/modules/fetch.test.js
new file mode 100644
index 0000000..e4bec3c
--- /dev/null
+++ b/web_src/js/modules/fetch.test.js
@@ -0,0 +1,10 @@
+import {GET, POST, PATCH, PUT, DELETE} from './fetch.js';
+
+// tests here are only to satisfy the linter for unused functions
+test('exports', () => {
+ expect(GET).toBeTruthy();
+ expect(POST).toBeTruthy();
+ expect(PATCH).toBeTruthy();
+ expect(PUT).toBeTruthy();
+ expect(DELETE).toBeTruthy();
+});
diff --git a/web_src/js/modules/fomantic.js b/web_src/js/modules/fomantic.js
new file mode 100644
index 0000000..c04bc6e
--- /dev/null
+++ b/web_src/js/modules/fomantic.js
@@ -0,0 +1,34 @@
+import $ from 'jquery';
+import {initFomanticApiPatch} from './fomantic/api.js';
+import {initAriaCheckboxPatch} from './fomantic/checkbox.js';
+import {initAriaFormFieldPatch} from './fomantic/form.js';
+import {initAriaDropdownPatch} from './fomantic/dropdown.js';
+import {initAriaModalPatch} from './fomantic/modal.js';
+import {initFomanticTransition} from './fomantic/transition.js';
+import {svg} from '../svg.js';
+
+export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
+
+export function initGiteaFomantic() {
+ // Silence fomantic's error logging when tabs are used without a target content element
+ $.fn.tab.settings.silent = true;
+
+ // By default, use "exact match" for full text search
+ $.fn.dropdown.settings.fullTextSearch = 'exact';
+ // Do not use "cursor: pointer" for dropdown labels
+ $.fn.dropdown.settings.className.label += ' tw-cursor-default';
+ // Always use Gitea's SVG icons
+ $.fn.dropdown.settings.templates.label = function(_value, text, preserveHTML, className) {
+ const escape = $.fn.dropdown.settings.templates.escape;
+ return escape(text, preserveHTML) + svg('octicon-x', 16, `${className.delete} icon`);
+ };
+
+ initFomanticTransition();
+ initFomanticApiPatch();
+
+ // Use the patches to improve accessibility, these patches are designed to be as independent as possible, make it easy to modify or remove in the future.
+ initAriaCheckboxPatch();
+ initAriaFormFieldPatch();
+ initAriaDropdownPatch();
+ initAriaModalPatch();
+}
diff --git a/web_src/js/modules/fomantic/api.js b/web_src/js/modules/fomantic/api.js
new file mode 100644
index 0000000..ca212c9
--- /dev/null
+++ b/web_src/js/modules/fomantic/api.js
@@ -0,0 +1,40 @@
+import $ from 'jquery';
+
+export function initFomanticApiPatch() {
+ //
+ // Fomantic API module has some very buggy behaviors:
+ //
+ // If encodeParameters=true, it calls `urlEncodedValue` to encode the parameter.
+ // However, `urlEncodedValue` just tries to "guess" whether the parameter is already encoded, by decoding the parameter and encoding it again.
+ //
+ // There are 2 problems:
+ // 1. It may guess wrong, and skip encoding a parameter which looks like encoded.
+ // 2. If the parameter can't be decoded, `decodeURIComponent` will throw an error, and the whole request will fail.
+ //
+ // This patch only fixes the second error behavior at the moment.
+ //
+ const patchKey = '_giteaFomanticApiPatch';
+ const oldApi = $.api;
+ $.api = $.fn.api = function(...args) {
+ const apiCall = oldApi.bind(this);
+ const ret = oldApi.apply(this, args);
+
+ if (typeof args[0] !== 'string') {
+ const internalGet = apiCall('internal', 'get');
+ if (!internalGet.urlEncodedValue[patchKey]) {
+ const oldUrlEncodedValue = internalGet.urlEncodedValue;
+ internalGet.urlEncodedValue = function (value) {
+ try {
+ return oldUrlEncodedValue(value);
+ } catch {
+ // if Fomantic API module's `urlEncodedValue` throws an error, we encode it by ourselves.
+ return encodeURIComponent(value);
+ }
+ };
+ internalGet.urlEncodedValue[patchKey] = true;
+ }
+ }
+ return ret;
+ };
+ $.api.settings = oldApi.settings;
+}
diff --git a/web_src/js/modules/fomantic/aria.md b/web_src/js/modules/fomantic/aria.md
new file mode 100644
index 0000000..5836a34
--- /dev/null
+++ b/web_src/js/modules/fomantic/aria.md
@@ -0,0 +1,117 @@
+# Background
+
+This document is used as aria/accessibility(a11y) reference for future developers.
+
+There are a lot of a11y problems in the Fomantic UI library. Files in
+`web_src/js/modules/fomantic/` are used as a workaround to make the UI more accessible.
+
+The aria-related code is designed to avoid touching the official Fomantic UI library,
+and to be as independent as possible, so it can be easily modified/removed in the future.
+
+To test the aria/accessibility with screen readers, developers can use the following steps:
+
+* On macOS, you can use VoiceOver.
+ * Press `Command + F5` to turn on VoiceOver.
+ * Try to operate the UI with keyboard-only.
+ * Use Tab/Shift+Tab to switch focus between elements.
+ * Arrow keys (Option+Up/Down) to navigate between menu/combobox items (only aria-active, not really focused).
+ * Press Enter to trigger the aria-active element.
+* On Android, you can use TalkBack.
+ * Go to Settings -> Accessibility -> TalkBack, turn it on.
+ * Long-press or press+swipe to switch the aria-active element (not really focused).
+ * Double-tap means old single-tap on the aria-active element.
+ * Double-finger swipe means old single-finger swipe.
+* TODO: on Windows, on Linux, on iOS
+
+# Known Problems
+
+* Tested with Apple VoiceOver: If a dropdown menu/combobox is opened by mouse click, then arrow keys don't work.
+ But if the dropdown is opened by keyboard Tab, then arrow keys work, and from then on, the keys almost work with mouse click too.
+ The clue: when the dropdown is only opened by mouse click, VoiceOver doesn't send 'keydown' events of arrow keys to the DOM,
+ VoiceOver expects to use arrow keys to navigate between some elements, but it couldn't.
+ Users could use Option+ArrowKeys to navigate between menu/combobox items or selection labels if the menu/combobox is opened by mouse click.
+
+# Checkbox
+
+## Accessibility-friendly Checkbox
+
+The ideal checkboxes should be:
+
+```html
+<label><input type="checkbox"> ... </label>
+```
+
+However, the templates still have the Fomantic-style HTML layout:
+
+```html
+<div class="ui checkbox">
+ <input type="checkbox">
+ <label>...</label>
+</div>
+```
+
+We call `initAriaCheckboxPatch` to link the `input` and `label` which makes clicking the
+label etc. work. There is still a problem: These checkboxes are not friendly to screen readers,
+so we add IDs to all the Fomantic UI checkboxes automatically by JS. If the `label` part is empty,
+then the checkbox needs to get the `aria-label` attribute manually.
+
+# Fomantic Dropdown
+
+Fomantic Dropdown is designed to be used for many purposes:
+
+* Menu (the profile menu in navbar, the language menu in footer)
+* Popup (the branch/tag panel, the review box)
+* Simple `<select>` , used in many forms
+* Searchable option-list with static items (used in many forms)
+* Searchable option-list with dynamic items (ajax)
+* Searchable multiple selection option-list with dynamic items: the repo topic setting
+* More complex usages, like the Issue Label selector
+
+Fomantic Dropdown requires that the focus must be on its primary element.
+If the focus changes, it hides or panics.
+
+At the moment, the aria-related code only tries to partially resolve the a11y problems for dropdowns with items.
+
+There are different solutions:
+
+* combobox + listbox + option:
+ * https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
+ * A combobox is an input widget with an associated popup that enables users to select a value for the combobox from
+ a collection of possible values. In some implementations, the popup presents allowed values, while in other implementations,
+ the popup presents suggested values, and users may either select one of the suggestions or type a value.
+* menu + menuitem:
+ * https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
+ * A menu is a widget that offers a list of choices to the user, such as a set of actions or functions.
+
+The current approach is: detect if the dropdown has an input,
+if yes, it works like a combobox, otherwise it works like a menu.
+Multiple selection dropdown is not well-supported yet, it needs more work.
+
+Some important pages for dropdown testing:
+
+* Home(dashboard) page, the "Create Repo" / "Profile" / "Language" menu.
+* Create New Repo page, a lot of dropdowns as combobox.
+* Collaborators page, the "permission" dropdown (the old behavior was not quite good, it just works).
+
+```html
+<!-- read-only dropdown -->
+<div class="ui dropdown"> <!-- focused here, then it's not perfect to use aria-activedescendant to point to the menu item -->
+ <input type="hidden" ...>
+ <div class="text">Default</div>
+ <div class="menu" tabindex="-1"> <!-- "transition hidden|visible" classes will be added by $.dropdown() and when the dropdown is working -->
+ <div class="item active selected">Default</div>
+ <div class="item">...</div>
+ </div>
+</div>
+
+<!-- search input dropdown -->
+<div class="ui dropdown">
+ <input type="hidden" ...>
+ <input class="search" autocomplete="off" tabindex="0"> <!-- focused here -->
+ <div class="text"></div>
+ <div class="menu" tabindex="-1"> <!-- "transition hidden|visible" classes will be added by $.dropdown() and when the dropdown is working -->
+ <div class="item selected">...</div>
+ <div class="item">...</div>
+ </div>
+</div>
+```
diff --git a/web_src/js/modules/fomantic/base.js b/web_src/js/modules/fomantic/base.js
new file mode 100644
index 0000000..7574fdd
--- /dev/null
+++ b/web_src/js/modules/fomantic/base.js
@@ -0,0 +1,18 @@
+let ariaIdCounter = 0;
+
+export function generateAriaId() {
+ return `_aria_auto_id_${ariaIdCounter++}`;
+}
+
+export function linkLabelAndInput(label, input) {
+ const labelFor = label.getAttribute('for');
+ const inputId = input.getAttribute('id');
+
+ if (inputId && !labelFor) { // missing "for"
+ label.setAttribute('for', inputId);
+ } else if (!inputId && !labelFor) { // missing both "id" and "for"
+ const id = generateAriaId();
+ input.setAttribute('id', id);
+ label.setAttribute('for', id);
+ }
+}
diff --git a/web_src/js/modules/fomantic/checkbox.js b/web_src/js/modules/fomantic/checkbox.js
new file mode 100644
index 0000000..ed77406
--- /dev/null
+++ b/web_src/js/modules/fomantic/checkbox.js
@@ -0,0 +1,13 @@
+import {linkLabelAndInput} from './base.js';
+
+export function initAriaCheckboxPatch() {
+ // link the label and the input element so it's clickable and accessible
+ for (const el of document.querySelectorAll('.ui.checkbox')) {
+ if (el.hasAttribute('data-checkbox-patched')) continue;
+ const label = el.querySelector('label');
+ const input = el.querySelector('input');
+ if (!label || !input) continue;
+ linkLabelAndInput(label, input);
+ el.setAttribute('data-checkbox-patched', 'true');
+ }
+}
diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
new file mode 100644
index 0000000..82e7108
--- /dev/null
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -0,0 +1,256 @@
+import $ from 'jquery';
+import {generateAriaId} from './base.js';
+
+const ariaPatchKey = '_giteaAriaPatchDropdown';
+const fomanticDropdownFn = $.fn.dropdown;
+
+// use our own `$().dropdown` function to patch Fomantic's dropdown module
+export function initAriaDropdownPatch() {
+ if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
+ $.fn.dropdown = ariaDropdownFn;
+ ariaDropdownFn.settings = fomanticDropdownFn.settings;
+}
+
+// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
+// * it does the one-time attaching on the first call
+// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
+function ariaDropdownFn(...args) {
+ const ret = fomanticDropdownFn.apply(this, args);
+
+ // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
+ // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
+ const needDelegate = (!args.length || typeof args[0] !== 'string');
+ for (const el of this) {
+ if (!el[ariaPatchKey]) {
+ attachInit(el);
+ }
+ if (needDelegate) {
+ delegateOne($(el));
+ }
+ }
+ return ret;
+}
+
+// make the item has role=option/menuitem, add an id if there wasn't one yet, make items as non-focusable
+// the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element.
+function updateMenuItem(dropdown, item) {
+ if (!item.id) item.id = generateAriaId();
+ item.setAttribute('role', dropdown[ariaPatchKey].listItemRole);
+ item.setAttribute('tabindex', '-1');
+ for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1');
+}
+/**
+ * make the label item and its "delete icon" have correct aria attributes
+ * @param {HTMLElement} label
+ */
+function updateSelectionLabel(label) {
+ // the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>"
+ if (!label.id) {
+ label.id = generateAriaId();
+ }
+ label.tabIndex = -1;
+
+ const deleteIcon = label.querySelector('.delete.icon');
+ if (deleteIcon) {
+ deleteIcon.setAttribute('aria-hidden', 'false');
+ deleteIcon.setAttribute('aria-label', window.config.i18n.remove_label_str.replace('%s', label.getAttribute('data-value')));
+ deleteIcon.setAttribute('role', 'button');
+ }
+}
+
+// delegate the dropdown's template functions and callback functions to add aria attributes.
+function delegateOne($dropdown) {
+ const dropdownCall = fomanticDropdownFn.bind($dropdown);
+
+ // If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked.
+ // Actually, Fomantic UI doesn't support such layout/usage. It needs to patch the "focusSearch" / "blurSearch" functions to make sure it toggles the menu.
+ const oldFocusSearch = dropdownCall('internal', 'focusSearch');
+ const oldBlurSearch = dropdownCall('internal', 'blurSearch');
+ // * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu
+ dropdownCall('internal', 'focusSearch', function () { dropdownCall('show'); oldFocusSearch.call(this) });
+ // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu
+ dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') });
+
+ // the "template" functions are used for dynamic creation (eg: AJAX)
+ const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()};
+ const dropdownTemplatesMenuOld = dropdownTemplates.menu;
+ dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
+ // when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
+ const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
+ const div = document.createElement('div');
+ div.innerHTML = menuItems;
+ const $wrapper = $(div);
+ const $items = $wrapper.find('> .item');
+ $items.each((_, item) => updateMenuItem($dropdown[0], item));
+ $dropdown[0][ariaPatchKey].deferredRefreshAriaActiveItem();
+ return $wrapper.html();
+ };
+ dropdownCall('setting', 'templates', dropdownTemplates);
+
+ // the `onLabelCreate` is used to add necessary aria attributes for dynamically created selection labels
+ const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate');
+ dropdownCall('setting', 'onLabelCreate', function(value, text) {
+ const $label = dropdownOnLabelCreateOld.call(this, value, text);
+ updateSelectionLabel($label[0]);
+ return $label;
+ });
+}
+
+// for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes
+function attachStaticElements(dropdown, focusable, menu) {
+ // prepare static dropdown menu list popup
+ if (!menu.id) {
+ menu.id = generateAriaId();
+ }
+
+ $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item));
+
+ // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash
+ menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole);
+
+ // prepare selection label items
+ for (const label of dropdown.querySelectorAll('.ui.label')) {
+ updateSelectionLabel(label);
+ }
+
+ // make the primary element (focusable) aria-friendly
+ focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole);
+ focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole);
+ focusable.setAttribute('aria-controls', menu.id);
+ focusable.setAttribute('aria-expanded', 'false');
+
+ // use tooltip's content as aria-label if there is no aria-label
+ const tooltipContent = dropdown.getAttribute('data-tooltip-content');
+ if (tooltipContent && !dropdown.getAttribute('aria-label')) {
+ dropdown.setAttribute('aria-label', tooltipContent);
+ }
+}
+
+function attachInit(dropdown) {
+ dropdown[ariaPatchKey] = {};
+ if (dropdown.classList.contains('custom')) return;
+
+ // Dropdown has 2 different focusing behaviors
+ // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
+ // * without search input (but the readonly text), the dropdown itself is focused. then the aria-activedescendant points to the element inside dropdown
+ // Some desktop screen readers may change the focus, but dropdown requires that the focus must be on its primary element, then they don't work well.
+
+ // Expected user interactions for dropdown with aria support:
+ // * user can use Tab to focus in the dropdown, then the dropdown menu (list) will be shown
+ // * user presses Tab on the focused dropdown to move focus to next sibling focusable element (but not the menu item)
+ // * user can use arrow key Up/Down to navigate between menu items
+ // * when user presses Enter:
+ // - if the menu item is clickable (eg: <a>), then trigger the click event
+ // - otherwise, the dropdown control (low-level code) handles the Enter event, hides the dropdown menu
+
+ // TODO: multiple selection is only partially supported. Check and test them one by one in the future.
+
+ const textSearch = dropdown.querySelector('input.search');
+ const focusable = textSearch || dropdown; // the primary element for focus, see comment above
+ if (!focusable) return;
+
+ // as a combobox, the input should not have autocomplete by default
+ if (textSearch && !textSearch.getAttribute('autocomplete')) {
+ textSearch.setAttribute('autocomplete', 'off');
+ }
+
+ let menu = $(dropdown).find('> .menu')[0];
+ if (!menu) {
+ // some "multiple selection" dropdowns don't have a static menu element in HTML, we need to pre-create it to make it have correct aria attributes
+ menu = document.createElement('div');
+ menu.classList.add('menu');
+ dropdown.append(menu);
+ }
+
+ // There are 2 possible solutions about the role: combobox or menu.
+ // The idea is that if there is an input, then it's a combobox, otherwise it's a menu.
+ // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before.
+ const isComboBox = dropdown.querySelectorAll('input').length > 0;
+
+ dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu';
+ dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : '';
+ dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem';
+
+ attachDomEvents(dropdown, focusable, menu);
+ attachStaticElements(dropdown, focusable, menu);
+}
+
+function attachDomEvents(dropdown, focusable, menu) {
+ // when showing, it has class: ".animating.in"
+ // when hiding, it has class: ".visible.animating.out"
+ const isMenuVisible = () => (menu.classList.contains('visible') && !menu.classList.contains('out')) || menu.classList.contains('in');
+
+ // update aria attributes according to current active/selected item
+ const refreshAriaActiveItem = () => {
+ const menuVisible = isMenuVisible();
+ focusable.setAttribute('aria-expanded', menuVisible ? 'true' : 'false');
+
+ // if there is an active item, use it (the user is navigating between items)
+ // otherwise use the "selected" for combobox (for the last selected item)
+ const active = $(menu).find('> .item.active, > .item.selected')[0];
+ if (!active) return;
+ // if the popup is visible and has an active/selected item, use its id as aria-activedescendant
+ if (menuVisible) {
+ focusable.setAttribute('aria-activedescendant', active.id);
+ } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') {
+ // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item
+ focusable.removeAttribute('aria-activedescendant');
+ active.classList.remove('active', 'selected');
+ }
+ };
+
+ dropdown.addEventListener('keydown', (e) => {
+ // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
+ if (e.key === 'Enter') {
+ const dropdownCall = fomanticDropdownFn.bind($(dropdown));
+ let $item = dropdownCall('get item', dropdownCall('get value'));
+ if (!$item) $item = $(menu).find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
+ // if the selected item is clickable, then trigger the click event.
+ // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
+ if ($item?.[0]?.matches('a, .js-aria-clickable')) $item[0].click();
+ }
+ });
+
+ // use setTimeout to run the refreshAria in next tick (to make sure the Fomantic UI code has finished its work)
+ // do not return any value, jQuery has return-value related behaviors.
+ // when the popup is hiding, it's better to have a small "delay", because there is a Fomantic UI animation
+ // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation.
+ const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) };
+ dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem;
+ dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); });
+
+ // if the dropdown has been opened by focus, do not trigger the next click event again.
+ // otherwise the dropdown will be closed immediately, especially on Android with TalkBack
+ // * desktop event sequence: mousedown -> focus -> mouseup -> click
+ // * mobile event sequence: focus -> mousedown -> mouseup -> click
+ // Fomantic may stop propagation of blur event, use capture to make sure we can still get the event
+ let ignoreClickPreEvents = 0, ignoreClickPreVisible = 0;
+ dropdown.addEventListener('mousedown', () => {
+ ignoreClickPreVisible += isMenuVisible() ? 1 : 0;
+ ignoreClickPreEvents++;
+ }, true);
+ dropdown.addEventListener('focus', () => {
+ ignoreClickPreVisible += isMenuVisible() ? 1 : 0;
+ ignoreClickPreEvents++;
+ deferredRefreshAriaActiveItem();
+ }, true);
+ dropdown.addEventListener('blur', () => {
+ ignoreClickPreVisible = ignoreClickPreEvents = 0;
+ deferredRefreshAriaActiveItem(100);
+ }, true);
+ dropdown.addEventListener('mouseup', () => {
+ setTimeout(() => {
+ ignoreClickPreVisible = ignoreClickPreEvents = 0;
+ deferredRefreshAriaActiveItem(100);
+ }, 0);
+ }, true);
+ dropdown.addEventListener('click', (e) => {
+ if (isMenuVisible() &&
+ ignoreClickPreVisible !== 2 && // dropdown is switch from invisible to visible
+ ignoreClickPreEvents === 2 // the click event is related to mousedown+focus
+ ) {
+ e.stopPropagation(); // if the dropdown menu has been opened by focus, do not trigger the next click event again
+ }
+ ignoreClickPreEvents = ignoreClickPreVisible = 0;
+ }, true);
+}
diff --git a/web_src/js/modules/fomantic/form.js b/web_src/js/modules/fomantic/form.js
new file mode 100644
index 0000000..3bb0058
--- /dev/null
+++ b/web_src/js/modules/fomantic/form.js
@@ -0,0 +1,13 @@
+import {linkLabelAndInput} from './base.js';
+
+export function initAriaFormFieldPatch() {
+ // link the label and the input element so it's clickable and accessible
+ for (const el of document.querySelectorAll('.ui.form .field')) {
+ if (el.hasAttribute('data-field-patched')) continue;
+ const label = el.querySelector(':scope > label');
+ const input = el.querySelector(':scope > input');
+ if (!label || !input) continue;
+ linkLabelAndInput(label, input);
+ el.setAttribute('data-field-patched', 'true');
+ }
+}
diff --git a/web_src/js/modules/fomantic/modal.js b/web_src/js/modules/fomantic/modal.js
new file mode 100644
index 0000000..8b455cf
--- /dev/null
+++ b/web_src/js/modules/fomantic/modal.js
@@ -0,0 +1,28 @@
+import $ from 'jquery';
+
+const fomanticModalFn = $.fn.modal;
+
+// use our own `$.fn.modal` to patch Fomantic's modal module
+export function initAriaModalPatch() {
+ if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
+ $.fn.modal = ariaModalFn;
+ ariaModalFn.settings = fomanticModalFn.settings;
+}
+
+// the patched `$.fn.modal` modal function
+// * it does the one-time attaching on the first call
+function ariaModalFn(...args) {
+ const ret = fomanticModalFn.apply(this, args);
+ if (args[0] === 'show' || args[0]?.autoShow) {
+ for (const el of this) {
+ // If there is a form in the modal, there might be a "cancel" button before "ok" button (all buttons are "type=submit" by default).
+ // In such case, the "Enter" key will trigger the "cancel" button instead of "ok" button, then the dialog will be closed.
+ // It breaks the user experience - the "Enter" key should confirm the dialog and submit the form.
+ // So, all "cancel" buttons without "[type]" must be marked as "type=button".
+ for (const button of el.querySelectorAll('form button.cancel:not([type])')) {
+ button.setAttribute('type', 'button');
+ }
+ }
+ }
+ return ret;
+}
diff --git a/web_src/js/modules/fomantic/transition.js b/web_src/js/modules/fomantic/transition.js
new file mode 100644
index 0000000..78aa053
--- /dev/null
+++ b/web_src/js/modules/fomantic/transition.js
@@ -0,0 +1,54 @@
+import $ from 'jquery';
+
+export function initFomanticTransition() {
+ const transitionNopBehaviors = new Set([
+ 'clear queue', 'stop', 'stop all', 'destroy',
+ 'force repaint', 'repaint', 'reset',
+ 'looping', 'remove looping', 'disable', 'enable',
+ 'set duration', 'save conditions', 'restore conditions',
+ ]);
+ // stand-in for removed transition module
+ $.fn.transition = function (arg0, arg1, arg2) {
+ if (arg0 === 'is supported') return true;
+ if (arg0 === 'is animating') return false;
+ if (arg0 === 'is inward') return false;
+ if (arg0 === 'is outward') return false;
+
+ let argObj;
+ if (typeof arg0 === 'string') {
+ // many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
+ if (transitionNopBehaviors.has(arg0)) return this;
+ // now, the arg0 is an animation name, the syntax: (animation, duration, complete)
+ argObj = {animation: arg0, ...(arg1 && {duration: arg1}), ...(arg2 && {onComplete: arg2})};
+ } else if (typeof arg0 === 'object') {
+ argObj = arg0;
+ } else {
+ throw new Error(`invalid argument: ${arg0}`);
+ }
+
+ const isAnimationIn = argObj.animation?.startsWith('show') || argObj.animation?.endsWith(' in');
+ const isAnimationOut = argObj.animation?.startsWith('hide') || argObj.animation?.endsWith(' out');
+ this.each((_, el) => {
+ let toShow = isAnimationIn;
+ if (!isAnimationIn && !isAnimationOut) {
+ // If the animation is not in/out, then it must be a toggle animation.
+ // Fomantic uses computed styles to check "visibility", but to avoid unnecessary arguments, here it only checks the class.
+ toShow = this.hasClass('hidden'); // maybe it could also check "!this.hasClass('visible')", leave it to the future until there is a real problem.
+ }
+ argObj.onStart?.call(el);
+ if (toShow) {
+ el.classList.remove('hidden');
+ el.classList.add('visible', 'transition');
+ if (argObj.displayType) el.style.setProperty('display', argObj.displayType, 'important');
+ argObj.onShow?.call(el);
+ } else {
+ el.classList.add('hidden');
+ el.classList.remove('visible'); // don't remove the transition class because the Fomantic animation style is `.hidden.transition`.
+ el.style.removeProperty('display');
+ argObj.onHidden?.call(el);
+ }
+ argObj.onComplete?.call(el);
+ });
+ return this;
+ };
+}
diff --git a/web_src/js/modules/sortable.js b/web_src/js/modules/sortable.js
new file mode 100644
index 0000000..1c9adb6
--- /dev/null
+++ b/web_src/js/modules/sortable.js
@@ -0,0 +1,19 @@
+export async function createSortable(el, opts = {}) {
+ const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
+
+ return new Sortable(el, {
+ animation: 150,
+ ghostClass: 'card-ghost',
+ onChoose: (e) => {
+ const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
+ handle.classList.add('tw-cursor-grabbing');
+ opts.onChoose?.(e);
+ },
+ onUnchoose: (e) => {
+ const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item;
+ handle.classList.remove('tw-cursor-grabbing');
+ opts.onUnchoose?.(e);
+ },
+ ...opts,
+ });
+}
diff --git a/web_src/js/modules/stores.js b/web_src/js/modules/stores.js
new file mode 100644
index 0000000..1a0ed7e
--- /dev/null
+++ b/web_src/js/modules/stores.js
@@ -0,0 +1,10 @@
+import {reactive} from 'vue';
+
+let diffTreeStoreReactive;
+export function diffTreeStore() {
+ if (!diffTreeStoreReactive) {
+ diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo);
+ window.config.pageData.diffFileInfo = diffTreeStoreReactive;
+ }
+ return diffTreeStoreReactive;
+}
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
new file mode 100644
index 0000000..83b28e5
--- /dev/null
+++ b/web_src/js/modules/tippy.js
@@ -0,0 +1,195 @@
+import tippy, {followCursor} from 'tippy.js';
+import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import {formatDatetime} from '../utils/time.js';
+
+const visibleInstances = new Set();
+const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
+
+export function createTippy(target, opts = {}) {
+ // the callback functions should be destructured from opts,
+ // because we should use our own wrapper functions to handle them, do not let the user override them
+ const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
+
+ const instance = tippy(target, {
+ appendTo: document.body,
+ animation: false,
+ allowHTML: false,
+ hideOnClick: false,
+ interactiveBorder: 20,
+ ignoreAttributes: true,
+ maxWidth: 500, // increase over default 350px
+ onHide: (instance) => {
+ visibleInstances.delete(instance);
+ return onHide?.(instance);
+ },
+ onDestroy: (instance) => {
+ visibleInstances.delete(instance);
+ return onDestroy?.(instance);
+ },
+ onShow: (instance) => {
+ // hide other tooltip instances so only one tooltip shows at a time
+ for (const visibleInstance of visibleInstances) {
+ if (visibleInstance.props.role === 'tooltip') {
+ visibleInstance.hide();
+ }
+ }
+ visibleInstances.add(instance);
+ return onShow?.(instance);
+ },
+ arrow: arrow || (theme === 'bare' ? false : arrowSvg),
+ role: role || 'menu', // HTML role attribute
+ theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
+ plugins: [followCursor],
+ ...other,
+ });
+
+ if (role === 'menu') {
+ target.setAttribute('aria-haspopup', 'true');
+ }
+
+ return instance;
+}
+
+/**
+ * Attach a tooltip tippy to the given target element.
+ * If the target element already has a tooltip tippy attached, the tooltip will be updated with the new content.
+ * If the target element has no content, then no tooltip will be attached, and it returns null.
+ *
+ * Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation.
+ *
+ * @param target {HTMLElement}
+ * @param content {null|string}
+ * @returns {null|tippy}
+ */
+function attachTooltip(target, content = null) {
+ switchTitleToTooltip(target);
+
+ content = content ?? target.getAttribute('data-tooltip-content');
+ if (!content) return null;
+
+ // when element has a clipboard target, we update the tooltip after copy
+ // in which case it is undesirable to automatically hide it on click as
+ // it would momentarily flash the tooltip out and in.
+ const hasClipboardTarget = target.hasAttribute('data-clipboard-target');
+ const hideOnClick = !hasClipboardTarget;
+
+ const props = {
+ content,
+ delay: 100,
+ role: 'tooltip',
+ theme: 'tooltip',
+ hideOnClick,
+ placement: target.getAttribute('data-tooltip-placement') || 'top-start',
+ followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
+ ...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
+ };
+
+ if (!target._tippy) {
+ createTippy(target, props);
+ } else {
+ target._tippy.setProps(props);
+ }
+ return target._tippy;
+}
+
+function switchTitleToTooltip(target) {
+ let title = target.getAttribute('title');
+ if (title) {
+ // apply custom formatting to relative-time's tooltips
+ if (target.tagName.toLowerCase() === 'relative-time') {
+ const datetime = target.getAttribute('datetime');
+ if (datetime) {
+ title = formatDatetime(new Date(datetime));
+ }
+ }
+ target.setAttribute('data-tooltip-content', title);
+ target.setAttribute('aria-label', title);
+ // keep the attribute, in case there are some other "[title]" selectors
+ // and to prevent infinite loop with <relative-time> which will re-add
+ // title if it is absent
+ target.setAttribute('title', '');
+ }
+}
+
+/**
+ * Creating tooltip tippy instance is expensive, so we only create it when the user hovers over the element
+ * According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
+ * Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
+ * The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
+ * @param e {Event}
+ */
+function lazyTooltipOnMouseHover(e) {
+ e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
+ attachTooltip(this);
+}
+
+// Activate the tooltip for current element.
+// If the element has no aria-label, use the tooltip content as aria-label.
+function attachLazyTooltip(el) {
+ el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
+
+ // meanwhile, if the element has no aria-label, use the tooltip content as aria-label
+ if (!el.hasAttribute('aria-label')) {
+ const content = el.getAttribute('data-tooltip-content');
+ if (content) {
+ el.setAttribute('aria-label', content);
+ }
+ }
+}
+
+// Activate the tooltip for all children elements.
+function attachChildrenLazyTooltip(target) {
+ for (const el of target.querySelectorAll('[data-tooltip-content]')) {
+ attachLazyTooltip(el);
+ }
+}
+
+export function initGlobalTooltips() {
+ // use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
+ const observerConnect = (observer) => observer.observe(document, {
+ subtree: true,
+ childList: true,
+ attributeFilter: ['data-tooltip-content', 'title'],
+ });
+ const observer = new MutationObserver((mutationList, observer) => {
+ const pending = observer.takeRecords();
+ observer.disconnect();
+ for (const mutation of [...mutationList, ...pending]) {
+ if (mutation.type === 'childList') {
+ // mainly for Vue components and AJAX rendered elements
+ for (const el of mutation.addedNodes) {
+ if (!isDocumentFragmentOrElementNode(el)) continue;
+ attachChildrenLazyTooltip(el);
+ if (el.hasAttribute('data-tooltip-content')) {
+ attachLazyTooltip(el);
+ }
+ }
+ } else if (mutation.type === 'attributes') {
+ attachTooltip(mutation.target);
+ }
+ }
+ observerConnect(observer);
+ });
+ observerConnect(observer);
+
+ attachChildrenLazyTooltip(document.documentElement);
+}
+
+export function showTemporaryTooltip(target, content) {
+ // if the target is inside a dropdown, don't show the tooltip because when the dropdown
+ // closes, the tippy would be pushed unsightly to the top-left of the screen like seen
+ // on the issue comment menu.
+ if (target.closest('.ui.dropdown > .menu')) return;
+
+ const tippy = target._tippy ?? attachTooltip(target, content);
+ tippy.setContent(content);
+ if (!tippy.state.isShown) tippy.show();
+ tippy.setProps({
+ onHidden: (tippy) => {
+ // reset the default tooltip content, if no default, then this temporary tooltip could be destroyed
+ if (!attachTooltip(target)) {
+ tippy.destroy();
+ }
+ },
+ });
+}
diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js
new file mode 100644
index 0000000..d12d203
--- /dev/null
+++ b/web_src/js/modules/toast.js
@@ -0,0 +1,55 @@
+import {htmlEscape} from 'escape-goat';
+import {svg} from '../svg.js';
+import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
+
+const levels = {
+ info: {
+ icon: 'octicon-check',
+ background: 'var(--color-green)',
+ duration: 2500,
+ },
+ warning: {
+ icon: 'gitea-exclamation',
+ background: 'var(--color-orange)',
+ duration: -1, // requires dismissal to hide
+ },
+ error: {
+ icon: 'gitea-exclamation',
+ background: 'var(--color-red)',
+ duration: -1, // requires dismissal to hide
+ },
+};
+
+// See https://github.com/apvarun/toastify-js#api for options
+function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
+ const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
+ const toast = Toastify({
+ text: `
+ <div class='toast-icon'>${svg(icon)}</div>
+ <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
+ <button class='toast-close'>${svg('octicon-x')}</button>
+ `,
+ escapeMarkup: false,
+ gravity: gravity ?? 'top',
+ position: position ?? 'center',
+ duration: duration ?? levelDuration,
+ style: {background},
+ ...other,
+ });
+
+ toast.showToast();
+ toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
+ return toast;
+}
+
+export function showInfoToast(message, opts) {
+ return showToast(message, 'info', opts);
+}
+
+export function showWarningToast(message, opts) {
+ return showToast(message, 'warning', opts);
+}
+
+export function showErrorToast(message, opts) {
+ return showToast(message, 'error', opts);
+}
diff --git a/web_src/js/modules/toast.test.js b/web_src/js/modules/toast.test.js
new file mode 100644
index 0000000..357f18d
--- /dev/null
+++ b/web_src/js/modules/toast.test.js
@@ -0,0 +1,16 @@
+import {showInfoToast, showErrorToast, showWarningToast} from './toast.js';
+
+test('showInfoToast', async () => {
+ showInfoToast('success 😀', {duration: -1});
+ expect(document.querySelector('.toastify')).toBeTruthy();
+});
+
+test('showWarningToast', async () => {
+ showWarningToast('warning 😐', {duration: -1});
+ expect(document.querySelector('.toastify')).toBeTruthy();
+});
+
+test('showErrorToast', async () => {
+ showErrorToast('error 🙁', {duration: -1});
+ expect(document.querySelector('.toastify')).toBeTruthy();
+});
diff --git a/web_src/js/render/ansi.js b/web_src/js/render/ansi.js
new file mode 100644
index 0000000..bb622dd
--- /dev/null
+++ b/web_src/js/render/ansi.js
@@ -0,0 +1,45 @@
+import {AnsiUp} from 'ansi_up';
+
+const replacements = [
+ [/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
+ [/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
+];
+
+// render ANSI to HTML
+export function renderAnsi(line) {
+ // create a fresh ansi_up instance because otherwise previous renders can influence
+ // the output of future renders, because ansi_up is stateful and remembers things like
+ // unclosed opening tags for colors.
+ const ansi_up = new AnsiUp();
+ ansi_up.use_classes = true;
+
+ if (line.endsWith('\r\n')) {
+ line = line.substring(0, line.length - 2);
+ } else if (line.endsWith('\n')) {
+ line = line.substring(0, line.length - 1);
+ }
+
+ if (line.includes('\x1b')) {
+ for (const [regex, replacement] of replacements) {
+ line = line.replace(regex, replacement);
+ }
+ }
+
+ if (!line.includes('\r')) {
+ return ansi_up.ansi_to_html(line);
+ }
+
+ // handle "\rReading...1%\rReading...5%\rReading...100%",
+ // convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
+ const lines = [];
+ for (const part of line.split('\r')) {
+ if (part === '') continue;
+ const partHtml = ansi_up.ansi_to_html(part);
+ if (partHtml !== '') {
+ lines.push(partHtml);
+ }
+ }
+
+ // the log message element is with "white-space: break-spaces;", so use "\n" to break lines
+ return lines.join('\n');
+}
diff --git a/web_src/js/render/ansi.test.js b/web_src/js/render/ansi.test.js
new file mode 100644
index 0000000..5afff71
--- /dev/null
+++ b/web_src/js/render/ansi.test.js
@@ -0,0 +1,20 @@
+import {renderAnsi} from './ansi.js';
+
+test('renderAnsi', () => {
+ expect(renderAnsi('abc')).toEqual('abc');
+ expect(renderAnsi('abc\n')).toEqual('abc');
+ expect(renderAnsi('abc\r\n')).toEqual('abc');
+ expect(renderAnsi('\r')).toEqual('');
+ expect(renderAnsi('\rx\rabc')).toEqual('x\nabc');
+ expect(renderAnsi('\rabc\rx\r')).toEqual('abc\nx');
+ expect(renderAnsi('\x1b[30mblack\x1b[37mwhite')).toEqual('<span class="ansi-black-fg">black</span><span class="ansi-white-fg">white</span>'); // unclosed
+ expect(renderAnsi('<script>')).toEqual('&lt;script&gt;');
+ expect(renderAnsi('\x1b[1A\x1b[2Ktest\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
+ expect(renderAnsi('\x1b[1A\x1b[2K\rtest\r\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
+ expect(renderAnsi('\x1b[1A\x1b[2Ktest\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
+ expect(renderAnsi('\x1b[1A\x1b[2K\rtest\r\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
+
+ // treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally.
+ expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc');
+ expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`<span style="background-color:rgb(135,0,0)">a</span><span style="background-color:rgb(175,255,255)">b</span>`);
+});
diff --git a/web_src/js/render/pdf.js b/web_src/js/render/pdf.js
new file mode 100644
index 0000000..f31f161
--- /dev/null
+++ b/web_src/js/render/pdf.js
@@ -0,0 +1,19 @@
+import {htmlEscape} from 'escape-goat';
+
+export async function initPdfViewer() {
+ const els = document.querySelectorAll('.pdf-content');
+ if (!els.length) return;
+
+ const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
+
+ for (const el of els) {
+ const src = el.getAttribute('data-src');
+ const fallbackText = el.getAttribute('data-fallback-button-text');
+ pdfobject.embed(src, el, {
+ fallbackLink: htmlEscape`
+ <a role="button" class="ui basic button pdf-fallback-button" href="[url]">${fallbackText}</a>
+ `,
+ });
+ el.classList.remove('is-loading');
+ }
+}
diff --git a/web_src/js/standalone/devtest.js b/web_src/js/standalone/devtest.js
new file mode 100644
index 0000000..d0ca511
--- /dev/null
+++ b/web_src/js/standalone/devtest.js
@@ -0,0 +1,11 @@
+import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js';
+
+document.getElementById('info-toast').addEventListener('click', () => {
+ showInfoToast('success 😀');
+});
+document.getElementById('warning-toast').addEventListener('click', () => {
+ showWarningToast('warning 😐');
+});
+document.getElementById('error-toast').addEventListener('click', () => {
+ showErrorToast('error 🙁');
+});
diff --git a/web_src/js/standalone/forgejo-swagger.js b/web_src/js/standalone/forgejo-swagger.js
new file mode 100644
index 0000000..b7550b8
--- /dev/null
+++ b/web_src/js/standalone/forgejo-swagger.js
@@ -0,0 +1,23 @@
+window.addEventListener('load', async () => {
+ const [{default: SwaggerUI}] = await Promise.all([
+ import(/* webpackChunkName: "swagger-ui" */'swagger-ui-dist/swagger-ui-es-bundle.js'),
+ import(/* webpackChunkName: "swagger-ui" */'swagger-ui-dist/swagger-ui.css'),
+ ]);
+ const url = document.getElementById('swagger-ui').getAttribute('data-source');
+
+ const ui = SwaggerUI({
+ url,
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ docExpansion: 'none',
+ defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
+ presets: [
+ SwaggerUI.presets.apis,
+ ],
+ plugins: [
+ SwaggerUI.plugins.DownloadUrl,
+ ],
+ });
+
+ window.ui = ui;
+});
diff --git a/web_src/js/standalone/swagger.js b/web_src/js/standalone/swagger.js
new file mode 100644
index 0000000..ec2115e
--- /dev/null
+++ b/web_src/js/standalone/swagger.js
@@ -0,0 +1,34 @@
+window.addEventListener('load', async () => {
+ const [{default: SwaggerUI}] = await Promise.all([
+ import(/* webpackChunkName: "swagger-ui" */'swagger-ui-dist/swagger-ui-es-bundle.js'),
+ import(/* webpackChunkName: "swagger-ui" */'swagger-ui-dist/swagger-ui.css'),
+ ]);
+
+ const url = document.getElementById('swagger-ui').getAttribute('data-source');
+ const res = await fetch(url);
+ const spec = await res.json();
+
+ // Make the page's protocol be at the top of the schemes list
+ const proto = window.location.protocol.slice(0, -1);
+ spec.schemes.sort((a, b) => {
+ if (a === proto) return -1;
+ if (b === proto) return 1;
+ return 0;
+ });
+
+ const ui = SwaggerUI({
+ spec,
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ docExpansion: 'none',
+ defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
+ presets: [
+ SwaggerUI.presets.apis,
+ ],
+ plugins: [
+ SwaggerUI.plugins.DownloadUrl,
+ ],
+ });
+
+ window.ui = ui;
+});
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
new file mode 100644
index 0000000..9ef5f28
--- /dev/null
+++ b/web_src/js/svg.js
@@ -0,0 +1,228 @@
+import {h} from 'vue';
+import {parseDom, serializeXml} from './utils.js';
+import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
+import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
+import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg';
+import giteaExclamation from '../../public/assets/img/svg/gitea-exclamation.svg';
+import octiconArchive from '../../public/assets/img/svg/octicon-archive.svg';
+import octiconArrowSwitch from '../../public/assets/img/svg/octicon-arrow-switch.svg';
+import octiconBlocked from '../../public/assets/img/svg/octicon-blocked.svg';
+import octiconBold from '../../public/assets/img/svg/octicon-bold.svg';
+import octiconCheck from '../../public/assets/img/svg/octicon-check.svg';
+import octiconCheckbox from '../../public/assets/img/svg/octicon-checkbox.svg';
+import octiconCheckCircleFill from '../../public/assets/img/svg/octicon-check-circle-fill.svg';
+import octiconChevronDown from '../../public/assets/img/svg/octicon-chevron-down.svg';
+import octiconChevronLeft from '../../public/assets/img/svg/octicon-chevron-left.svg';
+import octiconChevronRight from '../../public/assets/img/svg/octicon-chevron-right.svg';
+import octiconClock from '../../public/assets/img/svg/octicon-clock.svg';
+import octiconCode from '../../public/assets/img/svg/octicon-code.svg';
+import octiconColumns from '../../public/assets/img/svg/octicon-columns.svg';
+import octiconCopy from '../../public/assets/img/svg/octicon-copy.svg';
+import octiconDiffAdded from '../../public/assets/img/svg/octicon-diff-added.svg';
+import octiconDiffModified from '../../public/assets/img/svg/octicon-diff-modified.svg';
+import octiconDiffRemoved from '../../public/assets/img/svg/octicon-diff-removed.svg';
+import octiconDiffRenamed from '../../public/assets/img/svg/octicon-diff-renamed.svg';
+import octiconDotFill from '../../public/assets/img/svg/octicon-dot-fill.svg';
+import octiconDownload from '../../public/assets/img/svg/octicon-download.svg';
+import octiconEye from '../../public/assets/img/svg/octicon-eye.svg';
+import octiconFile from '../../public/assets/img/svg/octicon-file.svg';
+import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-directory-fill.svg';
+import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
+import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
+import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg';
+import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg';
+import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg';
+import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg';
+import octiconGitPullRequestClosed from '../../public/assets/img/svg/octicon-git-pull-request-closed.svg';
+import octiconGitPullRequestDraft from '../../public/assets/img/svg/octicon-git-pull-request-draft.svg';
+import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg';
+import octiconHorizontalRule from '../../public/assets/img/svg/octicon-horizontal-rule.svg';
+import octiconImage from '../../public/assets/img/svg/octicon-image.svg';
+import octiconIssueClosed from '../../public/assets/img/svg/octicon-issue-closed.svg';
+import octiconIssueOpened from '../../public/assets/img/svg/octicon-issue-opened.svg';
+import octiconItalic from '../../public/assets/img/svg/octicon-italic.svg';
+import octiconKebabHorizontal from '../../public/assets/img/svg/octicon-kebab-horizontal.svg';
+import octiconLink from '../../public/assets/img/svg/octicon-link.svg';
+import octiconListOrdered from '../../public/assets/img/svg/octicon-list-ordered.svg';
+import octiconListUnordered from '../../public/assets/img/svg/octicon-list-unordered.svg';
+import octiconLock from '../../public/assets/img/svg/octicon-lock.svg';
+import octiconMeter from '../../public/assets/img/svg/octicon-meter.svg';
+import octiconMilestone from '../../public/assets/img/svg/octicon-milestone.svg';
+import octiconMirror from '../../public/assets/img/svg/octicon-mirror.svg';
+import octiconOrganization from '../../public/assets/img/svg/octicon-organization.svg';
+import octiconPlay from '../../public/assets/img/svg/octicon-play.svg';
+import octiconPlus from '../../public/assets/img/svg/octicon-plus.svg';
+import octiconProject from '../../public/assets/img/svg/octicon-project.svg';
+import octiconQuote from '../../public/assets/img/svg/octicon-quote.svg';
+import octiconRepo from '../../public/assets/img/svg/octicon-repo.svg';
+import octiconRepoForked from '../../public/assets/img/svg/octicon-repo-forked.svg';
+import octiconRepoTemplate from '../../public/assets/img/svg/octicon-repo-template.svg';
+import octiconRss from '../../public/assets/img/svg/octicon-rss.svg';
+import octiconScreenFull from '../../public/assets/img/svg/octicon-screen-full.svg';
+import octiconSearch from '../../public/assets/img/svg/octicon-search.svg';
+import octiconSidebarCollapse from '../../public/assets/img/svg/octicon-sidebar-collapse.svg';
+import octiconSidebarExpand from '../../public/assets/img/svg/octicon-sidebar-expand.svg';
+import octiconSkip from '../../public/assets/img/svg/octicon-skip.svg';
+import octiconStar from '../../public/assets/img/svg/octicon-star.svg';
+import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethrough.svg';
+import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
+import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
+import octiconTag from '../../public/assets/img/svg/octicon-tag.svg';
+import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
+import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
+import octiconX from '../../public/assets/img/svg/octicon-x.svg';
+import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
+
+const svgs = {
+ 'gitea-double-chevron-left': giteaDoubleChevronLeft,
+ 'gitea-double-chevron-right': giteaDoubleChevronRight,
+ 'gitea-empty-checkbox': giteaEmptyCheckbox,
+ 'gitea-exclamation': giteaExclamation,
+ 'octicon-archive': octiconArchive,
+ 'octicon-arrow-switch': octiconArrowSwitch,
+ 'octicon-blocked': octiconBlocked,
+ 'octicon-bold': octiconBold,
+ 'octicon-check': octiconCheck,
+ 'octicon-check-circle-fill': octiconCheckCircleFill,
+ 'octicon-checkbox': octiconCheckbox,
+ 'octicon-chevron-down': octiconChevronDown,
+ 'octicon-chevron-left': octiconChevronLeft,
+ 'octicon-chevron-right': octiconChevronRight,
+ 'octicon-clock': octiconClock,
+ 'octicon-code': octiconCode,
+ 'octicon-columns': octiconColumns,
+ 'octicon-copy': octiconCopy,
+ 'octicon-diff-added': octiconDiffAdded,
+ 'octicon-diff-modified': octiconDiffModified,
+ 'octicon-diff-removed': octiconDiffRemoved,
+ 'octicon-diff-renamed': octiconDiffRenamed,
+ 'octicon-dot-fill': octiconDotFill,
+ 'octicon-download': octiconDownload,
+ 'octicon-eye': octiconEye,
+ 'octicon-file': octiconFile,
+ 'octicon-file-directory-fill': octiconFileDirectoryFill,
+ 'octicon-filter': octiconFilter,
+ 'octicon-gear': octiconGear,
+ 'octicon-git-branch': octiconGitBranch,
+ 'octicon-git-commit': octiconGitCommit,
+ 'octicon-git-merge': octiconGitMerge,
+ 'octicon-git-pull-request': octiconGitPullRequest,
+ 'octicon-git-pull-request-closed': octiconGitPullRequestClosed,
+ 'octicon-git-pull-request-draft': octiconGitPullRequestDraft,
+ 'octicon-heading': octiconHeading,
+ 'octicon-horizontal-rule': octiconHorizontalRule,
+ 'octicon-image': octiconImage,
+ 'octicon-issue-closed': octiconIssueClosed,
+ 'octicon-issue-opened': octiconIssueOpened,
+ 'octicon-italic': octiconItalic,
+ 'octicon-kebab-horizontal': octiconKebabHorizontal,
+ 'octicon-link': octiconLink,
+ 'octicon-list-ordered': octiconListOrdered,
+ 'octicon-list-unordered': octiconListUnordered,
+ 'octicon-lock': octiconLock,
+ 'octicon-meter': octiconMeter,
+ 'octicon-milestone': octiconMilestone,
+ 'octicon-mirror': octiconMirror,
+ 'octicon-organization': octiconOrganization,
+ 'octicon-play': octiconPlay,
+ 'octicon-plus': octiconPlus,
+ 'octicon-project': octiconProject,
+ 'octicon-quote': octiconQuote,
+ 'octicon-repo': octiconRepo,
+ 'octicon-repo-forked': octiconRepoForked,
+ 'octicon-repo-template': octiconRepoTemplate,
+ 'octicon-rss': octiconRss,
+ 'octicon-screen-full': octiconScreenFull,
+ 'octicon-search': octiconSearch,
+ 'octicon-sidebar-collapse': octiconSidebarCollapse,
+ 'octicon-sidebar-expand': octiconSidebarExpand,
+ 'octicon-skip': octiconSkip,
+ 'octicon-star': octiconStar,
+ 'octicon-strikethrough': octiconStrikethrough,
+ 'octicon-sync': octiconSync,
+ 'octicon-table': octiconTable,
+ 'octicon-tag': octiconTag,
+ 'octicon-trash': octiconTrash,
+ 'octicon-triangle-down': octiconTriangleDown,
+ 'octicon-x': octiconX,
+ 'octicon-x-circle-fill': octiconXCircleFill,
+};
+
+// TODO: use a more general approach to access SVG icons.
+// At the moment, developers must check, pick and fill the names manually,
+// most of the SVG icons in assets couldn't be used directly.
+
+// retrieve an HTML string for given SVG icon name, size and additional classes
+export function svg(name, size = 16, className = '') {
+ if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
+ if (size === 16 && !className) return svgs[name];
+
+ const document = parseDom(svgs[name], 'image/svg+xml');
+ const svgNode = document.firstChild;
+ if (size !== 16) {
+ svgNode.setAttribute('width', String(size));
+ svgNode.setAttribute('height', String(size));
+ }
+ if (className) svgNode.classList.add(...className.split(/\s+/).filter(Boolean));
+ return serializeXml(svgNode);
+}
+
+export function svgParseOuterInner(name) {
+ const svgStr = svgs[name];
+ if (!svgStr) throw new Error(`Unknown SVG icon: ${name}`);
+
+ // parse the SVG string to 2 parts
+ // * svgInnerHtml: the inner part of the SVG, will be used as the content of the <svg> VNode
+ // * svgOuter: the outer part of the SVG, including attributes
+ // the builtin SVG contents are clean, so it's safe to use `indexOf` to split the content:
+ // eg: <svg outer-attributes>${svgInnerHtml}</svg>
+ const p1 = svgStr.indexOf('>'), p2 = svgStr.lastIndexOf('<');
+ if (p1 === -1 || p2 === -1) throw new Error(`Invalid SVG icon: ${name}`);
+ const svgInnerHtml = svgStr.slice(p1 + 1, p2);
+ const svgOuterHtml = svgStr.slice(0, p1 + 1) + svgStr.slice(p2);
+ const svgDoc = parseDom(svgOuterHtml, 'image/svg+xml');
+ const svgOuter = svgDoc.firstChild;
+ return {svgOuter, svgInnerHtml};
+}
+
+export const SvgIcon = {
+ name: 'SvgIcon',
+ props: {
+ name: {type: String, required: true},
+ size: {type: Number, default: 16},
+ className: {type: String, default: ''},
+ symbolId: {type: String},
+ },
+ render() {
+ let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
+ // https://vuejs.org/guide/extras/render-function.html#creating-vnodes
+ // the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc
+ const attrs = {};
+ for (const attr of svgOuter.attributes) {
+ if (attr.name === 'class') continue;
+ attrs[`^${attr.name}`] = attr.value;
+ }
+ attrs[`^width`] = this.size;
+ attrs[`^height`] = this.size;
+
+ // make the <SvgIcon class="foo" class-name="bar"> classes work together
+ const classes = [];
+ for (const cls of svgOuter.classList) {
+ classes.push(cls);
+ }
+ // TODO: drop the `className/class-name` prop in the future, only use "class" prop
+ if (this.className) {
+ classes.push(...this.className.split(/\s+/).filter(Boolean));
+ }
+ if (this.symbolId) {
+ classes.push('tw-hidden', 'svg-symbol-container');
+ svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
+ }
+ // create VNode
+ return h('svg', {
+ ...attrs,
+ class: classes,
+ innerHTML: svgInnerHtml,
+ });
+ },
+};
diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js
new file mode 100644
index 0000000..06b320c
--- /dev/null
+++ b/web_src/js/svg.test.js
@@ -0,0 +1,27 @@
+import {svg, SvgIcon, svgParseOuterInner} from './svg.js';
+import {createApp, h} from 'vue';
+
+test('svg', () => {
+ expect(svg('octicon-repo')).toMatch(/^<svg/);
+ expect(svg('octicon-repo', 16)).toContain('width="16"');
+ expect(svg('octicon-repo', 32)).toContain('width="32"');
+});
+
+test('svgParseOuterInner', () => {
+ const {svgOuter, svgInnerHtml} = svgParseOuterInner('octicon-repo');
+ expect(svgOuter.nodeName).toMatch('svg');
+ expect(svgOuter.classList.contains('octicon-repo')).toBeTruthy();
+ expect(svgInnerHtml).toContain('<path');
+});
+
+test('SvgIcon', () => {
+ const root = document.createElement('div');
+ createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root);
+ const node = root.firstChild;
+ expect(node.nodeName).toEqual('svg');
+ expect(node.getAttribute('width')).toEqual('24');
+ expect(node.getAttribute('height')).toEqual('24');
+ expect(node.classList.contains('octicon-link')).toBeTruthy();
+ expect(node.classList.contains('base')).toBeTruthy();
+ expect(node.classList.contains('extra')).toBeTruthy();
+});
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
new file mode 100644
index 0000000..ce0fb66
--- /dev/null
+++ b/web_src/js/utils.js
@@ -0,0 +1,144 @@
+import {encode, decode} from 'uint8-to-base64';
+
+// transform /path/to/file.ext to file.ext
+export function basename(path = '') {
+ const lastSlashIndex = path.lastIndexOf('/');
+ return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
+}
+
+// transform /path/to/file.ext to .ext
+export function extname(path = '') {
+ const lastPointIndex = path.lastIndexOf('.');
+ return lastPointIndex < 0 ? '' : path.substring(lastPointIndex);
+}
+
+// test whether a variable is an object
+export function isObject(obj) {
+ return Object.prototype.toString.call(obj) === '[object Object]';
+}
+
+// returns whether a dark theme is enabled
+export function isDarkTheme() {
+ const style = window.getComputedStyle(document.documentElement);
+ return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
+}
+
+// strip <tags> from a string
+export function stripTags(text) {
+ return text.replace(/<[^>]*>?/g, '');
+}
+
+export function parseIssueHref(href) {
+ const path = (href || '').replace(/[#?].*$/, '');
+ const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
+ return {owner, repo, type, index};
+}
+
+// parse a URL, either relative '/path' or absolute 'https://localhost/path'
+export function parseUrl(str) {
+ return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
+}
+
+// return current locale chosen by user
+export function getCurrentLocale() {
+ return document.documentElement.lang;
+}
+
+// given a month (0-11), returns it in the documents language
+export function translateMonth(month) {
+ return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'});
+}
+
+// given a weekday (0-6, Sunday to Saturday), returns it in the documents language
+export function translateDay(day) {
+ return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'});
+}
+
+// convert a Blob to a DataURI
+export function blobToDataURI(blob) {
+ return new Promise((resolve, reject) => {
+ try {
+ const reader = new FileReader();
+ reader.addEventListener('load', (e) => {
+ resolve(e.target.result);
+ });
+ reader.addEventListener('error', () => {
+ reject(new Error('FileReader failed'));
+ });
+ reader.readAsDataURL(blob);
+ } catch (err) {
+ reject(err);
+ }
+ });
+}
+
+// convert image Blob to another mime-type format.
+export function convertImage(blob, mime) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const img = new Image();
+ const canvas = document.createElement('canvas');
+ img.addEventListener('load', () => {
+ try {
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ canvas.toBlob((blob) => {
+ if (!(blob instanceof Blob)) return reject(new Error('imageBlobToPng failed'));
+ resolve(blob);
+ }, mime);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ img.addEventListener('error', () => {
+ reject(new Error('imageBlobToPng failed'));
+ });
+ img.src = await blobToDataURI(blob);
+ } catch (err) {
+ reject(err);
+ }
+ });
+}
+
+export function toAbsoluteUrl(url) {
+ if (url.startsWith('http://') || url.startsWith('https://')) {
+ return url;
+ }
+ if (url.startsWith('//')) {
+ return `${window.location.protocol}${url}`; // it's also a somewhat absolute URL (with the current scheme)
+ }
+ if (url && !url.startsWith('/')) {
+ throw new Error('unsupported url, it should either start with / or http(s)://');
+ }
+ return `${window.location.origin}${url}`;
+}
+
+// Encode an ArrayBuffer into a URLEncoded base64 string.
+export function encodeURLEncodedBase64(arrayBuffer) {
+ return encode(arrayBuffer)
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+}
+
+// Decode a URLEncoded base64 to an ArrayBuffer string.
+export function decodeURLEncodedBase64(base64url) {
+ return decode(base64url
+ .replace(/_/g, '/')
+ .replace(/-/g, '+'));
+}
+
+const domParser = new DOMParser();
+const xmlSerializer = new XMLSerializer();
+
+export function parseDom(text, contentType) {
+ return domParser.parseFromString(text, contentType);
+}
+
+export function serializeXml(node) {
+ return xmlSerializer.serializeToString(node);
+}
+
+export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js
new file mode 100644
index 0000000..c49bb2a
--- /dev/null
+++ b/web_src/js/utils.test.js
@@ -0,0 +1,189 @@
+import {
+ basename, extname, isObject, stripTags, parseIssueHref,
+ parseUrl, translateMonth, translateDay, blobToDataURI,
+ toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64,
+ isDarkTheme, getCurrentLocale, parseDom, serializeXml, sleep,
+} from './utils.js';
+
+afterEach(() => {
+ // Reset head and body sections of the document
+ document.documentElement.innerHTML = '<head></head><body></body>';
+
+ // Remove 'lang' and 'style' attributes of html tag
+ document.documentElement.removeAttribute('lang');
+ document.documentElement.removeAttribute('style');
+});
+
+test('basename', () => {
+ expect(basename('/path/to/file.js')).toEqual('file.js');
+ expect(basename('/path/to/file')).toEqual('file');
+ expect(basename('file.js')).toEqual('file.js');
+});
+
+test('extname', () => {
+ expect(extname('/path/to/file.js')).toEqual('.js');
+ expect(extname('/path/')).toEqual('');
+ expect(extname('/path')).toEqual('');
+ expect(extname('file.js')).toEqual('.js');
+});
+
+test('isObject', () => {
+ expect(isObject({})).toBeTruthy();
+ expect(isObject([])).toBeFalsy();
+});
+
+test('should return true if dark theme is enabled', () => {
+ // When --is-dark-theme var is defined with value true
+ document.documentElement.style.setProperty('--is-dark-theme', 'true');
+ expect(isDarkTheme()).toBeTruthy();
+
+ // when --is-dark-theme var is defined with value TRUE
+ document.documentElement.style.setProperty('--is-dark-theme', 'TRUE');
+ expect(isDarkTheme()).toBeTruthy();
+});
+
+test('should return false if dark theme is disabled', () => {
+ // when --is-dark-theme var is defined with value false
+ document.documentElement.style.setProperty('--is-dark-theme', 'false');
+ expect(isDarkTheme()).toBeFalsy();
+
+ // when --is-dark-theme var is defined with value FALSE
+ document.documentElement.style.setProperty('--is-dark-theme', 'FALSE');
+ expect(isDarkTheme()).toBeFalsy();
+});
+
+test('should return false if dark theme is not defined', () => {
+ // when --is-dark-theme var is not exist
+ expect(isDarkTheme()).toBeFalsy();
+});
+
+test('stripTags', () => {
+ expect(stripTags('<a>test</a>')).toEqual('test');
+});
+
+test('parseIssueHref', () => {
+ expect(parseIssueHref('/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'});
+ expect(parseIssueHref('/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'});
+ expect(parseIssueHref('/sub/sub2/owner/repo/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('https://example.com/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('https://example.com/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'});
+ expect(parseIssueHref('https://example.com/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('https://example.com/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'});
+ expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
+ expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined});
+});
+
+test('parseUrl', () => {
+ expect(parseUrl('').pathname).toEqual('/');
+ expect(parseUrl('/path').pathname).toEqual('/path');
+ expect(parseUrl('/path?search').pathname).toEqual('/path');
+ expect(parseUrl('/path?search').search).toEqual('?search');
+ expect(parseUrl('/path?search#hash').hash).toEqual('#hash');
+ expect(parseUrl('https://localhost/path').pathname).toEqual('/path');
+ expect(parseUrl('https://localhost/path?search').pathname).toEqual('/path');
+ expect(parseUrl('https://localhost/path?search').search).toEqual('?search');
+ expect(parseUrl('https://localhost/path?search#hash').hash).toEqual('#hash');
+});
+
+test('getCurrentLocale', () => {
+ // HTML document without explicit lang
+ expect(getCurrentLocale()).toEqual('');
+
+ // HTML document with explicit lang
+ document.documentElement.setAttribute('lang', 'en-US');
+ expect(getCurrentLocale()).toEqual('en-US');
+});
+
+test('translateMonth', () => {
+ const originalLang = document.documentElement.lang;
+ document.documentElement.lang = 'en-US';
+ expect(translateMonth(0)).toEqual('Jan');
+ expect(translateMonth(4)).toEqual('May');
+ document.documentElement.lang = 'es-ES';
+ expect(translateMonth(5)).toEqual('jun');
+ expect(translateMonth(6)).toEqual('jul');
+ document.documentElement.lang = originalLang;
+});
+
+test('translateDay', () => {
+ const originalLang = document.documentElement.lang;
+ document.documentElement.lang = 'fr-FR';
+ expect(translateDay(1)).toEqual('lun.');
+ expect(translateDay(5)).toEqual('ven.');
+ document.documentElement.lang = 'pl-PL';
+ expect(translateDay(1)).toEqual('pon.');
+ expect(translateDay(5)).toEqual('pt.');
+ document.documentElement.lang = originalLang;
+});
+
+test('blobToDataURI', async () => {
+ const blob = new Blob([JSON.stringify({test: true})], {type: 'application/json'});
+ expect(await blobToDataURI(blob)).toEqual('data:application/json;base64,eyJ0ZXN0Ijp0cnVlfQ==');
+});
+
+test('toAbsoluteUrl', () => {
+ expect(toAbsoluteUrl('//host/dir')).toEqual('http://host/dir');
+ expect(toAbsoluteUrl('https://host/dir')).toEqual('https://host/dir');
+ expect(toAbsoluteUrl('http://host/dir')).toEqual('http://host/dir');
+ expect(toAbsoluteUrl('')).toEqual('http://localhost:3000');
+ expect(toAbsoluteUrl('/user/repo')).toEqual('http://localhost:3000/user/repo');
+ expect(() => toAbsoluteUrl('path')).toThrowError('unsupported');
+});
+
+test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
+ // TextEncoder is Node.js API while Uint8Array is jsdom API and their outputs are not
+ // structurally comparable, so we convert to array to compare. The conversion can be
+ // removed once https://github.com/jsdom/jsdom/issues/2524 is resolved.
+ const encoder = new TextEncoder();
+ const uint8array = encoder.encode.bind(encoder);
+
+ expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/"
+ expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+"
+
+ expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?')));
+ expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~')));
+ expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?')));
+ expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~')));
+
+ expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ=="
+ expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a')));
+ expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a')));
+});
+
+test('parseDom', () => {
+ const paragraphStr = 'This is sample paragraph';
+ const paragraphTagStr = `<p>${paragraphStr}</p>`;
+ const content = parseDom(paragraphTagStr, 'text/html');
+ expect(content.body.innerHTML).toEqual(paragraphTagStr);
+
+ // Content should have only one paragraph
+ const paragraphs = content.getElementsByTagName('p');
+ expect(paragraphs.length).toEqual(1);
+ expect(paragraphs[0].textContent).toEqual(paragraphStr);
+});
+
+test('serializeXml', () => {
+ const textStr = 'This is a sample text';
+ const tagName = 'item';
+ const node = document.createElement(tagName);
+ node.textContent = textStr;
+ expect(serializeXml(node)).toEqual(`<${tagName}>${textStr}</${tagName}>`);
+});
+
+test('sleep', async () => {
+ await testSleep(2000);
+});
+
+async function testSleep(ms) {
+ const startTime = Date.now(); // Record the start time
+ await sleep(ms);
+ const endTime = Date.now(); // Record the end time
+ const actualSleepTime = endTime - startTime;
+ expect(actualSleepTime >= ms).toBeTruthy();
+}
diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js
new file mode 100644
index 0000000..198f97c
--- /dev/null
+++ b/web_src/js/utils/color.js
@@ -0,0 +1,33 @@
+import tinycolor from 'tinycolor2';
+
+// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// Keep this in sync with modules/util/color.go
+function getRelativeLuminance(color) {
+ const {r, g, b} = tinycolor(color).toRgb();
+ return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
+}
+
+function useLightText(backgroundColor) {
+ return getRelativeLuminance(backgroundColor) < 0.453;
+}
+
+// Given a background color, returns a black or white foreground color that the highest
+// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
+// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
+export function contrastColor(backgroundColor) {
+ return useLightText(backgroundColor) ? '#fff' : '#000';
+}
+
+function resolveColors(obj) {
+ const styles = window.getComputedStyle(document.documentElement);
+ const getColor = (name) => styles.getPropertyValue(name).trim();
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
+}
+
+export const chartJsColors = resolveColors({
+ text: '--color-text',
+ border: '--color-secondary-alpha-60',
+ commits: '--color-primary-alpha-60',
+ additions: '--color-green',
+ deletions: '--color-red',
+});
diff --git a/web_src/js/utils/color.test.js b/web_src/js/utils/color.test.js
new file mode 100644
index 0000000..fee9afc
--- /dev/null
+++ b/web_src/js/utils/color.test.js
@@ -0,0 +1,22 @@
+import {contrastColor} from './color.js';
+
+test('contrastColor', () => {
+ expect(contrastColor('#d73a4a')).toBe('#fff');
+ expect(contrastColor('#0075ca')).toBe('#fff');
+ expect(contrastColor('#cfd3d7')).toBe('#000');
+ expect(contrastColor('#a2eeef')).toBe('#000');
+ expect(contrastColor('#7057ff')).toBe('#fff');
+ expect(contrastColor('#008672')).toBe('#fff');
+ expect(contrastColor('#e4e669')).toBe('#000');
+ expect(contrastColor('#d876e3')).toBe('#000');
+ expect(contrastColor('#ffffff')).toBe('#000');
+ expect(contrastColor('#2b8684')).toBe('#fff');
+ expect(contrastColor('#2b8786')).toBe('#fff');
+ expect(contrastColor('#2c8786')).toBe('#000');
+ expect(contrastColor('#3bb6b3')).toBe('#000');
+ expect(contrastColor('#7c7268')).toBe('#fff');
+ expect(contrastColor('#7e716c')).toBe('#fff');
+ expect(contrastColor('#81706d')).toBe('#fff');
+ expect(contrastColor('#807070')).toBe('#fff');
+ expect(contrastColor('#84b6eb')).toBe('#000');
+});
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
new file mode 100644
index 0000000..44adc79
--- /dev/null
+++ b/web_src/js/utils/dom.js
@@ -0,0 +1,305 @@
+import {debounce} from 'throttle-debounce';
+
+function elementsCall(el, func, ...args) {
+ if (typeof el === 'string' || el instanceof String) {
+ el = document.querySelectorAll(el);
+ }
+ if (el instanceof Node) {
+ func(el, ...args);
+ } else if (el.length !== undefined) {
+ // this works for: NodeList, HTMLCollection, Array, jQuery
+ for (const e of el) {
+ func(e, ...args);
+ }
+ } else {
+ throw new Error('invalid argument to be shown/hidden');
+ }
+}
+
+/**
+ * @param el string (selector), Node, NodeList, HTMLCollection, Array or jQuery
+ * @param force force=true to show or force=false to hide, undefined to toggle
+ */
+function toggleShown(el, force) {
+ if (force === true) {
+ el.classList.remove('tw-hidden');
+ } else if (force === false) {
+ el.classList.add('tw-hidden');
+ } else if (force === undefined) {
+ el.classList.toggle('tw-hidden');
+ } else {
+ throw new Error('invalid force argument');
+ }
+}
+
+export function showElem(el) {
+ elementsCall(el, toggleShown, true);
+}
+
+export function hideElem(el) {
+ elementsCall(el, toggleShown, false);
+}
+
+export function toggleElem(el, force) {
+ elementsCall(el, toggleShown, force);
+}
+
+export function isElemHidden(el) {
+ const res = [];
+ elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
+ if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
+ return res[0];
+}
+
+function applyElemsCallback(elems, fn) {
+ if (fn) {
+ for (const el of elems) {
+ fn(el);
+ }
+ }
+ return elems;
+}
+
+export function queryElemSiblings(el, selector = '*', fn) {
+ return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn);
+}
+
+// it works like jQuery.children: only the direct children are selected
+export function queryElemChildren(parent, selector = '*', fn) {
+ return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
+}
+
+export function queryElems(selector, fn) {
+ return applyElemsCallback(document.querySelectorAll(selector), fn);
+}
+
+export function onDomReady(cb) {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', cb);
+ } else {
+ cb();
+ }
+}
+
+// checks whether an element is owned by the current document, and whether it is a document fragment or element node
+// if it is, it means it is a "normal" element managed by us, which can be modified safely.
+export function isDocumentFragmentOrElementNode(el) {
+ try {
+ return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
+ } catch {
+ // in case the el is not in the same origin, then the access to nodeType would fail
+ return false;
+ }
+}
+
+// autosize a textarea to fit content. Based on
+// https://github.com/github/textarea-autosize
+// ---------------------------------------------------------------------
+// Copyright (c) 2018 GitHub, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+// ---------------------------------------------------------------------
+export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
+ let isUserResized = false;
+ // lastStyleHeight and initialStyleHeight are CSS values like '100px'
+ let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight;
+
+ function onUserResize(event) {
+ if (isUserResized) return;
+ if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) {
+ const newStyleHeight = textarea.style.height;
+ if (lastStyleHeight && lastStyleHeight !== newStyleHeight) {
+ isUserResized = true;
+ }
+ lastStyleHeight = newStyleHeight;
+ }
+
+ lastMouseX = event.clientX;
+ lastMouseY = event.clientY;
+ }
+
+ function overflowOffset() {
+ let offsetTop = 0;
+ let el = textarea;
+
+ while (el !== document.body && el !== null) {
+ offsetTop += el.offsetTop || 0;
+ el = el.offsetParent;
+ }
+
+ const top = offsetTop - document.defaultView.scrollY;
+ const bottom = document.documentElement.clientHeight - (top + textarea.offsetHeight);
+ return {top, bottom};
+ }
+
+ function resizeToFit() {
+ if (isUserResized) return;
+ if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return;
+
+ try {
+ const {top, bottom} = overflowOffset();
+ const isOutOfViewport = top < 0 || bottom < 0;
+
+ const computedStyle = getComputedStyle(textarea);
+ const topBorderWidth = parseFloat(computedStyle.borderTopWidth);
+ const bottomBorderWidth = parseFloat(computedStyle.borderBottomWidth);
+ const isBorderBox = computedStyle.boxSizing === 'border-box';
+ const borderAddOn = isBorderBox ? topBorderWidth + bottomBorderWidth : 0;
+
+ const adjustedViewportMarginBottom = bottom < viewportMarginBottom ? bottom : viewportMarginBottom;
+ const curHeight = parseFloat(computedStyle.height);
+ const maxHeight = curHeight + bottom - adjustedViewportMarginBottom;
+
+ textarea.style.height = 'auto';
+ let newHeight = textarea.scrollHeight + borderAddOn;
+
+ if (isOutOfViewport) {
+ // it is already out of the viewport:
+ // * if the textarea is expanding: do not resize it
+ if (newHeight > curHeight) {
+ newHeight = curHeight;
+ }
+ // * if the textarea is shrinking, shrink line by line (just use the
+ // scrollHeight). do not apply max-height limit, otherwise the page
+ // flickers and the textarea jumps
+ } else {
+ // * if it is in the viewport, apply the max-height limit
+ newHeight = Math.min(maxHeight, newHeight);
+ }
+
+ textarea.style.height = `${newHeight}px`;
+ lastStyleHeight = textarea.style.height;
+ } finally {
+ // ensure that the textarea is fully scrolled to the end, when the cursor
+ // is at the end during an input event
+ if (textarea.selectionStart === textarea.selectionEnd &&
+ textarea.selectionStart === textarea.value.length) {
+ textarea.scrollTop = textarea.scrollHeight;
+ }
+ }
+ }
+
+ function onFormReset() {
+ isUserResized = false;
+ if (initialStyleHeight !== undefined) {
+ textarea.style.height = initialStyleHeight;
+ } else {
+ textarea.style.removeProperty('height');
+ }
+ }
+
+ textarea.addEventListener('mousemove', onUserResize);
+ textarea.addEventListener('input', resizeToFit);
+ textarea.form?.addEventListener('reset', onFormReset);
+ initialStyleHeight = textarea.style.height ?? undefined;
+ if (textarea.value) resizeToFit();
+
+ return {
+ resizeToFit,
+ destroy() {
+ textarea.removeEventListener('mousemove', onUserResize);
+ textarea.removeEventListener('input', resizeToFit);
+ textarea.form?.removeEventListener('reset', onFormReset);
+ },
+ };
+}
+
+export function onInputDebounce(fn) {
+ return debounce(300, fn);
+}
+
+// Set the `src` attribute on an element and returns a promise that resolves once the element
+// has loaded or errored. Suitable for all elements mention in:
+// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event
+export function loadElem(el, src) {
+ return new Promise((resolve) => {
+ el.addEventListener('load', () => resolve(true), {once: true});
+ el.addEventListener('error', () => resolve(false), {once: true});
+ el.src = src;
+ });
+}
+
+// some browsers like PaleMoon don't have "SubmitEvent" support, so polyfill it by a tricky method: use the last clicked button as submitter
+// it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)"
+const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
+
+export function submitEventSubmitter(e) {
+ return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
+}
+
+function submitEventPolyfillListener(e) {
+ const form = e.target.closest('form');
+ if (!form) return;
+ form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]');
+}
+
+export function initSubmitEventPolyfill() {
+ if (!needSubmitEventPolyfill) return;
+ console.warn(`This browser doesn't have "SubmitEvent" support, use a tricky method to polyfill`);
+ document.body.addEventListener('click', submitEventPolyfillListener);
+ document.body.addEventListener('focus', submitEventPolyfillListener);
+}
+
+/**
+ * Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
+ * Note: This function doesn't account for all possible visibility scenarios.
+ * @param {HTMLElement} element The element to check.
+ * @returns {boolean} True if the element is visible.
+ */
+export function isElemVisible(element) {
+ if (!element) return false;
+
+ return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
+}
+
+// extract text and images from "paste" event
+export function getPastedContent(e) {
+ const images = [];
+ for (const item of e.clipboardData?.items ?? []) {
+ if (item.type?.startsWith('image/')) {
+ images.push(item.getAsFile());
+ }
+ }
+ const text = e.clipboardData?.getData?.('text') ?? '';
+ return {text, images};
+}
+
+// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
+export function replaceTextareaSelection(textarea, text) {
+ const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
+ const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
+ let success = true;
+
+ textarea.contentEditable = 'true';
+ try {
+ success = document.execCommand('insertText', false, text);
+ } catch {
+ success = false;
+ }
+ textarea.contentEditable = 'false';
+
+ if (success && !textarea.value.slice(0, textarea.selectionStart ?? undefined).endsWith(text)) {
+ success = false;
+ }
+
+ if (!success) {
+ textarea.value = `${before}${text}${after}`;
+ textarea.dispatchEvent(new CustomEvent('change', {bubbles: true, cancelable: true}));
+ }
+}
+
+// Warning: Do not enter any unsanitized variables here
+export function createElementFromHTML(htmlString) {
+ const div = document.createElement('div');
+ div.innerHTML = htmlString.trim();
+ return div.firstChild;
+}
diff --git a/web_src/js/utils/dom.test.js b/web_src/js/utils/dom.test.js
new file mode 100644
index 0000000..fd7d97c
--- /dev/null
+++ b/web_src/js/utils/dom.test.js
@@ -0,0 +1,5 @@
+import {createElementFromHTML} from './dom.js';
+
+test('createElementFromHTML', () => {
+ expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
+});
diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js
new file mode 100644
index 0000000..ed5d98e
--- /dev/null
+++ b/web_src/js/utils/image.js
@@ -0,0 +1,47 @@
+export async function pngChunks(blob) {
+ const uint8arr = new Uint8Array(await blob.arrayBuffer());
+ const chunks = [];
+ if (uint8arr.length < 12) return chunks;
+ const view = new DataView(uint8arr.buffer);
+ if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
+
+ const decoder = new TextDecoder();
+ let index = 8;
+ while (index < uint8arr.length) {
+ const len = view.getUint32(index);
+ chunks.push({
+ name: decoder.decode(uint8arr.slice(index + 4, index + 8)),
+ data: uint8arr.slice(index + 8, index + 8 + len),
+ });
+ index += len + 12;
+ }
+
+ return chunks;
+}
+
+// decode a image and try to obtain width and dppx. If will never throw but instead
+// return default values.
+export async function imageInfo(blob) {
+ let width = 0; // 0 means no width could be determined
+ let dppx = 1; // 1 dot per pixel for non-HiDPI screens
+
+ if (blob.type === 'image/png') { // only png is supported currently
+ try {
+ for (const {name, data} of await pngChunks(blob)) {
+ const view = new DataView(data.buffer);
+ if (name === 'IHDR' && data?.length) {
+ // extract width from mandatory IHDR chunk
+ width = view.getUint32(0);
+ } else if (name === 'pHYs' && data?.length) {
+ // extract dppx from optional pHYs chunk, assuming pixels are square
+ const unit = view.getUint8(8);
+ if (unit === 1) {
+ dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx
+ }
+ }
+ }
+ } catch {}
+ }
+
+ return {width, dppx};
+}
diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js
new file mode 100644
index 0000000..ba47582
--- /dev/null
+++ b/web_src/js/utils/image.test.js
@@ -0,0 +1,29 @@
+import {pngChunks, imageInfo} from './image.js';
+
+const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg==';
+const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A==';
+const pngEmpty = 'data:image/png;base64,';
+
+async function dataUriToBlob(datauri) {
+ return await (await globalThis.fetch(datauri)).blob();
+}
+
+test('pngChunks', async () => {
+ expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([
+ {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])},
+ {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])},
+ {name: 'IEND', data: new Uint8Array([])},
+ ]);
+ expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([
+ {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])},
+ {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])},
+ {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])},
+ ]);
+ expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]);
+});
+
+test('imageInfo', async () => {
+ expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1});
+ expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2});
+ expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1});
+});
diff --git a/web_src/js/utils/match.js b/web_src/js/utils/match.js
new file mode 100644
index 0000000..17fdfed
--- /dev/null
+++ b/web_src/js/utils/match.js
@@ -0,0 +1,43 @@
+import emojis from '../../../assets/emoji.json';
+
+const maxMatches = 6;
+
+function sortAndReduce(map) {
+ const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1]));
+ return Array.from(sortedMap.keys()).slice(0, maxMatches);
+}
+
+export function matchEmoji(queryText) {
+ const query = queryText.toLowerCase().replaceAll('_', ' ');
+ if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]);
+
+ // results is a map of weights, lower is better
+ const results = new Map();
+ for (const {aliases} of emojis) {
+ const mainAlias = aliases[0];
+ for (const [aliasIndex, alias] of aliases.entries()) {
+ const index = alias.replaceAll('_', ' ').indexOf(query);
+ if (index === -1) continue;
+ const existing = results.get(mainAlias);
+ const rankedIndex = index + aliasIndex;
+ results.set(mainAlias, existing ? existing - rankedIndex : rankedIndex);
+ }
+ }
+
+ return sortAndReduce(results);
+}
+
+export function matchMention(queryText) {
+ const query = queryText.toLowerCase();
+
+ // results is a map of weights, lower is better
+ const results = new Map();
+ for (const obj of window.config.mentionValues ?? []) {
+ const index = obj.key.toLowerCase().indexOf(query);
+ if (index === -1) continue;
+ const existing = results.get(obj);
+ results.set(obj, existing ? existing - index : index);
+ }
+
+ return sortAndReduce(results);
+}
diff --git a/web_src/js/utils/match.test.js b/web_src/js/utils/match.test.js
new file mode 100644
index 0000000..1e30b45
--- /dev/null
+++ b/web_src/js/utils/match.test.js
@@ -0,0 +1,50 @@
+import {matchEmoji, matchMention} from './match.js';
+
+test('matchEmoji', () => {
+ expect(matchEmoji('')).toEqual([
+ '+1',
+ '-1',
+ '100',
+ '1234',
+ '1st_place_medal',
+ '2nd_place_medal',
+ ]);
+
+ expect(matchEmoji('hea')).toEqual([
+ 'headphones',
+ 'headstone',
+ 'health_worker',
+ 'hear_no_evil',
+ 'heard_mcdonald_islands',
+ 'heart',
+ ]);
+
+ expect(matchEmoji('hear')).toEqual([
+ 'hear_no_evil',
+ 'heard_mcdonald_islands',
+ 'heart',
+ 'heart_decoration',
+ 'heart_eyes',
+ 'heart_eyes_cat',
+ ]);
+
+ expect(matchEmoji('poo')).toEqual([
+ 'poodle',
+ 'hankey',
+ 'spoon',
+ 'bowl_with_spoon',
+ ]);
+
+ expect(matchEmoji('1st_')).toEqual([
+ '1st_place_medal',
+ ]);
+
+ expect(matchEmoji('jellyfis')).toEqual([
+ 'jellyfish',
+ ]);
+});
+
+test('matchMention', () => {
+ expect(matchMention('')).toEqual(window.config.mentionValues.slice(0, 6));
+ expect(matchMention('user4')).toEqual([window.config.mentionValues[3]]);
+});
diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js
new file mode 100644
index 0000000..7c7eabd
--- /dev/null
+++ b/web_src/js/utils/time.js
@@ -0,0 +1,72 @@
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc.js';
+import {getCurrentLocale} from '../utils.js';
+
+dayjs.extend(utc);
+
+/**
+ * Returns an array of millisecond-timestamps of start-of-week days (Sundays)
+ *
+ * @param startConfig The start date. Can take any type that `Date` accepts.
+ * @param endConfig The end date. Can take any type that `Date` accepts.
+ */
+export function startDaysBetween(startDate, endDate) {
+ const start = dayjs.utc(startDate);
+ const end = dayjs.utc(endDate);
+
+ let current = start;
+
+ // Ensure the start date is a Sunday
+ while (current.day() !== 0) {
+ current = current.add(1, 'day');
+ }
+
+ const startDays = [];
+ while (current.isBefore(end)) {
+ startDays.push(current.valueOf());
+ current = current.add(1, 'week');
+ }
+
+ return startDays;
+}
+
+export function firstStartDateAfterDate(inputDate) {
+ if (!(inputDate instanceof Date)) {
+ throw new Error('Invalid date');
+ }
+ const dayOfWeek = inputDate.getUTCDay();
+ const daysUntilSunday = 7 - dayOfWeek;
+ const resultDate = new Date(inputDate.getTime());
+ resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
+ return resultDate.valueOf();
+}
+
+export function fillEmptyStartDaysWithZeroes(startDays, data) {
+ const result = {};
+
+ for (const startDay of startDays) {
+ result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0};
+ }
+
+ return Object.values(result);
+}
+
+let dateFormat;
+
+// format a Date object to document's locale, but with 24h format from user's current locale because this
+// option is a personal preference of the user, not something that the document's locale should dictate.
+export function formatDatetime(date) {
+ if (!dateFormat) {
+ // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
+ dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ hour: 'numeric',
+ hour12: !Number.isInteger(Number(new Intl.DateTimeFormat([], {hour: 'numeric'}).format())),
+ minute: '2-digit',
+ timeZoneName: 'short',
+ });
+ }
+ return dateFormat.format(date);
+}
diff --git a/web_src/js/utils/time.test.js b/web_src/js/utils/time.test.js
new file mode 100644
index 0000000..dbe5d7d
--- /dev/null
+++ b/web_src/js/utils/time.test.js
@@ -0,0 +1,40 @@
+import {firstStartDateAfterDate, startDaysBetween, fillEmptyStartDaysWithZeroes} from './time.js';
+
+test('startDaysBetween', () => {
+ expect(startDaysBetween(new Date('2024-02-15'), new Date('2024-04-18'))).toEqual([
+ 1708214400000,
+ 1708819200000,
+ 1709424000000,
+ 1710028800000,
+ 1710633600000,
+ 1711238400000,
+ 1711843200000,
+ 1712448000000,
+ 1713052800000,
+ ]);
+});
+
+test('firstStartDateAfterDate', () => {
+ const expectedDate = new Date('2024-02-18').getTime();
+ expect(firstStartDateAfterDate(new Date('2024-02-15'))).toEqual(expectedDate);
+
+ expect(() => firstStartDateAfterDate('2024-02-15')).toThrowError('Invalid date');
+});
+test('fillEmptyStartDaysWithZeroes with data', () => {
+ expect(fillEmptyStartDaysWithZeroes([1708214400000, 1708819200000, 1708819300000], {
+ 1708214400000: {'week': 1708214400000, 'additions': 1, 'deletions': 2, 'commits': 3},
+ 1708819200000: {'week': 1708819200000, 'additions': 4, 'deletions': 5, 'commits': 6},
+ })).toEqual([
+ {'week': 1708214400000, 'additions': 1, 'deletions': 2, 'commits': 3},
+ {'week': 1708819200000, 'additions': 4, 'deletions': 5, 'commits': 6},
+ {
+ 'additions': 0,
+ 'commits': 0,
+ 'deletions': 0,
+ 'week': 1708819300000,
+ }]);
+});
+
+test('fillEmptyStartDaysWithZeroes with empty array', () => {
+ expect(fillEmptyStartDaysWithZeroes([], {})).toEqual([]);
+});
diff --git a/web_src/js/utils/url.js b/web_src/js/utils/url.js
new file mode 100644
index 0000000..470ece3
--- /dev/null
+++ b/web_src/js/utils/url.js
@@ -0,0 +1,15 @@
+export function pathEscapeSegments(s) {
+ return s.split('/').map(encodeURIComponent).join('/');
+}
+
+function stripSlash(url) {
+ return url.endsWith('/') ? url.slice(0, -1) : url;
+}
+
+export function isUrl(url) {
+ try {
+ return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim();
+ } catch {
+ return false;
+ }
+}
diff --git a/web_src/js/utils/url.test.js b/web_src/js/utils/url.test.js
new file mode 100644
index 0000000..08c6373
--- /dev/null
+++ b/web_src/js/utils/url.test.js
@@ -0,0 +1,13 @@
+import {pathEscapeSegments, isUrl} from './url.js';
+
+test('pathEscapeSegments', () => {
+ expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c');
+ expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c');
+});
+
+test('isUrl', () => {
+ expect(isUrl('https://example.com')).toEqual(true);
+ expect(isUrl('https://example.com/')).toEqual(true);
+ expect(isUrl('https://example.com/index.html')).toEqual(true);
+ expect(isUrl('/index.html')).toEqual(false);
+});
diff --git a/web_src/js/vendor/jquery.are-you-sure.js b/web_src/js/vendor/jquery.are-you-sure.js
new file mode 100644
index 0000000..e06da39
--- /dev/null
+++ b/web_src/js/vendor/jquery.are-you-sure.js
@@ -0,0 +1,195 @@
+// Fork of the upstream module. The only changes are the addition of `const` on
+// lines 93 and 161 to make it strict mode compatible.
+
+/*!
+ * jQuery Plugin: Are-You-Sure (Dirty Form Detection)
+ * https://github.com/codedance/jquery.AreYouSure/
+ *
+ * Copyright (c) 2012-2014, Chris Dance and PaperCut Software http://www.papercut.com/
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Author: chris.dance@papercut.com
+ * Version: 1.9.0
+ * Date: 13th August 2014
+ */
+(function($) {
+
+ $.fn.areYouSure = function(options) {
+
+ var settings = $.extend(
+ {
+ 'message' : 'You have unsaved changes!',
+ 'dirtyClass' : 'dirty',
+ 'change' : null,
+ 'silent' : false,
+ 'addRemoveFieldsMarksDirty' : false,
+ 'fieldEvents' : 'change keyup propertychange input',
+ 'fieldSelector': ":input:not(input[type=submit]):not(input[type=button])"
+ }, options);
+
+ var getValue = function($field) {
+ if ($field.hasClass('ays-ignore')
+ || $field.hasClass('aysIgnore')
+ || $field.attr('data-ays-ignore')
+ || $field.attr('name') === undefined) {
+ return null;
+ }
+
+ if ($field.is(':disabled')) {
+ return 'ays-disabled';
+ }
+
+ var val;
+ var type = $field.attr('type');
+ if ($field.is('select')) {
+ type = 'select';
+ }
+
+ switch (type) {
+ case 'checkbox':
+ case 'radio':
+ val = $field.is(':checked');
+ break;
+ case 'select':
+ val = '';
+ $field.find('option').each(function(o) {
+ var $option = $(this);
+ if ($option.is(':selected')) {
+ val += $option.val();
+ }
+ });
+ break;
+ default:
+ val = $field.val();
+ }
+
+ return val;
+ };
+
+ var storeOrigValue = function($field) {
+ $field.data('ays-orig', getValue($field));
+ };
+
+ var checkForm = function(evt) {
+
+ var isFieldDirty = function($field) {
+ var origValue = $field.data('ays-orig');
+ if (undefined === origValue) {
+ return false;
+ }
+ return (getValue($field) != origValue);
+ };
+
+ var $form = ($(this).is('form'))
+ ? $(this)
+ : $(this).parents('form');
+
+ // Test on the target first as it's the most likely to be dirty
+ if (isFieldDirty($(evt.target))) {
+ setDirtyStatus($form, true);
+ return;
+ }
+
+ const $fields = $form.find(settings.fieldSelector);
+
+ if (settings.addRemoveFieldsMarksDirty) {
+ // Check if field count has changed
+ var origCount = $form.data("ays-orig-field-count");
+ if (origCount != $fields.length) {
+ setDirtyStatus($form, true);
+ return;
+ }
+ }
+
+ // Brute force - check each field
+ var isDirty = false;
+ $fields.each(function() {
+ var $field = $(this);
+ if (isFieldDirty($field)) {
+ isDirty = true;
+ return false; // break
+ }
+ });
+
+ setDirtyStatus($form, isDirty);
+ };
+
+ var initForm = function($form) {
+ var fields = $form.find(settings.fieldSelector);
+ $(fields).each(function() { storeOrigValue($(this)); });
+ $(fields).unbind(settings.fieldEvents, checkForm);
+ $(fields).bind(settings.fieldEvents, checkForm);
+ $form.data("ays-orig-field-count", $(fields).length);
+ setDirtyStatus($form, false);
+ };
+
+ var setDirtyStatus = function($form, isDirty) {
+ var changed = isDirty != $form.hasClass(settings.dirtyClass);
+ $form.toggleClass(settings.dirtyClass, isDirty);
+
+ // Fire change event if required
+ if (changed) {
+ if (settings.change) settings.change.call($form, $form);
+
+ if (isDirty) $form.trigger('dirty.areYouSure', [$form]);
+ if (!isDirty) $form.trigger('clean.areYouSure', [$form]);
+ $form.trigger('change.areYouSure', [$form]);
+ }
+ };
+
+ var rescan = function() {
+ var $form = $(this);
+ var fields = $form.find(settings.fieldSelector);
+ $(fields).each(function() {
+ var $field = $(this);
+ if (!$field.data('ays-orig')) {
+ storeOrigValue($field);
+ $field.bind(settings.fieldEvents, checkForm);
+ }
+ });
+ // Check for changes while we're here
+ $form.trigger('checkform.areYouSure');
+ };
+
+ var reinitialize = function() {
+ initForm($(this));
+ }
+
+ if (!settings.silent && !window.aysUnloadSet) {
+ window.aysUnloadSet = true;
+ $(window).bind('beforeunload', function() {
+ const $dirtyForms = $("form").filter('.' + settings.dirtyClass);
+ if ($dirtyForms.length == 0) {
+ return;
+ }
+ // Prevent multiple prompts - seen on Chrome and IE
+ if (navigator.userAgent.toLowerCase().match(/msie|chrome/)) {
+ if (window.aysHasPrompted) {
+ return;
+ }
+ window.aysHasPrompted = true;
+ window.setTimeout(function() {window.aysHasPrompted = false;}, 900);
+ }
+ return settings.message;
+ });
+ }
+
+ return this.each(function(elem) {
+ if (!$(this).is('form')) {
+ return;
+ }
+ var $form = $(this);
+
+ $form.submit(function() {
+ $form.removeClass(settings.dirtyClass);
+ });
+ $form.bind('reset', function() { setDirtyStatus($form, false); });
+ // Add a custom events
+ $form.bind('rescan.areYouSure', rescan);
+ $form.bind('reinitialize.areYouSure', reinitialize);
+ $form.bind('checkform.areYouSure', checkForm);
+ initForm($form);
+ });
+ };
+})(jQuery);
diff --git a/web_src/js/vitest.setup.js b/web_src/js/vitest.setup.js
new file mode 100644
index 0000000..5366958
--- /dev/null
+++ b/web_src/js/vitest.setup.js
@@ -0,0 +1,18 @@
+window.__webpack_public_path__ = '';
+
+window.config = {
+ csrfToken: 'test-csrf-token-123456',
+ pageData: {},
+ i18n: {},
+ customEmojis: {},
+ appSubUrl: '',
+ mentionValues: [
+ {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
+ {key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'},
+ {key: 'org3 User 3', value: 'org3', name: 'org3', fullname: 'User 3', avatar: 'https://avatar3.com'},
+ {key: 'user4 User 4', value: 'user4', name: 'user4', fullname: 'User 4', avatar: 'https://avatar4.com'},
+ {key: 'user5 User 5', value: 'user5', name: 'user5', fullname: 'User 5', avatar: 'https://avatar5.com'},
+ {key: 'org6 User 6', value: 'org6', name: 'org6', fullname: 'User 6', avatar: 'https://avatar6.com'},
+ {key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'},
+ ],
+};
diff --git a/web_src/js/webcomponents/README.md b/web_src/js/webcomponents/README.md
new file mode 100644
index 0000000..45af58e
--- /dev/null
+++ b/web_src/js/webcomponents/README.md
@@ -0,0 +1,11 @@
+# Web Components
+
+This `webcomponents` directory contains the source code for the web components used in the Gitea Web UI.
+
+https://developer.mozilla.org/en-US/docs/Web/Web_Components
+
+# Guidelines
+
+* These components are loaded in `<head>` (before DOM body) in a separate entry point, they need to be lightweight to not affect the page loading time too much.
+* Do not import `svg.js` into a web component because that file is currently not tree-shakeable, import svg files individually insteat.
+* All our components must be added to `webpack.config.js` so they work correctly in Vue.
diff --git a/web_src/js/webcomponents/absolute-date.js b/web_src/js/webcomponents/absolute-date.js
new file mode 100644
index 0000000..d2be455
--- /dev/null
+++ b/web_src/js/webcomponents/absolute-date.js
@@ -0,0 +1,40 @@
+import {Temporal} from 'temporal-polyfill';
+
+export function toAbsoluteLocaleDate(dateStr, lang, opts) {
+ return Temporal.PlainDate.from(dateStr).toLocaleString(lang ?? [], opts);
+}
+
+window.customElements.define('absolute-date', class extends HTMLElement {
+ static observedAttributes = ['date', 'year', 'month', 'weekday', 'day'];
+
+ update = () => {
+ const year = this.getAttribute('year') ?? '';
+ const month = this.getAttribute('month') ?? '';
+ const weekday = this.getAttribute('weekday') ?? '';
+ const day = this.getAttribute('day') ?? '';
+ const lang = this.closest('[lang]')?.getAttribute('lang') ||
+ this.ownerDocument.documentElement.getAttribute('lang') || '';
+
+ // only use the first 10 characters, e.g. the `yyyy-mm-dd` part
+ const dateStr = this.getAttribute('date').substring(0, 10);
+
+ if (!this.shadowRoot) this.attachShadow({mode: 'open'});
+ this.shadowRoot.textContent = toAbsoluteLocaleDate(dateStr, lang, {
+ ...(year && {year}),
+ ...(month && {month}),
+ ...(weekday && {weekday}),
+ ...(day && {day}),
+ });
+ };
+
+ attributeChangedCallback(_name, oldValue, newValue) {
+ if (!this.initialized || oldValue === newValue) return;
+ this.update();
+ }
+
+ connectedCallback() {
+ this.initialized = false;
+ this.update();
+ this.initialized = true;
+ }
+});
diff --git a/web_src/js/webcomponents/absolute-date.test.js b/web_src/js/webcomponents/absolute-date.test.js
new file mode 100644
index 0000000..ba04451
--- /dev/null
+++ b/web_src/js/webcomponents/absolute-date.test.js
@@ -0,0 +1,15 @@
+import {toAbsoluteLocaleDate} from './absolute-date.js';
+
+test('toAbsoluteLocaleDate', () => {
+ expect(toAbsoluteLocaleDate('2024-03-15', 'en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })).toEqual('March 15, 2024');
+
+ expect(toAbsoluteLocaleDate('2024-03-15', 'de-DE', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })).toEqual('15. März 2024');
+});
diff --git a/web_src/js/webcomponents/index.js b/web_src/js/webcomponents/index.js
new file mode 100644
index 0000000..7cec9da
--- /dev/null
+++ b/web_src/js/webcomponents/index.js
@@ -0,0 +1,5 @@
+import './polyfills.js';
+import '@github/relative-time-element';
+import './origin-url.js';
+import './overflow-menu.js';
+import './absolute-date.js';
diff --git a/web_src/js/webcomponents/origin-url.js b/web_src/js/webcomponents/origin-url.js
new file mode 100644
index 0000000..09aa77f
--- /dev/null
+++ b/web_src/js/webcomponents/origin-url.js
@@ -0,0 +1,22 @@
+// Convert an absolute or relative URL to an absolute URL with the current origin. It only
+// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'.
+// NOTE: Keep this function in sync with clone_script.tmpl
+export function toOriginUrl(urlStr) {
+ try {
+ if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
+ const {origin, protocol, hostname, port} = window.location;
+ const url = new URL(urlStr, origin);
+ url.protocol = protocol;
+ url.hostname = hostname;
+ url.port = port || (protocol === 'https:' ? '443' : '80');
+ return url.toString();
+ }
+ } catch {}
+ return urlStr;
+}
+
+window.customElements.define('origin-url', class extends HTMLElement {
+ connectedCallback() {
+ this.textContent = toOriginUrl(this.getAttribute('data-url'));
+ }
+});
diff --git a/web_src/js/webcomponents/origin-url.test.js b/web_src/js/webcomponents/origin-url.test.js
new file mode 100644
index 0000000..3b2ab89
--- /dev/null
+++ b/web_src/js/webcomponents/origin-url.test.js
@@ -0,0 +1,17 @@
+import {toOriginUrl} from './origin-url.js';
+
+test('toOriginUrl', () => {
+ const oldLocation = window.location;
+ for (const origin of ['https://example.com', 'https://example.com:3000']) {
+ window.location = new URL(`${origin}/`);
+ expect(toOriginUrl('/')).toEqual(`${origin}/`);
+ expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`);
+ expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`);
+ expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`);
+ expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`);
+ expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`);
+ expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`);
+ expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`);
+ }
+ window.location = oldLocation;
+});
diff --git a/web_src/js/webcomponents/overflow-menu.js b/web_src/js/webcomponents/overflow-menu.js
new file mode 100644
index 0000000..a69ce16
--- /dev/null
+++ b/web_src/js/webcomponents/overflow-menu.js
@@ -0,0 +1,220 @@
+import {throttle} from 'throttle-debounce';
+import {createTippy} from '../modules/tippy.js';
+import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg';
+
+window.customElements.define('overflow-menu', class extends HTMLElement {
+ updateItems = throttle(100, () => {
+ if (!this.tippyContent) {
+ const div = document.createElement('div');
+ div.classList.add('tippy-target');
+ div.tabIndex = '-1'; // for initial focus, programmatic focus only
+ div.addEventListener('keydown', (e) => {
+ if (e.key === 'Tab') {
+ const items = this.tippyContent.querySelectorAll('[role="menuitem"]');
+ if (e.shiftKey) {
+ if (document.activeElement === items[0]) {
+ e.preventDefault();
+ items[items.length - 1].focus();
+ }
+ } else {
+ if (document.activeElement === items[items.length - 1]) {
+ e.preventDefault();
+ items[0].focus();
+ }
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ e.stopPropagation();
+ this.button._tippy.hide();
+ this.button.focus();
+ } else if (e.key === ' ' || e.code === 'Enter') {
+ if (document.activeElement?.matches('[role="menuitem"]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ document.activeElement.click();
+ }
+ } else if (e.key === 'ArrowDown') {
+ if (document.activeElement?.matches('.tippy-target')) {
+ e.preventDefault();
+ e.stopPropagation();
+ document.activeElement.querySelector('[role="menuitem"]:first-of-type').focus();
+ } else if (document.activeElement?.matches('[role="menuitem"]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ document.activeElement.nextElementSibling?.focus();
+ }
+ } else if (e.key === 'ArrowUp') {
+ if (document.activeElement?.matches('.tippy-target')) {
+ e.preventDefault();
+ e.stopPropagation();
+ document.activeElement.querySelector('[role="menuitem"]:last-of-type').focus();
+ } else if (document.activeElement?.matches('[role="menuitem"]')) {
+ e.preventDefault();
+ e.stopPropagation();
+ document.activeElement.previousElementSibling?.focus();
+ }
+ }
+ });
+ this.append(div);
+ this.tippyContent = div;
+ }
+
+ // move items in tippy back into the menu items for subsequent measurement
+ for (const item of this.tippyItems || []) {
+ this.menuItemsEl.append(item);
+ }
+
+ // measure which items are partially outside the element and move them into the button menu
+ this.tippyItems = [];
+ const menuRight = this.offsetLeft + this.offsetWidth;
+ const menuItems = this.menuItemsEl.querySelectorAll('.item');
+ const settingItem = this.menuItemsEl.querySelector('#settings-btn');
+ for (const item of menuItems) {
+ const itemRight = item.offsetLeft + item.offsetWidth;
+ // Width of the settings button plus a small value to get the next item to the left if there is directly one
+ // If no setting button is in the menu the default threshold is 38 - roughly the width of .overflow-menu-button
+ const overflowBtnThreshold = 38;
+ const threshold = settingItem?.offsetWidth ?? overflowBtnThreshold;
+ // If we have a settings item on the right-hand side, we must also check if the first,
+ // possibly overflowing item would still fit on the left-hand side of the overflow menu
+ // If not, it must be added to the array (twice). The duplicate is removed with the shift.
+ if (settingItem && !this.tippyItems?.length && item !== settingItem && menuRight - itemRight < overflowBtnThreshold) {
+ this.tippyItems.push(settingItem);
+ }
+ if (menuRight - itemRight < threshold) {
+ this.tippyItems.push(item);
+ }
+ }
+
+ // Special handling for settings button on right. Only done if a setting item is present
+ if (settingItem) {
+ // If less than 2 items overflow, remove all items (only settings "overflowed" - because it's on the right side)
+ if (this.tippyItems?.length < 2) {
+ this.tippyItems = [];
+ } else {
+ // Remove the first item of the list, because we have always one item more in the array due to the big threshold above
+ this.tippyItems.shift();
+ }
+ }
+
+ // if there are no overflown items, remove any previously created button
+ if (!this.tippyItems?.length) {
+ const btn = this.querySelector('.overflow-menu-button');
+ btn?._tippy?.destroy();
+ btn?.remove();
+ return;
+ }
+
+ // remove aria role from items that moved from tippy to menu
+ for (const item of menuItems) {
+ if (!this.tippyItems.includes(item)) {
+ item.removeAttribute('role');
+ }
+ }
+
+ // move all items that overflow into tippy
+ for (const item of this.tippyItems) {
+ item.setAttribute('role', 'menuitem');
+ this.tippyContent.append(item);
+ }
+
+ // update existing tippy
+ if (this.button?._tippy) {
+ this.button._tippy.setContent(this.tippyContent);
+ return;
+ }
+
+ // create button initially
+ const btn = document.createElement('button');
+ btn.classList.add('overflow-menu-button', 'btn', 'tw-px-2', 'hover:tw-text-text-dark');
+ btn.setAttribute('aria-label', window.config.i18n.more_items);
+ btn.innerHTML = octiconKebabHorizontal;
+ this.append(btn);
+ this.button = btn;
+
+ createTippy(btn, {
+ trigger: 'click',
+ hideOnClick: true,
+ interactive: true,
+ placement: 'bottom-end',
+ role: 'menu',
+ content: this.tippyContent,
+ onShow: () => { // FIXME: onShown doesn't work (never be called)
+ setTimeout(() => {
+ this.tippyContent.focus();
+ }, 0);
+ },
+ });
+ });
+
+ init() {
+ // for horizontal menus where fomantic boldens active items, prevent this bold text from
+ // enlarging the menu's active item replacing the text node with a div that renders a
+ // invisible pseudo-element that enlarges the box.
+ if (this.matches('.ui.secondary.pointing.menu, .ui.tabular.menu')) {
+ for (const item of this.querySelectorAll('.item')) {
+ for (const child of item.childNodes) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ const text = child.textContent.trim(); // whitespace is insignificant inside flexbox
+ if (!text) continue;
+ const span = document.createElement('span');
+ span.classList.add('resize-for-semibold');
+ span.setAttribute('data-text', text);
+ span.textContent = text;
+ child.replaceWith(span);
+ }
+ }
+ }
+ }
+
+ // ResizeObserver triggers on initial render, so we don't manually call `updateItems` here which
+ // also avoids a full-page FOUC in Firefox that happens when `updateItems` is called too soon.
+ this.resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ const newWidth = entry.contentBoxSize[0].inlineSize;
+ if (newWidth !== this.lastWidth) {
+ requestAnimationFrame(() => {
+ this.updateItems();
+ });
+ this.lastWidth = newWidth;
+ }
+ }
+ });
+ this.resizeObserver.observe(this);
+ }
+
+ connectedCallback() {
+ this.setAttribute('role', 'navigation');
+
+ // check whether the mandatory `.overflow-menu-items` element is present initially which happens
+ // with Vue which renders differently than browsers. If it's not there, like in the case of browser
+ // template rendering, wait for its addition.
+ // The eslint rule is not sophisticated enough or aware of this problem, see
+ // https://github.com/43081j/eslint-plugin-wc/pull/130
+ const menuItemsEl = this.querySelector('.overflow-menu-items'); // eslint-disable-line wc/no-child-traversal-in-connectedcallback
+ if (menuItemsEl) {
+ this.menuItemsEl = menuItemsEl;
+ this.init();
+ } else {
+ this.mutationObserver = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ for (const node of mutation.addedNodes) {
+ if (!isDocumentFragmentOrElementNode(node)) continue;
+ if (node.classList.contains('overflow-menu-items')) {
+ this.menuItemsEl = node;
+ this.mutationObserver?.disconnect();
+ this.init();
+ }
+ }
+ }
+ });
+ this.mutationObserver.observe(this, {childList: true});
+ }
+ }
+
+ disconnectedCallback() {
+ this.mutationObserver?.disconnect();
+ this.resizeObserver?.disconnect();
+ }
+});
diff --git a/web_src/js/webcomponents/polyfills.js b/web_src/js/webcomponents/polyfills.js
new file mode 100644
index 0000000..38f50fa
--- /dev/null
+++ b/web_src/js/webcomponents/polyfills.js
@@ -0,0 +1,17 @@
+try {
+ // some browsers like PaleMoon don't have full support for Intl.NumberFormat, so do the minimum polyfill to support "relative-time-element"
+ // https://repo.palemoon.org/MoonchildProductions/UXP/issues/2289
+ new Intl.NumberFormat('en', {style: 'unit', unit: 'minute'}).format(1);
+} catch {
+ const intlNumberFormat = Intl.NumberFormat;
+ Intl.NumberFormat = function(locales, options) {
+ if (options.style === 'unit') {
+ return {
+ format(value) {
+ return ` ${value} ${options.unit}`;
+ },
+ };
+ }
+ return intlNumberFormat(locales, options);
+ };
+}
diff --git a/web_src/svg/fontawesome-openid.svg b/web_src/svg/fontawesome-openid.svg
new file mode 100644
index 0000000..ea145d5
--- /dev/null
+++ b/web_src/svg/fontawesome-openid.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="448" height="448" viewBox="0 0 448 448">
+ <path d="M271.5 0v384l-68 32c-115-10.25-203.5-71.5-203.5-145.75 0-71.5 82.5-131 191.75-144.25v43c-71.5 12.5-124 53-124 101.25 0 51 58.5 93.25 135.75 103v0-340zM438.75 145.5l9.25 97.5-131.25-28.5 36.75-20.75c-19.5-11.5-43.5-20-70-24.75v-43c46.25 5.5 87.75 19.5 120.25 39.25z"></path>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/fontawesome-save.svg b/web_src/svg/fontawesome-save.svg
new file mode 100644
index 0000000..763d26a
--- /dev/null
+++ b/web_src/svg/fontawesome-save.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 448 512"><path d="m434 130-84-84a48 48 0 0 0-33.9-14H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V163.9a48 48 0 0 0-14-34zM224 416a64 64 0 1 1 0-128 64 64 0 0 1 0 128zm96-304.5V212a12 12 0 0 1-12 12H76a12 12 0 0 1-12-12V108a12 12 0 0 1 12-12h228.5a12 12 0 0 1 8.5 3.5l3.5 3.5a12 12 0 0 1 3.5 8.5z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/fontawesome-send.svg b/web_src/svg/fontawesome-send.svg
new file mode 100644
index 0000000..c02090f
--- /dev/null
+++ b/web_src/svg/fontawesome-send.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="449" height="448" viewBox="0 0 449 448"><path d="M441 2.8c5.3 3.7 7.8 9.7 6.8 16l-64 384A16 16 0 0 1 368 416c-2 0-4-.5-6-1.3l-113.3-46.2-60.4 73.8c-3 3.7-7.6 5.7-12.3 5.7a16 16 0 0 1-16-16v-87.3L376 80 108.7 311.3 10 270.6a15.8 15.8 0 0 1-2-28.4l416-240c2.5-1.5 5.3-2.3 8-2.3 3.3 0 6.5 1 9 2.8z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/fontawesome-windows.svg b/web_src/svg/fontawesome-windows.svg
new file mode 100644
index 0000000..4663fd7
--- /dev/null
+++ b/web_src/svg/fontawesome-windows.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="416" height="448" viewBox="0 0 416 448">
+ <path d="M170.5 251.5v162.75l-170.5-23.5v-139.25h170.5zM170.5 65.75v164.75h-170.5v-141.25zM416 251.5v196.5l-226.75-31.25v-165.25h226.75zM416 32v198.5h-226.75v-167.25z"></path>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-alpine.svg b/web_src/svg/gitea-alpine.svg
new file mode 100644
index 0000000..a297d95
--- /dev/null
+++ b/web_src/svg/gitea-alpine.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 186 162" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.25 0 0 -1.25 -268 592)" fill="#0d597f"><g transform="translate(268 393)"><path d="m0 0v15.7l-11.3-11.3c1.22-0.847 2.36-1.54 3.44-2.11 1.08-0.567 2.09-1 3.03-1.34 0.941-0.334 1.81-0.562 2.62-0.71 0.804-0.147 1.54-0.213 2.21-0.222m57.8 1.3c0.02-0.017 0.13-0.11 0.333-0.239 0.204-0.13 0.502-0.297 0.898-0.462 0.395-0.164 0.889-0.327 1.48-0.448 0.596-0.122 1.29-0.202 2.1-0.202 0.671 0 1.41 0.059 2.22 0.2 0.812 0.142 1.69 0.367 2.64 0.699 0.953 0.333 1.98 0.773 3.07 1.34 1.09 0.572 2.26 1.28 3.5 2.14l-8.57 8.4-28.4 28.5-12.5-12.5-16.8 17.3-42.2-41.7c1.24-0.86 2.4-1.56 3.49-2.14 1.09-0.571 2.12-1.01 3.07-1.34 0.951-0.332 1.83-0.557 2.64-0.698 0.81-0.142 1.55-0.201 2.22-0.201 0.804 0 1.5 0.08 2.1 0.202 0.596 0.121 1.09 0.284 1.48 0.449 0.396 0.164 0.693 0.331 0.897 0.461s0.314 0.223 0.334 0.24l19.1 19.1 6.81 6.53 18.9-18.9 6.59-6.78c0.02-0.017 0.13-0.11 0.334-0.239 0.204-0.13 0.501-0.297 0.897-0.462 0.396-0.164 0.89-0.327 1.48-0.448 0.596-0.122 1.3-0.202 2.1-0.202 0.67 0 1.41 0.059 2.22 0.2 0.811 0.142 1.69 0.367 2.64 0.699 0.952 0.333 1.98 0.773 3.07 1.34 1.09 0.572 2.26 1.28 3.5 2.14l-15.1 14.8 2.82 2.82 13.1-13.1 7.64-7.57m-0.163 79 37.2-64.5-37.2-64.5h-74.5l-37.2 64.5 37.2 64.5z" fill="#0d597f"/></g><g transform="translate(303 412)"><path d="m0 0-9.91 9.9 0.705 0.709 9.98-9.87z" fill="#0d597f"/></g></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg
new file mode 100644
index 0000000..ba8254d
--- /dev/null
+++ b/web_src/svg/gitea-arch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-azuread.svg b/web_src/svg/gitea-azuread.svg
new file mode 100644
index 0000000..6b69abc
--- /dev/null
+++ b/web_src/svg/gitea-azuread.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" class="svg gitea-azuread" width="16" height="16" aria-hidden="true"><defs><linearGradient id="gitea-azuread__a" x1="13.25" x2="8.62" y1="13.02" y2="4.25" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1988d9"/><stop offset=".9" stop-color="#54aef0"/></linearGradient><linearGradient id="gitea-azuread__b" x1="11.26" x2="14.46" y1="10.47" y2="15.99" gradientUnits="userSpaceOnUse"><stop offset=".1" stop-color="#54aef0"/><stop offset=".29" stop-color="#4fabee"/><stop offset=".51" stop-color="#41a2e9"/><stop offset=".74" stop-color="#2a93e0"/><stop offset=".88" stop-color="#1988d9"/></linearGradient></defs><path fill="#50e6ff" d="m1.01 10.19 7.92 5.14 8.06-5.16L18 11.35l-9.07 5.84L0 11.35l1.01-1.16z"/><path fill="#fff" d="M1.61 9.53 8.93.81l7.47 8.73-7.47 4.72-7.32-4.73z"/><path fill="#50e6ff" d="M8.93.81v13.45L1.61 9.53 8.93.81z"/><path fill="url(#gitea-azuread__a)" d="M8.93.81v13.45l7.47-4.72L8.93.81z"/><path fill="#53b1e0" d="m8.93 7.76 7.47 1.78-7.47 4.72v-6.5z"/><path fill="#9cebff" d="M8.93 14.26 1.61 9.53l7.32-1.77v6.5z"/><path fill="url(#gitea-azuread__b)" d="M8.93 17.19 18 11.35l-1.01-1.18-8.06 5.16v1.86z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-azureadv2.svg b/web_src/svg/gitea-azureadv2.svg
new file mode 100644
index 0000000..6b69abc
--- /dev/null
+++ b/web_src/svg/gitea-azureadv2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" class="svg gitea-azuread" width="16" height="16" aria-hidden="true"><defs><linearGradient id="gitea-azuread__a" x1="13.25" x2="8.62" y1="13.02" y2="4.25" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1988d9"/><stop offset=".9" stop-color="#54aef0"/></linearGradient><linearGradient id="gitea-azuread__b" x1="11.26" x2="14.46" y1="10.47" y2="15.99" gradientUnits="userSpaceOnUse"><stop offset=".1" stop-color="#54aef0"/><stop offset=".29" stop-color="#4fabee"/><stop offset=".51" stop-color="#41a2e9"/><stop offset=".74" stop-color="#2a93e0"/><stop offset=".88" stop-color="#1988d9"/></linearGradient></defs><path fill="#50e6ff" d="m1.01 10.19 7.92 5.14 8.06-5.16L18 11.35l-9.07 5.84L0 11.35l1.01-1.16z"/><path fill="#fff" d="M1.61 9.53 8.93.81l7.47 8.73-7.47 4.72-7.32-4.73z"/><path fill="#50e6ff" d="M8.93.81v13.45L1.61 9.53 8.93.81z"/><path fill="url(#gitea-azuread__a)" d="M8.93.81v13.45l7.47-4.72L8.93.81z"/><path fill="#53b1e0" d="m8.93 7.76 7.47 1.78-7.47 4.72v-6.5z"/><path fill="#9cebff" d="M8.93 14.26 1.61 9.53l7.32-1.77v6.5z"/><path fill="url(#gitea-azuread__b)" d="M8.93 17.19 18 11.35l-1.01-1.18-8.06 5.16v1.86z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-bitbucket.svg b/web_src/svg/gitea-bitbucket.svg
new file mode 100644
index 0000000..ac490c9
--- /dev/null
+++ b/web_src/svg/gitea-bitbucket.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42"><defs><linearGradient id="a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.71 2.71 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-cargo.svg b/web_src/svg/gitea-cargo.svg
new file mode 100644
index 0000000..dbec107
--- /dev/null
+++ b/web_src/svg/gitea-cargo.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
+<path d="m15.993 1.54c-7.972 0-14.461 6.492-14.461 14.462 0 7.969 6.492 14.461 14.461 14.461 7.97 0 14.462-6.492 14.462-14.461 0-7.97-6.492-14.462-14.462-14.462zm-0.021 1.285c0.511 0.013 0.924 0.439 0.924 0.951 0 0.522-0.43 0.952-0.952 0.952s-0.951-0.43-0.951-0.952 0.429-0.952 0.951-0.952c0.01 0 0.019 1e-3 0.028 1e-3zm2.178 1.566c3.379 0.633 6.313 2.723 8.016 5.709l-1.123 2.533c-0.193 0.438 6e-3 0.952 0.44 1.147l2.16 0.958c0.067 0.675 0.076 1.355 0.025 2.031h-1.202c-0.12 0-0.169 0.08-0.169 0.196v0.551c0 1.297-0.731 1.582-1.373 1.652-0.612 0.07-1.288-0.257-1.374-0.63-0.361-2.029-0.961-2.46-1.909-3.21 1.178-0.746 2.401-1.85 2.401-3.325 0-1.594-1.092-2.597-1.835-3.09-1.046-0.688-2.203-0.826-2.515-0.826h-12.421c1.717-1.918 4.02-3.218 6.55-3.696l1.466 1.536c0.33 0.346 0.878 0.361 1.223 0.028l1.64-1.564zm-13.522 7.043c0.511 0.015 0.924 0.44 0.924 0.951 0 0.522-0.43 0.952-0.952 0.952s-0.951-0.43-0.951-0.952 0.429-0.951 0.951-0.951h0.028zm22.685 0.043c0.511 0.015 0.924 0.44 0.924 0.951 0 0.522-0.43 0.952-0.952 0.952s-0.951-0.43-0.951-0.952 0.429-0.952 0.951-0.952c0.01 0 0.019 0 0.028 1e-3zm-20.892 0.153h1.658v7.477h-3.347c-0.414-1.452-0.542-2.97-0.38-4.47l2.05-0.912c0.438-0.195 0.637-0.706 0.441-1.144l-0.422-0.951zm6.92 0.079h3.949c0.205 0 1.441 0.236 1.441 1.163 0 0.768-0.948 1.043-1.728 1.043h-3.665l3e-3 -2.206zm0 5.373h3.026c0.275 0 1.477 0.079 1.86 1.615 0.119 0.471 0.385 2.007 0.566 2.499 0.18 0.551 0.911 1.652 1.691 1.652h4.938c-0.331 0.444-0.693 0.863-1.083 1.255l-2.01-0.432c-0.468-0.101-0.93 0.199-1.031 0.667l-0.477 2.228c-3.104 1.406-6.672 1.389-9.762-0.046l-0.478-2.228c-0.101-0.468-0.56-0.767-1.028-0.667l-1.967 0.423c-0.365-0.377-0.704-0.778-1.016-1.2h9.567c0.107 0 0.181-0.018 0.181-0.119v-3.384c0-0.097-0.074-0.119-0.181-0.119h-2.799l3e-3 -2.144zm-4.415 7.749c0.512 0.015 0.924 0.44 0.924 0.951 0 0.522-0.429 0.952-0.951 0.952s-0.952-0.43-0.952-0.952 0.43-0.952 0.952-0.952c9e-3 0 0.018 1e-3 0.027 1e-3zm14.089 0.043c0.511 0.015 0.924 0.439 0.923 0.951 0 0.522-0.429 0.952-0.951 0.952s-0.951-0.43-0.951-0.952 0.429-0.952 0.951-0.952c9e-3 0 0.018 0 0.028 1e-3z"/><path d="m29.647 16.002c0 7.49-6.163 13.653-13.654 13.653-7.49 0-13.654-6.163-13.654-13.653 0-7.491 6.164-13.654 13.654-13.654 7.491 0 13.654 6.163 13.654 13.654zm-0.257-1.319 2.13 1.319-2.13 1.318 1.83 1.71-2.344 0.878 1.463 2.035-2.475 0.404 1.04 2.282-2.506-0.089 0.575 2.442-2.441-0.576 0.089 2.506-2.283-1.04-0.403 2.475-2.035-1.462-0.878 2.343-1.71-1.829-1.319 2.129-1.318-2.129-1.71 1.829-0.878-2.343-2.035 1.462-0.404-2.475-2.282 1.04 0.089-2.506-2.442 0.576 0.575-2.442-2.505 0.089 1.04-2.282-2.475-0.404 1.462-2.035-2.343-0.878 1.829-1.71-2.129-1.318 2.129-1.319-1.829-1.71 2.343-0.878-1.462-2.035 2.475-0.404-1.04-2.282 2.505 0.089-0.575-2.441 2.442 0.575-0.089-2.506 2.282 1.04 0.404-2.475 2.035 1.463 0.878-2.344 1.71 1.83 1.318-2.13 1.319 2.13 1.71-1.83 0.878 2.344 2.035-1.463 0.403 2.475 2.283-1.04-0.089 2.506 2.441-0.575-0.575 2.441 2.506-0.089-1.04 2.282 2.475 0.404-1.463 2.035 2.344 0.878-1.83 1.71z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-chef.svg b/web_src/svg/gitea-chef.svg
new file mode 100644
index 0000000..ce318c6
--- /dev/null
+++ b/web_src/svg/gitea-chef.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
+<g fill="none" fill-rule="evenodd">
+<g transform="translate(-54 -32)">
+<path d="m72 57.8c-4.3 0-7.7-3.6-7.7-8s3.4-7.9 7.7-7.9c3.5 0 6.4 2.4 7.3 5.7h3c-1-5-5.2-8.7-10.3-8.7-5.9 0-10.6 4.9-10.6 10.9 0 6.1 4.7 11 10.6 11 5.1 0 9.3-3.7 10.3-8.7h-3c-0.9 3.3-3.8 5.7-7.3 5.7" fill="#435363"/>
+<path d="m66.8 55.2c1.3 1.4 3.1 2.3 5.2 2.3v-3.2c-1.2 0-2.3-0.5-3.1-1.3l-2.1 2.2" fill="#435363"/>
+<path d="m64.6 49.8c0 1.1 0.3 2.2 0.6 3.1l2.9-1.3c-0.3-0.5-0.4-1.1-0.4-1.8 0-2.4 1.9-4.4 4.3-4.4v-3.2c-4.1 0-7.4 3.4-7.4 7.6" fill="#F38B00"/>
+<path d="m74.6 42.7-1.1 3c0.9 0.4 1.7 1.1 2.2 1.9h3.3c-0.7-2.2-2.3-4-4.4-4.9" fill="#435363"/>
+<path d="m73.5 54 1.1 2.9c2.1-0.8 3.7-2.6 4.4-4.8h-3.3c-0.5 0.8-1.3 1.5-2.2 1.9" fill="#F38B00"/>
+<path d="m58.4 54.1c-0.1-0.2-0.1-0.3-0.1-0.5-0.1-0.2-0.1-0.3-0.2-0.5v-0.1c0-0.1 0-0.3-0.1-0.4v-0.2-0.3c-0.1-0.1-0.1-0.2-0.1-0.3-0.1-0.6-0.1-1.3-0.1-2h-2.9c0 0.8 0 1.5 0.1 2.2 0 0.2 0.1 0.4 0.1 0.6v0.1c0 0.2 0.1 0.4 0.1 0.5s0 0.2 0.1 0.3v0.3c0.1 0.1 0.1 0.2 0.1 0.4 0 0 0.1 0.1 0.1 0.2 0 0.2 0 0.3 0.1 0.4v0.2c0.2 0.7 0.5 1.3 0.7 2l2.7-1.2c-0.2-0.6-0.4-1.1-0.6-1.7" fill="#435363"/>
+<path d="m72 64.6c-3.9 0-7.5-1.7-10.1-4.4l-2 2.2c3.1 3.2 7.3 5.2 12.1 5.2 8.7 0 15.8-6.8 16.9-15.5h-2.9c-1.1 7-7 12.5-14 12.5" fill="#F38B00"/>
+<path d="m72 35.1c3.1 0 6.1 1.1 8.4 2.9l1.8-2.4c-2.9-2.2-6.4-3.5-10.2-3.5-7.3 0-13.5 4.7-15.9 11.3l2.7 1.1c2-5.5 7.2-9.4 13.2-9.4" fill="#F38B00"/>
+<path d="m86 47.6h2.9c-0.3-2.6-1.2-5-2.5-7.2l-2.4 1.6c1 1.7 1.7 3.6 2 5.6" fill="#435363"/>
+<path d="m82.7 47.6h2.9c-0.8-5.1-4.1-9.3-8.6-11.1l-1.1 2.8c3.5 1.3 6 4.5 6.8 8.3" fill="#F38B00"/>
+<path d="m72 38.5v-3c-5.9 0-10.9 3.8-12.9 9.1l2.7 1.1c1.6-4.2 5.5-7.2 10.2-7.2" fill="#435363"/>
+<path d="m61 49.8h-2.9c0 6.1 3.6 11.2 8.7 13.4l1.1-2.8c-4-1.7-6.9-5.8-6.9-10.6" fill="#F38B00"/>
+<path d="m72 61.2v3c6.9 0 12.6-5.3 13.6-12.1h-2.9c-1 5.2-5.4 9.1-10.7 9.1" fill="#435363"/>
+</g>
+</g>
+</svg>
diff --git a/web_src/svg/gitea-codebase.svg b/web_src/svg/gitea-codebase.svg
new file mode 100644
index 0000000..13c67e7
--- /dev/null
+++ b/web_src/svg/gitea-codebase.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2516.000000 543.000000">
+<g transform="translate(0.000000,543.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
+<path d="M7600 4219 l0 -800 -62 72 c-114 132 -302 257 -483 322 -134 47 -226 61 -410 61 -245 0 -390 -33 -600 -133 -288 -138 -549 -431 -674 -758 l-21 -53 330 -334 c274 -277 330 -338 330 -361 0 -24 -57 -85 -341 -371 l-342 -343 28 -78 c173 -492 574 -839 1060 -919 122 -19 361 -15 475 10 242 53 448 165 622 338 l88 87 0 -184 0 -185 505 0 505 0 0 2215 0 2215 -505 0 -505 0 0 -801z m-432 -1269 c145 -35 274 -104 380 -203 l52 -50 0 -498 0 -498 -51 -50 c-68 -67 -142 -115 -247 -161 -115 -49 -210 -70 -326 -70 -126 0 -182 13 -296 67 -206 99 -351 308 -391 564 -16 105 -6 305 21 399 28 98 88 211 154 292 83 100 174 160 311 204 96 31 274 33 393 4z"/>
+<path d="M12890 2805 l0 -2215 175 0 175 0 0 251 0 251 42 -54 c209 -266 544 -465 867 -513 442 -67 875 72 1171 375 150 153 290 387 360 599 136 415 134 1002 -6 1402 -175 504 -544 846 -1024 950 -147 32 -416 32 -567 0 -314 -66 -581 -233 -784 -491 l-64 -82 -3 871 -2 871 -170 0 -170 0 0 -2215z m1550 754 c122 -15 193 -35 296 -84 178 -83 312 -198 419 -360 147 -220 222 -457 247 -775 25 -326 -32 -659 -154 -905 -153 -307 -373 -495 -676 -577 -96 -26 -330 -35 -456 -19 -259 35 -537 173 -726 361 -48 47 -103 110 -123 140 l-37 55 0 791 0 791 49 69 c94 134 258 272 441 372 226 123 475 172 720 141z"/>
+<path d="M1675 3874 c-292 -30 -497 -87 -702 -194 -434 -226 -728 -614 -829 -1094 -35 -164 -44 -482 -20 -657 50 -357 186 -643 420 -887 213 -221 434 -358 730 -452 282 -89 676 -105 984 -40 198 42 416 131 576 238 71 47 264 230 304 288 l22 32 -328 309 -327 308 -95 -95 c-75 -76 -111 -104 -171 -133 -112 -55 -211 -77 -344 -77 -359 0 -637 213 -726 556 -26 101 -29 325 -6 425 62 261 230 451 476 537 68 23 94 27 236 30 145 3 168 1 241 -21 123 -35 223 -98 312 -193 l76 -81 327 305 c268 249 326 308 319 322 -18 33 -151 173 -218 229 -137 115 -302 202 -501 265 -193 61 -301 78 -521 81 -107 2 -213 1 -235 -1z"/>
+<path d="M10427 3869 c-334 -32 -684 -182 -911 -390 -226 -208 -386 -446 -480 -718 -70 -200 -80 -275 -80 -556 0 -267 8 -343 59 -526 101 -358 321 -657 645 -874 318 -214 703 -308 1156 -285 212 11 374 37 572 92 263 72 428 153 621 307 l25 19 -223 335 -223 335 -81 -55 c-103 -71 -194 -113 -327 -153 -152 -45 -255 -60 -405 -60 -164 0 -279 26 -423 96 -80 40 -111 62 -177 128 -82 82 -146 187 -161 264 l-6 32 1136 0 1136 0 0 210 c0 313 -30 523 -106 742 -86 245 -211 444 -396 624 -259 254 -565 396 -937 433 -118 12 -285 12 -414 0z m527 -843 c205 -57 361 -211 410 -406 l13 -55 -650 -3 c-358 -1 -652 0 -655 3 -10 10 30 131 63 190 87 156 224 251 413 285 74 14 338 4 406 -14z"/>
+<path d="M17330 3870 c-254 -31 -500 -123 -715 -266 -82 -55 -219 -168 -277 -230 l-27 -29 85 -110 c47 -61 89 -114 93 -118 5 -5 48 32 97 80 160 159 371 280 594 338 90 24 131 28 282 32 237 7 340 -12 504 -94 156 -77 264 -181 330 -318 64 -135 67 -151 71 -572 3 -211 2 -383 -2 -383 -4 0 -42 33 -84 73 -182 173 -433 299 -680 343 -131 23 -351 34 -460 23 -376 -38 -729 -264 -879 -564 -39 -78 -77 -200 -92 -296 -19 -116 -8 -383 19 -482 87 -326 330 -583 671 -710 154 -57 232 -70 425 -70 405 -1 727 120 999 375 l86 80 0 -191 0 -191 170 0 170 0 0 1183 c0 978 -3 1198 -15 1277 -42 282 -163 478 -382 622 -210 139 -420 197 -733 203 -96 2 -209 0 -250 -5z m260 -1510 c299 -37 562 -171 731 -373 l49 -59 0 -355 0 -355 -102 -102 c-210 -209 -446 -308 -774 -325 -240 -13 -375 9 -528 85 -101 51 -153 89 -237 175 -87 88 -156 214 -185 334 -24 99 -24 300 -1 385 82 307 349 537 687 593 52 9 279 7 360 -3z"/>
+<path d="M23345 3873 c-382 -35 -739 -228 -988 -535 -327 -405 -450 -883 -372 -1447 53 -374 201 -679 454 -932 120 -119 234 -204 370 -273 133 -67 216 -97 361 -129 508 -113 1004 -18 1402 266 102 73 240 195 236 209 -8 22 -164 217 -175 217 -6 1 -18 -7 -25 -17 -26 -36 -144 -136 -223 -187 -161 -106 -342 -178 -529 -212 -131 -24 -422 -24 -536 1 -228 48 -414 145 -577 299 -211 201 -358 497 -398 804 -8 63 -15 123 -15 134 0 19 18 19 1340 19 l1340 0 0 118 c0 416 -124 816 -346 1114 -77 103 -237 258 -331 321 -144 96 -315 167 -483 201 -144 29 -367 42 -505 29z m351 -304 c171 -24 345 -93 476 -191 186 -138 348 -378 428 -637 31 -99 60 -263 60 -338 l0 -43 -1166 0 -1166 0 7 63 c49 452 288 841 636 1036 78 43 206 89 297 105 94 17 323 20 428 5z"/>
+<path d="M20115 3855 c-194 -30 -394 -107 -515 -197 -256 -193 -369 -419 -358 -717 4 -96 10 -131 35 -202 55 -159 160 -283 336 -396 145 -94 259 -133 737 -252 377 -94 468 -125 626 -211 196 -108 283 -240 284 -430 0 -299 -229 -540 -585 -615 -121 -26 -377 -31 -495 -11 -134 23 -248 60 -375 120 -138 66 -235 134 -348 245 l-88 86 -22 -26 c-12 -15 -59 -72 -104 -127 l-83 -100 99 -95 c214 -205 463 -330 779 -389 100 -19 153 -22 357 -22 258 0 331 8 505 61 429 129 690 463 690 885 0 227 -81 422 -238 570 -202 190 -337 247 -962 404 -284 71 -388 107 -544 188 -184 95 -256 190 -264 350 -8 159 38 279 153 392 84 83 207 150 340 185 110 30 395 38 540 15 275 -42 476 -142 639 -318 66 -71 78 -80 91 -67 7 8 52 61 100 119 l86 105 -73 69 c-228 215 -475 333 -799 380 -128 19 -427 20 -544 1z"/>
+<path d="M3736 3569 c-48 -50 -353 -363 -679 -695 -511 -523 -592 -609 -592 -635 0 -25 91 -122 683 -724 l683 -695 115 120 c63 65 124 130 135 144 11 14 26 26 32 26 7 0 77 -66 158 -146 l146 -146 689 696 c514 519 690 702 692 722 3 23 -27 58 -220 253 -123 124 -434 439 -691 700 l-466 474 -155 -147 -154 -146 -138 145 c-75 80 -141 145 -145 145 -3 0 -46 -41 -93 -91z m728 -1008 l315 -324 -316 -324 c-174 -178 -320 -322 -324 -321 -4 2 -156 147 -337 323 l-331 320 327 328 c180 180 332 326 339 325 7 -2 154 -149 327 -327z" fill="#e22c2c"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-composer.svg b/web_src/svg/gitea-composer.svg
new file mode 100644
index 0000000..79925d3
--- /dev/null
+++ b/web_src/svg/gitea-composer.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 711.2 383.6" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <clipPath id="d">
+ <path d="m11.52 162c0-80.323 123.79-145.44 276.48-145.44s276.48 65.116 276.48 145.44c0 80.322-123.79 145.44-276.48 145.44s-276.48-65.117-276.48-145.44"/>
+ </clipPath>
+ <radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(363.06 0 0 -363.06 177.52 256.31)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#aeb2d5" offset="0"/>
+ <stop stop-color="#aeb2d5" offset=".3"/>
+ <stop stop-color="#484c89" offset=".75"/>
+ <stop stop-color="#484c89" offset="1"/>
+ </radialGradient>
+ <clipPath id="c">
+ <path d="m0 324h576v-324h-576v324z"/>
+ </clipPath>
+ <clipPath id="b">
+ <path d="m0 324h576v-324h-576v324z"/>
+ </clipPath>
+ </defs>
+ <g transform="matrix(1.25 0 0 -1.25 -4.4 394.3)">
+ <g clip-path="url(#d)">
+ <path d="m11.52 162c0-80.323 123.79-145.44 276.48-145.44s276.48 65.116 276.48 145.44c0 80.322-123.79 145.44-276.48 145.44s-276.48-65.117-276.48-145.44" fill="url(#a)"/>
+ </g>
+ <g clip-path="url(#c)">
+ <g transform="translate(288 27.359)">
+ <path d="m0 0c146.73 0 265.68 60.281 265.68 134.64 0 74.359-118.95 134.64-265.68 134.64s-265.68-60.282-265.68-134.64c0-74.36 118.95-134.64 265.68-134.64" fill="#777bb3"/>
+ </g>
+ </g>
+ <g clip-path="url(#b)">
+ <g transform="translate(161.73 145.31)">
+ <path d="m0 0c12.065 0 21.072 2.225 26.771 6.611 5.638 4.341 9.532 11.862 11.573 22.353 1.903 9.806 1.178 16.653-2.154 20.348-3.407 3.774-10.773 5.688-21.893 5.688h-19.281l-10.689-55h15.673zm-63.063-67.75c-0.895 0-1.745 0.4-2.314 1.092-0.57 0.691-0.801 1.601-0.63 2.48l28.328 145.75c0.274 1.409 1.509 2.427 2.945 2.427h61.054c19.188 0 33.47-5.21 42.447-15.487 9.025-10.331 11.812-24.772 8.283-42.921-1.436-7.394-3.906-14.261-7.341-20.409-3.439-6.155-7.984-11.85-13.511-16.93-6.616-6.192-14.104-10.682-22.236-13.324-8.003-2.607-18.281-3.929-30.548-3.929h-24.722l-7.06-36.322c-0.274-1.41-1.508-2.428-2.944-2.428h-31.751z"/>
+ </g>
+ <g transform="translate(159.22 197.31)">
+ <path d="m0 0h16.808c13.421 0 18.083-2.945 19.667-4.7 2.628-2.914 3.124-9.058 1.435-17.767-1.898-9.75-5.416-16.663-10.458-20.545-5.162-3.974-13.554-5.988-24.941-5.988h-12.034l9.523 49zm28.831 35h-61.055c-2.872 0-5.341-2.036-5.889-4.855l-28.328-145.75c-0.342-1.759 0.12-3.578 1.259-4.961 1.14-1.383 2.838-2.183 4.63-2.183h31.75c2.873 0 5.342 2.036 5.89 4.855l6.588 33.895h22.249c12.582 0 23.174 1.372 31.479 4.077 8.541 2.775 16.399 7.48 23.354 13.984 5.752 5.292 10.49 11.232 14.08 17.657 3.591 6.427 6.171 13.594 7.668 21.302 3.715 19.104 0.697 34.402-8.969 45.466-9.572 10.958-24.614 16.514-44.706 16.514m-45.633-90h19.313c12.801 0 22.336 2.411 28.601 7.234 6.266 4.824 10.492 12.875 12.688 24.157 2.101 10.832 1.144 18.476-2.871 22.929-4.02 4.453-12.059 6.68-24.121 6.68h-21.754l-11.856-61m45.633 84c18.367 0 31.766-4.82 40.188-14.461 8.421-9.641 10.957-23.098 7.597-40.375-1.383-7.117-3.722-13.624-7.015-19.519-3.297-5.899-7.602-11.293-12.922-16.184-6.34-5.933-13.383-10.161-21.133-12.679-7.75-2.525-17.621-3.782-29.621-3.782h-27.196l-7.531-38.75h-31.75l28.328 145.75h61.055" fill="#fff"/>
+ </g>
+ <g transform="translate(311.58 116.31)">
+ <path d="m0 0c-0.896 0-1.745 0.4-2.314 1.092-0.571 0.691-0.802 1.6-0.631 2.48l12.531 64.489c1.192 6.133 0.898 10.535-0.827 12.395-1.056 1.137-4.228 3.044-13.607 3.044h-22.702l-15.755-81.072c-0.274-1.41-1.509-2.428-2.945-2.428h-31.5c-0.896 0-1.745 0.4-2.315 1.092-0.57 0.691-0.801 1.601-0.63 2.48l28.328 145.75c0.274 1.409 1.509 2.427 2.945 2.427h31.5c0.896 0 1.745-0.4 2.315-1.091 0.57-0.692 0.801-1.601 0.63-2.481l-6.836-35.178h24.422c18.605 0 31.221-3.28 38.569-10.028 7.49-6.884 9.827-17.891 6.947-32.719l-13.18-67.825c-0.274-1.41-1.508-2.428-2.945-2.428h-32z"/>
+ </g>
+ <g transform="translate(293.66 271.06)">
+ <path d="m0 0h-31.5c-2.873 0-5.342-2.036-5.89-4.855l-28.328-145.75c-0.342-1.759 0.12-3.578 1.26-4.961s2.838-2.183 4.63-2.183h31.5c2.872 0 5.342 2.036 5.89 4.855l15.283 78.645h20.229c9.363 0 11.328-2 11.407-2.086 0.568-0.611 1.315-3.441 0.082-9.781l-12.531-64.489c-0.342-1.759 0.12-3.578 1.26-4.961s2.838-2.183 4.63-2.183h32c2.872 0 5.342 2.036 5.89 4.855l13.179 67.825c3.093 15.921 0.447 27.864-7.861 35.5-7.928 7.281-21.208 10.82-40.599 10.82h-20.784l6.143 31.605c0.341 1.759-0.12 3.579-1.26 4.961-1.14 1.383-2.838 2.184-4.63 2.184m0-6-7.531-38.75h28.062c17.657 0 29.836-3.082 36.539-9.238 6.703-6.16 8.711-16.141 6.032-29.938l-13.18-67.824h-32l12.531 64.488c1.426 7.336 0.902 12.34-1.574 15.008-2.477 2.668-7.746 4.004-15.805 4.004h-25.176l-16.226-83.5h-31.5l28.328 145.75h31.5" fill="#fff"/>
+ </g>
+ <g transform="translate(409.55 145.31)">
+ <path d="m0 0c12.065 0 21.072 2.225 26.771 6.611 5.638 4.34 9.532 11.861 11.574 22.353 1.903 9.806 1.178 16.653-2.155 20.348-3.407 3.774-10.773 5.688-21.893 5.688h-19.281l-10.689-55h15.673zm-63.062-67.75c-0.895 0-1.745 0.4-2.314 1.092-0.57 0.691-0.802 1.601-0.631 2.48l28.328 145.75c0.275 1.409 1.509 2.427 2.946 2.427h61.053c19.189 0 33.47-5.21 42.448-15.487 9.025-10.33 11.811-24.771 8.283-42.921-1.438-7.394-3.907-14.261-7.342-20.409-3.439-6.155-7.984-11.85-13.511-16.93-6.616-6.192-14.104-10.682-22.236-13.324-8.003-2.607-18.281-3.929-30.548-3.929h-24.723l-7.057-36.322c-0.275-1.41-1.509-2.428-2.946-2.428h-31.75z"/>
+ </g>
+ <g transform="translate(407.04 197.31)">
+ <path d="m0 0h16.808c13.421 0 18.083-2.945 19.667-4.7 2.629-2.914 3.125-9.058 1.435-17.766-1.898-9.751-5.417-16.664-10.458-20.546-5.162-3.974-13.554-5.988-24.941-5.988h-12.033l9.522 49zm28.831 35h-61.054c-2.872 0-5.341-2.036-5.889-4.855l-28.328-145.75c-0.342-1.759 0.12-3.578 1.259-4.961 1.14-1.383 2.838-2.183 4.63-2.183h31.75c2.872 0 5.342 2.036 5.89 4.855l6.587 33.895h22.249c12.582 0 23.174 1.372 31.479 4.077 8.541 2.775 16.401 7.481 23.356 13.986 5.752 5.291 10.488 11.23 14.078 17.655 3.591 6.427 6.171 13.594 7.668 21.302 3.715 19.105 0.697 34.403-8.969 45.467-9.572 10.957-24.613 16.513-44.706 16.513m-45.632-90h19.312c12.801 0 22.336 2.411 28.601 7.234 6.267 4.824 10.492 12.875 12.688 24.157 2.102 10.832 1.145 18.476-2.871 22.929-4.02 4.453-12.059 6.68-24.121 6.68h-21.754l-11.855-61m45.632 84c18.367 0 31.766-4.82 40.188-14.461s10.957-23.098 7.597-40.375c-1.383-7.117-3.722-13.624-7.015-19.519-3.297-5.899-7.602-11.293-12.922-16.184-6.34-5.933-13.383-10.161-21.133-12.679-7.75-2.525-17.621-3.782-29.621-3.782h-27.196l-7.53-38.75h-31.75l28.328 145.75h61.054" fill="#fff"/>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/web_src/svg/gitea-conan.svg b/web_src/svg/gitea-conan.svg
new file mode 100644
index 0000000..f1719ce
--- /dev/null
+++ b/web_src/svg/gitea-conan.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="147 6 105 106" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
+ <path d="m198.7 59.75-51.08-29.62v47.49l51.08 33.65z" fill="#6699cb"/>
+ <clipPath id="a"><path d="m147.49 30.14 51.21 29.61 51.08-27.24-52.39-25.78z"/></clipPath>
+ <path d="m147.49 6.73h102.3v53.01h-102.3z" clip-path="url(#a)" fill="#afd5e6"/>
+ <path d="m198.7 59.75 51.08-27.24v47.48l-51.08 31.28z" clip-rule="evenodd" fill="#7ba7d3" fill-rule="evenodd"/>
+ <path d="m198.93 19.49-2.96.33-.43.18-.47.01-.42.18-2.31.55-.33.14-.31.01-.28.23-4.27 1.58-.22.17c-1.93.75-3.49 1.8-5.16 2.66l-.19.2c-1.5.84-2.03 1.28-3.08 2.32l-.25.17-1.06 1.42-.21.18-.35.71-.19.2c-1.2 2.75-1.18 3.19-.93 6.4l.21.32v.33l.15.29.4.99.17.23.18.51.21.18c.61 1.1 1.37 1.97 2.1 2.77.41.45 2.16 1.87 2.85 2.22l.19.21c1.4.67 2.44 1.51 4.22 2.13l.24.16 3.45 1.08.39.19c1.19.13 2.44.48 3.76.65 1.44.19 2.2-.5 3.4-1.02l.23-.17h.16l.23-.17 5.47-2.52.23-.17h.16l.23-.17 3.15-1.49-.28-.12c-1.85-.08-4.04.2-6.04.15-2.01-.05-3.87-.42-5.71-.5l-.39-.19c-1.33-.13-2.66-.69-3.81-1.08l-.25-.16c-1.85-.66-3.55-2.12-4.35-3.63-1.27-2.4-.48-4.18.48-6.21l.21-.18.17-.33.22-.18c.99-1.41 3.43-3.37 5.83-4.13l.25-.16 2.54-.72.37-.19.39.02.39-.19 1.69-.14c.41-.27.62-.23 1.2-.24h3.93c.62-.02 1.16-.02 1.6.23l2.29.31.28.22c1.39.2 2.55.97 3.72 1.4l.2.19.73.34.19.2c1.23.65 3.41 2.65 3.87 4.24l.16.26c.52 1.8.39 2.4-.01 4.17l-.16.33-.64 1.38.96-.39.21-.18 7.56-3.91.21-.18 1.81-.89.21-.18 1.81-.89.21-.2c.07-.39-2.27-2.32-2.77-2.79l-.18-.25c-.61-.52-1.49-1.28-2.21-1.73l-.18-.22c-.72-.41-1.33-1.05-2.03-1.39l-.19-.2-1.83-1.05-.19-.2-2.38-1.24-.23-.17-3.07-1.27-.26-.16-1.85-.52-.29-.22h-.32l-.36-.16h-.34l-.32-.21c-1.51-.14-3.17-.63-4.86-.79-2.03-.18-4.01.05-5.83-.11l-.72.22z" fill="#6699cb"/>
+ <path d="m225.14 45.65 1.91-1.02v49.28l-1.91 1.17z" clip-rule="evenodd" fill="#2f6799" fill-rule="evenodd"/>
+</svg>
diff --git a/web_src/svg/gitea-conda.svg b/web_src/svg/gitea-conda.svg
new file mode 100644
index 0000000..c5797c8
--- /dev/null
+++ b/web_src/svg/gitea-conda.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.559,8.137a7.2,7.2,0,0,0-1.234-1.708,7.586,7.586,0,0,0-.19,2.183,5.161,5.161,0,0,1,1.424-.475Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M13.617,9.466a7.992,7.992,0,0,0-1.993-1.2,8.123,8.123,0,0,0,.885,2.183c0,.063.443-.475,1.108-.981Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M17.445,7.188a9.143,9.143,0,0,1,1.3-2.246A7.585,7.585,0,0,0,17,2.854a8.35,8.35,0,0,0-1.3,2.278,8.451,8.451,0,0,1,1.74,2.056Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="m11.592 11.744a10.276 10.276 0 0 0-2.692-0.158 7.478 7.478 0 0 0 1.93 1.9 6.858 6.858 0 0 1 0.759-1.74z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="m6.878 15.161a7.44 7.44 0 0 1 2.942-1.139 10.019 10.019 0 0 1-2.056-2.278 7.639 7.639 0 0 0-2.847 1.2 7.11 7.11 0 0 0 1.961 2.215z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="m10.516 14.876a6.16 6.16 0 0 0-2.815 0.886 9.936 9.936 0 0 0 2.815 1.2 7.683 7.683 0 0 1 0-2.088z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M14.281,5.543A7.839,7.839,0,0,0,11.592,4.4,8.361,8.361,0,0,0,11.4,7,8.875,8.875,0,0,1,13.87,8.264a10.292,10.292,0,0,1,.411-2.721Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M24.025,3.234a20.488,20.488,0,0,1,.917,4.112,6.823,6.823,0,0,0-3.068,1.519,7.443,7.443,0,0,1,1.55,1.044,1.351,1.351,0,0,0,1.645.316,36.938,36.938,0,0,0,2.721-2.72,1.273,1.273,0,0,0-.159-1.835,20.521,20.521,0,0,0-3.606-2.436Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M4.379,12.06A8.67,8.67,0,0,1,7.226,10.8a7.763,7.763,0,0,1-.759-2.974A14.687,14.687,0,0,0,4.379,12.06Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M11.339,10.668a9.991,9.991,0,0,1-.949-2.784,7.928,7.928,0,0,0-2.911-.126,7.312,7.312,0,0,0,.791,2.879,9.664,9.664,0,0,1,3.069.031Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M6.119,15.73a8.894,8.894,0,0,1-2.025-2.373,14.208,14.208,0,0,0-.063,4.9A8.522,8.522,0,0,1,6.119,15.73Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M22.538,3.487A7.581,7.581,0,0,0,20.323,5.1a11.789,11.789,0,0,1,.823,2.5,9.775,9.775,0,0,1,2.309-1.329,6.593,6.593,0,0,0-.917-2.784Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M19.374,6.3a8.608,8.608,0,0,0-.822,1.676h0a9.645,9.645,0,0,1,1.329.19A7.568,7.568,0,0,0,19.374,6.3Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M19.659,3.9a9.577,9.577,0,0,1,2.056-1.487A15.38,15.38,0,0,0,18.046,2a9.709,9.709,0,0,1,1.613,1.9Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068489px"/>
+<path d="M27.378,23.892c-1.993-1.9-2.4-3.132-4.081-1.835a7.837,7.837,0,0,1-12.591-4.144A10.179,10.179,0,0,1,6.878,16.3a9.427,9.427,0,0,0-2.562,3.321H4.284C7.163,30.5,21.178,33.035,27.663,26.233,28.739,25.094,27.758,24.3,27.378,23.892ZM6.309,20.855a7.559,7.559,0,0,1,.917-2.025,6.872,6.872,0,0,0,2.151.538c1.013,2.689,4.556,6.264,8.922,6.264a9.632,9.632,0,0,0,6.3-2.309,12.841,12.841,0,0,1,1.772,1.771c.095.127.095.159.095.159C20.7,30.283,10.928,29.555,6.309,20.855Z" fill="#43b02a"/>
+<path d="M10.67,4.11a19.934,19.934,0,0,0-.214,2.509,10.512,10.512,0,0,0-2.689-.093A18,18,0,0,1,10.67,4.11Z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".066605px"/>
+<path d="m12.26 3.274a9.107 9.107 0 0 1 2.445 1.053 14.083 14.083 0 0 1 1.253-2.137 12.106 12.106 0 0 0-3.698 1.084z" fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".066605px"/>
+</svg>
diff --git a/web_src/svg/gitea-cran.svg b/web_src/svg/gitea-cran.svg
new file mode 100644
index 0000000..41d98aa
--- /dev/null
+++ b/web_src/svg/gitea-cran.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg preserveAspectRatio="xMidYMid" viewBox="0.88 3 721.12 556.07" xmlns="http://www.w3.org/2000/svg">
+<defs>
+<linearGradient id="b" y2="1">
+<stop stop-color="#cbced0" offset="0"/>
+<stop stop-color="#84838b" offset="1"/>
+</linearGradient>
+<linearGradient id="a" y2="1">
+<stop stop-color="#276dc3" offset="0"/>
+<stop stop-color="#165caa" offset="1"/>
+</linearGradient>
+</defs>
+<path d="m361.45 485.94c-199.12 0-360.55-108.11-360.55-241.47 0-133.36 161.42-241.47 360.55-241.47 199.12 0 360.55 108.11 360.55 241.47 0 133.36-161.42 241.47-360.55 241.47zm55.188-388.53c-151.35 0-274.05 73.908-274.05 165.08s122.7 165.08 274.05 165.08c151.35 0 263.05-50.529 263.05-165.08 0-114.51-111.7-165.08-263.05-165.08z" fill="url(#b)" fill-rule="evenodd"/>
+<path d="m550 377s21.822 6.585 34.5 13c4.399 2.226 12.01 6.668 17.5 12.5 5.378 5.712 8 11.5 8 11.5l86 145-139 0.062-65-122.06s-13.31-22.869-21.5-29.5c-6.832-5.531-9.745-7.5-16.5-7.5h-33.026l0.026 158.97-123 0.052v-406.09h247s112.5 2.029 112.5 109.06-107.5 115-107.5 115zm-53.5-135.98-74.463-0.048-0.037 69.05 74.5-0.024s34.5-0.107 34.5-35.125c0-35.722-34.5-33.853-34.5-33.853z" fill="url(#a)" fill-rule="evenodd"/>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-debian.svg b/web_src/svg/gitea-debian.svg
new file mode 100644
index 0000000..4046f7f
--- /dev/null
+++ b/web_src/svg/gitea-debian.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" viewBox="0 0 210 260" xmlns="http://www.w3.org/2000/svg">
+<g transform="translate(60,75)" fill="#D70751">
+<path d="M64.525 62.053c-4.125.058.78 2.125 6.165 2.954 1.488-1.161 2.838-2.336 4.04-3.479-3.354.821-6.765.838-10.205.525m22.14-5.52c2.457-3.389 4.246-7.102 4.878-10.939-.551 2.736-2.035 5.099-3.435 7.592-7.711 4.854-.726-2.883-.004-5.824-8.29 10.436-1.138 6.257-1.439 9.171m8.174-21.265c.497-7.428-1.462-5.08-2.121-2.245.766.4 1.377 5.237 2.121 2.245M48.883-66.264c2.201.395 4.757.698 4.398 1.224 2.407-.528 2.954-1.015-4.398-1.224"/>
+<path d="m53.281-65.04-1.556.32 1.448-.127.108-.193"/>
+<path d="M121.93 38.085c.247 6.671-1.95 9.907-3.932 15.637l-3.564 1.781c-2.919 5.666.282 3.598-1.807 8.105-4.556 4.049-13.823 12.67-16.789 13.457-2.163-.047 1.469-2.554 1.943-3.537-6.097 4.188-4.894 6.285-14.217 8.83l-.273-.607C60.29 92.569 28.344 71.129 28.765 41.875c-.246 1.857-.698 1.393-1.208 2.144-1.186-15.052 6.952-30.17 20.675-36.343 13.427-6.646 29.163-3.918 38.78 5.044C81.73 5.8 71.217-1.534 58.757-.848c-12.208.193-23.625 7.95-27.436 16.369-6.253 3.938-6.979 15.177-9.704 17.233-3.665 26.943 6.896 38.583 24.762 52.275 2.812 1.896.792 2.184 1.173 3.627-5.936-2.779-11.372-6.976-15.841-12.114 2.372 3.473 4.931 6.847 8.239 9.499-5.596-1.897-13.074-13.563-15.256-14.038 9.647 17.274 39.142 30.295 54.587 23.836-7.146.263-16.226.146-24.256-2.822-3.371-1.734-7.958-5.331-7.14-6.003 21.079 7.875 42.854 5.965 61.09-8.655 4.641-3.614 9.709-9.761 11.173-9.846-2.206 3.317.377 1.596-1.318 4.523 4.625-7.456-2.008-3.035 4.779-12.877l2.507 3.453c-.931-6.188 7.687-13.704 6.813-23.492 1.975-2.994 2.206 3.22.107 10.107 2.912-7.64.767-8.867 1.516-15.171.81 2.118 1.867 4.37 2.412 6.606-1.895-7.382 1.948-12.433 2.898-16.724-.937-.415-2.928 3.264-3.383-5.457.065-3.788 1.054-1.985 1.435-2.917-.744-.427-2.694-3.33-3.88-8.9.86-1.308 2.3 3.393 3.47 3.586-.753-4.429-2.049-7.805-2.103-11.202-3.421-7.149-1.211.953-3.985-3.069-3.641-11.357 3.021-2.637 3.47-7.796 5.52 7.995 8.667 20.387 10.11 25.519-1.103-6.258-2.883-12.32-5.058-18.185 1.677.705-2.699-12.875 2.18-3.882-5.21-19.172-22.302-37.087-38.025-45.493 1.924 1.76 4.354 3.971 3.481 4.317-7.819-4.656-6.444-5.018-7.565-6.985-6.369-2.591-6.788.208-11.007.004-12.005-6.368-14.318-5.69-25.368-9.681l.502 2.349c-7.953-2.649-9.265 1.005-17.862.009-.523-.409 2.753-1.479 5.452-1.871-7.69 1.015-7.329-1.515-14.854.279 1.855-1.301 3.815-2.162 5.793-3.269-6.271.381-14.971 3.649-12.286.677C20.144-62.46 1.976-56.053-8.218-46.494l-.321-2.142c-4.672 5.608-20.371 16.748-21.622 24.011l-1.249.291c-2.431 4.116-4.004 8.781-5.932 13.016-3.18 5.417-4.661 2.085-4.208 2.934-6.253 12.679-9.359 23.332-12.043 32.069 1.912 2.858.046 17.206.769 28.688-3.141 56.709 39.8 111.77 86.737 124.48 6.88 2.459 17.11 2.364 25.813 2.618-10.268-2.937-11.595-1.556-21.595-5.044-7.215-3.398-8.797-7.277-13.907-11.711l2.022 3.573c-10.021-3.547-5.829-4.39-13.982-6.972l2.16-2.82c-3.249-.246-8.604-5.475-10.069-8.371l-3.553.14c-4.27-5.269-6.545-9.063-6.379-12.005l-1.148 2.047c-1.301-2.235-15.709-19.759-8.234-15.679-1.389-1.271-3.235-2.067-5.237-5.703l1.522-1.739c-3.597-4.627-6.621-10.562-6.391-12.536 1.919 2.592 3.25 3.075 4.568 3.52-9.083-22.539-9.593-1.242-16.474-22.942l1.456-.116c-1.116-1.682-1.793-3.506-2.69-5.298l.633-6.313c-6.541-7.562-1.829-32.151-.887-45.637.655-5.485 5.459-11.322 9.114-20.477l-2.227-.384C-27.316-2.419-7.271-24.81 2.011-23.658c4.499-5.649-.892-.02-1.772-1.443 9.878-10.223 12.984-7.222 19.65-9.061 7.19-4.268-6.17 1.664-2.761-1.628 12.427-3.174 8.808-7.216 25.021-8.828 1.71.973-3.969 1.503-5.395 2.766 10.354-5.066 32.769-3.914 47.326 2.811 16.895 7.896 35.873 31.232 36.622 53.189l.852.229c-.431 8.729 1.336 18.822-1.727 28.094l2.1-4.385"/>
+<path d="m19.5 67.715-.578 2.893c2.71 3.683 4.861 7.673 8.323 10.552-2.49-4.863-4.341-6.872-7.745-13.445m6.409-.251c-1.435-1.587-2.284-3.497-3.235-5.4.909 3.345 2.771 6.219 4.504 9.143l-1.269-3.743m113.411-24.65-.605 1.52c-1.111 7.892-3.511 15.701-7.189 22.941 4.06-7.639 6.69-15.995 7.79-24.461M49.698-68.243c2.789-1.022 6.855-.56 9.814-1.233-3.855.324-7.693.517-11.484 1.005l1.67.228m-97.917 52.067c.642 5.951-4.477 8.26 1.134 4.337 3.007-6.773-1.175-1.87-1.134-4.337m-6.593 27.538c1.292-3.967 1.526-6.349 2.02-8.645-3.571 4.566-1.643 5.539-2.02 8.645"/>
+</g>
+</svg>
diff --git a/web_src/svg/gitea-discord.svg b/web_src/svg/gitea-discord.svg
new file mode 100644
index 0000000..4cadbc7
--- /dev/null
+++ b/web_src/svg/gitea-discord.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path fill="#5865f2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-double-chevron-left.svg b/web_src/svg/gitea-double-chevron-left.svg
new file mode 100644
index 0000000..c223e73
--- /dev/null
+++ b/web_src/svg/gitea-double-chevron-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="M7.91 12.77a.75.75 0 01-1.06 0L2.61 8.52a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.2 8l3.72 3.72a.75.75 0 010 1.06z"/><path d="M13.39 12.77a.75.75 0 01-1.06 0L8.08 8.52a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L9.67 8l3.72 3.72a.75.75 0 010 1.06z"/></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-double-chevron-right.svg b/web_src/svg/gitea-double-chevron-right.svg
new file mode 100644
index 0000000..fd510e8
--- /dev/null
+++ b/web_src/svg/gitea-double-chevron-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="M8.09 3.23a.75.75 0 011.06 0l4.24 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L11.8 8 8.08 4.28a.75.75 0 010-1.06z"/><path d="M2.61 3.23a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L6.33 8 2.61 4.28a.75.75 0 010-1.06z"/></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-dropbox.svg b/web_src/svg/gitea-dropbox.svg
new file mode 100644
index 0000000..e75e7cc
--- /dev/null
+++ b/web_src/svg/gitea-dropbox.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="25 24.999 850 790.592" class="svg gitea-dropbox" width="16" height="16" aria-hidden="true"><g fill="#007ee5"><path d="M275.037 24.999 25 188.255l172.886 138.448L450 171.023zM25 465.16l250.037 163.256L450 482.374l-252.114-155.67zm425 17.214 174.963 146.042L875 465.16 702.114 326.703z"/><path d="M875 188.255 624.963 24.999 450 171.024l252.114 155.68zM450.513 513.797l-175.476 145.61-75.09-49.028v54.959L450.513 815.59 701.08 665.338v-54.96l-75.09 49.029z"/></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-empty-checkbox.svg b/web_src/svg/gitea-empty-checkbox.svg
new file mode 100644
index 0000000..f0ef20a
--- /dev/null
+++ b/web_src/svg/gitea-empty-checkbox.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.5 2.75v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25H2.75a.25.25 0 0 0-.25.25Z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-exclamation.svg b/web_src/svg/gitea-exclamation.svg
new file mode 100644
index 0000000..b4b8795
--- /dev/null
+++ b/web_src/svg/gitea-exclamation.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="-1 -1 26 26"><path d="M12 15.99a2 2 0 0 0 2-2V2.03a2 2 0 0 0-4 0v11.96a2 2 0 0 0 2 2zm0 7.98a2.5 2.5 0 1 0-2.5-2.5 2.5 2.5 0 0 0 2.5 2.5z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-facebook.svg b/web_src/svg/gitea-facebook.svg
new file mode 100644
index 0000000..68cd207
--- /dev/null
+++ b/web_src/svg/gitea-facebook.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025z"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167h1657z"/></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-forgejo.svg b/web_src/svg/gitea-forgejo.svg
new file mode 100644
index 0000000..e00e596
--- /dev/null
+++ b/web_src/svg/gitea-forgejo.svg
@@ -0,0 +1,9 @@
+<svg width="64" height="64" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" class="forgejo-logo" aria-hidden="true">
+ <g transform="translate(28,28)">
+ <path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" fill="none" stroke="#ff6600" stroke-width="25" />
+ <path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" fill="none" stroke="#d40000" stroke-width="25" />
+ <circle cx="142" cy="20" r="18" fill="none" stroke="#ff6600" stroke-width="15" />
+ <circle cx="142" cy="88" r="18" fill="none" stroke="#d40000" stroke-width="15" />
+ <circle cx="58" cy="180" r="18" fill="none" stroke="#d40000" stroke-width="15" />
+ </g>
+</svg>
diff --git a/web_src/svg/gitea-git.svg b/web_src/svg/gitea-git.svg
new file mode 100644
index 0000000..8b8a7b5
--- /dev/null
+++ b/web_src/svg/gitea-git.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="64px" height="64px"><path d="M42.2,22.1L25.9,5.8C25.4,5.3,24.7,5,24,5c0,0,0,0,0,0c-0.7,0-1.4,0.3-1.9,0.8l-3.5,3.5l4.1,4.1c0.4-0.2,0.8-0.3,1.3-0.3c1.7,0,3,1.3,3,3c0,0.5-0.1,0.9-0.3,1.3l4,4c0.4-0.2,0.8-0.3,1.3-0.3c1.7,0,3,1.3,3,3s-1.3,3-3,3c-1.7,0-3-1.3-3-3c0-0.5,0.1-0.9,0.3-1.3l-4-4c-0.1,0-0.2,0.1-0.3,0.1v10.4c1.2,0.4,2,1.5,2,2.8c0,1.7-1.3,3-3,3s-3-1.3-3-3c0-1.3,0.8-2.4,2-2.8V18.8c-1.2-0.4-2-1.5-2-2.8c0-0.5,0.1-0.9,0.3-1.3l-4.1-4.1L5.8,22.1C5.3,22.6,5,23.3,5,24c0,0.7,0.3,1.4,0.8,1.9l16.3,16.3c0,0,0,0,0,0c0.5,0.5,1.2,0.8,1.9,0.8s1.4-0.3,1.9-0.8l16.3-16.3c0.5-0.5,0.8-1.2,0.8-1.9C43,23.3,42.7,22.6,42.2,22.1z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-gitbucket.svg b/web_src/svg/gitea-gitbucket.svg
new file mode 100644
index 0000000..a960d6d
--- /dev/null
+++ b/web_src/svg/gitea-gitbucket.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="316.000000pt" height="329.000000pt" viewBox="0 0 316.000000 329.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.14, written by Peter Selinger 2001-2017
+</metadata>
+<g transform="translate(0.000000,329.000000) scale(0.100000,-0.100000)"
+stroke="none">
+<path d="M1230 3079 c-448 -28 -840 -128 -971 -246 -50 -45 -50 -71 0 -116
+158 -142 707 -256 1241 -257 l106 0 54 56 c118 121 213 124 326 12 l52 -50
+133 17 c338 42 615 127 718 220 53 48 53 72 0 120 -101 91 -391 181 -704 219
+-283 34 -656 44 -955 25z"/>
+<path d="M232 2484 c4 -16 70 -429 148 -919 79 -490 147 -895 152 -902 17 -21
+258 -114 416 -159 130 -37 351 -84 400 -84 6 0 12 6 12 13 0 6 -174 185 -386
+397 -395 394 -424 428 -424 500 0 72 30 107 383 459 301 300 348 343 366 335
+11 -5 87 -74 170 -152 l151 -144 0 -59 c0 -73 24 -124 77 -167 l38 -30 0 -309
+0 -309 -38 -34 c-102 -89 -102 -229 -1 -307 31 -23 49 -28 108 -31 82 -4 115
+11 162 73 22 30 29 51 32 100 4 65 -2 83 -53 154 l-25 34 0 266 c0 267 3 311
+23 311 5 0 54 -44 109 -97 96 -95 99 -100 108 -157 17 -103 89 -166 190 -166
+76 0 135 39 174 117 32 64 16 143 -43 206 -41 44 -73 57 -149 57 l-68 0 -127
+121 c-107 101 -127 126 -132 157 -12 72 -16 82 -47 117 -39 45 -77 65 -122 65
+-19 0 -45 4 -58 9 -13 5 -99 82 -193 170 l-170 160 -71 1 c-106 0 -360 27
+-524 55 -228 40 -385 86 -579 172 -11 5 -13 0 -9 -23z"/>
+<path d="M2785 2454 c-86 -36 -280 -88 -425 -114 -69 -12 -129 -26 -135 -31
+-6 -6 93 -112 275 -294 156 -156 285 -283 287 -281 2 2 19 158 38 347 19 189
+37 356 40 372 6 34 -2 34 -80 1z"/>
+<path d="M2552 697 c-78 -78 -142 -146 -142 -150 0 -4 7 -7 16 -7 22 0 230 89
+242 103 9 11 35 187 29 193 -2 2 -67 -61 -145 -139z"/>
+<path d="M560 455 c0 -34 40 -95 84 -129 61 -46 197 -104 317 -135 169 -43
+321 -62 534 -68 l190 -5 -70 71 c-68 69 -71 71 -120 71 -189 0 -551 79 -806
+176 -64 24 -119 44 -123 44 -3 0 -6 -11 -6 -25z"/>
+<path d="M2620 456 c-53 -28 -250 -97 -360 -126 l-114 -29 -78 -78 c-42 -42
+-76 -79 -74 -81 9 -8 244 41 334 69 164 53 267 114 308 185 24 40 31 74 17 74
+-5 -1 -19 -7 -33 -14z"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-gitea.svg b/web_src/svg/gitea-gitea.svg
new file mode 100644
index 0000000..5d89fa1
--- /dev/null
+++ b/web_src/svg/gitea-gitea.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" class="svg gitea-gitea" width="16" height="16" aria-hidden="true"><path fill="#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12"/><g fill="#609926"><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z"/></g></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-gitlab.svg b/web_src/svg/gitea-gitlab.svg
new file mode 100644
index 0000000..e966a4e
--- /dev/null
+++ b/web_src/svg/gitea-gitlab.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#E24329" d="m31.462 12.779-.045-.115-4.35-11.35a1.137 1.137 0 0 0-.447-.541 1.163 1.163 0 0 0-1.343.071c-.187.15-.322.356-.386.587l-2.94 9.001h-11.9l-2.941-9A1.138 1.138 0 0 0 6.065.592a1.153 1.153 0 0 0-1.13.72L.579 12.68l-.045.113a8.09 8.09 0 0 0 2.68 9.34l.016.012.038.03 6.635 4.967 3.28 2.484 1.994 1.51a1.35 1.35 0 0 0 1.627 0l1.994-1.51 3.282-2.484 6.673-4.997.018-.013a8.088 8.088 0 0 0 2.69-9.352Z"/><path fill="#FC6D26" d="m31.462 12.779-.045-.115a14.748 14.748 0 0 0-5.856 2.634l-9.553 7.24A11225.6 11225.6 0 0 0 22.1 27.14l6.673-4.997.019-.013a8.09 8.09 0 0 0 2.67-9.352Z"/><path fill="#FCA326" d="m9.908 27.14 3.275 2.485 1.994 1.51a1.35 1.35 0 0 0 1.627 0l1.994-1.51 3.282-2.484s-2.835-2.14-6.092-4.603l-6.08 4.603Z"/><path fill="#FC6D26" d="M6.435 15.305A14.712 14.712 0 0 0 .58 12.672l-.045.113a8.09 8.09 0 0 0 2.68 9.347l.016.012.038.03 6.635 4.967 6.105-4.603-9.573-7.233Z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-go.svg b/web_src/svg/gitea-go.svg
new file mode 100644
index 0000000..84e201e
--- /dev/null
+++ b/web_src/svg/gitea-go.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="24.7 77.83 205.42 76.8" xml:space="preserve">
+<g style="fill:#00ACD7">
+ <path d="M40.2,101.1c-0.4,0-0.5-0.2-0.3-0.5l2.1-2.7c0.2-0.3,0.7-0.5,1.1-0.5l35.7,0c0.4,0,0.5,0.3,0.3,0.6 l-1.7,2.6c-0.2,0.3-0.7,0.6-1,0.6L40.2,101.1z"/>
+ <path d="M25.1,110.3c-0.4,0-0.5-0.2-0.3-0.5l2.1-2.7c0.2-0.3,0.7-0.5,1.1-0.5l45.6,0c0.4,0,0.6,0.3,0.5,0.6 l-0.8,2.4c-0.1,0.4-0.5,0.6-0.9,0.6L25.1,110.3z"/>
+ <path d="M49.3,119.5c-0.4,0-0.5-0.3-0.3-0.6l1.4-2.5c0.2-0.3,0.6-0.6,1-0.6l20,0c0.4,0,0.6,0.3,0.6,0.7l-0.2,2.4 c0,0.4-0.4,0.7-0.7,0.7L49.3,119.5z"/>
+ <path d="M153.1,99.3c-6.3,1.6-10.6,2.8-16.8,4.4c-1.5,0.4-1.6,0.5-2.9-1c-1.5-1.7-2.6-2.8-4.7-3.8 c-6.3-3.1-12.4-2.2-18.1,1.5c-6.8,4.4-10.3,10.9-10.2,19c0.1,8,5.6,14.6,13.5,15.7c6.8,0.9,12.5-1.5,17-6.6 c0.9-1.1,1.7-2.3,2.7-3.7c-3.6,0-8.1,0-19.3,0c-2.1,0-2.6-1.3-1.9-3c1.3-3.1,3.7-8.3,5.1-10.9c0.3-0.6,1-1.6,2.5-1.6 c5.1,0,23.9,0,36.4,0c-0.2,2.7-0.2,5.4-0.6,8.1c-1.1,7.2-3.8,13.8-8.2,19.6c-7.2,9.5-16.6,15.4-28.5,17 c-9.8,1.3-18.9-0.6-26.9-6.6c-7.4-5.6-11.6-13-12.7-22.2c-1.3-10.9,1.9-20.7,8.5-29.3c7.1-9.3,16.5-15.2,28-17.3 c9.4-1.7,18.4-0.6,26.5,4.9c5.3,3.5,9.1,8.3,11.6,14.1C154.7,98.5,154.3,99,153.1,99.3z"/>
+ <path d="M186.2,154.6c-9.1-0.2-17.4-2.8-24.4-8.8c-5.9-5.1-9.6-11.6-10.8-19.3c-1.8-11.3,1.3-21.3,8.1-30.2 c7.3-9.6,16.1-14.6,28-16.7c10.2-1.8,19.8-0.8,28.5,5.1c7.9,5.4,12.8,12.7,14.1,22.3c1.7,13.5-2.2,24.5-11.5,33.9 c-6.6,6.7-14.7,10.9-24,12.8C191.5,154.2,188.8,154.3,186.2,154.6z M210,114.2c-0.1-1.3-0.1-2.3-0.3-3.3 c-1.8-9.9-10.9-15.5-20.4-13.3c-9.3,2.1-15.3,8-17.5,17.4c-1.8,7.8,2,15.7,9.2,18.9c5.5,2.4,11,2.1,16.3-0.6 C205.2,129.2,209.5,122.8,210,114.2z"/>
+</g>
+</svg>
diff --git a/web_src/svg/gitea-gogs.svg b/web_src/svg/gitea-gogs.svg
new file mode 100644
index 0000000..7c4d5c2
--- /dev/null
+++ b/web_src/svg/gitea-gogs.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="67.73333mm"
+ height="67.73333mm"
+ viewBox="0 0 67.73333 67.73333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
+ sodipodi:docname="gitea-gogs.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.4352161"
+ inkscape:cx="145.15203"
+ inkscape:cy="129.56614"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer3"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1006"
+ inkscape:window-x="1200"
+ inkscape:window-y="444"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer3"
+ inkscape:label="Layer 3"
+ style="display:inline">
+ <path
+ style="display:inline;fill:#d75547;fill-opacity:1;fill-rule:evenodd;stroke:#428f29;stroke-width:0.0190672;paint-order:markers fill stroke"
+ d="M 28.763808,62.186078 C 28.552224,62.156588 28.3282,62.079645 28.265977,62.01509 28.123377,61.867145 27.941284,61.235718 27.375971,58.928893 27.128273,57.918138 26.870167,56.952588 26.8024,56.783223 26.653428,56.410912 26.321729,56.197407 24.664484,55.407125 22.815394,54.525354 20.800132,53.303558 19.034519,51.99383 l -0.753641,-0.55905 -2.322335,0.678345 c -1.277286,0.37309 -2.657942,0.75099 -3.068125,0.839774 l -0.745787,0.161424 -0.351437,-0.536616 C 10.841488,51.124528 8.2297585,46.620395 7.1670955,44.599636 L 6.6102577,43.540752 7.0934858,42.948695 c 0.2657754,-0.325634 1.2926661,-1.41771 2.2819797,-2.426837 l 1.7987515,-1.834779 -0.180416,-1.038637 c -0.160803,-0.925735 -0.180684,-1.319661 -0.182886,-3.623976 -0.002,-2.082515 0.02593,-2.77882 0.143552,-3.580102 L 11.100491,29.449609 9.0366642,27.414662 C 7.5050421,25.904473 6.9326263,25.287565 6.8169034,25.02237 6.6626002,24.668764 6.6626297,24.660735 6.8197249,24.254845 7.2963868,23.023252 11.239804,16.273894 12.005659,15.378857 l 0.257067,-0.300427 0.484759,0.125942 c 0.266618,0.06927 1.585016,0.43706 2.929773,0.817315 l 2.445013,0.691373 0.429771,-0.260466 c 0.236371,-0.143253 0.844892,-0.564341 1.352266,-0.935741 1.30433,-0.954784 2.690014,-1.762213 4.707543,-2.74306 1.87475,-0.911433 1.838916,-0.883043 2.124645,-1.683256 0.06563,-0.1838 0.336217,-1.1703787 0.601305,-2.1923981 0.26509,-1.0220196 0.573575,-2.1360624 0.685518,-2.4756513 0.17614,-0.5343091 0.23295,-0.623873 0.421984,-0.6652627 0.483669,-0.1058996 3.97054,-0.1502609 7.292057,-0.092772 l 3.518704,0.060902 0.430989,1.7479767 c 0.510047,2.0686117 0.909017,3.4804444 1.127722,3.9906574 0.174557,0.407221 0.0081,0.308631 2.593742,1.53554 1.3556,0.643225 2.654323,1.424602 4.307061,2.591347 0.702523,0.495943 1.33877,0.943783 1.413882,0.995202 0.109699,0.0751 0.502152,-0.01615 1.994813,-0.463793 1.022033,-0.306501 2.381778,-0.689488 3.021651,-0.85108 l 1.163411,-0.293805 0.145052,0.19205 c 0.93221,1.234241 5.324678,8.900157 5.324678,9.292846 0,0.333447 -0.488016,0.914807 -2.540796,3.02678 l -1.893181,1.947777 0.15824,0.960735 c 0.227755,1.382822 0.274355,3.126299 0.15827,5.921847 l -0.101763,2.450639 1.510118,1.495377 c 1.549811,1.534681 2.47101,2.550338 2.683271,2.958414 0.115947,0.22291 0.103768,0.264805 -0.328833,1.13109 -1.289906,2.583039 -4.510781,8.106197 -4.924647,8.444791 -0.229142,0.187467 -0.906252,0.05936 -3.361841,-0.636096 -1.264388,-0.358089 -2.427812,-0.66997 -2.585382,-0.693076 -0.254259,-0.03729 -0.377393,0.02128 -1.094426,0.520539 -2.75021,1.914888 -3.396183,2.301016 -5.615132,3.356412 -0.864328,0.4111 -1.586938,0.808234 -1.713456,0.94169 -0.279925,0.295269 -0.499881,1.010417 -1.141053,3.709893 l -0.508517,2.140988 -1.893589,0.04981 c -2.516342,0.0662 -8.338233,0.06367 -8.822539,-0.0038 z m 7.858077,-12.120535 c 2.091378,-0.388455 5.175143,-1.723813 6.867427,-2.973786 1.796481,-1.326945 4.757949,-4.883235 4.97652,-5.976082 0.04707,-0.23538 0.0195,-0.268126 -0.544626,-0.646577 -0.89966,-0.603557 -2.187787,-1.377201 -5.078243,-3.049968 -4.184205,-2.421491 -4.52345,-2.676285 -4.524118,-3.397889 -0.0011,-1.340016 -1.219035,-3.278179 -2.4241,-3.858035 -1.790528,-0.861573 -3.841611,-0.52172 -5.213619,0.863866 -0.315085,0.318202 -0.680808,0.789483 -0.82133,1.058382 -1.355657,2.59418 0.09164,5.558218 3.005202,6.154597 0.886416,0.181439 1.72976,0.07365 2.913276,-0.372328 l 0.879991,-0.331607 2.02184,1.158352 c 2.672299,1.531012 3.61875,2.12207 4.1007,2.560884 0.340094,0.309652 0.391458,0.394601 0.346398,0.57285 -0.0768,0.303803 -0.673843,1.002125 -1.394132,1.630629 -1.608465,1.403495 -3.352982,2.265892 -5.547142,2.742214 -1.178042,0.255735 -3.523379,0.256058 -4.699639,6.79e-4 -1.211182,-0.262989 -1.987409,-0.533958 -3.06492,-1.06991 -3.757271,-1.868855 -6.310115,-5.470633 -6.829909,-9.636234 -0.156607,-1.255046 -0.06748,-3.197221 0.200967,-4.379595 1.024517,-4.512371 4.409288,-7.971423 8.971948,-9.168835 1.368354,-0.359109 1.98948,-0.43042 3.32699,-0.381966 1.261093,0.04568 1.953923,0.167871 3.540434,0.624377 0.565292,0.162659 1.094549,0.260446 1.454278,0.268701 0.495297,0.01136 0.620752,-0.01798 1.009916,-0.236201 1.079693,-0.605431 1.3594,-1.855128 0.602683,-2.692712 -1.317562,-1.458359 -6.682213,-2.214558 -10.373764,-1.462279 -2.483279,0.506053 -5.053495,1.805105 -6.993517,3.534699 -2.575021,2.295712 -4.259712,4.711906 -5.127071,7.353271 -0.924677,2.815913 -1.025226,6.141317 -0.275431,9.109059 0.774038,3.063694 2.549965,5.963534 4.958139,8.095943 2.441521,2.161939 5.76675,3.672334 8.927653,4.055148 1.089179,0.13191 3.734827,0.04956 4.807199,-0.149613 z"
+ id="path851" />
+ </g>
+</svg>
diff --git a/web_src/svg/gitea-google.svg b/web_src/svg/gitea-google.svg
new file mode 100644
index 0000000..d15a591
--- /dev/null
+++ b/web_src/svg/gitea-google.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg gitea-google" viewBox="0 0 24 24" width="16" height="16"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" /><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" /><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" /><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" /><path d="M1 1h22v22H1z" fill="none" /></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-helm.svg b/web_src/svg/gitea-helm.svg
new file mode 100644
index 0000000..1209965
--- /dev/null
+++ b/web_src/svg/gitea-helm.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62 65" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round">
+<g><path fill="#3f7a9c" d="M41.868 16.659l.248.19c1.503 1.172 2.825 2.564 3.919 4.128l.9 1.414 2.774-1.601-1.05-1.65c-.959-1.37-2.068-2.628-3.304-3.748.728-.642 1.491-1.459 2.215-2.403 2.158-2.813 3.141-5.657 2.204-6.376s-3.43.966-5.589 3.779c-.725.944-1.317 1.892-1.748 2.762-3.013-1.94-6.525-3.176-10.308-3.48.189-.957.298-2.076.298-3.273 0-3.546-.951-6.4-2.133-6.4S28.16 2.854 28.16 6.4c0 1.198.109 2.317.298 3.273-3.783.304-7.296 1.54-10.308 3.48-.431-.87-1.024-1.818-1.748-2.762-2.158-2.813-4.651-4.498-5.589-3.779s.045 3.563 2.204 6.376c.725.944 1.487 1.761 2.215 2.403-1.236 1.12-2.345 2.378-3.304 3.748a22.33 22.33 0 0 0-1.05 1.65l2.774 1.601a19.13 19.13 0 0 1 .9-1.414 19.21 19.21 0 0 1 3.919-4.128l.248-.19c3.214-2.424 7.221-3.859 11.574-3.859s8.36 1.435 11.574 3.859zM14.551 43.023A19.15 19.15 0 0 0 30.293 51.2a19.15 19.15 0 0 0 15.742-8.177l2.624 1.837c-.959 1.37-2.068 2.628-3.304 3.748.728.642 1.491 1.459 2.215 2.403 2.158 2.813 3.141 5.657 2.204 6.376s-3.43-.966-5.589-3.779c-.725-.944-1.317-1.892-1.748-2.762-3.013 1.94-6.525 3.176-10.308 3.48.189.957.298 2.076.298 3.273 0 3.546-.951 6.4-2.133 6.4s-2.133-2.854-2.133-6.4c0-1.198.109-2.317.298-3.273-3.783-.304-7.296-1.54-10.308-3.48-.431.87-1.024 1.818-1.748 2.762-2.158 2.813-4.651 4.498-5.589 3.779s.045-3.563 2.204-6.376c.725-.944 1.487-1.761 2.215-2.403-1.236-1.12-2.345-2.378-3.304-3.748zM44.8 40.533V24.32h4.693l3.413 9.813 3.413-9.813h4.267v16.213h-3.84v-5.12l.853-5.973-3.84 9.813h-2.133l-3.84-9.813.853 5.973v5.12zM31.147 24.32v16.213h10.667V37.12h-6.4v-12.8zm-14.08 0v16.213H28.16V37.12h-6.827v-2.987h5.547V30.72h-5.547v-2.987h6.4V24.32zm-12.8 16.213v-6.4h5.12v6.4h4.267V24.32H9.387v5.973h-5.12V24.32H0v16.213z" stroke="none"/></g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-jetbrains.svg b/web_src/svg/gitea-jetbrains.svg
new file mode 100644
index 0000000..a7884c4
--- /dev/null
+++ b/web_src/svg/gitea-jetbrains.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
+<g>
+ <g>
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893">
+ <stop offset="0.2581" style="stop-color:#F97A12"/>
+ <stop offset="0.4591" style="stop-color:#B07B58"/>
+ <stop offset="0.7241" style="stop-color:#577BAE"/>
+ <stop offset="0.9105" style="stop-color:#1E7CE5"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 "/>
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57">
+ <stop offset="0" style="stop-color:#F97A12"/>
+ <stop offset="7.179946e-002" style="stop-color:#CB7A3E"/>
+ <stop offset="0.1541" style="stop-color:#9E7B6A"/>
+ <stop offset="0.242" style="stop-color:#757B91"/>
+ <stop offset="0.3344" style="stop-color:#537BB1"/>
+ <stop offset="0.4324" style="stop-color:#387CCC"/>
+ <stop offset="0.5381" style="stop-color:#237CE0"/>
+ <stop offset="0.6552" style="stop-color:#147CEF"/>
+ <stop offset="0.7925" style="stop-color:#0B7CF7"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 "/>
+ <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191">
+ <stop offset="0" style="stop-color:#FE315D"/>
+ <stop offset="7.840246e-002" style="stop-color:#CB417E"/>
+ <stop offset="0.1601" style="stop-color:#9E4E9B"/>
+ <stop offset="0.2474" style="stop-color:#755BB4"/>
+ <stop offset="0.3392" style="stop-color:#5365CA"/>
+ <stop offset="0.4365" style="stop-color:#386DDB"/>
+ <stop offset="0.5414" style="stop-color:#2374E9"/>
+ <stop offset="0.6576" style="stop-color:#1478F3"/>
+ <stop offset="0.794" style="stop-color:#0B7BF8"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 "/>
+ <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58">
+ <stop offset="0" style="stop-color:#FE315D"/>
+ <stop offset="4.023279e-002" style="stop-color:#F63462"/>
+ <stop offset="0.1037" style="stop-color:#DF3A71"/>
+ <stop offset="0.1667" style="stop-color:#C24383"/>
+ <stop offset="0.2912" style="stop-color:#AD4A91"/>
+ <stop offset="0.5498" style="stop-color:#755BB4"/>
+ <stop offset="0.9175" style="stop-color:#1D76ED"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 "/>
+ </g>
+ <g>
+ <rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/>
+ <rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
+ <polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37
+ 29.4,33.7 26.9,33.7 26.9,22.4 "/>
+ <path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3
+ c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2
+ c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/>
+ </g>
+</g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-join.svg b/web_src/svg/gitea-join.svg
new file mode 100644
index 0000000..04c3e92
--- /dev/null
+++ b/web_src/svg/gitea-join.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M14 10.9V8.75h1.25a.75.75 0 0 0 0-1.5H14V5.1a.25.25 0 0 0-.43-.17l-2.9 2.9a.25.25 0 0 0 0 .35l2.9 2.9a.25.25 0 0 0 .43-.18ZM.75 8.75a.75.75 0 0 1 0-1.5H2V5.1a.25.25 0 0 1 .43-.17l2.9 2.9a.25.25 0 0 1 0 .35l-2.9 2.9A.25.25 0 0 1 2 10.9V8.75Zm6.5-6.5a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 6a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 6Zm-.75 2.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 12a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 12Zm-.75 2.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-lock-cog.svg b/web_src/svg/gitea-lock-cog.svg
new file mode 100644
index 0000000..da8e954
--- /dev/null
+++ b/web_src/svg/gitea-lock-cog.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16"><path d="M12.5 7h-.75V4.75C11.75 2.131 9.619 0 7 0S2.25 2.131 2.25 4.75V7H1.5A1.5 1.5 0 000 8.5v6A1.5 1.5 0 001.5 16h11a1.5 1.5 0 001.5-1.5v-6A1.5 1.5 0 0012.5 7zM9.25 7h-4.5V4.75C4.75 3.51 5.76 2.5 7 2.5s2.25 1.01 2.25 2.25z"/><path d="M6.433 10.244q-.508.237-.7.765-.192.527.045 1.036.237.508.764.7.528.192 1.036-.045.509-.237.7-.765.193-.527-.044-1.036-.237-.508-.765-.7-.528-.192-1.036.045zM4.765 8.032l1.065-.497q.057-.027.128-.013.07.014.107.067l.548.825q.302-.03.524-.017.162-.28.423-.822.035-.074.097-.103.063-.03.13-.009.239.047.74.234.502.186.551.292.027.057.015.145l-.21.903q.21.16.386.352.689-.228.958-.277.15-.03.214.11l.497 1.065q.031.067.014.136-.017.07-.077.103l-.82.546q.032.271.023.515l.83.437q.065.027.098.1.032.067.003.144-.265.86-.436 1.167-.033.056-.095.085-.058.027-.128.013-.135-.025-.433-.096-.3-.07-.46-.1-.179.24-.382.417l.28.938q.019.067-.014.13-.032.061-.094.09l-1.065.497q-.058.026-.128.013-.071-.014-.105-.062l-.55-.83q-.253.036-.53.019-.183.32-.422.822-.035.074-.093.101-.048.023-.13.009-.231-.045-.736-.233-.505-.187-.554-.293-.03-.062-.01-.147l.204-.901q-.209-.16-.386-.352-.688.228-.957.277-.15.03-.215-.11l-.496-1.065q-.032-.067-.014-.136.017-.07.077-.104l.82-.545q-.033-.271-.024-.515l-.83-.442q-.064-.023-.098-.095-.029-.062-.008-.142.283-.885.447-1.172.023-.051.09-.082.057-.027.128-.014.134.026.433.096.299.071.459.102.182-.237.378-.416l-.275-.94q-.02-.067.013-.13.033-.061.095-.09z" fill="#fff" fill-rule="evenodd" stroke-width=".2"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-lock.svg b/web_src/svg/gitea-lock.svg
new file mode 100644
index 0000000..0508e3b
--- /dev/null
+++ b/web_src/svg/gitea-lock.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16"><path d="M12.5 7h-.75V4.75A4.756 4.756 0 007 0a4.756 4.756 0 00-4.75 4.75V7H1.5A1.5 1.5 0 000 8.5v6A1.5 1.5 0 001.5 16h11a1.5 1.5 0 001.5-1.5v-6A1.5 1.5 0 0012.5 7zM9.25 7h-4.5V4.75C4.75 3.51 5.76 2.5 7 2.5s2.25 1.01 2.25 2.25z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-mastodon.svg b/web_src/svg/gitea-mastodon.svg
new file mode 100644
index 0000000..f21f1e1
--- /dev/null
+++ b/web_src/svg/gitea-mastodon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.41 0.22 747.62 801.45" class="svg gitea-mastodon" width="16" height="16" aria-hidden="true"><path fill="#3088d4" d="M729.94 479.5c-10.96 56.4-98.17 118.12-198.34 130.08-52.23 6.23-103.66 11.96-158.49 9.44-89.68-4.1-160.45-21.4-160.45-21.4 0 8.73.54 17.04 1.62 24.81 11.66 88.52 87.76 93.82 159.84 96.29 72.76 2.49 137.55-17.94 137.55-17.94l2.99 65.79s-50.89 27.32-141.55 32.35c-50 2.75-112.07-1.26-184.37-20.39C31.94 737.02 4.97 569.86.85 400.26-.41 349.9.37 302.42.37 262.7.37 89.27 113.99 38.44 113.99 38.44 171.28 12.12 269.59 1.06 371.79.22h2.52c102.19.84 200.57 11.9 257.86 38.22 0 0 113.62 50.83 113.62 224.26 0 0 1.42 127.96-15.85 216.8"/><path fill="#fff" d="M611.77 276.16v209.99h-83.2V282.33c0-42.97-18.07-64.77-54.23-64.77-39.98 0-60.01 25.86-60.01 77.02v111.57h-82.71V294.58c0-51.16-20.04-77.02-60.01-77.02-36.16 0-54.24 21.8-54.24 64.77v203.82h-83.19V276.16c0-42.92 10.93-77.03 32.88-102.26 22.63-25.23 52.27-38.17 89.07-38.17 42.57 0 74.81 16.37 96.12 49.1l20.72 34.74 20.73-34.74c21.31-32.73 53.55-49.1 96.12-49.1 36.79 0 66.44 12.94 89.07 38.17 21.95 25.23 32.88 59.34 32.88 102.26z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-matrix.svg b/web_src/svg/gitea-matrix.svg
new file mode 100644
index 0000000..63b23fc
--- /dev/null
+++ b/web_src/svg/gitea-matrix.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
+<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
+ <path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
+ c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
+ c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
+ c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
+ c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
+ c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
+ v107.6h-50.9V169.2H166.3z"/>
+ <path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-maven.svg b/web_src/svg/gitea-maven.svg
new file mode 100644
index 0000000..8f8502e
--- /dev/null
+++ b/web_src/svg/gitea-maven.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 2392.5 4226.6" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" x1="-5167.1" x2="-4570.1" y1="697.55" y2="1395.6" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#F69923" offset="0"/><stop stop-color="#F79A23" offset=".312"/><stop stop-color="#E97826" offset=".838"/></linearGradient><path d="M1798.9 20.1C1732.6 59.2 1622.5 170 1491 330.5l120.8 228c84.8-121.3 170.9-230.4 257.8-323.6 6.7-7.4 10.2-10.9 10.2-10.9-3.4 3.6-6.8 7.3-10.2 10.9-28.1 31-113.4 130.5-242.1 328.1 123.9-6.2 314.3-31.5 469.6-58.1 46.2-258.8-45.3-377.3-45.3-377.3S1935.5-60.6 1798.9 20.1z" fill="url(#a)"/><path d="M1594.4 1320.7c.9-.2 1.8-.3 2.7-.5l-17.4 1.9c-1.1.5-2 1-3.1 1.4 6-.9 11.9-1.9 17.8-2.8zM1471.1 1729.1c-9.9 2.2-20 3.9-30.2 5.4 10.2-1.5 20.3-3.3 30.2-5.4zM633.1 2645.2c1.3-3.4 2.6-6.8 3.8-10.2 26.6-70.2 52.9-138.4 79-204.9 29.3-74.6 58.2-146.8 86.8-216.8 30.1-73.8 59.8-145.1 89.1-214 30.7-72.3 61-141.9 90.7-208.9 24.2-54.5 48-107.3 71.5-158.4 7.8-17 15.6-33.9 23.4-50.6 15.4-33.1 30.7-65.6 45.7-97.3 13.9-29.3 27.7-57.9 41.4-86 4.5-9.4 9.1-18.6 13.6-27.9.7-1.5 1.5-3 2.2-4.5l-14.8 1.6-11.8-23.2c-1.1 2.3-2.3 4.5-3.5 6.8-21.2 42.1-42.2 84.6-63 127.5-12 24.8-24 49.7-35.9 74.7-33 69.3-65.5 139.2-97.4 209.6-32.3 71.1-63.9 142.6-94.9 214.2-30.5 70.3-60.3 140.7-89.6 210.9-29.2 70.1-57.7 140-85.6 209.4-29.1 72.5-57.4 144.3-84.8 215.3-6.2 16-12.4 32-18.5 48-22 57.3-43.4 113.8-64.3 169.6l18.6 36.7 16.6-1.8c.6-1.7 1.2-3.4 1.8-5 26.9-73.5 53.5-145.1 79.9-214.8zM1433.2 1735.7c.1 0 .1-.1.2-.1 0 0-.1 0-.2.1z" fill="none"/><path d="M1393.2 1934.8c-15.4 2.8-31.3 5.5-47.6 8.3-.1 0-.2.1-.3.1 8.2-1.2 16.3-2.4 24.3-3.8s15.8-2.9 23.6-4.6z" fill="#BE202E"/><path d="M1393.2 1934.8c-15.4 2.8-31.3 5.5-47.6 8.3-.1 0-.2.1-.3.1 8.2-1.2 16.3-2.4 24.3-3.8s15.8-2.9 23.6-4.6z" fill="#BE202E" opacity=".35"/><path d="M1433.6 1735.5s-.1 0-.1.1c-.1 0-.1.1-.2.1 2.6-.3 5.1-.8 7.6-1.1 10.3-1.5 20.4-3.3 30.2-5.4-12.3 2-24.8 4.2-37.5 6.3z" fill="#BE202E"/><path d="M1433.6 1735.5s-.1 0-.1.1c-.1 0-.1.1-.2.1 2.6-.3 5.1-.8 7.6-1.1 10.3-1.5 20.4-3.3 30.2-5.4-12.3 2-24.8 4.2-37.5 6.3z" fill="#BE202E" opacity=".35"/><linearGradient id="b" x1="-9585.3" x2="-5326.2" y1="620.5" y2="620.5" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M1255.7 1147.6c36.7-68.6 73.9-135.7 111.5-201 39-67.8 78.5-133.6 118.4-197 2.3-3.7 4.7-7.5 7-11.3 39.4-62.4 79.2-122.4 119.3-179.8l-120.8-228c-9.1 11.1-18.2 22.4-27.5 33.9-34.8 43.4-71 90.1-108.1 139.6-41.8 55.8-84.8 115.4-128.5 177.9-40.3 57.8-81.2 118.3-122.1 180.9-34.8 53.3-69.8 108.2-104.5 164.5l-3.9 6.3 157.2 310.5c33.6-66.5 67.6-132.1 102-196.5z" fill="url(#b)"/><linearGradient id="c" x1="-9071.2" x2="-6533.2" y1="1047.7" y2="1047.7" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#282662" offset="0"/><stop stop-color="#662E8D" offset=".095"/><stop stop-color="#9F2064" offset=".788"/><stop stop-color="#CD2032" offset=".949"/></linearGradient><path d="M539.7 2897.1c-20.8 57.2-41.7 115.4-62.7 174.9-.3.9-.6 1.7-.9 2.6-3 8.4-5.9 16.8-8.9 25.2-14.1 40.1-26.4 76.2-54.5 158.3 46.3 21.1 83.5 76.7 118.7 139.8-3.7-65.3-30.8-126.7-82.1-174.2 228.3 10.3 425-47.4 526.7-214.3 9.1-14.9 17.4-30.5 24.9-47.2-46.2 58.6-103.5 83.5-211.4 77.4-.2.1-.5.2-.7.3.2-.1.5-.2.7-.3 158.8-71.1 238.5-139.3 308.9-252.4 16.7-26.8 32.9-56.1 49.5-88.6-138.9 142.6-299.8 183.2-469.3 152.4l-127.1 13.9c-4 10.7-7.9 21.4-11.8 32.2z" fill="url(#c)"/><linearGradient id="d" x1="-9346.1" x2="-5087" y1="580.82" y2="580.82" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M599 2612.4c27.5-71 55.8-142.8 84.8-215.3 27.8-69.4 56.4-139.2 85.6-209.4s59.1-140.5 89.6-210.9c31-71.6 62.7-143.1 94.9-214.2 31.9-70.3 64.4-140.3 97.4-209.6 11.9-25 23.9-49.9 35.9-74.7 20.8-42.9 41.8-85.4 63-127.5 1.1-2.3 2.3-4.5 3.5-6.8l-157.2-310.5c-2.6 4.2-5.1 8.4-7.7 12.6-36.6 59.8-73.1 121-108.9 183.5-36.2 63.1-71.7 127.4-106.4 192.6-29.3 55-57.9 110.5-85.7 166.5-5.6 11.4-11.1 22.6-16.6 33.9-34.3 70.5-65.2 138.6-93.2 204.1-31.7 74.2-59.6 145.1-84 212.3-16.1 44.2-30.7 86.9-44.1 127.9-11 35-21.5 70.1-31.4 105-23.5 82.3-43.7 164.4-60.3 246.2l158 311.9c20.9-55.8 42.3-112.3 64.3-169.6 6.1-15.9 12.3-32 18.5-48z" fill="url(#d)"/><linearGradient id="e" x1="-9035.5" x2="-6797.2" y1="638.44" y2="638.44" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#282662" offset="0"/><stop stop-color="#662E8D" offset=".095"/><stop stop-color="#9F2064" offset=".788"/><stop stop-color="#CD2032" offset=".949"/></linearGradient><path d="M356.1 2529.2c-19.8 99.8-33.9 199.2-41 298-.2 3.5-.6 6.9-.8 10.4-49.3-79-181.3-156.1-181-155.4 94.5 137 166.2 273 176.9 406.5-50.6 10.4-119.9-4.6-200-34.1 83.5 76.7 146.2 97.9 170.6 103.6-76.7 4.8-156.6 57.5-237.1 118.2 117.7-48 212.8-67 280.9-51.6-108 305.8-216.3 643.4-324.6 1001.8 33.2-9.8 53-32.1 64.1-62.3 19.3-64.9 147.4-490.7 348.1-1050.4 5.7-15.9 11.5-31.9 17.3-48 1.6-4.5 3.3-9 4.9-13.4 21.2-58.7 43.2-118.6 65.9-179.7 5.2-13.9 10.4-27.8 15.6-41.8.1-.3.2-.6.3-.8l-157.8-311.8c-.7 3.5-1.6 7.1-2.3 10.8z" fill="url(#e)"/><linearGradient id="f" x1="-9346.1" x2="-5087" y1="1021.6" y2="1021.6" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M1178.1 1370.3c-4.5 9.2-9 18.5-13.6 27.9-13.6 28.1-27.4 56.7-41.4 86-15.1 31.7-30.3 64.1-45.7 97.3-7.8 16.7-15.5 33.5-23.4 50.6-23.5 51.1-47.3 103.9-71.5 158.4-29.7 67-60 136.6-90.7 208.9-29.3 68.9-59 140.2-89.1 214-28.6 70-57.5 142.3-86.8 216.8-26.1 66.5-52.4 134.7-79 204.9-1.3 3.4-2.6 6.8-3.8 10.2-26.4 69.7-53 141.3-79.8 214.7-.6 1.7-1.2 3.4-1.8 5l127.1-13.9c-2.5-.5-5.1-.8-7.6-1.3 152-18.9 354-132.5 484.6-272.7 60.2-64.6 114.8-140.8 165.3-230 37.6-66.4 72.9-140 106.5-221.5 29.4-71.2 57.6-148.3 84.8-231.9-34.9 18.4-74.9 31.9-119 41.3-7.7 1.6-15.6 3.2-23.6 4.6s-16.1 2.7-24.3 3.8c.1 0 .2-.1.3-.1 141.7-54.5 231.1-159.8 296.1-288.7-37.3 25.4-97.9 58.7-170.5 74.7-9.9 2.2-20 3.9-30.2 5.4-2.6.4-5.1.8-7.6 1.1.1 0 .1-.1.2-.1 0 0 .1 0 .1-.1 49.2-20.6 90.7-43.6 126.7-70.8 7.7-5.8 15.2-11.8 22.4-18.1 11-9.5 21.4-19.5 31.4-30 6.4-6.7 12.6-13.6 18.6-20.8 14.1-16.8 27.3-34.9 39.7-54.6 3.8-6 7.5-12.1 11.2-18.4 4.7-9.1 9.2-18 13.6-26.8 19.8-39.8 35.6-75.3 48.2-106.5 6.3-15.6 11.8-30 16.5-43.4 1.9-5.3 3.7-10.5 5.4-15.5 5-15 9.1-28.3 12.3-40 4.8-17.5 7.7-31.4 9.3-41.5-4.8 3.8-10.3 7.6-16.5 11.3-42.8 25.6-116.2 48.8-175.4 59.7l116.7-12.8-116.7 12.8c-.9.2-1.8.3-2.7.5-5.9 1-11.9 1.9-17.9 2.9 1.1-.5 2-1 3.1-1.4l-399.3 43.8c-.7 1.4-1.4 2.8-2.2 4.3z" fill="url(#f)"/><linearGradient id="g" x1="-9610.3" x2="-5351.2" y1="999.73" y2="999.73" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M1627.6 563.1c-35.5 54.5-74.3 116.4-116 186.5-2.2 3.6-4.4 7.4-6.6 11.1-36 60.7-74.3 127.3-114.5 200.3-34.8 63-71 130.6-108.6 203.3-32.8 63.3-66.7 130.5-101.5 201.6l399.3-43.8c116.3-53.5 168.3-101.9 218.8-171.9 13.4-19.3 26.9-39.5 40.3-60.4 41-64 81.2-134.5 117.2-204.6 34.7-67.7 65.3-134.8 88.8-195.3 14.9-38.5 26.9-74.3 35.2-105.7 7.3-27.7 13-54 17.4-79.1-155.5 26.5-345.9 51.9-469.8 58z" fill="url(#g)"/><path d="M1369.6 1939.4c-8 1.4-16.1 2.7-24.3 3.8 8.2-1.1 16.3-2.4 24.3-3.8z" fill="#BE202E"/><path d="M1369.6 1939.4c-8 1.4-16.1 2.7-24.3 3.8 8.2-1.1 16.3-2.4 24.3-3.8z" fill="#BE202E" opacity=".35"/><linearGradient id="h" x1="-9346.1" x2="-5087" y1="1152.7" y2="1152.7" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M1369.6 1939.4c-8 1.4-16.1 2.7-24.3 3.8 8.2-1.1 16.3-2.4 24.3-3.8z" fill="url(#h)"/><path d="M1433.2 1735.7c2.6-.3 5.1-.8 7.6-1.1-2.5.3-5 .7-7.6 1.1z" fill="#BE202E"/><path d="M1433.2 1735.7c2.6-.3 5.1-.8 7.6-1.1-2.5.3-5 .7-7.6 1.1z" fill="#BE202E" opacity=".35"/><linearGradient id="i" x1="-9346.1" x2="-5087" y1="1137.7" y2="1137.7" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M1433.2 1735.7c2.6-.3 5.1-.8 7.6-1.1-2.5.3-5 .7-7.6 1.1z" fill="url(#i)"/><path d="M1433.5 1735.6s.1 0 .1-.1c0 0-.1 0-.1.1z" fill="#BE202E"/><path d="M1433.5 1735.6s.1 0 .1-.1c0 0-.1 0-.1.1z" fill="#BE202E" opacity=".35"/><linearGradient id="j" x1="-6953.4" x2="-6012" y1="1134.7" y2="1134.7" gradientTransform="rotate(-65.001 -2052.931 -4777.847)" gradientUnits="userSpaceOnUse"><stop stop-color="#9E2064" offset=".323"/><stop stop-color="#C92037" offset=".63"/><stop stop-color="#CD2335" offset=".751"/><stop stop-color="#E97826" offset="1"/></linearGradient><path d="M1433.5 1735.6s.1 0 .1-.1c0 0-.1 0-.1.1z" fill="url(#j)"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-microsoftonline.svg b/web_src/svg/gitea-microsoftonline.svg
new file mode 100644
index 0000000..eb28296
--- /dev/null
+++ b/web_src/svg/gitea-microsoftonline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="url(#a)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#b)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#g)" d="M4.004 30.998Z"/><path fill="url(#h)" d="M4.004 30.998Z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="translate(17.4186 10.6383) rotate(110.528) scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="translate(10.4299 36.3511) rotate(-8.36717) scale(31.0503 20.5108)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-nextcloud.svg b/web_src/svg/gitea-nextcloud.svg
new file mode 100644
index 0000000..900dfa6
--- /dev/null
+++ b/web_src/svg/gitea-nextcloud.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" class="svg gitea-nextcloud" width="16" height="16" aria-hidden="true"><path fill="#0382c9" d="M12.5-.5h102a796.486 796.486 0 0 0 7 4.5 68.97 68.97 0 0 1 6 8.5v102a785.54 785.54 0 0 0-4.5 7 68.927 68.927 0 0 1-8.5 6h-102a779.088 779.088 0 0 0-7-4.5 68.94 68.94 0 0 1-6-8.5v-102a789.852 789.852 0 0 0 4.5-7 68.982 68.982 0 0 1 8.5-6Z" style="opacity:.997"/><path fill="#fafcfe" d="M55.5 37.5c16.649-2.824 28.149 3.51 34.5 19 6.704-10.658 15.537-12.825 26.5-6.5 9.555 10.007 9.222 19.673-1 29-10.492 5.063-18.992 2.897-25.5-6.5C83.734 86.553 72.9 92.72 57.5 91c-9.864-2.843-16.697-9.01-20.5-18.5-6.582 9.277-15.082 11.444-25.5 6.5-10.222-9.327-10.555-18.993-1-29 10.809-5.923 19.642-3.756 26.5 6.5 3.713-8.878 9.88-15.211 18.5-19Z" style="opacity:1"/><path fill="#0b83c9" d="M58.5 47.5c17.163-.677 23.996 7.323 20.5 24-6.538 9.38-15.038 11.546-25.5 6.5-10.605-12.356-8.939-22.523 5-30.5Z" style="opacity:1"/><path fill="#1986cb" d="M18.5 56.5c7.7-.138 10.867 3.529 9.5 11-4.041 4.813-8.375 5.146-13 1-1.407-4.857-.24-8.857 3.5-12Z" style="opacity:1"/><path fill="#2088cb" d="M103.5 56.5c7.801.619 10.635 4.619 8.5 12-4.709 4.27-9.042 3.936-13-1-1.607-5.062-.107-8.728 4.5-11Z" style="opacity:1"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-npm.svg b/web_src/svg/gitea-npm.svg
new file mode 100644
index 0000000..c6d1108
--- /dev/null
+++ b/web_src/svg/gitea-npm.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="540px" height="210px" version="1.1" viewBox="0 0 18 7" xmlns="http://www.w3.org/2000/svg">
+<path d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z" fill="#CB3837"/>
+<polygon points="1 5 3 5 3 2 4 2 4 5 5 5 5 1 1 1" fill="#fff"/>
+<path d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z" fill="#fff"/>
+<polygon points="11 1 11 5 13 5 13 2 14 2 14 5 15 5 15 2 16 2 16 5 17 5 17 1" fill="#fff"/>
+</svg>
diff --git a/web_src/svg/gitea-nuget.svg b/web_src/svg/gitea-nuget.svg
new file mode 100644
index 0000000..f92fb0f
--- /dev/null
+++ b/web_src/svg/gitea-nuget.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<defs>
+<polygon id="a" points="0 46.021 0 3.7003 84.652 3.7003 84.652 88.342 0 88.342"/>
+</defs>
+<g fill="none" fill-rule="evenodd">
+<g transform="translate(0 6)" fill="#004880" fill-rule="evenodd">
+<path d="m374.42 454.86c-46.749 0-84.652-37.907-84.652-84.661 0-46.733 37.903-84.661 84.652-84.661s84.652 37.928 84.652 84.661c0 46.754-37.903 84.661-84.652 84.661m-168.86-194.04c-29.226 0-52.908-23.705-52.908-52.913 0-29.229 23.681-52.913 52.908-52.913 29.226 0 52.908 23.684 52.908 52.913 0 29.208-23.681 52.913-52.908 52.913m172.61-165.17h-141.28c-71.997 0-130.41 58.416-130.41 130.44v141.28c0 72.046 58.41 130.42 130.41 130.42h141.28c72.039 0 130.41-58.374 130.41-130.42v-141.28c0-72.025-58.368-130.44-130.41-130.44"/>
+<mask id="b" fill="white">
+<use xlink:href="#a"/>
+</mask>
+<path d="m84.652 46.012c0 23.388-18.962 42.33-42.326 42.33-23.385 0-42.326-18.943-42.326-42.33 0-23.366 18.941-42.33 42.326-42.33 23.364 0 42.326 18.964 42.326 42.33" mask="url(#b)"/>
+</g>
+</g>
+</svg>
diff --git a/web_src/svg/gitea-onedev.svg b/web_src/svg/gitea-onedev.svg
new file mode 100644
index 0000000..490c22f
--- /dev/null
+++ b/web_src/svg/gitea-onedev.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
+ preserveAspectRatio="xMidYMid meet">
+
+<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M3155 6004 c-295 -40 -558 -120 -812 -248 l-113 -56 -52 40 c-149
+113 -376 149 -558 90 -191 -63 -351 -222 -411 -410 -27 -83 -36 -229 -19 -312
+15 -80 50 -165 91 -225 31 -47 31 -48 14 -78 -235 -405 -344 -749 -375 -1182
+-11 -157 -1 -402 21 -530 l11 -65 -49 -44 c-28 -23 -75 -76 -106 -116 -194
+-255 -247 -579 -144 -883 92 -269 312 -488 584 -581 206 -69 406 -70 611 -1
+l67 22 105 -71 c456 -310 920 -455 1460 -455 330 0 616 52 910 164 676 258
+1229 811 1484 1484 l27 72 77 38 c91 45 211 157 259 243 211 375 -10 843 -432
+917 -199 35 -393 -27 -539 -172 -71 -71 -117 -145 -153 -247 -34 -94 -38 -258
+-9 -353 28 -95 85 -193 153 -264 72 -76 72 -60 0 -205 -89 -181 -203 -341
+-352 -495 -346 -357 -782 -563 -1283 -603 -428 -34 -893 89 -1250 330 -11 8
+-10 17 8 52 121 236 135 537 39 780 -87 218 -275 416 -486 512 -90 41 -227 74
+-340 83 l-91 7 -8 96 c-35 469 135 998 455 1417 65 86 243 267 336 342 438
+356 1013 528 1581 472 399 -39 790 -191 1106 -430 169 -128 375 -349 486 -520
+l43 -67 -33 -52 c-29 -47 -33 -63 -36 -134 -3 -73 -1 -86 25 -136 32 -61 102
+-120 163 -139 228 -68 430 169 326 382 -31 64 -93 122 -147 138 -25 8 -41 21
+-52 43 -9 17 -32 58 -51 92 l-35 60 36 50 c177 244 158 575 -44 794 -80 86
+-155 136 -259 172 -198 68 -389 42 -565 -78 l-78 -53 -153 74 c-279 134 -550
+213 -840 244 -133 15 -481 12 -603 -5z"/>
+<path d="M2718 4289 c-139 -21 -305 -173 -405 -374 -183 -364 -134 -815 98
+-915 152 -65 345 27 486 232 55 80 97 157 90 165 -3 2 -20 -3 -38 -12 -24 -13
+-51 -16 -105 -13 -61 3 -79 8 -116 34 -89 62 -124 191 -79 290 24 52 90 108
+147 124 91 26 200 -14 252 -92 l27 -42 3 124 c4 189 -34 316 -124 405 -63 63
+-142 88 -236 74z"/>
+<path d="M4205 4290 c-116 -19 -202 -113 -249 -270 -21 -69 -31 -200 -22 -274
+l8 -57 21 32 c102 150 316 140 399 -20 60 -115 15 -251 -104 -312 -50 -25
+-150 -26 -200 -1 l-36 19 14 -33 c61 -145 200 -301 323 -361 57 -28 144 -40
+204 -29 52 10 121 61 161 119 181 264 81 790 -200 1058 -108 102 -216 146
+-319 129z"/>
+<path d="M3225 2681 c-161 -16 -235 -61 -235 -143 0 -114 130 -211 340 -254
+102 -20 312 -15 405 10 135 37 238 103 276 177 49 97 -2 171 -138 200 -61 12
+-542 20 -648 10z"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-open-with-jetbrains.svg b/web_src/svg/gitea-open-with-jetbrains.svg
new file mode 100644
index 0000000..a7884c4
--- /dev/null
+++ b/web_src/svg/gitea-open-with-jetbrains.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
+<g>
+ <g>
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893">
+ <stop offset="0.2581" style="stop-color:#F97A12"/>
+ <stop offset="0.4591" style="stop-color:#B07B58"/>
+ <stop offset="0.7241" style="stop-color:#577BAE"/>
+ <stop offset="0.9105" style="stop-color:#1E7CE5"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 "/>
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57">
+ <stop offset="0" style="stop-color:#F97A12"/>
+ <stop offset="7.179946e-002" style="stop-color:#CB7A3E"/>
+ <stop offset="0.1541" style="stop-color:#9E7B6A"/>
+ <stop offset="0.242" style="stop-color:#757B91"/>
+ <stop offset="0.3344" style="stop-color:#537BB1"/>
+ <stop offset="0.4324" style="stop-color:#387CCC"/>
+ <stop offset="0.5381" style="stop-color:#237CE0"/>
+ <stop offset="0.6552" style="stop-color:#147CEF"/>
+ <stop offset="0.7925" style="stop-color:#0B7CF7"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 "/>
+ <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191">
+ <stop offset="0" style="stop-color:#FE315D"/>
+ <stop offset="7.840246e-002" style="stop-color:#CB417E"/>
+ <stop offset="0.1601" style="stop-color:#9E4E9B"/>
+ <stop offset="0.2474" style="stop-color:#755BB4"/>
+ <stop offset="0.3392" style="stop-color:#5365CA"/>
+ <stop offset="0.4365" style="stop-color:#386DDB"/>
+ <stop offset="0.5414" style="stop-color:#2374E9"/>
+ <stop offset="0.6576" style="stop-color:#1478F3"/>
+ <stop offset="0.794" style="stop-color:#0B7BF8"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 "/>
+ <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58">
+ <stop offset="0" style="stop-color:#FE315D"/>
+ <stop offset="4.023279e-002" style="stop-color:#F63462"/>
+ <stop offset="0.1037" style="stop-color:#DF3A71"/>
+ <stop offset="0.1667" style="stop-color:#C24383"/>
+ <stop offset="0.2912" style="stop-color:#AD4A91"/>
+ <stop offset="0.5498" style="stop-color:#755BB4"/>
+ <stop offset="0.9175" style="stop-color:#1D76ED"/>
+ <stop offset="1" style="stop-color:#087CFA"/>
+ </linearGradient>
+ <polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 "/>
+ </g>
+ <g>
+ <rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/>
+ <rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
+ <polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37
+ 29.4,33.7 26.9,33.7 26.9,22.4 "/>
+ <path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3
+ c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2
+ c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/>
+ </g>
+</g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-open-with-vscode.svg b/web_src/svg/gitea-open-with-vscode.svg
new file mode 100644
index 0000000..62cd1a3
--- /dev/null
+++ b/web_src/svg/gitea-open-with-vscode.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="-1 -1 34 34"><path d="M30.9 3.4 24.3.3a2 2 0 0 0-2.3.4L9.4 12.2 3.9 8c-.5-.4-1.2-.4-1.7 0L.4 9.8c-.5.5-.5 1.4 0 2L5.2 16 .4 20.3c-.5.6-.5 1.5 0 2L2.2 24c.5.5 1.2.5 1.7 0l5.5-4L22 31.2a2 2 0 0 0 2.3.4l6.6-3.2a2 2 0 0 0 1.1-1.8V5.2a2 2 0 0 0-1.1-1.8zM24 23.3 14.4 16 24 8.7z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-open-with-vscodium.svg b/web_src/svg/gitea-open-with-vscodium.svg
new file mode 100644
index 0000000..483676f
--- /dev/null
+++ b/web_src/svg/gitea-open-with-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-openid.svg b/web_src/svg/gitea-openid.svg
new file mode 100644
index 0000000..262ca10
--- /dev/null
+++ b/web_src/svg/gitea-openid.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 2400 2400"><path fill="#ff7c00" d="m1270 218.3-173.1 84.3-.7 981.8c-.3 540 .2 981.4 1.1 981l174.5-81.8 172.3-80.8v-984.4c0-541.7-.2-984.7-.4-984.4z"/><path fill="#aaa" d="M981.9 785.5c-425.3 63.2-766.5 264.1-889 523a491.5 491.5 0 0 0-43.6 146c-4.2 29.2-4.7 95-1.2 124 19 152.6 115.2 299.9 273.2 418.8 147.7 111 350.5 196.5 568.6 239.7 59 11.6 179 29 200.5 29 2.3 0 3-23.2 3-109.1v-109.2l-5.1-1-37.9-6a1182 1182 0 0 1-305.4-90.6c-122.2-55.7-225.1-137.7-284.6-226.4-107.5-160.5-81.3-344.3 70-491.3 57-55.5 115.4-95.2 199.5-136.1a1112.6 1112.6 0 0 1 269.4-89.2l29.7-6c3.7-1.2 4-8.6 4-111.5V779.5l-6.3.2a823 823 0 0 0-44.8 5.8zm525 104c0 103 .2 110.4 4.1 111.6l29.5 6a1221.6 1221.6 0 0 1 207.7 61.3A1087.8 1087.8 0 0 1 1862 1123c4.6 3.7 1.4 5.8-88 56-51.1 28.5-93 52.7-93 53.4 0 1.9 671.6 146.8 673.2 145.2 1.2-1.2-45.5-496-47-497.6-.2-.2-38.5 21-85 47.2l-89.6 50.2c-4.2 2-8.8.2-27.9-10.7-130.8-75-289.6-132.2-460.8-166.1a1870.8 1870.8 0 0 0-132.9-21.1c-4 0-4.2 6.7-4.2 110z"/><path fill="#cbaa7c" d="M1094.5 2156.9c0 60.6.3 85.5.5 55 .5-30.2.5-79.9 0-110.3-.2-30.2-.5-5.3-.5 55.3z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-pub.svg b/web_src/svg/gitea-pub.svg
new file mode 100644
index 0000000..5ec2b88
--- /dev/null
+++ b/web_src/svg/gitea-pub.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 502.87 502.87" xmlns="http://www.w3.org/2000/svg">
+<defs>
+ <style>.cls-1{fill:#01579b;}.cls-2{fill:#40c4ff;}.cls-3{fill:#29b6f6;}.cls-4{fill:#fff;}.cls-4,.cls-5,.cls-6{opacity:0.2;isolation:isolate;}.cls-5{fill:#263238;}.cls-6{fill:url(#a);}</style>
+ <radialGradient id="a" cx="251.42" cy="631.97" r="251.4" gradientTransform="translate(0 -380.56)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#fff" stop-opacity=".1" offset="0"/>
+ <stop stop-color="#fff" stop-opacity="0" offset="1"/>
+ </radialGradient>
+</defs>
+<path class="cls-1" d="M102.56,400.31l-86-86C6.32,303.82,0,289,0,274.58c0-6.69,3.77-17.16,6.62-23.15L86,86Z"/>
+<path class="cls-2" d="M397,102.56l-86-86C303.49,9,287.85,0,274.61,0c-11.38,0-22.55,2.29-29.76,6.62L86.07,86Z"/>
+<polygon class="cls-2" points="205.11 502.87 413.55 502.87 413.55 413.55 258.05 363.9 115.79 413.55"/>
+<path class="cls-3" d="M86,354c0,26.54,3.33,33.05,16.53,46.32l13.23,13.24H413.55L268,248.14,86,86Z"/>
+<path class="cls-1" d="M350.7,86H86L413.55,413.51h89.32V208.4L397,102.52C382.12,87.62,368.92,86,350.7,86Z"/>
+<path class="cls-4" d="M105.88,403.6C92.65,390.33,89.36,377.24,89.36,354V89.32L86.07,86V354c0,23.25,0,29.69,19.81,49.61l9.91,9.91h0Z"/>
+<polygon class="cls-5" points="499.58 205.11 499.58 410.22 410.26 410.22 413.55 413.55 502.87 413.55 502.87 208.4"/>
+<path class="cls-4" d="M397,102.56C380.61,86.14,367.19,86,347.41,86H86.07l3.29,3.29H347.41c9.87,0,34.79-1.66,49.61,13.24Z"/>
+<path class="cls-6" d="M499.58,205.11,397,102.56l-86-86C303.49,9,287.85,0,274.61,0c-11.38,0-22.55,2.29-29.76,6.62L86.07,86,6.65,251.43C3.81,257.46,0,267.92,0,274.58c0,14.45,6.36,29.2,16.52,39.7L95.83,393a92.42,92.42,0,0,0,6.73,7.32l3.29,3.29,9.9,9.91,86,86,3.29,3.29h208.4V413.51h89.32V208.4Z"/>
+</svg>
diff --git a/web_src/svg/gitea-python.svg b/web_src/svg/gitea-python.svg
new file mode 100644
index 0000000..b1b19b4
--- /dev/null
+++ b/web_src/svg/gitea-python.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 110.42 109.85" xmlns="http://www.w3.org/2000/svg">
+<defs>
+<linearGradient id="b" x1="89.137" x2="147.78" y1="111.92" y2="168.1" gradientUnits="userSpaceOnUse">
+<stop stop-color="#ffe052" offset="0"/>
+<stop stop-color="#ffc331" offset="1"/>
+</linearGradient>
+<linearGradient id="a" x1="55.549" x2="110.15" y1="77.07" y2="131.85" gradientUnits="userSpaceOnUse">
+<stop stop-color="#387eb8" offset="0"/>
+<stop stop-color="#366994" offset="1"/>
+</linearGradient>
+</defs>
+<g transform="translate(-473.36 -251.72)">
+<g transform="translate(428.42 184.26)">
+<path d="m99.75 67.469c-28.032 2e-6 -26.281 12.156-26.281 12.156l0.03125 12.594h26.75v3.7812h-37.375s-17.938-2.0343-17.938 26.25c-2e-6 28.284 15.656 27.281 15.656 27.281h9.3438v-13.125s-0.50365-15.656 15.406-15.656h26.531s14.906 0.24096 14.906-14.406v-24.219c0-2e-6 2.2632-14.656-27.031-14.656zm-14.75 8.4688c2.6614-2e-6 4.8125 2.1511 4.8125 4.8125 2e-6 2.6614-2.1511 4.8125-4.8125 4.8125-2.6614 2e-6 -4.8125-2.1511-4.8125-4.8125-2e-6 -2.6614 2.1511-4.8125 4.8125-4.8125z" color="#000000" fill="url(#a)"/>
+<path d="m100.55 177.31c28.032 0 26.281-12.156 26.281-12.156l-0.03125-12.594h-26.75v-3.7812h37.375s17.938 2.0343 17.938-26.25c1e-5 -28.284-15.656-27.281-15.656-27.281h-9.3438v13.125s0.50366 15.656-15.406 15.656h-26.531s-14.906-0.24096-14.906 14.406v24.219s-2.2632 14.656 27.031 14.656zm14.75-8.4688c-2.6614 0-4.8125-2.1511-4.8125-4.8125s2.1511-4.8125 4.8125-4.8125 4.8125 2.1511 4.8125 4.8125c1e-5 2.6614-2.1511 4.8125-4.8125 4.8125z" color="#000000" fill="url(#b)"/>
+</g>
+</g>
+</svg>
diff --git a/web_src/svg/gitea-rpm.svg b/web_src/svg/gitea-rpm.svg
new file mode 100644
index 0000000..eba9dd0
--- /dev/null
+++ b/web_src/svg/gitea-rpm.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="920" height="537.4" viewBox="0 0 640 409" xmlns="http://www.w3.org/2000/svg">
+<g transform="matrix(1.111 0 0 1.111 -37.67 -15.95)">
+<path d="m242.1 26.14c12.57-1.932 25.26-3.344 37.97-4.014 2.258-0.538 4.594-0.4281 6.901-0.5105 18.04-0.5753 36.1-0.5493 54.13 0.2629 32.54 2.301 64.92 7.707 96.31 16.72l-0.1329 0.1763c-4.602 6.224-9.218 12.44-13.73 18.73-1.352 0.2495-2.536-0.6058-3.74-1.036-23.82-9.827-49.17-15.53-74.68-18.62-41.97-4.298-84.94-0.8447-125.2 12.07-29.64 9.588-58.27 24.37-80.38 46.67-15.67 16.02-27.69 36.74-29.33 59.45-1.519 10.82 1.072 21.62 3.364 32.13l-99.42 17.53c-0.2403-17.54 4.337-34.9 11.62-50.77 14.49-30.9 39.62-55.59 67.61-74.49 18.93-12.71 39.39-23.08 60.65-31.3 28.32-11.07 57.98-18.63 88.06-22.99z" fill="#040404"/>
+<path d="m423.6 57.5c4.513-6.288 9.128-12.5 13.73-18.73 42.15 12.34 82.85 31.84 116.1 60.88 13.59 12.17 25.89 25.94 35.33 41.61 11.18 18.44 18.27 39.56 19.03 61.19-33.06-5.778-66.1-11.6-99.16-17.4 4.068-12.94 4.103-27.02 1.317-40.23-5.163-22.82-19.79-42.41-37.44-57.28-14.71-12.38-31.49-22.3-49.27-29.56z" fill="#d72123"/>
+<path d="m301.7 148.6c1.946-3.169 5.372-5.696 9.269-5.227 4.405 0.1127 7.799 3.752 8.711 7.872l310.8 55.28-1.038 5.206-310.7-54.48c-2.478 4.312-8.228 6.539-12.74 3.993-3.131-1.369-4.2-4.72-5.613-7.531l-19.13-3.065 0.8769-5.381z"/>
+<path d="m156.1 299.5 0.1743-77.35 32.9 0.0614c34.68 0.0648 42.48 0.5632 57.86 3.698 37.71 7.684 62.74 25.76 67.65 48.85 4.979 23.41-13.56 44.79-51.04 58.88-13.48 5.067-31.77 9.041-46.31 10.07l-6.231 0.4388-0.373 24.86-2.212 0.3956c-1.217 0.2176-12.54 1.804-25.17 3.526s-23.96 3.307-25.19 3.524l-2.23 0.3938zm60.01 29.84c1.769-0.4157 6.3-2.234 10.07-4.041 14.18-6.795 24.53-16.45 30.1-28.09 4.309-8.991 4.629-17.43 1.027-27.06-4.125-11.03-15.91-23.02-29.19-29.72-4.374-2.206-13.08-4.908-15.8-4.908h-1.556v47.29c0 42.35 0.1115 47.29 1.067 47.29 0.5869 0 2.514-0.3402 4.283-0.7559zm-202-46.87v-60.65h54.83v1.456c0 1.28 0.2694 1.401 2.227 0.9981 12.87-2.647 32.27-3.843 40.59-2.5 2.889 0.466 7.351 1.561 9.916 2.433 5.431 1.847 15.09 6.023 15.08 6.522-3e-3 0.1885-2.541 1.86-5.641 3.714s-9.715 5.957-14.7 9.117l-9.064 5.745-2.566-1.744c-5.163-3.508-10.69-4.839-20.08-4.839-4.957 0-10.08 0.3898-12.16 0.925l-3.598 0.925v98.55h-54.83zm337.2-0.0456v-60.7l107.4 0.2996c109 0.304 112.3 0.3808 126.2 2.928 10.41 1.908 16.53 4.219 19.79 7.479l2.913 2.911v107.8h-54.83v-106.1l-2.913-1.252c-2.549-1.096-5.417-1.283-22.96-1.499l-20.05-0.2471v109.1h-54.82l-0.3511-106.4-2.741-1.055c-2.177-0.838-6.871-1.11-22.79-1.323l-20.05-0.2672v109.1h-54.83z" fill="#040404" stroke-width=".6853"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-rubygems.svg b/web_src/svg/gitea-rubygems.svg
new file mode 100644
index 0000000..d3eb4f7
--- /dev/null
+++ b/web_src/svg/gitea-rubygems.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 198.13 197.58" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="p" x1="194.9" x2="141.03" y1="153.56" y2="117.41" gradientUnits="userSpaceOnUse"><stop stop-color="#871101" offset="0"/><stop stop-color="#911209" offset=".99"/><stop stop-color="#911209" offset="1"/></linearGradient><linearGradient id="o" x1="151.8" x2="97.93" y1="217.79" y2="181.64" gradientUnits="userSpaceOnUse"><stop stop-color="#871101" offset="0"/><stop stop-color="#911209" offset=".99"/><stop stop-color="#911209" offset="1"/></linearGradient><linearGradient id="n" x1="38.696" x2="47.047" y1="127.39" y2="181.66" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#E57252" offset=".23"/><stop stop-color="#DE3B20" offset=".46"/><stop stop-color="#A60003" offset=".99"/><stop stop-color="#A60003" offset="1"/></linearGradient><linearGradient id="m" x1="96.133" x2="99.21" y1="76.715" y2="132.1" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#E4714E" offset=".23"/><stop stop-color="#BE1A0D" offset=".56"/><stop stop-color="#A80D00" offset=".99"/><stop stop-color="#A80D00" offset="1"/></linearGradient><linearGradient id="l" x1="147.1" x2="156.31" y1="25.521" y2="65.216" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#E46342" offset=".18"/><stop stop-color="#C82410" offset=".4"/><stop stop-color="#A80D00" offset=".99"/><stop stop-color="#A80D00" offset="1"/></linearGradient><linearGradient id="k" x1="118.98" x2="158.67" y1="11.542" y2="-8.3048" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#C81F11" offset=".54"/><stop stop-color="#BF0905" offset=".99"/><stop stop-color="#BF0905" offset="1"/></linearGradient><linearGradient id="j" x1="3.9033" x2="7.1702" y1="113.55" y2="146.26" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#DE4024" offset=".31"/><stop stop-color="#BF190B" offset=".99"/><stop stop-color="#BF190B" offset="1"/></linearGradient><linearGradient id="i" x1="-18.556" x2="135.02" y1="155.1" y2="-2.8093" gradientUnits="userSpaceOnUse"><stop stop-color="#BD0012" offset="0"/><stop stop-color="#fff" offset=".07"/><stop stop-color="#fff" offset=".17"/><stop stop-color="#C82F1C" offset=".27"/><stop stop-color="#820C01" offset=".33"/><stop stop-color="#A31601" offset=".46"/><stop stop-color="#B31301" offset=".72"/><stop stop-color="#E82609" offset=".99"/><stop stop-color="#E82609" offset="1"/></linearGradient><linearGradient id="h" x1="99.075" x2="52.818" y1="171.03" y2="159.62" gradientUnits="userSpaceOnUse"><stop stop-color="#8C0C01" offset="0"/><stop stop-color="#990C00" offset=".54"/><stop stop-color="#A80D0E" offset=".99"/><stop stop-color="#A80D0E" offset="1"/></linearGradient><linearGradient id="g" x1="178.53" x2="137.43" y1="115.51" y2="78.684" gradientUnits="userSpaceOnUse"><stop stop-color="#7E110B" offset="0"/><stop stop-color="#9E0C00" offset=".99"/><stop stop-color="#9E0C00" offset="1"/></linearGradient><linearGradient id="f" x1="193.62" x2="173.15" y1="47.937" y2="26.054" gradientUnits="userSpaceOnUse"><stop stop-color="#79130D" offset="0"/><stop stop-color="#9E120B" offset=".99"/><stop stop-color="#9E120B" offset="1"/></linearGradient><radialGradient id="e" cx="143.83" cy="79.388" r="50.358" gradientUnits="userSpaceOnUse"><stop stop-color="#A80D00" offset="0"/><stop stop-color="#7E0E08" offset=".99"/><stop stop-color="#7E0E08" offset="1"/></radialGradient><radialGradient id="d" cx="74.092" cy="145.75" r="66.944" gradientUnits="userSpaceOnUse"><stop stop-color="#A30C00" offset="0"/><stop stop-color="#800E08" offset=".99"/><stop stop-color="#800E08" offset="1"/></radialGradient><linearGradient id="c" x1="26.67" x2="9.9887" y1="197.34" y2="140.74" gradientUnits="userSpaceOnUse"><stop stop-color="#8B2114" offset="0"/><stop stop-color="#9E100A" offset=".43"/><stop stop-color="#B3100C" offset=".99"/><stop stop-color="#B3100C" offset="1"/></linearGradient><linearGradient id="b" x1="154.64" x2="192.04" y1="9.7979" y2="26.306" gradientUnits="userSpaceOnUse"><stop stop-color="#B31000" offset="0"/><stop stop-color="#910F08" offset=".44"/><stop stop-color="#791C12" offset=".99"/><stop stop-color="#791C12" offset="1"/></linearGradient><linearGradient id="a" x1="174.07" x2="132.28" y1="215.55" y2="141.75" gradientUnits="userSpaceOnUse"><stop stop-color="#FB7655" offset="0"/><stop stop-color="#E42B1E" offset=".41"/><stop stop-color="#900" offset=".99"/><stop stop-color="#900" offset="1"/></linearGradient></defs>
+<polygon points="153.5 130.41 40.38 197.58 186.85 187.64 198.13 39.95" clip-rule="evenodd" fill="url(#a)" fill-rule="evenodd"/><polygon points="187.09 187.54 174.5 100.65 140.21 145.93" clip-rule="evenodd" fill="url(#p)" fill-rule="evenodd"/><polygon points="187.26 187.54 95.03 180.3 40.87 197.39" clip-rule="evenodd" fill="url(#o)" fill-rule="evenodd"/><polygon points="41 197.41 64.04 121.93 13.34 132.77" clip-rule="evenodd" fill="url(#n)" fill-rule="evenodd"/><polygon points="140.2 146.18 119 63.14 58.33 120.01" clip-rule="evenodd" fill="url(#m)" fill-rule="evenodd"/><polygon points="193.32 64.31 135.97 17.47 120 69.1" clip-rule="evenodd" fill="url(#l)" fill-rule="evenodd"/><polygon points="166.5 0.77 132.77 19.41 111.49 0.52" clip-rule="evenodd" fill="url(#k)" fill-rule="evenodd"/><polygon points="0 158.09 14.13 132.32 2.7 101.62" clip-rule="evenodd" fill="url(#j)" fill-rule="evenodd"/><path d="m1.94 100.65 11.5 32.62 49.97-11.211 57.05-53.02 16.1-51.139-25.351-17.9-43.1 16.13c-13.579 12.63-39.929 37.62-40.879 38.09-0.94 0.48-17.4 31.59-25.29 46.43z" clip-rule="evenodd" fill="#fff" fill-rule="evenodd"/><path d="m42.32 42.05c29.43-29.18 67.37-46.42 81.93-31.73 14.551 14.69-0.88 50.39-30.31 79.56s-66.9 47.36-81.45 32.67c-14.56-14.68 0.4-51.33 29.83-80.5z" clip-rule="evenodd" fill="url(#i)" fill-rule="evenodd"/><path d="m41 197.38 22.86-75.72 75.92 24.39c-27.45 25.74-57.98 47.5-98.78 51.33z" clip-rule="evenodd" fill="url(#h)" fill-rule="evenodd"/><path d="m120.56 68.89 19.49 77.2c22.93-24.11 43.51-50.03 53.589-82.09l-73.079 4.89z" clip-rule="evenodd" fill="url(#g)" fill-rule="evenodd"/><path d="m193.44 64.39c7.8-23.54 9.6-57.31-27.181-63.58l-30.18 16.67 57.361 46.91z" clip-rule="evenodd" fill="url(#f)" fill-rule="evenodd"/><path d="m0 157.75c1.08 38.851 29.11 39.43 41.05 39.771l-27.58-64.411-13.47 24.64z" clip-rule="evenodd" fill="#9e1209" fill-rule="evenodd"/><path d="m120.67 69.01c17.62 10.83 53.131 32.58 53.851 32.98 1.119 0.63 15.31-23.93 18.53-37.81l-72.381 4.83z" clip-rule="evenodd" fill="url(#e)" fill-rule="evenodd"/><path d="m63.83 121.66 30.56 58.96c18.07-9.8 32.22-21.74 45.18-34.53l-75.74-24.43z" clip-rule="evenodd" fill="url(#d)" fill-rule="evenodd"/><path d="m13.35 133.19-4.33 51.56c8.17 11.16 19.41 12.13 31.2 11.26-8.53-21.23-25.57-63.68-26.87-62.82z" clip-rule="evenodd" fill="url(#c)" fill-rule="evenodd"/><path d="m135.9 17.61 60.71 8.52c-3.24-13.73-13.19-22.59-30.15-25.36l-30.56 16.84z" clip-rule="evenodd" fill="url(#b)" fill-rule="evenodd"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-split.svg b/web_src/svg/gitea-split.svg
new file mode 100644
index 0000000..e5686b2
--- /dev/null
+++ b/web_src/svg/gitea-split.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M7.25 14.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 12a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 12ZM7.25 8.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM8 6a.75.75 0 0 1-.75-.75v-.5a.75.75 0 0 1 1.5 0v.5A.75.75 0 0 1 8 6ZM7.25 2.25a.75.75 0 0 0 1.5 0v-.5a.75.75 0 0 0-1.5 0zM11.35 8.75a.75.75 0 0 1 0-1.5h1.25V5.1a.25.25 0 0 1 .43-.17l2.9 2.9a.25.25 0 0 1 0 .35l-2.9 2.9a.25.25 0 0 1-.43-.18V8.75ZM3.4 10.9V8.75h1.25a.75.75 0 0 0 0-1.5H3.4V5.1a.25.25 0 0 0-.43-.17l-2.9 2.9a.25.25 0 0 0 0 .35l2.9 2.9a.25.25 0 0 0 .43-.18z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-swift.svg b/web_src/svg/gitea-swift.svg
new file mode 100644
index 0000000..8af43d3
--- /dev/null
+++ b/web_src/svg/gitea-swift.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 59.5 59.5" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
+ <path d="m59.387 16.45c-0.0035-0.5973-0.0101-1.1943-0.0266-1.7923-0.0348-1.3008-0.1117-2.6134-0.3429-3.9003-0.2346-1.3069-0.6181-2.5221-1.2225-3.7093-0.5933-1.1659-1.3689-2.2328-2.2941-3.158-0.925-0.9252-1.9919-1.7007-3.1583-2.2943-1.1862-0.6037-2.4016-0.9871-3.7073-1.2217-1.2876-0.2319-2.6002-0.3083-3.902-0.3435-0.5977-0.0162-1.1948-0.023-1.7923-0.0267-0.7092-4e-3 -1.4189-4e-3 -2.1279-4e-3h-22.235c-0.7097 0-1.4186 0-2.1276 0.0041-0.5977 0.0037-1.1955 0.0105-1.7923 0.0267-0.3254 0.0088-0.6515 0.0202-0.9778 0.0359-0.9788 0.0472-1.9591 0.1337-2.9243 0.3076-0.9793 0.176-1.9079 0.4356-2.8113 0.8091-0.3011 0.1244-0.5995 0.2616-0.8961 0.4125-0.8748 0.4452-1.6938 0.9927-2.4387 1.6289-0.2483 0.2121-0.4884 0.434-0.7196 0.6653-0.9254 0.9252-1.701 1.9921-2.2943 3.158-0.6044 1.1872-0.9874 2.4024-1.2222 3.7093-0.231 1.2869-0.3078 2.5995-0.3428 3.9003-0.0164 0.598-0.0233 1.195-0.0272 1.7923-0.0045 0.7094-0.0039 1.4189-0.0039 2.1281v22.234c0 0.7099-7e-4 1.4187 0.0039 2.1286 0.0039 0.5973 0.0108 1.1943 0.0272 1.7913 0.035 1.3015 0.1117 2.6144 0.3428 3.9007 0.2348 1.3065 0.6178 2.5228 1.2222 3.7097 0.5933 1.1662 1.3689 2.2328 2.2943 3.1576 0.9247 0.9256 1.9919 1.701 3.1584 2.295 1.1863 0.6038 2.4016 0.9867 3.7076 1.2213 1.2868 0.2316 2.6004 0.3086 3.9019 0.3434 0.5968 0.0159 1.1946 0.023 1.7923 0.0264 0.709 0.0051 1.4179 0.0044 2.1276 0.0044h22.235c0.709 0 1.4187 7e-4 2.1278-0.0044 0.5975-0.0034 1.1946-0.0105 1.7923-0.0264 1.3018-0.0348 2.6144-0.1119 3.902-0.3434 1.3057-0.2346 2.5211-0.6176 3.7073-1.2213 1.1664-0.5939 2.2333-1.3694 3.1583-2.295 0.9252-0.9249 1.7009-1.9914 2.2941-3.1576 0.6044-1.1869 0.9879-2.4031 1.2225-3.7097 0.2312-1.2863 0.3081-2.5992 0.3429-3.9007 0.0164-0.597 0.023-1.1939 0.0266-1.7913 0.0046-0.7099 0.0042-1.4187 0.0042-2.1286v-22.234c1e-4 -0.7092 4e-4 -1.4187-0.0041-2.128z" fill="#F05138"/>
+ <path d="m47.061 36.661c-0.0014-0.0018-0.0027-0.0031-0.0042-0.0048 0.0657-0.2236 0.1335-0.4458 0.191-0.675 2.465-9.8209-3.5511-21.432-13.732-27.545 4.4613 6.0479 6.4339 13.373 4.6813 19.78-0.1563 0.5714-0.3442 1.1198-0.5519 1.6528-0.2254-0.1481-0.5094-0.3162-0.8908-0.5265 0 0-10.127-6.2527-21.103-17.312-0.288-0.2903 5.8528 8.777 12.822 16.14-3.2834-1.8427-12.434-8.5004-18.227-13.802 0.7117 1.1869 1.5582 2.3298 2.4887 3.4301 4.8375 6.1349 11.146 13.704 18.704 19.517-5.3104 3.2498-12.814 3.5025-20.285 0.0034-1.8479-0.866-3.5851-1.9109-5.1932-3.0981 3.1625 5.0585 8.0332 9.4229 13.961 11.971 7.0695 3.0381 14.1 2.8321 19.336 0.0498l-0.0041 6e-3c0.0239-0.0151 0.0543-0.0316 0.0791-0.0469 0.215-0.1156 0.4284-0.2333 0.6371-0.3576 2.5157-1.3058 7.4847-2.6306 10.152 2.5588 0.6532 1.27 2.0412-5.4604-3.0617-11.739z" fill="#fff"/>
+</svg>
diff --git a/web_src/svg/gitea-twitter.svg b/web_src/svg/gitea-twitter.svg
new file mode 100644
index 0000000..f972d23
--- /dev/null
+++ b/web_src/svg/gitea-twitter.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23l-8.905-12.684zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982l-5.926-8.3z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-unlock.svg b/web_src/svg/gitea-unlock.svg
new file mode 100644
index 0000000..1b32b8c
--- /dev/null
+++ b/web_src/svg/gitea-unlock.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16"><path d="M12.5 8H4.75V4.778c0-1.237.99-2.265 2.228-2.278A2.253 2.253 0 019.25 4.75v.5c0 .416.334.75.75.75h1c.416 0 .75-.334.75-.75v-.5c0-2.625-2.14-4.76-4.766-4.75C4.36.01 2.25 2.172 2.25 4.797V8H1.5A1.5 1.5 0 000 9.5v5A1.5 1.5 0 001.5 16h11a1.5 1.5 0 001.5-1.5v-5A1.5 1.5 0 0012.5 8z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-vagrant.svg b/web_src/svg/gitea-vagrant.svg
new file mode 100644
index 0000000..37bc167
--- /dev/null
+++ b/web_src/svg/gitea-vagrant.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 255 263" xmlns="http://www.w3.org/2000/svg">
+<polygon points="254.22 20.234 196.03 53.47 194.39 74.088 150.2 173.86 123.93 191.2 127.11 262.8 176.64 234.25 254.22 44.304" fill="#1159CC"/>
+<polygon points="92.45 56.933 92.45 34.051 92.212 33.915 53.729 53.017 55.371 76.051 103.4 180.6 129.42 165.89 127.11 137.8" fill="#1159CC"/>
+<polygon points="219.56 0 161.81 33.814 161.77 33.814 161.77 33.848 161.77 33.848 161.77 33.848 161.77 56.933 127.11 137.8 127.11 164.82 103.99 178.23 57.788 74.146 57.788 53.81 92.45 33.848 34.668 0 0.0057522 20.234 0.0057522 45.017 78.022 234.49 127.11 262.8 127.11 191.64 150.2 178.23 149.93 178.06 196.44 74.146 196.44 53.844 196.44 53.81 254.22 20.234" fill="#127EFF"/>
+</svg>
diff --git a/web_src/svg/gitea-vscodium.svg b/web_src/svg/gitea-vscodium.svg
new file mode 100644
index 0000000..483676f
--- /dev/null
+++ b/web_src/svg/gitea-vscodium.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-whitespace.svg b/web_src/svg/gitea-whitespace.svg
new file mode 100644
index 0000000..1588c04
--- /dev/null
+++ b/web_src/svg/gitea-whitespace.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><path d="m2.5 7.5.35.35a.5.5 0 0 0 0-.7l-.35.35ZM3 4h12V3H3v1Zm4 4h8V7H7v1Zm-4 4h12v-1H3v1ZM.85 9.85l2-2-.7-.7-2 2 .7.7Zm2-2.7-2-2-.7.7 2 2 .7-.7Z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/gitea-yandex.svg b/web_src/svg/gitea-yandex.svg
new file mode 100644
index 0000000..a128e8a
--- /dev/null
+++ b/web_src/svg/gitea-yandex.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" class="svg gitea-yandex" width="16" height="16" aria-hidden="true"><path fill="#e52620" d="M49.07 0c.524.405.262.88.095 1.333l-6.643 18.095-8.047 22.12a4.21 4.21 0 0 0-.262 1.429v19.81c0 1.2-.024 1.2-1.214 1.2-1.238 0-2.476-.048-3.714.024-.786.024-1.07-.238-1.048-1.024l.024-7.333V42.928c0-.5-.07-1.048-.262-1.524L14.976 7.333c-.095-.262-.238-.476-.357-.714v-.5c.38-.12.762-.3 1.143-.3l4.12-.024s1.357 0 1.81 1.286l9.7 27.31.405.976.333-1.095 1.905-6.976 8.5-26.31c.12-.333.405-.62.62-.93L49.07 0z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/material-invert-colors.svg b/web_src/svg/material-invert-colors.svg
new file mode 100644
index 0000000..e6445ab
--- /dev/null
+++ b/web_src/svg/material-invert-colors.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 768 768"><path d="M384 627V163.5l-135 135c-36 36-57 85.5-57 136.5 0 103.19 88.8 192 192 192zm181.5-373.5C666 354 666 514.5 565.5 615 516 664.5 450 690 384 690s-132-25.5-181.5-75C102 514.5 102 354 202.5 253.5L384 72z"/></svg> \ No newline at end of file
diff --git a/web_src/svg/material-palette.svg b/web_src/svg/material-palette.svg
new file mode 100644
index 0000000..df0e175
--- /dev/null
+++ b/web_src/svg/material-palette.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 768 768"><path d="M559.5 384c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-96-127.5c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-159 0c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zm-96 127.5c27 0 48-21 48-48s-21-48-48-48-48 21-48 48 21 48 48 48zM384 96c159 0 288 115.5 288 256.5 0 88.5-72 159-160.5 159H456c-27 0-48 21-48 48 0 12 4.5 22.5 12 31.5s12 21 12 33c0 27-21 48-48 48-159 0-288-129-288-288S225 96 384 96z"/></svg> \ No newline at end of file