@dorsk/tsumikit 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/autoresize.d.ts +2 -0
  2. package/dist/autoresize.js +15 -1
  3. package/dist/components/atoms/Artwork.svelte +19 -18
  4. package/dist/components/atoms/Artwork.svelte.d.ts +3 -2
  5. package/dist/components/atoms/Badge.svelte +60 -26
  6. package/dist/components/atoms/Badge.svelte.d.ts +14 -3
  7. package/dist/components/atoms/Button.svelte +30 -12
  8. package/dist/components/atoms/Button.svelte.d.ts +4 -2
  9. package/dist/components/atoms/Card.svelte +47 -30
  10. package/dist/components/atoms/Card.svelte.d.ts +12 -3
  11. package/dist/components/atoms/Checkbox.svelte +16 -2
  12. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  13. package/dist/components/atoms/Dot.svelte +17 -11
  14. package/dist/components/atoms/Dot.svelte.d.ts +5 -2
  15. package/dist/components/atoms/Gauge.svelte +19 -18
  16. package/dist/components/atoms/Gauge.svelte.d.ts +3 -2
  17. package/dist/components/atoms/Heading.svelte +20 -19
  18. package/dist/components/atoms/Heading.svelte.d.ts +3 -2
  19. package/dist/components/atoms/Icon.svelte +33 -13
  20. package/dist/components/atoms/Icon.svelte.d.ts +17 -2
  21. package/dist/components/atoms/Input.svelte +17 -3
  22. package/dist/components/atoms/Input.svelte.d.ts +2 -1
  23. package/dist/components/atoms/Kbd.svelte +25 -3
  24. package/dist/components/atoms/Kbd.svelte.d.ts +5 -1
  25. package/dist/components/atoms/Link.svelte +16 -12
  26. package/dist/components/atoms/Link.svelte.d.ts +6 -2
  27. package/dist/components/atoms/Progress.svelte +119 -4
  28. package/dist/components/atoms/Progress.svelte.d.ts +28 -2
  29. package/dist/components/atoms/Scrim.svelte +7 -2
  30. package/dist/components/atoms/Scrim.svelte.d.ts +2 -0
  31. package/dist/components/atoms/SegmentedProgress.svelte +14 -2
  32. package/dist/components/atoms/SegmentedProgress.svelte.d.ts +5 -2
  33. package/dist/components/atoms/Select.svelte +18 -6
  34. package/dist/components/atoms/Select.svelte.d.ts +3 -3
  35. package/dist/components/atoms/Skeleton.svelte +12 -11
  36. package/dist/components/atoms/Skeleton.svelte.d.ts +3 -2
  37. package/dist/components/atoms/Slider.svelte +45 -3
  38. package/dist/components/atoms/Slider.svelte.d.ts +7 -0
  39. package/dist/components/atoms/Spinner.svelte +2 -0
  40. package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
  41. package/dist/components/atoms/Switch.svelte +70 -18
  42. package/dist/components/atoms/Switch.svelte.d.ts +5 -1
  43. package/dist/components/atoms/Text.svelte +39 -26
  44. package/dist/components/atoms/Text.svelte.d.ts +7 -3
  45. package/dist/components/atoms/Textarea.svelte +47 -10
  46. package/dist/components/atoms/Textarea.svelte.d.ts +10 -3
  47. package/dist/components/layouts/AppShell.svelte +7 -3
  48. package/dist/components/layouts/AppShell.svelte.d.ts +2 -0
  49. package/dist/components/layouts/AutoGrid.svelte +4 -0
  50. package/dist/components/layouts/AutoGrid.svelte.d.ts +2 -0
  51. package/dist/components/layouts/Cluster.svelte +62 -1
  52. package/dist/components/layouts/Cluster.svelte.d.ts +9 -0
  53. package/dist/components/layouts/NavItem.svelte +22 -1
  54. package/dist/components/layouts/NavItem.svelte.d.ts +5 -0
  55. package/dist/components/layouts/NavSection.svelte +4 -1
  56. package/dist/components/layouts/NavSection.svelte.d.ts +2 -0
  57. package/dist/components/layouts/ResizablePanel.svelte +14 -3
  58. package/dist/components/layouts/ResizablePanel.svelte.d.ts +5 -0
  59. package/dist/components/layouts/Stack.svelte +41 -0
  60. package/dist/components/layouts/Stack.svelte.d.ts +9 -0
  61. package/dist/components/layouts/Toolbar.svelte +122 -0
  62. package/dist/components/layouts/Toolbar.svelte.d.ts +22 -0
  63. package/dist/components/molecules/AttachmentList.svelte +59 -0
  64. package/dist/components/molecules/AttachmentList.svelte.d.ts +13 -0
  65. package/dist/components/molecules/Breadcrumb.svelte +6 -2
  66. package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -0
  67. package/dist/components/molecules/Callout.svelte +10 -4
  68. package/dist/components/molecules/Callout.svelte.d.ts +1 -1
  69. package/dist/components/molecules/CapBar.svelte +5 -1
  70. package/dist/components/molecules/ChatBubble.svelte +158 -0
  71. package/dist/components/molecules/ChatBubble.svelte.d.ts +26 -0
  72. package/dist/components/molecules/CodeBlock.svelte +36 -3
  73. package/dist/components/molecules/CodeBlock.svelte.d.ts +8 -0
  74. package/dist/components/molecules/Composer.svelte +195 -0
  75. package/dist/components/molecules/Composer.svelte.d.ts +27 -0
  76. package/dist/components/molecules/ConfirmModal.svelte +15 -5
  77. package/dist/components/molecules/ConfirmModal.svelte.d.ts +5 -1
  78. package/dist/components/molecules/Dropzone.svelte +9 -3
  79. package/dist/components/molecules/Dropzone.svelte.d.ts +2 -0
  80. package/dist/components/molecules/EmptyState.svelte +9 -5
  81. package/dist/components/molecules/EmptyState.svelte.d.ts +3 -1
  82. package/dist/components/molecules/Field.svelte +7 -1
  83. package/dist/components/molecules/Field.svelte.d.ts +2 -0
  84. package/dist/components/molecules/FileButton.svelte +2 -1
  85. package/dist/components/molecules/FileButton.svelte.d.ts +2 -1
  86. package/dist/components/molecules/FilterInput.svelte +13 -3
  87. package/dist/components/molecules/FilterInput.svelte.d.ts +4 -1
  88. package/dist/components/molecules/FontScalePicker.svelte +9 -4
  89. package/dist/components/molecules/IconButton.svelte +32 -7
  90. package/dist/components/molecules/IconButton.svelte.d.ts +9 -1
  91. package/dist/components/molecules/KeyValue.svelte +4 -1
  92. package/dist/components/molecules/KeyValue.svelte.d.ts +2 -1
  93. package/dist/components/molecules/Menu.svelte +50 -5
  94. package/dist/components/molecules/Menu.svelte.d.ts +10 -1
  95. package/dist/components/molecules/Meter.svelte +91 -0
  96. package/dist/components/molecules/Meter.svelte.d.ts +29 -0
  97. package/dist/components/molecules/Metric.svelte +134 -8
  98. package/dist/components/molecules/Metric.svelte.d.ts +20 -2
  99. package/dist/components/molecules/Modal.svelte +26 -12
  100. package/dist/components/molecules/Modal.svelte.d.ts +7 -1
  101. package/dist/components/molecules/Pagination.svelte +25 -1
  102. package/dist/components/molecules/Pagination.svelte.d.ts +9 -2
  103. package/dist/components/molecules/Popover.svelte +14 -3
  104. package/dist/components/molecules/Popover.svelte.d.ts +5 -0
  105. package/dist/components/molecules/Prose.svelte +140 -0
  106. package/dist/components/molecules/Prose.svelte.d.ts +13 -0
  107. package/dist/components/molecules/RadioGroup.svelte +42 -2
  108. package/dist/components/molecules/RadioGroup.svelte.d.ts +4 -1
  109. package/dist/components/molecules/SectionHeader.svelte +5 -2
  110. package/dist/components/molecules/SectionHeader.svelte.d.ts +1 -1
  111. package/dist/components/molecules/SegmentedControl.svelte +36 -1
  112. package/dist/components/molecules/SegmentedControl.svelte.d.ts +11 -1
  113. package/dist/components/molecules/SelectButton.svelte +64 -4
  114. package/dist/components/molecules/SelectButton.svelte.d.ts +18 -1
  115. package/dist/components/molecules/Tabs.svelte +21 -3
  116. package/dist/components/molecules/Tabs.svelte.d.ts +6 -0
  117. package/dist/components/molecules/Timestamp.svelte +13 -6
  118. package/dist/components/molecules/Timestamp.svelte.d.ts +4 -0
  119. package/dist/components/molecules/Toaster.svelte +54 -38
  120. package/dist/components/molecules/Toaster.svelte.d.ts +5 -1
  121. package/dist/components/molecules/Toggle.svelte +2 -1
  122. package/dist/components/molecules/Toggle.svelte.d.ts +2 -1
  123. package/dist/components/molecules/Tooltip.svelte +32 -5
  124. package/dist/components/molecules/Tooltip.svelte.d.ts +9 -0
  125. package/dist/components/molecules/Truncate.svelte +156 -18
  126. package/dist/components/molecules/Truncate.svelte.d.ts +17 -1
  127. package/dist/components/molecules/WorkingDir.svelte +6 -0
  128. package/dist/components/molecules/WorkingDir.svelte.d.ts +2 -0
  129. package/dist/components/organisms/DataTable.svelte +38 -10
  130. package/dist/components/organisms/DataTable.svelte.d.ts +4 -0
  131. package/dist/components/organisms/FilterSearchBar.svelte +9 -2
  132. package/dist/components/organisms/FilterSearchBar.svelte.d.ts +4 -1
  133. package/dist/index.d.ts +10 -2
  134. package/dist/index.js +8 -1
  135. package/dist/size.d.ts +2 -0
  136. package/dist/size.js +1 -0
  137. package/dist/styles/tokens.css +3 -0
  138. package/dist/timestamp.d.ts +2 -2
  139. package/dist/timestamp.js +4 -4
  140. package/dist/tone.d.ts +1 -1
  141. package/dist/tone.js +1 -1
  142. package/dist/truncate.d.ts +13 -1
  143. package/dist/truncate.js +39 -0
  144. package/package.json +5 -3
