@heyramzi/ui 3.1.0 → 3.2.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 (144) hide show
  1. package/dist/lib/sliding-pill.js +2 -2
  2. package/dist/record/measure-clamp.js +23 -0
  3. package/dist/shadcn/button-group/index.js +6 -0
  4. package/dist/shadcn/checkbox/checkbox-box.js +3 -0
  5. package/dist/shadcn/input-group/index.js +9 -0
  6. package/dist/shadcn/item/index.js +13 -0
  7. package/dist/shadcn/native-select/index.js +6 -0
  8. package/dist/shadcn/pagination/index.js +12 -0
  9. package/package.json +1 -1
  10. package/scripts/sync-shadcn.mjs +0 -5
  11. package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
  12. package/src/canvas/pen/PenOverlay.svelte +1 -30
  13. package/src/canvas/screen/PartView.svelte +13 -8
  14. package/src/canvas/screen/ScreenBody.svelte +18 -11
  15. package/src/canvas/ui/BarTooltip.svelte +46 -0
  16. package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
  17. package/src/canvas/ui/CanvasToolbar.svelte +170 -156
  18. package/src/canvas/ui/StylePanel.svelte +179 -102
  19. package/src/canvas/ui/canvas-ui.css +12 -76
  20. package/src/charts/dashboard/data-table.svelte +80 -80
  21. package/src/charts/dashboard/hint.svelte +34 -0
  22. package/src/charts/dashboard/hint.svelte.d.ts +10 -0
  23. package/src/charts/dashboard/rating-chip.svelte +4 -3
  24. package/src/charts/legend.css +0 -8
  25. package/src/charts/legend.svelte +26 -30
  26. package/src/components/copy/copy-button.svelte +10 -2
  27. package/src/components/copy/copy-button.test.ts +7 -0
  28. package/src/components/data-table/table-skeleton.svelte +15 -7
  29. package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
  30. package/src/components/docs/on-this-page.svelte +2 -2
  31. package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
  32. package/src/components/list-page/filter-item.svelte +20 -7
  33. package/src/components/list-page/filter-popover.svelte +41 -16
  34. package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
  35. package/src/components/list-page/grouped-list.svelte +41 -23
  36. package/src/components/picker/picker.svelte +8 -3
  37. package/src/components/picker/picker.svelte.d.ts +2 -1
  38. package/src/components/segmented/segmented-filter.svelte +19 -10
  39. package/src/components/segmented/segmented-track.svelte +11 -3
  40. package/src/components/table-view/table-view.svelte +22 -6
  41. package/src/components/tags-input/tags-input.svelte +94 -77
  42. package/src/components/toolbar/toolbar.svelte +2 -1
  43. package/src/components/toolbar/toolbar.test.ts +5 -4
  44. package/src/components/work-parts/group-head.svelte +28 -9
  45. package/src/components/work-parts/group-head.svelte.d.ts +5 -2
  46. package/src/editor/editor-toolbar.svelte +223 -193
  47. package/src/editor/editor.svelte +56 -38
  48. package/src/editor/image-toolbar.svelte +99 -37
  49. package/src/editor/image-toolbar.svelte.d.ts +4 -5
  50. package/src/editor/url-dialog.svelte +96 -0
  51. package/src/editor/url-dialog.svelte.d.ts +15 -0
  52. package/src/effects/theme-toggle/ThemeToggle.css +8 -8
  53. package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
  54. package/src/inbox/thread-inbox.svelte +9 -1
  55. package/src/lib/live-region.svelte +12 -4
  56. package/src/lib/live-region.svelte.d.ts +5 -1
  57. package/src/lib/sliding-pill.ts +3 -2
  58. package/src/record/info-list.svelte +33 -59
  59. package/src/record/measure-clamp.ts +31 -0
  60. package/src/record/record-shell.svelte +38 -39
  61. package/src/record/record-timeline.svelte +10 -5
  62. package/src/record/steps-rail.svelte +50 -36
  63. package/src/record/where-we-are.svelte +5 -19
  64. package/src/shadcn/button-group/button-group-separator.svelte +23 -0
  65. package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
  66. package/src/shadcn/button-group/button-group-text.svelte +28 -0
  67. package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
  68. package/src/shadcn/button-group/button-group.svelte +46 -0
  69. package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
  70. package/src/shadcn/button-group/index.ts +15 -0
  71. package/src/shadcn/checkbox/checkbox-box.ts +4 -0
  72. package/src/shadcn/checkbox/checkbox.svelte +2 -4
  73. package/src/shadcn/input-group/index.ts +22 -0
  74. package/src/shadcn/input-group/input-group-addon.svelte +51 -0
  75. package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
  76. package/src/shadcn/input-group/input-group-button.svelte +49 -0
  77. package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
  78. package/src/shadcn/input-group/input-group-input.svelte +20 -0
  79. package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  80. package/src/shadcn/input-group/input-group-text.svelte +19 -0
  81. package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  82. package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
  83. package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  84. package/src/shadcn/input-group/input-group.svelte +24 -0
  85. package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
  86. package/src/shadcn/item/index.ts +34 -0
  87. package/src/shadcn/item/item-actions.svelte +20 -0
  88. package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
  89. package/src/shadcn/item/item-content.svelte +23 -0
  90. package/src/shadcn/item/item-content.svelte.d.ts +5 -0
  91. package/src/shadcn/item/item-description.svelte +23 -0
  92. package/src/shadcn/item/item-description.svelte.d.ts +5 -0
  93. package/src/shadcn/item/item-footer.svelte +20 -0
  94. package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
  95. package/src/shadcn/item/item-group.svelte +21 -0
  96. package/src/shadcn/item/item-group.svelte.d.ts +5 -0
  97. package/src/shadcn/item/item-header.svelte +20 -0
  98. package/src/shadcn/item/item-header.svelte.d.ts +5 -0
  99. package/src/shadcn/item/item-media.svelte +42 -0
  100. package/src/shadcn/item/item-media.svelte.d.ts +17 -0
  101. package/src/shadcn/item/item-separator.svelte +19 -0
  102. package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
  103. package/src/shadcn/item/item-title.svelte +20 -0
  104. package/src/shadcn/item/item-title.svelte.d.ts +5 -0
  105. package/src/shadcn/item/item.svelte +61 -0
  106. package/src/shadcn/item/item.svelte.d.ts +28 -0
  107. package/src/shadcn/native-select/index.ts +13 -0
  108. package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
  109. package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
  110. package/src/shadcn/native-select/native-select-option.svelte +20 -0
  111. package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
  112. package/src/shadcn/native-select/native-select.svelte +45 -0
  113. package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
  114. package/src/shadcn/pagination/index.ts +31 -0
  115. package/src/shadcn/pagination/pagination-content.svelte +20 -0
  116. package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
  117. package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
  118. package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
  119. package/src/shadcn/pagination/pagination-item.svelte +14 -0
  120. package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
  121. package/src/shadcn/pagination/pagination-link.svelte +41 -0
  122. package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
  123. package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
  124. package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
  125. package/src/shadcn/pagination/pagination-next.svelte +26 -0
  126. package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
  127. package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
  128. package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
  129. package/src/shadcn/pagination/pagination-previous.svelte +30 -0
  130. package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
  131. package/src/shadcn/pagination/pagination.svelte +27 -0
  132. package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
  133. package/src/slides/Controls.svelte +39 -36
  134. package/src/slides/Deck.svelte +11 -8
  135. package/src/thread/thread.svelte +15 -8
  136. package/src/work/detail/task-detail.svelte +1 -1
  137. package/src/work/gantt/gantt-task-row.svelte +3 -6
  138. package/src/work/inbox/inbox-card.svelte +7 -5
  139. package/src/work/inbox/inbox-trace.svelte +14 -10
  140. package/src/work/kanban-board.svelte +40 -25
  141. package/src/work/kanban-hidden-rail.svelte +67 -65
  142. package/src/work/task-list.svelte +145 -124
  143. package/src/work/task-list.svelte.test.ts +5 -2
  144. package/src/work/time/time-tracker.svelte +35 -10
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { Reasoning, ReasoningContent, ReasoningTrigger } from "../../ai/reasoning/index.js";
2
+ import { Brain } from "phosphor-svelte";
3
+ import * as Trace from "../../shadcn/trace/index.js";
3
4
  import type { InboxTrace } from "./inbox-types.js";
