@dorsk/tsumikit 0.71.0 → 0.73.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 (84) hide show
  1. package/README.md +54 -5
  2. package/dist/autoresize.js +13 -0
  3. package/dist/clipboard.js +12 -0
  4. package/dist/components/atoms/Artwork.svelte +4 -2
  5. package/dist/components/atoms/Avatar.svelte +4 -3
  6. package/dist/components/atoms/Badge.svelte +13 -0
  7. package/dist/components/atoms/Button.svelte +25 -9
  8. package/dist/components/atoms/Card.svelte +2 -0
  9. package/dist/components/atoms/Dot.svelte +1 -1
  10. package/dist/components/atoms/Icon.svelte +7 -0
  11. package/dist/components/atoms/Icon.svelte.d.ts +7 -0
  12. package/dist/components/atoms/Kbd.svelte +28 -16
  13. package/dist/components/atoms/Link.svelte +6 -4
  14. package/dist/components/atoms/Scrim.svelte +18 -1
  15. package/dist/components/atoms/Slider.svelte +11 -1
  16. package/dist/components/atoms/Swatch.svelte +12 -0
  17. package/dist/components/atoms/TimeDisplay.svelte +98 -0
  18. package/dist/components/atoms/TimeDisplay.svelte.d.ts +24 -0
  19. package/dist/components/layouts/AppShell.svelte +72 -40
  20. package/dist/components/layouts/Container.svelte +25 -3
  21. package/dist/components/layouts/MasterDetail.svelte +59 -33
  22. package/dist/components/layouts/MasterDetail.svelte.d.ts +1 -1
  23. package/dist/components/layouts/NavItem.svelte +8 -4
  24. package/dist/components/layouts/NavSection.svelte +8 -2
  25. package/dist/components/layouts/ResizablePanel.svelte +19 -4
  26. package/dist/components/layouts/TabBar.svelte +217 -0
  27. package/dist/components/layouts/TabBar.svelte.d.ts +27 -0
  28. package/dist/components/layouts/Toolbar.svelte +5 -0
  29. package/dist/components/layouts/toolbar-roving.d.ts +16 -0
  30. package/dist/components/layouts/toolbar-roving.js +47 -1
  31. package/dist/components/molecules/Accordion.svelte +2 -1
  32. package/dist/components/molecules/AttachmentList.svelte +1 -0
  33. package/dist/components/molecules/Breadcrumb.svelte +7 -6
  34. package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -1
  35. package/dist/components/molecules/CapBar.svelte +3 -1
  36. package/dist/components/molecules/Composer.svelte +27 -5
  37. package/dist/components/molecules/Drawer.svelte +10 -1
  38. package/dist/components/molecules/Dropzone.svelte +2 -1
  39. package/dist/components/molecules/FilterInput.svelte +17 -1
  40. package/dist/components/molecules/FilterInput.svelte.d.ts +2 -0
  41. package/dist/components/molecules/Metric.svelte +28 -3
  42. package/dist/components/molecules/Modal.svelte +20 -4
  43. package/dist/components/molecules/Popover.svelte +7 -3
  44. package/dist/components/molecules/PullToRefresh.svelte +205 -0
  45. package/dist/components/molecules/PullToRefresh.svelte.d.ts +18 -0
  46. package/dist/components/molecules/SectionHeader.svelte +29 -14
  47. package/dist/components/molecules/SeekSlider.svelte +370 -0
  48. package/dist/components/molecules/SeekSlider.svelte.d.ts +37 -0
  49. package/dist/components/molecules/SegmentedControl.svelte +10 -6
  50. package/dist/components/molecules/SelectButton.svelte +33 -22
  51. package/dist/components/molecules/SwipeRow.svelte +349 -0
  52. package/dist/components/molecules/SwipeRow.svelte.d.ts +37 -0
  53. package/dist/components/molecules/Tabs.svelte +11 -5
  54. package/dist/components/molecules/Timestamp.svelte +1 -1
  55. package/dist/components/molecules/Toaster.svelte +70 -13
  56. package/dist/components/molecules/Tooltip.svelte +19 -10
  57. package/dist/components/molecules/VolumeSlider.svelte +367 -0
  58. package/dist/components/molecules/VolumeSlider.svelte.d.ts +30 -0
  59. package/dist/components/molecules/combobox-keyboard.js +2 -1
  60. package/dist/components/organisms/ConversationFeed.svelte +22 -8
  61. package/dist/components/organisms/ConversationFeed.svelte.d.ts +2 -0
  62. package/dist/components/organisms/DataTable.svelte +22 -5
  63. package/dist/components/organisms/DataTable.svelte.d.ts +4 -2
  64. package/dist/components/organisms/FilterSearchBar.svelte +4 -0
  65. package/dist/components/organisms/FilterSearchBar.svelte.d.ts +2 -0
  66. package/dist/index.d.ts +7 -0
  67. package/dist/index.js +7 -0
  68. package/dist/markdown.js +15 -7
  69. package/dist/media-time.d.ts +6 -0
  70. package/dist/media-time.js +25 -0
  71. package/dist/query/edit.js +1 -16
  72. package/dist/query/parser.js +68 -28
  73. package/dist/query/query.js +9 -5
  74. package/dist/query/quotes.d.ts +24 -0
  75. package/dist/query/quotes.js +78 -0
  76. package/dist/query/single.js +1 -3
  77. package/dist/query/suggest.js +25 -32
  78. package/dist/stores/fontscale.svelte.js +3 -2
  79. package/dist/stores/storage.d.ts +2 -0
  80. package/dist/stores/storage.js +16 -0
  81. package/dist/stores/theme.svelte.js +4 -7
  82. package/dist/styles/reset.css +30 -0
  83. package/dist/styles/syntax.css +0 -29
  84. package/package.json +1 -1
