@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
@@ -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,112 +119,155 @@
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
132
  <div
112
- class="w-full overflow-auto rounded-lg border border-hairline bg-bg shadow-xs {className}"
133
+ class="flex w-full flex-col overflow-hidden rounded-lg border border-hairline bg-bg shadow-xs {className}"
113
134
  {...rest}
114
135
  >
115
- <table class="w-full border-collapse font-sans text-sm">
116
- {#if caption}
117
- <caption
118
- class="border-b border-hairline px-4 py-3 text-left font-sans text-[13px] text-text-muted"
136
+ {#if bulkBar}
137
+ <div
138
+ class="flex flex-wrap items-center gap-3 border-b border-hairline bg-brand-light px-4 py-3"
139
+ >
140
+ <span class="font-sans text-[13px] font-semibold text-text">
141
+ {selected.length}
142
+ {t.current.selected}
143
+ </span>
144
+ <button
145
+ type="button"
146
+ onclick={clearSelection}
147
+ class="rounded font-sans text-[13px] font-medium text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
119
148
  >
120
- {caption}
121
- </caption>
122
- {/if}
123
- <thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
124
- <tr class="border-b border-hairline bg-bg-alt">
125
- {#if selectable}
126
- <th scope="col" class="w-10 {pad}">
127
- <Checkbox
128
- checked={allSelected}
129
- indeterminate={someSelected}
130
- {tone}
131
- aria-label={t.current.selectAllRows}
132
- onchange={toggleAll}
133
- />
134
- </th>
135
- {/if}
136
- {#each columns as column (column.key)}
137
- <th
138
- scope="col"
139
- aria-sort={sort?.key === column.key
140
- ? sort.direction === 'asc'
141
- ? 'ascending'
142
- : 'descending'
143
- : undefined}
144
- class="text-[13px] font-semibold text-text-muted {pad} {aligns[
145
- column.align ?? 'left'
146
- ]} {column.class ?? ''}"
147
- >
148
- {#if column.sortable}
149
- <button
150
- type="button"
151
- onclick={() => toggleSort(column)}
152
- class="inline-flex items-center gap-1.5 rounded transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
149
+ {t.current.clear}
150
+ </button>
151
+ <div class="ml-auto flex flex-wrap items-center gap-2">
152
+ {@render bulkActions?.({ selected, clear: clearSelection })}
153
+ </div>
154
+ </div>
155
+ {:else if header}
156
+ <div
157
+ class="flex flex-wrap items-center gap-3 border-b border-hairline px-4 py-3 font-sans text-[13px] text-text-secondary"
158
+ >
159
+ {@render header()}
160
+ </div>
161
+ {/if}
162
+ <div class="min-h-0 w-full flex-1 overflow-auto">
163
+ <table class="w-full border-collapse font-sans text-sm">
164
+ {#if caption}
165
+ <caption
166
+ class="border-b border-hairline px-4 py-3 text-left font-sans text-[13px] text-text-muted"
167
+ >
168
+ {caption}
169
+ </caption>
170
+ {/if}
171
+ <thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
172
+ <tr class="border-b border-hairline bg-bg-alt">
173
+ {#if selectable}
174
+ <th scope="col" class="w-10 {pad}">
175
+ <Checkbox
176
+ checked={allSelected}
177
+ indeterminate={someSelected}
178
+ {tone}
179
+ aria-label={t.current.selectAllRows}
180
+ onchange={toggleAll}
181
+ />
182
+ </th>
183
+ {/if}
184
+ {#each columns as column (column.key)}
185
+ <th
186
+ scope="col"
187
+ aria-sort={sort?.key === column.key
188
+ ? sort.direction === 'asc'
189
+ ? 'ascending'
190
+ : 'descending'
191
+ : undefined}
192
+ class="text-[13px] font-semibold text-text-muted {pad} {aligns[
193
+ column.align ?? 'left'
194
+ ]} {column.class ?? ''}"
195
+ >
196
+ {#if column.sortable}
197
+ <button
198
+ type="button"
199
+ onclick={() => toggleSort(column)}
200
+ class="inline-flex items-center gap-1.5 rounded transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
153
201
  {sort?.key === column.key ? 'text-text' : ''}"
154
- >
202
+ >
203
+ {column.label}
204
+ <span class="text-[9px] leading-none" aria-hidden="true">
205
+ {#if sort?.key === column.key}
206
+ {sort.direction === 'asc' ? '▲' : '▼'}
207
+ {:else}
208
+
209
+ {/if}
210
+ </span>
211
+ </button>
212
+ {:else}
155
213
  {column.label}
156
- <span class="text-[9px] leading-none" aria-hidden="true">
157
- {#if sort?.key === column.key}
158
- {sort.direction === 'asc' ? '▲' : '▼'}
159
- {:else}
160
-
161
- {/if}
162
- </span>
163
- </button>
164
- {:else}
165
- {column.label}
166
- {/if}
167
- </th>
168
- {/each}
169
- </tr>
170
- </thead>
171
- <tbody>
172
- {#if sorted.length === 0}
173
- <tr>
174
- <td
175
- colspan={columns.length + (selectable ? 1 : 0)}
176
- class="px-4 py-10 text-center text-sm text-text-muted"
177
- >
178
- {#if empty}{@render empty()}{:else}{t.current.noData}{/if}
179
- </td>
214
+ {/if}
215
+ </th>
216
+ {/each}
180
217
  </tr>
181
- {:else}
182
- {#each sorted as row, i (selectable ? keyOf(row) : i)}
183
- {@const isSelected = selectable && selected.includes(keyOf(row))}
184
- <tr
185
- aria-selected={selectable ? isSelected : undefined}
186
- class="border-b border-hairline last:border-b-0
218
+ </thead>
219
+ <tbody>
220
+ {#if sorted.length === 0}
221
+ <tr>
222
+ <td
223
+ colspan={columns.length + (selectable ? 1 : 0)}
224
+ class="px-4 py-10 text-center text-sm text-text-muted"
225
+ >
226
+ {#if empty}{@render empty()}{:else}{t.current.noData}{/if}
227
+ </td>
228
+ </tr>
229
+ {:else}
230
+ {#each sorted as row, i (selectable ? keyOf(row) : i)}
231
+ {@const isSelected = selectable && selected.includes(keyOf(row))}
232
+ <tr
233
+ aria-selected={selectable ? isSelected : undefined}
234
+ class="border-b border-hairline last:border-b-0
187
235
  {isSelected ? 'bg-brand-light' : striped && i % 2 === 1 ? 'bg-bg-alt' : ''}
188
236
  {hoverable ? 'transition-colors duration-150 hover:bg-bg-alt' : ''}"
189
- >
190
- {#if selectable}
191
- <td class={pad}>
192
- <Checkbox
193
- checked={isSelected}
194
- {tone}
195
- aria-label={t.current.selectRow}
196
- onchange={() => toggleRow(row)}
197
- />
198
- </td>
199
- {/if}
200
- {#each columns as column (column.key)}
201
- <td
202
- class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
203
- ''}"
204
- >
205
- {#if cell}
206
- {@render cell({ row, column, value: row[column.key] })}
207
- {:else}
208
- {row[column.key] ?? ''}
209
- {/if}
210
- </td>
211
- {/each}
212
- </tr>
213
- {/each}
214
- {/if}
215
- </tbody>
216
- </table>
237
+ >
238
+ {#if selectable}
239
+ <td class={pad}>
240
+ <Checkbox
241
+ checked={isSelected}
242
+ {tone}
243
+ aria-label={t.current.selectRow}
244
+ onchange={() => toggleRow(row)}
245
+ />
246
+ </td>
247
+ {/if}
248
+ {#each columns as column (column.key)}
249
+ <td
250
+ class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
251
+ ''}"
252
+ >
253
+ {#if cell}
254
+ {@render cell({ row, column, value: row[column.key] })}
255
+ {:else}
256
+ {row[column.key] ?? ''}
257
+ {/if}
258
+ </td>
259
+ {/each}
260
+ </tr>
261
+ {/each}
262
+ {/if}
263
+ </tbody>
264
+ </table>
265
+ </div>
266
+ {#if footer}
267
+ <div
268
+ class="flex flex-wrap items-center gap-3 border-t border-hairline px-4 py-3 font-sans text-[13px] text-text-secondary"
269
+ >
270
+ {@render footer()}
271
+ </div>
272
+ {/if}
217
273
  </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
@@ -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 { 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
 
@@ -61,6 +62,8 @@
61
62
  <span class="mt-0.5 shrink-0 {toneText[item.tone]}">
62
63
  {#if item.loading}
63
64
  <Spinner size="sm" label="" />
65
+ {:else if item.icon}
66
+ <span class={iconMd}>{@render item.icon()}</span>
64
67
  {:else if item.tone === 'success'}
65
68
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
66
69
  <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5" />
@@ -1,8 +1,8 @@
1
1
  /*
2
- * Optional: Plus Jakarta Sans, the face paper is designed around. Import this
3
- * only if your project doesn't already self-host or load it another way —
4
- * without it the tokens fall back to the system stack, which works but reads
5
- * harder and colder.
2
+ * Optional: the two faces paper is designed around Plus Jakarta Sans
3
+ * (headings) and Work Sans (body). Import this only if your project doesn't
4
+ * already self-host or load them another way without it the tokens fall back
5
+ * to the system stack, which works but reads harder and colder.
6
6
  *
7
7
  * These are plain `@font-face` rules rather than an `@import` of the Google
8
8
  * stylesheet on purpose: `@import` is only valid before every other rule, so a
@@ -11,13 +11,13 @@
11
11
  * you put it, and it skips the CSS-to-CSS round trip before the font files
12
12
  * start downloading.
13
13
  *
14
- * Two rules rather than matte's eight: this is a variable font, so one file per
15
- * subset covers every weight from 400 to 700. Latin and Latin Extended only
16
- * around 49 kB in total. Add
14
+ * Four rules for two families: both are variable fonts, so one file per subset
15
+ * covers every weight from 400 to 700. Latin and Latin Extended only. Add
17
16
  * `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>` to
18
17
  * your document head to open the connection earlier.
19
18
  */
20
19
 
20
+ /* --- Plus Jakarta Sans --- */
21
21
  /* latin */
22
22
  @font-face {
23
23
  font-family: 'Plus Jakarta Sans';
@@ -44,3 +44,30 @@
44
44
  U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
45
45
  U+A720-A7FF;
46
46
  }
47
+
48
+ /* --- Work Sans --- */
49
+ /* latin */
50
+ @font-face {
51
+ font-family: 'Work Sans';
52
+ font-style: normal;
53
+ font-weight: 400 700;
54
+ font-display: swap;
55
+ src: url(https://fonts.gstatic.com/s/worksans/v24/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
56
+ unicode-range:
57
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
58
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
59
+ }
60
+
61
+ /* latin-ext */
62
+ @font-face {
63
+ font-family: 'Work Sans';
64
+ font-style: normal;
65
+ font-weight: 400 700;
66
+ font-display: swap;
67
+ src: url(https://fonts.gstatic.com/s/worksans/v24/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2)
68
+ format('woff2');
69
+ unicode-range:
70
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
71
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
72
+ U+A720-A7FF;
73
+ }
@@ -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';
@@ -76,7 +74,7 @@ export type { StepItem } from './Steps.svelte';
76
74
  export { default as Navbar } from './Navbar.svelte';
77
75
  export type { NavItem } from './Navbar.svelte';
78
76
  export { default as Sidebar } from './Sidebar.svelte';
79
- export type { SidebarItem, SidebarSection } from './Sidebar.svelte';
77
+ export type { SidebarVariant, SidebarItem, SidebarSection } from './Sidebar.svelte';
80
78
  export { default as Footer } from './Footer.svelte';
81
79
  export type { FooterLink, FooterColumn } from './Footer.svelte';
82
80
  export { default as HeroSection } from './HeroSection.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';
@@ -78,16 +78,19 @@
78
78
 
79
79
  /* --- type --- */
80
80
  /*
81
- * Plus Jakarta Sans: rounded terminals, open counters, low contrast. Softer
82
- * than a system stack, which lands on Segoe UI for most readers and gives
83
- * paper's generous spacing nothing warm to sit in.
81
+ * Work Sans sets running text in every style, so a project that swaps the
82
+ * word after `@nqmcreative/ui/` keeps the same reading rhythm and only the
83
+ * headings change voice.
84
84
  *
85
- * One variable file covers 400 to 700, so this costs a single request per
86
- * subsetsee `fonts.css`. Everything degrades to the system stack if that
87
- * file is not imported, so the webfont stays optional.
85
+ * Plus Jakarta Sans keeps the headings: rounded terminals, open counters,
86
+ * low contrast softer than a system stack, which lands on Segoe UI for
87
+ * most readers and gives paper's generous spacing nothing warm to sit in.
88
+ *
89
+ * Both are variable files, one request per subset — see `fonts.css`.
90
+ * Everything degrades to the system stack if that file is not imported, so
91
+ * the webfonts stay optional.
88
92
  */
89
- --font-sans:
90
- 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
93
+ --font-sans: 'Work Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
91
94
  --font-heading:
92
95
  'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
93
96
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
@@ -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>