4
5
 
5
6
  interface Props {
@@ -18,13 +19,16 @@
18
19
  );
19
20
  </script>
20
21
 
21
- <!-- WHY the shared Reasoning and not a local disclosure: it is already the
22
- collapsed-by-default "how the agent got here" pattern, with the same chevron. -->
23
- <Reasoning>
24
- <ReasoningTrigger class="w-auto">
25
- <span>Trace{count > 0 ? ` · ${count} ${count === 1 ? "quote" : "quotes"}` : ""}</span>
26
- </ReasoningTrigger>
27
- <ReasoningContent>
22
+ <Trace.Collapsible class="text-sm">
23
+ <Trace.CollapsibleTrigger class="gap-2">
24
+ <Trace.Line>
25
+ <Trace.Icon><Brain aria-hidden="true" /></Trace.Icon>
26
+ <Trace.Title
27
+ >Trace{count > 0 ? ` · ${count} ${count === 1 ? "quote" : "quotes"}` : ""}</Trace.Title
28
+ >
29
+ </Trace.Line>
30
+ </Trace.CollapsibleTrigger>
31
+ <Trace.CollapsibleContent class="mt-3 mb-0 ml-0 border-l-2 pr-0 pl-3">
28
32
  <div class="flex flex-col gap-3">
29
33
  {#if label || trace.source.href}
30
34
  <p class="text-foreground">
@@ -54,5 +58,5 @@
54
58
  <p><span class="text-foreground font-medium">Why:</span> {reasoning}</p>
55
59
  {/if}
56
60
  </div>
57
- </ReasoningContent>
58
- </Reasoning>
61
+ </Trace.CollapsibleContent>
62
+ </Trace.Collapsible>
@@ -14,6 +14,7 @@
14
14
  type KanbanColumnView,
15
15
  } from "./kanban-board-state.svelte.js";
