@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.
- package/CLAUDE.md +32 -1
- package/THIRD_PARTY_NOTICES.md +51 -0
- package/dist/components/BulkActionBar.svelte +32 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -1
- package/dist/components/FloatingActionBar.svelte +102 -34
- package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
- package/dist/components/MediaExplorer.svelte +249 -0
- package/dist/components/MediaExplorer.svelte.d.ts +37 -0
- package/dist/components/NotificationCenter.svelte +481 -0
- package/dist/components/NotificationCenter.svelte.d.ts +29 -0
- package/dist/components/SearchDropdown.svelte +54 -8
- package/dist/components/SearchDropdown.svelte.d.ts +12 -0
- package/dist/components/VideoPlayer.svelte +269 -3
- package/dist/components/agent/AgentLog.svelte +217 -0
- package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
- package/dist/components/agent/AgentLogRow.svelte +281 -0
- package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
- package/dist/components/agent/AgentTaskList.svelte +607 -0
- package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
- package/dist/components/agent/AgentThinking.svelte +280 -0
- package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
- package/dist/components/agent/ComposerGlow.svelte +265 -0
- package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
- package/dist/components/agent/ContextMeter.svelte +277 -0
- package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
- package/dist/components/agent/Questionnaire.svelte +767 -0
- package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
- package/dist/components/agent/ShimmerText.svelte +100 -0
- package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
- package/dist/components/agent/StreamedText.svelte +135 -0
- package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
- package/dist/components/agent/agentLog.d.ts +22 -0
- package/dist/components/agent/agentLog.js +26 -0
- package/dist/components/agent/agentTaskList.d.ts +90 -0
- package/dist/components/agent/agentTaskList.js +93 -0
- package/dist/components/agent/agentThinking.d.ts +46 -0
- package/dist/components/agent/agentThinking.js +78 -0
- package/dist/components/agent/contextMeter.d.ts +97 -0
- package/dist/components/agent/contextMeter.js +156 -0
- package/dist/components/agent/questionnaire.d.ts +104 -0
- package/dist/components/agent/questionnaire.js +225 -0
- package/dist/components/agent/revealCore.d.ts +74 -0
- package/dist/components/agent/revealCore.js +160 -0
- package/dist/components/agent/revealTicker.d.ts +34 -0
- package/dist/components/agent/revealTicker.js +73 -0
- package/dist/components/agent/streamedText.d.ts +28 -0
- package/dist/components/agent/streamedText.js +59 -0
- package/dist/components/messaging/MessageInput.svelte +34 -4
- package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
- package/dist/components/messaging/MessageItem.svelte +2 -2
- package/dist/components/notificationCenter.d.ts +65 -0
- package/dist/components/notificationCenter.js +110 -0
- package/dist/components/progress/Steps.svelte +269 -5
- package/dist/components/progress/Steps.svelte.d.ts +16 -1
- package/dist/components/progress/stepsSummary.d.ts +14 -0
- package/dist/components/progress/stepsSummary.js +32 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +29 -0
- package/dist/selection/clickSelection.d.ts +89 -0
- package/dist/selection/clickSelection.js +152 -0
- package/dist/selection/createClickSelection.d.ts +79 -0
- package/dist/selection/createClickSelection.js +134 -0
- package/dist/video/keys.d.ts +5 -5
- package/dist/video/keys.js +9 -5
- package/dist/video/transcriptSearch.d.ts +80 -0
- package/dist/video/transcriptSearch.js +153 -0
- 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
|
+
}
|
package/dist/video/keys.d.ts
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* and `,` (settings) with the settings panel
|
|
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. */
|
package/dist/video/keys.js
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* and `,` (settings) with the settings panel
|
|
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('&', '&')
|
|
27
|
+
.replaceAll('<', '<')
|
|
28
|
+
.replaceAll('>', '>')
|
|
29
|
+
.replaceAll('"', '"')
|
|
30
|
+
.replaceAll("'", ''');
|
|
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.
|
|
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",
|