@heyramzi/ui 3.0.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 (160) hide show
  1. package/dist/charts/dual-axis.js +38 -0
  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/input-group/index.js +9 -0
  7. package/dist/shadcn/item/index.js +13 -0
  8. package/dist/shadcn/native-select/index.js +6 -0
  9. package/dist/shadcn/pagination/index.js +12 -0
  10. package/dist/slides/deck-controller.svelte.js +62 -15
  11. package/dist/slides/layout.js +3 -2
  12. package/package.json +9 -2
  13. package/scripts/svelte-dts.mjs +3 -0
  14. package/scripts/sync-shadcn.mjs +0 -5
  15. package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
  16. package/src/canvas/pen/PenOverlay.svelte +1 -30
  17. package/src/canvas/screen/PartView.svelte +13 -8
  18. package/src/canvas/screen/ScreenBody.svelte +18 -11
  19. package/src/canvas/ui/BarTooltip.svelte +46 -0
  20. package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
  21. package/src/canvas/ui/CanvasToolbar.svelte +170 -156
  22. package/src/canvas/ui/StylePanel.svelte +179 -102
  23. package/src/canvas/ui/canvas-ui.css +12 -76
  24. package/src/charts/area-chart.css +6 -0
  25. package/src/charts/area-chart.svelte +115 -17
  26. package/src/charts/area-chart.svelte.d.ts +11 -2
  27. package/src/charts/chart-tokens.css +4 -3
  28. package/src/charts/dashboard/data-table.svelte +80 -80
  29. package/src/charts/dashboard/hint.svelte +34 -0
  30. package/src/charts/dashboard/hint.svelte.d.ts +10 -0
  31. package/src/charts/dashboard/rating-chip.svelte +4 -3
  32. package/src/charts/dual-axis.test.ts +40 -0
  33. package/src/charts/dual-axis.ts +43 -0
  34. package/src/charts/legend.css +0 -8
  35. package/src/charts/legend.svelte +26 -30
  36. package/src/components/copy/copy-button.svelte +10 -2
  37. package/src/components/copy/copy-button.test.ts +7 -0
  38. package/src/components/data-table/table-skeleton.svelte +15 -7
  39. package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
  40. package/src/components/docs/on-this-page.svelte +2 -2
  41. package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
  42. package/src/components/list-page/filter-item.svelte +20 -7
  43. package/src/components/list-page/filter-popover.svelte +41 -16
  44. package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
  45. package/src/components/list-page/grouped-list.svelte +41 -23
  46. package/src/components/picker/picker.svelte +8 -3
  47. package/src/components/picker/picker.svelte.d.ts +2 -1
  48. package/src/components/segmented/segmented-filter.svelte +19 -10
  49. package/src/components/segmented/segmented-track.svelte +11 -3
  50. package/src/components/table-view/table-view.svelte +22 -6
  51. package/src/components/tags-input/tags-input.svelte +94 -77
  52. package/src/components/toolbar/toolbar.svelte +2 -1
  53. package/src/components/toolbar/toolbar.test.ts +5 -4
  54. package/src/components/work-parts/group-head.svelte +28 -9
  55. package/src/components/work-parts/group-head.svelte.d.ts +5 -2
  56. package/src/editor/editor-toolbar.svelte +231 -193
  57. package/src/editor/editor.svelte +57 -39
  58. package/src/editor/image-toolbar.svelte +99 -37
  59. package/src/editor/image-toolbar.svelte.d.ts +4 -5
  60. package/src/editor/url-dialog.svelte +96 -0
  61. package/src/editor/url-dialog.svelte.d.ts +15 -0
  62. package/src/effects/theme-toggle/ThemeToggle.css +8 -8
  63. package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
  64. package/src/inbox/thread-inbox.svelte +9 -1
  65. package/src/lib/live-region.svelte +12 -4
  66. package/src/lib/live-region.svelte.d.ts +5 -1
  67. package/src/lib/sliding-pill.ts +3 -2
  68. package/src/record/info-list.svelte +33 -59
  69. package/src/record/measure-clamp.ts +31 -0
  70. package/src/record/record-shell.svelte +38 -39
  71. package/src/record/record-timeline.svelte +10 -5
  72. package/src/record/steps-rail.svelte +50 -36
  73. package/src/record/where-we-are.svelte +5 -19
  74. package/src/shadcn/button-group/button-group-separator.svelte +23 -0
  75. package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
  76. package/src/shadcn/button-group/button-group-text.svelte +28 -0
  77. package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
  78. package/src/shadcn/button-group/button-group.svelte +46 -0
  79. package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
  80. package/src/shadcn/button-group/index.ts +15 -0
  81. package/src/shadcn/checkbox/checkbox-box.ts +4 -0
  82. package/src/shadcn/checkbox/checkbox.svelte +2 -4
  83. package/src/shadcn/input-group/index.ts +22 -0
  84. package/src/shadcn/input-group/input-group-addon.svelte +51 -0
  85. package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
  86. package/src/shadcn/input-group/input-group-button.svelte +49 -0
  87. package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
  88. package/src/shadcn/input-group/input-group-input.svelte +20 -0
  89. package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
  90. package/src/shadcn/input-group/input-group-text.svelte +19 -0
  91. package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
  92. package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
  93. package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
  94. package/src/shadcn/input-group/input-group.svelte +24 -0
  95. package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
  96. package/src/shadcn/item/index.ts +34 -0
  97. package/src/shadcn/item/item-actions.svelte +20 -0
  98. package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
  99. package/src/shadcn/item/item-content.svelte +23 -0
  100. package/src/shadcn/item/item-content.svelte.d.ts +5 -0
  101. package/src/shadcn/item/item-description.svelte +23 -0
  102. package/src/shadcn/item/item-description.svelte.d.ts +5 -0
  103. package/src/shadcn/item/item-footer.svelte +20 -0
  104. package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
  105. package/src/shadcn/item/item-group.svelte +21 -0
  106. package/src/shadcn/item/item-group.svelte.d.ts +5 -0
  107. package/src/shadcn/item/item-header.svelte +20 -0
  108. package/src/shadcn/item/item-header.svelte.d.ts +5 -0
  109. package/src/shadcn/item/item-media.svelte +42 -0
  110. package/src/shadcn/item/item-media.svelte.d.ts +17 -0
  111. package/src/shadcn/item/item-separator.svelte +19 -0
  112. package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
  113. package/src/shadcn/item/item-title.svelte +20 -0
  114. package/src/shadcn/item/item-title.svelte.d.ts +5 -0
  115. package/src/shadcn/item/item.svelte +61 -0
  116. package/src/shadcn/item/item.svelte.d.ts +28 -0
  117. package/src/shadcn/native-select/index.ts +13 -0
  118. package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
  119. package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
  120. package/src/shadcn/native-select/native-select-option.svelte +20 -0
  121. package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
  122. package/src/shadcn/native-select/native-select.svelte +45 -0
  123. package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
  124. package/src/shadcn/pagination/index.ts +31 -0
  125. package/src/shadcn/pagination/pagination-content.svelte +20 -0
  126. package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
  127. package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
  128. package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
  129. package/src/shadcn/pagination/pagination-item.svelte +14 -0
  130. package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
  131. package/src/shadcn/pagination/pagination-link.svelte +41 -0
  132. package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
  133. package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
  134. package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
  135. package/src/shadcn/pagination/pagination-next.svelte +26 -0
  136. package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
  137. package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
  138. package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
  139. package/src/shadcn/pagination/pagination-previous.svelte +30 -0
  140. package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
  141. package/src/shadcn/pagination/pagination.svelte +27 -0
  142. package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
  143. package/src/slides/Controls.svelte +39 -36
  144. package/src/slides/Deck.svelte +23 -10
  145. package/src/slides/Overview.svelte +10 -4
  146. package/src/slides/README.md +1 -1
  147. package/src/slides/Slide.svelte +17 -11
  148. package/src/slides/deck-controller.svelte.ts +62 -16
  149. package/src/slides/layout.ts +3 -2
  150. package/src/slides/theme.css +34 -5
  151. package/src/thread/thread.svelte +15 -8
  152. package/src/work/detail/task-detail.svelte +1 -1
  153. package/src/work/gantt/gantt-task-row.svelte +3 -6
  154. package/src/work/inbox/inbox-card.svelte +7 -5
  155. package/src/work/inbox/inbox-trace.svelte +14 -10
  156. package/src/work/kanban-board.svelte +40 -25
  157. package/src/work/kanban-hidden-rail.svelte +67 -65
  158. package/src/work/task-list.svelte +145 -124
  159. package/src/work/task-list.svelte.test.ts +5 -2
  160. package/src/work/time/time-tracker.svelte +35 -10
