@webship.co/workspace 0.0.0-stage → 1.0.0-beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +466 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1439 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +7923 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- package/workspaces.js +553 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* ---------- Workspace cards ---------- */
|
|
2
|
+
|
|
3
|
+
.workspace-card { border-radius:0.5rem; }
|
|
4
|
+
|
|
5
|
+
.workspace-card .icon {
|
|
6
|
+
width:3.25rem;
|
|
7
|
+
height:3.25rem;
|
|
8
|
+
margin:0 auto 0.5rem;
|
|
9
|
+
border-radius:0.75rem;
|
|
10
|
+
background: var(--subtle-brand);
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
font-size:1.625rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Hover and focus on a card, in BOTH themes.
|
|
18
|
+
*
|
|
19
|
+
* UIKit paints `.uk-card-default.uk-card-hover:hover` white — two classes, so it beat our own
|
|
20
|
+
* single-class hover rule, and `uikit.min.css` is linked after the assembled stylesheet anyway. In
|
|
21
|
+
* dark mode that meant every workspace card flashed a white panel under the cursor, which is the
|
|
22
|
+
* brightest thing on the page and the one nobody asked for. Out-specified rather than forced: the
|
|
23
|
+
* surface stays the card's own, and hover is said with the shadow and the border, as everywhere else.
|
|
24
|
+
*
|
|
25
|
+
* Focus gets the same treatment plus a real ring: these cards are links, and a keyboard user tabbing
|
|
26
|
+
* the home page had only the browser's default outline over a dark panel to go on. */
|
|
27
|
+
.uk-card-default.uk-card-hover:hover,
|
|
28
|
+
.uk-card-default.uk-card-hover:focus-visible,
|
|
29
|
+
.uk-card-default.uk-card-hover:focus-within {
|
|
30
|
+
background-color: var(--surface-card);
|
|
31
|
+
box-shadow: var(--shadow-card-hover);
|
|
32
|
+
border-color: var(--line-strong);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.workspace-card:focus-visible {
|
|
36
|
+
outline: 2px solid var(--brand-primary);
|
|
37
|
+
outline-offset: 3px;
|
|
38
|
+
border-radius: 0.5rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* The card's corner: its backups count, Edit and Settings. Beside the card's link, never inside it. */
|
|
42
|
+
.ws-card-corner {
|
|
43
|
+
position: absolute;
|
|
44
|
+
top: 0.5rem;
|
|
45
|
+
right: 0.5rem;
|
|
46
|
+
z-index: 2;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 0.25rem;
|
|
50
|
+
}
|
|
51
|
+
.ws-card-corner .ws-backups { position: static; }
|
|
52
|
+
.ws-card-tool {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
width: 1.75rem;
|
|
57
|
+
height: 1.75rem;
|
|
58
|
+
padding: 0;
|
|
59
|
+
border: 1px solid transparent;
|
|
60
|
+
border-radius: 0.5rem;
|
|
61
|
+
background: transparent;
|
|
62
|
+
color: var(--text-subtle);
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
transition: color .12s ease, background-color .12s ease;
|
|
65
|
+
}
|
|
66
|
+
.ws-card-tool:is(:hover, :focus-visible) { color: var(--brand-primary); background: var(--subtle-brand); text-decoration: none; }
|
|
67
|
+
.ws-card-tool:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 1px; }
|
|
68
|
+
|
|
69
|
+
/* Five or six across leaves no room beside the icon, so the tools wait for the pointer there. */
|
|
70
|
+
html:is([data-home-view="grid-5"], [data-home-view="grid-6"]) .ws-card-tool { opacity: 0; }
|
|
71
|
+
html:is([data-home-view="grid-5"], [data-home-view="grid-6"]) .ws-card-wrap:is(:hover, :focus-within) .ws-card-tool { opacity: 1; }
|
|
72
|
+
|
|
73
|
+
/* List: the corner is the end of the row. */
|
|
74
|
+
html[data-home-view="list"] .ws-card-corner { top: 50%; transform: translateY(-50%); right: 0.75rem; }
|
|
75
|
+
html[data-home-view="list"] .workspace-card { padding-right: 7.5rem; }
|
|
76
|
+
|
|
77
|
+
/* The Edit modal is the editor alone: the settings page's label column has nothing to say here. */
|
|
78
|
+
.ws-card-edit #ws-presentation { display: block; padding: 0; border: 0; }
|
|
79
|
+
.ws-card-edit #ws-presentation > .settings-key { display: none; }
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var htmx=(()=>{const e={parse(t){if(!t)return{};if(t.startsWith("{"))return JSON.parse(t);let r=/(?:"([^"]+)"|'([^']+)'|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<((?:[^/]|\/(?!>))+)\/>|([^\s,]+)))?(?=\s|,|$)/g,i={};for(let s of t.matchAll(r)){let[,t,r,n,o,a,l,h]=s,c=t??r??n,u=(o??a??l??h??"true").trim();try{u=JSON.parse(u)}catch{}let d=n?.includes("."),f=d?c.split(".").reduceRight((e,t)=>({[t]:e}),u):{[c]:u};e.merge(f,i)}return i},split:e=>e.split(/,(?![^\[]*\])(?![^(]*\))(?![^<]*\/>)(?=(?:[^"']|"[^"]*"|'[^']*')*$)/),merge(t,r){"string"==typeof t&&(t=e.parse(t));for(let[i,s]of Object.entries(t)){if(["__proto__","constructor","prototype"].includes(i))continue;let t=s?.constructor===Object,n=r[i]?.constructor===Object;t&&n?e.merge(s,r[i]):r[i]=s}return r}};class t{#e=null;#t=[];admit(e,t,r){if(!this.#e)return this.#e={strategy:e,abort:r},"run";if("replace"===e||"abort"!==e&&"abort"===this.#e.strategy)return this.#t=[],this.#e.abort?.(),this.#e={strategy:e,abort:r},"run";if("queue all"===e)this.#t.push(t);else if("queue last"===e)this.#t=[t];else{if("abort"===e||"drop"===e||0!==this.#t.length)return"dropped";this.#t.push(t)}return"queued"}continue(){this.#e=null,this.#t.shift()?.()}abort(){this.#e?.abort?.()}}return new class{#r=e;#i=new Map;#s="";#n=new Set;_loc=window.location;#o;#a=Function;#l=Object.getPrototypeOf(async function(){}).constructor;#h={createHTML:e=>e,createScript:e=>e};#c;#u="a,form";#d=["get","post","put","patch","delete","query"];#f;#m;#p;#g;#x;constructor(){this.#b(),this.#y(),this.#c=this.#S("[hx-action],[hx-get],[hx-post],[hx-put],[hx-patch],[hx-delete],[hx-query]"),this.#f=(new XPathEvaluator).createExpression(`.//*[@*[${this.#v("hx-on").map(e=>`starts-with(name(), "${e}")`).join(" or ")}]]`),this.#o={HCON:e,attributeValue:this.#E.bind(this),parseTriggerSpecs:this.#w.bind(this),determineMethodAndAction:this.#A.bind(this),createRequestContext:this.#T.bind(this),collectFormData:this.#C.bind(this),getAttributeObject:this.#q.bind(this),insertContent:this.#N.bind(this),morph:this.#I.bind(this),isSoftMatch:this.#M.bind(this),initSecurity:(e,t,r)=>{e&&(this.#h=e),t&&(this.#a=t),r&&(this.#l=r)},onTrigger:this.#O.bind(this),htmxProp:this.#H.bind(this),triggerHtmxEvent:this.#L.bind(this),executeJavaScript:this.#k.bind(this)};let t=()=>this.initialize();"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):setTimeout(t)}#b(){this.version="4.0.0",this.config={logAll:!1,prefix:"data-hx-",transitions:!1,history:!0,mode:"same-origin",defaultSwap:"innerHTML",defaultFocusScroll:!1,indicatorClass:"htmx-indicator",requestClass:"htmx-request",includeIndicatorCSS:!0,defaultTimeout:6e4,extensions:"",morphIgnore:["data-htmx-powered"],morphSkip:"[hx-morph-skip]",morphSkipChildren:"[hx-morph-skip-children]",morphScanLimit:10,noSwap:[204,304],implicitInheritance:!1,defaultSettleDelay:1,allowEmptySwapAfterOOB:!1};let t=document.querySelector('meta[name="htmx-config"]');t&&e.merge(t.content,this.config),this.#s=this.config.extensions}#y(){if(!1!==this.config.includeIndicatorCSS){let e=this.config.indicatorClass,t=this.config.requestClass,r=new CSSStyleSheet;r.replaceSync(`.${e}{opacity:0;visibility: hidden} .${t} .${e}, .${t}.${e}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`),document.adoptedStyleSheets=[...document.adoptedStyleSheets,r]}}registerExtension(e,t){return!(this.#s&&!this.#s.split(/,\s*/).includes(e))&&(!this.#n.has(e)&&(this.#n.add(e),t.init&&t.init(this.#o),void Object.entries(t).forEach(([e,t])=>{this.#i.get(e)?.push(t)||this.#i.set(e,[t])})))}#V(e){let t=this.config.prefix;return!e.closest||null!=e.closest("[hx-ignore]")||t&&null!=e.closest(`[${t}ignore]`)}#P(e,t){let r=this.config.prefix;return e.getAttribute(t)??(r?e.getAttribute(t.replace("hx-",r)):null)}#R(e,t){let r=this.config.prefix&&t.replace("hx-",this.config.prefix);return e.hasAttribute(t)?t:r&&e.hasAttribute(r)?r:null}#S(e){return this.#v(e).join(",")}#v(e){let t=[e];return this.config.prefix&&t.push(e.replaceAll("hx-",this.config.prefix)),t}#j(e,t){let r=[...e.querySelectorAll?.(t)??[]];return e.matches?.(t)&&r.unshift(e),r}#B(e){return"before"===e?"beforebegin":"after"===e?"afterend":"prepend"===e?"afterbegin":"append"===e?"beforeend":e}#F(e,t){let r=[];return this.#E(e,t,void 0,(e,t)=>{e?.split(/\s*[,:]\s*/).includes("this")&&r.push(t)}),r}#E(e,t,r,i){t=this.#z(t);let s=this.#z(":inherited"),n=this.#z(":append"),o=this.#P(e,t)??this.#P(e,t+s);if(null!=o)return i?i(o,e):o;let a=CSS.escape(this.config.implicitInheritance?t:t+s),l=CSS.escape(t+s+n),h=this.#S(`[${a}],[${l}]`),c=this.#R(e,t+n)??this.#R(e,t+s+n);if(c){let r=e.getAttribute(c),s=e.parentNode?.closest?.(h);if(i&&i(r,e),s){let e=this.#E(s,t,void 0,i);return e?(e+","+r).replace(/[{}]/g,""):r}return r}let u=e.parentNode?.closest?.(h);return u?(o=this.#E(u,t,void 0,i),!i&&o&&this.config.implicitInheritance&&this.#_(e,"htmx:after:implicitInheritance",{elt:e,name:t,parent:u}),o):r}#w(t){return e.split(t).flatMap(t=>{let[,r,i]=t.match(/^\s*(\S+\[[^\]]*\]|\S+)\s*(.*?)\s*$/)??[];if(!r)return[];if(/\[[^\]]*$/.test(r))throw"unterminated:"+r;return[{name:r,...e.parse(i)}]})}#A(e,t){let r=this.#E(e,"hx-method"),i=this.#E(e,"hx-action");if(!i)for(let t of this.#d){let s=this.#E(e,"hx-"+t);if(null!=s){i=s,r=t;break}}return this.#D(e)&&(i||=t.submitter?.getAttribute?.("formAction")||e.getAttribute(e.matches("a")?"href":"action")),r||=t.submitter?.getAttribute?.("formmethod")||e.getAttribute("method")||"GET",{action:i,method:r.toUpperCase()}}#H(e){return e._htmx||(e._htmx={listeners:[],triggerSpecs:[]},e.setAttribute("data-htmx-powered","true")),e._htmx}#$(e){return e._htmx_state||={}}#W(e){if(this.#U(e)&&this.#L(e,"htmx:before:init",{},!0)){let t=this.#H(e);t.initialized=!0,t.eventHandler=this.#Q(e),this.#J(e),this.#L(e,"htmx:after:init",{},!0)}}#Q(e){return async t=>{try{let r=this.#T(e,t);await this.#X(r)}catch(t){this.#L(e,"htmx:error",{error:t})}}}#T(t,r){let{action:i,method:s}=this.#A(t,r),[n,o]=(i||"").split("#"),a=new AbortController,l={sourceElement:t,sourceEvent:r,status:"created",select:this.#E(t,"hx-select"),selectOOB:this.#E(t,"hx-select-oob"),target:this.#E(t,"hx-target"),swap:this.#E(t,"hx-swap")??this.config.defaultSwap,push:this.#E(t,"hx-push-url"),replace:this.#E(t,"hx-replace-url"),transition:this.config.transitions,confirm:this.#E(t,"hx-confirm"),request:{validate:"true"===this.#E(t,"hx-validate",!t.matches("form")||t.noValidate||r.submitter?.formNoValidate?"false":"true"),action:n,anchor:o,method:s,headers:this.#G(t),abort:a.abort.bind(a),credentials:"same-origin",signal:a.signal,mode:this.config.mode}};t._htmx?.boosted&&e.merge(t._htmx.boosted,l),l.target=this.#K(t,l.target),l.target&&(l.request.headers["HX-Target"]=this.#Y(l.target));let h=this.#E(t,"hx-config");return h&&(e.merge(h,l.request),l.request.mode=this.config.mode),l}#Y(e){return`${e.tagName.toLowerCase()}${e.id?"#"+encodeURI(e.id):""}`}#G(e){let t={"HX-Request":"true","HX-Source":this.#Y(e),"HX-Current-URL":location.href,Accept:"text/html"};return this.#D(e)&&(t["HX-Boosted"]="true"),t}#Z(e,t){return this.#q(e,"hx-headers",e=>{for(let r in e)t.request.headers[r]=String(e[r])},{ctx:t})}#K(e,t){return t instanceof Element?t:null!=t?this.#ee(e,t,"hx-target"):this.#D(e)?document.body:e}#D(e){return e?._htmx?.boosted}async#X(e){let t=e.sourceElement,r=e.sourceEvent;if(!t.isConnected)return;if(this.#te(r))return;this.#re(r)&&r.preventDefault();let i=/GET|DELETE/.test(e.request.method),s=i?t.matches("form")?t:null:t.form||t.closest("form"),n=this.#C(t,s,r.submitter,e.request.validate,i);if(!n)return;let o=this.#q(t,"hx-vals",t=>{e.vals=t;for(let e in t)n.set(e,t[e])},{ctx:e});if(o&&await o,e.values)for(let t in e.values)n.delete(t),n.append(t,e.values[t]);let a=this.#Z(t,e);if(a&&await a,Object.assign(e.request,{form:s,submitter:r.submitter,body:n}),!this.#L(t,"htmx:config:request",{ctx:e}))return;if("DIALOG"===e.request.method)return;let l=this.#ie(e.request.action);if(null!=l){let t=Object.fromEntries(e.request.body);return void await this.#k(e.sourceElement,t,l,!1)}if(i){let t=new URL(e.request.action,document.baseURI);for(let r of e.request.body.keys())t.searchParams.delete(r);for(let[r,i]of e.request.body)t.searchParams.append(r,i);t.origin===location.origin?e.request.action=t.pathname+t.search:e.request.action=t.href,e.request.body=null}else"multipart/form-data"!==(this.#E(t,"hx-encoding")??s?.enctype)&&(e.request.body=new URLSearchParams(e.request.body));await this.#se(e)}async#se(e){let t=e.sourceElement,r=this.#ne(t),i=this.#oe(t);if(this.#ae(t),"run"!==i.admit(r,()=>this.#se(e),()=>e.request?.abort?.()))return;e.status="issuing";let s=[],n=[];try{if(e.confirm){if(!await new Promise(r=>{let i={ctx:e,issueRequest:()=>r(!0),dropRequest:()=>r(!1)};if(this.#L(t,"htmx:confirm",i)){let i=this.#ie(e.confirm);r(i?this.#k(t,{ctx:e},i,!0):window.confirm(e.confirm))}}))return}if(this.#le(e),s=this.#he(t),n=this.#ce(t),e.fetch||=window.fetch.bind(window),e.request.headers["HX-Request-Type"]=e.target===document.body||e.select?"full":"partial",!this.#L(t,"htmx:before:request",{ctx:e}))return;let r=await e.fetch(e.request.action,e.request);if(e.response={raw:r,status:r.status,headers:r.headers},this.#ue(e),!this.#L(t,"htmx:before:response",{ctx:e}))return;if(e.text=await r.text(),!this.#L(t,"htmx:after:request",{ctx:e}))return;if(e.response.status>=400&&this.#L(t,"htmx:response:error",{ctx:e}),this.#de(e))return void(e.keepIndicators=!0);"issuing"===e.status&&(e.hx.retarget&&(e.target=e.hx.retarget),e.hx.reswap&&(e.swap=e.hx.reswap),e.hx.reselect&&(e.select=e.hx.reselect),e.status="response received",this.#fe(e),await this.swap(e),e.status="swapped")}catch(r){e.status="error: "+r,this.#L(t,"htmx:error",{ctx:e,error:r})}finally{await(e.extensionPromise?.catch(()=>{})),clearTimeout(e.requestTimeout),e.hx?.trigger&&this.#me(e.hx.trigger,e.sourceElement),this.#L(t,"htmx:finally:request",{ctx:e}),e.keepIndicators||(this.#pe(s),this.#ge(n)),i.continue()}}#ue(e){e.hx={};for(let[t,r]of e.response.raw.headers)t.toLowerCase().startsWith("hx-")&&(e.hx[t.slice(3).toLowerCase().replace(/-/g,"")]=r)}#de(t){if("true"===t.hx.refresh)return this._loc.reload(),!0;if(t.hx.redirect)return this._loc.href=t.hx.redirect,!0;if(t.hx.location){let r=t.hx.location,i={},s=e.parse(r);return"{"!==r[0]&&null==s.path||(i=s,r=i.path,delete i.path),null==i.push&&null==i.replace&&(i.push="true"),this.ajax("GET",r,i),!0}}#le(e){let t=null!=e.request.timeout?this.parseInterval(e.request.timeout):this.config.defaultTimeout;t&&(e.requestTimeout=setTimeout(()=>e.request?.abort?.(),t))}#ne(e){let t=this.#E(e,"hx-sync");if(!t)return"queue first";let r=t.split(":").pop().trim();return/^(drop|abort|replace|queue)/.test(r)?r:"queue first"}#oe(e){let r=this.#E(e,"hx-sync"),i=e;if(r){let t=r.includes(":")?r.slice(0,r.lastIndexOf(":")).trim():/^(drop|abort|replace|queue)/.test(r)?null:r;t&&(i=this.#ee(e,t,"hx-sync")||e)}return this.#$(i).rq||=new t}#te(e){return"click"===e.type&&(e.ctrlKey||e.metaKey||e.shiftKey)&&!!e.currentTarget?.closest?.("a[href]")}#re(e){let t=e.currentTarget;if("submit"===e.type&&"FORM"===t?.tagName)return!0;if(!("click"===e.type&&0===e.button))return!1;let r=t?.closest?.('button, input[type="submit"], input[type="image"]'),i=r?.form||r?.closest("form");if(r&&!r.disabled&&i&&("submit"===r.type||"image"===r.type||!r.type&&"BUTTON"===r.tagName))return!0;let s=t?.closest?.("a");if(!s||!s.href)return!1;let n=s.getAttribute("href");return!(n&&n.startsWith("#")&&n.length>1)}#J(e,t=e._htmx.eventHandler){let r=this.#E(e,"hx-trigger")||(e.matches("form")?"submit":e.matches("input:not([type=button]):not([type=submit]),select,textarea")?"change":"click");this.#O(e,r,t)}#O(e,t,r){let i=this.#w(t);this.#H(e).triggerSpecs.push(...i);for(let t of i){t.listeners=[];let[i,s]=this.#xe(t.name),n=[e];"outside"===t.from?n=[document]:t.from&&"self"!==t.from&&(n=this.#be(e,t.from));let o=e=>{if((t.halt||t.prevent)&&e.preventDefault(),(t.halt||t.stop||t.consume)&&e.stopPropagation(),t.once)for(let e of t.listeners)e.fromElt.removeEventListener(e.eventName,e.handler,e);r(e)},a=o;if(t.delay?a=e=>{clearTimeout(t.timeout),t.timeout=setTimeout(()=>o(e),this.parseInterval(t.delay))}:t.throttle&&(a=e=>{t.throttled?t.throttledEvent=e:(t.throttled=!0,o(e),t.throttleTimeout=setTimeout(()=>{if(t.throttled=!1,t.throttledEvent){let e=t.throttledEvent;t.throttledEvent=null,a(e)}},this.parseInterval(t.throttle)))}),t.handler=r=>{if(("self"!==t.from||r.target===e)&&("outside"!==t.from||!e.contains(r.target))&&(!t.target||r.target?.matches?.(t.target))){if(t.changed){let e=t.values??=new WeakMap,r=!1;for(let t of n)e.get(t)!==t.value&&(r=!0,e.set(t,t.value));if(!r)return}if(s){this.#re(r)&&r.preventDefault();let t={};for(let e in r)t[e]=r[e];if(!this.#k(e,t,s,!0,!1))return}a(r)}},"intersect"===i||"revealed"===i){let r={rootMargin:t.rootMargin};t.root&&(r.root=this.#ee(e,t.root)),t.threshold&&(r.threshold=parseFloat(t.threshold));let s="revealed"===i;t.observer=new IntersectionObserver(r=>{for(let i=0;i<r.length;i++)if(r[i].isIntersecting){this.trigger(e,"intersect",{},!1),s&&t.observer.disconnect();break}},r),i="intersect",t.observer.observe(e)}if("every"===i){let r=Object.keys(t).find(e=>"name"!==e);t.interval=setInterval(()=>{e.isConnected?this.#L(e,"every",{},!1):clearInterval(t.interval)},this.parseInterval(r))}if("load"!==i)for(let r of n){let s={fromElt:r,eventName:i,handler:t.handler,capture:!!t.capture,passive:!!t.passive};e._htmx.listeners.push(s),t.listeners.push(s),r.addEventListener(i,t.handler,s)}else t.handler(new CustomEvent("load"))}}#xe(e){let t=e.match(/^([^\[]*)\[([^\]]*)]/);return t?[t[1],t[2]]:[e,null]}#me(t,r){if("{"===t[0]){let i=e.parse(t);for(let e in i){let t=i[e],s=r;t?.target&&(s=this.find(t.target)),this.trigger(s,e,"object"==typeof t?t:{value:t})}}else t.split(",").forEach(e=>this.trigger(r,e.trim(),{}))}#ye(e){let t={},r=Object.getPrototypeOf(this);for(let i of Object.getOwnPropertyNames(r))"constructor"!==i&&"function"==typeof this[i]&&(["find","findAll"].includes(i)?t[i]=(t,r)=>void 0===r?this[i](e,t):this[i](t,r):t[i]=this[i].bind(this));return t}#k(e,t,r,i=!0,s=!0,n=!1){let o={};Object.assign(o,this.#ye(e));let a={},l={scope:a,code:r};this.#_(e,"htmx:scope",l),r=l.code,Object.assign(o,a),Object.assign(o,t);let h=Object.keys(o),c=Object.values(o),u=new(s?this.#l:this.#a)(...h,i?`return (${r})`:r);return n?()=>u.call(e,...c):u.call(e,...c)}process(e,t){if(!e?.isConnected)return;if(!(e instanceof Element)){for(let r of e.children||[])this.process(r,t);return}if(t&&this.#Se(e,!0),this.#V(e))return;if(!this.#L(e,"htmx:before:process"))return;let r=[e],i=this.#f.evaluate(e),s=null;for(;s=i.iterateNext();)r.push(s);for(let e of r)!this.#V(e)&&this.#L(e,"htmx:before:on:init",{},!0)&&this.#ve(e);for(let t of this.#j(e,this.#c))this.#W(t);for(let t of this.#j(e,this.#u))this.#Ee(t);this.#L(e,"htmx:after:process")}#Ee(e){let t=this.#E(e,"hx-boost");if(t&&"false"!==t&&this.#we(e)&&this.#L(e,"htmx:before:init",{},!0)){let r=this.#H(e);r.initialized=!0,r.eventHandler=this.#Q(e),r.boosted=t;let i=e.matches("a")?"click":"submit";e._htmx.listeners.push({fromElt:e,eventName:i,handler:e._htmx.eventHandler}),e.addEventListener(i,e._htmx.eventHandler),this.#L(e,"htmx:after:init",{},!0)}}#we(e){if(this.#U(e))if("A"===e.tagName){if(""===e.target||"_self"===e.target)return!e.hasAttribute("download")&&!e.getAttribute("href")?.startsWith?.("#")&&this.#Ae(e.href)}else if("FORM"===e.tagName)return"dialog"!==e.method&&this.#Ae(e.action)}#Ae(e){try{return new URL(e,window.location.href).origin===window.location.origin}catch(e){return!1}}#U(e){return!e._htmx?.initialized&&!this.#V(e)}#Se(e,t){let r=[e,...e.querySelectorAll?.("[data-htmx-powered]")??[]];for(let e of r)if(e._htmx){this.#L(e,"htmx:before:cleanup");for(let t of e._htmx.triggerSpecs||[])t.interval&&clearInterval(t.interval),t.timeout&&clearTimeout(t.timeout),t.throttleTimeout&&clearTimeout(t.throttleTimeout),t.observer?.disconnect();for(let t of e._htmx.listeners||[])t.fromElt.removeEventListener(t.eventName,t.handler,t);e.removeAttribute("data-htmx-powered"),this.#L(e,"htmx:after:cleanup"),t&&delete e._htmx}}#Te(e){let t=document.createElement("div");t.hidden=!0,document.body.insertAdjacentElement("afterend",t);let r=e.querySelectorAll?.(this.#S("[hx-preserve]"))||[];for(let e of r){let r=document.getElementById(e.id);r&&this.#Ce(t,r,null)}return t}#qe(e){for(let t of[...e.children]){let e=document.getElementById(t.id);e&&(this.#Ce(e.parentNode,t,e),this.#Se(e),e.remove())}e.remove()}#Ne(e){let t=this.#h.createHTML(e);return Document.parseHTMLUnsafe?.(t)||(new DOMParser).parseFromString(t,"text/html")}#Ie(e){let t=e.replace(/<hx-([a-z]+)(\s+|>)/gi,'<template hx type="$1"$2').replace(/<\/hx-[a-z]+>/gi,"</template>"),r="";t=t.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,e=>(r=this.#Ne(e).title,""));let i,s,n=t.match(/<([a-z][^\/>\x20\t\r\n\f]*)/i)?.[1]?.toLowerCase();if("html"===n||"body"===n?(i=this.#Ne(t),s=document.createDocumentFragment(),s.append(i.body)):(i=this.#Ne(`<template>${t}</template>`),s=i.querySelector("template").content),!r){let e=s.querySelector("title:not(svg title)");e&&(r=e.textContent,e.remove())}return this.#Me(s),{fragment:s,title:r}}#Oe(e,t,r,i){let s=t.id?"#"+CSS.escape(t.id):null;"true"!==r&&r&&!r.includes(" ")&&([r,s=s]=r.split(/:(.*)/)),"true"!==r&&r||(r="outerHTML");let n=this.#He(r);if(s=n.target||s,n.strip??=!n.style.startsWith("outer"),!s)return;let o=[...document.querySelectorAll(s)];for(let r of o){let s=document.createDocumentFragment();s.append(t.cloneNode(!0)),e.push({type:"oob",fragment:s,target:r,swapSpec:n,sourceElement:i})}t.remove()}#Le(e,t,r){let i=[];if(r)for(let s of r.split(",")){let[r,n="true"]=s.split(/:(.*)/);for(let s of e.querySelectorAll(r))this.#Oe(i,s,n,t)}for(let r of e.querySelectorAll(this.#S("[hx-swap-oob]"))){let e=this.#R(r,"hx-swap-oob"),s=r.getAttribute(e);r.removeAttribute(e),this.#Oe(i,r,s,t)}return i}#ke(e,t,r){t?t.before(...r.childNodes):e.append(...r.childNodes)}#He(t){t=t.trim();let r=this.config.defaultSwap;if(t&&!/^\S*:/.test(t)){let e=t.match(/^(\S+)\s*(.*)$/);r=e[1],t=e[2]}return{style:this.#B(r),...e.parse(t)}}#Ve(e,t){let r=[];for(let i of e.querySelectorAll("template[hx]")){let e=i.getAttribute("type");if("partial"===e){let e=this.#P(i,"hx-target")||(i.id?"#"+CSS.escape(i.id):null);if(e){this.#Me(i.content);let s=this.#He(this.#P(i,"hx-swap")||this.config.defaultSwap),n=this.#be(t.sourceElement,e);for(let e of n.length?n:[null])r.push({type:"partial",fragment:i.content.cloneNode(!0),target:e,swapSpec:s,sourceElement:t.sourceElement})}}else this.#_(i,"htmx:process:"+e,{ctx:t,tasks:r});i.remove()}return r}#Pe(e,t,r,i){try{null!=r&&e.setSelectionRange&&e.setSelectionRange(r,i),e.focus(t)}catch(e){}}#Re(e){let t=this.#j(e,"[autofocus]")[0];t&&this.#Pe(t)}#je(e,t){if(e.scroll){let r=e.scrollTarget?this.#Be(e.scrollTarget):t;r&&("top"===e.scroll?r.scrollTop=0:"bottom"===e.scroll&&(r.scrollTop=r.scrollHeight))}if("top"===e.show||"bottom"===e.show){let r=e.showTarget?this.#Be(e.showTarget):t;r?.scrollIntoView?.("top"===e.show)}}#Fe(e){e.request?.anchor&&document.getElementById(e.request.anchor)?.scrollIntoView({block:"start",behavior:"auto"})}#Me(e){let t=this.#j(e,"script");for(let e of t){let t=document.createElement("script");for(let r of e.attributes)t.setAttribute(r.name,r.value);this.config.inlineScriptNonce&&(t.nonce=this.config.inlineScriptNonce),t.textContent=this.#h.createScript(e.textContent),e.replaceWith(t)}}initialize(){this.config.history&&!this.#g&&(this.#g=!0,history.state||history.replaceState({htmx:!0},"",location.href),window.navigation&&!/firefox/i.test(navigator.userAgent)?navigation.addEventListener("navigate",e=>{"traverse"===e.navigationType&&e.canIntercept&&!e.hashChange&&e.intercept({handler:()=>this.#ze()})}):window.addEventListener("popstate",e=>this.#ze(e.state))),this.process(document.body)}async swap(e){try{this.#_e(e);let{fragment:t,title:r}=this.#Ie(e.text);e.title=r;let i=[],s=this.#Le(t,e.sourceElement,e.selectOOB),n=this.#Ve(t,e);i.push(...s,...n);let o=n.length||s.length&&!this.config.allowEmptySwapAfterOOB,a=this.#De(e,t,o);if(a&&i.unshift(a),!this.#L(e.sourceElement,"htmx:before:swap",{ctx:e,tasks:i}))return;let l=[],h=[];for(let t of i)t.swapSpec?.transition??a?.transition??e.transition?h.push(t):l.push(this.#N(t));if(h.length>0){let t=async()=>{for(let e of h)await this.#N(e,!1)};l.push(this.#$e(t,e))}await Promise.all(l),!e.sourceElement?.isConnected&&a?.target?.isConnected&&(e.sourceElement=a.target),this.#L(e.sourceElement,"htmx:after:swap",{ctx:e}),e.title&&!a?.swapSpec?.ignoreTitle&&(document.title=e.title),this.#Fe(e)}finally{this.#L(e.sourceElement,"htmx:finally:swap",{ctx:e})}}#De(e,t,r){let i=this.#He(e.swap||this.config.defaultSwap);if("delete"===i.style||t.childElementCount>0||t.textContent.trim()||(i.swapEmpty??!r)){if(e.select){let r=t.querySelectorAll(e.select);(t=document.createDocumentFragment()).append(...r)}return this.#D(e.sourceElement)&&(i.show||="top"),{type:"main",fragment:t,target:this.#K(e.sourceElement||document.body,i.target||e.target),swapSpec:i,sourceElement:e.sourceElement,transition:e.transition&&!1!==i.transition}}}async#N(e,t=!0){let{target:r,swapSpec:i,fragment:s}=e;if("string"==typeof r&&(r=document.querySelector(r)),!r)return;"string"==typeof i&&(i=this.#He(i));let n,o=i.style;if("none"===o)return;if("BODY"===s.firstElementChild?.tagName){const e=r===document.body&&o.startsWith("outer");e&&"outerHTML"===o&&(o="outerSync"),i.strip??=!e}if(i.strip&&s.firstElementChild&&(s=document.createDocumentFragment(),s.append(...(e.fragment.firstElementChild.content||e.fragment.firstElementChild).childNodes)),this.#We(r,"htmx-swapping"),t&&e.swapSpec?.swap&&await this.timeout(e.swapSpec?.swap),"delete"===o)return void(r.parentNode&&(this.#Se(r),r.parentNode.removeChild(r)));let a=[],l=i.settle??this.config.defaultSettleDelay,h=r.parentNode;if("innerHTML"===o||"outerHTML"===o&&h){let e=document.activeElement;if(e?.id){let t,r;try{t=e.selectionStart,r=e.selectionEnd}catch(e){}n={elt:e,start:t,end:r}}a=t&&l?this.#Ue(s,r):[]}let c=this.#Te(s),u=[...s.childNodes];try{if("innerHTML"===o){for(const e of r.children)this.#Se(e);r.replaceChildren(...s.childNodes)}else if("textContent"===o){for(const e of r.querySelectorAll("[data-htmx-powered]"))this.#Se(e);r.textContent=s.textContent}else if("outerHTML"===o)h&&(this.#ke(h,r,s),this.#Se(r),h.removeChild(r),r=u[0]||h);else if("outerSync"===o){this.#Qe(r,s.firstElementChild);for(const e of r.children)this.#Se(e);r.replaceChildren(...s.firstElementChild.childNodes),u=[r]}else if("innerMorph"===o)this.#I(r,s,!0),u=[...r.childNodes];else if("outerMorph"===o)this.#I(r,s,!1),u.push(r);else if("beforebegin"===o)h&&this.#ke(h,r,s);else if("afterbegin"===o)this.#ke(r,r.firstChild,s);else if("beforeend"===o)this.#ke(r,null,s);else if("afterend"===o)h&&this.#ke(h,r.nextSibling,s);else{let e=this.#i.get("handle_swap")||[],t=!1;for(const n of e){let e=n(o,r,s,i);if(e){t=!0,Array.isArray(e)&&(u=e);break}}if(!t)throw new Error(`Unknown swap style: ${o}`)}}finally{this.#Je(r,"htmx-swapping")}if(e.target=r,this.#qe(c),n&&!n.elt.matches(":focus")){let e=document.getElementById(n.elt.id);if(e){let t={preventScroll:void 0!==i.focusScroll?!i.focusScroll:!this.config.defaultFocusScroll};this.#Pe(e,t,n.start,n.end)}}this.#L(r,"htmx:before:settle",{task:e,newContent:u,settleTasks:a});for(const e of u)this.#We(e,"htmx-added");if(t&&a.length>0){this.#We(r,"htmx-settling"),await this.timeout(l);for(let e of a)e();this.#Je(r,"htmx-settling")}this.#L(r,"htmx:after:settle",{task:e,newContent:u,settleTasks:a});for(const e of u)this.#Je(e,"htmx-added"),this.process(e),this.#Re(e);this.#je(i,r)}#L(e,t,r={},i=!0){if(r.error){let i=`htmx: ${t}: ${r.error.message??r.error}`;r.error instanceof Error?console.error(i,r.error,{elt:e,detail:r}):console.error(i,{elt:e,detail:r})}else r.warn?console.warn(`htmx: ${t}: ${r.warn}`,{elt:e,detail:r}):this.config.logAll&&console.log(`htmx: ${t}`,{elt:e,detail:r});return e=this.#Xe(e),this.#_(e,t,r),this.trigger(e,this.#z(t),r,i)}#_(e,t,r={}){let i=this.#i.get(t.replace(/:/g,"_"));if(i){r.cancelled=!1;for(const t of i)if(!1===t(e,r)||r.cancelled)return r.cancelled=!0,!1}return!0}timeout(e){if((e=this.parseInterval(e))>0)return new Promise(t=>setTimeout(t,e))}onLoad(e){this.on(this.#z("htmx:after:process"),t=>{e(t.target)})}on(e,t,r){let i,s=document;return void 0===r?(i=e,r=t):(s=this.#Xe(e),i=t),s.addEventListener(i,r),r}find(e,t){return this.#Be(e,t)}findAll(e,t){return this.#be(e,t)}parseInterval(e){if("number"==typeof e)return e;let[,t,r]=e?.match(/^([\d.]+)(ms|s|m)?$/)||[],i=parseFloat(t)*({ms:1,s:1e3,m:6e4}[r]||1);return isNaN(i)?void 0:i}trigger(e,t,r={},i=!0){e=this.#Xe(e);let s=new CustomEvent(t,{detail:r,cancelable:!0,bubbles:i,composed:!0}),n=e?.isConnected?e:document;return!r.cancelled&&n.dispatchEvent(s)}ajax(e,t,r){(!r||r instanceof Element||"string"==typeof r)&&(r={target:r});let i="string"==typeof r.source?document.querySelector(r.source):r.source;if("string"==typeof r.source&&!i)return Promise.reject(new Error("Source not found"));if(r.target){let e=this.#K(document.body,r.target);if(!e)return Promise.reject(new Error("Target not found"));i||=e}i||=document.body;let s=this.#T(i,r.event||{});return Object.assign(s,r),r.target&&(s.target=this.#K(document.body,r.target)),Object.assign(s.request,{action:t,method:e.toUpperCase()}),r.headers&&Object.assign(s.request.headers,r.headers),this.#X(s)}#Ge(e){this.config.history&&(history.state||history.replaceState({htmx:!0},"",location.href),history.pushState({htmx:!0},"",e),this.#L(document,"htmx:after:history:push",{path:e}))}#Ke(e){this.config.history&&(history.replaceState({htmx:!0},"",e),this.#L(document,"htmx:after:history:replace",{path:e}))}async#ze(e,t){if(await this.timeout(1),e??=history.state,!e?.htmx)return;this.#p?.abort(),t=t||location.pathname+location.search;let r=document.querySelector(this.#S("[hx-history-elt]"))||document.body;if(this.#L(document,"htmx:before:history:restore",{path:t,cacheMiss:!0})){if("reload"!==this.config.history)return this.#p=new AbortController,this.ajax("GET",t,{target:r,swap:"outerSync",select:r!==document.body?this.#S("[hx-history-elt]"):void 0,request:{headers:{"HX-History-Restore-Request":"true"},signal:this.#p.signal}});this._loc.reload()}}#Ye(e){let{sourceElement:t,push:r,replace:i,hx:s,response:n}=e;if((s?.pushurl||s?.replaceurl)&&(r=s.pushurl,i=s.replaceurl),null==r&&null==i&&this.#D(t)&&(r="true"),"false"!==r&&!1!==r||(r=null),"false"!==i&&!1!==i||(i=null),!r&&!i)return null;let o=r||i;if("true"===o){let t=n?.raw?.url||e.request.action,r=new URL(t,location.href);o=r.pathname+r.search+(e.request.anchor?"#"+e.request.anchor:"")}return{type:r?"push":"replace",path:o}}#_e(e){if(!this.config.history)return;let t=this.#Ye(e);if(!t)return;let r={history:t,sourceElement:e.sourceElement,response:e.response};this.#L(document,"htmx:before:history:update",r)&&("push"===t.type?this.#Ge(t.path):this.#Ke(t.path),this.#L(document,"htmx:after:history:update",r))}#ve(e){if(e._htmx?.onInitialized)return;let t=this.#v("hx-on"),r=this.config.metaCharacter||":",i=t=>async r=>{try{await this.#k(e,{event:r},`with(event?.detail||{}){${t}}`,!1)}catch(t){"symbol"!=typeof t&&this.#L(e,"htmx:error",{error:t})}};for(let s of e.getAttributeNames()){let n=t.find(e=>s.startsWith(e));if(!n)continue;this.#H(e).onInitialized=!0;let o=s.substring(n.length),a=e.getAttribute(s);if(!o){for(let t of a.split(/;(?=[^;]*->)/)){let r=t.indexOf("->");-1!==r&&this.#O(e,t.substring(0,r).trim(),i(t.substring(r+2).trim()))}continue}if(o[0]!==r)continue;let l=o.substring(1);l.startsWith(r)&&(l="htmx"+r+l.substring(1)),this.#O(e,l,i(a))}}#he(e){let t,r=this.#E(e,"hx-indicator");if(r)t=this.#be(e,r,"hx-indicator");else{if(e===document.body)return[];t=[e]}for(const e of t){let t=this.#$(e);t.rc=(t.rc||0)+1,this.#We(e,this.config.requestClass)}return t}#pe(e){for(let t of e){let e=this.#$(t);e.rc&&--e.rc<=0&&(this.#Je(t,this.config.requestClass),delete e.rc)}}#ce(e){let t=this.#E(e,"hx-disable"),r=[];if(t){r=this.#be(e,t,"hx-disable");for(let e of r){let t=this.#$(e);t.dc=(t.dc||0)+1,e.disabled=!0}}return r}#ge(e){for(const t of e){let e=this.#$(t);e.dc&&--e.dc<=0&&(t.disabled=!1,delete e.dc)}}#C(e,t,r,i,s){if(i&&t&&!t.reportValidity())return;let n=t?new FormData(t):new FormData,o=t?new Set(t.elements):new Set;if(!t){if(i&&e.reportValidity&&!e.reportValidity())return;this.#Ze(e,o,n,s)}r&&r.name&&(n.append(r.name,r.value),o.add(r));let a=this.#E(e,"hx-include");if(a)for(let t of this.#be(e,a)){if(i&&t.reportValidity&&!t.reportValidity())return;this.#Ze(t,o,n)}return n}#Ze(e,t,r,i){let s=e.tagName,n=[];"BUTTON"===s||s.includes("-")?n=[e]:!["INPUT","SELECT","TEXTAREA","FIELDSET"].includes(s)&&i||(n=this.#j(e,"[name]:not(button)"));for(let e of n){let i=e.name||e.getAttribute?.("name");if(!i||e.matches(":disabled")||t.has(e))continue;t.add(e);let s=e.type;if("checkbox"===s||"radio"===s||"INPUT"!==e.tagName&&"checked"in e)e.checked&&r.append(i,e.value);else if("file"===s)for(let t of e.files)r.append(i,t);else if("select-multiple"===s)for(let t of e.selectedOptions)r.append(i,t.value);else if(Array.isArray(e.value))for(let t of e.value)r.append(i,t);else r.append(i,e.value)}}#q(t,r,i,s={}){let n=this.#E(t,r);if(!n)return null;let o=this.#ie(n);if(o)return 0!==o.indexOf("{")&&(o="{"+o+"}"),this.#k(t,s,o,!0).then(e=>{i(e)});i(e.parse(n))}#et(e){let t=e.trim();return t.startsWith("<")&&t.endsWith("/>")?t.slice(1,-2):t}#be(t,r,i,s){let n=r??t,o=r?this.#Xe(t)||document.body:document;if(n.startsWith("global "))return this.#be(o,n.slice(7),i,!0);let a=n?e.split(n):[],l=[],h=[];for(const e of a){let t,r=this.#et(e);if(r.startsWith("closest "))t=o.closest(r.slice(8));else if(r.startsWith("find "))t=o.querySelector(r.slice(5));else if(r.startsWith("findAll "))l.push(...o.querySelectorAll(r.slice(8)));else if("next"===r||"nextElementSibling"===r)t=o.nextElementSibling;else if(r.startsWith("next "))t=this.#tt(o,r.slice(5),!!s);else if("previous"===r||"previousElementSibling"===r)t=o.previousElementSibling;else if(r.startsWith("previous "))t=this.#rt(o,r.slice(9),!!s);else if("document"===r)t=document;else if("window"===r)t=window;else if("body"===r)t=document.body;else if("host"===r)t=o.getRootNode().host;else if("this"===r){if(i){l.push(...this.#F(o,i));continue}t=o}else h.push(r);t&&l.push(t)}if(h.length>0){let e=h.join(","),t=this.#it(o,!!s);l.push(...t.querySelectorAll(e))}return[...new Set(l)]}#tt(e,t,r){return this.#st(this.#it(e,r).querySelectorAll(t),e,Node.DOCUMENT_POSITION_PRECEDING)}#rt(e,t,r){let i=[...this.#it(e,r).querySelectorAll(t)].reverse();return this.#st(i,e,Node.DOCUMENT_POSITION_FOLLOWING)}#st(e,t,r){for(const i of e)if(i.compareDocumentPosition(t)===r)return i}#it(e,t){return e.isConnected&&e.getRootNode?e.getRootNode?.({composed:t}):document}#ee(e,t,r){let i=this.#be(e,t,r)[0];return i||console.warn(`htmx: '${t}' on ${r} did not match any element`,{elt:e,selector:t,attr:r}),i}#Be(e,t,r){return this.#be(e,t,r)[0]}#ie(e){if(null!=e){if(e.startsWith("js:"))return e.substring(3);if(e.startsWith("javascript:"))return e.substring(11)}}#ae(e){let t=this.#H(e);if(t.abortInitialized)return;t.abortInitialized=!0;let r=()=>{this.#oe(e).abort()};e.addEventListener("htmx:abort",r),t.listeners.push({fromElt:e,eventName:"htmx:abort",handler:r})}#I(e,t,r){let{persistentIds:i,idMap:s}=this.#nt(e,t),n=document.createElement("div");n.hidden=!0,document.body.after(n);let o={target:e,idMap:s,persistentIds:i,pantry:n,futureMatches:new WeakSet};r?this.#ot(o,e,t):this.#ot(o,e.parentNode,t,e,e.nextSibling),this.#Se(n),n.remove()}#ot(e,t,r,i=null,s=null){t instanceof HTMLTemplateElement&&r instanceof HTMLTemplateElement&&(t=t.content,r=r.content),i||=t.firstChild;let n=r.firstChild;for(;n;){let r;if(i&&i!=s&&(r=this.#at(e,n,i,s),r&&r!==i)){let o=i;for(;o&&o!==r;){let r=o;o=o.nextSibling,r instanceof Element&&(e.idMap.has(r)||this.#lt(e,r,n))?this.#Ce(t,r,s):this.#ht(e,r)}}if(!r&&n instanceof Element&&e.persistentIds.has(n.id)){let s=CSS.escape(n.id);r=e.target.id===n.id&&e.target||e.target.querySelector(`[id="${s}"]`)||e.pantry.querySelector(`[id="${s}"]`);let o=r;for(;o=o.parentNode;){let t=e.idMap.get(o);t&&(t.delete(r.id),t.size||e.idMap.delete(o))}this.#Ce(t,r,i)}if(r){this.#ct(r,n,e),i=r.nextSibling,n=n.nextSibling;continue}let o=n.nextSibling;if(e.idMap.has(n)){let r=document.createElement(n.tagName);t.insertBefore(r,i),this.#ct(r,n,e),this.process(r),i=r.nextSibling}else t.insertBefore(n,i),i=n.nextSibling;n=o}for(;i&&i!=s;){let t=i;i=i.nextSibling,this.#ht(e,t)}}#lt(e,t,r){if(e.futureMatches.has(t))return!0;for(let i=r.nextSibling,s=0;i&&s<this.config.morphScanLimit;i=i.nextSibling,s++)if(i instanceof Element&&t.isEqualNode(i))return e.futureMatches.add(t),!0;return!1}#at(e,t,r,i){if(3===t.nodeType)return 3===r?.nodeType?r:null;if(!(t instanceof Element))return null;let s=null,n=0,o=this.config.morphScanLimit,a=e.idMap.get(t),l=a?.size||0;if(t.id&&!a)return null;let h=r;for(;h&&h!=i;){let r=e.idMap.get(h);if(this.#o.isSoftMatch(h,t)){if(r&&a&&[...r].some(e=>a.has(e)))return h;if(!r){if(o>0&&h.isEqualNode(t))return h;s||(s=h)}}if(n+=r?.size||0,n>l)break;if(null!=document.activeElement?.selectionStart&&h.contains(document.activeElement))break;if(--o<1&&0===l)break;h=h.nextSibling}return s&&this.#lt(e,s,t)?null:s}#M(e,t){return e instanceof Element&&e.tagName===t.tagName&&(!("SCRIPT"===e.tagName&&!e.isEqualNode(t))&&(!e.id||e.id===t.id))}#ht(e,t){e.idMap.has(t)?this.#Ce(e.pantry,t,null):(this.#Se(t),t.remove())}#Ce(e,t,r){if(e.moveBefore)try{return void e.moveBefore(t,r)}catch(e){}e.insertBefore(t,r)}#ct(e,t,r){if(3===e.nodeType)return void(e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue));if(this.config.morphSkip&&e.matches?.(this.config.morphSkip))return;if(!this.#_(e,"htmx:before:morph:node",{oldNode:e,newNode:t}))return;this.#Qe(e,t),e instanceof HTMLTextAreaElement&&document.activeElement!==e&&e.defaultValue!=t.defaultValue&&(e.value=t.value),this.config.morphSkipChildren&&e.matches?.(this.config.morphSkipChildren)||e.isEqualNode(t)&&"TEMPLATE"!==t.tagName&&!t.querySelector?.("template")||this.#ot(r,e,t)}#Qe(e,t){let r=this.config.morphIgnore||[],i=!1,s=e=>this.#v("hx-").some(t=>e.startsWith(t));for(const n of t.attributes)if(!r.some(e=>n.name.startsWith(e))&&e.getAttribute(n.name)!==n.value){if(s(n.name)&&(i=!0),!this.#_(e,"htmx:before:morph:attr",{attrName:n.name,newValue:n.value}))continue;e.setAttribute(n.name,n.value),"value"===n.name&&e instanceof HTMLInputElement&&"file"!==e.type&&document.activeElement!==e&&(e.value=n.value)}for(let n=e.attributes.length-1;n>=0;n--){let o=e.attributes[n];if(o&&!t.hasAttribute(o.name)&&!r.some(e=>o.name.startsWith(e))){if(s(o.name)&&(i=!0),!this.#_(e,"htmx:before:morph:attr",{attrName:o.name,newValue:null}))continue;e.removeAttribute(o.name)}}i&&this.#Se(e,!0)}#ut(e,t,r,i){for(const s of i)if(t.has(s.id)){let t=s;for(;t&&t!==r;){let r=e.get(t);null==r&&(r=new Set,e.set(t,r)),r.add(s.id),t=t.parentElement}}}#nt(e,t){let r=this.#j(e,"[id]"),i=t.querySelectorAll("[id]"),s=this.#dt(r,i),n=new Map;return this.#ut(n,s,e.parentElement,r),this.#ut(n,s,t,i),{persistentIds:s,idMap:n}}#dt(e,t){let r=new Set,i=new Map;for(const{id:t,tagName:s}of e)i.has(t)?r.add(t):t&&i.set(t,s);let s=new Set;for(const{id:e,tagName:n}of t)s.has(e)?r.add(e):i.get(e)===n&&s.add(e);for(const e of r)s.delete(e);return s}#fe(t){let r=t.response.raw.status,i=this.config.noSwap.map(e=>e+""),s=r+"";for(let r of[s,s.slice(0,2)+"x",s[0]+"xx"]){if(i.includes(r))return void(t.swap="none");let s=this.#E(t.sourceElement,"hx-status:"+r);if(s)return void e.merge(s,t)}}#$e(e,t){return new Promise(r=>{this.#m||=[],this.#m.push({task:e,resolve:r,ctx:t}),this.#x||this.#ft()})}async#ft(){if(0===this.#m.length||this.#x)return;this.#x=!0;let{task:e,resolve:t,ctx:r}=this.#m.shift();try{if(document.startViewTransition){let t={task:e,ctx:r};this.#L(r.sourceElement,"htmx:before:viewTransition",t),await document.startViewTransition(t.task).finished,this.#L(r.sourceElement,"htmx:after:viewTransition",t)}else await e()}catch(e){}finally{this.#x=!1,t(),this.#ft()}}#Ue(e,t){let r=t.querySelectorAll("[id]"),i=Object.fromEntries([...r].map(e=>[e.id,e])),s=e.querySelectorAll("[id]"),n=[];for(let e of s){let t=i[e.id];if(t?.tagName===e.tagName){let r=e.cloneNode(!1);this.#Qe(e,t),n.push(()=>{this.#Qe(e,r)})}}return n}#We(e,t){e?.classList?.add?.(t)}#Je(e,t){e?.classList?.remove?.(t),0===e?.classList?.length&&e.removeAttribute("class")}#Xe(e){return"string"==typeof e?this.find(e):e}#z(e){return this.config.metaCharacter?e.replace(/:/g,this.config.metaCharacter):e}}})();
|
package/public/logo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// The player in the editor modal, with the habits people bring from YouTube and Vimeo: it starts when
|
|
2
|
+
// opened, remembers the speed you chose, answers the keyboard, shows the chapter you are in, steps to the
|
|
3
|
+
// previous and next recording in the list, and a card shows a short silent preview while the pointer is on it.
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const SPEED_KEY = 'ws-media-speed';
|
|
8
|
+
const MIN = 0.5;
|
|
9
|
+
const MAX = 5;
|
|
10
|
+
const STEP = 0.25;
|
|
11
|
+
|
|
12
|
+
const store = {
|
|
13
|
+
get() { try { return localStorage.getItem(SPEED_KEY); } catch (_) { return null; } },
|
|
14
|
+
set(v) { try { localStorage.setItem(SPEED_KEY, String(v)); } catch (_) { /* private window */ } },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const savedSpeed = () => { const v = parseFloat(store.get()); return v >= MIN && v <= MAX ? v : 1; };
|
|
18
|
+
const clock = (s) => {
|
|
19
|
+
const n = Math.round(s); const h = Math.floor(n / 3600); const m = Math.floor((n % 3600) / 60);
|
|
20
|
+
const pad = (x) => String(x).padStart(2, '0');
|
|
21
|
+
return h ? `${h}:${pad(m)}:${pad(n % 60)}` : `${m}:${pad(n % 60)}`;
|
|
22
|
+
};
|
|
23
|
+
const cardOf = (el) => el.closest && el.closest('.media-card');
|
|
24
|
+
const mediaOf = (card) => card && card.querySelector('video, audio');
|
|
25
|
+
|
|
26
|
+
function setSpeed(card, media, rate) {
|
|
27
|
+
const r = Math.min(MAX, Math.max(MIN, Math.round(rate / STEP) * STEP));
|
|
28
|
+
media.playbackRate = r;
|
|
29
|
+
const label = card.querySelector('[data-speed-label]');
|
|
30
|
+
if (label) label.textContent = `${r}×`;
|
|
31
|
+
card.querySelectorAll('[data-sound-speed]').forEach((b) => {
|
|
32
|
+
const on = parseFloat(b.dataset.soundSpeed) === r;
|
|
33
|
+
b.classList.toggle('is-active', on);
|
|
34
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
35
|
+
});
|
|
36
|
+
store.set(r);
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- speed and loop -------------------------------------------------------------------------------
|
|
41
|
+
document.addEventListener('click', (e) => {
|
|
42
|
+
const b = e.target.closest && e.target.closest('[data-sound-speed]');
|
|
43
|
+
const card = b && cardOf(b);
|
|
44
|
+
const media = mediaOf(card);
|
|
45
|
+
if (!media) return;
|
|
46
|
+
setSpeed(card, media, parseFloat(b.dataset.soundSpeed) || 1);
|
|
47
|
+
const drop = b.closest('.uk-dropdown');
|
|
48
|
+
if (drop && window.UIkit) { try { window.UIkit.dropdown(drop).hide(false); } catch (_) { /* closed already */ } }
|
|
49
|
+
});
|
|
50
|
+
document.addEventListener('change', (e) => {
|
|
51
|
+
if (!e.target.matches || !e.target.matches('[data-sound-loop]')) return;
|
|
52
|
+
const media = mediaOf(cardOf(e.target));
|
|
53
|
+
if (media) media.loop = e.target.checked;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---- a section time, or a chapter, seeks ---------------------------------------------------------
|
|
57
|
+
document.addEventListener('click', (e) => {
|
|
58
|
+
const b = e.target.closest && e.target.closest('[data-seek]');
|
|
59
|
+
const media = mediaOf(b && cardOf(b));
|
|
60
|
+
if (!media) return;
|
|
61
|
+
media.currentTime = parseFloat(b.dataset.seek) || 0;
|
|
62
|
+
media.play().catch(() => { /* a gesture was asked for first; the seek still happened */ });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ---- previous and next, through the list the recording was opened from ---------------------------
|
|
66
|
+
let list = { els: [], index: -1 };
|
|
67
|
+
document.addEventListener('click', (e) => {
|
|
68
|
+
const t = e.target.closest && e.target.closest('[hx-get*="/play/"]');
|
|
69
|
+
if (!t || !t.closest('.fx-file')) return;
|
|
70
|
+
const els = [...document.querySelectorAll('.fx-file')].map((row) => row.querySelector('[hx-get*="/play/"]')).filter(Boolean);
|
|
71
|
+
list = { els, index: els.findIndex((x) => x.closest('.fx-file') === t.closest('.fx-file')) };
|
|
72
|
+
}, true);
|
|
73
|
+
document.addEventListener('click', (e) => {
|
|
74
|
+
const b = e.target.closest && e.target.closest('[data-media-step]');
|
|
75
|
+
if (!b || b.disabled) return;
|
|
76
|
+
const next = list.els[list.index + parseInt(b.dataset.mediaStep, 10)];
|
|
77
|
+
if (next && document.contains(next)) next.click();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ---- the card behind the modal --------------------------------------------------------------------
|
|
81
|
+
function setupChapters(card, media) {
|
|
82
|
+
const bar = card.querySelector('[data-chapters]');
|
|
83
|
+
const label = card.querySelector('[data-chapter]');
|
|
84
|
+
if (!bar) return;
|
|
85
|
+
let chapters = [];
|
|
86
|
+
try { chapters = JSON.parse(bar.dataset.chapters); } catch (_) { return; }
|
|
87
|
+
if (!chapters.length) return;
|
|
88
|
+
const draw = () => {
|
|
89
|
+
const total = media.duration || chapters[chapters.length - 1].end;
|
|
90
|
+
bar.innerHTML = '';
|
|
91
|
+
chapters.forEach((c, i) => {
|
|
92
|
+
const seg = document.createElement('button');
|
|
93
|
+
seg.type = 'button';
|
|
94
|
+
seg.className = 'chapter-seg';
|
|
95
|
+
seg.dataset.seek = c.start;
|
|
96
|
+
seg.title = `${c.title} · ${clock(c.start)}`;
|
|
97
|
+
seg.style.flexGrow = Math.max(0.5, Math.min(c.end, total) - c.start);
|
|
98
|
+
seg.dataset.index = i;
|
|
99
|
+
bar.appendChild(seg);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
let current = -1;
|
|
103
|
+
const track = () => {
|
|
104
|
+
const t = media.currentTime;
|
|
105
|
+
let i = chapters.findIndex((c) => t >= c.start && t < c.end);
|
|
106
|
+
if (i < 0) i = t >= chapters[chapters.length - 1].end ? chapters.length - 1 : 0;
|
|
107
|
+
if (i === current) return;
|
|
108
|
+
current = i;
|
|
109
|
+
bar.querySelectorAll('.chapter-seg').forEach((s, k) => { s.classList.toggle('is-past', k < i); s.classList.toggle('is-current', k === i); });
|
|
110
|
+
if (label) { label.hidden = false; label.textContent = chapters[i].title; }
|
|
111
|
+
const sections = card.querySelectorAll('.transcript-section');
|
|
112
|
+
sections.forEach((s) => s.classList.toggle('is-current', parseFloat(s.dataset.start) === chapters[i].start));
|
|
113
|
+
const now = [...sections].find((s) => s.classList.contains('is-current'));
|
|
114
|
+
const box = card.querySelector('.transcript-body');
|
|
115
|
+
if (now && box && !media.paused) box.scrollTo({ top: now.offsetTop - box.offsetTop - 8, behavior: 'smooth' });
|
|
116
|
+
};
|
|
117
|
+
draw();
|
|
118
|
+
media.addEventListener('loadedmetadata', draw);
|
|
119
|
+
media.addEventListener('timeupdate', track);
|
|
120
|
+
media.addEventListener('seeked', track);
|
|
121
|
+
track();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function setupCard(root) {
|
|
125
|
+
const card = root.querySelector && root.querySelector('.media-card:not([data-ready])');
|
|
126
|
+
const media = mediaOf(card);
|
|
127
|
+
if (!media) return;
|
|
128
|
+
card.dataset.ready = '1';
|
|
129
|
+
setSpeed(card, media, savedSpeed());
|
|
130
|
+
media.addEventListener('loadedmetadata', () => {
|
|
131
|
+
media.playbackRate = savedSpeed();
|
|
132
|
+
const slot = card.querySelector('[data-duration]');
|
|
133
|
+
if (slot && media.duration) slot.textContent = ` · ${clock(media.duration)}`;
|
|
134
|
+
});
|
|
135
|
+
setupChapters(card, media);
|
|
136
|
+
const at = list.index;
|
|
137
|
+
card.querySelectorAll('[data-media-step]').forEach((b) => {
|
|
138
|
+
const target = list.els[at + parseInt(b.dataset.mediaStep, 10)];
|
|
139
|
+
b.disabled = !(at >= 0 && target && document.contains(target));
|
|
140
|
+
});
|
|
141
|
+
media.play().catch(() => { /* autoplay refused; the play button is there */ });
|
|
142
|
+
}
|
|
143
|
+
const onSwap = (e) => {
|
|
144
|
+
const el = (e.detail && e.detail.target) || e.target;
|
|
145
|
+
if (el && el.id === 'editor-modal-body') setupCard(el);
|
|
146
|
+
};
|
|
147
|
+
document.body.addEventListener('htmx:after:swap', onSwap);
|
|
148
|
+
document.body.addEventListener('htmx:after:settle', onSwap);
|
|
149
|
+
|
|
150
|
+
// ---- the keyboard -----------------------------------------------------------------------------------
|
|
151
|
+
document.addEventListener('keydown', (e) => {
|
|
152
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
153
|
+
const modal = document.getElementById('editor-modal');
|
|
154
|
+
const card = modal && modal.classList.contains('uk-open') && document.querySelector('#editor-modal-body .media-card');
|
|
155
|
+
const media = mediaOf(card);
|
|
156
|
+
if (!media) return;
|
|
157
|
+
const t = e.target;
|
|
158
|
+
if (t.closest && t.closest('input:not([type="checkbox"]), textarea, select, [contenteditable="true"]')) return;
|
|
159
|
+
if (t.closest && t.closest('button, a, summary') && (e.key === ' ' || e.key === 'Enter')) return;
|
|
160
|
+
const seek = (by) => { media.currentTime = Math.max(0, Math.min(media.duration || Infinity, media.currentTime + by)); };
|
|
161
|
+
const key = e.key;
|
|
162
|
+
let handled = true;
|
|
163
|
+
if (key === ' ' || key === 'k' || key === 'K') { if (media.paused) media.play().catch(() => {}); else media.pause(); }
|
|
164
|
+
else if (key === 'j' || key === 'J') seek(-10);
|
|
165
|
+
else if (key === 'l' || key === 'L') seek(10);
|
|
166
|
+
else if (key === 'ArrowLeft') seek(-5);
|
|
167
|
+
else if (key === 'ArrowRight') seek(5);
|
|
168
|
+
else if (key === 'ArrowUp') media.volume = Math.min(1, media.volume + 0.1);
|
|
169
|
+
else if (key === 'ArrowDown') media.volume = Math.max(0, media.volume - 0.1);
|
|
170
|
+
else if (key === 'm' || key === 'M') media.muted = !media.muted;
|
|
171
|
+
else if ((key === 'f' || key === 'F') && media.tagName === 'VIDEO') {
|
|
172
|
+
if (document.fullscreenElement) document.exitFullscreen(); else media.requestFullscreen().catch(() => {});
|
|
173
|
+
} else if (key === '<' || key === ',') setSpeed(card, media, media.playbackRate - STEP);
|
|
174
|
+
else if (key === '>' || key === '.') setSpeed(card, media, media.playbackRate + STEP);
|
|
175
|
+
else if (/^[0-9]$/.test(key) && media.duration) media.currentTime = (media.duration * parseInt(key, 10)) / 10;
|
|
176
|
+
else if (key === 'Home') media.currentTime = 0;
|
|
177
|
+
else if (key === 'End' && media.duration) media.currentTime = media.duration - 0.1;
|
|
178
|
+
else handled = false;
|
|
179
|
+
if (handled) e.preventDefault();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---- a card shows a short, silent preview while the pointer rests on it ----------------------------
|
|
183
|
+
let timer = null;
|
|
184
|
+
let preview = null;
|
|
185
|
+
const calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
186
|
+
const hovers = window.matchMedia && window.matchMedia('(hover: hover)').matches;
|
|
187
|
+
function stopPreview() {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
if (preview) { try { preview.pause(); } catch (_) { /* gone */ } preview.remove(); preview = null; }
|
|
190
|
+
}
|
|
191
|
+
document.addEventListener('mouseover', (e) => {
|
|
192
|
+
if (calm || !hovers) return;
|
|
193
|
+
const thumb = e.target.closest && e.target.closest('.fx-thumb[data-preview]');
|
|
194
|
+
if (!thumb || thumb.contains(e.relatedTarget) || getComputedStyle(thumb).display === 'none') return;
|
|
195
|
+
stopPreview();
|
|
196
|
+
timer = setTimeout(() => {
|
|
197
|
+
const v = document.createElement('video');
|
|
198
|
+
v.className = 'fx-preview';
|
|
199
|
+
v.muted = true;
|
|
200
|
+
v.playsInline = true;
|
|
201
|
+
v.preload = 'auto';
|
|
202
|
+
v.src = thumb.dataset.preview;
|
|
203
|
+
v.addEventListener('timeupdate', () => { if (v.currentTime > 6) v.currentTime = 0.2; });
|
|
204
|
+
v.addEventListener('playing', () => thumb.classList.add('is-previewing'));
|
|
205
|
+
thumb.insertBefore(v, thumb.firstChild);
|
|
206
|
+
preview = v;
|
|
207
|
+
v.play().catch(stopPreview);
|
|
208
|
+
}, 450);
|
|
209
|
+
});
|
|
210
|
+
document.addEventListener('mouseout', (e) => {
|
|
211
|
+
const thumb = e.target.closest && e.target.closest('.fx-thumb[data-preview]');
|
|
212
|
+
if (thumb && !thumb.contains(e.relatedTarget)) { thumb.classList.remove('is-previewing'); stopPreview(); }
|
|
213
|
+
});
|
|
214
|
+
document.addEventListener('click', stopPreview, true);
|
|
215
|
+
})();
|
package/public/mic.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* The small microphone beside every text field.
|
|
2
|
+
*
|
|
3
|
+
* A different thing from the assistant's microphone: that one is deep-chat's, configured as an
|
|
4
|
+
* object and drawn inside its shadow root. These are plain `SpeechRecognition`, added to every field
|
|
5
|
+
* worth typing into — 36 of them on the settings page alone — and they came first.
|
|
6
|
+
*
|
|
7
|
+
* They used to pin themselves to `document.documentElement.lang`, which meant `speech_language` in
|
|
8
|
+
* settings.yml moved the assistant and left every other microphone on English. That is the failure
|
|
9
|
+
* this whole setting exists to prevent: a recogniser on the wrong language does not error, it returns
|
|
10
|
+
* confident nonsense, so the bug looks like bad dictation rather than a configuration problem. Both
|
|
11
|
+
* kinds of microphone now read the same resolved config, from speech.js, through `window.WS_SPEECH`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
18
|
+
|
|
19
|
+
// Resolved server-side by speech.js. Absent only if the page failed to render its head, in which
|
|
20
|
+
// case the browser's own defaults are the right fallback rather than refusing to work.
|
|
21
|
+
const cfg = (window.WS_SPEECH && window.WS_SPEECH.fieldMic) || null;
|
|
22
|
+
const enabled = () => !!SpeechRecognition && cfg !== false;
|
|
23
|
+
|
|
24
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
25
|
+
if (!SpeechRecognition) {
|
|
26
|
+
document.querySelectorAll('.mic-btn').forEach((b) => { b.hidden = true; });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Dictate into a field. Shared by the composer's round mic and every field mic. */
|
|
31
|
+
function dictateInto(btn, field) {
|
|
32
|
+
if (!enabled() || !field) return;
|
|
33
|
+
const rec = new SpeechRecognition();
|
|
34
|
+
|
|
35
|
+
// An empty lang is left UNSET rather than assigned "": SpeechRecognition treats an empty string
|
|
36
|
+
// as a language it cannot match, where an unset one correctly means "use the browser's".
|
|
37
|
+
if (cfg && cfg.lang) rec.lang = cfg.lang;
|
|
38
|
+
else if (document.documentElement.lang) rec.lang = document.documentElement.lang;
|
|
39
|
+
|
|
40
|
+
rec.interimResults = !!(cfg && cfg.interimResults);
|
|
41
|
+
rec.maxAlternatives = 1;
|
|
42
|
+
btn.classList.add('listening');
|
|
43
|
+
|
|
44
|
+
// What was in the field before this started. Interim results rewrite the tail as the recogniser
|
|
45
|
+
// changes its mind, so the original has to be remembered rather than read back each time — with
|
|
46
|
+
// interim results on, reading the live value would compound each guess onto the last.
|
|
47
|
+
const before = field.value.trim();
|
|
48
|
+
let settled = '';
|
|
49
|
+
|
|
50
|
+
rec.onresult = (ev) => {
|
|
51
|
+
let interim = '';
|
|
52
|
+
for (let i = ev.resultIndex; i < ev.results.length; i += 1) {
|
|
53
|
+
const chunk = ev.results[i][0].transcript;
|
|
54
|
+
if (ev.results[i].isFinal) settled += chunk;
|
|
55
|
+
else interim += chunk;
|
|
56
|
+
}
|
|
57
|
+
const said = (settled + interim).trim();
|
|
58
|
+
// Append rather than replace: a field may already hold something typed, and losing it to a
|
|
59
|
+
// stray mic press is worse than a long value.
|
|
60
|
+
field.value = before ? `${before} ${said}` : said;
|
|
61
|
+
if (field.setSelectionRange) field.setSelectionRange(field.value.length, field.value.length);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Silence, a denied microphone and an unsupported language all arrive here. Saying which is
|
|
65
|
+
// worth the two lines: "no-speech" means try again, "not-allowed" means a browser permission
|
|
66
|
+
// nobody can guess at, and "language-not-supported" is a setting to change.
|
|
67
|
+
rec.onerror = (ev) => {
|
|
68
|
+
btn.classList.remove('listening');
|
|
69
|
+
const why = {
|
|
70
|
+
'not-allowed': 'the browser blocked microphone access for this site',
|
|
71
|
+
'service-not-allowed': 'the browser blocked microphone access for this site',
|
|
72
|
+
'language-not-supported': `this browser has no recogniser for ${rec.lang || 'the page language'}`,
|
|
73
|
+
'no-speech': 'nothing was heard',
|
|
74
|
+
'audio-capture': 'no microphone was found',
|
|
75
|
+
}[ev.error];
|
|
76
|
+
if (why) btn.setAttribute('title', `Dictation stopped: ${why}`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
rec.onend = () => {
|
|
80
|
+
btn.classList.remove('listening');
|
|
81
|
+
field.focus();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
rec.start();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
document.addEventListener('click', (e) => {
|
|
88
|
+
const btn = e.target.closest('.mic-btn');
|
|
89
|
+
if (!btn || !enabled()) return;
|
|
90
|
+
const form = btn.closest('form');
|
|
91
|
+
dictateInto(btn, form && form.querySelector('input[name="message"]'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
document.addEventListener('click', (e) => {
|
|
95
|
+
const btn = e.target.closest('.field-mic');
|
|
96
|
+
if (!btn) return;
|
|
97
|
+
e.preventDefault(); // it lives inside forms; it must never submit one
|
|
98
|
+
dictateInto(btn, btn.parentElement.querySelector('input, textarea'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/* Give every field that wants typing a mic, rather than hand-adding markup to each one —
|
|
102
|
+
* including fields that arrive later in an HTMX fragment.
|
|
103
|
+
*
|
|
104
|
+
* Skipped: the composer, which has its own round mic; anything not typed into (checkbox, select,
|
|
105
|
+
* file, hidden); and password fields, where dictating a secret aloud is not something to offer.
|
|
106
|
+
*/
|
|
107
|
+
function addFieldMics(root) {
|
|
108
|
+
if (!enabled()) return;
|
|
109
|
+
const fields = (root || document).querySelectorAll(
|
|
110
|
+
'input[type="text"]:not([data-mic]), input:not([type]):not([data-mic]), textarea:not([data-mic])');
|
|
111
|
+
fields.forEach((field) => {
|
|
112
|
+
field.dataset.mic = '1';
|
|
113
|
+
if (field.closest('.chat-input')) return;
|
|
114
|
+
if (field.readOnly || field.disabled) return;
|
|
115
|
+
const wrap = document.createElement('span');
|
|
116
|
+
wrap.className = 'field-mic-wrap';
|
|
117
|
+
field.parentNode.insertBefore(wrap, field);
|
|
118
|
+
wrap.appendChild(field);
|
|
119
|
+
const btn = document.createElement('button');
|
|
120
|
+
btn.type = 'button';
|
|
121
|
+
btn.className = 'field-mic';
|
|
122
|
+
// The language is named in the tooltip. Dictation that produces nonsense looks like a broken
|
|
123
|
+
// microphone; seeing "Dictate (Arabic (Jordan))" is what turns it into a setting to check.
|
|
124
|
+
const label = (cfg && cfg.lang) ? `Dictate into this field (${cfg.lang})` : 'Dictate into this field';
|
|
125
|
+
btn.title = label;
|
|
126
|
+
btn.setAttribute('aria-label', label);
|
|
127
|
+
btn.innerHTML = '<span uk-icon="icon: microphone; ratio: .7"></span>';
|
|
128
|
+
wrap.appendChild(btn);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
document.addEventListener('DOMContentLoaded', () => addFieldMics());
|
|
133
|
+
document.body.addEventListener('htmx:after:swap', (e) => {
|
|
134
|
+
const t = (e.detail && e.detail.target) || e.target;
|
|
135
|
+
addFieldMics(t && t.isConnected ? t : undefined);
|
|
136
|
+
});
|
|
137
|
+
}());
|
|
138
|
+
|
|
139
|
+
/* What voices this browser actually has, shown where the setting asks for one.
|
|
140
|
+
*
|
|
141
|
+
* Two problems, one answer.
|
|
142
|
+
*
|
|
143
|
+
* `voice_name` asks for a name spelled exactly as the browser spells it — "Google US English",
|
|
144
|
+
* "Microsoft Zira - English (United States)" — and the lists differ per browser, per OS and per
|
|
145
|
+
* install. Asking someone to type one blind is asking them to guess.
|
|
146
|
+
*
|
|
147
|
+
* Worse, a browser with NO voices installed fails silently. `speechSynthesis` exists, `speak()`
|
|
148
|
+
* accepts the utterance, and the only sign is an `error: synthesis-failed` nobody is listening for.
|
|
149
|
+
* Headless Chromium has none; so does a Linux desktop without speech-dispatcher. The read-aloud
|
|
150
|
+
* setting then looks switched on and does nothing at all, which is the worst state a setting can be
|
|
151
|
+
* in — so if there are none, this says so instead of leaving a silent success.
|
|
152
|
+
*/
|
|
153
|
+
(function voicePicker() {
|
|
154
|
+
'use strict';
|
|
155
|
+
|
|
156
|
+
/* Fill the reading-voice dropdown with the voices this browser actually has.
|
|
157
|
+
*
|
|
158
|
+
* The server cannot do it: which voices exist is a property of the browser and the OS, they differ
|
|
159
|
+
* per machine, and the names are exact strings ("Microsoft Zira - English (United States)"). So the
|
|
160
|
+
* server renders the select with the saved value and this adds the rest.
|
|
161
|
+
*
|
|
162
|
+
* The dictation language leads the order. `voice_name: default` means "let the browser choose for
|
|
163
|
+
* the language we set", which with en-US pinned is an en-US voice — so the voices for that language
|
|
164
|
+
* are the ones offered first, and the rest are still reachable below them.
|
|
165
|
+
*
|
|
166
|
+
* A browser with NO voices fails silently: `speechSynthesis` exists, `speak()` accepts the
|
|
167
|
+
* utterance, and the only sign is an `error: synthesis-failed` nobody is listening for. Headless
|
|
168
|
+
* Chromium has none, and so does a Linux desktop without speech-dispatcher — the read-aloud setting
|
|
169
|
+
* then looks switched on and does nothing at all. That case says so rather than leaving an empty
|
|
170
|
+
* dropdown that reads as "still loading".
|
|
171
|
+
*/
|
|
172
|
+
const mount = () => {
|
|
173
|
+
const select = document.querySelector('select[data-voice-name]');
|
|
174
|
+
const note = document.querySelector('[data-voice-list]');
|
|
175
|
+
if (!select || select.dataset.done) return;
|
|
176
|
+
|
|
177
|
+
const lang = (window.WS_SPEECH && window.WS_SPEECH.fieldMic && window.WS_SPEECH.fieldMic.lang) || '';
|
|
178
|
+
|
|
179
|
+
const render = (voices) => {
|
|
180
|
+
select.dataset.done = '1';
|
|
181
|
+
|
|
182
|
+
if (!voices.length) {
|
|
183
|
+
if (note) {
|
|
184
|
+
note.innerHTML = '<span class="uk-text-danger">This browser has no speech voices installed, '
|
|
185
|
+
+ 'so nothing will be read aloud whatever this is set to. On Linux that usually means '
|
|
186
|
+
+ '<code>speech-dispatcher</code> and a voice package are missing.</span>';
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const saved = select.value;
|
|
192
|
+
// Voices for the dictation language first — with en-US pinned, those are the ones anybody
|
|
193
|
+
// picking a "reading voice" actually means.
|
|
194
|
+
const preferred = lang ? voices.filter((v) => v.lang && v.lang.replace('_', '-') === lang) : [];
|
|
195
|
+
const rest = voices.filter((v) => !preferred.includes(v));
|
|
196
|
+
|
|
197
|
+
const optionFor = (v) => {
|
|
198
|
+
const o = document.createElement('option');
|
|
199
|
+
o.value = v.name;
|
|
200
|
+
o.textContent = `${v.name} — ${v.lang}`;
|
|
201
|
+
return o;
|
|
202
|
+
};
|
|
203
|
+
const groupFor = (label, list) => {
|
|
204
|
+
if (!list.length) return null;
|
|
205
|
+
const g = document.createElement('optgroup');
|
|
206
|
+
g.label = label;
|
|
207
|
+
list.forEach((v) => g.appendChild(optionFor(v)));
|
|
208
|
+
return g;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Keep the "default" entry and drop the placeholder copy of the saved value; it comes back as
|
|
212
|
+
// a real option below if the browser still has that voice.
|
|
213
|
+
[...select.querySelectorAll('option')].forEach((o) => { if (o.value !== 'default') o.remove(); });
|
|
214
|
+
const groups = [
|
|
215
|
+
groupFor(lang ? `Voices for ${lang}` : 'Voices', preferred),
|
|
216
|
+
groupFor(preferred.length ? 'Other languages' : 'Voices', rest),
|
|
217
|
+
].filter(Boolean);
|
|
218
|
+
groups.forEach((g) => select.appendChild(g));
|
|
219
|
+
|
|
220
|
+
if (saved && saved !== 'default') {
|
|
221
|
+
if ([...select.options].some((o) => o.value === saved)) {
|
|
222
|
+
select.value = saved;
|
|
223
|
+
} else {
|
|
224
|
+
// The saved voice is not installed here. Keep it and say so rather than silently switching
|
|
225
|
+
// to something else — the file says one thing and the form must not quietly say another.
|
|
226
|
+
const o = document.createElement('option');
|
|
227
|
+
o.value = saved;
|
|
228
|
+
o.textContent = `${saved} — not installed in this browser`;
|
|
229
|
+
select.appendChild(o);
|
|
230
|
+
select.value = saved;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (note) {
|
|
235
|
+
note.innerHTML = `<span class="uk-text-meta">${voices.length} voices in this browser`
|
|
236
|
+
+ `${preferred.length ? `, ${preferred.length} for ${lang}` : ''}. `
|
|
237
|
+
+ 'Default lets the browser choose one for the dictation language.</span>';
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// getVoices() is empty until the engine has enumerated them, and `voiceschanged` may never fire
|
|
242
|
+
// when there are none — so the timeout is the answer for "none", not a fallback for slowness.
|
|
243
|
+
const now = window.speechSynthesis.getVoices();
|
|
244
|
+
if (now.length) return render(now);
|
|
245
|
+
const timer = setTimeout(() => render(window.speechSynthesis.getVoices()), 1500);
|
|
246
|
+
window.speechSynthesis.onvoiceschanged = () => {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
render(window.speechSynthesis.getVoices());
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (!window.speechSynthesis) return;
|
|
253
|
+
document.addEventListener('DOMContentLoaded', mount);
|
|
254
|
+
document.body && document.body.addEventListener('htmx:after:swap', mount);
|
|
255
|
+
}());
|