@dorsk/tsumikit 0.72.0 → 0.73.1

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 (53) hide show
  1. package/README.md +25 -0
  2. package/dist/components/atoms/Button.svelte +12 -1
  3. package/dist/components/atoms/Checkbox.svelte +12 -0
  4. package/dist/components/atoms/Icon.svelte +5 -0
  5. package/dist/components/atoms/Icon.svelte.d.ts +5 -0
  6. package/dist/components/atoms/TimeDisplay.svelte +98 -0
  7. package/dist/components/atoms/TimeDisplay.svelte.d.ts +24 -0
  8. package/dist/components/layouts/AppShell.svelte +3 -2
  9. package/dist/components/layouts/ResizablePanel.svelte +5 -4
  10. package/dist/components/layouts/TabBar.svelte +3 -1
  11. package/dist/components/molecules/Accordion.svelte +2 -1
  12. package/dist/components/molecules/AttachmentList.svelte +1 -0
  13. package/dist/components/molecules/Breadcrumb.svelte +7 -6
  14. package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -1
  15. package/dist/components/molecules/CapBar.svelte +3 -1
  16. package/dist/components/molecules/Composer.svelte +27 -5
  17. package/dist/components/molecules/Drawer.svelte +10 -1
  18. package/dist/components/molecules/Dropzone.svelte +2 -1
  19. package/dist/components/molecules/FilterInput.svelte +10 -1
  20. package/dist/components/molecules/Metric.svelte +28 -3
  21. package/dist/components/molecules/Modal.svelte +20 -4
  22. package/dist/components/molecules/Popover.svelte +7 -3
  23. package/dist/components/molecules/SectionHeader.svelte +29 -14
  24. package/dist/components/molecules/SeekSlider.svelte +370 -0
  25. package/dist/components/molecules/SeekSlider.svelte.d.ts +37 -0
  26. package/dist/components/molecules/SegmentedControl.svelte +54 -6
  27. package/dist/components/molecules/SelectButton.svelte +33 -22
  28. package/dist/components/molecules/SwipeRow.svelte +6 -2
  29. package/dist/components/molecules/SwipeRow.svelte.d.ts +4 -0
  30. package/dist/components/molecules/Tabs.svelte +11 -5
  31. package/dist/components/molecules/Timestamp.svelte +1 -1
  32. package/dist/components/molecules/Toaster.svelte +70 -13
  33. package/dist/components/molecules/Tooltip.svelte +19 -10
  34. package/dist/components/molecules/VolumeSlider.svelte +367 -0
  35. package/dist/components/molecules/VolumeSlider.svelte.d.ts +30 -0
  36. package/dist/components/molecules/combobox-keyboard.js +2 -1
  37. package/dist/components/organisms/DataTable.svelte +12 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +4 -0
  40. package/dist/media-time.d.ts +6 -0
  41. package/dist/media-time.js +25 -0
  42. package/dist/query/edit.js +1 -16
  43. package/dist/query/parser.js +68 -28
  44. package/dist/query/query.js +9 -5
  45. package/dist/query/quotes.d.ts +24 -0
  46. package/dist/query/quotes.js +78 -0
  47. package/dist/query/single.js +1 -3
  48. package/dist/query/suggest.js +25 -32
  49. package/dist/stores/fontscale.svelte.js +3 -2
  50. package/dist/stores/storage.d.ts +2 -0
  51. package/dist/stores/storage.js +16 -0
  52. package/dist/stores/theme.svelte.js +4 -7
  53. package/package.json +1 -1
