@dorsk/tsumikit 0.70.0 → 0.72.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 (56) hide show
  1. package/README.md +49 -8
  2. package/dist/autoresize.js +13 -0
  3. package/dist/clipboard.js +12 -0
  4. package/dist/components/atoms/Artwork.svelte +10 -3
  5. package/dist/components/atoms/Artwork.svelte.d.ts +2 -0
  6. package/dist/components/atoms/Avatar.svelte +4 -3
  7. package/dist/components/atoms/Badge.svelte +23 -8
  8. package/dist/components/atoms/Badge.svelte.d.ts +2 -1
  9. package/dist/components/atoms/Button.svelte +39 -11
  10. package/dist/components/atoms/Card.svelte +2 -0
  11. package/dist/components/atoms/Dot.svelte +1 -1
  12. package/dist/components/atoms/Icon.svelte +2 -0
  13. package/dist/components/atoms/Icon.svelte.d.ts +2 -0
  14. package/dist/components/atoms/Input.svelte +3 -0
  15. package/dist/components/atoms/Kbd.svelte +28 -16
  16. package/dist/components/atoms/Link.svelte +18 -4
  17. package/dist/components/atoms/Scrim.svelte +18 -1
  18. package/dist/components/atoms/Select.svelte +1 -0
  19. package/dist/components/atoms/Slider.svelte +11 -1
  20. package/dist/components/atoms/Swatch.svelte +12 -0
  21. package/dist/components/atoms/Switch.svelte +12 -0
  22. package/dist/components/layouts/AppShell.svelte +70 -39
  23. package/dist/components/layouts/Container.svelte +25 -3
  24. package/dist/components/layouts/MasterDetail.svelte +59 -33
  25. package/dist/components/layouts/MasterDetail.svelte.d.ts +1 -1
  26. package/dist/components/layouts/NavItem.svelte +8 -4
  27. package/dist/components/layouts/NavSection.svelte +8 -2
  28. package/dist/components/layouts/ResizablePanel.svelte +14 -0
  29. package/dist/components/layouts/TabBar.svelte +217 -0
  30. package/dist/components/layouts/TabBar.svelte.d.ts +27 -0
  31. package/dist/components/layouts/Toolbar.svelte +5 -0
  32. package/dist/components/layouts/toolbar-roving.d.ts +16 -0
  33. package/dist/components/layouts/toolbar-roving.js +47 -1
  34. package/dist/components/molecules/Breadcrumb.svelte +44 -2
  35. package/dist/components/molecules/Breadcrumb.svelte.d.ts +6 -0
  36. package/dist/components/molecules/FilterInput.svelte +7 -0
  37. package/dist/components/molecules/FilterInput.svelte.d.ts +2 -0
  38. package/dist/components/molecules/PullToRefresh.svelte +205 -0
  39. package/dist/components/molecules/PullToRefresh.svelte.d.ts +18 -0
  40. package/dist/components/molecules/SegmentedControl.svelte +44 -1
  41. package/dist/components/molecules/SwipeRow.svelte +349 -0
  42. package/dist/components/molecules/SwipeRow.svelte.d.ts +37 -0
  43. package/dist/components/molecules/Toggle.svelte +12 -0
  44. package/dist/components/organisms/ConversationFeed.svelte +22 -8
  45. package/dist/components/organisms/ConversationFeed.svelte.d.ts +2 -0
  46. package/dist/components/organisms/DataTable.svelte +72 -12
  47. package/dist/components/organisms/DataTable.svelte.d.ts +7 -2
  48. package/dist/components/organisms/FilterSearchBar.svelte +4 -0
  49. package/dist/components/organisms/FilterSearchBar.svelte.d.ts +2 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +3 -0
  52. package/dist/markdown.js +15 -7
  53. package/dist/styles/reset.css +30 -0
  54. package/dist/styles/syntax.css +0 -29
  55. package/dist/styles/tokens.css +6 -0
  56. package/package.json +1 -1
