@nqmcreative/ui 0.2.3 → 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 (170) hide show
  1. package/README.md +15 -15
  2. package/cli/index.mjs +81 -42
  3. package/dist/core/toast.svelte.d.ts +3 -0
  4. package/dist/styles/matte/AccordionItem.svelte +5 -0
  5. package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
  6. package/dist/styles/matte/Avatar.svelte +15 -0
  7. package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
  8. package/dist/styles/matte/Badge.svelte +5 -0
  9. package/dist/styles/matte/Badge.svelte.d.ts +2 -0
  10. package/dist/styles/matte/Breadcrumb.svelte +11 -4
  11. package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
  12. package/dist/styles/matte/Button.svelte +58 -7
  13. package/dist/styles/matte/Button.svelte.d.ts +14 -2
  14. package/dist/styles/matte/Card.svelte +6 -1
  15. package/dist/styles/matte/Card.svelte.d.ts +2 -0
  16. package/dist/styles/matte/CheckboxGroup.svelte +6 -0
  17. package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
  18. package/dist/styles/matte/Combobox.svelte +5 -0
  19. package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
  20. package/dist/styles/matte/CommandPalette.svelte +6 -0
  21. package/dist/styles/matte/CommandPalette.svelte.d.ts +3 -0
  22. package/dist/styles/matte/Drawer.svelte +5 -0
  23. package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
  24. package/dist/styles/matte/Footer.svelte +6 -2
  25. package/dist/styles/matte/Footer.svelte.d.ts +2 -0
  26. package/dist/styles/matte/Link.svelte +9 -0
  27. package/dist/styles/matte/Link.svelte.d.ts +4 -0
  28. package/dist/styles/matte/MenuItem.svelte +2 -1
  29. package/dist/styles/matte/Modal.svelte +5 -0
  30. package/dist/styles/matte/Modal.svelte.d.ts +2 -0
  31. package/dist/styles/matte/MultiSelect.svelte +7 -1
  32. package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
  33. package/dist/styles/matte/Navbar.svelte +18 -4
  34. package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
  35. package/dist/styles/matte/PricingCard.svelte +16 -8
  36. package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
  37. package/dist/styles/matte/RadioGroup.svelte +6 -0
  38. package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
  39. package/dist/styles/matte/SegmentedControl.svelte +16 -2
  40. package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
  41. package/dist/styles/matte/Sidebar.svelte +2 -1
  42. package/dist/styles/matte/Stat.svelte +8 -1
  43. package/dist/styles/matte/Stat.svelte.d.ts +3 -0
  44. package/dist/styles/matte/Steps.svelte +7 -0
  45. package/dist/styles/matte/Steps.svelte.d.ts +3 -0
  46. package/dist/styles/matte/Table.svelte +153 -97
  47. package/dist/styles/matte/Table.svelte.d.ts +12 -0
  48. package/dist/styles/matte/Tabs.svelte +5 -0
  49. package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
  50. package/dist/styles/matte/Toaster.svelte +3 -0
  51. package/dist/styles/matte/icon.d.ts +21 -0
  52. package/dist/styles/matte/icon.js +21 -0
  53. package/dist/styles/matte/index.d.ts +0 -2
  54. package/dist/styles/matte/index.js +0 -2
  55. package/dist/styles/paper/AccordionItem.svelte +5 -0
  56. package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
  57. package/dist/styles/paper/Avatar.svelte +15 -0
  58. package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
  59. package/dist/styles/paper/Badge.svelte +5 -0
  60. package/dist/styles/paper/Badge.svelte.d.ts +2 -0
  61. package/dist/styles/paper/Breadcrumb.svelte +8 -4
  62. package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
  63. package/dist/styles/paper/Button.svelte +58 -7
  64. package/dist/styles/paper/Button.svelte.d.ts +14 -2
  65. package/dist/styles/paper/Card.svelte +6 -1
  66. package/dist/styles/paper/Card.svelte.d.ts +2 -0
  67. package/dist/styles/paper/CheckboxGroup.svelte +6 -0
  68. package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
  69. package/dist/styles/paper/Combobox.svelte +5 -0
  70. package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
  71. package/dist/styles/paper/CommandPalette.svelte +6 -0
  72. package/dist/styles/paper/CommandPalette.svelte.d.ts +3 -0
  73. package/dist/styles/paper/Drawer.svelte +5 -0
  74. package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
  75. package/dist/styles/paper/Footer.svelte +6 -2
  76. package/dist/styles/paper/Footer.svelte.d.ts +2 -0
  77. package/dist/styles/paper/Link.svelte +9 -0
  78. package/dist/styles/paper/Link.svelte.d.ts +4 -0
  79. package/dist/styles/paper/MenuItem.svelte +2 -1
  80. package/dist/styles/paper/Modal.svelte +5 -0
  81. package/dist/styles/paper/Modal.svelte.d.ts +2 -0
  82. package/dist/styles/paper/MultiSelect.svelte +7 -1
  83. package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
  84. package/dist/styles/paper/Navbar.svelte +18 -4
  85. package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
  86. package/dist/styles/paper/PricingCard.svelte +23 -15
  87. package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
  88. package/dist/styles/paper/RadioGroup.svelte +6 -0
  89. package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
  90. package/dist/styles/paper/SegmentedControl.svelte +16 -2
  91. package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
  92. package/dist/styles/paper/Sidebar.svelte +2 -1
  93. package/dist/styles/paper/Stat.svelte +8 -1
  94. package/dist/styles/paper/Stat.svelte.d.ts +3 -0
  95. package/dist/styles/paper/Steps.svelte +7 -0
  96. package/dist/styles/paper/Steps.svelte.d.ts +3 -0
  97. package/dist/styles/paper/Table.svelte +153 -97
  98. package/dist/styles/paper/Table.svelte.d.ts +12 -0
  99. package/dist/styles/paper/Tabs.svelte +5 -0
  100. package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
  101. package/dist/styles/paper/Toaster.svelte +3 -0
  102. package/dist/styles/paper/icon.d.ts +21 -0
  103. package/dist/styles/paper/icon.js +21 -0
  104. package/dist/styles/paper/index.d.ts +0 -2
  105. package/dist/styles/paper/index.js +0 -2
  106. package/dist/styles/sprout/AccordionItem.svelte +5 -0
  107. package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
  108. package/dist/styles/sprout/Avatar.svelte +15 -0
  109. package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
  110. package/dist/styles/sprout/Badge.svelte +5 -0
  111. package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
  112. package/dist/styles/sprout/Breadcrumb.svelte +8 -4
  113. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
  114. package/dist/styles/sprout/Button.svelte +58 -7
  115. package/dist/styles/sprout/Button.svelte.d.ts +14 -2
  116. package/dist/styles/sprout/Card.svelte +6 -1
  117. package/dist/styles/sprout/Card.svelte.d.ts +2 -0
  118. package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
  119. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
  120. package/dist/styles/sprout/Combobox.svelte +5 -0
  121. package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
  122. package/dist/styles/sprout/CommandPalette.svelte +6 -0
  123. package/dist/styles/sprout/CommandPalette.svelte.d.ts +3 -0
  124. package/dist/styles/sprout/Drawer.svelte +5 -0
  125. package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
  126. package/dist/styles/sprout/Footer.svelte +6 -2
  127. package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
  128. package/dist/styles/sprout/Link.svelte +9 -0
  129. package/dist/styles/sprout/Link.svelte.d.ts +4 -0
  130. package/dist/styles/sprout/MenuItem.svelte +2 -1
  131. package/dist/styles/sprout/Modal.svelte +5 -0
  132. package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
  133. package/dist/styles/sprout/MultiSelect.svelte +7 -1
  134. package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
  135. package/dist/styles/sprout/Navbar.svelte +18 -4
  136. package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
  137. package/dist/styles/sprout/PricingCard.svelte +23 -15
  138. package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
  139. package/dist/styles/sprout/RadioGroup.svelte +6 -0
  140. package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
  141. package/dist/styles/sprout/SegmentedControl.svelte +16 -2
  142. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
  143. package/dist/styles/sprout/Sidebar.svelte +2 -1
  144. package/dist/styles/sprout/Stat.svelte +8 -1
  145. package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
  146. package/dist/styles/sprout/Steps.svelte +7 -0
  147. package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
  148. package/dist/styles/sprout/Table.svelte +153 -97
  149. package/dist/styles/sprout/Table.svelte.d.ts +12 -0
  150. package/dist/styles/sprout/Tabs.svelte +5 -0
  151. package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
  152. package/dist/styles/sprout/Toaster.svelte +3 -0
  153. package/dist/styles/sprout/icon.d.ts +21 -0
  154. package/dist/styles/sprout/icon.js +21 -0
  155. package/dist/styles/sprout/index.d.ts +0 -2
  156. package/dist/styles/sprout/index.js +0 -2
  157. package/package.json +2 -26
  158. package/registry.json +1 -35
  159. package/dist/styles/matte/NumberInput.svelte +0 -96
  160. package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
  161. package/dist/styles/matte/PasswordInput.svelte +0 -95
  162. package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
  163. package/dist/styles/paper/NumberInput.svelte +0 -96
  164. package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
  165. package/dist/styles/paper/PasswordInput.svelte +0 -100
  166. package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
  167. package/dist/styles/sprout/NumberInput.svelte +0 -97
  168. package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
  169. package/dist/styles/sprout/PasswordInput.svelte +0 -101
  170. package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