@@ -0,0 +1,367 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Compact volume control: a mute toggle whose glyph tracks the level, and a
4
+ * short slider. `oninput` fires on every change, `onchange` once per
5
+ * release or keyboard/wheel step. The slider hides on touch devices by
6
+ * default (`hideOnTouch`), where the OS owns volume; the mute button stays.
7
+ */
8
+ import type { HTMLAttributes } from 'svelte/elements';
9
+ import type { IconName } from '../atoms/Icon.svelte';
10
+ import IconButton from './IconButton.svelte';
11
+
12
+ type Own = {
13
+ /** Level 0–1. */
14
+ value?: number;
15
+ muted?: boolean;
16
+ orientation?: 'horizontal' | 'vertical';
17
+ size?: 'sm' | 'md';
18
+ /** Level per arrow key or wheel notch. */
19
+ step?: number;
20
+ disabled?: boolean;
21
+ hideOnTouch?: boolean;
22
+ label?: string;
23
+ muteLabel?: string;
24
+ unmuteLabel?: string;
25
+ oninput?: (value: number) => void;
26
+ onchange?: (value: number) => void;
27
+ onmutechange?: (muted: boolean) => void;
28
+ class?: string;
29
+ style?: string;
30
+ };
31
+
32
+ let {
33
+ value = $bindable(1),
34
+ muted = $bindable(false),
35
+ orientation = 'horizontal',
36
+ size = 'sm',
37
+ step = 0.05,
38
+ disabled = false,
39
+ hideOnTouch = true,
40
+ label = 'Volume',
41
+ muteLabel = 'Mute',
42
+ unmuteLabel = 'Unmute',
43
+ oninput,
44
+ onchange,
45
+ onmutechange,
46
+ class: klass = '',
47
+ style: styleProp,
48
+ ...rest
49
+ }: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
50
+
51
+ let dragging = $state(false);
52
+ let pointerId: number | null = null;
53
+
54
+ const clamp = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4;
55
+ const level = $derived(muted ? 0 : clamp(value));
56
+ const pct = $derived(Math.round(level * 100));
57
+ const vertical = $derived(orientation === 'vertical');
58
+ const icon = $derived<IconName>(
59
+ muted ? 'volume-mute' : level === 0 ? 'volume' : level < 0.5 ? 'volume-low' : 'volume-high'
60
+ );
61
+ const valueText = $derived(muted ? 'Muted' : `${pct}%`);
62
+
63
+ function setMuted(m: boolean) {
64
+ if (m === muted) return;
65
+ muted = m;
66
+ onmutechange?.(m);
67
+ }
68
+
69
+ function apply(v: number) {
70
+ const next = clamp(v);
71
+ value = next;
72
+ if (next > 0) setMuted(false);
73
+ oninput?.(next);
74
+ return next;
75
+ }
76
+
77
+ function commit(v: number) {
78
+ onchange?.(apply(v));
79
+ }
80
+
81
+ function fracAt(el: HTMLElement, e: PointerEvent) {
82
+ const r = el.getBoundingClientRect();
83
+ let f: number;
84
+ if (vertical) {
85
+ f = r.height ? 1 - (e.clientY - r.top) / r.height : 0;
86
+ } else {
87
+ f = r.width ? (e.clientX - r.left) / r.width : 0;
88
+ if (getComputedStyle(el).direction === 'rtl') f = 1 - f;
89
+ }
90
+ return clamp(f);
91
+ }
92
+
93
+ function release(el: HTMLElement) {
94
+ if (pointerId !== null && typeof el.releasePointerCapture === 'function') {
95
+ try {
96
+ el.releasePointerCapture(pointerId);
97
+ } catch {}
98
+ }
99
+ pointerId = null;
100
+ dragging = false;
101
+ }
102
+
103
+ function onpointerdown(e: PointerEvent) {
104
+ if (disabled || e.button !== 0 || pointerId !== null) return;
105
+ const el = e.currentTarget as HTMLElement;
106
+ e.preventDefault();
107
+ pointerId = e.pointerId;
108
+ dragging = true;
109
+ if (typeof el.setPointerCapture === 'function') {
110
+ try {
111
+ el.setPointerCapture(e.pointerId);
112
+ } catch {}
113
+ }
114
+ el.focus?.({ preventScroll: true });
115
+ apply(fracAt(el, e));
116
+ }
117
+
118
+ function onpointermove(e: PointerEvent) {
119
+ if (pointerId !== e.pointerId) return;
120
+ apply(fracAt(e.currentTarget as HTMLElement, e));
121
+ }
122
+
123
+ function onpointerup(e: PointerEvent) {
124
+ if (pointerId !== e.pointerId) return;
125
+ release(e.currentTarget as HTMLElement);
126
+ onchange?.(value);
127
+ }
128
+
129
+ function onpointercancel(e: PointerEvent) {
130
+ if (pointerId !== e.pointerId) return;
131
+ release(e.currentTarget as HTMLElement);
132
+ onchange?.(value);
133
+ }
134
+
135
+ function onlostpointercapture(e: PointerEvent) {
136
+ if (pointerId === null || pointerId !== e.pointerId) return;
137
+ pointerId = null;
138
+ dragging = false;
139
+ onchange?.(value);
140
+ }
141
+
142
+ function onkeydown(e: KeyboardEvent) {
143
+ if (disabled) return;
144
+ const base = level;
145
+ let next: number | null = null;
146
+ switch (e.key) {
147
+ case 'ArrowRight':
148
+ case 'ArrowUp':
149
+ next = base + step;
150
+ break;
151
+ case 'ArrowLeft':
152
+ case 'ArrowDown':
153
+ next = base - step;
154
+ break;
155
+ case 'PageUp':
156
+ next = base + step * 2;
157
+ break;
158
+ case 'PageDown':
159
+ next = base - step * 2;
160
+ break;
161
+ case 'Home':
162
+ next = 0;
163
+ break;
164
+ case 'End':
165
+ next = 1;
166
+ break;
167
+ }
168
+ if (next === null) return;
169
+ e.preventDefault();
170
+ commit(next);
171
+ }
172
+
173
+ function wheel(el: HTMLElement) {
174
+ const onwheel = (e: WheelEvent) => {
175
+ if (disabled) return;
176
+ const d = e.deltaY || e.deltaX;
177
+ if (!d) return;
178
+ e.preventDefault();
179
+ commit(level + (d < 0 ? step : -step));
180
+ };
181
+ el.addEventListener('wheel', onwheel, { passive: false });
182
+ return () => el.removeEventListener('wheel', onwheel);
183
+ }
184
+ </script>
185
+
186
+ <div
187
+ data-tsu="VolumeSlider"
188
+ class="vol vol--{size} vol--{orientation} {klass}"
189
+ class:vol--hide-touch={hideOnTouch}
190
+ class:vol--dragging={dragging}
191
+ class:vol--muted={muted}
192
+ style="--pct: {pct};{styleProp ?? ''}"
193
+ {...rest}
194
+ >
195
+ <IconButton
196
+ {icon}
197
+ label={muted ? unmuteLabel : muteLabel}
198
+ pressed={muted}
199
+ box="sm"
200
+ size={size === 'sm' ? 16 : 18}
201
+ {disabled}
202
+ onclick={() => setMuted(!muted)}
203
+ data-part="mute"
204
+ />
205
+ <div
206
+ class="vol-slab"
207
+ data-part="slab"
208
+ role="slider"
209
+ tabindex={disabled ? -1 : 0}
210
+ aria-label={label}
211
+ aria-orientation={orientation}
212
+ aria-valuemin={0}
213
+ aria-valuemax={100}
214
+ aria-valuenow={pct}
215
+ aria-valuetext={valueText}
216
+ aria-disabled={disabled ? 'true' : undefined}
217
+ {onpointerdown}
218
+ {onpointermove}
219
+ {onpointerup}
220
+ {onpointercancel}
221
+ {onlostpointercapture}
222
+ {onkeydown}
223
+ {@attach wheel}
224
+ >
225
+ <span class="vol-track" data-part="track">
226
+ <i class="vol-fill" data-part="fill"></i>
227
+ </span>
228
+ <span class="vol-anchor">
229
+ <i class="vol-thumb" data-part="thumb"></i>
230
+ </span>
231
+ </div>
232
+ </div>
233
+
234
+ <style>
235
+ .vol {
236
+ --vol-track: 3px;
237
+ --vol-thumb: 10px;
238
+ --vol-length: 5rem;
239
+ --vol-accent: var(--slider-accent, var(--accent));
240
+ display: inline-flex;
241
+ align-items: center;
242
+ gap: var(--sp-1);
243
+ vertical-align: middle;
244
+ }
245
+ .vol--md {
246
+ --vol-track: 4px;
247
+ --vol-thumb: 12px;
248
+ --vol-length: 7rem;
249
+ }
250
+ .vol--vertical {
251
+ flex-direction: column-reverse;
252
+ }
253
+ .vol-slab {
254
+ position: relative;
255
+ flex: none;
256
+ width: var(--vol-length);
257
+ min-height: 1.25rem;
258
+ cursor: pointer;
259
+ touch-action: none;
260
+ user-select: none;
261
+ -webkit-user-select: none;
262
+ outline: none;
263
+ border-radius: var(--r-sm);
264
+ }
265
+ .vol--vertical .vol-slab {
266
+ width: 1.25rem;
267
+ height: var(--vol-length);
268
+ min-height: 0;
269
+ }
270
+ @media (pointer: coarse) {
271
+ .vol-slab {
272
+ min-height: var(--touch-target-compact);
273
+ }
274
+ .vol--vertical .vol-slab {
275
+ min-width: var(--touch-target-compact);
276
+ }
277
+ }
278
+ @media (hover: none) and (pointer: coarse) {
279
+ .vol--hide-touch .vol-slab {
280
+ display: none;
281
+ }
282
+ }
283
+ .vol-slab[aria-disabled='true'] {
284
+ cursor: default;
285
+ opacity: 0.45;
286
+ }
287
+ .vol-slab:focus-visible {
288
+ outline: var(--focus-ring);
289
+ outline-offset: var(--focus-ring-offset);
290
+ }
291
+ .vol-track {
292
+ position: absolute;
293
+ inset-inline: 0;
294
+ top: 50%;
295
+ height: var(--vol-track);
296
+ margin-top: calc(var(--vol-track) / -2);
297
+ border-radius: var(--r-pill);
298
+ background: var(--bg-elevated-2);
299
+ overflow: hidden;
300
+ }
301
+ .vol--vertical .vol-track {
302
+ inset-inline: auto;
303
+ inset-block: 0;
304
+ left: 50%;
305
+ top: auto;
306
+ width: var(--vol-track);
307
+ height: auto;
308
+ margin-top: 0;
309
+ margin-left: calc(var(--vol-track) / -2);
310
+ }
311
+ .vol-fill {
312
+ position: absolute;
313
+ inset-block: 0;
314
+ inset-inline-start: 0;
315
+ width: calc(var(--pct) * 1%);
316
+ background: var(--vol-accent);
317
+ }
318
+ .vol--vertical .vol-fill {
319
+ inset-inline: 0;
320
+ inset-block: auto 0;
321
+ width: auto;
322
+ height: calc(var(--pct) * 1%);
323
+ }
324
+ .vol-anchor {
325
+ position: absolute;
326
+ top: 50%;
327
+ inset-inline-start: calc(var(--pct) * 1%);
328
+ width: 0;
329
+ height: 0;
330
+ pointer-events: none;
331
+ }
332
+ .vol--vertical .vol-anchor {
333
+ top: auto;
334
+ bottom: calc(var(--pct) * 1%);
335
+ inset-inline-start: 50%;
336
+ }
337
+ .vol-thumb {
338
+ position: absolute;
339
+ left: 0;
340
+ top: 0;
341
+ width: var(--vol-thumb);
342
+ height: var(--vol-thumb);
343
+ border-radius: 50%;
344
+ background: var(--vol-accent);
345
+ border: 2px solid var(--bg);
346
+ box-shadow: var(--shadow-sm);
347
+ transform: translate(-50%, -50%) scale(0.6);
348
+ opacity: 0;
349
+ transition:
350
+ opacity 120ms var(--ease),
351
+ transform 120ms var(--ease);
352
+ }
353
+ .vol-slab:hover .vol-thumb,
354
+ .vol-slab:focus-visible .vol-thumb,
355
+ .vol--dragging .vol-thumb {
356
+ opacity: 1;
357
+ transform: translate(-50%, -50%) scale(1);
358
+ }
359
+ .vol--muted .vol-fill {
360
+ background: var(--text-faint);
361
+ }
362
+ @media (prefers-reduced-motion: reduce) {
363
+ .vol-thumb {
364
+ transition: none;
365
+ }
366
+ }
367
+ </style>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Compact volume control: a mute toggle whose glyph tracks the level, and a
3
+ * short slider. `oninput` fires on every change, `onchange` once per
4
+ * release or keyboard/wheel step. The slider hides on touch devices by
5
+ * default (`hideOnTouch`), where the OS owns volume; the mute button stays.
6
+ */
7
+ import type { HTMLAttributes } from 'svelte/elements';
8
+ type Own = {
9
+ /** Level 0–1. */
10
+ value?: number;
11
+ muted?: boolean;
12
+ orientation?: 'horizontal' | 'vertical';
13
+ size?: 'sm' | 'md';
14
+ /** Level per arrow key or wheel notch. */
15
+ step?: number;
16
+ disabled?: boolean;
17
+ hideOnTouch?: boolean;
18
+ label?: string;
19
+ muteLabel?: string;
20
+ unmuteLabel?: string;
21
+ oninput?: (value: number) => void;
22
+ onchange?: (value: number) => void;
23
+ onmutechange?: (muted: boolean) => void;
24
+ class?: string;
25
+ style?: string;
26
+ };
27
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
28
+ declare const VolumeSlider: import("svelte").Component<$$ComponentProps, {}, "muted" | "value">;
29
+ type VolumeSlider = ReturnType<typeof VolumeSlider>;
30
+ export default VolumeSlider;
@@ -157,8 +157,9 @@ export function caretRect(el) {
157
157
  }
