@heyramzi/ui 3.1.0 → 3.3.1

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 (172) hide show
  1. package/dist/components/list-page/toolbar-density.js +20 -1
  2. package/dist/lib/sliding-pill.js +2 -2
  3. package/dist/record/measure-clamp.js +23 -0
  4. package/dist/shadcn/button-group/index.js +6 -0
  5. package/dist/shadcn/checkbox/checkbox-box.js +3 -0
  6. package/dist/shadcn/drawer/index.js +14 -0
  7. package/dist/shadcn/input-group/index.js +9 -0
  8. package/dist/shadcn/item/index.js +13 -0
  9. package/dist/shadcn/native-select/index.js +6 -0
  10. package/dist/shadcn/pagination/index.js +12 -0
  11. package/package.json +7 -2
  12. package/scripts/sync-shadcn.mjs +0 -6
  13. package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
  14. package/src/canvas/pen/PenOverlay.svelte +1 -30
  15. package/src/canvas/screen/PartView.svelte +13 -8
  16. package/src/canvas/screen/ScreenBody.svelte +18 -11
  17. package/src/canvas/ui/BarTooltip.svelte +46 -0
  18. package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
  19. package/src/canvas/ui/CanvasToolbar.svelte +170 -156
  20. package/src/canvas/ui/StylePanel.svelte +179 -102
  21. package/src/canvas/ui/canvas-ui.css +12 -76
  22. package/src/charts/dashboard/data-table.svelte +80 -80
  23. package/src/charts/dashboard/hint.svelte +34 -0
  24. package/src/charts/dashboard/hint.svelte.d.ts +10 -0
  25. package/src/charts/dashboard/rating-chip.svelte +4 -3
  26. package/src/charts/legend.css +0 -8
  27. package/src/charts/legend.svelte +26 -30
  28. package/src/components/copy/copy-button.svelte +10 -2
  29. package/src/components/copy/copy-button.test.ts +7 -0
  30. package/src/components/data-table/table-skeleton.svelte +15 -7
  31. package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
  32. package/src/components/docs/on-this-page.svelte +2 -2
  33. package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
  34. package/src/components/list-page/filter-item.svelte +20 -7
  35. package/src/components/list-page/filter-popover.svelte +57 -17
  36. package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
  37. package/src/components/list-page/grouped-list.svelte +41 -23
  38. package/src/components/list-page/list-page-shell.svelte +155 -69
  39. package/src/components/list-page/list-page-shell.svelte.d.ts +6 -1
  40. package/src/components/list-page/toolbar-density.ts +24 -1
  41. package/src/components/picker/picker.svelte +8 -3
  42. package/src/components/picker/picker.svelte.d.ts +2 -1
  43. package/src/components/segmented/segmented-filter.svelte +19 -10
  44. package/src/components/segmented/segmented-track.svelte +11 -3
  45. package/src/components/table-view/table-view.svelte +22 -6
  46. package/src/components/tags-input/tags-input.svelte +94 -77
  47. package/src/components/toolbar/toolbar.svelte +2 -1
  48. package/src/components/toolbar/toolbar.test.ts +5 -4
  49. package/src/components/work-parts/group-head.svelte +28 -9
  50. package/src/components/work-parts/group-head.svelte.d.ts +5 -2
  51. package/src/editor/editor-toolbar.svelte +223 -193
  52. package/src/editor/editor.svelte +56 -38
  53. package/src/editor/image-toolbar.svelte +99 -37
  54. package/src/editor/image-toolbar.svelte.d.ts +4 -5
  55. package/src/editor/url-dialog.svelte +96 -0
  56. package/src/editor/url-dialog.svelte.d.ts +15 -0
  57. package/src/effects/theme-toggle/ThemeToggle.css +8 -8
  58. package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
  59. package/src/inbox/thread-inbox.svelte +9 -1
  60. package/src/lib/live-region.svelte +12 -4
  61. package/src/lib/live-region.svelte.d.ts +5 -1
  62. package/src/lib/sliding-pill.ts +3 -2
  63. package/src/record/info-list.svelte +33 -59
  64. package/src/record/measure-clamp.ts +31 -0
  65. package/src/record/record-shell.svelte +38 -39
  66. package/src/record/record-timeline.svelte +10 -5
  67. package/src/record/steps-rail.svelte +50 -36
  68. package/src/record/where-we-are.svelte +5 -19
  69. package/src/shadcn/button-group/button-group-separator.svelte +23 -0
  70. package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
  71. package/src/shadcn/button-group/button-group-text.svelte +28 -0
  72. package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
  73. package/src/shadcn/button-group/button-group.svelte +46 -0
  74. package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
  75. package/src/shadcn/button-group/index.ts +15 -0
  76. package/src/shadcn/checkbox/checkbox-box.ts +4 -0
  77. package/src/shadcn/checkbox/checkbox.svelte +2 -4
  78. package/src/shadcn/drawer/drawer-close.svelte +7 -0
  79. package/src/shadcn/drawer/drawer-close.svelte.d.ts +4 -0
  80. package/src/shadcn/drawer/drawer-content.svelte +36 -0
  81. package/src/shadcn/drawer/drawer-content.svelte.d.ts +10 -0
  82. package/src/shadcn/drawer/drawer-description.svelte +17 -0
  83. package/src/shadcn/drawer/drawer-description.svelte.d.ts +4 -0
  84. package/src/shadcn/drawer/drawer-footer.svelte +20 -0
  85. package/src/shadcn/drawer/drawer-footer.svelte.d.ts +5 -0
  86. package/src/shadcn/drawer/drawer-header.svelte +23 -0
  87. package/src/shadcn/drawer/drawer-header.svelte.d.ts +5 -0
  88. package/src/shadcn/drawer/drawer-nested.svelte +12 -0
  89. package/src/shadcn/drawer/drawer-nested.svelte.d.ts +3 -0
  90. package/src/shadcn/drawer/drawer-overlay.svelte +20 -0
  91. package/src/shadcn/drawer/drawer-overlay.svelte.d.ts +4 -0
  92. package/src/shadcn/drawer/drawer-portal.svelte +7 -0
  93. package/src/shadcn/drawer/drawer-portal.svelte.d.ts +3 -0
  94. package/src/shadcn/drawer/drawer-title.svelte +17 -0
  95. package/src/shadcn/drawer/drawer-title.svelte.d.ts +4 -0
  96. package/src/shadcn/drawer/drawer-trigger.svelte +7 -0
  97. package/src/shadcn/drawer/drawer-trigger.svelte.d.ts +4 -0
  98. package/src/shadcn/drawer/drawer.svelte +12 -0
  99. package/src/shadcn/drawer/drawer.svelte.d.ts +3 -0
  100. package/src/shadcn/drawer/index.ts +38 -0
  101. package/src/shadcn/input-group/index.ts +22 -0
  102. package/src/shadcn/input-group/input-group-addon.svelte +51 -0
  103. package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
  104. package/src/shadcn/input-group/input-group-button.svelte +49 -0
  105. package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
  106. package/src/shadcn/input-group/input-group-input.svelte +20 -0
  107. package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  108. package/src/shadcn/input-group/input-group-text.svelte +19 -0
  109. package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  110. package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
  111. package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  112. package/src/shadcn/input-group/input-group.svelte +24 -0
  113. package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
  114. package/src/shadcn/item/index.ts +34 -0
  115. package/src/shadcn/item/item-actions.svelte +20 -0
  116. package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
  117. package/src/shadcn/item/item-content.svelte +23 -0
  118. package/src/shadcn/item/item-content.svelte.d.ts +5 -0
  119. package/src/shadcn/item/item-description.svelte +23 -0
  120. package/src/shadcn/item/item-description.svelte.d.ts +5 -0
  121. package/src/shadcn/item/item-footer.svelte +20 -0
  122. package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
  123. package/src/shadcn/item/item-group.svelte +21 -0
  124. package/src/shadcn/item/item-group.svelte.d.ts +5 -0
  125. package/src/shadcn/item/item-header.svelte +20 -0
  126. package/src/shadcn/item/item-header.svelte.d.ts +5 -0
  127. package/src/shadcn/item/item-media.svelte +42 -0
  128. package/src/shadcn/item/item-media.svelte.d.ts +17 -0
  129. package/src/shadcn/item/item-separator.svelte +19 -0
  130. package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
  131. package/src/shadcn/item/item-title.svelte +20 -0
  132. package/src/shadcn/item/item-title.svelte.d.ts +5 -0
  133. package/src/shadcn/item/item.svelte +61 -0
  134. package/src/shadcn/item/item.svelte.d.ts +28 -0
  135. package/src/shadcn/native-select/index.ts +13 -0
  136. package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
  137. package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
  138. package/src/shadcn/native-select/native-select-option.svelte +20 -0
  139. package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
  140. package/src/shadcn/native-select/native-select.svelte +45 -0
  141. package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
  142. package/src/shadcn/pagination/index.ts +31 -0
  143. package/src/shadcn/pagination/pagination-content.svelte +20 -0
  144. package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
  145. package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
  146. package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
  147. package/src/shadcn/pagination/pagination-item.svelte +14 -0
  148. package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
  149. package/src/shadcn/pagination/pagination-link.svelte +41 -0
  150. package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
  151. package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
  152. package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
  153. package/src/shadcn/pagination/pagination-next.svelte +26 -0
  154. package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
  155. package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
  156. package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
  157. package/src/shadcn/pagination/pagination-previous.svelte +30 -0
  158. package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
  159. package/src/shadcn/pagination/pagination.svelte +27 -0
  160. package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
  161. package/src/slides/Controls.svelte +39 -36
  162. package/src/slides/Deck.svelte +11 -8
  163. package/src/thread/thread.svelte +15 -8
  164. package/src/work/detail/task-detail.svelte +1 -1
  165. package/src/work/gantt/gantt-task-row.svelte +3 -6
  166. package/src/work/inbox/inbox-card.svelte +7 -5
  167. package/src/work/inbox/inbox-trace.svelte +14 -10
  168. package/src/work/kanban-board.svelte +40 -25
  169. package/src/work/kanban-hidden-rail.svelte +67 -65
  170. package/src/work/task-list.svelte +145 -124
  171. package/src/work/task-list.svelte.test.ts +5 -2
  172. package/src/work/time/time-tracker.svelte +35 -10
