@nqmcreative/ui 0.2.2 → 0.3.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 (189) hide show
  1. package/README.md +15 -15
  2. package/cli/index.mjs +81 -42
  3. package/dist/core/index.d.ts +1 -1
  4. package/dist/core/index.js +1 -1
  5. package/dist/core/toast.svelte.d.ts +3 -0
  6. package/dist/core/trigger.d.ts +6 -0
  7. package/dist/core/trigger.js +13 -0
  8. package/dist/styles/matte/AccordionItem.svelte +5 -0
  9. package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
  10. package/dist/styles/matte/Avatar.svelte +15 -0
  11. package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
  12. package/dist/styles/matte/Badge.svelte +5 -0
  13. package/dist/styles/matte/Badge.svelte.d.ts +2 -0
  14. package/dist/styles/matte/Breadcrumb.svelte +11 -4
  15. package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
  16. package/dist/styles/matte/Button.svelte +62 -11
  17. package/dist/styles/matte/Button.svelte.d.ts +14 -2
  18. package/dist/styles/matte/Card.svelte +9 -4
  19. package/dist/styles/matte/Card.svelte.d.ts +2 -0
  20. package/dist/styles/matte/CheckboxGroup.svelte +6 -0
  21. package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
  22. package/dist/styles/matte/Combobox.svelte +7 -2
  23. package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
  24. package/dist/styles/matte/CommandPalette.svelte +38 -7
  25. package/dist/styles/matte/CommandPalette.svelte.d.ts +8 -0
  26. package/dist/styles/matte/DatePicker.svelte +2 -2
  27. package/dist/styles/matte/Drawer.svelte +7 -2
  28. package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
  29. package/dist/styles/matte/Dropzone.svelte +1 -1
  30. package/dist/styles/matte/FeatureCard.svelte +1 -1
  31. package/dist/styles/matte/Footer.svelte +6 -2
  32. package/dist/styles/matte/Footer.svelte.d.ts +2 -0
  33. package/dist/styles/matte/Input.svelte +4 -4
  34. package/dist/styles/matte/Link.svelte +9 -0
  35. package/dist/styles/matte/Link.svelte.d.ts +4 -0
  36. package/dist/styles/matte/MenuItem.svelte +3 -2
  37. package/dist/styles/matte/Modal.svelte +8 -3
  38. package/dist/styles/matte/Modal.svelte.d.ts +2 -0
  39. package/dist/styles/matte/MultiSelect.svelte +7 -1
  40. package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
  41. package/dist/styles/matte/Navbar.svelte +18 -4
  42. package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
  43. package/dist/styles/matte/PricingCard.svelte +19 -11
  44. package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
  45. package/dist/styles/matte/RadioGroup.svelte +6 -0
  46. package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
  47. package/dist/styles/matte/SegmentedControl.svelte +17 -3
  48. package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
  49. package/dist/styles/matte/Select.svelte +1 -1
  50. package/dist/styles/matte/Sidebar.svelte +76 -26
  51. package/dist/styles/matte/Sidebar.svelte.d.ts +15 -1
  52. package/dist/styles/matte/Stat.svelte +8 -1
  53. package/dist/styles/matte/Stat.svelte.d.ts +3 -0
  54. package/dist/styles/matte/StatsBand.svelte +1 -1
  55. package/dist/styles/matte/Steps.svelte +7 -0
  56. package/dist/styles/matte/Steps.svelte.d.ts +3 -0
  57. package/dist/styles/matte/Table.svelte +154 -98
  58. package/dist/styles/matte/Table.svelte.d.ts +12 -0
  59. package/dist/styles/matte/Tabs.svelte +5 -0
  60. package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
  61. package/dist/styles/matte/Testimonial.svelte +2 -2
  62. package/dist/styles/matte/Textarea.svelte +1 -1
  63. package/dist/styles/matte/Toaster.svelte +3 -0
  64. package/dist/styles/matte/icon.d.ts +21 -0
  65. package/dist/styles/matte/icon.js +21 -0
  66. package/dist/styles/matte/index.d.ts +1 -3
  67. package/dist/styles/matte/index.js +0 -2
  68. package/dist/styles/paper/AccordionItem.svelte +5 -0
  69. package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
  70. package/dist/styles/paper/Avatar.svelte +15 -0
  71. package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
  72. package/dist/styles/paper/Badge.svelte +5 -0
  73. package/dist/styles/paper/Badge.svelte.d.ts +2 -0
  74. package/dist/styles/paper/Breadcrumb.svelte +8 -4
  75. package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
  76. package/dist/styles/paper/Button.svelte +58 -7
  77. package/dist/styles/paper/Button.svelte.d.ts +14 -2
  78. package/dist/styles/paper/Card.svelte +6 -1
  79. package/dist/styles/paper/Card.svelte.d.ts +2 -0
  80. package/dist/styles/paper/CheckboxGroup.svelte +6 -0
  81. package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
  82. package/dist/styles/paper/Combobox.svelte +5 -0
  83. package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
  84. package/dist/styles/paper/CommandPalette.svelte +31 -2
  85. package/dist/styles/paper/CommandPalette.svelte.d.ts +8 -0
  86. package/dist/styles/paper/Drawer.svelte +5 -0
  87. package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
  88. package/dist/styles/paper/Footer.svelte +6 -2
  89. package/dist/styles/paper/Footer.svelte.d.ts +2 -0
  90. package/dist/styles/paper/Link.svelte +9 -0
  91. package/dist/styles/paper/Link.svelte.d.ts +4 -0
  92. package/dist/styles/paper/MenuItem.svelte +2 -1
  93. package/dist/styles/paper/Modal.svelte +14 -1
  94. package/dist/styles/paper/Modal.svelte.d.ts +2 -0
  95. package/dist/styles/paper/MultiSelect.svelte +7 -1
  96. package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
  97. package/dist/styles/paper/Navbar.svelte +18 -4
  98. package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
  99. package/dist/styles/paper/PricingCard.svelte +23 -15
  100. package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
  101. package/dist/styles/paper/RadioGroup.svelte +6 -0
  102. package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
  103. package/dist/styles/paper/SegmentedControl.svelte +16 -2
  104. package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
  105. package/dist/styles/paper/Sidebar.svelte +76 -26
  106. package/dist/styles/paper/Sidebar.svelte.d.ts +15 -1
  107. package/dist/styles/paper/Stat.svelte +8 -1
  108. package/dist/styles/paper/Stat.svelte.d.ts +3 -0
  109. package/dist/styles/paper/Steps.svelte +7 -0
  110. package/dist/styles/paper/Steps.svelte.d.ts +3 -0
  111. package/dist/styles/paper/Table.svelte +153 -97
  112. package/dist/styles/paper/Table.svelte.d.ts +12 -0
  113. package/dist/styles/paper/Tabs.svelte +5 -0
  114. package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
  115. package/dist/styles/paper/Toaster.svelte +3 -0
  116. package/dist/styles/paper/fonts.css +34 -7
  117. package/dist/styles/paper/icon.d.ts +21 -0
  118. package/dist/styles/paper/icon.js +21 -0
  119. package/dist/styles/paper/index.d.ts +1 -3
  120. package/dist/styles/paper/index.js +0 -2
  121. package/dist/styles/paper/theme.css +11 -8
  122. package/dist/styles/sprout/AccordionItem.svelte +5 -0
  123. package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
  124. package/dist/styles/sprout/Avatar.svelte +15 -0
  125. package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
  126. package/dist/styles/sprout/Badge.svelte +5 -0
  127. package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
  128. package/dist/styles/sprout/Breadcrumb.svelte +8 -4
  129. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
  130. package/dist/styles/sprout/Button.svelte +58 -7
  131. package/dist/styles/sprout/Button.svelte.d.ts +14 -2
  132. package/dist/styles/sprout/Card.svelte +6 -1
  133. package/dist/styles/sprout/Card.svelte.d.ts +2 -0
  134. package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
  135. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
  136. package/dist/styles/sprout/Combobox.svelte +5 -0
  137. package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
  138. package/dist/styles/sprout/CommandPalette.svelte +31 -2
  139. package/dist/styles/sprout/CommandPalette.svelte.d.ts +8 -0
  140. package/dist/styles/sprout/Drawer.svelte +5 -0
  141. package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
  142. package/dist/styles/sprout/Footer.svelte +6 -2
  143. package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
  144. package/dist/styles/sprout/Link.svelte +9 -0
  145. package/dist/styles/sprout/Link.svelte.d.ts +4 -0
  146. package/dist/styles/sprout/MenuItem.svelte +2 -1
  147. package/dist/styles/sprout/Modal.svelte +14 -1
  148. package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
  149. package/dist/styles/sprout/MultiSelect.svelte +7 -1
  150. package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
  151. package/dist/styles/sprout/Navbar.svelte +18 -4
  152. package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
  153. package/dist/styles/sprout/PricingCard.svelte +23 -15
  154. package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
  155. package/dist/styles/sprout/RadioGroup.svelte +6 -0
  156. package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
  157. package/dist/styles/sprout/SegmentedControl.svelte +16 -2
  158. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
  159. package/dist/styles/sprout/Sidebar.svelte +76 -26
  160. package/dist/styles/sprout/Sidebar.svelte.d.ts +15 -1
  161. package/dist/styles/sprout/Stat.svelte +8 -1
  162. package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
  163. package/dist/styles/sprout/Steps.svelte +7 -0
  164. package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
  165. package/dist/styles/sprout/Table.svelte +153 -97
  166. package/dist/styles/sprout/Table.svelte.d.ts +12 -0
  167. package/dist/styles/sprout/Tabs.svelte +5 -0
  168. package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
  169. package/dist/styles/sprout/Toaster.svelte +3 -0
  170. package/dist/styles/sprout/fonts.css +7 -8
  171. package/dist/styles/sprout/icon.d.ts +21 -0
  172. package/dist/styles/sprout/icon.js +21 -0
  173. package/dist/styles/sprout/index.d.ts +1 -3
  174. package/dist/styles/sprout/index.js +0 -2
  175. package/dist/styles/sprout/theme.css +6 -6
  176. package/package.json +2 -26
  177. package/registry.json +3 -36
  178. package/dist/styles/matte/NumberInput.svelte +0 -96
  179. package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
  180. package/dist/styles/matte/PasswordInput.svelte +0 -95
  181. package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
  182. package/dist/styles/paper/NumberInput.svelte +0 -96
  183. package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
  184. package/dist/styles/paper/PasswordInput.svelte +0 -100
  185. package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
  186. package/dist/styles/sprout/NumberInput.svelte +0 -97
  187. package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
  188. package/dist/styles/sprout/PasswordInput.svelte +0 -101
  189. package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