158
158
  mirror.textContent = el.value.slice(0, caret);
159
159
  const marker = doc.createElement('span');
160
- marker.textContent = el.value.slice(caret) || '.';
160
+ marker.textContent = el.value[caret] ?? '.';
161
161
  mirror.appendChild(marker);
162
+ mirror.appendChild(doc.createTextNode(el.value.slice(caret + 1)));
162
163
  doc.body.appendChild(mirror);
163
164
  const m = mirror.getBoundingClientRect();
164
165
  const s = marker.getBoundingClientRect();
@@ -503,6 +503,18 @@
503
503
  .dt-actions {
504
504
  opacity: 1;
505
505
  }
506
+ /* Sort slab: inline-end stays at 0 so it never reaches the next header
507
+ cell, and the inline-start reach stays inside the cell's own padding. */
508
+ .dt-sort {
509
+ position: relative;
510
+ }
511
+ .dt-sort::before {
512
+ content: '';
513
+ position: absolute;
514
+ inset-block: var(--touch-slab);
515
+ inset-inline-start: var(--touch-slab);
516
+ inset-inline-end: 0;
517
+ }
506
518
  }
507
519
  @container (max-width: 30rem) {
508
520
  [data-hide-below='sm'] {
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export { default as Swatch } from './components/atoms/Swatch.svelte';
30
30
  export { default as Switch } from './components/atoms/Switch.svelte';
31
31
  export { default as Text } from './components/atoms/Text.svelte';
32
32
  export { default as Textarea } from './components/atoms/Textarea.svelte';
33
+ export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
33
34
  export { default as AppShell } from './components/layouts/AppShell.svelte';
34
35
  export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
35
36
  export { default as Cluster } from './components/layouts/Cluster.svelte';
@@ -84,6 +85,7 @@ export { default as PullToRefresh } from './components/molecules/PullToRefresh.s
84
85
  export { default as RadialMenu, type RadialArc, } from './components/molecules/RadialMenu.svelte';
85
86
  export { default as RadioGroup, type RadioOption, } from './components/molecules/RadioGroup.svelte';
86
87
  export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
88
+ export { default as SeekSlider } from './components/molecules/SeekSlider.svelte';
87
89
  export { default as SegmentedControl, type SegmentOption, } from './components/molecules/SegmentedControl.svelte';
88
90
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
89
91
  export { default as SortableList, SORTABLE_LIST_MESSAGES, type SortableListMessages, type SortableListMove, type SortableListRowContext, } from './components/molecules/SortableList.svelte';
@@ -97,6 +99,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
97
99
  export { default as Toggle } from './components/molecules/Toggle.svelte';
98
100
  export { default as Tooltip } from './components/molecules/Tooltip.svelte';
99
101
  export { default as Truncate } from './components/molecules/Truncate.svelte';
102
+ export { default as VolumeSlider } from './components/molecules/VolumeSlider.svelte';
100
103
  export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
101
104
  export { default as ConversationFeed } from './components/organisms/ConversationFeed.svelte';
102
105
  export { default as ConversationPanel } from './components/organisms/ConversationPanel.svelte';
@@ -106,6 +109,7 @@ export { formatCount, hasCount } from './count';
106
109
  export { EMOJI_GROUPS, type EmojiEntry, type EmojiGroup, searchEmoji } from './emoji';
107
110
  export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
108
111
  export { escapeHtml, type Highlight, type RenderMarkdownOptions, renderMarkdown, stripAnsi, } from './markdown';
112
+ export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
109
113
  export * as filterQuery from './query';
110
114
  export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, singleQuery, suggest, suggestSingle, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
111
115
  export { type OptionSection, sectionOptions } from './select-options';
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ export { default as Switch } from './components/atoms/Switch.svelte';
34
34
  // ---- atoms ----
35
35
  export { default as Text } from './components/atoms/Text.svelte';
36
36
  export { default as Textarea } from './components/atoms/Textarea.svelte';
37
+ export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
37
38
  // ---- layouts ----
38
39
  export { default as AppShell } from './components/layouts/AppShell.svelte';
39
40
  export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
@@ -92,6 +93,7 @@ export { default as PullToRefresh } from './components/molecules/PullToRefresh.s
92
93
  export { default as RadialMenu, } from './components/molecules/RadialMenu.svelte';
93
94
  export { default as RadioGroup, } from './components/molecules/RadioGroup.svelte';
94
95
  export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
96
+ export { default as SeekSlider } from './components/molecules/SeekSlider.svelte';
95
97
  export { default as SegmentedControl, } from './components/molecules/SegmentedControl.svelte';
96
98
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
97
99
  export { default as SortableList, SORTABLE_LIST_MESSAGES, } from './components/molecules/SortableList.svelte';
@@ -105,6 +107,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
105
107
  export { default as Toggle } from './components/molecules/Toggle.svelte';
106
108
  export { default as Tooltip } from './components/molecules/Tooltip.svelte';
107
109
  export { default as Truncate } from './components/molecules/Truncate.svelte';
110
+ export { default as VolumeSlider } from './components/molecules/VolumeSlider.svelte';
108
111
  export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
109
112
  // ---- organisms ----
110
113
  export { default as ConversationFeed } from './components/organisms/ConversationFeed.svelte';
@@ -115,6 +118,7 @@ export { formatCount, hasCount } from './count';
115
118
  export { EMOJI_GROUPS, searchEmoji } from './emoji';
116
119
  export { FIELD_KEY, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
117
120
  export { escapeHtml, renderMarkdown, stripAnsi, } from './markdown';
121
+ export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
118
122
  // ---- query core (headless: schema / parser / AST / suggest / compilers) ----
119
123
  export * as filterQuery from './query';
120
124
  export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
@@ -0,0 +1,6 @@
1
+ /** Seconds as `m:ss`, or `h:mm:ss` once the value (or `ref`) reaches an hour. */
2
+ export declare function formatMediaTime(seconds: number, ref?: number): string;
3
+ /** True when a duration cannot be scrubbed: unknown, live, or empty. */
4
+ export declare function isLiveDuration(duration: number | null | undefined): boolean;
5
+ /** Spoken position: "1:02 of 23:45", or just the position when live. */
6
+ export declare function describeMediaTime(value: number, duration: number | null | undefined, format?: (s: number, ref?: number) => string): string;
@@ -0,0 +1,25 @@
1
+ /** Seconds as `m:ss`, or `h:mm:ss` once the value (or `ref`) reaches an hour. */
2
+ export function formatMediaTime(seconds, ref = seconds) {
3
+ if (!Number.isFinite(seconds))
4
+ return '--:--';
5
+ const neg = seconds < 0;
6
+ const total = Math.floor(Math.abs(seconds));
7
+ const h = Math.floor(total / 3600);
8
+ const m = Math.floor((total % 3600) / 60);
9
+ const s = total % 60;
10
+ const pad = (n) => String(n).padStart(2, '0');
11
+ const hours = h > 0 || (Number.isFinite(ref) && Math.abs(ref) >= 3600);
12
+ const body = hours ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
13
+ return neg ? `-${body}` : body;
14
+ }
15
+ /** True when a duration cannot be scrubbed: unknown, live, or empty. */
16
+ export function isLiveDuration(duration) {
17
+ return duration == null || !Number.isFinite(duration) || duration <= 0;
18
+ }
19
+ /** Spoken position: "1:02 of 23:45", or just the position when live. */
20
+ export function describeMediaTime(value, duration, format = formatMediaTime) {
21
+ if (isLiveDuration(duration))
22
+ return format(value);
23
+ const d = duration;
24
+ return `${format(value, d)} of ${format(d, d)}`;
25
+ }
@@ -8,6 +8,7 @@
8
8
  // step opens, so spaces land inside the quotes — the already-supported phrase
9
9
  // form — without any grammar change. UI-free and framework-agnostic.
10
10
  // ─────────────────────────────────────────────────────────────────────────
11
+ import { openQuoteAt } from './quotes';
11
12
  import { findField } from './schema';
12
13
  // A field name immediately followed by a string operator (`:` contains / `!:`
13
14
  // not-contains), anchored at the caret.
@@ -32,22 +33,6 @@ export function autoQuoteEdit(schema, value, pos) {
32
33
  return null;
33
34
  return { value: `${value.slice(0, pos)}""${value.slice(pos)}`, pos: pos + 1 };
34
35
  }
35
- // Scan from the start tracking quote state (spaces inside quotes are NOT
36
- // boundaries), so the caret's quote context is correct even for `"star wars"`.
37
- function openQuoteAt(value, pos) {
38
- let quote = null;
39
- for (let i = 0; i < pos; i++) {
40
- const c = value[i];
41
- if (quote) {
42
- if (c === quote)
43
- quote = null;
44
- }
45
- else if (isQuote(c)) {
46
- quote = c;
47
- }
48
- }
49
- return quote;
50
- }
51
36
  /** True when the caret sits inside an open quote. */
52
37
  export function insideQuoteAtCaret(value, pos) {
53
38
  return openQuoteAt(value, pos) !== null;