@@ -3,6 +3,7 @@
3
3
  // IMPORTS
4
4
  // =========================
5
5
  import { tick } from "svelte";
6
+ import { Combobox } from "bits-ui";
6
7
  import { Plus } from "phosphor-svelte";
7
8
  import { cn } from "../../lib/utils.js";
8
9
  import TagChip from "../chip/tag-chip.svelte";
@@ -79,11 +80,11 @@
79
80
  // =========================
80
81
  let editing = $state(false);
81
82
  let query = $state("");
82
- let active = $state(0);
83
83
  // What the list shows while a save is in flight, so a tag doesn't blink out and back.
84
84
  let pending = $state<string[] | null>(null);
85
85
  let input = $state<HTMLInputElement | null>(null);
86
86
  let root = $state<HTMLElement | null>(null);
87
+ let menu = $state<HTMLElement | null>(null);
87
88
  /** What the live region last said: the tags a save added or removed. */
88
89
  let announcement = $state("");
89
90
  const listId = $props.id();
@@ -177,30 +178,35 @@
177
178
 
178
179
  function add(name: string) {
179
180
  query = "";
180
- active = 0;
181
181
  void commit(addTag(tags, name, options, max));
182
182
  }
183
183
 
184
+ // Bits adds the picked row's value to the list (Enter or a click). The name goes through add for its spelling and the max.
185
+ function pick(next: string[]) {
186
+ const name = next.find((entry) => !tags.includes(entry));
187
+ if (name !== undefined) add(name);
188
+ }
189
+
190
+ // The row Bits highlights: a close option when there is one, else the visible Create row.
191
+ function highlighted() {
192
+ const lit = menu?.querySelector("[data-highlighted]")?.getAttribute("data-value");
193
+ return rows.find((row) => row.name === lit) ?? rows[0];
194
+ }
195
+
196
+ // Bits owns the arrows and Enter on a typed query. These keys sit on top of it, and run first.
184
197
  function onKey(event: KeyboardEvent) {
185
- const last = rows.length - 1;
186
- const separator = event.key === "," || (free && event.key === " ");
187
- if (free && (separator || event.key === "Enter" || (event.key === "Tab" && query.trim()))) {
188
- event.preventDefault();
189
- if (query.trim()) add(query);
190
- else if (event.key === "Enter") finish();
191
- } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
192
- if (rows.length === 0) return;
198
+ const typed = query.trim() !== "";
199
+ const separator = event.key === "," || (event.key === "Tab" && typed);
200
+ if (free && (separator || event.key === " " || (event.key === "Enter" && typed))) {
193
201
  event.preventDefault();
194
- const step = event.key === "ArrowDown" ? 1 : -1;
195
- active = (active + step + rows.length) % rows.length;
196
- } else if (event.key === "Enter") {
202
+ if (typed) add(query);
203
+ } else if (!free && separator) {
197
204
  event.preventDefault();
198
- // The highlighted row: a close option when there is one, else the visible Create row.
199
- if (!query.trim()) finish();
200
- else if (rows.length > 0) add(rows[Math.min(active, last)].name);
201
- } else if (event.key === "," || (event.key === "Tab" && query.trim())) {
205
+ const row = highlighted();
206
+ if (typed && row) add(row.name);
207
+ } else if (event.key === "Enter" && !typed) {
202
208
  event.preventDefault();
203
- if (query.trim() && rows.length > 0) add(rows[Math.min(active, last)].name);
209
+ finish();
204
210
  } else if (
205
211
  event.key === "ArrowLeft" &&
206
212
  tags.length > 0 &&
@@ -257,12 +263,6 @@
257
263
  const next = event.relatedTarget as Node | null;
258
264
  if (editing && !(next && root?.contains(next))) stop();
259
265
  }
260
-
261
- // A new query starts from its best match.
262
- $effect(() => {
263
- void query;
264
- active = 0;
265
- });
266
266
  </script>
267
267
 
268
268
  <!-- =========================== -->
@@ -283,6 +283,23 @@
283
283
  </TagChip>
284
284
  {/snippet}
285
285
 
286
+ <!-- The free box is a plain text field. With suggestions, Bits adds the combobox role, state and keys. -->
287
+ {#snippet box(attrs: Record<string, unknown>)}
288
+ <input
289
+ {...attrs}
290
+ bind:this={input}
291
+ bind:value={query}
292
+ type="text"
293
+ aria-label={label}
294
+ autocomplete="off"
295
+ autocapitalize="off"
296
+ spellcheck="false"
297
+ enterkeyhint="done"
298
+ placeholder={tags.length === 0 ? text.add : ""}
299
+ class="h-5 min-w-16 flex-1 bg-transparent px-1 text-base outline-none placeholder:text-muted-foreground sm:text-xs"
300
+ />
301
+ {/snippet}
302
+
286
303
  <div
287
304
  bind:this={root}
288
305
  class={cn("relative min-w-0", className)}
@@ -298,62 +315,62 @@
298
315
  {/each}
299
316
  {#if full}
300
317
  <span class="px-1 text-xs text-muted-foreground">{text.max}</span>
318
+ {:else if free}
319
+ {@render box({ onkeydown: onKey, onpaste: onPaste })}
301
320
  {:else}
302
- <input
303
- bind:this={input}
304
- bind:value={query}
305
- type="text"
306
- role="combobox"
307
- aria-label={label}
308
- aria-expanded={open}
309
- aria-controls={listId}
310
- aria-autocomplete="list"
311
- aria-activedescendant={open ? `${listId}-${active}` : undefined}
312
- autocomplete="off"
313
- autocapitalize="off"
314
- spellcheck="false"
315
- enterkeyhint="done"
316
- placeholder={tags.length === 0 ? text.add : ""}
317
- class="h-5 min-w-16 flex-1 bg-transparent px-1 text-base outline-none placeholder:text-muted-foreground sm:text-xs"
318
- onkeydown={onKey}
319
- onpaste={onPaste}
320
- />
321
+ <!-- The list's open state follows the query, so Bits' own open and close requests are dropped. -->
322
+ <Combobox.Root
323
+ type="multiple"
324
+ loop
325
+ bind:value={() => tags, pick}
326
+ bind:open={() => open, () => {}}
327
+ >
328
+ <Combobox.Input onkeydown={onKey} onpaste={onPaste}>
329
+ {#snippet child({ props })}
330
+ <!-- The box shows the query, not the text Bits writes after a pick. -->
331
+ {@const { value: _shown, ...bits } = props}
332
+ {@render box({ ...bits, "aria-controls": open ? listId : undefined })}
333
+ {/snippet}
334
+ </Combobox.Input>
335
+ <Combobox.Portal>
336
+ <!-- WHY customAnchor: the list sits under the whole field, not under the box at the end of the tags. -->
337
+ <Combobox.Content
338
+ bind:ref={menu}
339
+ id={listId}
340
+ aria-label={label}
341
+ customAnchor={root}
342
+ side="bottom"
343
+ align="start"
344
+ sideOffset={4}
345
+ collisionPadding={8}
346
+ class="z-50 max-h-60 w-(--bits-combobox-anchor-width) min-w-48 rounded-xl bg-popover text-popover-foreground shadow-edge-raised"
347
+ >
348
+ <Combobox.Viewport class="p-1">
349
+ {#each rows as row (row.kind + row.name)}
350
+ <Combobox.Item
351
+ value={row.name}
352
+ label={row.name}
353
+ class="flex min-h-8 cursor-default items-center gap-2 rounded-lg px-2 text-sm any-pointer-coarse:min-h-11 data-highlighted:bg-foreground/6 data-highlighted:text-foreground"
354
+ >
355
+ {#if row.kind === "create"}
356
+ <Plus class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
357
+ <span class="truncate">{text.create(row.name)}</span>
358
+ {:else}
359
+ <span class="truncate">{row.name}</span>
360
+ {/if}
361
+ </Combobox.Item>
362
+ {/each}
363
+ {#if nothing}
364
+ <div class="flex min-h-8 items-center px-2 text-sm text-muted-foreground">
365
+ {text.none}
366
+ </div>
367
+ {/if}
368
+ </Combobox.Viewport>
369
+ </Combobox.Content>
370
+ </Combobox.Portal>
371
+ </Combobox.Root>
321
372
  {/if}
322
373
  </div>
323
- {#if open}
324
- <ul
325
- id={listId}
326
- role="listbox"
327
- aria-label={label}
328
- class="absolute top-full left-0 z-50 mt-1 max-h-60 w-full min-w-48 overflow-y-auto rounded-xl bg-popover p-1 text-popover-foreground shadow-edge-raised"
329
- >
330
- {#each rows as row, index (row.kind + row.name)}
331
- <!-- svelte-ignore a11y_click_events_have_key_events -->
332
- <li
333
- id="{listId}-{index}"
334
- role="option"
335
- aria-selected={index === active}
336
- class={cn(
337
- "flex min-h-8 cursor-default items-center gap-2 rounded-lg px-2 text-sm any-pointer-coarse:min-h-11",
338
- index === active && "bg-foreground/6 text-foreground",
339
- )}
340
- onmousedown={(e) => e.preventDefault()}
341
- onmousemove={() => (active = index)}
342
- onclick={() => add(row.name)}
343
- >
344
- {#if row.kind === "create"}
345
- <Plus class="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
346
- <span class="truncate">{text.create(row.name)}</span>
347
- {:else}
348
- <span class="truncate">{row.name}</span>
349
- {/if}
350
- </li>
351
- {/each}
352
- {#if nothing}
353
- <li class="flex min-h-8 items-center px-2 text-sm text-muted-foreground">{text.none}</li>
354
- {/if}
355
- </ul>
356
- {/if}
357
374
  {:else}
358
375
  <button
359
376
  type="button"
@@ -101,8 +101,9 @@
101
101
  class="@max-2xl/toolbar:hidden"
102
102
  />
103
103
  <DropdownMenu.Root>
104
+ <!-- The name carries the tab it shows (WCAG 2.5.3), so "click Open" reaches it. -->
104
105
  <DropdownMenu.Trigger
105
- aria-label={tabsLabel}
106
+ aria-label={currentTab ? `${tabsLabel}: ${currentTab.label}` : tabsLabel}
106
107
  class={cn(toolbarTriggerClass(), "min-w-0 @2xl/toolbar:hidden")}
107
108
  >
108
109
  <span class="truncate">{currentTab?.label}</span>
@@ -55,22 +55,23 @@ describe("Toolbar", () => {
55
55
  tabsLabel: "Status",
56
56
  viewLabel: "Layout",
57
57
  });
58
- for (const label of ["Find", "Status", "Layout"]) {
58
+ for (const label of ["Find", "Status: All", "Layout"]) {
59
59
  expect(root.querySelector(`button[aria-label="${label}"]`), label).not.toBeNull();
60
60
  }
61
61
  });
62
62
 
63
- it("shows the current tab on the narrow menu trigger", async () => {
63
+ // The name holds the visible tab too (WCAG 2.5.3), so a speech user can say it.
64
+ it("shows the current tab on the narrow menu trigger and names it", async () => {
64
65
  const root = render({ search: "", tabs, tab: "open", views, view: "list" });
65
66
  await tick();
66
- expect(root.querySelector('button[aria-label="Show"]')?.textContent?.trim()).toBe("Open");
67
+ expect(root.querySelector('button[aria-label="Show: Open"]')?.textContent?.trim()).toBe("Open");
67
68
  });
68
69
 
69
70
  it("leaves out what it isn't given", () => {
70
71
  const root = render({ tabs, tab: "all" });
71
72
  expect(root.querySelector('button[aria-label="Search"]')).toBeNull();
72
73
  expect(root.querySelector('button[aria-label="View"]')).toBeNull();
73
- expect(root.querySelector('button[aria-label="Show"]')).not.toBeNull();
74
+ expect(root.querySelector('button[aria-label="Show: All"]')).not.toBeNull();
74
75
  });
75
76
 
76
77
  // The 10 Nov audit: the wide search box had only its placeholder for a name.
@@ -6,10 +6,13 @@
6
6
  import CountChip from "../chip/count-chip.svelte";
7
7
 
8
8
  interface Props {
9
- label: string;
9
+ label?: string;
10
+ /** Drawn in place of `label`, in the caller's own type, for a head whose name is markup. */
11
+ title?: Snippet;
10
12
  /** The status colour of the dot. Leave out for no dot. */
11
13
  hue?: string;
12
- count: number;
14
+ /** Leave out for no count chip. */
15
+ count?: number;
13
16
  /** Shows a plus at the end that adds an item to this group. */
14
17
  onAdd?: () => void;
15
18
  addLabel?: string;
@@ -18,16 +21,32 @@
18
21
  class?: string;
19
22
  }
20
23
 
21
- let { label, hue, count, onAdd, addLabel = "Add", children, class: className }: Props = $props();
24
+ let {
25
+ label,
26
+ title,
27
+ hue,
28
+ count,
29
+ onAdd,
30
+ addLabel = "Add",
31
+ children,
32
+ class: className,
33
+ }: Props = $props();
22
34
  </script>
23
35
 
24
- <!-- A group's head, the same in a kanban column and a list group: dot, name, count, plus. -->
25
- <div class={cn("flex h-8 min-w-0 flex-1 items-center gap-2", className)}>
36
+ <!-- A group's head, the same in a kanban column, a list group, a grouped list and a table group:
37
+ dot, name, count, plus. A span, since two callers put it inside their toggle button. -->
38
+ <span class={cn("flex h-8 min-w-0 flex-1 items-center gap-2", className)}>
26
39
  {#if hue}
27
40
  <span class="size-2 shrink-0 rounded-full" style:background={hue} aria-hidden="true"></span>
28
41
  {/if}
29
- <span class="truncate text-sm font-medium text-foreground">{label}</span>
30
- <CountChip {count} />
42
+ {#if title}
43
+ {@render title()}
44
+ {:else}
45
+ <span class="truncate text-sm font-medium text-foreground">{label}</span>
46
+ {/if}
47
+ {#if count !== undefined}
48
+ <CountChip {count} />
49
+ {/if}
31
50
  {#if children || onAdd}
32
51
  <span class="ms-auto flex shrink-0 items-center gap-0.5">
33
52
  {@render children?.()}
@@ -36,7 +55,7 @@
36
55
  variant="ghost"
37
56
  size="sm"
38
57
  icon
39
- aria-label="{addLabel}: {label}"
58
+ aria-label={label ? `${addLabel}: ${label}` : addLabel}
40
59
  class="text-muted-foreground"
41
60
  onclick={onAdd}
42
61
  >
@@ -45,4 +64,4 @@
45
64
  {/if}
46
65
  </span>
47
66
  {/if}
48
- </div>
67
+ </span>
@@ -1,9 +1,12 @@
1
1
  import type { Snippet } from "svelte";
2
2
  interface Props {
3
- label: string;
3
+ label?: string;
4
+ /** Drawn in place of `label`, in the caller's own type, for a head whose name is markup. */
5
+ title?: Snippet;
4
6
  /** The status colour of the dot. Leave out for no dot. */
5
7
  hue?: string;
6
- count: number;
8
+ /** Leave out for no count chip. */
9
+ count?: number;
7
10
  /** Shows a plus at the end that adds an item to this group. */
8
11
  onAdd?: () => void;
9
12
  addLabel?: string;