@@ -9,6 +9,7 @@
9
9
  // `resizeKey`).
10
10
  import type { Snippet } from 'svelte';
11
11
  import { browser } from '../../env';
12
+ import { readStorage, writeStorage } from '../../stores/storage';
12
13
  import Icon, { type IconName } from '../atoms/Icon.svelte';
13
14
  import Spinner from '../atoms/Spinner.svelte';
14
15
  import IconButton from './IconButton.svelte';
@@ -76,7 +77,7 @@
76
77
 
77
78
  function loadWidth(): number | null {
78
79
  if (!browser || !resizeKey) return null;
79
- const n = Number(localStorage.getItem(resizeKey));
80
+ const n = Number(readStorage(resizeKey));
80
81
  return Number.isFinite(n) && n >= MIN_W ? Math.min(n, MAX_W) : null;
81
82
  }
82
83
  let width = $state<number | null>(loadWidth());
@@ -103,9 +104,17 @@
103
104
  }
104
105
 
105
106
  // Click outside: <dialog fills the viewport; clicks on its padding-free self
106
- // (not the inner .sheet) are backdrop clicks.
107
+ // (not the inner .sheet) are backdrop clicks. A drag that starts in the sheet
108
+ // and ends on the backdrop also fires click on the dialog, so the press must
109
+ // have landed on the backdrop too.
110
+ let pressOnBackdrop = false;
111
+ function onDialogPointerDown(e: PointerEvent) {
112
+ pressOnBackdrop = e.target === dialogEl;
113
+ }
107
114
  function onDialogClick(e: MouseEvent) {
108
- if (e.target === dialogEl) requestClose();
115
+ const outside = pressOnBackdrop && e.target === dialogEl;
116
+ pressOnBackdrop = false;
117
+ if (outside) requestClose();
109
118
  }
110
119
 
111
120
  // --- resize ---
@@ -146,7 +155,7 @@
146
155
  } catch {
147
156
  /* already released */
148
157
  }
149
- if (browser && resizeKey && width != null) localStorage.setItem(resizeKey, String(width));
158
+ if (browser && resizeKey && width != null) writeStorage(resizeKey, String(width));
150
159
  }
151
160
  </script>
152
161
 
@@ -166,6 +175,7 @@
166
175
  onclose={() => {
167
176
  if (controlled) open = false;
168
177
  }}
178
+ onpointerdown={onDialogPointerDown}
169
179
  onclick={onDialogClick}
170
180
  >
171
181
  <div
@@ -283,6 +293,12 @@
283
293
  opacity: 0.4;
284
294
  }