@@ -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'> {
@@ -67,6 +72,8 @@
67
72
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
68
73
  <path d="m2 7 3.2 3.2L12 3.6" stroke="currentColor" stroke-width="1.8" />
69
74
  </svg>
75
+ {:else if item.icon}
76
+ <span class={iconMd}>{@render item.icon()}</span>
70
77
  {:else}
71
78
  {index + 1}
72
79
  {/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;
@@ -32,6 +32,16 @@
32
32
  /** Header stays put while the body scrolls — pair with a `max-h-*` class. */
33
33
  stickyHeader?: boolean;
34
34
 
35
+ /** Bar above the table — a title, a search field, a filter row. */
36
+ header?: Snippet;
37
+ /** Bar below the table — pagination, a total, a summary line. */
38
+ footer?: Snippet;
39
+ /**
40
+ * Shown in place of `header` while rows are ticked, after the count and the
41
+ * clear button. Passing it is what turns the bulk bar on.
42
+ */
43
+ bulkActions?: Snippet<[{ selected: string[]; clear: () => void }]>;
44
+
35
45
  /** Bindable. Set it to sort; leave `sortable` columns to drive it. */
36
46
  sort?: TableSort | null;
37
47
  /** Sort the rows here rather than refetching them server-side. Default true. */
@@ -57,6 +67,9 @@
57
67
  hoverable = true,
58
68
  compact = false,
59
69
  stickyHeader = false,
70
+ header,
71
+ footer,
72
+ bulkActions,
60
73
  sort = $bindable(null),
61
74
  sortLocally = true,
62
75
  onsort,
@@ -106,109 +119,152 @@
106
119
  selected = toggleKey(selected, keyOf(row));
107
120
  onselect?.(selected);
108
121
  }