@@ -2,6 +2,8 @@
2
2
  export interface NavItem {
3
3
  label: string;
4
4
  href?: string;
5
+ /** Leading icon, 16px. */
6
+ icon?: Snippet;
5
7
  /** Marks the current page — also sets `aria-current`. */
6
8
  active?: boolean;
7
9
  disabled?: boolean;
@@ -15,6 +17,7 @@
15
17
  <script lang="ts">
16
18
  import type { Snippet } from 'svelte';
17
19
  import { useLocale } from '../../core/locale.svelte.js';
20
+ import { iconMd } from './icon.js';
18
21
  import { focusRing, toneRing, toneSoft, toneText, type Tone } from '../../core/tones.js';
19
22
  import Drawer from './Drawer.svelte';
20
23
  import Dropdown from './Dropdown.svelte';
@@ -84,6 +87,7 @@
84
87
  ? `${toneSoft[tone]}`
85
88
  : 'text-text-secondary hover:bg-bg-inset hover:text-text'}"
86
89
  >
90
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
87
91
  {item.label}
88
92
  <svg class="size-3" viewBox="0 0 16 16" fill="none" aria-hidden="true">
89
93
  <path
@@ -96,7 +100,12 @@
96
100
  </button>
97
101
  {/snippet}
98
102
  {#each item.items as child (child.label)}
99
- <MenuItem href={child.href} disabled={child.disabled} selected={child.active}>
103
+ <MenuItem
104
+ href={child.href}
105
+ disabled={child.disabled}
106
+ selected={child.active}
107
+ icon={child.icon}
108
+ >
100
109
  {child.label}
101
110
  </MenuItem>
102
111
  {/each}
@@ -111,6 +120,7 @@
111
120
  ? 'pointer-events-none opacity-50'
112
121
  : ''}"