@@ -6,6 +6,8 @@
6
6
  description?: string;
7
7
  note?: string;
8
8
  disabled?: boolean;
9
+ /** `swatch` variant: any CSS colour (or var()). */
10
+ color?: string;
9
11
  }
10
12
  </script>
11
13
 
@@ -30,7 +32,8 @@
30
32
  value?: string;
31
33
  name?: string;
32
34
  label: string;
33
- variant?: 'list' | 'rows';
35
+ /** `swatch` renders each option as a colour chip with the label as its name. */
36
+ variant?: 'list' | 'rows' | 'swatch';
34
37
  action?: Snippet<[RadioOption]>;
35
38
  below?: Snippet<[RadioOption]>;
36
39
  class?: string;
@@ -42,11 +45,17 @@
42
45
  aria-label={label}
43
46
  class="radio-group {klass}"
44
47
  class:rows={variant === 'rows'}
48
+ class:swatches={variant === 'swatch'}
45
49
  data-tsu="RadioGroup"
46
50
  data-variant={variant}
47
51
  >
48
52
  {#each options as o (o.value)}
49
- {#if variant === 'rows'}
53
+ {#if variant === 'swatch'}
54
+ <label class="swatch" class:disabled={o.disabled} title={o.label}>
55
+ <input type="radio" {name} value={o.value} bind:group={value} disabled={o.disabled} aria-label={o.label} />
56
+ <span class="swatch-chip" aria-hidden="true" style="--swatch: {o.color ?? 'var(--accent)'}"></span>
57
+ </label>
58
+ {:else if variant === 'rows'}
50
59
  <div class="row-wrap">
51
60
  <div class="row" class:selected={o.value === value} class:disabled={o.disabled}>
52
61
  <label class="radio">
@@ -215,4 +224,35 @@
215
224
  .below:empty {
216
225
  display: none;
217
226
  }
227
+ .swatches {
228
+ flex-direction: row;
229
+ flex-wrap: wrap;
230
+ gap: var(--sp-2);
231
+ }
232
+ .swatch {
233
+ position: relative;
234
+ display: inline-flex;
235
+ cursor: pointer;
236
+ }
237
+ .swatch.disabled {
238
+ opacity: 0.45;
239
+ cursor: not-allowed;
240
+ }
241
+ .swatch-chip {
242
+ width: 1.6rem;
243
+ height: 1.6rem;
244
+ border-radius: var(--r-pill);
245
+ background: var(--swatch);
246
+ border: 2px solid transparent;
247
+ box-shadow: 0 0 0 1px var(--border-strong);
248
+ transition: box-shadow 0.12s var(--ease);
249
+ }
250
+ .swatch input:checked + .swatch-chip {
251
+ border-color: var(--bg);
252
+ box-shadow: 0 0 0 2px var(--accent);
253
+ }
254
+ .swatch input:focus-visible + .swatch-chip {
255
+ outline: var(--focus-ring);
256
+ outline-offset: 2px;
257
+ }
218
258
  </style>
@@ -5,6 +5,8 @@ export interface RadioOption {
5
5
  description?: string;
6
6
  note?: string;
7
7
  disabled?: boolean;
8
+ /** `swatch` variant: any CSS colour (or var()). */
9
+ color?: string;
8
10
  }
9
11
  import type { Snippet } from 'svelte';
10
12
  type $$ComponentProps = {
@@ -12,7 +14,8 @@ type $$ComponentProps = {
12
14
  value?: string;
13
15
  name?: string;
14
16
  label: string;
15
- variant?: 'list' | 'rows';
17
+ /** `swatch` renders each option as a colour chip with the label as its name. */
18
+ variant?: 'list' | 'rows' | 'swatch';
16
19
  action?: Snippet<[RadioOption]>;
17
20
  below?: Snippet<[RadioOption]>;
18
21
  class?: string;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { canonicalTone, type Tone } from '../../tone';
2
3
  // Section / group header: heading + optional subtitle, icon, count, hue
3
4
  // swatch and right-aligned actions on one wrapping row. `divider` underlines
4
5
  // it, `sticky` pins it under the app header and publishes its height as
@@ -11,7 +12,6 @@
11
12
  import Icon, { type IconName } from '../atoms/Icon.svelte';
12
13
  import Text from '../atoms/Text.svelte';
13
14
 
14
- type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
15
15
  type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
16
16
 
17
17
  let {
@@ -80,7 +80,7 @@
80
80
  bind:this={el}
81
81
  bind:offsetHeight={height}
82
82
  data-tsu="SectionHeader"
83
- class="section-header sh-{tone} {klass}"
83
+ class="section-header sh-{canonicalTone(tone)} {klass}"
84
84
  class:sh-divider={divider}
85
85
  class:sh-group={variant === 'group'}
86
86
  class:sh-sticky={sticky}
@@ -150,6 +150,9 @@
150
150
  .sh-info {
151
151
  --sh-tone: var(--info);
152
152
  }
153
+ .sh-accent {
154
+ --sh-tone: var(--accent);
155
+ }
153
156
  .sh-row {
154
157
  display: flex;
155
158
  align-items: center;
@@ -1,6 +1,6 @@
1
+ import { type Tone } from '../../tone';
1
2
  import type { Snippet } from 'svelte';
2
3
  import { type IconName } from '../atoms/Icon.svelte';
3
- type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
4
4
  type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
5
5
  type $$ComponentProps = {
6
6
  title?: string;
@@ -13,6 +13,7 @@
13
13
  </script>
14
14
 
15
15
  <script lang="ts">
16
+ import type { ControlSize } from '../../size';
16
17
  // Single-select segmented control. One token-driven row of options covering
17
18
  // the two repeated redesign controls:
18
19
  // • filter pills — label + optional count badge ("All 742 / Missing 120")
@@ -27,11 +28,15 @@
27
28
  let {
28
29
  options,
29
30
  value = $bindable(),
31
+ onchange,
32
+ justify,
33
+ wrap = false,
30
34
  // `icon` packs the segments into compact square buttons (label hidden as an
31
35
  // accessible name only); `pill` keeps the labelled filter-pill layout.
32
36
  variant = 'pill',
33
37
  size = 'md',
34
38
  control = false,
39
+ box = false,
35
40
  label = 'Options',
36
41
  // `mobile` hides segment labels below the mobile breakpoint, collapsing
37
42
  // labelled segments to centered icon-only squares (aria-label preserved).
@@ -46,11 +51,20 @@
46
51
  }: {
47
52
  options: SegmentOption[];
48
53
  value?: string;
54
+ /** Fires after a selection (click or keyboard). */
55
+ onchange?: (value: string) => void;
56
+ /** Pin the control to the start/end of a flex row. */
57
+ justify?: 'start' | 'end';
58
+ /** Allow segments to wrap onto multiple lines. */
59
+ wrap?: boolean;
49
60
  variant?: 'pill' | 'icon';
50
- size?: 'sm' | 'md';
61
+ size?: ControlSize;
51
62
  /** Adopt the shared `--control-height` toolbar contract (like Popover/Button
52
63
  * `control`) so the whole segmented control height-matches its siblings. */
53
64
  control?: boolean;
65
+ /** Adopt `--control-height-default` so an icon toggle lines up with
66
+ * `Button square` / `box` siblings. */
67
+ box?: boolean;
54
68
  label?: string;
55
69
  collapseLabels?: 'never' | 'mobile' | 'container';
56
70
  /** Single non-wrapping row that scrolls horizontally (scrollbar hidden). */
@@ -75,6 +89,7 @@
75
89
  function select(val: string, focus = false) {
76
90
  if (options.find((o) => o.value === val)?.disabled) return;
77
91
  value = val;
92
+ onchange?.(val);
78
93
  if (focus) {
79
94
  queueMicrotask(() =>
80
95
  listEl?.querySelector<HTMLButtonElement>(`#${baseId}-${val}`)?.focus()
@@ -97,6 +112,10 @@
97
112
  tabindex="-1"
98
113
  class="seg seg-{variant} seg-{size} {klass}"
99
114
  class:seg-control={control}
115
+ class:seg-box={box}
116
+ class:seg-wrap={wrap}
117
+ class:justify-start={justify === 'start'}
118
+ class:justify-end={justify === 'end'}
100
119
  class:seg-collapse-mobile={collapseLabels === 'mobile'}
101
120
  class:collapse-container={collapseLabels === 'container'}
102
121
  class:seg-scroll={scroll}
@@ -143,6 +162,15 @@
143
162
  .seg-icon {
144
163
  border-radius: var(--r-md);
145
164
  }
165
+ .seg-wrap {
166
+ flex-wrap: wrap;
167
+ }
168
+ .justify-start {
169
+ margin-inline-end: auto;
170
+ }
171
+ .justify-end {
172
+ margin-inline-start: auto;
173
+ }
146
174
  .seg-scroll {
147
175
  flex: 1 0 100%;
148
176
  flex-wrap: nowrap;
@@ -206,6 +234,13 @@
206
234
  .seg-control .seg-item {
207
235
  height: 100%;
208
236
  }
237
+ .seg-box {
238
+ height: var(--control-height-default);
239
+ min-height: var(--control-height-default);
240
+ }
241
+ .seg-box .seg-item {
242
+ height: 100%;
243
+ }
209
244
  .seg-icon .seg-item {
210
245
  gap: 0;
211
246
  padding: 0.35rem;
@@ -9,15 +9,25 @@ export interface SegmentOption {
9
9
  /** Greyed out, not selectable, skipped by keyboard navigation. */
10
10
  disabled?: boolean;
11
11
  }
12
+ import type { ControlSize } from '../../size';
12
13
  import type { Snippet } from 'svelte';
13
14
  type $$ComponentProps = {
14
15
  options: SegmentOption[];
15
16
  value?: string;
17
+ /** Fires after a selection (click or keyboard). */
18
+ onchange?: (value: string) => void;
19
+ /** Pin the control to the start/end of a flex row. */
20
+ justify?: 'start' | 'end';
21
+ /** Allow segments to wrap onto multiple lines. */
22
+ wrap?: boolean;
16
23
  variant?: 'pill' | 'icon';
17
- size?: 'sm' | 'md';
24
+ size?: ControlSize;
18
25
  /** Adopt the shared `--control-height` toolbar contract (like Popover/Button
19
26
  * `control`) so the whole segmented control height-matches its siblings. */
20
27
  control?: boolean;
28
+ /** Adopt `--control-height-default` so an icon toggle lines up with
29
+ * `Button square` / `box` siblings. */
30
+ box?: boolean;
21
31
  label?: string;
22
32
  collapseLabels?: 'never' | 'mobile' | 'container';
23
33
  /** Single non-wrapping row that scrolls horizontally (scrollbar hidden). */
@@ -4,13 +4,19 @@
4
4
  // affordance. Used by the header font-size + theme pickers and the drawer
5
5
  // font-size picker (CCT-250 #5 / CCT-297 #11). Composes the Button primitive
6
6
  // for the base look.
7
+ import type { Snippet } from 'svelte';
7
8
  import Button from '../atoms/Button.svelte';
9
+ import Icon, { type IconName } from '../atoms/Icon.svelte';
8
10
  import Select from '../atoms/Select.svelte';
9
11
 
10
12
  type Option = { value: string; label: string };
11
13
 
12
14
  let {
13
15
  glyph,
16
+ icon,
17
+ iconChildren,
18
+ variant = 'square',
19
+ active = false,
14
20
  label,
15
21
  title,
16
22
  value,
@@ -19,12 +25,30 @@
19
25
  onchange,
20
26
  box,
21
27
  hitArea = 'auto',
28
+ grow = false,
29
+ shrink = true,
30
+ block = false,
22
31
  class: klass = '',
23
32
  ...rest
24
33
  }: {
25
34
  // Visible content of the button (a letter like "A" or an emoji icon).
26
- glyph: string;
35
+ glyph?: string;
36
+ /** Registry glyph, used instead of `glyph`. */
37
+ icon?: IconName;
38
+ /** Raw SVG children for an off-registry glyph. */
39
+ iconChildren?: Snippet;
40
+ /** `row` is a full-width, left-aligned row showing `label` beside the glyph
41
+ * (overflow menus); `square` is the compact icon box. */
42
+ variant?: 'square' | 'row';
43
+ /** Accent tint when the picker's current value is "on". */
44
+ active?: boolean;
27
45
  label: string;
46
+ /** Fill the free space of a flex row (`flex: 1 1 0`). */
47
+ grow?: boolean;
48
+ /** `false` pins the box (`flex: none`) so a flex row cannot squeeze it. */
49
+ shrink?: boolean;
50
+ /** Full-width block. */
51
+ block?: boolean;
28
52
  title?: string;
29
53
  value: string;
30
54
  // Flat option list. Ignored when `groups` is given.
@@ -43,9 +67,21 @@
43
67
 
44
68
  <!-- The wrapper (owned here, so styled scoped) is the positioning context that
45
69
  clips the transparent overlaid <select>; the icon Button shows through. -->
46
- <span class="select-button {klass}" data-tsu="SelectButton" {...rest}>
47
- <Button variant="ghost" icon {box} {hitArea} {title} aria-label={label}>
48
- <span aria-hidden="true">{glyph}</span>
70
+ <span
71
+ class="select-button {klass}"
72
+ class:row={variant === 'row'}
73
+ class:active
74
+ data-tsu="SelectButton"
75
+ {...rest}
76
+ class:grow={grow}
77
+ class:no-shrink={!shrink}
78
+ class:block={block || variant === 'row'}
79
+ >
80
+ <Button variant="ghost" icon={variant === 'square'} block={variant === 'row'} {box} {hitArea} {title} aria-label={label}>
81
+ <span aria-hidden="true" class="sb-glyph">
82
+ {#if iconChildren}<Icon>{@render iconChildren()}</Icon>{:else if icon}<Icon name={icon} />{:else}{glyph}{/if}
83
+ </span>
84
+ {#if variant === 'row'}<span aria-hidden="true" class="sb-label">{label}</span>{/if}
49
85
  <Select
50
86
  variant="ghost"
51
87
  aria-label={label}
@@ -70,6 +106,17 @@
70
106
  </span>
71
107
 
72
108
  <style>
109
+ .grow {
110
+ flex: 1 1 0;
111
+ min-width: 0;
112
+ }
113
+ .no-shrink {
114
+ flex: none;
115
+ }
116
+ .block {
117
+ display: flex;
118
+ width: 100%;
119
+ }
73
120
  .select-button {
74
121
  position: relative;
75
122
  display: inline-flex;
@@ -77,4 +124,17 @@
77
124
  border-radius: var(--r-md);
78
125
  font-weight: var(--fw-bold);
79
126
  }
127
+ .select-button.active {
128
+ color: var(--accent);
129
+ }
130
+ .select-button.row :global(.btn) {
131
+ justify-content: flex-start;
132
+ gap: var(--sp-2);
133
+ width: 100%;
134
+ font-weight: var(--fw-medium);
135
+ }
136
+ .sb-glyph {
137
+ display: inline-flex;
138
+ align-items: center;
139
+ }
80
140
  </style>
@@ -1,10 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type IconName } from '../atoms/Icon.svelte';
1
3
  type Option = {
2
4
  value: string;
3
5
  label: string;
4
6
  };
5
7
  type $$ComponentProps = {
6
- glyph: string;
8
+ glyph?: string;
9
+ /** Registry glyph, used instead of `glyph`. */
10
+ icon?: IconName;
11
+ /** Raw SVG children for an off-registry glyph. */
12
+ iconChildren?: Snippet;
13
+ /** `row` is a full-width, left-aligned row showing `label` beside the glyph
14
+ * (overflow menus); `square` is the compact icon box. */
15
+ variant?: 'square' | 'row';
16
+ /** Accent tint when the picker's current value is "on". */
17
+ active?: boolean;
7
18
  label: string;
19
+ /** Fill the free space of a flex row (`flex: 1 1 0`). */
20
+ grow?: boolean;
21
+ /** `false` pins the box (`flex: none`) so a flex row cannot squeeze it. */
22
+ shrink?: boolean;
23
+ /** Full-width block. */
24
+ block?: boolean;
8
25
  title?: string;
9
26
  value: string;
10
27
  options?: Option[];
@@ -5,10 +5,13 @@
5
5
  icon?: import('../atoms/Icon.svelte').IconName;
6
6
  /** Greyed out, not selectable, skipped by keyboard navigation. */
7
7
  disabled?: boolean;
8
+ /** Trailing count badge. */
9
+ count?: number | string;
8
10
  }
9
11
  </script>
10
12
 
11
13
  <script lang="ts">
14
+ import Badge from '../atoms/Badge.svelte';
12
15
  // WAI-ARIA tabs. A `tablist` of `tab`s controlling a single `tabpanel`.
13
16
  // Follows the automatic-activation pattern: ←/→ (and Home/End) move selection
14
17
  // and reveal the panel in one step; roving tabindex keeps exactly one tab in
@@ -21,12 +24,20 @@
21
24
  tabs,
22
25
  value = $bindable(),
23
26
  label = 'Tabs',
24
- panel
27
+ panel,
28
+ class: klass = '',
29
+ style: styleProp = '',
30
+ panelClass = '',
31
+ panelPadding = 'md',
25
32
  }: {
26
33
  tabs: TabItem[];
27
34
  value?: string;
28
35
  label?: string;
29
36
  panel: Snippet<[string]>;
37
+ class?: string;
38
+ style?: string;
39
+ panelClass?: string;
40
+ panelPadding?: 'none' | 'sm' | 'md';
30
41
  } = $props();
31
42
 
32
43
  // Default to the first selectable tab when no value is supplied.
@@ -73,7 +84,7 @@
73
84
  }
74
85
  </script>
75
86
 
76
- <div class="tabs" data-tsu="Tabs">
87
+ <div class="tabs {klass}" style={styleProp} data-tsu="Tabs">
77
88
  <div bind:this={listEl} role="tablist" aria-label={label} tabindex="-1" class="tablist" {onkeydown}>
78
89
  {#each tabs as t (t.id)}
79
90
  <button
@@ -90,10 +101,11 @@
90
101
  >
91
102
  {#if t.icon}<Icon name={t.icon} />{/if}
92
103
  <span>{t.label}</span>
104
+ {#if t.count !== undefined}<Badge size="sm" tone={value === t.id ? 'accent' : 'neutral'}>{t.count}</Badge>{/if}
93
105
  </button>
94
106
  {/each}
95
107
  </div>
96
- <div role="tabpanel" id="{baseId}-panel" tabindex="0" class="tabpanel">
108
+ <div role="tabpanel" id="{baseId}-panel" tabindex="0" class="tabpanel {panelClass}" class:pad-none={panelPadding === 'none'} class:pad-sm={panelPadding === 'sm'}>
97
109
  {#if value !== undefined}{@render panel(value)}{/if}
98
110
  </div>
99
111
  </div>
@@ -139,6 +151,12 @@
139
151
  .tabpanel {
140
152
  padding-top: var(--sp-4);
141
153
  }
154
+ .tabpanel.pad-sm {
155
+ padding-top: var(--sp-2);
156
+ }
157
+ .tabpanel.pad-none {
158
+ padding-top: 0;
159
+ }
142
160
  .tabpanel:focus-visible {
143
161
  outline: 2px solid var(--accent);
144
162
  outline-offset: 2px;
@@ -4,6 +4,8 @@ export interface TabItem {
4
4
  icon?: import('../atoms/Icon.svelte').IconName;
5
5
  /** Greyed out, not selectable, skipped by keyboard navigation. */
6
6
  disabled?: boolean;
7
+ /** Trailing count badge. */
8
+ count?: number | string;
7
9
  }
8
10
  import type { Snippet } from 'svelte';
9
11
  type $$ComponentProps = {
@@ -11,6 +13,10 @@ type $$ComponentProps = {
11
13
  value?: string;
12
14
  label?: string;
13
15
  panel: Snippet<[string]>;
16
+ class?: string;
17
+ style?: string;
18
+ panelClass?: string;
19
+ panelPadding?: 'none' | 'sm' | 'md';
14
20
  };
15
21
  declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "value">;
16
22
  type Tabs = ReturnType<typeof Tabs>;
@@ -39,7 +39,10 @@
39
39
  mono = false,
40
40
  tone = 'muted',
41
41
  size,
42
- tickMs = 30_000
42
+ short = false,
43
+ tickMs = 30_000,
44
+ class: klass = '',
45
+ style: styleProp = '',
43
46
  }: {
44
47
  /** The instant: a Date, epoch milliseconds, or an ISO/parseable string.
45
48
  * null/undefined (or unparseable input) renders an inert "—". */
@@ -68,8 +71,12 @@
68
71
  /** Font size, mirroring <Text> sizes (maps to --fs-* tokens). Omit to
69
72
  * inherit the surrounding size. */
70
73
  size?: Size;
74
+ /** Relative mode without the "ago" suffix ("6m", "2h", "3d"). */
75
+ short?: boolean;
71
76
  /** How often relative mode re-renders so "3m ago" stays fresh. */
72
77
  tickMs?: number;
78
+ class?: string;
79
+ style?: string;
73
80
  } = $props();
74
81
 
75
82
  // User's in-popover choice overrides the `mode` prop; until they pick, we
@@ -88,11 +95,11 @@
88
95
  });
89
96
 
90
97
  const date = $derived(toDate(value));
91
- const label = $derived(formatTimestamp(value, current, now, utc, precision));
98
+ const label = $derived(formatTimestamp(value, current, now, utc, precision, short));
92
99
  // datetime= wants a valid ISO string; omit it entirely on bad input.
93
100
  const machine = $derived(date ? toISO(date) : undefined);
94
101
  const showPopover = $derived(details || selectable);
95
- const cls = $derived(`ts tone-${tone}${mono ? ' mono' : ''}`);
102
+ const cls = $derived(`ts tone-${tone}${mono ? ' mono' : ''} ${klass}`);
96
103
  // Size maps straight onto the --fs-* scale; null leaves font-size unset so the
97
104
  // timestamp inherits its surrounding run.
98
105
  const sizeVar = $derived(size ? `var(--fs-${size})` : null);
@@ -121,11 +128,11 @@
121
128
 
122
129
  {#if !date}
123
130
  <!-- Unparseable input: degrade to an inert dash rather than empty text. -->
124
- <time class="{cls} ts-invalid" data-tsu="Timestamp" style:font-size={sizeVar}>—</time>
131
+ <time class="{cls} ts-invalid" data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar}>—</time>
125
132
  {:else if showPopover}
126
133
  <Popover label="Timestamp details" placement="bottom-start" bare>
127
134
  {#snippet trigger()}
128
- <time class="{cls} ts-trigger" data-tsu="Timestamp" style:font-size={sizeVar} datetime={machine}>{label}</time>
135
+ <time class="{cls} ts-trigger" data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar} datetime={machine}>{label}</time>
129
136
  {/snippet}
130
137
  <div class="ts-panel">
131
138
  {#if selectable}
@@ -152,7 +159,7 @@
152
159
  </div>
153
160
  </Popover>
154
161
  {:else}
155
- <time class={cls} data-tsu="Timestamp" style:font-size={sizeVar} datetime={machine}>{label}</time>
162
+ <time class={cls} data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar} datetime={machine}>{label}</time>
156
163
  {/if}
157
164
 
158
165
  <style>
@@ -29,8 +29,12 @@ type $$ComponentProps = {
29
29
  /** Font size, mirroring <Text> sizes (maps to --fs-* tokens). Omit to
30
30
  * inherit the surrounding size. */
31
31
  size?: Size;
32
+ /** Relative mode without the "ago" suffix ("6m", "2h", "3d"). */
33
+ short?: boolean;
32
34
  /** How often relative mode re-renders so "3m ago" stays fresh. */
33
35
  tickMs?: number;
36
+ class?: string;
37
+ style?: string;
34
38
  };
35
39
  declare const Timestamp: import("svelte").Component<$$ComponentProps, {}, "">;
36
40
  type Timestamp = ReturnType<typeof Timestamp>;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
- // Renders the toast queue in a bottom-centered, polite live region. Mount once
3
- // near the app root. The stack lives in a manual popover so it paints in the
2
+ // Renders the toast queue bottom-centered in two live regions: polite for
3
+ // ok/info/neutral, assertive (role=alert) for errors. Mount once near the
4
+ // app root. The stack lives in a manual popover so it paints in the
4
5
  // top layer above an open Modal <dialog>; without the Popover API the
5
6
  // attribute is ignored and the fixed/z-index positioning applies instead.
6
7
  import { toasts } from '../../stores/toast.svelte';
@@ -8,7 +9,11 @@
8
9
  import Card from '../atoms/Card.svelte';
9
10
  import Icon from '../atoms/Icon.svelte';
10
11
 
12
+ let { class: klass = '', style: styleProp = '' }: { class?: string; style?: string } = $props();
13
+
11
14
  let el: HTMLDivElement | undefined = $state();
15
+ const polite = $derived(toasts.items.filter((t) => t.tone !== 'error'));
16
+ const assertive = $derived(toasts.items.filter((t) => t.tone === 'error'));
12
17
 
13
18
  $effect(() => {
14
19
  if (!el || typeof el.showPopover !== 'function') return;
@@ -19,46 +24,51 @@
19
24
  });
20
25
  </script>
21
26
 
22
- <div
23
- class="toaster"
24
- bind:this={el}
25
- popover="manual"
26
- role="status"
27
- aria-live="polite"
28
- aria-relevant="additions"
29
- data-tsu="Toaster"
30
- >
31
- {#each toasts.items as t (t.id)}
32
- {#if t.action}
33
- <Card
34
- as="div"
35
- surface="raised"
36
- class="toast"
37
- tone={t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
38
- >
39
- <button type="button" class="toast-text" onclick={() => toasts.dismiss(t.id)}>
40
- {#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
41
- <span class="msg">{t.message}</span>
42
- </button>
43
- <Button size="sm" variant="ghost" loading={t.pending} onclick={() => toasts.act(t.id)}>
44
- {t.action.label}
45
- </Button>
46
- </Card>
47
- {:else}
48
- <Card
49
- as="button"
50
- surface="raised"
51
- class="toast"
52
- tone={t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
27
+ {#snippet toast(t: (typeof toasts.items)[number])}
28
+ {@const tone = t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
29
+ {#if t.action}
30
+ <Card as="div" surface="raised" class="toast" {tone}>
31
+ <button
53
32
  type="button"
33
+ class="toast-text"
34
+ aria-label="Dismiss: {t.message}"
54
35
  onclick={() => toasts.dismiss(t.id)}
55
36
  >
56
- {#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
37
+ <span aria-hidden="true" class="toast-icon">
38
+ {#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
39
+ </span>
57
40
  <span class="msg">{t.message}</span>
58
- <Icon name="x" />
59
- </Card>
60
- {/if}
61
- {/each}
41
+ </button>
42
+ <Button size="sm" variant="ghost" loading={t.pending} onclick={() => toasts.act(t.id)}>
43
+ {t.action.label}
44
+ </Button>
45
+ </Card>
46
+ {:else}
47
+ <Card
48
+ as="button"
49
+ surface="raised"
50
+ class="toast"
51
+ {tone}
52
+ type="button"
53
+ aria-label="Dismiss: {t.message}"
54
+ onclick={() => toasts.dismiss(t.id)}
55
+ >
56
+ <span aria-hidden="true" class="toast-icon">
57
+ {#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
58
+ </span>
59
+ <span class="msg">{t.message}</span>
60
+ <span aria-hidden="true" class="toast-icon"><Icon name="x" /></span>
61
+ </Card>
62
+ {/if}
63
+ {/snippet}
64
+
65
+ <div class="toaster {klass}" style={styleProp} bind:this={el} popover="manual" data-tsu="Toaster">
66
+ <div class="region" role="status" aria-live="polite">
67
+ {#each polite as t (t.id)}{@render toast(t)}{/each}
68
+ </div>
69
+ <div class="region" role="alert" aria-live="assertive">
70
+ {#each assertive as t (t.id)}{@render toast(t)}{/each}
71
+ </div>
62
72
  </div>
63
73
 
64
74
  <style>
@@ -84,6 +94,12 @@
84
94
  .toaster:not(:popover-open) {
85
95
  display: none;
86
96
  }
97
+ .region {
98
+ display: contents;
99
+ }
100
+ .toast-icon {
101
+ display: inline-flex;
102
+ }
87
103
  .toaster :global(.toast) {
88
104
  pointer-events: auto;
89
105
  display: flex;