@@ -152,19 +152,19 @@
152
152
  transition-delay: 0s;
153
153
  }
154
154
 
155
- /* The overview shows every slide. They all leave the stack at once and settle in turn, later ones slower. */
156
- :global(.deck[data-view="overview"]) .slide {
155
+ /*
156
+ * The overview shows every slide. Opening and closing move them as one rigid grid, like a
157
+ * camera pulling back or pushing in, so no slide gets its own delay.
158
+ */
159
+ :global(.deck[data-view="overview"]) .slide,
160
+ :global(.deck[data-zoom]) .slide {
157
161
  visibility: visible;
162
+ transition-delay: 0s;
158
163
  }
159
- :global(.deck[data-morphing]) .slide {
164
+ :global(.deck[data-zoom="run"]) .slide {
160
165
  transition:
161
- visibility 0s linear calc(var(--deck-t-morph) + 10 * var(--deck-stagger)),
162
- transform calc(var(--deck-t-morph) + min(var(--i), 10) * var(--deck-stagger))
163
- var(--deck-ease);
164
- }
165
- /* Opening shows the slides at once, so only closing waits before hiding them. */
166
- :global(.deck[data-morphing][data-view="overview"]) .slide {
167
- transition-delay: 0s;
166
+ visibility 0s,
167
+ transform var(--deck-t-zoom) var(--deck-ease);
168
168
  }
169
169
 
170
170
  .slide__surface {
@@ -178,11 +178,17 @@
178
178
  font-family: var(--deck-font);
179
179
  animation: var(--deck-t-slide) var(--deck-ease) backwards;
180
180
  }
181
- :global(.deck[data-view="overview"]) .slide__surface {
181
+ /* Framed on one slide the grid looks like that slide, so it keeps a slide's square corners. */
182
+ :global(.deck[data-view="overview"]:not([data-zoom="hold"])) .slide__surface {
182
183
  /* Design px, so divide by the scale to get the same corner on every thumbnail. */
183
184
  border-radius: calc(10px / var(--slide-scale, 1));
184
185
  box-shadow: 0 0 0 calc(1px / var(--slide-scale, 1)) var(--deck-line);
185
186
  }
187
+ :global(.deck[data-zoom="run"]) .slide__surface {
188
+ transition:
189
+ border-radius var(--deck-t-zoom) var(--deck-ease),
190
+ box-shadow var(--deck-t-zoom) var(--deck-ease);
191
+ }
186
192
 
187
193
  .slide__surface[data-align="center"],
188
194
  .slide__surface[data-align="start"] {
@@ -44,16 +44,34 @@ export type ControllerOptions = {
44
44
  labels?: Partial<DeckLabels>;
45
45
  /** Wraps a position change so a View Transition can snapshot around it. */
46
46
  transition?: (update: () => void) => void;
47
+ /** How long the overview zoom plays, in ms. 0, or left out, swaps at once. */
48
+ zoomMs?: () => number;
47
49
  };
48
50
 
51
+ /**
52
+ * The overview zoom while it plays. `index` is the slide the camera frames. `framed` puts that
53
+ * slide in its full-size spot with the grid around it, and `running` lets the boxes transition.
54
+ */
55
+ export type Zoom = { index: number; framed: boolean; running: boolean };
56
+
49
57
  /** Height the controls bar takes at the bottom of the overview, in px. */
50
58
  const CONTROLS_ROOM = 56;
51
59
 
52
- /** How long the overview morph flag stays up, a little past the longest stagger. */
53
- const MORPH_MS = 900;
54
-
55
60
  const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
56
61
 
62
+ /**
63
+ * Where `cell` lands when the camera moves `target` onto `fit`. Every box shares one scale and
64
+ * shift, and a transform tween is linear in both, so the grid stays rigid while it zooms.
65
+ */
66
+ function frame(cell: Box, target: Box, fit: Box): Box {
67
+ const k = fit.scale / target.scale;
68
+ return {
69
+ x: fit.x + (cell.x - target.x) * k,
70
+ y: fit.y + (cell.y - target.y) * k,
71
+ scale: cell.scale * k,
72
+ };
73
+ }
74
+
57
75
  function isEditable(el: HTMLElement) {
58
76
  return el.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName);
59
77
  }
@@ -71,12 +89,13 @@ export class DeckController {
71
89
  /** The highlighted thumbnail in the overview, as an index among the visible slides. */
72
90
  cursor = $state(0);
73
91
  direction = $state<1 | -1>(1);
74
- morphing = $state(false);
92
+ zoom = $state<Zoom | null>(null);
75
93
  size = $state<Size>({ w: 0, h: 0 });
76
94
 
77
95
  readonly options!: ControllerOptions;
78
96
  #listeners = new Set<Listener>();
79
97
  #timer: ReturnType<typeof setTimeout> | undefined;
98
+ #frame = 0;
80
99
 
81
100
  constructor(options: ControllerOptions) {
82
101
  this.options = options;
@@ -116,8 +135,13 @@ export class DeckController {
116
135
 
117
136
  /** Where slide `index` sits right now: centred to fit, or in its overview cell. */
118
137
  boxOf(index: number): Box | undefined {
119
- if (!this.overview) return this.fit;
120
- return this.grid.boxes[this.visible.indexOf(index)];
138
+ const zoom = this.zoom;
139
+ const cell = this.grid.boxes[this.visible.indexOf(index)];
140
+ if (zoom?.framed) {
141
+ const target = this.grid.boxes[this.visible.indexOf(zoom.index)];
142
+ return cell && target ? frame(cell, target, this.fit) : this.fit;
143
+ }
144
+ return this.overview || zoom ? cell : this.fit;
121
145
  }
122
146
 
123
147
  register(entry: SlideEntry): () => void {
@@ -137,7 +161,7 @@ export class DeckController {
137
161
  }
138
162
 
139
163
  mount(): () => void {
140
- return () => clearTimeout(this.#timer);
164
+ return () => this.#stopZoom();
141
165
  }
142
166
 
143
167
  /** Which deck hears a key is the page's call: Deck passes only keys from focus inside it. */
@@ -150,14 +174,16 @@ export class DeckController {
150
174
  this.options.step = position.step;
151
175
  }
152
176
 
153
- commit(target: Position | null, source: Source = "local") {
177
+ /** `animate: false` skips the slide transition, for a move the overview zoom already shows. */
178
+ commit(target: Position | null, source: Source = "local", animate = true) {
154
179
  if (!target) return;
155
180
  const from = this.position;
156
181
  const to = settle(this.slides, target);
157
182
  if (to.index === from.index && to.step === from.step) return;
158
183
  this.direction = to.index >= from.index ? 1 : -1;
159
184
  const apply = () => this.#write(to);
160
- if (to.index !== from.index && this.options.transition) this.options.transition(apply);
185
+ if (animate && to.index !== from.index && this.options.transition)
186
+ this.options.transition(apply);
161
187
  else apply();
162
188
  for (const listener of this.#listeners) listener(to, source);
163
189
  }
@@ -174,25 +200,45 @@ export class DeckController {
174
200
  this.commit({ index, step }, source);
175
201
  }
176
202
 
203
+ /**
204
+ * Opening zooms the camera out from the current slide to the whole grid. Closing zooms into
205
+ * the current slide, so `select` moves there first.
206
+ */
177
207
  toggleOverview(force?: boolean) {
178
208
  const open = force ?? !this.overview;
179
209
  if (open === this.overview) return;
180
- if (open) this.cursor = Math.max(0, this.visible.indexOf(this.position.index));
210
+ const index = this.position.index;
211
+ if (open) this.cursor = Math.max(0, this.visible.indexOf(index));
181
212
  this.view = open ? "overview" : "slide";
182
- this.#morph();
213
+ this.#stopZoom();
214
+ const ms = this.options.zoomMs?.() ?? 0;
215
+ if (ms <= 0 || !this.visible.includes(index)) return;
216
+
217
+ /*
218
+ * Each way paints its start state first and moves on a later frame. Moved in the same
219
+ * flush, a style read in between (the overview handing focus back) commits the boxes
220
+ * before the transition rule is on, and the zoom jumps to its end.
221
+ */
222
+ this.zoom = { index, framed: open, running: !open };
223
+ this.#frame = requestAnimationFrame(() => {
224
+ this.#frame = requestAnimationFrame(() => {
225
+ this.zoom = { index, framed: !open, running: true };
226
+ this.#timer = setTimeout(() => this.#stopZoom(), ms);
227
+ });
228
+ });
183
229
  }
184
230
 
185
231
  /** Leaves the overview on slide `index`. Picking the current slide keeps its step. */
186
232
  select(index: number) {
187
- this.toggleOverview(false);
188
233
  const keep = index === this.position.index;
189
- this.go(index, keep ? this.position.step : 0);
234
+ this.commit({ index, step: keep ? this.position.step : 0 }, "local", false);
235
+ this.toggleOverview(false);
190
236
  }
191
237
 
192
- #morph() {
193
- this.morphing = true;
238
+ #stopZoom() {
194
239
  clearTimeout(this.#timer);
195
- this.#timer = setTimeout(() => (this.morphing = false), MORPH_MS);
240
+ cancelAnimationFrame(this.#frame);
241
+ this.zoom = null;
196
242
  }
197
243
 
198
244
  #moveCursor(to: number) {
@@ -25,7 +25,8 @@ function scaleFor(
25
25
  ): number {
26
26
  const rows = Math.ceil(count / cols);
27
27
  const byWidth = (view.w - 2 * pad - (cols - 1) * gap) / cols / canvas.w;
28
- const byHeight = (view.h - 2 * pad - (rows - 1) * gap) / rows / canvas.h;
28
+ /* Every row, the last too, keeps a gap under it for its numbers. */
29
+ const byHeight = (view.h - 2 * pad - rows * gap) / rows / canvas.h;
29
30
  return Math.min(byWidth, byHeight);
30
31
  }
31
32
 
@@ -47,7 +48,7 @@ export function gridBoxes(view: Size, canvas: Canvas, count: number): Grid {
47
48
  const cellH = canvas.h * scale;
48
49
  const rows = Math.ceil(count / cols);
49
50
  const left = (view.w - (cols * cellW + (cols - 1) * gap)) / 2;
50
- const top = (view.h - (rows * cellH + (rows - 1) * gap)) / 2;
51
+ const top = (view.h - rows * (cellH + gap)) / 2;
51
52
 
52
53
  const boxes = Array.from({ length: count }, (_, i) => ({
53
54
  x: left + (i % cols) * (cellW + gap),
@@ -2,7 +2,8 @@
2
2
 
3
3
  /*
4
4
  * Everything global to a deck, in design px since the whole canvas scales, and in flat
5
- * hex because Chromium's PDF pipeline shifts oklch.
5
+ * hex because Chromium's PDF pipeline shifts oklch. The accent comes from the host app's
6
+ * `--primary`, which is often oklch, so it goes through `color-mix(in srgb)` to land as sRGB.
6
7
  */
7
8
 
8
9
  @layer components {
@@ -12,8 +13,30 @@
12
13
 
13
14
  --deck-bg: light-dark(#f2f1ec, #0e1014);
14
15
  --deck-fg: light-dark(#15171b, #f3f1ea);
15
- --deck-accent: light-dark(#3e6b00, #c8f15a);
16
- --deck-on-accent: light-dark(#f6f6f0, #0e1014);
16
+ /* Without a `--primary` the accent is the deck's own ink, so nothing ships a colour the app never chose. */
17
+ /*
18
+ * `--primary` follows the page's theme and the deck sets its own scheme: a dark deck on a
19
+ * light page got the page's dark primary on a dark slide (2.7:1). The brand pair is picked
20
+ * by the deck's scheme; `--primary` is the fallback for an app without the kit palette.
21
+ */
22
+ --deck-accent: color-mix(
23
+ in srgb,
24
+ light-dark(
25
+ var(--brand-primary, var(--primary, #15171b)),
26
+ var(--brand-primary-on-dark, var(--primary, #f3f1ea))
27
+ )
28
+ 100%,
29
+ #000
30
+ );
31
+ --deck-on-accent: color-mix(
32
+ in srgb,
33
+ light-dark(
34
+ var(--brand-primary-foreground, var(--primary-foreground, #f6f6f0)),
35
+ var(--brand-primary-foreground-on-dark, var(--primary-foreground, #0e1014))
36
+ )
37
+ 100%,
38
+ #000
39
+ );
17
40
  --deck-muted: color-mix(in srgb, var(--deck-fg) 66%, transparent);
18
41
  --deck-line: color-mix(in srgb, var(--deck-fg) 14%, transparent);
19
42
  --deck-surface: color-mix(in srgb, var(--deck-fg) 6%, transparent);
@@ -32,6 +55,8 @@
32
55
 
33
56
  --deck-t-slide: var(--duration-fast, 250ms);
34
57
  --deck-t-morph: var(--duration-slow, 300ms);
58
+ /* The overview zoom moves the whole canvas, so it takes the overlay length, not the UI ceiling. */
59
+ --deck-t-zoom: var(--duration-overlay, 500ms);
35
60
  --deck-t-quick: var(--duration-quick, 150ms);
36
61
  --deck-stagger: var(--duration-stagger, 40ms);
37
62
  --deck-ease: var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
@@ -124,8 +149,12 @@
124
149
  animation-name: deck-zoom;
125
150
  }
126
151
 
127
- .deck[data-morphing] .slide__surface {
128
- animation: none !important;
152
+ /*
153
+ * The zoom already shows the slide arriving, so its enter animation finishes at once. A
154
+ * 0s duration, not `animation: none`, which would replay the enter when the zoom ends.
155
+ */
156
+ .deck[data-zoom] .slide__surface {
157
+ animation-duration: 0s !important;
129
158
  }
130
159
 
131
160
  /* A morph runs on the whole page, so it gets the deck's timing while it plays. */
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
+ import { Toggle } from "bits-ui";
3
4
  import { ArrowBendDownRight, Check, MapPin, PencilSimple, Trash } from "phosphor-svelte";
4
5
  import { cn } from "../lib/utils.js";
5
6
  import { focusAfterRender } from "../lib/focus.js";
@@ -338,14 +339,20 @@
338
339
  {labels.title}
339
340
  </h2>
340
341
  {#if resolved > 0}
341
- <Button
342
- variant="ghost"
343
- size="sm"
344
- aria-expanded={showResolved}
345
- onclick={() => (showResolved = !showResolved)}
346
- >
347
- {showResolved ? labels.hideResolved : fillCount(labels.showResolved, resolved)}
348
- </Button>
342
+ <!-- A filter on the list, so it reports pressed, not expanded. The wash it had as an
343
+ expanded ghost button moves to aria-pressed. -->
344
+ <Toggle.Root bind:pressed={showResolved}>
345
+ {#snippet child({ props })}
346
+ <Button
347
+ {...props}
348
+ variant="ghost"
349
+ size="sm"
350
+ class="aria-pressed:bg-foreground/6 aria-pressed:text-foreground"
351
+ >
352
+ {showResolved ? labels.hideResolved : fillCount(labels.showResolved, resolved)}
353
+ </Button>
354
+ {/snippet}
355
+ </Toggle.Root>
349
356
  {/if}
350
357
  </div>
351
358
 
@@ -159,7 +159,7 @@
159
159
  variant="text"
160
160
  onclick={startTitle}
161
161
  disabled={!onTitleChange}
162
- aria-label={labels.editTitle}
162
+ aria-label="{labels.editTitle}: {title}"
163
163
  class={cn(
164
164
  "group -mx-2 -my-1 px-2 py-1 text-2xl leading-tight font-semibold tracking-tight disabled:opacity-100",
165
165
  !onTitleChange && "hover:bg-transparent",
@@ -140,9 +140,8 @@
140
140
  </button>
141
141
  {:else if !row.own}
142
142
  <!-- A parent with no dates of its own: the span of what is below it. Not draggable. -->
143
- <div
144
- role="button"
145
- tabindex="0"
143
+ <button
144
+ type="button"
146
145
  aria-label="{title}, {describe(span)}"
147
146
  title="{title}, {describe(span)}"
148
147
  class={cn(
@@ -155,11 +154,9 @@
155
154
  style:width="{barWidth(span, layout)}px"
156
155
  style:height="{Math.max(layout.rowHeight - 24, 10)}px"
157
156
  onclick={() => onSelect?.(row.id)}
158
- onkeydown={(e) =>
159
- (e.key === "Enter" || e.key === " ") && (e.preventDefault(), onSelect?.(row.id))}
160
157
  onpointerenter={() => onHover(row.id, true)}
161
158
  onpointerleave={() => onHover(row.id, false)}
162
- ></div>
159
+ ></button>
163
160
  {:else}
164
161
  <GanttBar
165
162
  id={row.id}
@@ -28,6 +28,7 @@
28
28
  import InboxTraceView from "./inbox-trace.svelte";
29
29
  import { TagChip } from "../../components/chip/index.js";
30
30
  import { workLooks } from "../../components/work-parts/looks.js";
31
+ import { measureClamp } from "../../record/measure-clamp.js";
31
32
  import { FolderSimple } from "phosphor-svelte";
32
33
  import { Copied } from "../../components/copy/copied.svelte.js";
33
34
  import type { InboxDraftEdits, InboxItem, InboxVerdict } from "./inbox-types.js";
@@ -68,10 +69,7 @@
68
69
  // for a link the host already shows.
69
70
  let hasTrace = $derived(item.trace.evidence.length > 0 || !!why);
70
71
  let expanded = $state(false);
71
- let longCommand = $derived(
72
- !!facts.command &&
73
- (facts.command.text.length > 240 || facts.command.text.split("\n").length > 3),
74
- );
72
+ let commandCut = $state(false);
75
73
 
76
74
  let pickedDraft = $state<string>();
77
75
  let edits = $state<InboxDraftEdits>({});
@@ -210,13 +208,17 @@
210
208
  <!-- The padding sits on a wrapper: on the clamped box itself, the 4th line showed through it. -->
211
209
  <div class={cn(workLooks.inset, "w-full px-2.5 py-2")}>
212
210
  <pre
211
+ {@attach measureClamp(
212
+ () => expanded,
213
+ (cut) => (commandCut = cut),
214
+ )}
213
215
  class={cn(
214
216
  "text-foreground font-mono text-xs leading-5 break-words whitespace-pre-wrap",
215
217
  !expanded && "line-clamp-3",
216
218
  )}><span class="text-muted-foreground select-none"
217
219
  >{facts.command.tool} </span>{facts.command.text}</pre>
218
220
  </div>
219
- {#if longCommand}
221
+ {#if commandCut || expanded}
220
222
  <button
221
223
  type="button"
222
224
  class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
@@ -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)}