@@ -25,6 +25,16 @@
25
25
  * detail cells share lines (`responsive="stack"` only, default true). */
26
26
  stackLabel?: boolean;
27
27
  }
28
+
29
+ let rootFontPx: number | undefined;
30
+
31
+ function rootFontSize(): number {
32
+ if (rootFontPx === undefined) {
33
+ rootFontPx = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
34
+ window.addEventListener('resize', () => (rootFontPx = undefined), { once: true });
35
+ }
36
+ return rootFontPx;
37
+ }
28
38
  </script>
29
39
 
30
40
  <script lang="ts" generics="T">
@@ -60,6 +70,7 @@
60
70
  loadMoreLabel = 'Load more',
61
71
  responsive = 'scroll',
62
72
  stackBelow = '48rem',
73
+ stackActions = 'start',
63
74
  class: klass = '',
64
75
  style: styleProp = '',
65
76
  tableClass = '',
@@ -76,9 +87,11 @@
76
87
  /** Replaces a column's header content (e.g. a select-all checkbox). */
77
88
  headerSnippets?: Record<string, Snippet<[Column<T>]>>;
78
89
  empty?: string | Snippet;
90
+ /** Sticks within the table's own scroller, which also scrolls horizontally,
91
+ * so it only sticks when the table has a bounded height
92
+ * (e.g. `style="max-height: 60vh"`), not to the page. */
79
93
  stickyHeader?: boolean;
80
- /** Distance from the scroll container's top for the sticky header, e.g.
81
- * `var(--header-h)` when the page header overlaps. Defaults to 0. */
94
+ /** Distance from the scroller's top for the sticky header. Defaults to 0. */
82
95
  stickyOffset?: string;
83
96
  /** `fixed` makes declared column widths authoritative (needed for truncate). */
84
97
  layout?: 'auto' | 'fixed';
@@ -101,6 +114,9 @@
101
114
  responsive?: 'scroll' | 'stack';
102
115
  /** Width (px/em/rem) under which `stack` mode kicks in. */
103
116
  stackBelow?: string;
117
+ /** Where actions sit in a stacked card: `start` beside the title (wraps
118
+ * under a long one), `end` always closing the card. */
119
+ stackActions?: 'start' | 'end';
104
120
  class?: string;
105
121
  style?: string;
106
122
  /** Class on the inner <table>. */
@@ -121,22 +137,31 @@
121
137
  return { destroy: () => observer.disconnect() };
122
138
  }
123
139
 
140
+ function markEmpty(node: HTMLElement) {
141
+ const update = () => node.toggleAttribute('data-empty', !node.firstElementChild && !node.textContent?.trim());
142
+ update();
143
+ const observer = new MutationObserver(update);
144
+ observer.observe(node, { childList: true, subtree: true, characterData: true });
145
+ return () => observer.disconnect();
146
+ }
147
+
124
148
  function toPx(length: string, el: HTMLElement): number {
125
149
  const n = Number.parseFloat(length);
126
- const fontSize = (node: Element) => Number.parseFloat(getComputedStyle(node).fontSize);
127
- if (length.endsWith('rem')) return n * fontSize(document.documentElement);
128
- if (length.endsWith('em')) return n * fontSize(el);
150
+ if (length.endsWith('rem')) return n * rootFontSize();
151
+ if (length.endsWith('em')) return n * Number.parseFloat(getComputedStyle(el).fontSize);
129
152
  return n;
130
153
  }
131
154
 
