@dorsk/tsumikit 0.38.1 → 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 (143) 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/IconButton.svelte +32 -7
  89. package/dist/components/molecules/IconButton.svelte.d.ts +9 -1
  90. package/dist/components/molecules/KeyValue.svelte +4 -1
  91. package/dist/components/molecules/KeyValue.svelte.d.ts +2 -1
  92. package/dist/components/molecules/Menu.svelte +50 -5
  93. package/dist/components/molecules/Menu.svelte.d.ts +10 -1
  94. package/dist/components/molecules/Meter.svelte +91 -0
  95. package/dist/components/molecules/Meter.svelte.d.ts +29 -0
  96. package/dist/components/molecules/Metric.svelte +134 -8
  97. package/dist/components/molecules/Metric.svelte.d.ts +20 -2
  98. package/dist/components/molecules/Modal.svelte +26 -12
  99. package/dist/components/molecules/Modal.svelte.d.ts +7 -1
  100. package/dist/components/molecules/Pagination.svelte +25 -1
  101. package/dist/components/molecules/Pagination.svelte.d.ts +9 -2
  102. package/dist/components/molecules/Popover.svelte +14 -3
  103. package/dist/components/molecules/Popover.svelte.d.ts +5 -0
  104. package/dist/components/molecules/Prose.svelte +140 -0
  105. package/dist/components/molecules/Prose.svelte.d.ts +13 -0
  106. package/dist/components/molecules/RadioGroup.svelte +42 -2
  107. package/dist/components/molecules/RadioGroup.svelte.d.ts +4 -1
  108. package/dist/components/molecules/SectionHeader.svelte +5 -2
  109. package/dist/components/molecules/SectionHeader.svelte.d.ts +1 -1
  110. package/dist/components/molecules/SegmentedControl.svelte +36 -1
  111. package/dist/components/molecules/SegmentedControl.svelte.d.ts +11 -1
  112. package/dist/components/molecules/SelectButton.svelte +64 -4
  113. package/dist/components/molecules/SelectButton.svelte.d.ts +18 -1
  114. package/dist/components/molecules/Tabs.svelte +21 -3
  115. package/dist/components/molecules/Tabs.svelte.d.ts +6 -0
  116. package/dist/components/molecules/Timestamp.svelte +13 -6
  117. package/dist/components/molecules/Timestamp.svelte.d.ts +4 -0
  118. package/dist/components/molecules/Toaster.svelte +54 -38
  119. package/dist/components/molecules/Toaster.svelte.d.ts +5 -1
  120. package/dist/components/molecules/Toggle.svelte +2 -1
  121. package/dist/components/molecules/Toggle.svelte.d.ts +2 -1
  122. package/dist/components/molecules/Tooltip.svelte +32 -5
  123. package/dist/components/molecules/Tooltip.svelte.d.ts +9 -0
  124. package/dist/components/molecules/Truncate.svelte +156 -18
  125. package/dist/components/molecules/Truncate.svelte.d.ts +17 -1
  126. package/dist/components/molecules/WorkingDir.svelte +6 -0
  127. package/dist/components/molecules/WorkingDir.svelte.d.ts +2 -0
  128. package/dist/components/organisms/DataTable.svelte +38 -10
  129. package/dist/components/organisms/DataTable.svelte.d.ts +4 -0
  130. package/dist/components/organisms/FilterSearchBar.svelte +9 -2
  131. package/dist/components/organisms/FilterSearchBar.svelte.d.ts +4 -1
  132. package/dist/index.d.ts +10 -2
  133. package/dist/index.js +8 -1
  134. package/dist/size.d.ts +2 -0
  135. package/dist/size.js +1 -0
  136. package/dist/styles/tokens.css +3 -0
  137. package/dist/timestamp.d.ts +2 -2
  138. package/dist/timestamp.js +4 -4
  139. package/dist/tone.d.ts +1 -1
  140. package/dist/tone.js +1 -1
  141. package/dist/truncate.d.ts +13 -1
  142. package/dist/truncate.js +39 -0
  143. package/package.json +5 -3