16
16
  import { Button } from "../shadcn/button/index.js";
17
+ import * as Collapsible from "../shadcn/collapsible/index.js";
17
18
  import { workLooks } from "../components/work-parts/looks.js";
18
19
  import type {
19
20
  KanbanCardContext,
@@ -278,33 +279,47 @@
278
279
  </div>
279
280
  {/if}
280
281
 
281
- {#if showTail}
282
- <!-- The tail sits shut in a 0fr row, so opening and closing animate its height. -->
283
- <div class="kanban-fold" data-open={open} inert={!open}>
284
- <div class="kanban-fold-inner" role="list">
285
- {#each tail as item, index (getId(item))}
286
- {@render card(column, item, head.length + index, list)}
287
- {/each}
288
- </div>
289
- </div>
290
- {/if}
282
+ <!-- The root renders no element, so the fold and its button stay items of the column's flex well. -->
283
+ <Collapsible.Root
284
+ bind:open={() => open, (next) => (next ? board.expand(column.id) : board.collapse(column.id))}
285
+ >
286
+ {#snippet child()}
287
+ {#if showTail}
288
+ <!-- The tail sits shut in a 0fr row, so opening and closing animate its height. -->
289
+ <Collapsible.Content forceMount>
290
+ {#snippet child({ props })}
291
+ <div {...props} class="kanban-fold" data-open={open} inert={!open}>
292
+ <div class="kanban-fold-inner" role="list">
293
+ {#each tail as item, index (getId(item))}
294
+ {@render card(column, item, head.length + index, list)}
295
+ {/each}
296
+ </div>
297
+ </div>
298
+ {/snippet}
299
+ </Collapsible.Content>
300
+ {/if}
291
301
 
292
- {#if dnd.showIndicatorEnd(column.id) && board.idAt(shown, shown.length - 1) !== dnd.draggedId}
293
- {@render dropSlot()}
294
- {/if}
302
+ {#if dnd.showIndicatorEnd(column.id) && board.idAt(shown, shown.length - 1) !== dnd.draggedId}
303
+ {@render dropSlot()}
304
+ {/if}
295
305
 
296
- {#if tail.length > 0 || open}
297
- <Button
298
- variant="ghost"
299
- size="sm"
300
- aria-expanded={open}
301
- onclick={() => (open ? board.collapse(column.id) : board.expand(column.id))}
302
- class="shrink-0 justify-start gap-1.5 px-2 text-xs font-normal text-muted-foreground"
303
- >
304
- <KanbanFoldChevron {open} class="size-2.5" />
305
- <span class="tabular-nums">{open ? lessLabel : moreLabel(tail.length)}</span>
306
- </Button>
307
- {/if}
306
+ {#if tail.length > 0 || open}
307
+ <Collapsible.Trigger>
308
+ {#snippet child({ props })}
309
+ <Button
310
+ {...props}
311
+ variant="ghost"
312
+ size="sm"
313
+ class="shrink-0 justify-start gap-1.5 px-2 text-xs font-normal text-muted-foreground"
314
+ >
315
+ <KanbanFoldChevron {open} class="size-2.5" />
316
+ <span class="tabular-nums">{open ? lessLabel : moreLabel(tail.length)}</span>
317
+ </Button>
318
+ {/snippet}
319
+ </Collapsible.Trigger>
320
+ {/if}
321
+ {/snippet}
322
+ </Collapsible.Root>
308
323
 
309
324
  {#if columnFooter}
310
325
  {@render columnFooter(column, context)}
@@ -3,6 +3,7 @@
3
3
  import { CaretDown } from "phosphor-svelte";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { Button } from "../shadcn/button/index.js";
6
+ import * as Collapsible from "../shadcn/collapsible/index.js";
6
7
  import { workLooks } from "../components/work-parts/looks.js";
7
8
  import type { KanbanRailState } from "./kanban-rail-state.svelte.js";
8
9
  import type { KanbanColumn as KanbanColumnDef } from "./kanban-types.js";
@@ -21,79 +22,80 @@
21
22
  let { columns, rail, countOf, hiddenLabel, showAllLabel, hiddenItem }: Props = $props();
22
23
  </script>
23
24
 
24
- <div class="kanban-rail flex w-40 shrink-0 flex-col gap-1">
25
- <Button
26
- variant="ghost"
27
- size="sm"
28
- aria-expanded={rail.railOpen}
29
- onclick={() => (rail.railOpen = !rail.railOpen)}
30
- class="justify-start px-2 text-xs font-normal text-muted-foreground"
31
- >
32
- <span
33
- class={cn(
34
- "size-2.5 transition-transform duration-fast ease-smooth-out motion-reduce:transition-none [&>svg]:size-full",
35
- !rail.railOpen && "-rotate-90",
36
- )}
37
- aria-hidden="true"><CaretDown aria-hidden="true" weight="bold" /></span
38
- >
39
- {hiddenLabel}
40
- </Button>
41
-
42
- {#if rail.railOpen}
43
- <div class="flex flex-col gap-0.5">
44
- {#each columns as column (column.id)}
45
- {@const count = countOf(column.id)}
46
- {@const over = rail.dragOverHidden === column.id}
47
- {@const hue = column.hue ?? "var(--color-primary)"}
48
- <div
49
- role="region"
50
- aria-label={column.ariaLabel ?? column.label}
25
+ <Collapsible.Root bind:open={rail.railOpen} class="kanban-rail flex w-40 shrink-0 flex-col gap-1">
26
+ <Collapsible.Trigger>
27
+ {#snippet child({ props })}
28
+ <Button
29
+ {...props}
30
+ variant="ghost"
31
+ size="sm"
32
+ class="justify-start px-2 text-xs font-normal text-muted-foreground"
33
+ >
34
+ <span
51
35
  class={cn(
52
- "kanban-rail-item transition-[background-color,outline-color] duration-quick ease-smooth-out",
53
- workLooks.row,
54
- rail.leaving.has(column.id) && "kanban-rail-item-leaving",
36
+ "size-2.5 transition-transform duration-fast ease-smooth-out motion-reduce:transition-none [&>svg]:size-full",
37
+ !rail.railOpen && "-rotate-90",
55
38
  )}
56
- style:background="color-mix(in oklch, {hue}
57
- {over ? 10 : 0}%, transparent)"
58
- style:outline="1px solid color-mix(in oklch, {hue}
59
- {over ? 28 : 0}%, transparent)"
60
- ondragover={(e) => rail.onHiddenDragOver(e, column.id)}
61
- ondragleave={rail.onHiddenDragLeave}
62
- ondrop={(e) => rail.onHiddenDrop(e, column.id)}
39
+ aria-hidden="true"><CaretDown aria-hidden="true" weight="bold" /></span
63
40
  >
64
- <Button
65
- variant="ghost"
66
- size="lg"
67
- onclick={() => rail.show(column.id)}
68
- class="w-full justify-start gap-2 text-xs text-muted-foreground"
69
- >
70
- {#if hiddenItem}
71
- {@render hiddenItem(column, { count, active: over })}
72
- {:else}
73
- <span class="truncate whitespace-nowrap">{column.label}</span>
74
- {#if count > 0}
75
- <span class="ms-auto text-xs text-muted-foreground/60 tabular-nums"
76
- >{count.toLocaleString()}</span
77
- >
78
- {/if}
79
- {/if}
80
- </Button>
81
- </div>
82
- {/each}
41
+ {hiddenLabel}
42
+ </Button>
43
+ {/snippet}
44
+ </Collapsible.Trigger>
83
45
 
84
- {#if columns.length > 1}
46
+ <Collapsible.Content class="flex flex-col gap-0.5">
47
+ {#each columns as column (column.id)}
48
+ {@const count = countOf(column.id)}
49
+ {@const over = rail.dragOverHidden === column.id}
50
+ {@const hue = column.hue ?? "var(--color-primary)"}
51
+ <div
52
+ role="region"
53
+ aria-label={column.ariaLabel ?? column.label}
54
+ class={cn(
55
+ "kanban-rail-item transition-[background-color,outline-color] duration-quick ease-smooth-out",
56
+ workLooks.row,
57
+ rail.leaving.has(column.id) && "kanban-rail-item-leaving",
58
+ )}
59
+ style:background="color-mix(in oklch, {hue}
60
+ {over ? 10 : 0}%, transparent)"
61
+ style:outline="1px solid color-mix(in oklch, {hue}
62
+ {over ? 28 : 0}%, transparent)"
63
+ ondragover={(e) => rail.onHiddenDragOver(e, column.id)}
64
+ ondragleave={rail.onHiddenDragLeave}
65
+ ondrop={(e) => rail.onHiddenDrop(e, column.id)}
66
+ >
85
67
  <Button
86
68
  variant="ghost"
87
- size="sm"
88
- onclick={rail.showAll}
89
- class="mt-1 justify-start text-xs font-normal text-muted-foreground"
69
+ size="lg"
70
+ onclick={() => rail.show(column.id)}
71
+ class="w-full justify-start gap-2 text-xs text-muted-foreground"
90
72
  >
91
- {showAllLabel}
73
+ {#if hiddenItem}
74
+ {@render hiddenItem(column, { count, active: over })}
75
+ {:else}
76
+ <span class="truncate whitespace-nowrap">{column.label}</span>
77
+ {#if count > 0}
78
+ <span class="ms-auto text-xs text-muted-foreground/60 tabular-nums"
79
+ >{count.toLocaleString()}</span
80
+ >
81
+ {/if}
82
+ {/if}
92
83
  </Button>
93
- {/if}
94
- </div>
95
- {/if}
96
- </div>
84
+ </div>
85
+ {/each}
86
+
87
+ {#if columns.length > 1}
88
+ <Button
89
+ variant="ghost"
90
+ size="sm"
91
+ onclick={rail.showAll}
92
+ class="mt-1 justify-start text-xs font-normal text-muted-foreground"
93
+ >
94
+ {showAllLabel}
95
+ </Button>
96
+ {/if}
97
+ </Collapsible.Content>
98
+ </Collapsible.Root>
97
99
 
98
100
  <style>
99
101
  /* Unlayered: the row's transition-[background-color,outline-color] utility would otherwise drop the fade out. */
@@ -11,6 +11,7 @@
11
11
  import { createKanbanDnd } from "./use-kanban-dnd.svelte.js";
12
12
  import { CaretRight } from "phosphor-svelte";
13
13
  import { Button } from "../shadcn/button/index.js";
14
+ import * as Collapsible from "../shadcn/collapsible/index.js";
14
15
  import GroupHead from "../components/work-parts/group-head.svelte";
15
16
  import { workLooks } from "../components/work-parts/looks.js";
16
17
  import type {
@@ -182,9 +183,10 @@
182
183
  isDroppable: canDrop,
183
184
  });
184
185
 
185
- function toggle(groupId: string): void {
186
+ function setOpen(groupId: string, open: boolean): void {
187
+ if (open !== folded.has(groupId)) return;
186
188
  const next = new Set(folded);
187
- if (next.has(groupId)) next.delete(groupId);
189
+ if (open) next.delete(groupId);
188
190
  else next.add(groupId);
189
191
  folded = next;
190
192
  onCollapsedChange?.([...next]);
@@ -255,134 +257,153 @@
255
257
  {@const open = !folded.has(group.id)}
256
258
  {@const hue = group.hue ?? "var(--color-primary)"}
257
259
  {@const context = { count, active, open, depth, hasChildren: children.length > 0 }}
258
- <section
259
- aria-label={group.ariaLabel ?? group.label}
260
- class={cn(
261
- "tl-group group/tl",
262
- depth === 0 ? "border-b border-border last:border-b-0" : "border-t border-border/60",
263
- )}
264
- class:tl-group-active={active}
265
- style:--tl-hue={hue}
266
- ondragover={children.length ? undefined : (e) => dnd.onColumnDragOver(e, group.id)}
267
- ondragleave={children.length ? undefined : dnd.onDragLeave}
268
- ondrop={children.length ? undefined : (e) => dnd.onDrop(e, group.id)}
269
- >
270
- <div
271
- class={cn(
272
- "hover-row sticky z-10 flex h-10 items-center gap-2 px-3 backdrop-blur-sm",
273
- flat ? "bg-card/90" : "bg-card/90",
274
- depth === 0 && (head ? "top-9" : "top-0"),
275
- )}
276
- style:top={depth > 0 ? `${(head ? 2.25 : 0) + depth * 2.5}rem` : undefined}
277
- style:padding-inline-start={depth > 0 ? `${0.75 + depth * 1.25}rem` : undefined}
278
- >
279
- {#if collapsible}
280
- <Button
281
- variant="ghost"
282
- size="sm"
283
- icon
284
- aria-expanded={open}
285
- aria-label="{open ? 'Fold' : 'Unfold'} {group.label}"
286
- onclick={() => toggle(group.id)}
287
- class="-ml-1.5 text-muted-foreground"
260
+ <Collapsible.Root bind:open={() => open, (next) => setOpen(group.id, next)}>
261
+ {#snippet child({ props })}
262
+ <section
263
+ {...props}
264
+ aria-label={group.ariaLabel ?? group.label}
265
+ class={cn(
266
+ "tl-group group/tl",
267
+ depth === 0 ? "border-b border-border last:border-b-0" : "border-t border-border/60",
268
+ )}
269
+ class:tl-group-active={active}
270
+ style:--tl-hue={hue}
271
+ ondragover={children.length ? undefined : (e) => dnd.onColumnDragOver(e, group.id)}
272
+ ondragleave={children.length ? undefined : dnd.onDragLeave}
273
+ ondrop={children.length ? undefined : (e) => dnd.onDrop(e, group.id)}
274
+ >
275
+ <div
276
+ class={cn(
277
+ "hover-row sticky z-10 flex h-10 items-center gap-2 px-3 backdrop-blur-sm",
278
+ flat ? "bg-card/90" : "bg-card/90",
279
+ depth === 0 && (head ? "top-9" : "top-0"),
280
+ )}
281
+ style:top={depth > 0 ? `${(head ? 2.25 : 0) + depth * 2.5}rem` : undefined}
282
+ style:padding-inline-start={depth > 0 ? `${0.75 + depth * 1.25}rem` : undefined}
288
283
  >
289
- <span
290
- class="tl-chevron size-3.5 [&>svg]:size-full"
291
- class:tl-chevron-open={open}
292
- aria-hidden="true"
293
- >
294
- <CaretRight aria-hidden="true" weight="bold" />
295
- </span>
296
- </Button>
297
- {/if}
298
- {#if groupHeader}
299
- {@render groupHeader(group, context)}
300
- {:else}
301
- <GroupHead
302
- label={group.label}
303
- {hue}
304
- {count}
305
- onAdd={onAdd ? () => onAdd(group.id) : undefined}
306
- {addLabel}
307
- />
308
- {/if}
309
- {#if actions}
310
- <div class="tl-actions row-action ms-auto flex shrink-0 items-center gap-1">
311
- {@render actions(group, context)}
312
- </div>
313
- {/if}
314
- </div>
315
-
316
- <!-- inert: a folded group's rows leave the Tab order and the accessibility tree. -->
317
- <div class="tl-fold" class:tl-fold-open={open} inert={!open}>
318
- <div class="tl-fold-inner">
319
- <div role="list" aria-label={group.label} class="flex flex-col pb-1.5">
320
- {#each rows as item, index (getId(item))}
321
- {@const id = getId(item)}
322
- {@const rowId = `${uid}-row-${encodeURIComponent(id)}`}
323
- {@const dropBefore =
324
- dnd.showIndicatorBefore(group.id, id) &&
325
- !dnd.isDragSource(id) &&
326
- idAt(rows, index - 1) !== dnd.draggedId}
327
- <!-- WHY: a row is a list item that takes focus for the Alt+arrow move; making it a button would hide the list from screen readers. -->
328
- <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
329
- <div
330
- role="listitem"
331
- tabindex={draggable ? 0 : undefined}
332
- id={rowId}
333
- data-task-row={id}
334
- aria-label={getLabel?.(item)}
335
- aria-labelledby={draggable && !getLabel ? rowId : undefined}
336
- aria-describedby={draggable ? hintId : undefined}
337
- aria-keyshortcuts={draggable ? "Alt+ArrowUp Alt+ArrowDown" : undefined}
338
- draggable={draggable ? "true" : undefined}
339
- animate:flip={{ duration: () => durationMs("fast"), easing: cubicOut }}
340
- ondragstart={draggable
341
- ? (e) => {
342
- liftDragImage(e);
343
- dnd.onDragStart(e, id);
344
- }
345
- : undefined}
346
- ondragend={draggable ? () => dnd.resetDrag() : undefined}
347
- ondragover={draggable
348
- ? (e) => dnd.onItemDragOver(e, group.id, id, idAt(rows, index + 1))
349
- : undefined}
350
- onkeydown={draggable ? (e) => void onRowKeyDown(e, id) : undefined}
351
- style:--tl-i={Math.min(index, STAGGER_CAP)}
352
- class={cn(
353
- "tl-row relative mx-1.5 outline-none focus-visible:shadow-(--ring-focus)",
354
- workLooks.row,
355
- workLooks.rowHeight,
356
- draggable && "cursor-grab active:cursor-grabbing",
357
- dropBefore && "tl-drop-before",
358
- dnd.isDragSource(id) && "opacity-40",
359
- )}
360
- >
361
- {@render renderRow(item, { groupId: group.id, index })}
362
- </div>
363
- {/each}
364
-
365
- {#if dnd.showIndicatorEnd(group.id) && idAt(rows, rows.length - 1) !== dnd.draggedId}
366
- <div class="tl-drop-end relative mx-1.5 h-2" aria-hidden="true"></div>
284
+ {#if collapsible}
285
+ <Collapsible.Trigger>
286
+ {#snippet child({ props })}
287
+ <Button
288
+ {...props}
289
+ variant="ghost"
290
+ size="sm"
291
+ icon
292
+ aria-label="{open ? 'Fold' : 'Unfold'} {group.label}"
293
+ class="-ml-1.5 text-muted-foreground"
294
+ >
295
+ <span
296
+ class="tl-chevron size-3.5 [&>svg]:size-full"
297
+ class:tl-chevron-open={open}
298
+ aria-hidden="true"
299
+ >
300
+ <CaretRight aria-hidden="true" weight="bold" />
301
+ </span>
302
+ </Button>
303
+ {/snippet}
304
+ </Collapsible.Trigger>
367
305
  {/if}
368
-
369
- {#if groupFooter}
370
- {@render groupFooter(group, context)}
306
+ {#if groupHeader}
307
+ {@render groupHeader(group, context)}
308
+ {:else}
309
+ <GroupHead
310
+ label={group.label}
311
+ {hue}
312
+ {count}
313
+ onAdd={onAdd ? () => onAdd(group.id) : undefined}
314
+ {addLabel}
315
+ />
371
316
  {/if}
372
-
373
- {#if rows.length === 0 && children.length === 0 && !active}
374
- <p class={cn(workLooks.rowHole, "mx-1.5 px-3 py-3 text-xs text-muted-foreground")}>
375
- {emptyLabel}
376
- </p>
317
+ {#if actions}
318
+ <div class="tl-actions row-action ms-auto flex shrink-0 items-center gap-1">
319
+ {@render actions(group, context)}
320
+ </div>
377
321
  {/if}
378
322
  </div>
379
323
 
380
- {#each children as child (child.id)}
381
- {@render section(child, depth + 1)}
382
- {/each}
383
- </div>
384
- </div>
385
- </section>
324
+ <!-- inert: a folded group's rows leave the Tab order and the accessibility tree. -->
325
+ <Collapsible.Content forceMount>
326
+ {#snippet child({ props })}
327
+ <div {...props} class="tl-fold" class:tl-fold-open={open}>
328
+ <!-- On the inner box: beside the spread Svelte writes inert as an attribute, and
329
+ clearing it leaves the rows inert in the unit tests' DOM. -->
330
+ <div class="tl-fold-inner" inert={!open}>
331
+ <div role="list" aria-label={group.label} class="flex flex-col pb-1.5">
332
+ {#each rows as item, index (getId(item))}
333
+ {@const id = getId(item)}
334
+ {@const rowId = `${uid}-row-${encodeURIComponent(id)}`}
335
+ {@const dropBefore =
336
+ dnd.showIndicatorBefore(group.id, id) &&
337
+ !dnd.isDragSource(id) &&
338
+ idAt(rows, index - 1) !== dnd.draggedId}
339
+ <!-- WHY: a row is a list item that takes focus for the Alt+arrow move; making it a button would hide the list from screen readers. -->
340
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
341
+ <div
342
+ role="listitem"
343
+ tabindex={draggable ? 0 : undefined}
344
+ id={rowId}
345
+ data-task-row={id}
346
+ aria-label={getLabel?.(item)}
347
+ aria-labelledby={draggable && !getLabel ? rowId : undefined}
348
+ aria-describedby={draggable ? hintId : undefined}
349
+ aria-keyshortcuts={draggable ? "Alt+ArrowUp Alt+ArrowDown" : undefined}
350
+ draggable={draggable ? "true" : undefined}
351
+ animate:flip={{ duration: () => durationMs("fast"), easing: cubicOut }}
352
+ ondragstart={draggable
353
+ ? (e) => {
354
+ liftDragImage(e);
355
+ dnd.onDragStart(e, id);
356
+ }
357
+ : undefined}
358
+ ondragend={draggable ? () => dnd.resetDrag() : undefined}
359
+ ondragover={draggable
360
+ ? (e) => dnd.onItemDragOver(e, group.id, id, idAt(rows, index + 1))
361
+ : undefined}
362
+ onkeydown={draggable ? (e) => void onRowKeyDown(e, id) : undefined}
363
+ style:--tl-i={Math.min(index, STAGGER_CAP)}
364
+ class={cn(
365
+ "tl-row relative mx-1.5 outline-none focus-visible:shadow-(--ring-focus)",
366
+ workLooks.row,
367
+ workLooks.rowHeight,
368
+ draggable && "cursor-grab active:cursor-grabbing",
369
+ dropBefore && "tl-drop-before",
370
+ dnd.isDragSource(id) && "opacity-40",
371
+ )}
372
+ >
373
+ {@render renderRow(item, { groupId: group.id, index })}
374
+ </div>
375
+ {/each}
376
+
377
+ {#if dnd.showIndicatorEnd(group.id) && idAt(rows, rows.length - 1) !== dnd.draggedId}
378
+ <div class="tl-drop-end relative mx-1.5 h-2" aria-hidden="true"></div>
379
+ {/if}
380
+
381
+ {#if groupFooter}
382
+ {@render groupFooter(group, context)}
383
+ {/if}
384
+
385
+ {#if rows.length === 0 && children.length === 0 && !active}
386
+ <p
387
+ class={cn(
388
+ workLooks.rowHole,
389
+ "mx-1.5 px-3 py-3 text-xs text-muted-foreground",
390
+ )}
391
+ >
392
+ {emptyLabel}
393
+ </p>
394
+ {/if}
395
+ </div>
396
+
397
+ {#each children as sub (sub.id)}
398
+ {@render section(sub, depth + 1)}
399
+ {/each}
400
+ </div>
401
+ </div>
402
+ {/snippet}
403
+ </Collapsible.Content>
404
+ </section>
405
+ {/snippet}
406
+ </Collapsible.Root>
386
407
  {/snippet}
387
408
 
388
409
  <div
@@ -54,9 +54,12 @@ async function settle() {
54
54
  flushSync();
55
55
  }
56
56
 
57
- // Svelte sets inert as a property, and jsdom doesn't reflect it to the attribute.
57
+ // jsdom doesn't reflect inert between property and attribute, and Svelte sets the attribute
58
+ // when the element also takes a spread (the Collapsible's props), so read both.
58
59
  function insideInert(el: HTMLElement | null): boolean {
59
- for (let node = el; node; node = node.parentElement) if (node.inert) return true;
60
+ for (let node = el; node; node = node.parentElement) {
61
+ if (node.inert || node.hasAttribute("inert")) return true;
62
+ }
60
63
  return false;
61
64
  }
62
65