@joewinke/jatui 0.38.5 → 0.39.0

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.
Files changed (67) hide show
  1. package/CLAUDE.md +32 -1
  2. package/THIRD_PARTY_NOTICES.md +51 -0
  3. package/dist/components/BulkActionBar.svelte +32 -2
  4. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  5. package/dist/components/FloatingActionBar.svelte +102 -34
  6. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  7. package/dist/components/MediaExplorer.svelte +249 -0
  8. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  9. package/dist/components/NotificationCenter.svelte +481 -0
  10. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  11. package/dist/components/SearchDropdown.svelte +54 -8
  12. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  13. package/dist/components/VideoPlayer.svelte +269 -3
  14. package/dist/components/agent/AgentLog.svelte +217 -0
  15. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  16. package/dist/components/agent/AgentLogRow.svelte +281 -0
  17. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  18. package/dist/components/agent/AgentTaskList.svelte +607 -0
  19. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  20. package/dist/components/agent/AgentThinking.svelte +280 -0
  21. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  22. package/dist/components/agent/ComposerGlow.svelte +265 -0
  23. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  24. package/dist/components/agent/ContextMeter.svelte +277 -0
  25. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  26. package/dist/components/agent/Questionnaire.svelte +767 -0
  27. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  28. package/dist/components/agent/ShimmerText.svelte +100 -0
  29. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  30. package/dist/components/agent/StreamedText.svelte +135 -0
  31. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  32. package/dist/components/agent/agentLog.d.ts +22 -0
  33. package/dist/components/agent/agentLog.js +26 -0
  34. package/dist/components/agent/agentTaskList.d.ts +90 -0
  35. package/dist/components/agent/agentTaskList.js +93 -0
  36. package/dist/components/agent/agentThinking.d.ts +46 -0
  37. package/dist/components/agent/agentThinking.js +78 -0
  38. package/dist/components/agent/contextMeter.d.ts +97 -0
  39. package/dist/components/agent/contextMeter.js +156 -0
  40. package/dist/components/agent/questionnaire.d.ts +104 -0
  41. package/dist/components/agent/questionnaire.js +225 -0
  42. package/dist/components/agent/revealCore.d.ts +74 -0
  43. package/dist/components/agent/revealCore.js +160 -0
  44. package/dist/components/agent/revealTicker.d.ts +34 -0
  45. package/dist/components/agent/revealTicker.js +73 -0
  46. package/dist/components/agent/streamedText.d.ts +28 -0
  47. package/dist/components/agent/streamedText.js +59 -0
  48. package/dist/components/messaging/MessageInput.svelte +34 -4
  49. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  50. package/dist/components/messaging/MessageItem.svelte +2 -2
  51. package/dist/components/notificationCenter.d.ts +65 -0
  52. package/dist/components/notificationCenter.js +110 -0
  53. package/dist/components/progress/Steps.svelte +269 -5
  54. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  55. package/dist/components/progress/stepsSummary.d.ts +14 -0
  56. package/dist/components/progress/stepsSummary.js +32 -0
  57. package/dist/index.d.ts +34 -0
  58. package/dist/index.js +29 -0
  59. package/dist/selection/clickSelection.d.ts +89 -0
  60. package/dist/selection/clickSelection.js +152 -0
  61. package/dist/selection/createClickSelection.d.ts +79 -0
  62. package/dist/selection/createClickSelection.js +134 -0
  63. package/dist/video/keys.d.ts +5 -5
  64. package/dist/video/keys.js +9 -5
  65. package/dist/video/transcriptSearch.d.ts +80 -0
  66. package/dist/video/transcriptSearch.js +153 -0
  67. package/package.json +3 -2
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Pointer-first multi-selection: the desktop-standard click rules, as pure
3
+ * functions over a plain `Set` (a `SvelteSet` is a `Set`, so the reactive
4
+ * controller in `createClickSelection.svelte.ts` runs exactly this code).
5
+ *
6
+ * click select only this one; it becomes the anchor
7
+ * Ctrl/⌘ click toggle this one; it becomes the anchor
8
+ * Shift click select the range anchor..this, in the current id order
9
+ * (replaces the selection; the anchor stays put)
10
+ * Shift Ctrl/⌘ add the range anchor..this to the selection
11
+ * Shift arrows extend from the anchor (the caller says which id an
12
+ * arrow lands on, because only it knows the layout)
13
+ * Ctrl/⌘ A select every id
14
+ * Esc clear
15
+ *
16
+ * The selection is a set of ids, not of positions: re-sorting, filtering or
17
+ * live inserts keep it, and an id that is no longer in `ids` is simply not
18
+ * selected any more (`prune` drops it, the readers filter it out first).
19
+ *
20
+ * `head` is the moving end of the last range (the last id clicked or
21
+ * extended to), so Shift+arrows keep growing from where they left off.
22
+ *
23
+ * No DOM, no Svelte: unit-tested in tests/unit/clickSelection.test.ts.
24
+ */
25
+ import { isTypingTarget } from '../utils/keyboardShortcutRegistry';
26
+ /** Ctrl on Linux/Windows, ⌘ on macOS: either counts as "toggle / add". */
27
+ export const isAdditive = (m) => !!(m.ctrlKey || m.metaKey);
28
+ /** The ids from `a` to `b` inclusive, in `ids` order (either direction). Empty when either is missing. */
29
+ export function rangeBetween(ids, a, b) {
30
+ const i = ids.indexOf(a);
31
+ const j = ids.indexOf(b);
32
+ if (i < 0 || j < 0)
33
+ return [];
34
+ return i <= j ? ids.slice(i, j + 1) : ids.slice(j, i + 1);
35
+ }
36
+ /** Select only `id` and make it the anchor (a plain click, or a plain arrow move). */
37
+ export function selectOnly(s, id) {
38
+ s.selected.clear();
39
+ s.selected.add(id);
40
+ s.anchor = id;
41
+ s.head = id;
42
+ }
43
+ /** Toggle `id` and make it the anchor (Ctrl/⌘ click). */
44
+ export function toggleOne(s, id) {
45
+ if (s.selected.has(id))
46
+ s.selected.delete(id);
47
+ else
48
+ s.selected.add(id);
49
+ s.anchor = id;
50
+ s.head = id;
51
+ }
52
+ /**
53
+ * Select the range from the anchor to `id` (Shift click / Shift arrows).
54
+ * `additive` keeps what was selected and adds the range (Shift+Ctrl/⌘).
55
+ * With no usable anchor (none yet, or it vanished) it behaves as a plain
56
+ * click on `id` (or a toggle-on when additive).
57
+ */
58
+ export function extendTo(s, ids, id, additive = false) {
59
+ if (!ids.includes(id))
60
+ return;
61
+ const anchor = s.anchor !== null && ids.includes(s.anchor) ? s.anchor : null;
62
+ if (anchor === null) {
63
+ if (additive) {
64
+ s.selected.add(id);
65
+ s.anchor = id;
66
+ s.head = id;
67
+ }
68
+ else
69
+ selectOnly(s, id);
70
+ return;
71
+ }
72
+ const range = rangeBetween(ids, anchor, id);
73
+ if (!additive)
74
+ s.selected.clear();
75
+ for (const x of range)
76
+ s.selected.add(x);
77
+ s.head = id;
78
+ }
79
+ /** Apply one click on `id` with its modifiers. Ids not in `ids` are ignored. */
80
+ export function applyClick(s, ids, id, m = {}) {
81
+ if (!ids.includes(id))
82
+ return;
83
+ prune(s, ids);
84
+ if (m.shiftKey)
85
+ return extendTo(s, ids, id, isAdditive(m));
86
+ if (isAdditive(m))
87
+ return toggleOne(s, id);
88
+ selectOnly(s, id);
89
+ }
90
+ /** Select every id (Ctrl/⌘ A). The anchor stays if it is still there, else the first id. */
91
+ export function selectAll(s, ids) {
92
+ s.selected.clear();
93
+ for (const x of ids)
94
+ s.selected.add(x);
95
+ if (s.anchor === null || !ids.includes(s.anchor))
96
+ s.anchor = ids[0] ?? null;
97
+ s.head = ids.at(-1) ?? null;
98
+ }
99
+ /** Clear the selection and the range ends (Esc). */
100
+ export function clearSelection(s) {
101
+ s.selected.clear();
102
+ s.anchor = null;
103
+ s.head = null;
104
+ }
105
+ /** Drop selected ids that are no longer in `ids` (and an anchor or head that vanished). True when anything changed. */
106
+ export function prune(s, ids) {
107
+ const present = new Set(ids);
108
+ let changed = false;
109
+ for (const x of [...s.selected]) {
110
+ if (!present.has(x)) {
111
+ s.selected.delete(x);
112
+ changed = true;
113
+ }
114
+ }
115
+ if (s.anchor !== null && !present.has(s.anchor)) {
116
+ s.anchor = null;
117
+ changed = true;
118
+ }
119
+ if (s.head !== null && !present.has(s.head)) {
120
+ s.head = null;
121
+ changed = true;
122
+ }
123
+ return changed;
124
+ }
125
+ /** The selected ids that are still present, in `ids` order (what a range or a list shows). */
126
+ export const inOrder = (selected, ids) => ids.filter((x) => selected.has(x));
127
+ /** The selected ids that are still present, in the order they were selected (Set insertion order). */
128
+ export function inPickOrder(selected, ids) {
129
+ const present = new Set(ids);
130
+ return [...selected].filter((x) => present.has(x));
131
+ }
132
+ export const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
133
+ /**
134
+ * Which selection key this is, if any: Ctrl/⌘ A (select all), Esc (clear,
135
+ * only while something is selected, so Esc keeps its other meanings when
136
+ * nothing is), Shift+arrows (extend; with Ctrl/⌘ too, add the range).
137
+ * Never while typing in a field, never with Alt, never when already handled.
138
+ */
139
+ export function selectionKey(e, hasSelection) {
140
+ if (e.defaultPrevented || e.altKey)
141
+ return null;
142
+ if (isTypingTarget(e.target ?? null))
143
+ return null;
144
+ const mod = isAdditive(e);
145
+ if (mod && !e.shiftKey && (e.key === 'a' || e.key === 'A'))
146
+ return { kind: 'selectAll' };
147
+ if (e.key === 'Escape' && !mod && !e.shiftKey)
148
+ return hasSelection ? { kind: 'clear' } : null;
149
+ if (e.shiftKey && ARROW_KEYS.includes(e.key))
150
+ return { kind: 'extend', key: e.key, additive: mod };
151
+ return null;
152
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * createClickSelection: a reactive multi-selection for grids and lists that are
3
+ * used with the mouse (Shift-click ranges, Ctrl/⌘-click toggles) and the
4
+ * keyboard (Shift+arrows, Ctrl/⌘ A, Esc). The rules live in `clickSelection.ts`
5
+ * (pure, unit-tested); this wraps them in a `SvelteSet` plus `$state` ends so a
6
+ * component re-renders when the selection changes.
7
+ *
8
+ * Usage:
9
+ *
10
+ * const sel = createClickSelection({ ids: () => items.map((i) => i.id) });
11
+ *
12
+ * {#each items as item (item.id)}
13
+ * <button class:selected={sel.has(item.id)} onclick={(e) => sel.click(item.id, e)}>…</button>
14
+ * {/each}
15
+ *
16
+ * <svelte:window onkeydown={(e) => sel.handleKeydown(e, { next: (from, key) => neighbour(from, key) })} />
17
+ *
18
+ * <BulkActionBar selection={sel.ordered} actions={…} onClear={() => sel.clear()} />
19
+ *
20
+ * `ids()` is read on every call, in display order, so ranges always follow
21
+ * what is on screen; ids that left it are never reported (`ordered`,
22
+ * `picked`, `count`) and are dropped from the set on the next change.
23
+ *
24
+ * With listNav: listNav is the keyboard-only list motion (j/k, vim counts) and
25
+ * has its own opt-in selection (x, V, Shift+J/K). Give a surface ONE selection
26
+ * owner. A pointer-first grid uses clickSelection; a keyboard-first list uses
27
+ * listNav's. To have both, run listNav with `selectable: false` for focus
28
+ * motion and let clickSelection own the set: call `sel.click(id, e)` from the
29
+ * click handler and `sel.extendTo(id)` from your own Shift+J/K.
30
+ */
31
+ import { SvelteSet } from 'svelte/reactivity';
32
+ import { type ArrowKey, type SelectionKeyEvent, type SelectionModifiers } from './clickSelection';
33
+ export interface ClickSelectionOptions<Id = string> {
34
+ /** Every selectable id, in the order shown. Read lazily on each call. */
35
+ ids: () => readonly Id[];
36
+ /** Called after every change, with the selection in display order. */
37
+ onChange?: (ordered: Id[]) => void;
38
+ }
39
+ export interface ClickSelectionKeyOptions<Id = string> {
40
+ /**
41
+ * Where an arrow lands from `from` (null: the selection has no moving end
42
+ * yet). Return null to do nothing (an edge). Only the caller knows the
43
+ * layout (a grid's row above, a list's next item), so it decides.
44
+ */
45
+ next?: (from: Id | null, key: ArrowKey) => Id | null;
46
+ }
47
+ export interface ClickSelection<Id = string> {
48
+ /** The raw set (may briefly hold an id that just left `ids`; prefer `ordered`). */
49
+ readonly selected: SvelteSet<Id>;
50
+ /** The fixed end of a range: the last plain or Ctrl/⌘ click. */
51
+ readonly anchor: Id | null;
52
+ /** The moving end: the last id clicked or extended to. */
53
+ readonly head: Id | null;
54
+ /** Selected and still present, in display order. */
55
+ readonly ordered: Id[];
56
+ /** Selected and still present, in the order they were picked. */
57
+ readonly picked: Id[];
58
+ /** How many are selected and present. */
59
+ readonly count: number;
60
+ has(id: Id): boolean;
61
+ /** Apply a click with its modifiers (MouseEvent works as is). */
62
+ click(id: Id, mods?: SelectionModifiers): void;
63
+ /** Select only this one (a plain click or a plain arrow move). */
64
+ only(id: Id): void;
65
+ /** Toggle one (Ctrl/⌘ click, or an "x" key). */
66
+ toggle(id: Id): void;
67
+ /** Range from the anchor to `id`; `additive` keeps the rest. */
68
+ extendTo(id: Id, additive?: boolean): void;
69
+ selectAll(): void;
70
+ clear(): void;
71
+ /** Drop ids no longer in `ids()`. Every other method does this first. */
72
+ prune(): void;
73
+ /**
74
+ * Ctrl/⌘ A, Esc (while something is selected) and Shift+arrows (with
75
+ * `next`). Returns true and prevents the default when it handled the key.
76
+ */
77
+ handleKeydown(e: KeyboardEvent | SelectionKeyEvent, opts?: ClickSelectionKeyOptions<Id>): boolean;
78
+ }
79
+ export declare function createClickSelection<Id = string>(options: ClickSelectionOptions<Id>): ClickSelection<Id>;
@@ -0,0 +1,134 @@
1
+ /* createClickSelection.svelte.js generated by Svelte v5.53.11 */
2
+ import * as $ from 'svelte/internal/client';
3
+ import { SvelteSet } from 'svelte/reactivity';
4
+
5
+ import {
6
+ applyClick,
7
+ clearSelection,
8
+ extendTo,
9
+ inOrder,
10
+ inPickOrder,
11
+ prune,
12
+ selectAll,
13
+ selectionKey,
14
+ selectOnly,
15
+ toggleOne
16
+ } from './clickSelection';
17
+
18
+ export function createClickSelection(options) {
19
+ const selected = new SvelteSet();
20
+ let anchor = $.state(null);
21
+ let head = $.state(null);
22
+
23
+ const state = {
24
+ selected,
25
+ get anchor() {
26
+ return $.get(anchor);
27
+ },
28
+
29
+ set anchor(v) {
30
+ $.set(anchor, v, true);
31
+ },
32
+
33
+ get head() {
34
+ return $.get(head);
35
+ },
36
+
37
+ set head(v) {
38
+ $.set(head, v, true);
39
+ }
40
+ };
41
+
42
+ const ids = () => options.ids();
43
+ const changed = () => options.onChange?.(inOrder(selected, ids()));
44
+
45
+ return {
46
+ selected,
47
+ get anchor() {
48
+ return $.get(anchor);
49
+ },
50
+
51
+ get head() {
52
+ return $.get(head);
53
+ },
54
+
55
+ get ordered() {
56
+ return inOrder(selected, ids());
57
+ },
58
+
59
+ get picked() {
60
+ return inPickOrder(selected, ids());
61
+ },
62
+
63
+ get count() {
64
+ return inOrder(selected, ids()).length;
65
+ },
66
+ has: (id) => selected.has(id) && ids().includes(id),
67
+ click(id, mods = {}) {
68
+ applyClick(state, ids(), id, mods);
69
+ changed();
70
+ },
71
+
72
+ only(id) {
73
+ prune(state, ids());
74
+ selectOnly(state, id);
75
+ changed();
76
+ },
77
+
78
+ toggle(id) {
79
+ prune(state, ids());
80
+ toggleOne(state, id);
81
+ changed();
82
+ },
83
+
84
+ extendTo(id, additive = false) {
85
+ const all = ids();
86
+
87
+ prune(state, all);
88
+ extendTo(state, all, id, additive);
89
+ changed();
90
+ },
91
+
92
+ selectAll() {
93
+ selectAll(state, ids());
94
+ changed();
95
+ },
96
+
97
+ clear() {
98
+ clearSelection(state);
99
+ changed();
100
+ },
101
+
102
+ prune() {
103
+ if (prune(state, ids())) changed();
104
+ },
105
+
106
+ handleKeydown(e, opts = {}) {
107
+ const all = ids();
108
+ const action = selectionKey(e, inOrder(selected, all).length > 0);
109
+
110
+ if (!action) return false;
111
+
112
+ if (action.kind === 'extend') {
113
+ if (!opts.next) return false;
114
+
115
+ prune(state, all);
116
+
117
+ const to = opts.next($.get(head) ?? $.get(anchor), action.key);
118
+
119
+ if (to === null || to === undefined) {
120
+ e.preventDefault?.();
121
+
122
+ return true;
123
+ }
124
+
125
+ extendTo(state, all, to, action.additive);
126
+ } else if (action.kind === 'selectAll') selectAll(state, all); else clearSelection(state);
127
+
128
+ e.preventDefault?.();
129
+ changed();
130
+
131
+ return true;
132
+ }
133
+ };
134
+ }
@@ -7,11 +7,11 @@
7
7
  * Space play/pause, ←/→ seek, ↑/↓ volume, M mute, −/= speed, Backspace reset
8
8
  * speed, </> frame step, [/] chapter jumps, B bookmark, Shift+T transcript
9
9
  * (t is theater mode, YouTube's key — jatui-ee2ty), ?
10
- * shortcuts, Home/End. Deliberately NOT ported: F/M (favourite/file-under —
11
- * library-app verbs, not player verbs), `/` (transcript search — the pane
12
- * has none yet). N (night cycle) landed with the night module (mos-5lzns.33)
13
- * and `,` (settings) with the settings panel (mos-5lzns.35), each gated on
14
- * the host opting in.
10
+ * shortcuts, Home/End, and `/` transcript search (the pane has none yet).
11
+ * Deliberately NOT ported: F/M (favourite/file-under — library-app verbs,
12
+ * not player verbs). N (night cycle) landed with the night module
13
+ * (mos-5lzns.33) and `,` (settings) with the settings panel
14
+ * (mos-5lzns.35), each gated on the host opting in.
15
15
  */
16
16
  export interface PlayerKeyRow {
17
17
  /** Keycap label as shown in the help overlay — `/`-separated alternates. */
@@ -7,11 +7,11 @@
7
7
  * Space play/pause, ←/→ seek, ↑/↓ volume, M mute, −/= speed, Backspace reset
8
8
  * speed, </> frame step, [/] chapter jumps, B bookmark, Shift+T transcript
9
9
  * (t is theater mode, YouTube's key — jatui-ee2ty), ?
10
- * shortcuts, Home/End. Deliberately NOT ported: F/M (favourite/file-under —
11
- * library-app verbs, not player verbs), `/` (transcript search — the pane
12
- * has none yet). N (night cycle) landed with the night module (mos-5lzns.33)
13
- * and `,` (settings) with the settings panel (mos-5lzns.35), each gated on
14
- * the host opting in.
10
+ * shortcuts, Home/End, and `/` transcript search (the pane has none yet).
11
+ * Deliberately NOT ported: F/M (favourite/file-under — library-app verbs,
12
+ * not player verbs). N (night cycle) landed with the night module
13
+ * (mos-5lzns.33) and `,` (settings) with the settings panel
14
+ * (mos-5lzns.35), each gated on the host opting in.
15
15
  */
16
16
  /** Order follows the help overlay: transport, seek, volume, speed, structure. */
17
17
  export const PLAYER_KEYS = [
@@ -34,6 +34,10 @@ export const PLAYER_KEYS = [
34
34
  // t = theater, YouTube's key; the transcript moved to Shift+T (jatui-ee2ty).
35
35
  { keys: 't', description: 'Theater mode on / off', when: 'theater' },
36
36
  { keys: 'Shift+T', description: 'Show / hide the transcript', when: 'transcript' },
37
+ { keys: '/', description: 'Search transcript', when: 'transcript' },
38
+ // Shift+wheel over the transcript pane steps PLAYBACK line by line —
39
+ // a plain wheel there scrolls the pane natively and is never claimed.
40
+ { keys: 'Shift+wheel', description: 'Over the transcript: steps line by line', when: 'transcript' },
37
41
  { keys: 'N', description: 'Night dimming: auto / on / off', when: 'night' },
38
42
  { keys: 'Home / End', description: 'Jump to start / end' },
39
43
  { keys: ',', description: 'Settings', when: 'settings' },
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Transcript search + wheel line-scrub (jatui-xmtnv — extension-library
3
+ * parity, mos-telti). Pure, DOM-free helpers the component calls; no module
4
+ * here reads settings or touches the <video> element (the advisor doctrine,
5
+ * src/lib/video/README.md).
6
+ *
7
+ * Two features:
8
+ *
9
+ * 1. `/` opens a search box over the transcript pane. Matches highlight in
10
+ * place; Enter jumps to the next match (Shift+Enter the previous one,
11
+ * wrapping); Esc closes. The page omnibar's own `/` yields while focus is
12
+ * inside the player — the component stops the keydown's propagation when
13
+ * it claims the key.
14
+ * 2. The wheel over the transcript pane steps PLAYBACK one line at a time —
15
+ * not the pane's scroll. Deltas accumulate so one notch is one line: a
16
+ * mouse wheel's ~100px notches step exactly once each, and a trackpad's
17
+ * stream of small deltas never flies through the transcript.
18
+ */
19
+ /** Wheel delta that equals one line step. A mouse notch is ~100–120; a
20
+ * trackpad emits many small deltas — accumulating keeps one notch = one
21
+ * line without making trackpad scrub feel stuck. */
22
+ export declare const WHEEL_LINE_NOTCH = 50;
23
+ /** Escaping for the {@html} path: transcript text is host data, never markup. */
24
+ export declare function escapeHtml(text: string): string;
25
+ /**
26
+ * Case-insensitive, non-overlapping `[start, end)` ranges of every occurrence
27
+ * of `query` in `text`. An empty (or whitespace-only) query matches nothing.
28
+ */
29
+ export declare function searchHits(text: string, query: string): Array<{
30
+ start: number;
31
+ end: number;
32
+ }>;
33
+ /**
34
+ * HTML for one transcript row's text with `<mark>` around every match —
35
+ * safe to feed to `{@html}`: the surrounding text is escaped, only the mark
36
+ * tags are ours. `current` (the Enter-cursor's row) wears a second class so
37
+ * the player can style the match you're ON distinctly from the rest.
38
+ */
39
+ export declare function highlightRowHtml(text: string, query: string, current: boolean): string;
40
+ /**
41
+ * The segment to land on when the wheel steps playback one line. Wheel-down
42
+ * (direction 1) seeks to the NEXT line's start; wheel-up (−1) re-plays the
43
+ * line the playhead is already in when it is more than `grace` seconds past
44
+ * its start (one up-notch replays what you just heard), otherwise the
45
+ * previous line. Returns null at the transcript's edges.
46
+ */
47
+ export declare function lineScrubTarget(currentTime: number, starts: ArrayLike<number>, direction: 1 | -1, grace?: number): number | null;
48
+ /** What the transcript pane's wheel handler should do with one wheel event. */
49
+ export interface TranscriptWheelDecision {
50
+ /** True when the wheel is the shift verb — the handler must preventDefault. */
51
+ claimed: boolean;
52
+ /** True when the accumulated delta crossed a notch: step a line now. */
53
+ shouldStep: boolean;
54
+ /** Step direction: 1 = next line (wheel down), −1 = previous (wheel up). */
55
+ direction: 1 | -1;
56
+ /** The accumulator to carry to the next wheel event. */
57
+ nextAccum: number;
58
+ }
59
+ /**
60
+ * Decide one transcript-pane wheel event. A plain wheel (no shift) or a
61
+ * pane with no transcript is NEVER claimed — native scroll, accumulator
62
+ * untouched. A shift wheel accumulates deltas; crossing {@link WHEEL_LINE_NOTCH}
63
+ * spends the accumulator (back to 0) and steps one line in the sign's
64
+ * direction.
65
+ */
66
+ /**
67
+ * The effective wheel delta for a shift-modified gesture. Chrome on
68
+ * Linux/Windows and macOS turn Shift+wheel into a HORIZONTAL scroll:
69
+ * `e.deltaY` is 0 and the value sits in `e.deltaX`. Headless browsers don't
70
+ * swap, so tests only checking `deltaY` pass while a real browser never
71
+ * steps. Every shift verb must read this fallback.
72
+ */
73
+ export declare function effectiveWheelDelta(deltaY: number, deltaX: number): number;
74
+ export declare function transcriptWheelDecision(shiftKey: boolean, hasSegments: boolean, deltaY: number, accum: number): TranscriptWheelDecision;
75
+ /** Which match Enter/Shift+Enter lands on. With no cursor yet, the first
76
+ * match at/after the playhead (before it for backwards) — a search opened
77
+ * mid-talk starts where you are, not at the top. Afterwards, the cursor
78
+ * walks the match list and wraps at its ends. Returns the index, or −1 when
79
+ * there are no matches. */
80
+ export declare function pickMatch(matchStarts: ArrayLike<number>, currentTime: number, direction: 1 | -1, cursor: number): number;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Transcript search + wheel line-scrub (jatui-xmtnv — extension-library
3
+ * parity, mos-telti). Pure, DOM-free helpers the component calls; no module
4
+ * here reads settings or touches the <video> element (the advisor doctrine,
5
+ * src/lib/video/README.md).
6
+ *
7
+ * Two features:
8
+ *
9
+ * 1. `/` opens a search box over the transcript pane. Matches highlight in
10
+ * place; Enter jumps to the next match (Shift+Enter the previous one,
11
+ * wrapping); Esc closes. The page omnibar's own `/` yields while focus is
12
+ * inside the player — the component stops the keydown's propagation when
13
+ * it claims the key.
14
+ * 2. The wheel over the transcript pane steps PLAYBACK one line at a time —
15
+ * not the pane's scroll. Deltas accumulate so one notch is one line: a
16
+ * mouse wheel's ~100px notches step exactly once each, and a trackpad's
17
+ * stream of small deltas never flies through the transcript.
18
+ */
19
+ /** Wheel delta that equals one line step. A mouse notch is ~100–120; a
20
+ * trackpad emits many small deltas — accumulating keeps one notch = one
21
+ * line without making trackpad scrub feel stuck. */
22
+ export const WHEEL_LINE_NOTCH = 50;
23
+ /** Escaping for the {@html} path: transcript text is host data, never markup. */
24
+ export function escapeHtml(text) {
25
+ return text
26
+ .replaceAll('&', '&amp;')
27
+ .replaceAll('<', '&lt;')
28
+ .replaceAll('>', '&gt;')
29
+ .replaceAll('"', '&quot;')
30
+ .replaceAll("'", '&#39;');
31
+ }
32
+ /**
33
+ * Case-insensitive, non-overlapping `[start, end)` ranges of every occurrence
34
+ * of `query` in `text`. An empty (or whitespace-only) query matches nothing.
35
+ */
36
+ export function searchHits(text, query) {
37
+ const q = query.trim().toLowerCase();
38
+ if (!q)
39
+ return [];
40
+ const lower = text.toLowerCase();
41
+ const hits = [];
42
+ let from = 0;
43
+ for (;;) {
44
+ const at = lower.indexOf(q, from);
45
+ if (at < 0)
46
+ break;
47
+ hits.push({ start: at, end: at + q.length });
48
+ from = at + q.length;
49
+ }
50
+ return hits;
51
+ }
52
+ /**
53
+ * HTML for one transcript row's text with `<mark>` around every match —
54
+ * safe to feed to `{@html}`: the surrounding text is escaped, only the mark
55
+ * tags are ours. `current` (the Enter-cursor's row) wears a second class so
56
+ * the player can style the match you're ON distinctly from the rest.
57
+ */
58
+ export function highlightRowHtml(text, query, current) {
59
+ const hits = searchHits(text, query);
60
+ if (hits.length === 0)
61
+ return escapeHtml(text);
62
+ const cls = current ? 'vp-search-mark current' : 'vp-search-mark';
63
+ let out = '';
64
+ let at = 0;
65
+ for (const hit of hits) {
66
+ out += escapeHtml(text.slice(at, hit.start));
67
+ out += `<mark class="${cls}">${escapeHtml(text.slice(hit.start, hit.end))}</mark>`;
68
+ at = hit.end;
69
+ }
70
+ out += escapeHtml(text.slice(at));
71
+ return out;
72
+ }
73
+ /**
74
+ * The segment to land on when the wheel steps playback one line. Wheel-down
75
+ * (direction 1) seeks to the NEXT line's start; wheel-up (−1) re-plays the
76
+ * line the playhead is already in when it is more than `grace` seconds past
77
+ * its start (one up-notch replays what you just heard), otherwise the
78
+ * previous line. Returns null at the transcript's edges.
79
+ */
80
+ export function lineScrubTarget(currentTime, starts, direction, grace = 0.5) {
81
+ const n = starts.length;
82
+ if (n === 0)
83
+ return null;
84
+ // Index of the line the playhead is in (the last start ≤ time).
85
+ let idx = 0;
86
+ for (let i = 0; i < n; i++) {
87
+ if (starts[i] <= currentTime)
88
+ idx = i;
89
+ else
90
+ break;
91
+ }
92
+ if (direction === 1) {
93
+ return idx + 1 < n ? starts[idx + 1] : null;
94
+ }
95
+ if (currentTime - starts[idx] > grace)
96
+ return starts[idx];
97
+ return idx > 0 ? starts[idx - 1] : null;
98
+ }
99
+ /**
100
+ * Decide one transcript-pane wheel event. A plain wheel (no shift) or a
101
+ * pane with no transcript is NEVER claimed — native scroll, accumulator
102
+ * untouched. A shift wheel accumulates deltas; crossing {@link WHEEL_LINE_NOTCH}
103
+ * spends the accumulator (back to 0) and steps one line in the sign's
104
+ * direction.
105
+ */
106
+ // ── transcriptWheelDecision ───────────────────────────────────────────────
107
+ /**
108
+ * The effective wheel delta for a shift-modified gesture. Chrome on
109
+ * Linux/Windows and macOS turn Shift+wheel into a HORIZONTAL scroll:
110
+ * `e.deltaY` is 0 and the value sits in `e.deltaX`. Headless browsers don't
111
+ * swap, so tests only checking `deltaY` pass while a real browser never
112
+ * steps. Every shift verb must read this fallback.
113
+ */
114
+ export function effectiveWheelDelta(deltaY, deltaX) {
115
+ return deltaY !== 0 ? deltaY : deltaX;
116
+ }
117
+ export function transcriptWheelDecision(shiftKey, hasSegments, deltaY, accum) {
118
+ if (!shiftKey || !hasSegments) {
119
+ return { claimed: false, shouldStep: false, direction: 1, nextAccum: accum };
120
+ }
121
+ const next = accum + deltaY;
122
+ if (Math.abs(next) < WHEEL_LINE_NOTCH) {
123
+ return { claimed: true, shouldStep: false, direction: next > 0 ? 1 : -1, nextAccum: next };
124
+ }
125
+ return { claimed: true, shouldStep: true, direction: next > 0 ? 1 : -1, nextAccum: 0 };
126
+ }
127
+ /** Which match Enter/Shift+Enter lands on. With no cursor yet, the first
128
+ * match at/after the playhead (before it for backwards) — a search opened
129
+ * mid-talk starts where you are, not at the top. Afterwards, the cursor
130
+ * walks the match list and wraps at its ends. Returns the index, or −1 when
131
+ * there are no matches. */
132
+ export function pickMatch(matchStarts, currentTime, direction, cursor) {
133
+ const n = matchStarts.length;
134
+ if (n === 0)
135
+ return -1;
136
+ if (cursor < 0 || cursor >= n) {
137
+ if (direction === 1) {
138
+ for (let i = 0; i < n; i++)
139
+ if (matchStarts[i] >= currentTime)
140
+ return i;
141
+ return 0;
142
+ }
143
+ let idx = 0;
144
+ for (let i = 0; i < n; i++) {
145
+ if (matchStarts[i] <= currentTime)
146
+ idx = i;
147
+ else
148
+ break;
149
+ }
150
+ return idx;
151
+ }
152
+ return (cursor + direction + n) % n;
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.38.5",
3
+ "version": "0.39.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -84,7 +84,8 @@
84
84
  "dist",
85
85
  "CLAUDE.md",
86
86
  "README.md",
87
- "LICENSE"
87
+ "LICENSE",
88
+ "THIRD_PARTY_NOTICES.md"
88
89
  ],
89
90
  "scripts": {
90
91
  "dev": "npm run package && vite dev --port 5180",