@@ -1,12 +1,14 @@
1
+ import { type Tone } from '../../tone';
1
2
  import type { Snippet } from 'svelte';
2
3
  import { type IconName } from '../atoms/Icon.svelte';
3
4
  type $$ComponentProps = {
4
5
  title?: string;
5
6
  description?: string | Snippet;
6
7
  icon?: IconName;
7
- tone?: 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
8
+ tone?: Tone;
8
9
  actionLabel?: string;
9
10
  onAction?: () => void;
11
+ /** @deprecated use `size="compact"`; `size` wins when both are set. */
10
12
  compact?: boolean;
11
13
  size?: 'inline' | 'compact' | 'default';
12
14
  loading?: boolean;
@@ -17,6 +17,7 @@
17
17
  layout = 'stack',
18
18
  labelWidth,
19
19
  grow = false,
20
+ shrink = true,
20
21
  class: klass = '',
21
22
  children
22
23
  }: {
@@ -29,6 +30,8 @@
29
30
  labelWidth?: string;
30
31
  /** Fill the available width of a flex/Cluster row (flex: 1). */
31
32
  grow?: boolean;
33
+ /** `false` pins the field (`flex: none`) in a row. */
34
+ shrink?: boolean;
32
35
  class?: string;
33
36
  children?: Snippet;
34
37
  } = $props();
@@ -54,7 +57,7 @@
54
57
  });
55
58
  </script>
56
59
 
57
- <div class="field {klass}" class:field-grow={grow} class:field-inline={layout === 'inline'} data-tsu="Field">
60
+ <div class="field {klass}" class:field-grow={grow} class:field-no-shrink={!shrink} class:field-inline={layout === 'inline'} data-tsu="Field">
58
61
  {#if label}
59
62
  <label class="label" for={forId} style:width={labelWidth}>{label}</label>
60
63
  {/if}
@@ -75,6 +78,9 @@
75
78
  flex: 1 1 0;
76
79
  min-width: 0;
77
80
  }