113
122
  >
123
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
114
124
  {item.label}
115
125
  {#if item.badge !== undefined}
116
126
  <span class="text-[11px] text-text-muted tabular-nums">{item.badge}</span>
@@ -163,7 +173,10 @@
163
173
  ? 'pointer-events-none opacity-50'
164
174
  : ''}"
165
175
  >
166
- {item.label}
176
+ <span class="flex min-w-0 items-center gap-2.5">
177
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
178
+ {item.label}
179
+ </span>
167
180
  {#if item.badge !== undefined}
168
181
  <span class="text-[11px] text-text-muted tabular-nums">{item.badge}</span>
169
182
  {/if}
@@ -175,11 +188,12 @@
175
188
  <a
176
189
  href={child.href}
177
190
  onclick={() => (menuOpen = false)}
178
- class="block rounded-md px-3 py-2 font-sans text-[13px] text-text-muted hover:bg-bg-inset {toneText[
191
+ class="flex items-center gap-2.5 rounded-md px-3 py-2 font-sans text-[13px] text-text-muted hover:bg-bg-inset {toneText[
179
192
  tone
180
193
  ]}"
181
194
  >
182
- {child.label}
195
+ {#if child.icon}<span class={iconMd}>{@render child.icon()}</span
196
+ >{/if}{child.label}
183
197
  </a>
184
198
  </li>
185
199
  {/each}
@@ -1,6 +1,8 @@
1
1
  export interface NavItem {
2
2
  label: string;
3
3
  href?: string;
4
+ /** Leading icon, 16px. */
5
+ icon?: Snippet;
4
6
  /** Marks the current page — also sets `aria-current`. */
5
7
  active?: boolean;
6
8
  disabled?: boolean;
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { toneBorder, toneSoft, toneSolid, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDivElement> {
7
8
  name: string;
@@ -12,6 +13,8 @@
12
13
  description?: string;
13
14
  /** One line each. Rendered with a tick. */
14
15
  features?: string[];
16
+ /** Replaces the tick beside every feature. */
17
+ featureIcon?: Snippet;
15
18
  /** Pill in the top corner, e.g. "Most popular". */
16
19
  badge?: string;
17
20
  /** Ring, lift and a tinted header — for the plan you want chosen. */
@@ -29,6 +32,7 @@
29
32
  period,
30
33
  description,
31
34
  features = [],
35
+ featureIcon,
32
36
  badge,
33
37
  featured = false,
34
38
  tone = 'brand',
@@ -77,21 +81,25 @@
77
81
  <ul class="flex flex-1 flex-col gap-3 border-t border-hairline p-6">
78
82
  {#each features as feature (feature)}
79
83
  <li class="flex items-start gap-3 font-sans text-sm text-text-secondary">
80
- <span
81
- class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full {toneSoft[
82
- tone
83
- ]}"
84
- >
85
- <svg class="size-2.5" viewBox="0 0 16 16" fill="none" aria-hidden="true">
86
- <path
87
- d="m3 8 3.2 3.2L13 4.4"
88
- stroke="currentColor"
89
- stroke-width="2.4"
90
- stroke-linecap="round"
91
- stroke-linejoin="round"
92
- />
93
- </svg>
94
- </span>
84
+ {#if featureIcon}
85
+ <span class="mt-0.5 {iconMd}">{@render featureIcon()}</span>
86
+ {:else}
87
+ <span
88
+ class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full {toneSoft[
89
+ tone
90
+ ]}"
91
+ >
92
+ <svg class="size-2.5" viewBox="0 0 16 16" fill="none" aria-hidden="true">
93
+ <path
94
+ d="m3 8 3.2 3.2L13 4.4"
95
+ stroke="currentColor"
96
+ stroke-width="2.4"
97
+ stroke-linecap="round"
98
+ stroke-linejoin="round"
99
+ />
100
+ </svg>
101
+ </span>
102
+ {/if}
95
103
  <span class="leading-snug">{feature}</span>
96
104
  </li>
97
105
  {/each}
@@ -10,6 +10,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
10
10
  description?: string;
11
11
  /** One line each. Rendered with a tick. */
12
12
  features?: string[];
13
+ /** Replaces the tick beside every feature. */
14
+ featureIcon?: Snippet;
13
15
  /** Pill in the top corner, e.g. "Most popular". */
14
16
  badge?: string;
15
17
  /** Ring, lift and a tinted header — for the plan you want chosen. */
@@ -3,10 +3,13 @@
3
3
  import type { HTMLFieldsetAttributes } from 'svelte/elements';
4
4
  import Radio from './Radio.svelte';
5
5
  import { toneBorder, toneSurface, type Tone } from '../../core/tones.js';
6
+ import { iconLg } from './icon.js';
6
7
 
7
8
  export interface RadioOption {
8
9
  value: string;
9
10
  label: string;
11
+ /** Leading icon, 20px. Drawn in `boxed` mode only. */
12
+ icon?: Snippet;
10
13
  description?: string;
11
14
  disabled?: boolean;
12
15
  }
@@ -62,6 +65,9 @@
62
65
  : 'border-hairline bg-bg hover:border-hairline-strong'}
63
66
  {option.disabled ? 'pointer-events-none opacity-50' : ''}"
64
67
  >
68
+ {#if option.icon}
69
+ <span class={iconLg}>{@render option.icon()}</span>
70
+ {/if}
65
71
  <Radio
66
72
  bind:group={value}
67
73
  value={option.value}
@@ -4,6 +4,8 @@ import { type Tone } from '../../core/tones.js';
4
4
  export interface RadioOption {
5
5
  value: string;
6
6
  label: string;
7
+ /** Leading icon, 20px. Drawn in `boxed` mode only. */
8
+ icon?: Snippet;
7
9
  description?: string;
8
10
  disabled?: boolean;
9
11
  }
@@ -1,10 +1,14 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { focusRing, toneRing, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
4
6
 
5
7
  export interface SegmentOption {
6
8
  value: string;
7
9
  label: string;
10
+ /** Leading icon, 16px. */
11
+ icon?: Snippet;
8
12
  disabled?: boolean;
9
13
  }
10
14
 
@@ -20,6 +24,8 @@
20
24
  tone?: Tone;
21
25
  /** Spread the segments evenly across the full width. */
22
26
  fullWidth?: boolean;
27
+ /** Icons only — each option's `label` becomes its accessible name. */
28
+ iconOnly?: boolean;
23
29
  label?: string;
24
30
  disabled?: boolean;
25
31
  onchange?: (value: string) => void;
@@ -32,6 +38,7 @@
32
38
  size = 'md',
33
39
  tone = 'brand',
34
40
  fullWidth = false,
41
+ iconOnly = false,
35
42
  label,
36
43
  disabled = false,
37
44
  class: className = '',
@@ -44,6 +51,11 @@
44
51
  md: 'h-8 px-3.5 text-sm'
45
52
  };
46
53
 
54
+ const squares: Record<SegmentedSize, string> = {
55
+ sm: 'size-7 text-[13px]',
56
+ md: 'size-8 text-sm'
57
+ };
58
+
47
59
  function select(option: SegmentOption) {
48
60
  if (option.disabled || disabled) return;
49
61
  value = option.value;
@@ -74,14 +86,16 @@
74
86
  type="button"
75
87
  role="radio"
76
88
  aria-checked={value === option.value}
89
+ aria-label={iconOnly ? option.label : undefined}
77
90
  disabled={option.disabled || disabled}
78
91
  tabindex={value === option.value ? 0 : -1}
79
92
  onclick={() => select(option)}
80
93
  class="inline-flex items-center justify-center rounded-md font-sans font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-40
81
- {sizes[size]} {focusRing} {toneRing[tone]}
94
+ {iconOnly ? squares[size] : sizes[size]} {focusRing} {toneRing[tone]}
82
95
  {value === option.value ? 'bg-bg text-text shadow-xs' : 'text-text-muted hover:text-text'}"
83
96
  >
84
- {option.label}
97
+ {#if option.icon}<span class={iconMd}>{@render option.icon()}</span>{/if}
98
+ {#if !iconOnly}{option.label}{/if}
85
99
  </button>
86
100
  {/each}
87
101
  {#if name}<input type="hidden" {name} {value} />{/if}
@@ -1,8 +1,11 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
2
3
  import { type Tone } from '../../core/tones.js';
3
4
  export interface SegmentOption {
4
5
  value: string;
5
6
  label: string;
7
+ /** Leading icon, 16px. */
8
+ icon?: Snippet;
6
9
  disabled?: boolean;
7
10
  }
8
11
  export type SegmentedSize = 'sm' | 'md';
@@ -16,6 +19,8 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
16
19
  tone?: Tone;
17
20
  /** Spread the segments evenly across the full width. */
18
21
  fullWidth?: boolean;
22
+ /** Icons only — each option's `label` becomes its accessible name. */
23
+ iconOnly?: boolean;
19
24
  label?: string;
20
25
  disabled?: boolean;
21
26
  onchange?: (value: string) => void;
@@ -1,4 +1,8 @@
1
1
  <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export type SidebarVariant = 'plain' | 'filled' | 'floating';
5
+
2
6
  export interface SidebarItem {
3
7
  /** Stable id used for the active state. Falls back to `href`. */
4
8
  id?: string;
@@ -6,10 +10,21 @@
6
10
  href?: string;
7
11
  disabled?: boolean;
8
12
  badge?: string | number;
13
+ /**
14
+ * Leading glyph, 16px. The one thing a collapsed rail still shows, so
15
+ * an item that has to survive `collapsed` needs one.
16
+ */
17
+ icon?: Snippet;
9
18
  /** Nested links — the parent becomes a collapsible group. */
10
19
  items?: SidebarItem[];
11
20
  /** Open the group on first render. */
12
21
  open?: boolean;
22
+ /**
23
+ * `false` pins a group open: the parent stops being a button and the
24
+ * children are always on show. For navigation a reader should not be
25
+ * able to lose.
26
+ */
27
+ collapsible?: boolean;
13
28
  }
14
29
 
15
30
  export interface SidebarSection {
@@ -20,8 +35,8 @@
20
35
  </script>
21
36
 
22
37
  <script lang="ts">
23
- import type { Snippet } from 'svelte';
24
38
  import { useLocale } from '../../core/locale.svelte.js';
39
+ import { iconMd } from './icon.js';
25
40
  import { focusRing, toneRing, toneSoft, type Tone } from '../../core/tones.js';
26
41
 
27
42
  interface Props {
@@ -32,6 +47,8 @@
32
47
  collapsed?: boolean;
33
48
  /** Show the collapse button. */
34
49
  collapsible?: boolean;
50
+ /** Surface treatment: bare, the framed default, or a detached card. */
51
+ variant?: SidebarVariant;
35
52
  tone?: Tone;
36
53
  header?: Snippet;
37
54
  footer?: Snippet;
@@ -44,6 +61,7 @@
44
61
  value = $bindable(''),
45
62
  collapsed = $bindable(false),
46
63
  collapsible = true,
64
+ variant = 'filled',
47
65
  tone = 'brand',
48
66
  header,
49
67
  footer,
@@ -59,7 +77,8 @@
59
77
  let opened = $state<Record<string, boolean>>({});
60
78
 
61
79
  const isOpen = (item: SidebarItem) =>
62
- opened[keyOf(item)] ?? (item.open || item.items!.some((child) => keyOf(child) === value));
80
+ item.collapsible === false ||
81
+ (opened[keyOf(item)] ?? (item.open || item.items!.some((child) => keyOf(child) === value)));
63
82
 
64
83
  function toggle(item: SidebarItem) {
65
84
  opened = { ...opened, [keyOf(item)]: !isOpen(item) };
@@ -71,13 +90,50 @@
71
90
  onnavigate?.(item);
72
91
  }
73
92
 
93
+ const surfaces: Record<SidebarVariant, string> = {
94
+ plain: 'h-full bg-transparent',
95
+ filled: 'h-full border-r border-hairline bg-bg-alt',
96
+ floating: 'm-3 h-[calc(100%-1.5rem)] rounded-lg border border-hairline bg-bg-alt shadow-sm'
97
+ };
98
+
74
99
  const row =
75
100
  'flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left font-sans text-sm transition-colors duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-40';
76
101
  </script>
77
102
 
103
+ <!-- Nested links. One copy, rendered by both kinds of group. -->
104
+ {#snippet subitems(item: SidebarItem)}
105
+ <div class="ml-4 flex flex-col gap-0.5 border-l border-hairline pl-2">
106
+ {#each item.items as child (keyOf(child))}
107
+ <a
108
+ href={child.href}
109
+ onclick={() => go(child)}
110
+ aria-current={value === keyOf(child) ? 'page' : undefined}
111
+ class="{row} {focusRing} {toneRing[tone]} {value === keyOf(child)
112
+ ? `${toneSoft[tone]} font-medium`
113
+ : 'text-text-muted hover:bg-bg-inset hover:text-text'} {child.disabled
114
+ ? 'pointer-events-none opacity-40'
115
+ : ''}"
116
+ >
117
+ {@render glyph(child)}
118
+ <span class="min-w-0 flex-1 truncate">{child.label}</span>
119
+ {#if child.badge !== undefined}
120
+ <span class="shrink-0 text-[11px] tabular-nums">{child.badge}</span>
121
+ {/if}
122
+ </a>
123
+ {/each}
124
+ </div>
125
+ {/snippet}
126
+
127
+ <!-- One box for every glyph, so rows line up whether or not an item has one. -->
128
+ {#snippet glyph(item: SidebarItem)}
129
+ {#if item.icon}
130
+ <span class={iconMd}>{@render item.icon()}</span>
131
+ {/if}
132
+ {/snippet}
133
+
78
134
  <aside
79
- class="flex h-full shrink-0 flex-col border-r border-hairline bg-bg-alt transition-[width] duration-200 ease-brand-out
80
- {collapsed ? 'w-16' : 'w-60'} {className}"
135
+ class="flex shrink-0 flex-col transition-[width] duration-200 ease-brand-out
136
+ {surfaces[variant]} {collapsed ? 'w-16' : 'w-60'} {className}"
81
137
  >
82
138
  {#if header}
83
139
  <div class="flex h-16 shrink-0 items-center gap-3 border-b border-hairline px-3">
@@ -97,16 +153,25 @@
97
153
  {/if}
98
154
 
99
155
  {#each section.items as item (keyOf(item))}
100
- {#if item.items?.length}
156
+ {#if item.items?.length && item.collapsible === false}
157
+ <div class="{row} {collapsed ? 'justify-center' : ''} font-medium text-text-muted">
158
+ {@render glyph(item)}
159
+ <span class="min-w-0 flex-1 truncate {collapsed ? 'sr-only' : ''}">{item.label}</span>
160
+ </div>
161
+ {#if isOpen(item) && !collapsed}
162
+ {@render subitems(item)}
163
+ {/if}
164
+ {:else if item.items?.length}
101
165
  <button
102
166
  type="button"
103
167
  onclick={() => toggle(item)}
104
168
  aria-expanded={isOpen(item)}
105
169
  title={collapsed ? item.label : undefined}
106
- class="{row} {focusRing} {toneRing[
170
+ class="{row} {collapsed ? 'justify-center' : ''} {focusRing} {toneRing[
107
171
  tone
108
172
  ]} text-text-secondary hover:bg-bg-inset hover:text-text"
109
173
  >
174
+ {@render glyph(item)}
110
175
  <span class="min-w-0 flex-1 truncate {collapsed ? 'sr-only' : ''}">{item.label}</span>
111
176
  {#if !collapsed}
112
177
  <svg
@@ -128,25 +193,7 @@
128
193
  </button>
129
194
 
130
195
  {#if isOpen(item) && !collapsed}
131
- <div class="ml-4 flex flex-col gap-0.5 border-l border-hairline pl-2">
132
- {#each item.items as child (keyOf(child))}
133
- <a
134
- href={child.href}
135
- onclick={() => go(child)}
136
- aria-current={value === keyOf(child) ? 'page' : undefined}
137
- class="{row} {focusRing} {toneRing[tone]} {value === keyOf(child)
138
- ? `${toneSoft[tone]} font-medium`
139
- : 'text-text-muted hover:bg-bg-inset hover:text-text'} {child.disabled
140
- ? 'pointer-events-none opacity-40'
141
- : ''}"
142
- >
143
- <span class="min-w-0 flex-1 truncate">{child.label}</span>
144
- {#if child.badge !== undefined}
145
- <span class="shrink-0 text-[11px] tabular-nums">{child.badge}</span>
146
- {/if}
147
- </a>
148
- {/each}
149
- </div>
196
+ {@render subitems(item)}
150
197
  {/if}
151
198
  {:else}
152
199
  <a
@@ -154,12 +201,15 @@
154
201
  onclick={() => go(item)}
155
202
  aria-current={value === keyOf(item) ? 'page' : undefined}
156
203
  title={collapsed ? item.label : undefined}
157
- class="{row} {focusRing} {toneRing[tone]} {value === keyOf(item)
204
+ class="{row} {collapsed ? 'justify-center' : ''} {focusRing} {toneRing[
205
+ tone
206
+ ]} {value === keyOf(item)
158
207
  ? `${toneSoft[tone]} font-medium`
159
208
  : 'text-text-secondary hover:bg-bg-inset hover:text-text'} {item.disabled
160
209
  ? 'pointer-events-none opacity-40'
161
210
  : ''}"
162
211
  >
212
+ {@render glyph(item)}
163
213
  <span class="min-w-0 flex-1 truncate {collapsed ? 'sr-only' : ''}">{item.label}</span>
164
214
  {#if item.badge !== undefined && !collapsed}
165
215
  <span class="shrink-0 text-[11px] text-text-muted tabular-nums">{item.badge}</span>
@@ -1,3 +1,5 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type SidebarVariant = 'plain' | 'filled' | 'floating';
1
3
  export interface SidebarItem {
2
4
  /** Stable id used for the active state. Falls back to `href`. */
3
5
  id?: string;
@@ -5,17 +7,27 @@ export interface SidebarItem {
5
7
  href?: string;
6
8
  disabled?: boolean;
7
9
  badge?: string | number;
10
+ /**
11
+ * Leading glyph, 16px. The one thing a collapsed rail still shows, so
12
+ * an item that has to survive `collapsed` needs one.
13
+ */
14
+ icon?: Snippet;
8
15
  /** Nested links — the parent becomes a collapsible group. */
9
16
  items?: SidebarItem[];
10
17
  /** Open the group on first render. */
11
18
  open?: boolean;
19
+ /**
20
+ * `false` pins a group open: the parent stops being a button and the
21
+ * children are always on show. For navigation a reader should not be
22
+ * able to lose.
23
+ */
24
+ collapsible?: boolean;
12
25
  }
13
26
  export interface SidebarSection {
14
27
  /** Small heading above a run of items. */
15
28
  label?: string;
16
29
  items: SidebarItem[];
17
30
  }
18
- import type { Snippet } from 'svelte';
19
31
  import { type Tone } from '../../core/tones.js';
20
32
  interface Props {
21
33
  sections: SidebarSection[];
@@ -25,6 +37,8 @@ interface Props {
25
37
  collapsed?: boolean;
26
38
  /** Show the collapse button. */
27
39
  collapsible?: boolean;
40
+ /** Surface treatment: bare, the framed default, or a detached card. */
41
+ variant?: SidebarVariant;
28
42
  tone?: Tone;
29
43
  header?: Snippet;
30
44
  footer?: Snippet;
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneText, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
4
6
 
5
7
  export type StatTrend = 'up' | 'down' | 'flat';
6
8
 
@@ -11,6 +13,8 @@
11
13
  delta?: string;
12
14
  trend?: StatTrend;
13
15
  hint?: string;
16
+ /** Leading icon on the label row. */
17
+ icon?: Snippet;
14
18
  /** Colour of the value itself. Leave unset for plain `text`. */
15
19
  tone?: Tone;
16
20
  }
@@ -21,6 +25,7 @@
21
25
  delta,
22
26
  trend = 'flat',
23
27
  hint,
28
+ icon,
24
29
  tone,
25
30
  class: className = '',
26
31
  ...rest
@@ -31,7 +36,9 @@
31
36
  </script>
32
37
 
33
38
  <div class="flex flex-col gap-1 font-sans {className}" {...rest}>
34
- <span class="text-sm font-medium text-text-muted">{label}</span>
39
+ <span class="flex items-center gap-2 text-sm font-medium text-text-muted"
40
+ >{#if icon}<span class={iconMd}>{@render icon()}</span>{/if}{label}</span
41
+ >
35
42
  <div class="flex items-baseline gap-2">
36
43
  <span
37
44
  class="font-heading text-3xl font-semibold tracking-tight tabular-nums {tone
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
2
3
  import { type Tone } from '../../core/tones.js';
3
4
  export type StatTrend = 'up' | 'down' | 'flat';
@@ -8,6 +9,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
8
9
  delta?: string;
9
10
  trend?: StatTrend;
10
11
  hint?: string;
12
+ /** Leading icon on the label row. */
13
+ icon?: Snippet;
11
14
  /** Colour of the value itself. Leave unset for plain `text`. */
12
15
  tone?: Tone;
13
16
  }
@@ -1,7 +1,11 @@
1
1
  <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
2
4
  export interface StepItem {
3
5
  label: string;
4
6
  description?: string;
7
+ /** Replaces the step number. A finished step still shows its check. */
8
+ icon?: Snippet;
5
9
  /** Marks the step as failed — shown in `danger` regardless of position. */
6
10
  error?: boolean;
7
11
  disabled?: boolean;
@@ -10,6 +14,7 @@
10
14
 
11
15
  <script lang="ts">
12
16
  import type { HTMLAttributes } from 'svelte/elements';
17
+ import { iconMd } from './icon.js';
13
18
  import { focusRing, toneFill, toneRing, toneText, type Tone } from '../../core/tones.js';
14
19
 
15
20
  interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onchange'> {
@@ -73,6 +78,8 @@
73
78
  stroke-linejoin="round"
74
79
  />
75
80
  </svg>
81
+ {:else if item.icon}
82
+ <span class={iconMd}>{@render item.icon()}</span>
76
83
  {:else}
77
84
  {index + 1}
78
85
  {/if}
@@ -1,6 +1,9 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export interface StepItem {
2
3
  label: string;
3
4
  description?: string;
5
+ /** Replaces the step number. A finished step still shows its check. */
6
+ icon?: Snippet;
4
7
  /** Marks the step as failed — shown in `danger` regardless of position. */
5
8
  error?: boolean;
6
9
  disabled?: boolean;