122
+
123
+ function clearSelection() {
124
+ selected = [];
125
+ onselect?.(selected);
126
+ }
127
+
128
+ /** The bulk bar takes the header's place, so only one of the two is up. */
129
+ const bulkBar = $derived(selectable && bulkActions !== undefined && selected.length > 0);
109
130
  </script>
110
131
 
111
- <div class="w-full overflow-auto border border-hairline {className}" {...rest}>
112
- <table class="w-full border-collapse font-sans text-[15px]">
113
- {#if caption}
114
- <caption
115
- class="border-b border-hairline px-3.5 py-2.5 text-left font-sans text-sm text-text-muted"
132
+ <div class="flex w-full flex-col overflow-hidden border border-hairline {className}" {...rest}>
133
+ {#if bulkBar}
134
+ <div
135
+ class="flex flex-wrap items-center gap-3 border-b border-hairline bg-bg-inset px-3.5 py-2.5"
136
+ >
137
+ <span class="font-mono text-xs font-medium tracking-wide text-text uppercase">
138
+ {selected.length}
139
+ {t.current.selected}
140
+ </span>
141
+ <button
142
+ type="button"
143
+ onclick={clearSelection}
144
+ class="font-mono text-xs tracking-wide text-text-muted uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
116
145
  >
117
- {caption}
118
- </caption>
119
- {/if}
120
- <thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
121
- <tr class="border-b border-hairline bg-bg-alt">
122
- {#if selectable}
123
- <th scope="col" class="w-10 {pad}">
124
- <Checkbox
125
- checked={allSelected}
126
- indeterminate={someSelected}
127
- {tone}
128
- aria-label={t.current.selectAllRows}
129
- onchange={toggleAll}
130
- />
131
- </th>
132
- {/if}
133
- {#each columns as column (column.key)}
134
- <th
135
- scope="col"
136
- aria-sort={sort?.key === column.key
137
- ? sort.direction === 'asc'
138
- ? 'ascending'
139
- : 'descending'
140
- : undefined}
141
- class="font-mono text-xs font-medium tracking-wide text-text-muted uppercase {pad} {aligns[
142
- column.align ?? 'left'
143
- ]} {column.class ?? ''}"
144
- >
145
- {#if column.sortable}
146
- <button
147
- type="button"
148
- onclick={() => toggleSort(column)}
149
- class="inline-flex items-center gap-1.5 uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
146
+ {t.current.clear}
147
+ </button>
148
+ <div class="ml-auto flex flex-wrap items-center gap-2">
149
+ {@render bulkActions?.({ selected, clear: clearSelection })}
150
+ </div>
151
+ </div>
152
+ {:else if header}
153
+ <div
154
+ class="flex flex-wrap items-center gap-3 border-b border-hairline px-3.5 py-2.5 font-sans text-sm text-text-secondary"
155
+ >
156
+ {@render header()}
157
+ </div>
158
+ {/if}
159
+ <div class="min-h-0 w-full flex-1 overflow-auto">
160
+ <table class="w-full border-collapse font-sans text-[15px]">
161
+ {#if caption}
162
+ <caption
163
+ class="border-b border-hairline px-3.5 py-2.5 text-left font-sans text-sm text-text-muted"
164
+ >
165
+ {caption}
166
+ </caption>
167
+ {/if}
168
+ <thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
169
+ <tr class="border-b border-hairline bg-bg-alt">
170
+ {#if selectable}
171
+ <th scope="col" class="w-10 {pad}">
172
+ <Checkbox
173
+ checked={allSelected}
174
+ indeterminate={someSelected}
175
+ {tone}
176
+ aria-label={t.current.selectAllRows}
177
+ onchange={toggleAll}
178
+ />
179
+ </th>
180
+ {/if}
181
+ {#each columns as column (column.key)}
182
+ <th
183
+ scope="col"
184
+ aria-sort={sort?.key === column.key
185
+ ? sort.direction === 'asc'
186
+ ? 'ascending'
187
+ : 'descending'
188
+ : undefined}
189
+ class="font-mono text-xs font-medium tracking-wide text-text-muted uppercase {pad} {aligns[
190
+ column.align ?? 'left'
191
+ ]} {column.class ?? ''}"
192
+ >
193
+ {#if column.sortable}
194
+ <button
195
+ type="button"
196
+ onclick={() => toggleSort(column)}
197
+ class="inline-flex items-center gap-1.5 uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
150
198
  {sort?.key === column.key ? 'text-text' : ''}"
151
- >
199
+ >
200
+ {column.label}
201
+ <span class="text-[9px] leading-none" aria-hidden="true">
202
+ {#if sort?.key === column.key}
203
+ {sort.direction === 'asc' ? '▲' : '▼'}
204
+ {:else}
205
+
206
+ {/if}
207
+ </span>
208
+ </button>
209
+ {:else}
152
210
  {column.label}
153
- <span class="text-[9px] leading-none" aria-hidden="true">
154
- {#if sort?.key === column.key}
155
- {sort.direction === 'asc' ? '▲' : '▼'}
156
- {:else}
157
-
158
- {/if}
159
- </span>
160
- </button>
161
- {:else}
162
- {column.label}
163
- {/if}
164
- </th>
165
- {/each}
166
- </tr>
167
- </thead>
168
- <tbody>
169
- {#if sorted.length === 0}
170
- <tr>
171
- <td
172
- colspan={columns.length + (selectable ? 1 : 0)}
173
- class="px-3.5 py-10 text-center text-sm text-text-muted"
174
- >
175
- {#if empty}{@render empty()}{:else}{t.current.noData}{/if}
176
- </td>
211
+ {/if}
212
+ </th>
213
+ {/each}
177
214
  </tr>
178
- {:else}
179
- {#each sorted as row, i (selectable ? keyOf(row) : i)}
180
- {@const isSelected = selectable && selected.includes(keyOf(row))}
181
- <tr
182
- aria-selected={selectable ? isSelected : undefined}
183
- class="border-b border-hairline last:border-b-0
215
+ </thead>
216
+ <tbody>
217
+ {#if sorted.length === 0}
218
+ <tr>
219
+ <td
220
+ colspan={columns.length + (selectable ? 1 : 0)}
221
+ class="px-3.5 py-10 text-center text-sm text-text-muted"
222
+ >
223
+ {#if empty}{@render empty()}{:else}{t.current.noData}{/if}
224
+ </td>
225
+ </tr>
226
+ {:else}
227
+ {#each sorted as row, i (selectable ? keyOf(row) : i)}
228
+ {@const isSelected = selectable && selected.includes(keyOf(row))}
229
+ <tr
230
+ aria-selected={selectable ? isSelected : undefined}
231
+ class="border-b border-hairline last:border-b-0
184
232
  {isSelected ? 'bg-bg-inset' : striped && i % 2 === 1 ? 'bg-bg-alt' : ''}
185
233
  {hoverable ? 'transition-colors duration-150 hover:bg-bg-inset' : ''}"
186
- >
187
- {#if selectable}
188
- <td class={pad}>
189
- <Checkbox
190
- checked={isSelected}
191
- {tone}
192
- aria-label={t.current.selectRow}
193
- onchange={() => toggleRow(row)}
194
- />
195
- </td>
196
- {/if}
197
- {#each columns as column (column.key)}
198
- <td
199
- class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
200
- ''}"
201
- >
202
- {#if cell}
203
- {@render cell({ row, column, value: row[column.key] })}
204
- {:else}
205
- {row[column.key] ?? ''}
206
- {/if}
207
- </td>
208
- {/each}
209
- </tr>
210
- {/each}
211
- {/if}
212
- </tbody>
213
- </table>
234
+ >
235
+ {#if selectable}
236
+ <td class={pad}>
237
+ <Checkbox
238
+ checked={isSelected}
239
+ {tone}
240
+ aria-label={t.current.selectRow}
241
+ onchange={() => toggleRow(row)}
242
+ />
243
+ </td>
244
+ {/if}
245
+ {#each columns as column (column.key)}
246
+ <td
247
+ class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
248
+ ''}"
249
+ >
250
+ {#if cell}
251
+ {@render cell({ row, column, value: row[column.key] })}
252
+ {:else}
253
+ {row[column.key] ?? ''}
254
+ {/if}
255
+ </td>
256
+ {/each}
257
+ </tr>
258
+ {/each}
259
+ {/if}
260
+ </tbody>
261
+ </table>
262
+ </div>
263
+ {#if footer}
264
+ <div
265
+ class="flex flex-wrap items-center gap-3 border-t border-hairline px-3.5 py-2.5 font-sans text-sm text-text-secondary"
266
+ >
267
+ {@render footer()}
268
+ </div>
269
+ {/if}
214
270
  </div>
@@ -29,6 +29,18 @@ declare function $$render<T extends Record<string, unknown>>(): {
29
29
  compact?: boolean;
30
30
  /** Header stays put while the body scrolls — pair with a `max-h-*` class. */
31
31
  stickyHeader?: boolean;
32
+ /** Bar above the table — a title, a search field, a filter row. */
33
+ header?: Snippet;
34
+ /** Bar below the table — pagination, a total, a summary line. */
35
+ footer?: Snippet;
36
+ /**
37
+ * Shown in place of `header` while rows are ticked, after the count and the
38
+ * clear button. Passing it is what turns the bulk bar on.
39
+ */
40
+ bulkActions?: Snippet<[{
41
+ selected: string[];
42
+ clear: () => void;
43
+ }]>;
32
44
  /** Bindable. Set it to sort; leave `sortable` columns to drive it. */
33
45
  sort?: TableSort | null;
34
46
  /** Sort the rows here rather than refetching them server-side. Default true. */
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import {
4
5
  focusRing,
@@ -8,10 +9,13 @@
8
9
  toneText,
9
10
  type Tone
10
11
  } from '../../core/tones.js';
12
+ import { iconMd } from './icon.js';
11
13
 
12
14
  export interface TabItem {
13
15
  value: string;
14
16
  label: string;
17
+ /** Leading icon, 16px. */
18
+ icon?: Snippet;
15
19
  disabled?: boolean;
16
20
  /** Small count/label rendered after the tab title. */
17
21
  badge?: string | number;
@@ -89,6 +93,7 @@
89
93
  onclick={() => select(item)}
90
94
  class={tabClass(value === item.value)}
91
95
  >
96
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
92
97
  {item.label}
93
98
  {#if item.badge !== undefined}
94
99
  <span class="font-mono text-[11px] text-text-muted">{item.badge}</span>
@@ -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 TabItem {
4
5
  value: string;
5
6
  label: string;
7
+ /** Leading icon, 16px. */
8
+ icon?: Snippet;
6
9
  disabled?: boolean;
7
10
  /** Small count/label rendered after the tab title. */
8
11
  badge?: string | number;
@@ -4,6 +4,7 @@
4
4
  import { portal } from '../../core/actions/dismissable.js';
5
5
  import { toast } from '../../core/toast.svelte.js';
6
6
  import { toneBorderSoft, toneSurface, toneText } from '../../core/tones.js';
7
+ import { iconMd } from './icon.js';
7
8
  import Spinner from './Spinner.svelte';
8
9
  import { useLocale } from '../../core/locale.svelte.js';
9
10
 
@@ -57,6 +58,8 @@
57
58
  <span class="mt-0.5 shrink-0 {toneText[item.tone]}">
58
59
  {#if item.loading}
59
60
  <Spinner size="sm" label="" />
61
+ {:else if item.icon}
62
+ <span class={iconMd}>{@render item.icon()}</span>
60
63
  {:else if item.tone === 'success'}
61
64
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
62
65
  <path d="m3 8.5 3.2 3.2L13 5" stroke="currentColor" stroke-width="1.6" />
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The box a consumer-supplied icon renders into.
3
+ *
4
+ * Every icon in the library is a `Snippet` the app writes — the package ships
5
+ * no icon set and no runtime dependency — so the component has to size the
6
+ * glyph rather than trust it. The `*:` variant hits the snippet's own element,
7
+ * which is what keeps a bare `<svg viewBox="0 0 16 16">` from laying out at its
8
+ * intrinsic 300×150.
9
+ *
10
+ * Kept per style rather than in `core/`: an app only `@source`s the style
11
+ * folder it installed, so a class string living outside it would never be
12
+ * generated by Tailwind.
13
+ */
14
+ /** 14px — menu rows, chips, anything sitting on one line of small text. */
15
+ export declare const iconSm = "grid size-3.5 shrink-0 place-items-center *:size-3.5";
16
+ /** 16px — the default: nav items, tabs, buttons, list rows. */
17
+ export declare const iconMd = "grid size-4 shrink-0 place-items-center *:size-4";
18
+ /** 20px — headers, stats, anything sitting beside a heading. */
19
+ export declare const iconLg = "grid size-5 shrink-0 place-items-center *:size-5";
20
+ /** 24px — dialog and empty-state marks. */
21
+ export declare const iconXl = "grid size-6 shrink-0 place-items-center *:size-6";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The box a consumer-supplied icon renders into.
3
+ *
4
+ * Every icon in the library is a `Snippet` the app writes — the package ships
5
+ * no icon set and no runtime dependency — so the component has to size the
6
+ * glyph rather than trust it. The `*:` variant hits the snippet's own element,
7
+ * which is what keeps a bare `<svg viewBox="0 0 16 16">` from laying out at its
8
+ * intrinsic 300×150.
9
+ *
10
+ * Kept per style rather than in `core/`: an app only `@source`s the style
11
+ * folder it installed, so a class string living outside it would never be
12
+ * generated by Tailwind.
13
+ */
14
+ /** 14px — menu rows, chips, anything sitting on one line of small text. */
15
+ export const iconSm = 'grid size-3.5 shrink-0 place-items-center *:size-3.5';
16
+ /** 16px — the default: nav items, tabs, buttons, list rows. */
17
+ export const iconMd = 'grid size-4 shrink-0 place-items-center *:size-4';
18
+ /** 20px — headers, stats, anything sitting beside a heading. */
19
+ export const iconLg = 'grid size-5 shrink-0 place-items-center *:size-5';
20
+ /** 24px — dialog and empty-state marks. */
21
+ export const iconXl = 'grid size-6 shrink-0 place-items-center *:size-6';
@@ -22,8 +22,6 @@ export { default as Switch } from './Switch.svelte';
22
22
  export type { SwitchSize } from './Switch.svelte';
23
23
  export { default as Field } from './Field.svelte';
24
24
  export { default as Label } from './Label.svelte';
25
- export { default as PasswordInput } from './PasswordInput.svelte';
26
- export { default as NumberInput } from './NumberInput.svelte';
27
25
  export { default as Slider } from './Slider.svelte';
28
26
  export { default as RadioGroup } from './RadioGroup.svelte';
29
27
  export type { RadioOption } from './RadioGroup.svelte';
@@ -21,8 +21,6 @@ export { default as Radio } from './Radio.svelte';
21
21
  export { default as Switch } from './Switch.svelte';
22
22
  export { default as Field } from './Field.svelte';
23
23
  export { default as Label } from './Label.svelte';
24
- export { default as PasswordInput } from './PasswordInput.svelte';
25
- export { default as NumberInput } from './NumberInput.svelte';
26
24
  export { default as Slider } from './Slider.svelte';
27
25
  export { default as RadioGroup } from './RadioGroup.svelte';
28
26
  export { default as CheckboxGroup } from './CheckboxGroup.svelte';
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { toneText, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
8
  title: string;
@@ -9,6 +10,8 @@
9
10
  tone?: Tone;
10
11
  /** Short text on the right of the summary row. */
11
12
  meta?: string;
13
+ /** Leading icon on the summary row. */
14
+ icon?: Snippet;
12
15
  children: Snippet;
13
16
  }
14
17
 
@@ -17,6 +20,7 @@
17
20
  open = $bindable(false),
18
21
  tone = 'brand',
19
22
  meta,
23
+ icon,
20
24
  class: className = '',
21
25
  children,
22
26
  ...rest
@@ -27,6 +31,7 @@
27
31
  <summary
28
32
  class="flex cursor-pointer list-none items-center gap-4 px-4 py-3.5 font-sans text-sm font-medium text-text transition-colors duration-150 ease-brand-out select-none hover:bg-bg-alt focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-current [&::-webkit-details-marker]:hidden"
29
33
  >
34
+ {#if icon}<span class={iconMd}>{@render icon()}</span>{/if}
30
35
  <span class="flex-1">{title}</span>
31
36
  {#if meta}<span class="text-[13px] font-normal text-text-muted">{meta}</span>{/if}
32
37
  <svg
@@ -7,6 +7,8 @@ interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
7
  tone?: Tone;
8
8
  /** Short text on the right of the summary row. */
9
9
  meta?: string;
10
+ /** Leading icon on the summary row. */
11
+ icon?: Snippet;
10
12
  children: Snippet;
11
13
  }
12
14
  declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneSoft, type Tone } from '../../core/tones.js';
4
5
 
@@ -13,6 +14,8 @@
13
14
  tone?: Tone;
14
15
  /** Rounded square instead of the default circle. */
15
16
  squared?: boolean;
17
+ /** Drawn instead of the initials when there is no `src` — a user mark. */
18
+ fallback?: Snippet;
16
19
  }
17
20
 
18
21
  let {
@@ -22,6 +25,7 @@
22
25
  size = 'md',
23
26
  tone = 'brand',
24
27
  squared = false,
28
+ fallback,
25
29
  class: className = '',
26
30
  ...rest
27
31
  }: Props = $props();
@@ -34,6 +38,15 @@
34
38
  xl: 'size-16 text-lg'
35
39
  };
36
40
 
41
+ // The mark tracks the initials: about half the circle, whatever the size.
42
+ const glyphs: Record<AvatarSize, string> = {
43
+ xs: 'grid place-items-center *:size-3',
44
+ sm: 'grid place-items-center *:size-4',
45
+ md: 'grid place-items-center *:size-5',
46
+ lg: 'grid place-items-center *:size-6',
47
+ xl: 'grid place-items-center *:size-8'
48
+ };
49
+
37
50
  const initials = $derived(
38
51
  name
39
52
  .trim()
@@ -54,6 +67,8 @@
54
67
  >
55
68
  {#if src}
56
69
  <img {src} alt={alt ?? name} class="size-full object-cover" />
70
+ {:else if fallback}
71
+ <span class={glyphs[size]}>{@render fallback()}</span>
57
72
  {:else}
58
73
  {initials}
59
74
  {/if}
@@ -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 AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -10,6 +11,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
10
11
  tone?: Tone;
11
12
  /** Rounded square instead of the default circle. */
12
13
  squared?: boolean;
14
+ /** Drawn instead of the initials when there is no `src` — a user mark. */
15
+ fallback?: Snippet;
13
16
  }
14
17
  declare const Avatar: import("svelte").Component<Props, {}, "">;
15
18
  type Avatar = ReturnType<typeof Avatar>;
@@ -9,6 +9,7 @@
9
9
  toneText,
10
10
  type Tone
11
11
  } from '../../core/tones.js';
12
+ import { iconSm } from './icon.js';
12
13
 
13
14
  export type BadgeVariant = 'soft' | 'solid' | 'outline';
14
15
  export type BadgeSize = 'sm' | 'md';
@@ -19,6 +20,8 @@
19
20
  size?: BadgeSize;
20
21
  /** Small filled circle before the label. */
21
22
  dot?: boolean;
23
+ /** Leading icon, 14px — sits where the dot would. */
24
+ icon?: Snippet;
22
25
  children: Snippet;
23
26
  }
24
27
 
@@ -27,6 +30,7 @@
27
30
  variant = 'soft',
28
31
  size = 'md',
29
32
  dot = false,
33
+ icon,
30
34
  class: className = '',
31
35
  children,
32
36
  ...rest
@@ -54,5 +58,6 @@
54
58
  class="size-1.5 shrink-0 rounded-full {variant === 'solid' ? 'bg-current' : toneFill[tone]}"
55
59
  ></span>
56
60
  {/if}
61
+ {#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
57
62
  {@render children()}
58
63
  </span>
@@ -9,6 +9,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
9
9
  size?: BadgeSize;
10
10
  /** Small filled circle before the label. */
11
11
  dot?: boolean;
12
+ /** Leading icon, 14px — sits where the dot would. */
13
+ icon?: Snippet;
12
14
  children: Snippet;
13
15
  }
14
16
  declare const Badge: import("svelte").Component<Props, {}, "">;
@@ -1,11 +1,15 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneHoverText, type Tone } from '../../core/tones.js';
4
5
  import { useLocale } from '../../core/locale.svelte.js';
6
+ import { iconMd } from './icon.js';
5
7
 
6
8
  export interface BreadcrumbItem {
7
9
  label: string;
8
10
  href?: string;
11
+ /** Leading icon — a home mark on the first crumb, say. */
12
+ icon?: Snippet;
9
13
  }
10
14
 
11
15
  interface Props extends HTMLAttributes<HTMLElement> {
@@ -29,18 +33,18 @@
29
33
  {#if item.href && i < items.length - 1}
30
34
  <a
31
35
  href={item.href}
32
- class="rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
36
+ class="inline-flex items-center gap-1.5 rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
33
37
  tone
34
38
  ]}"
35
39
  >
36
- {item.label}
40
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
37
41
  </a>
38
42
  {:else}
39
43
  <span
40
- class="px-1 py-0.5 font-medium text-text"
44
+ class="inline-flex items-center gap-1.5 px-1 py-0.5 font-medium text-text"
41
45
  aria-current={i === items.length - 1 ? 'page' : undefined}
42
46
  >
43
- {item.label}
47
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
44
48
  </span>
45
49
  {/if}
46
50
  </li>