81
+ .field-no-shrink {
82
+ flex: none;
83
+ }
78
84
  .field-inline {
79
85
  flex-direction: row;
80
86
  align-items: center;
@@ -9,6 +9,8 @@ type $$ComponentProps = {
9
9
  labelWidth?: string;
10
10
  /** Fill the available width of a flex/Cluster row (flex: 1). */
11
11
  grow?: boolean;
12
+ /** `false` pins the field (`flex: none`) in a row. */
13
+ shrink?: boolean;
12
14
  class?: string;
13
15
  children?: Snippet;
14
16
  };
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { ControlSize } from '../../size';
2
3
  // File-picker button. A real <input type="file"> visually hidden inside a
3
4
  // <label> styled as a button — so it's keyboard-focusable and works with zero
4
5
  // JS to open the dialog. Emits the chosen files via `onfiles`. Dependency-free.
@@ -38,7 +39,7 @@
38
39
  variant?: 'default' | 'primary' | 'ghost';
39
40
  /** Match the Button atom's sizes so a FileButton lines up with buttons in
40
41
  * the same row. */
41
- size?: 'sm' | 'md' | 'lg';
42
+ size?: ControlSize;
42
43
  /** Uniform `--control-height`, like Button's `control`, so it lines up in a
43
44
  * composer/toolbar row. */
44
45
  control?: boolean;
@@ -1,3 +1,4 @@
1
+ import type { ControlSize } from '../../size';
1
2
  import type { IconName } from '../atoms/Icon.svelte';
2
3
  type $$ComponentProps = {
3
4
  onfiles: (files: File[]) => void;
@@ -17,7 +18,7 @@ type $$ComponentProps = {
17
18
  variant?: 'default' | 'primary' | 'ghost';
18
19
  /** Match the Button atom's sizes so a FileButton lines up with buttons in
19
20
  * the same row. */
20
- size?: 'sm' | 'md' | 'lg';
21
+ size?: ControlSize;
21
22
  /** Uniform `--control-height`, like Button's `control`, so it lines up in a
22
23
  * composer/toolbar row. */
23
24
  control?: boolean;
@@ -23,6 +23,7 @@
23
23
  </script>
24
24
 
25
25
  <script lang="ts">
26
+ import type { ControlSize } from '../../size';
26
27
  // ───────────────────────────────────────────────────────────────────────
27
28
  // FilterInput primitive — the headless base of the structured search bar.
28
29
  //
@@ -64,7 +65,9 @@
64
65
  onchange,
65
66
  onsubmit,
66
67
  inline,
67
- below
68
+ below,
69
+ class: klass = '',
70
+ style: styleProp = '',
68
71
  }: {
69
72
  schema: Schema;
70
73
  /** The raw textual query (two-way bindable). */
@@ -81,7 +84,7 @@
81
84
  /** Leading icon; pass `null` to hide it. */
82
85
  icon?: IconName | null;
83
86
  /** `sm` renders a compact-toolbar bar (`--control-height-compact`). */
84
- size?: 'sm' | 'md';
87
+ size?: ControlSize;
85
88
  shape?: 'square' | 'pill';
86
89
  /** Theme-aware surface shade of the bar (mirrors Card). */
87
90
  surface?: 'base' | 'raised' | 'sunken';
@@ -110,6 +113,8 @@
110
113
  * passes its chip row here; single-field hosts simply omit it.
111
114
  */
112
115
  below?: Snippet<[FilterInputContext]>;
116
+ class?: string;
117
+ style?: string;
113
118
  } = $props();
114
119
 
115
120
  const field = getFieldContext();
@@ -306,7 +311,8 @@
306
311
  </script>
307
312
 
308
313
  <div
309
- class="fi"
314
+ class="fi {klass}"
315
+ style={styleProp}
310
316
  class:fi--sm={size === 'sm'}
311
317
  class:fi--pill={shape === 'pill'}
312
318
  class:surface-raised={surface === 'raised'}
@@ -436,6 +442,10 @@
436
442
  border-color: var(--accent);
437
443
  box-shadow: 0 0 0 3px var(--accent-dim);
438
444
  }
445
+ .fi__bar:has(:focus-visible) {
446
+ outline: var(--focus-ring);
447
+ outline-offset: var(--focus-ring-offset);
448
+ }
439
449
  .fi__icon {
440
450
  display: flex;
441
451
  color: var(--text-faint);
@@ -18,6 +18,7 @@ export interface FilterInputContext {
18
18
  /** Focus the underlying input. */
19
19
  focus: () => void;
20
20
  }
21
+ import type { ControlSize } from '../../size';
21
22
  import type { Snippet } from 'svelte';
22
23
  import { type IconName } from '../atoms/Icon.svelte';
23
24
  import { type Schema } from '../../query/schema';
@@ -37,7 +38,7 @@ type $$ComponentProps = {
37
38
  /** Leading icon; pass `null` to hide it. */
38
39
  icon?: IconName | null;
39
40
  /** `sm` renders a compact-toolbar bar (`--control-height-compact`). */
40
- size?: 'sm' | 'md';
41
+ size?: ControlSize;
41
42
  shape?: 'square' | 'pill';
42
43
  /** Theme-aware surface shade of the bar (mirrors Card). */
43
44
  surface?: 'base' | 'raised' | 'sunken';
@@ -66,6 +67,8 @@ type $$ComponentProps = {
66
67
  * passes its chip row here; single-field hosts simply omit it.
67
68
  */
68
69
  below?: Snippet<[FilterInputContext]>;
70
+ class?: string;
71
+ style?: string;
69
72
  };
70
73
  declare const FilterInput: import("svelte").Component<$$ComponentProps, {}, "value">;
71
74
  type FilterInput = ReturnType<typeof FilterInput>;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Tone } from '../../tone';
2
3
  import type { Snippet } from 'svelte';
3
4
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
5
  import Button from '../atoms/Button.svelte';
@@ -22,7 +23,7 @@
22
23
  // Forwarded to Button.
23
24
  shrink?: boolean;
24
25
  // Semantic state tint (forwarded to Button). Pairs well with `chip`.
25
- tone?: 'none' | 'accent' | 'info' | 'warn' | 'danger';
26
+ tone?: Tone | 'none';
26
27
  // Outlined `--box-lg` square icon-chip (header/toolbar actions).
27
28
  chip?: boolean;
28
29
  // Shared square box scale (`--box-xs/sm/md/lg`); default `md` is the classic
@@ -48,6 +49,13 @@
48
49
  // open-on-GitHub action. Forwarded to Button; `href` implies `as="a"`.
49
50
  as?: 'button' | 'a';
50
51
  href?: string;
52
+ /** Render `label` as visible text beside the glyph. `row` is a full-width,
53
+ * left-aligned menu-row form for overflow menus. */
54
+ showLabel?: boolean | 'row';
55
+ /** Rotate the glyph (refresh in flight). */
56
+ spin?: boolean;
57
+ /** Forwarded to Button: spinner, blocks clicks, aria-busy. */
58
+ loading?: boolean;
51
59
  class?: string;
52
60
  };
53
61
 
@@ -69,6 +77,9 @@
69
77
  inline = false,
70
78
  hoverDanger = false,
71
79
  pressed,
80
+ showLabel = false,
81
+ spin = false,
82
+ loading = false,
72
83
  disabled = false,
73
84
  onclick,
74
85
  class: klass = '',
@@ -94,36 +105,50 @@
94
105
  {box}
95
106
  {hitArea}
96
107
  {disabled}
108
+ {loading}
97
109
  {title}
98
110
  {onclick}
99
- icon={!inline && !chip}
111
+ icon={!inline && !chip && !showLabel}
100
112
  iconInline={inline}
113
+ block={showLabel === 'row'}
101
114
  {hoverDanger}
102
115
  aria-pressed={pressed}
103
- class={klass}
116
+ class="{showLabel ? 'ib-labelled' : ''} {showLabel === 'row' ? 'ib-row' : ''} {klass}"
104
117
  aria-label={label}
105
118
  >
106
119
  {#if children}
107
120
  {#if glyphCss}
108
- <span class="glyph" style="font-size: {glyphCss}"><Icon>{@render children()}</Icon></span>
121
+ <span class="glyph" style="font-size: {glyphCss}"><Icon {spin}>{@render children()}</Icon></span>
109
122
  {:else}
110
- <Icon {size}>{@render children()}</Icon>
123
+ <Icon {size} {spin}>{@render children()}</Icon>
111
124
  {/if}
112
125
  {:else if emoji}
113
126
  <span class="emoji" style="font-size: {emojiCss}" aria-hidden="true">{emoji}</span>
114
127
  {:else if icon}
115
128
  {#if glyphCss}
116
- <span class="glyph" style="font-size: {glyphCss}"><Icon name={icon} /></span>
129
+ <span class="glyph" style="font-size: {glyphCss}"><Icon name={icon} {spin} /></span>
117
130
  {:else}
118
- <Icon name={icon} {size} />
131
+ <Icon name={icon} {size} {spin} />
119
132
  {/if}
120
133
  {/if}
134
+ {#if showLabel}<span class="ib-label">{label}</span>{/if}
121
135
  </Button>
122
136
 
123
137
  <style>
124
138
  /* Off-registry glyph (emoji) rendered as text rather than an SVG. Sized off the
125
139
  `size` prop (×1.35, since an emoji reads small next to an SVG glyph of the
126
140
  same px) unless `glyphSize` is exact; centered so it shares the tap target. */
141
+ .ib-label {
142
+ white-space: nowrap;
143
+ }
144
+ :global(.btn.ib-labelled) {
145
+ gap: var(--sp-2);
146
+ }
147
+ :global(.btn.ib-row) {
148
+ justify-content: flex-start;
149
+ width: 100%;
150
+ text-align: left;
151
+ }
127
152
  .emoji,
128
153
  .glyph {
129
154
  display: inline-flex;
@@ -1,3 +1,4 @@
1
+ import type { Tone } from '../../tone';
1
2
  import type { Snippet } from 'svelte';
2
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
3
4
  import type { IconName } from '../atoms/Icon.svelte';
@@ -14,7 +15,7 @@ type IconButtonProps = HTMLButtonAttributes & {
14
15
  variant?: 'default' | 'primary' | 'ghost' | 'danger';
15
16
  pill?: boolean;
16
17
  shrink?: boolean;
17
- tone?: 'none' | 'accent' | 'info' | 'warn' | 'danger';
18
+ tone?: Tone | 'none';
18
19
  chip?: boolean;
19
20
  box?: 'xs' | 'sm' | 'md' | 'lg';
20
21
  size?: number;
@@ -25,6 +26,13 @@ type IconButtonProps = HTMLButtonAttributes & {
25
26
  pressed?: boolean;
26
27
  as?: 'button' | 'a';
27
28
  href?: string;
29
+ /** Render `label` as visible text beside the glyph. `row` is a full-width,
30
+ * left-aligned menu-row form for overflow menus. */
31
+ showLabel?: boolean | 'row';
32
+ /** Rotate the glyph (refresh in flight). */
33
+ spin?: boolean;
34
+ /** Forwarded to Button: spinner, blocks clicks, aria-busy. */
35
+ loading?: boolean;
28
36
  class?: string;
29
37
  };
30
38
  declare const IconButton: import("svelte").Component<IconButtonProps, {}, "">;
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from 'svelte';
3
3
 
4
- export type KeyValueTone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
4
+ import type { Tone } from '../../tone';
5
+ export type KeyValueTone = Tone;
5
6
 
6
7
  export interface KeyValueRow {
7
8
  label: string;
@@ -23,6 +24,8 @@
23
24
  warn: 'warn',
24
25
  danger: 'danger',
25
26
  info: 'accent',
27
+ success: 'success',
28
+ accent: 'accent',
26
29
  } as const;
27
30
 
28
31
  let {
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
- export type KeyValueTone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
2
+ import type { Tone } from '../../tone';
3
+ export type KeyValueTone = Tone;
3
4
  export interface KeyValueRow {
4
5
  label: string;
5
6
  value: string | number | Snippet;
@@ -8,6 +8,10 @@
8
8
  /** Free-form trailing pill ("admin", "pro", "beta"…), rendered as a Badge after the label. */
9
9
  tag?: string;
10
10
  tagTone?: import('svelte').ComponentProps<typeof import('../atoms/Badge.svelte').default>['tone'];
11
+ /** Two-state item: rendered as `menuitemcheckbox` with a check glyph when on. */
12
+ pressed?: boolean;
13
+ /** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
14
+ content?: import('svelte').Snippet<[MenuItem]>;
11
15
  }
12
16
  </script>
13
17
 
@@ -59,7 +63,11 @@
59
63
  disabled,
60
64
  gap,
61
65
  onopen,
62
- onclose
66
+ onclose,
67
+ open = $bindable(false),
68
+ class: klass = '',
69
+ style: styleProp = '',
70
+ panelClass = '',
63
71
  }: {
64
72
  label: string;
65
73
  items: MenuItem[];
@@ -67,13 +75,28 @@
67
75
  /** Custom trailing content for items that carry a `tag`. */
68
76
  tag?: Snippet<[MenuItem]>;
69
77
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
78
+ /** Bindable open state; set it to open/close programmatically. */
79
+ open?: boolean;
80
+ class?: string;
81
+ style?: string;
82
+ panelClass?: string;
70
83
  } & TriggerChrome = $props();
71
84
 
72
85
  let listEl = $state<HTMLDivElement | null>(null);
73
86
 
87
+ $effect(() => {
88
+ const pop = listEl?.closest<HTMLElement>('[popover]');
89
+ if (!pop) return;
90
+ const isOpen = pop.matches(':popover-open');
91
+ try {
92
+ if (open && !isOpen) pop.showPopover();
93
+ else if (!open && isOpen) pop.hidePopover();
94
+ } catch {}
95
+ });
96
+
74
97
  function buttons(): HTMLButtonElement[] {
75
98
  return listEl
76
- ? Array.from(listEl.querySelectorAll<HTMLButtonElement>('[role="menuitem"]:not(:disabled)'))
99
+ ? Array.from(listEl.querySelectorAll<HTMLButtonElement>('[role="menuitem"]:not(:disabled), [role="menuitemcheckbox"]:not(:disabled)'))
77
100
  : [];
78
101
  }
79
102
  function focusAt(i: number) {
@@ -116,14 +139,21 @@
116
139
  {control}
117
140
  {block}
118
141
  {triggerClass}
142
+ class={klass}
143
+ style={styleProp}
144
+ {panelClass}
119
145
  {bare}
120
146
  {hitArea}
121
147
  {disabled}
122
148
  {gap}
123
149
  role="menu"
124
150
  haspopup="menu"
125
- {onclose}
151
+ onclose={() => {
152
+ open = false;
153
+ onclose?.();
154
+ }}
126
155
  onopen={() => {
156
+ open = true;
127
157
  queueMicrotask(() => focusAt(0));
128
158
  onopen?.();
129
159
  }}
@@ -132,14 +162,19 @@
132
162
  {#each items as item (item.label)}
133
163
  <button
134
164
  type="button"
135
- role="menuitem"
165
+ role={item.pressed === undefined ? 'menuitem' : 'menuitemcheckbox'}
166
+ aria-checked={item.pressed === undefined ? undefined : item.pressed}
136
167
  class="menu-item"
137
168
  class:danger={item.danger}
169
+ class:on={item.pressed}
138
170
  disabled={item.disabled}
139
171
  tabindex="-1"
140
172
  onclick={() => select(item)}
141
173
  >
142
- {#if item.icon}<Icon name={item.icon} />{/if}
174
+ {#if item.content}
175
+ {@render item.content(item)}
176
+ {:else}
177
+ {#if item.pressed !== undefined}<span class="menu-check"><Icon name="check" /></span>{:else if item.icon}<Icon name={item.icon} />{/if}
143
178
  <span>{item.label}</span>
144
179
  {#if item.tag !== undefined}
145
180
  {#if tag}
@@ -150,6 +185,7 @@
150
185
  </span>
151
186
  {/if}
152
187
  {/if}
188
+ {/if}
153
189
  </button>
154
190
  {/each}
155
191
  </div>
@@ -183,6 +219,15 @@
183
219
  .menu-item.danger {
184
220
  color: var(--danger);
185
221
  }
222
+ .menu-check {
223
+ display: inline-flex;
224
+ width: 1em;
225
+ visibility: hidden;
226
+ }
227
+ .menu-item.on .menu-check {
228
+ visibility: visible;
229
+ color: var(--accent);
230
+ }
186
231
  .menu-tag {
187
232
  display: inline-flex;
188
233
  flex: none;
@@ -7,6 +7,10 @@ export interface MenuItem {
7
7
  /** Free-form trailing pill ("admin", "pro", "beta"…), rendered as a Badge after the label. */
8
8
  tag?: string;
9
9
  tagTone?: import('svelte').ComponentProps<typeof import('../atoms/Badge.svelte').default>['tone'];
10
+ /** Two-state item: rendered as `menuitemcheckbox` with a check glyph when on. */
11
+ pressed?: boolean;
12
+ /** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
13
+ content?: import('svelte').Snippet<[MenuItem]>;
10
14
  }
11
15
  import type { ComponentProps, Snippet } from 'svelte';
12
16
  import Popover from './Popover.svelte';
@@ -19,7 +23,12 @@ type $$ComponentProps = {
19
23
  /** Custom trailing content for items that carry a `tag`. */
20
24
  tag?: Snippet<[MenuItem]>;
21
25
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
26
+ /** Bindable open state; set it to open/close programmatically. */
27
+ open?: boolean;
28
+ class?: string;
29
+ style?: string;
30
+ panelClass?: string;
22
31
  } & TriggerChrome;
23
- declare const Menu: import("svelte").Component<$$ComponentProps, {}, "">;
32
+ declare const Menu: import("svelte").Component<$$ComponentProps, {}, "open">;
24
33
  type Menu = ReturnType<typeof Menu>;
25
34
  export default Menu;
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ // Labelled usage meter: label row, Progress track (markers / thresholds
3
+ // forwarded), trailing value and an optional caption line (reset time…).
4
+ import type { Snippet } from 'svelte';
5
+ import Progress from '../atoms/Progress.svelte';
6
+ import type { ControlSize } from '../../size';
7
+ import type { Tone } from '../../tone';
8
+
9
+ let {
10
+ label,
11
+ value,
12
+ max = 100,
13
+ format = (v: number, m: number) => `${Math.round((v / m) * 100)}%`,
14
+ caption,
15
+ markers,
16
+ toneAt,
17
+ tone,
18
+ size = 'md',
19
+ actions,
20
+ class: klass = '',
21
+ style: styleProp = ''
22
+ }: {
23
+ label: string;
24
+ value: number;
25
+ max?: number;
26
+ /** Trailing readout; defaults to a percentage. */
27
+ format?: (value: number, max: number) => string;
28
+ caption?: string | Snippet;
29
+ markers?: { at: number; label?: string; tone?: Tone }[];
30
+ toneAt?: { warn?: number; danger?: number };
31
+ tone?: Tone;
32
+ size?: ControlSize;
33
+ /** Inline controls after the readout (edit cap, bypass…). */
34
+ actions?: Snippet;
35
+ class?: string;
36
+ style?: string;
37
+ } = $props();
38
+ </script>
39
+
40
+ <div data-tsu="Meter" class="meter size-{size} {klass}" style={styleProp}>
41
+ <div class="head">
42
+ <span class="label">{label}</span>
43
+ <span class="readout">{format(value, max)}</span>
44
+ {#if actions}<span class="actions">{@render actions()}</span>{/if}
45
+ </div>
46
+ <Progress {value} {max} label={label} {markers} {toneAt} tone={tone ?? 'accent'} size={size === 'lg' ? 'md' : size} />
47
+ {#if caption}
48
+ <div class="caption">{#if typeof caption === 'string'}{caption}{:else}{@render caption()}{/if}</div>
49
+ {/if}
50
+ </div>
51
+
52
+ <style>
53
+ .meter {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--sp-1);
57
+ min-width: 0;
58
+ font-size: var(--fs-sm);
59
+ }
60
+ .size-sm {
61
+ font-size: var(--fs-xs);
62
+ }
63
+ .head {
64
+ display: flex;
65
+ align-items: baseline;
66
+ gap: var(--sp-2);
67
+ }
68
+ .label {
69
+ flex: 1 1 auto;
70
+ min-width: 0;
71
+ overflow: hidden;
72
+ text-overflow: ellipsis;
73
+ white-space: nowrap;
74
+ color: var(--text-muted);
75
+ }
76
+ .readout {
77
+ font-variant-numeric: tabular-nums;
78
+ font-family: var(--font-mono);
79
+ color: var(--text);
80
+ white-space: nowrap;
81
+ }
82
+ .actions {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ gap: var(--sp-1);
86
+ }
87
+ .caption {
88
+ font-size: var(--fs-xs);
89
+ color: var(--text-faint);
90
+ }
91
+ </style>
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ControlSize } from '../../size';
3
+ import type { Tone } from '../../tone';
4
+ type $$ComponentProps = {
5
+ label: string;
6
+ value: number;
7
+ max?: number;
8
+ /** Trailing readout; defaults to a percentage. */
9
+ format?: (value: number, max: number) => string;
10
+ caption?: string | Snippet;
11
+ markers?: {
12
+ at: number;
13
+ label?: string;
14
+ tone?: Tone;
15
+ }[];
16
+ toneAt?: {
17
+ warn?: number;
18
+ danger?: number;
19
+ };
20
+ tone?: Tone;
21
+ size?: ControlSize;
22
+ /** Inline controls after the readout (edit cap, bypass…). */
23
+ actions?: Snippet;
24
+ class?: string;
25
+ style?: string;
26
+ };
27
+ declare const Meter: import("svelte").Component<$$ComponentProps, {}, "">;
28
+ type Meter = ReturnType<typeof Meter>;
29
+ export default Meter;