285
295
  }
296
+ @media (prefers-reduced-motion: reduce) {
297
+ .modal::backdrop,
298
+ .sheet {
299
+ animation: none;
300
+ }
301
+ }
286
302
  .modal.resizing .sheet {
287
303
  user-select: none;
288
304
  /* Make per-frame width changes cheap to paint: hint the animated property,
@@ -186,7 +186,11 @@
186
186
  }
187
187
 
188
188
  const hover = createHoverIntent({ open: show, close });
189
- $effect(() => () => hover.cancel());
189
+ $effect(() => () => {
190
+ hover.cancel();
191
+ removeEventListener('scroll', reposition, true);
192
+ removeEventListener('resize', reposition);
193
+ });
190
194
 
191
195
  function onPointerEnter(e: PointerEvent) {
192
196
  if (disabled || !opensOnHover(openOn, e.pointerType)) return;
@@ -194,8 +198,8 @@
194
198
  else hover.enter(hoverDelay);
195
199
  }
196
200
 
197
- function onPointerLeave() {
198
- if (openOn !== 'hover') return;
201
+ function onPointerLeave(e: PointerEvent) {
202
+ if (!opensOnHover(openOn, e.pointerType)) return;
199
203
  hover.leave(HOVER_CLOSE_GRACE);
200
204
  }
201
205
 
@@ -0,0 +1,205 @@
1
+ <script lang="ts">
2
+ // Pull-down-to-refresh scroll container. The pull only starts from
3
+ // `scrollTop === 0` with a touch or pen pointer, the distance is damped so
4
+ // the indicator asymptotically approaches twice the threshold, and a release
5
+ // past the threshold awaits `onrefresh` while a Spinner holds the slot.
6
+ // Mouse and keyboard users refresh through the page's own control.
7
+ import type { Snippet } from 'svelte';
8
+ import Icon from '../atoms/Icon.svelte';
9
+ import Spinner from '../atoms/Spinner.svelte';
10
+
11
+ let {
12
+ children,
13
+ onrefresh,
14
+ threshold = 72,
15
+ label = 'Refreshing…',
16
+ pullLabel = 'Pull to refresh',
17
+ releaseLabel = 'Release to refresh',
18
+ disabled = false,
19
+ class: klass = '',
20
+ ...rest
21
+ }: {
22
+ children?: Snippet;
23
+ /** Awaited after a release past the threshold; the spinner shows until it settles. */
24
+ onrefresh?: () => Promise<void> | void;
25
+ /** Damped pull distance (px) that arms the refresh. */
26
+ threshold?: number;
27
+ /** Announced through the live region while `onrefresh` is pending. */
28
+ label?: string;
29
+ pullLabel?: string;
30
+ releaseLabel?: string;
31
+ disabled?: boolean;
32
+ class?: string;
33
+ [key: string]: unknown;
34
+ } = $props();
35
+
36
+ const SLOP = 6;
37
+
38
+ let scroller: HTMLElement | undefined;
39
+ let offset = $state(0);
40
+ let pending = $state(false);
41
+ let pulling = $state(false);
42
+ let press: { id: number; y: number } | null = null;
43
+
44
+ const ready = $derived(pulling && offset >= threshold);
45
+ const phase = $derived(pending ? 'refreshing' : ready ? 'ready' : pulling ? 'pulling' : 'idle');
46
+ const progress = $derived(Math.min(1, offset / Math.max(1, threshold)));
47
+
48
+ function damp(dy: number) {
49
+ const max = threshold * 2;
50
+ return max * (1 - Math.exp(-dy / max));
51
+ }
52
+
53
+ function onpointerdown(e: PointerEvent) {
54
+ if (disabled || pending || press) return;
55
+ if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return;
56
+ if (!scroller || scroller.scrollTop > 0) return;
57
+ press = { id: e.pointerId, y: e.clientY };
58
+ addEventListener('pointermove', onpointermove);
59
+ addEventListener('pointerup', onpointerup);
60
+ addEventListener('pointercancel', onpointercancel);
61
+ }
62
+
63
+ function onpointermove(e: PointerEvent) {
64
+ if (!press || e.pointerId !== press.id) return;
65
+ const dy = e.clientY - press.y;
66
+ if (!pulling) {
67
+ if (dy < -SLOP || (scroller && scroller.scrollTop > 0)) return release();
68
+ if (dy < SLOP) return;
69
+ pulling = true;
70
+ }
71
+ offset = dy > 0 ? damp(dy) : 0;
72
+ }
73
+
74
+ function onpointerup(e: PointerEvent) {
75
+ if (!press || e.pointerId !== press.id) return;
76
+ const fire = ready;
77
+ release();
78
+ if (fire) void refresh();
79
+ }
80
+
81
+ function onpointercancel(e: PointerEvent) {
82
+ if (!press || e.pointerId !== press.id) return;
83
+ release();
84
+ }
85
+
86
+ function release() {
87
+ press = null;
88
+ pulling = false;
89
+ if (!pending) offset = 0;
90
+ removeEventListener('pointermove', onpointermove);
91
+ removeEventListener('pointerup', onpointerup);
92
+ removeEventListener('pointercancel', onpointercancel);
93
+ }
94
+
95
+ async function refresh() {
96
+ if (pending) return;
97
+ pending = true;
98
+ offset = threshold;
99
+ try {
100
+ await onrefresh?.();
101
+ } finally {
102
+ pending = false;
103
+ offset = 0;
104
+ }
105
+ }
106
+
107
+ function scrollRoot(el: HTMLElement) {
108
+ scroller = el;
109
+ const block = (e: TouchEvent) => {
110
+ if (pulling) e.preventDefault();
111
+ };
112
+ el.addEventListener('touchmove', block, { passive: false });
113
+ return () => {
114
+ el.removeEventListener('touchmove', block);
115
+ release();
116
+ scroller = undefined;
117
+ };
118
+ }
119
+ </script>
120
+
121
+ <div
122
+ data-tsu="PullToRefresh"
123
+ data-state={phase}
124
+ class="ptr {klass}"
125
+ class:pulling
126
+ class:pending
127
+ style:--ptr-offset="{offset}px"
128
+ style:--ptr-progress={progress}
129
+ aria-busy={pending || undefined}
130
+ {onpointerdown}
131
+ {@attach scrollRoot}
132
+ {...rest}
133
+ >
134
+ <div class="ptr-indicator" aria-hidden="true">
135
+ {#if pending}
136
+ <Spinner label={undefined} />
137
+ {:else}
138
+ <Icon name="arrow-down" class="ptr-arrow" />
139
+ {/if}
140
+ </div>
141
+ <div class="ptr-status sr-only" role="status" aria-live="polite">
142
+ {#if pending}{label}{:else if ready}{releaseLabel}{:else if pulling}{pullLabel}{/if}
143
+ </div>
144
+ <div class="ptr-content">
145
+ {@render children?.()}
146
+ </div>
147
+ </div>
148
+
149
+ <style>
150
+ .ptr {
151
+ position: relative;
152
+ overflow-y: auto;
153
+ overscroll-behavior-y: contain;
154
+ -webkit-overflow-scrolling: touch;
155
+ touch-action: pan-y;
156
+ }
157
+ .ptr-content {
158
+ translate: 0 var(--ptr-offset, 0px);
159
+ transition: translate 200ms var(--ease);
160
+ }
161
+ .pulling .ptr-content {
162
+ transition: none;
163
+ }
164
+ .ptr-indicator {
165
+ position: absolute;
166
+ inset: 0 0 auto;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ height: var(--ptr-offset, 0px);
171
+ overflow: hidden;
172
+ color: var(--text-muted);
173
+ opacity: var(--ptr-progress, 0);
174
+ transition: height 200ms var(--ease);
175
+ pointer-events: none;
176
+ }
177
+ .pulling .ptr-indicator {
178
+ transition: none;
179
+ }
180
+ .ptr-indicator :global(.ptr-arrow) {
181
+ rotate: calc(180deg * var(--ptr-progress, 0));
182
+ transition: rotate 150ms var(--ease);
183
+ }
184
+ .pending .ptr-indicator {
185
+ color: var(--accent);
186
+ }
187
+ .sr-only {
188
+ position: absolute;
189
+ width: 1px;
190
+ height: 1px;
191
+ overflow: hidden;
192
+ clip: rect(0 0 0 0);
193
+ white-space: nowrap;
194
+ }
195
+ @media (prefers-reduced-motion: reduce) {
196
+ .ptr-content,
197
+ .ptr-indicator,
198
+ .ptr-indicator :global(.ptr-arrow) {
199
+ transition: none;
200
+ }
201
+ .ptr-indicator :global(.ptr-arrow) {
202
+ rotate: none;
203
+ }
204
+ }
205
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ children?: Snippet;
4
+ /** Awaited after a release past the threshold; the spinner shows until it settles. */
5
+ onrefresh?: () => Promise<void> | void;
6
+ /** Damped pull distance (px) that arms the refresh. */
7
+ threshold?: number;
8
+ /** Announced through the live region while `onrefresh` is pending. */
9
+ label?: string;
10
+ pullLabel?: string;
11
+ releaseLabel?: string;
12
+ disabled?: boolean;
13
+ class?: string;
14
+ [key: string]: unknown;
15
+ };
16
+ declare const PullToRefresh: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type PullToRefresh = ReturnType<typeof PullToRefresh>;
18
+ export default PullToRefresh;
@@ -4,7 +4,7 @@
4
4
  // swatch and right-aligned actions on one wrapping row. `divider` underlines
5
5
  // it, `sticky` pins it under the app header and publishes its height as
6
6
  // `--section-header-h` on the parent so sibling sticky content can offset
7
- // itself. `collapsible` turns the title into a disclosure button that
7
+ // itself. `collapsible` puts a disclosure button inside the heading that
8
8
  // toggles the `children` block rendered beneath. `variant="group"` is the
9
9
  // non-wrapping list-group row: `lead` · title · count · flexible rule · actions.
10
10
  import type { Snippet } from 'svelte';
@@ -90,16 +90,7 @@
90
90
  >
91
91
  <div class="sh-row">
92
92
  {#if collapsible}
93
- <button
94
- type="button"
95
- class="sh-toggle"
96
- aria-expanded={open}
97
- aria-controls={panelId}
98
- onclick={() => (open = !open)}
99
- >
100
- <Icon name="chevron-right" class="sh-chevron" />
101
- {@render head()}
102
- </button>
93
+ <span class="sh-head">{@render head()}</span>
103
94
  {:else}
104
95
  {@render head()}
105
96
  {/if}
@@ -125,10 +116,28 @@
125
116
  {#if hue !== undefined}
126
117
  <span class="sh-swatch" style:--sh-hue={hue} aria-hidden="true"></span>
127
118
  {/if}
128
- {#if icon}
129
- <span class="sh-icon" aria-hidden="true"><Icon name={icon} /></span>
119
+ {#if collapsible}
120
+ <Heading {level} {size} class="sh-title">
121
+ <button
122
+ type="button"
123
+ class="sh-toggle"
124
+ aria-expanded={open}
125
+ aria-controls={open ? panelId : undefined}
126
+ onclick={() => (open = !open)}
127
+ >
128
+ <Icon name="chevron-right" class="sh-chevron" />
129
+ {#if icon}
130
+ <span class="sh-icon" aria-hidden="true"><Icon name={icon} /></span>
131
+ {/if}
132
+ {text}
133
+ </button>
134
+ </Heading>
135
+ {:else}
136
+ {#if icon}
137
+ <span class="sh-icon" aria-hidden="true"><Icon name={icon} /></span>
138
+ {/if}
139
+ <Heading {level} {size} class="sh-title">{text}</Heading>
130
140
  {/if}
131
- <Heading {level} {size} class="sh-title">{text}</Heading>
132
141
  {#if count !== undefined}
133
142
  <Text tone="faint" weight="normal" numeric class="sh-count">{count}</Text>
134
143
  {/if}
@@ -233,6 +242,12 @@
233
242
  gap: var(--sp-2);
234
243
  margin-inline-start: auto;
235
244
  }
245
+ .sh-head {
246
+ display: inline-flex;
247
+ align-items: center;
248
+ gap: var(--sp-2);
249
+ min-width: 0;
250
+ }
236
251
  .sh-toggle {
237
252
  display: inline-flex;
238
253
  align-items: center;