155
+ // Font sizes are read in the observer callback, after layout, so mounting never forces a style recalc.
132
156
  $effect(() => {
133
157
  if (responsive !== 'stack' || !wrapEl) {
134
158
  stacked = false;
135
159
  return;
136
160
  }
137
161
  const el = wrapEl;
138
- const limit = toPx(stackBelow, el);
162
+ const below = stackBelow;
139
163
  const observer = new ResizeObserver(([entry]) => {
164
+ const limit = toPx(below, el);
140
165
  stacked = entry.contentRect.width < limit;
141
166
  });
142
167
  observer.observe(el);
@@ -148,6 +173,13 @@
148
173
 
149
174
  const colCount = $derived(columns.length + (rowActions ? 1 : 0));
150
175
 
176
+ const NESTED_CONTROL = 'a,button,input,select,textarea,label,summary,[role="button"],[contenteditable]';
177
+
178
+ function fromNestedControl(e: Event & { currentTarget: HTMLElement }): boolean {
179
+ const hit = (e.target as Element).closest(NESTED_CONTROL);
180
+ return !!hit && hit !== e.currentTarget && e.currentTarget.contains(hit);
181
+ }
182
+
151
183
  function display(col: Column<T>, row: T): unknown {
152
184
  if (col.get) return col.get(row);
153
185
  return (row as Record<string, unknown>)[col.key];
@@ -188,6 +220,7 @@
188
220
  data-tsu="DataTable"
189
221
  data-size={size}
190
222
  data-stacked={stacked || undefined}
223
+ data-stack-actions={responsive === 'stack' && stackActions === 'end' ? 'end' : undefined}
191
224
  aria-busy={loading || undefined}
192
225
  >
193
226
  <table
@@ -221,7 +254,7 @@
221
254
  {:else if col.sortable}
222
255
  <button type="button" class="dt-sort" onclick={() => toggleSort(col)}>
223
256
  <span>{col.label}</span>
224
- <span class="dt-arrow" class:active={sortKey === col.key}>
257
+ <span class="dt-arrow" aria-hidden="true" class:active={sortKey === col.key}>
225
258
  {sortKey === col.key ? (sortDir === 'asc' ? '↑' : '↓') : '↕'}
226
259
  </span>
227
260
  </button>
@@ -256,9 +289,14 @@
256
289
  class:clickable={!!onrowclick}
257
290
  class:toned={!!tone && tone !== 'neutral'}
258
291
  tabindex={onrowclick ? 0 : undefined}
259
- onclick={onrowclick ? () => onrowclick(row) : undefined}
292
+ onclick={onrowclick
293
+ ? (e) => {
294
+ if (!fromNestedControl(e)) onrowclick(row);
295
+ }
296
+ : undefined}
260
297
  onkeydown={onrowclick
261
298
  ? (e) => {
299
+ if (e.target !== e.currentTarget) return;
262
300
  if (e.key === 'Enter' || e.key === ' ') {
263
301
  e.preventDefault();
264
302
  onrowclick(row);
@@ -267,20 +305,25 @@
267
305
  : undefined}
268
306
  >
269
307
  {#each columns as col (col.key)}
308
+ {@const value = cellSnippets[col.key] ? undefined : display(col, row)}
270
309
  <td
271
310
  data-part="cell"
272
311
  data-hide-below={col.hideBelow}
273
312
  data-role={responsive === 'stack' ? (col.role ?? 'detail') : undefined}
274
313
  data-label={responsive === 'stack' ? col.label : undefined}
275
314
  data-stack-label={responsive === 'stack' && col.stackLabel === false ? 'false' : undefined}
315
+ data-empty={responsive === 'stack' && !cellSnippets[col.key] && (value == null || value === '')
316
+ ? ''
317
+ : undefined}
276
318
  class:truncate={col.truncate}
277
319
  class:nowrap={col.nowrap}
278
320
  style:text-align={stacked ? undefined : (col.align ?? 'left')}
321
+ {@attach responsive === 'stack' && cellSnippets[col.key] ? markEmpty : undefined}
279
322
  >
280
323
  {#if cellSnippets[col.key]}
281
324
  {@render cellSnippets[col.key](row)}
282
325
  {:else}
283
- {display(col, row)}
326
+ {value}
284
327
  {/if}
285
328
  </td>
286
329
  {/each}
@@ -403,7 +446,10 @@
403
446
  tr.clickable:hover,
404
447
  tr.clickable:focus-visible {
405
448
  background: var(--bg-elevated-2);
406
- outline: none;
449
+ }
450
+ tr.clickable:focus-visible {
451
+ outline: var(--focus-ring);
452
+ outline-offset: -2px;
407
453
  }
408
454
  td.truncate {
409
455
  white-space: nowrap;
@@ -479,6 +525,9 @@
479
525
  [data-stacked] {
480
526
  overflow-x: visible;
481
527
  }
528
+ [data-stacked] .dt {
529
+ table-layout: fixed;
530
+ }
482
531
  [data-stacked] thead {
483
532
  position: absolute;
484
533
  width: 1px;
@@ -552,10 +601,17 @@
552
601
  [data-stacked] td[data-stack-label='false']::before {
553
602
  content: none;
554
603
  }
555
- [data-stacked] td[data-role='meta'] ~ td[data-role='meta'][data-stack-label='false'] {
604
+ [data-stacked] td[data-part='cell'][data-empty] {
605
+ display: none;
606
+ }
607
+ [data-stacked]
608
+ td[data-role='meta']:not([data-empty])
609
+ ~ td[data-role='meta'][data-stack-label='false']:not([data-empty]) {
556
610
  margin-inline-start: calc(var(--sp-1) - var(--sp-3));
557
611
  }
558
- [data-stacked] td[data-role='meta'] ~ td[data-role='meta'][data-stack-label='false']::before {
612
+ [data-stacked]
613
+ td[data-role='meta']:not([data-empty])
614
+ ~ td[data-role='meta'][data-stack-label='false']:not([data-empty])::before {
559
615
  content: '·';
560
616
  margin-inline-end: var(--sp-1);
561
617
  color: var(--text-faint);
@@ -568,6 +624,10 @@
568
624
  width: auto;
569
625
  opacity: 1;
570
626
  }
627
+ [data-stacked][data-stack-actions='end'] td[data-role='actions'],
628
+ [data-stacked][data-stack-actions='end'] td.dt-actions {
629
+ order: 2;
630
+ }
571
631
  [data-stacked] td[data-role='hidden'] {
572
632
  display: none;
573
633
  }
@@ -38,9 +38,11 @@ declare function $$render<T>(): {
38
38
  /** Replaces a column's header content (e.g. a select-all checkbox). */
39
39
  headerSnippets?: Record<string, Snippet<[Column<T>]>>;
40
40
  empty?: string | Snippet;
41
+ /** Sticks within the table's own scroller, which also scrolls horizontally,
42
+ * so it only sticks when the table has a bounded height
43
+ * (e.g. `style="max-height: 60vh"`), not to the page. */
41
44
  stickyHeader?: boolean;
42
- /** Distance from the scroll container's top for the sticky header, e.g.
43
- * `var(--header-h)` when the page header overlaps. Defaults to 0. */
45
+ /** Distance from the scroller's top for the sticky header. Defaults to 0. */
44
46
  stickyOffset?: string;
45
47
  /** `fixed` makes declared column widths authoritative (needed for truncate). */
46
48
  layout?: "auto" | "fixed";
@@ -63,6 +65,9 @@ declare function $$render<T>(): {
63
65
  responsive?: "scroll" | "stack";
64
66
  /** Width (px/em/rem) under which `stack` mode kicks in. */
65
67
  stackBelow?: string;
68
+ /** Where actions sit in a stacked card: `start` beside the title (wraps
69
+ * under a long one), `end` always closing the card. */
70
+ stackActions?: "start" | "end";
66
71
  class?: string;
67
72
  style?: string;
68
73
  /** Class on the inner <table>. */
@@ -29,6 +29,7 @@
29
29
  size = 'md',
30
30
  shape = 'square',
31
31
  surface = 'base',
32
+ font = 'mono',
32
33
  hotkey,
33
34
  showHotkey = false,
34
35
  grow = false,
@@ -56,6 +57,8 @@
56
57
  size?: ControlSize;
57
58
  shape?: 'square' | 'pill';
58
59
  surface?: 'base' | 'raised' | 'sunken';
60
+ /** `sans` renders the typed text and placeholder in `--font-sans`; chips stay mono. */
61
+ font?: 'mono' | 'sans';
59
62
  hotkey?: string;
60
63
  showHotkey?: boolean;
61
64
  grow?: boolean;
@@ -81,6 +84,7 @@
81
84
  {size}
82
85
  {shape}
83
86
  {surface}
87
+ {font}
84
88
  {hotkey}
85
89
  {showHotkey}
86
90
  {grow}
@@ -19,6 +19,8 @@ type Own = {
19
19
  size?: ControlSize;
20
20
  shape?: 'square' | 'pill';
21
21
  surface?: 'base' | 'raised' | 'sunken';
22
+ /** `sans` renders the typed text and placeholder in `--font-sans`; chips stay mono. */
23
+ font?: 'mono' | 'sans';
22
24
  hotkey?: string;
23
25
  showHotkey?: boolean;
24
26
  grow?: boolean;
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export { default as NavSection } from './components/layouts/NavSection.svelte';
41
41
  export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
42
42
  export { type ResizeHandleParams, resizeHandle, } from './components/layouts/resizable-panel-frame.js';
43
43
  export { default as Stack } from './components/layouts/Stack.svelte';
44
+ export { default as TabBar, type TabBarItem } from './components/layouts/TabBar.svelte';
44
45
  export { default as Toolbar } from './components/layouts/Toolbar.svelte';
45
46
  export { type AccordionHeaderContext, type AccordionItem, type AccordionVariant, default as Accordion, } from './components/molecules/Accordion.svelte';
46
47
  export { type Attachment, default as AttachmentList, } from './components/molecules/AttachmentList.svelte';
@@ -79,6 +80,7 @@ export { default as OptionButton } from './components/molecules/OptionButton.sve
79
80
  export { default as Pagination } from './components/molecules/Pagination.svelte';
80
81
  export { default as Popover } from './components/molecules/Popover.svelte';
81
82
  export { default as Prose } from './components/molecules/Prose.svelte';
83
+ export { default as PullToRefresh } from './components/molecules/PullToRefresh.svelte';
82
84
  export { default as RadialMenu, type RadialArc, } from './components/molecules/RadialMenu.svelte';
83
85
  export { default as RadioGroup, type RadioOption, } from './components/molecules/RadioGroup.svelte';
84
86
  export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
@@ -87,6 +89,7 @@ export { default as SelectButton } from './components/molecules/SelectButton.sve
87
89
  export { default as SortableList, SORTABLE_LIST_MESSAGES, type SortableListMessages, type SortableListMove, type SortableListRowContext, } from './components/molecules/SortableList.svelte';
88
90
  export { default as SplitButton } from './components/molecules/SplitButton.svelte';
89
91
  export { default as StatusBar } from './components/molecules/StatusBar.svelte';
92
+ export { default as SwipeRow, type SwipeAction } from './components/molecules/SwipeRow.svelte';
90
93
  export { default as Tabs, type TabItem } from './components/molecules/Tabs.svelte';
91
94
  export { default as ThemePicker } from './components/molecules/ThemePicker.svelte';
92
95
  export { default as Timestamp } from './components/molecules/Timestamp.svelte';
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ export { default as NavSection } from './components/layouts/NavSection.svelte';
46
46
  export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
47
47
  export { resizeHandle, } from './components/layouts/resizable-panel-frame.js';
48
48
  export { default as Stack } from './components/layouts/Stack.svelte';
49
+ export { default as TabBar } from './components/layouts/TabBar.svelte';
49
50
  export { default as Toolbar } from './components/layouts/Toolbar.svelte';
50
51
  export { default as Accordion, } from './components/molecules/Accordion.svelte';
51
52
  export { default as AttachmentList, } from './components/molecules/AttachmentList.svelte';
@@ -87,6 +88,7 @@ export { default as OptionButton } from './components/molecules/OptionButton.sve
87
88
  export { default as Pagination } from './components/molecules/Pagination.svelte';
88
89
  export { default as Popover } from './components/molecules/Popover.svelte';
89
90
  export { default as Prose } from './components/molecules/Prose.svelte';
91
+ export { default as PullToRefresh } from './components/molecules/PullToRefresh.svelte';
90
92
  export { default as RadialMenu, } from './components/molecules/RadialMenu.svelte';
91
93
  export { default as RadioGroup, } from './components/molecules/RadioGroup.svelte';
92
94
  export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
@@ -95,6 +97,7 @@ export { default as SelectButton } from './components/molecules/SelectButton.sve
95
97
  export { default as SortableList, SORTABLE_LIST_MESSAGES, } from './components/molecules/SortableList.svelte';
96
98
  export { default as SplitButton } from './components/molecules/SplitButton.svelte';
97
99
  export { default as StatusBar } from './components/molecules/StatusBar.svelte';
100
+ export { default as SwipeRow } from './components/molecules/SwipeRow.svelte';
98
101
  export { default as Tabs } from './components/molecules/Tabs.svelte';
99
102
  export { default as ThemePicker } from './components/molecules/ThemePicker.svelte';
100
103
  export { default as Timestamp } from './components/molecules/Timestamp.svelte';
package/dist/markdown.js CHANGED
@@ -15,6 +15,7 @@ const BLOCK_RE = new RegExp(`${BLOCK_L}(\\d+)${BLOCK_R}`, 'g');
15
15
  const SPAN_L = '\x10';
16
16
  const SPAN_R = '\x11';
17
17
  const SPAN_RE = new RegExp(`${SPAN_L}(\\d+)${SPAN_R}`, 'g');
18
+ const BARE_URL_RE = new RegExp(`\\bhttps?://(?:&amp;|[^\\s&<>"${SPAN_L}${SPAN_R}])+`, 'g');
18
19
  // Terminal escape sequences (the ansi-regex pattern), then the remaining C0
19
20
  // controls except tab and newline, plus DEL.
20
21
  const ANSI_RE =
@@ -62,20 +63,27 @@ function autolinkUrls(s) {
62
63
  });
63
64
  return s.replace(SPAN_RE, (_m, i) => saved[Number(i)]);
64
65
  }
65
- // Inline passes on already-escaped text.
66
+ // Inline passes on already-escaped text; emphasis never reaches inside code, link tags or URLs.
66
67
  function inline(s, autolink) {
67
- s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
68
- s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
69
- s = s.replace(/(^|[^*\w])\*([^*\n]+)\*/g, '$1<em>$2</em>');
70
- s = s.replace(/(^|[^\w])_([^_\n]+)_(?!\w)/g, '$1<em>$2</em>');
71
- s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>');
68
+ const held = [];
69
+ const hold = (html) => `${SPAN_L}${held.push(html) - 1}${SPAN_R}`;
70
+ s = s.replace(/`([^`]+)`/g, (_m, code) => hold(`<code>${code}</code>`));
72
71
  s = s.replace(/\[([^\]]+)\]\(([^\s)]+)\)/g, (m, text, url) => {
73
72
  const href = safeHref(url);
74
73
  if (!href)
75
74
  return m;
76
75
  const external = /^https?:/i.test(href) ? ' target="_blank" rel="noopener noreferrer"' : '';
77
- return `<a href="${href}"${external}>${text}</a>`;
76
+ return `${hold(`<a href="${href}"${external}>`)}${text}${hold('</a>')}`;
77
+ });
78
+ s = s.replace(BARE_URL_RE, (raw) => {
79
+ const url = raw.replace(/[*_~]+$/, '');
80
+ return hold(url) + raw.slice(url.length);
78
81
  });
82
+ s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
83
+ s = s.replace(/(^|[^*\w])\*([^*\n]+)\*/g, '$1<em>$2</em>');
84
+ s = s.replace(/(^|[^\w])_([^_\n]+)_(?!\w)/g, '$1<em>$2</em>');
85
+ s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>');
86
+ s = s.replace(SPAN_RE, (_m, i) => held[Number(i)]);
79
87
  return autolink ? autolinkUrls(s) : s;
80
88
  }
81
89
  function splitRow(row) {
@@ -78,6 +78,11 @@ select {
78
78
  code,
79
79
  pre {
80
80
  font-family: var(--font-mono);
81
+ }
82
+ code {
83
+ font-size: var(--fs-xs);
84
+ }
85
+ pre {
81
86
  font-size: 0.875em;
82
87
  }
83
88
 
@@ -101,3 +106,28 @@ pre {
101
106
  }
102
107
  }
103
108
 
109
+ @media (prefers-reduced-motion: reduce) {
110
+ *,
111
+ *::before,
112
+ *::after {
113
+ animation-duration: 0.01ms !important;
114
+ animation-iteration-count: 1 !important;
115
+ transition-duration: 0.01ms !important;
116
+ scroll-behavior: auto !important;
117
+ }
118
+ }
119
+
120
+ /* Windows High Contrast: keep the focus ring on a system color. */
121
+ @media (forced-colors: active) {
122
+ :focus-visible {
123
+ outline: 2px solid Highlight;
124
+ outline-offset: 2px;
125
+ }
126
+ }
127
+
128
+ /* Stronger separators when the user asks for more contrast. */
129
+ @media (prefers-contrast: more) {
130
+ :root {
131
+ --border: var(--border-strong);
132
+ }
133
+ }
@@ -63,32 +63,3 @@
63
63
  .hljs-deletion {
64
64
  color: var(--danger);
65
65
  }
66
-
67
- @media (prefers-reduced-motion: reduce) {
68
- *,
69
- *::before,
70
- *::after {
71
- animation-duration: 0.01ms !important;
72
- animation-iteration-count: 1 !important;
73
- transition-duration: 0.01ms !important;
74
- scroll-behavior: auto !important;
75
- }
76
- }
77
-
78
- /* Forced colors (Windows High Contrast). The OS replaces our palette with its
79
- own; our job is to not fight it and to keep affordances visible. Keep the
80
- focus ring mapped to a system color, and make sure the focus outline shows
81
- even where we'd normally rely on a token border. */
82
- @media (forced-colors: active) {
83
- :focus-visible {
84
- outline: 2px solid Highlight;
85
- outline-offset: 2px;
86
- }
87
- }
88
-
89
- /* Stronger separators when the user asks for more contrast. */
90
- @media (prefers-contrast: more) {
91
- :root {
92
- --border: var(--border-strong);
93
- }
94
- }
@@ -184,6 +184,12 @@
184
184
  /* Minimum hit area on coarse pointers (WCAG 2.5.8); px so it never shrinks
185
185
  under a small root font-size. */
186
186
  --touch-target: 44px;
187
+ /* Compact controls on coarse pointers: an absolute ::before with inset-block
188
+ and inset-inline-start at var(--touch-slab) and inset-inline-end: 0 (no
189
+ scrollable overflow sideways). Controls that clip their overflow grow
190
+ min-height to --touch-target-compact instead. */
191
+ --touch-target-compact: 40px;
192
+ --touch-slab: min(-8px, calc((100% - var(--touch-target-compact)) / 2));
187
193
 
188
194
  /* ---- Layout ---- */
189
195
  /* Fixed (px, not rem): the header is chrome that must NOT scale with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.70.0",
3
+ "version": "0.72.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",