@heyramzi/ui 2.1.0 → 2.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 (312) hide show
  1. package/package.json +7 -8
  2. package/scripts/sync-shadcn.mjs +3 -4
  3. package/src/agents/agent-avatar.svelte +604 -575
  4. package/src/agents/agent-cast.svelte +179 -183
  5. package/src/agents/demo.svelte +125 -113
  6. package/src/agents/helpers.ts +23 -17
  7. package/src/agents/index.ts +4 -4
  8. package/src/agents/page-state.ts +6 -6
  9. package/src/agents/types.ts +6 -6
  10. package/src/ai/ai-button/AiButton.svelte +4 -12
  11. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  12. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  13. package/src/ai/response/Response.css +2 -3
  14. package/src/animations/avatar-group-hover.css +2 -16
  15. package/src/animations/card-tilt.css +3 -6
  16. package/src/animations/digit-pop-in.css +2 -15
  17. package/src/animations/dropdown.css +4 -17
  18. package/src/animations/input-clear-dissolve.css +5 -18
  19. package/src/animations/input-shake.css +9 -41
  20. package/src/animations/learn-more-hover.css +17 -9
  21. package/src/animations/like-button.css +2 -4
  22. package/src/animations/page-side-by-side.css +2 -10
  23. package/src/animations/plus-menu-morph.css +2 -5
  24. package/src/animations/shimmer-text.css +5 -18
  25. package/src/animations/spinning-counter.css +2 -6
  26. package/src/animations/success-check.css +2 -15
  27. package/src/animations/thinking-states.css +2 -7
  28. package/src/brand/brand.test.ts +21 -5
  29. package/src/brand/index.ts +4 -10
  30. package/src/canvas/card/card-frame.svelte +43 -39
  31. package/src/canvas/card/resize-handles.svelte +89 -80
  32. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  33. package/src/canvas/chrome/lock-badge.svelte +8 -8
  34. package/src/canvas/chrome/side-handles.svelte +24 -19
  35. package/src/canvas/chrome/size-chip.svelte +30 -30
  36. package/src/canvas/chrome/snap-guides.svelte +20 -20
  37. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  38. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  39. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  40. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  41. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  42. package/src/canvas/edges/bend-handle.svelte +46 -46
  43. package/src/canvas/edges/edge-gradient.svelte +10 -10
  44. package/src/canvas/edges/gradient-edge.svelte +45 -29
  45. package/src/canvas/editor/lock.test.ts +6 -1
  46. package/src/canvas/editor/style.ts +7 -1
  47. package/src/canvas/headless/svg-figure.test.ts +5 -1
  48. package/src/canvas/lod/lod-switch.svelte +111 -106
  49. package/src/canvas/pen/PenField.svelte +87 -81
  50. package/src/canvas/pen/PenOverlay.svelte +127 -127
  51. package/src/canvas/pen/ShapeNode.svelte +31 -32
  52. package/src/canvas/pen/TextNode.svelte +54 -53
  53. package/src/canvas/pen/host-field.test.svelte +7 -7
  54. package/src/canvas/pen/shapes.ts +7 -1
  55. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  56. package/src/canvas/screen/PartView.svelte +138 -72
  57. package/src/canvas/screen/ScreenBody.svelte +81 -60
  58. package/src/canvas/screen/ScreenCard.svelte +20 -20
  59. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  60. package/src/canvas/ui/FrameLayer.svelte +59 -59
  61. package/src/canvas/ui/PenLayer.svelte +39 -30
  62. package/src/canvas/ui/StylePanel.svelte +119 -102
  63. package/src/canvas/ui/canvas-grid.svelte +137 -129
  64. package/src/charts/animated-value.svelte +1 -1
  65. package/src/charts/area-chart.css +2 -5
  66. package/src/charts/area-chart.svelte +1 -1
  67. package/src/charts/bar-chart.css +2 -3
  68. package/src/charts/bar-chart.svelte +1 -1
  69. package/src/charts/charts.css +5 -17
  70. package/src/charts/crosshair.svelte +1 -1
  71. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  72. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  73. package/src/charts/dashboard/data-panel.svelte +50 -50
  74. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  75. package/src/charts/dashboard/data-table.svelte +138 -138
  76. package/src/charts/dashboard/panel-grid.svelte +30 -30
  77. package/src/charts/dashboard/period-filter.svelte +25 -25
  78. package/src/charts/dashboard/rating-chip.svelte +19 -19
  79. package/src/charts/dashboard/stat-band.svelte +32 -32
  80. package/src/charts/dashboard/stat-tile.svelte +106 -99
  81. package/src/charts/donut-chart.svelte +1 -1
  82. package/src/charts/edge-fade.svelte +1 -1
  83. package/src/charts/figure.svelte +1 -1
  84. package/src/charts/gauge-chart.css +4 -7
  85. package/src/charts/gauge-chart.svelte +1 -1
  86. package/src/charts/hover-focus.svelte +1 -1
  87. package/src/charts/legend.css +1 -3
  88. package/src/charts/legend.svelte +1 -1
  89. package/src/charts/line-chart.svelte +1 -1
  90. package/src/charts/number-chart.css +6 -9
  91. package/src/charts/number-chart.svelte +1 -1
  92. package/src/charts/reference-line.svelte +19 -19
  93. package/src/charts/reveal.svelte +1 -1
  94. package/src/charts/ring-chart.css +2 -4
  95. package/src/charts/ring-chart.svelte +1 -1
  96. package/src/charts/sparkline.svelte +5 -2
  97. package/src/charts/stat-card.svelte +1 -1
  98. package/src/charts/time-series-chart.css +2 -5
  99. package/src/charts/trend-pill.css +2 -4
  100. package/src/charts/trend-pill.svelte +1 -1
  101. package/src/components/copy/copy-button.svelte +93 -98
  102. package/src/components/copy/copy-button.test.ts +10 -2
  103. package/src/components/data-table/infinite-scroll.svelte +42 -42
  104. package/src/components/data-table/pagination.svelte +34 -22
  105. package/src/components/data-table/row-arrow.svelte +19 -16
  106. package/src/components/data-table/row-context-menu.svelte +52 -52
  107. package/src/components/data-table/sortable-head.svelte +25 -25
  108. package/src/components/data-table/table-skeleton.svelte +41 -36
  109. package/src/components/docs/doc-page.svelte +18 -9
  110. package/src/components/docs/on-this-page.svelte +16 -3
  111. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  112. package/src/components/field-row/field-row.svelte +10 -8
  113. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  114. package/src/components/icon/icon.svelte +14 -14
  115. package/src/components/language-flag/language-flag.svelte +3 -1
  116. package/src/components/list-page/detail-empty.svelte +34 -34
  117. package/src/components/list-page/detail-header.svelte +55 -47
  118. package/src/components/list-page/detail-panel.svelte +51 -59
  119. package/src/components/list-page/detail-row.svelte +19 -19
  120. package/src/components/list-page/detail-stack.svelte +21 -21
  121. package/src/components/list-page/empty-state.svelte +30 -30
  122. package/src/components/list-page/filter-item.svelte +20 -20
  123. package/src/components/list-page/filter-popover.svelte +60 -55
  124. package/src/components/list-page/grouped-list.svelte +4 -2
  125. package/src/components/list-page/index.ts +5 -1
  126. package/src/components/list-page/list-page-shell.svelte +2 -5
  127. package/src/components/list-page/master-detail.svelte +87 -88
  128. package/src/components/list-page/notice-bar.svelte +38 -38
  129. package/src/components/list-page/prev-next.svelte +63 -63
  130. package/src/components/list-page/rail-body.svelte +18 -18
  131. package/src/components/list-page/rail-empty.svelte +23 -30
  132. package/src/components/list-page/rail-head.svelte +24 -24
  133. package/src/components/list-page/rail-row.svelte +77 -73
  134. package/src/components/list-page/section-host.ts +2 -3
  135. package/src/components/locale-switch/locale-switch.svelte +41 -41
  136. package/src/components/mark/mark.svelte +34 -38
  137. package/src/components/open-in/index.ts +1 -5
  138. package/src/components/open-in/open-in.svelte +40 -40
  139. package/src/components/picker/mark-picker.svelte +49 -49
  140. package/src/components/picker/picker.svelte +5 -3
  141. package/src/components/section-header/section-header.test.ts +7 -1
  142. package/src/components/section-header/section-label.svelte +20 -20
  143. package/src/components/segmented/animated-tabs.svelte +31 -2
  144. package/src/components/segmented/collapse.test.ts +2 -1
  145. package/src/components/segmented/index.ts +2 -0
  146. package/src/components/segmented/segment.svelte +52 -49
  147. package/src/components/segmented/segmented-filter.svelte +79 -80
  148. package/src/components/segmented/segmented-nav.svelte +34 -40
  149. package/src/components/segmented/segmented-overflow.svelte +53 -50
  150. package/src/components/segmented/segmented-track.svelte +62 -46
  151. package/src/components/segmented/segmented.ts +2 -3
  152. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  153. package/src/components/svg/svg.svelte +4 -4
  154. package/src/components/table-view/table-view.svelte +17 -2
  155. package/src/components/table-view/table-view.test.ts +7 -1
  156. package/src/components/table-view/types.ts +11 -11
  157. package/src/components/tags-input/index.ts +13 -0
  158. package/src/components/tags-input/tags-input.svelte +305 -0
  159. package/src/components/tags-input/tags.test.ts +64 -0
  160. package/src/components/tags-input/tags.ts +142 -0
  161. package/src/components/theme/index.ts +2 -4
  162. package/src/components/theme/theme-provider.svelte +1 -3
  163. package/src/components/toolbar/toolbar-search.svelte +25 -25
  164. package/src/components/toolbar/toolbar.svelte +11 -7
  165. package/src/components/toolbar/toolbar.test.ts +10 -1
  166. package/src/design/index.css +21 -78
  167. package/src/design/polish.css +26 -35
  168. package/src/editor/editor-toolbar.svelte +104 -99
  169. package/src/editor/editor.svelte +296 -266
  170. package/src/editor/image-toolbar.svelte +20 -22
  171. package/src/editor/index.ts +1 -4
  172. package/src/editor/inline-editor.svelte +280 -238
  173. package/src/editor/markdown.ts +15 -15
  174. package/src/editor/renderer.svelte +71 -71
  175. package/src/editor/slash-menu.svelte +24 -27
  176. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  177. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  178. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  179. package/src/effects/magic-card/MagicCard.css +14 -7
  180. package/src/effects/marquee/Marquee.svelte +3 -5
  181. package/src/effects/reveal/observe.ts +0 -3
  182. package/src/effects/spotlight/Spotlight.svelte +1 -1
  183. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  184. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  185. package/src/gallery.test.ts +14 -4
  186. package/src/inbox/README.md +1 -3
  187. package/src/inbox/inbox.test.ts +181 -138
  188. package/src/inbox/index.ts +20 -20
  189. package/src/inbox/logic.ts +114 -111
  190. package/src/inbox/thread-inbox.svelte +227 -151
  191. package/src/inbox/types.ts +130 -108
  192. package/src/lib/theme-transition.ts +2 -4
  193. package/src/lib/tone.ts +2 -4
  194. package/src/lib/utils.ts +2 -2
  195. package/src/modules.test.ts +79 -69
  196. package/src/onboarding/define-tours.test.ts +55 -40
  197. package/src/onboarding/define-tours.ts +23 -23
  198. package/src/onboarding/demo.svelte +24 -5
  199. package/src/onboarding/index.ts +1 -8
  200. package/src/onboarding/onboarding-provider.svelte +19 -7
  201. package/src/onboarding/onboarding.svelte.ts +16 -16
  202. package/src/onboarding/pick.test.ts +61 -52
  203. package/src/onboarding/pick.ts +16 -13
  204. package/src/onboarding/stores.test.ts +80 -80
  205. package/src/onboarding/stores.ts +41 -39
  206. package/src/onboarding/tour-card.svelte +30 -5
  207. package/src/onboarding/tour-runner.svelte +12 -3
  208. package/src/onboarding/types.ts +20 -12
  209. package/src/onboarding/wait.ts +22 -22
  210. package/src/panel/side-panel.svelte +94 -93
  211. package/src/record/demo-data.ts +291 -225
  212. package/src/record/field-section.svelte +38 -34
  213. package/src/record/fields.ts +148 -96
  214. package/src/record/format.ts +63 -56
  215. package/src/record/index.ts +49 -35
  216. package/src/record/info-list.svelte +145 -114
  217. package/src/record/kinds.ts +31 -31
  218. package/src/record/logic.ts +216 -214
  219. package/src/record/next-up.svelte +45 -34
  220. package/src/record/project-list.svelte +56 -50
  221. package/src/record/record-demo.svelte +130 -110
  222. package/src/record/record-header.svelte +37 -28
  223. package/src/record/record-shell.svelte +54 -54
  224. package/src/record/record-stats.svelte +25 -25
  225. package/src/record/record-timeline.svelte +134 -114
  226. package/src/record/record.test.ts +430 -309
  227. package/src/record/steps-rail.svelte +225 -216
  228. package/src/record/types.ts +234 -215
  229. package/src/record/where-we-are.svelte +62 -58
  230. package/src/shadcn/button/button.svelte +1 -1
  231. package/src/shadcn/card/card-title.svelte +1 -5
  232. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  233. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  234. package/src/shadcn/table/table-cell.svelte +2 -3
  235. package/src/shadcn/utilities.css +3 -8
  236. package/src/slides/Controls.svelte +150 -124
  237. package/src/slides/Deck.svelte +243 -238
  238. package/src/slides/Fragment.svelte +64 -57
  239. package/src/slides/Overview.svelte +106 -101
  240. package/src/slides/Presenter.svelte +300 -266
  241. package/src/slides/Slide.svelte +185 -170
  242. package/src/slides/SlideList.svelte +16 -12
  243. package/src/slides/ambient.d.ts +1 -1
  244. package/src/slides/bridges.ts +42 -23
  245. package/src/slides/deck-controller.svelte.ts +47 -35
  246. package/src/slides/demo-slides.svelte +330 -306
  247. package/src/slides/demo.svelte +164 -160
  248. package/src/slides/export/capture.ts +20 -16
  249. package/src/slides/export/ffmpeg.ts +43 -38
  250. package/src/slides/export/index.ts +5 -5
  251. package/src/slides/export/launch.ts +3 -1
  252. package/src/slides/export/pdf.ts +30 -27
  253. package/src/slides/export/types.ts +10 -8
  254. package/src/slides/index.ts +11 -11
  255. package/src/slides/labels.ts +22 -19
  256. package/src/slides/layout.ts +9 -2
  257. package/src/slides/model.ts +4 -4
  258. package/src/slides/navigation.ts +43 -31
  259. package/src/slides/theme.css +5 -7
  260. package/src/slides/types.ts +9 -10
  261. package/src/status/define-statuses.ts +17 -24
  262. package/src/status/index.ts +5 -5
  263. package/src/status/status-badge.svelte +3 -1
  264. package/src/status/status-dot.svelte +1 -3
  265. package/src/status/status-icon.svelte +1 -1
  266. package/src/status/status-types.ts +21 -21
  267. package/src/tasks/board-order.ts +2 -4
  268. package/src/tasks/demo.svelte +189 -31
  269. package/src/tasks/detail/detail-demo.svelte +73 -13
  270. package/src/tasks/detail/detail-types.ts +45 -45
  271. package/src/tasks/detail/task-detail.svelte +40 -10
  272. package/src/tasks/detail/task-property.svelte +8 -2
  273. package/src/tasks/detail/task-thread.svelte +53 -12
  274. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  275. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  276. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  277. package/src/tasks/gantt/gantt-header.svelte +9 -3
  278. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  279. package/src/tasks/gantt/gantt.css +2 -1
  280. package/src/tasks/gantt/gantt.test.ts +3 -1
  281. package/src/tasks/gantt/logic.test.ts +110 -20
  282. package/src/tasks/gantt/logic.ts +42 -6
  283. package/src/tasks/inbox/inbox-card.svelte +13 -5
  284. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  285. package/src/tasks/inbox/inbox-logic.ts +6 -40
  286. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  287. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  288. package/src/tasks/inbox/inbox-types.ts +54 -58
  289. package/src/tasks/inbox/index.ts +22 -21
  290. package/src/tasks/index.ts +49 -52
  291. package/src/tasks/kanban-board.svelte +16 -12
  292. package/src/tasks/kanban-types.ts +2 -3
  293. package/src/tasks/presets/sales.ts +6 -6
  294. package/src/tasks/task-gantt.svelte +28 -8
  295. package/src/tasks/task-list.svelte +41 -17
  296. package/src/tasks/time/duration.ts +40 -40
  297. package/src/tasks/time/time-chip.svelte +9 -2
  298. package/src/tasks/time/time-dot.svelte +1 -1
  299. package/src/tasks/time/time-ring.svelte +11 -3
  300. package/src/tasks/time/time-tracker.svelte +57 -15
  301. package/src/tasks/time/time-types.ts +57 -57
  302. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  303. package/src/thread/index.ts +7 -7
  304. package/src/thread/logic.ts +36 -33
  305. package/src/thread/thread.svelte +82 -16
  306. package/src/thread/thread.test.ts +58 -47
  307. package/src/thread/types.ts +68 -68
  308. package/src/tokens/craft.css +17 -53
  309. package/src/tokens/index.css +3 -13
  310. package/src/tokens/palette.css +13 -11
  311. package/src/tokens/primitives.css +10 -23
  312. package/src/tasks/inbox/inbox.svelte +0 -203
@@ -11,7 +11,13 @@
11
11
 
12
12
  <!-- A labelled row in the rail. In the narrow chip row the label hides and
13
13
  names the group for assistive tech. -->
14
- <div class="flex min-h-7 items-center @3xl:grid @3xl:grid-cols-[6rem_minmax(0,1fr)]" role="group" aria-label={label}>
15
- <span class="hidden text-xs font-medium text-muted-foreground @3xl:block" aria-hidden="true">{label}</span>
14
+ <div
15
+ class="flex min-h-7 items-center @3xl:grid @3xl:grid-cols-[6rem_minmax(0,1fr)]"
16
+ role="group"
17
+ aria-label={label}
18
+ >
19
+ <span class="hidden text-xs font-medium text-muted-foreground @3xl:block" aria-hidden="true"
20
+ >{label}</span
21
+ >
16
22
  <div class="flex min-w-0 items-center">{@render children()}</div>
17
23
  </div>
@@ -5,7 +5,11 @@
5
5
  import { AgentAvatar } from "../../agents/index.js";
6
6
  import { TONE_CLASS } from "../../lib/tone.js";
7
7
  import { Skeleton } from "../../shadcn/skeleton/index.js";
8
- import { DEFAULT_TASK_DETAIL_LABELS, type TaskComment, type TaskDetailLabels } from "./detail-types.js";
8
+ import {
9
+ DEFAULT_TASK_DETAIL_LABELS,
10
+ type TaskComment,
11
+ type TaskDetailLabels,
12
+ } from "./detail-types.js";
9
13
 
10
14
  interface Props {
11
15
  comments: TaskComment[];
@@ -31,7 +35,8 @@
31
35
  labels: labelOverrides,
32
36
  agentAvatar,
33
37
  avatar,
34
- when = (comment) => new Date(comment.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }),
38
+ when = (comment) =>
39
+ new Date(comment.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric" }),
35
40
  onSend,
36
41
  onEdit,
37
42
  onDelete,
@@ -100,7 +105,10 @@
100
105
  {#each comments as comment (comment.id)}
101
106
  <li class="group relative flex gap-3">
102
107
  <!-- The rail that ties one avatar to the next, as in an activity feed. -->
103
- <span class="absolute start-[11.5px] top-8 -bottom-3 w-px bg-border group-last:hidden" aria-hidden="true"></span>
108
+ <span
109
+ class="absolute start-[11.5px] top-8 -bottom-3 w-px bg-border group-last:hidden"
110
+ aria-hidden="true"
111
+ ></span>
104
112
  <span class="relative size-6 shrink-0">
105
113
  {#if comment.agent}
106
114
  {#if agentAvatar}
@@ -111,7 +119,10 @@
111
119
  {:else if avatar}
112
120
  {@render avatar(comment)}
113
121
  {:else}
114
- <span class="grid size-6 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground" aria-hidden="true">
122
+ <span
123
+ class="grid size-6 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground"
124
+ aria-hidden="true"
125
+ >
115
126
  {initial(comment.author)}
116
127
  </span>
117
128
  {/if}
@@ -121,7 +132,12 @@
121
132
  <div class="flex h-6 items-center gap-2">
122
133
  <span class="truncate text-[13px] font-medium">{comment.author}</span>
123
134
  {#if comment.agent}
124
- <span class={cn("inline-flex h-5 items-center rounded-sm border px-1.5 text-xs font-medium", TONE_CLASS.accent)}>
135
+ <span
136
+ class={cn(
137
+ "inline-flex h-5 items-center rounded-sm border px-1.5 text-xs font-medium",
138
+ TONE_CLASS.accent,
139
+ )}
140
+ >
125
141
  {labels.ai}
126
142
  </span>
127
143
  {/if}
@@ -130,14 +146,26 @@
130
146
  <span class="text-xs text-muted-foreground">{labels.commentEdited}</span>
131
147
  {/if}
132
148
  {#if canEdit(comment) && editingId !== comment.id}
133
- <span class="ms-auto flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100">
149
+ <span
150
+ class="ms-auto flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
151
+ >
134
152
  {#if onEdit}
135
- <button type="button" class="thread-icon" aria-label={labels.commentEdit} onclick={() => startEdit(comment)}>
153
+ <button
154
+ type="button"
155
+ class="thread-icon"
156
+ aria-label={labels.commentEdit}
157
+ onclick={() => startEdit(comment)}
158
+ >
136
159
  <PencilSimple class="size-3.5" aria-hidden="true" />
137
160
  </button>
138
161
  {/if}
139
162
  {#if onDelete}
140
- <button type="button" class="thread-icon" aria-label={labels.commentDelete} onclick={() => onDelete?.(comment)}>
163
+ <button
164
+ type="button"
165
+ class="thread-icon"
166
+ aria-label={labels.commentDelete}
167
+ onclick={() => onDelete?.(comment)}
168
+ >
141
169
  <Trash class="size-3.5" aria-hidden="true" />
142
170
  </button>
143
171
  {/if}
@@ -153,8 +181,14 @@
153
181
  class="mt-1 w-full resize-y rounded-md border border-input bg-transparent px-2.5 py-2 text-sm leading-relaxed outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
154
182
  ></textarea>
155
183
  <div class="mt-2 flex justify-end gap-1.5">
156
- <button type="button" class="thread-button" onclick={() => (editingId = null)}>{labels.cancel}</button>
157
- <button type="button" class="thread-button thread-primary" onclick={() => saveEdit(comment)}>{labels.save}</button>
184
+ <button type="button" class="thread-button" onclick={() => (editingId = null)}
185
+ >{labels.cancel}</button
186
+ >
187
+ <button
188
+ type="button"
189
+ class="thread-button thread-primary"
190
+ onclick={() => saveEdit(comment)}>{labels.save}</button
191
+ >
158
192
  </div>
159
193
  {:else}
160
194
  <p class="text-sm leading-relaxed whitespace-pre-wrap text-pretty">{comment.body}</p>
@@ -168,7 +202,9 @@
168
202
  {/if}
169
203
 
170
204
  {#if onSend}
171
- <div class="rounded-lg border border-input transition-shadow focus-within:ring-3 focus-within:ring-ring/50">
205
+ <div
206
+ class="rounded-lg border border-input transition-shadow focus-within:ring-3 focus-within:ring-ring/50"
207
+ >
172
208
  <textarea
173
209
  bind:value={draft}
174
210
  onkeydown={onComposerKeydown}
@@ -178,7 +214,12 @@
178
214
  class="block max-h-60 min-h-16 w-full resize-none border-0 bg-transparent px-3 py-2 text-sm leading-relaxed outline-none [field-sizing:content] placeholder:text-muted-foreground"
179
215
  ></textarea>
180
216
  <div class="flex justify-end px-2 pb-2">
181
- <button type="button" class="thread-button thread-primary" onclick={send} disabled={!canSend}>
217
+ <button
218
+ type="button"
219
+ class="thread-button thread-primary"
220
+ onclick={send}
221
+ disabled={!canSend}
222
+ >
182
223
  {sending ? labels.saving : labels.commentSend}
183
224
  </button>
184
225
  </div>
@@ -54,7 +54,9 @@
54
54
  function onPointerDown(e: PointerEvent, mode: Drag["mode"]): void {
55
55
  if (!editable || e.button !== 0) return;
56
56
  e.stopPropagation();
57
- (e.currentTarget as HTMLElement).closest<HTMLElement>("[data-bar]")?.setPointerCapture(e.pointerId);
57
+ (e.currentTarget as HTMLElement)
58
+ .closest<HTMLElement>("[data-bar]")
59
+ ?.setPointerCapture(e.pointerId);
58
60
  setDrag({ id, mode, originX: e.clientX, dx: 0 });
59
61
  }
60
62
 
@@ -95,7 +97,9 @@
95
97
  role="button"
96
98
  tabindex="0"
97
99
  aria-roledescription="task bar"
98
- aria-label="{title}, {describe(span)}.{editable ? ' Arrow keys move it a day, Shift plus arrow keys change its end.' : ''}"
100
+ aria-label="{title}, {describe(span)}.{editable
101
+ ? ' Arrow keys move it a day, Shift plus arrow keys change its end.'
102
+ : ''}"
99
103
  class={cn(
100
104
  "g-bar g-enter absolute top-1/2 flex -translate-y-1/2 items-center rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
101
105
  editable && "cursor-grab",
@@ -117,15 +121,21 @@
117
121
  {#if progress > 0}
118
122
  <!-- The clip keeps the bar's rounded corners while the fill scales. -->
119
123
  <span class="absolute inset-0 overflow-hidden rounded-lg" aria-hidden="true">
120
- <span class="g-progress absolute inset-0 origin-left" style:transform="scaleX({progress})"></span>
124
+ <span class="g-progress absolute inset-0 origin-left" style:transform="scaleX({progress})"
125
+ ></span>
121
126
  </span>
122
127
  {/if}
123
128
  {#if width >= inlineTitleMin}
124
129
  <span class="relative truncate px-2.5 text-xs font-medium text-foreground">{title}</span>
125
130
  {/if}
126
131
  {#if editable}
127
- <span class="g-handle left-0" aria-hidden="true" onpointerdown={(e) => onPointerDown(e, "start")}></span>
128
- <span class="g-handle right-0" aria-hidden="true" onpointerdown={(e) => onPointerDown(e, "end")}></span>
132
+ <span
133
+ class="g-handle left-0"
134
+ aria-hidden="true"
135
+ onpointerdown={(e) => onPointerDown(e, "start")}
136
+ ></span>
137
+ <span class="g-handle right-0" aria-hidden="true" onpointerdown={(e) => onPointerDown(e, "end")}
138
+ ></span>
129
139
  {/if}
130
140
  {#if dragging && drag && drag.dx !== 0}
131
141
  <span
@@ -182,7 +192,8 @@
182
192
 
183
193
  .g-progress {
184
194
  background: color-mix(in oklab, var(--g-hue) 32%, var(--color-card, white));
185
- transition: transform var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
195
+ transition: transform var(--duration-fast, 250ms)
196
+ var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
186
197
  }
187
198
 
188
199
  /* Resize grips: a wide invisible hit area, a slim pill that shows on hover. */
@@ -16,26 +16,55 @@
16
16
  </script>
17
17
 
18
18
  <!-- The layer behind every row: weekends, week lines, the today line and arrows. -->
19
- <div class="pointer-events-none absolute inset-y-0 left-(--g-label)" style:width="{layout.timelineWidth}px" aria-hidden="true">
19
+ <div
20
+ class="pointer-events-none absolute inset-y-0 left-(--g-label)"
21
+ style:width="{layout.timelineWidth}px"
22
+ aria-hidden="true"
23
+ >
20
24
  {#each days as d (d.day)}
21
25
  {#if d.weekend}
22
- <div class="absolute inset-y-0 bg-foreground/[0.025]" style:left="{d.i * layout.dayWidth}px" style:width="{layout.dayWidth}px"></div>
26
+ <div
27
+ class="absolute inset-y-0 bg-foreground/[0.025]"
28
+ style:left="{d.i * layout.dayWidth}px"
29
+ style:width="{layout.dayWidth}px"
30
+ ></div>
23
31
  {/if}
24
32
  {#if d.wd === 1}
25
- <div class="absolute inset-y-0 w-px bg-border/70" style:left="{d.i * layout.dayWidth}px"></div>
33
+ <div
34
+ class="absolute inset-y-0 w-px bg-border/70"
35
+ style:left="{d.i * layout.dayWidth}px"
36
+ ></div>
26
37
  {/if}
27
38
  {/each}
28
39
  {#if layout.todayDay >= layout.first && layout.todayDay <= layout.last}
29
- <div class="g-today absolute inset-y-0 w-px bg-primary" style:left="{dayX(layout, layout.todayDay) + layout.dayWidth / 2}px">
40
+ <div
41
+ class="g-today absolute inset-y-0 w-px bg-primary"
42
+ style:left="{dayX(layout, layout.todayDay) + layout.dayWidth / 2}px"
43
+ >
30
44
  <span class="absolute -top-1 -left-[3px] size-[7px] rounded-full bg-primary"></span>
31
45
  </div>
32
46
  {/if}
33
- <svg class="absolute inset-0 overflow-visible" width={layout.timelineWidth} height={rowCount * layout.rowHeight}>
47
+ <svg
48
+ class="absolute inset-0 overflow-visible"
49
+ width={layout.timelineWidth}
50
+ height={rowCount * layout.rowHeight}
51
+ >
34
52
  {#each arrows as arrow (arrow.key)}
35
- {@const lit = hoveredId === arrow.from || hoveredId === arrow.to || draggingId === arrow.from || draggingId === arrow.to}
53
+ {@const lit =
54
+ hoveredId === arrow.from ||
55
+ hoveredId === arrow.to ||
56
+ draggingId === arrow.from ||
57
+ draggingId === arrow.to}
36
58
  <g class="g-arrow" class:g-arrow-lit={lit}>
37
59
  <path d={arrow.d} pathLength="1" fill="none" stroke-width="1.5" class="g-arrow-line"></path>
38
- <path d={arrow.head} fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="g-arrow-head"></path>
60
+ <path
61
+ d={arrow.head}
62
+ fill="none"
63
+ stroke-width="1.5"
64
+ stroke-linecap="round"
65
+ stroke-linejoin="round"
66
+ class="g-arrow-head"
67
+ ></path>
39
68
  </g>
40
69
  {/each}
41
70
  </svg>
@@ -44,7 +73,8 @@
44
73
  <style>
45
74
  .g-today {
46
75
  transform-origin: top;
47
- animation: g-today-in var(--duration-very-slow, 500ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
76
+ animation: g-today-in var(--duration-very-slow, 500ms)
77
+ var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
48
78
  }
49
79
 
50
80
  .g-arrow {
@@ -59,7 +89,8 @@
59
89
  .g-arrow-line {
60
90
  stroke-dasharray: 1;
61
91
  stroke-dashoffset: 0;
62
- animation: g-draw var(--duration-very-slow, 500ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) 300ms backwards;
92
+ animation: g-draw var(--duration-very-slow, 500ms)
93
+ var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) 300ms backwards;
63
94
  }
64
95
 
65
96
  .g-arrow-head {
@@ -15,13 +15,19 @@
15
15
  let hue = $derived(row.group.hue ?? "var(--color-primary)");
16
16
  </script>
17
17
 
18
- <div role="listitem" class="relative flex border-b border-border/60 bg-muted/30" style:height="{layout.rowHeight}px">
18
+ <div
19
+ role="listitem"
20
+ class="relative flex border-b border-border/60 bg-muted/30"
21
+ style:height="{layout.rowHeight}px"
22
+ >
19
23
  <!-- Opaque card under the row's muted tint, so bars never show through the pinned column. -->
20
24
  <div class="sticky left-0 z-10 w-(--g-label) shrink-0 border-r border-border bg-card">
21
25
  <div class="flex h-full items-center gap-2 bg-muted/30 px-3">
22
26
  <span class="size-2 shrink-0 rounded-full" style:background={hue}></span>
23
27
  <span class="truncate text-sm font-medium text-foreground">{row.group.label}</span>
24
- <span class="grid h-5 min-w-5 place-items-center rounded-sm bg-foreground/5 px-1 text-xs font-medium text-muted-foreground tabular-nums">
28
+ <span
29
+ class="grid h-5 min-w-5 place-items-center rounded-sm bg-foreground/5 px-1 text-xs font-medium text-muted-foreground tabular-nums"
30
+ >
25
31
  {row.count}
26
32
  </span>
27
33
  </div>
@@ -13,7 +13,9 @@
13
13
 
14
14
  let { layout, days, months, labelHeading, locale, corner = $bindable() }: Props = $props();
15
15
 
16
- let weekdayFormat = $derived(new Intl.DateTimeFormat(locale, { weekday: "narrow", timeZone: "UTC" }));
16
+ let weekdayFormat = $derived(
17
+ new Intl.DateTimeFormat(locale, { weekday: "narrow", timeZone: "UTC" }),
18
+ );
17
19
  </script>
18
20
 
19
21
  <!-- Months over days, pinned to the top. -->
@@ -32,7 +34,9 @@
32
34
  style:left="{month.i * layout.dayWidth}px"
33
35
  style:width="{month.length * layout.dayWidth}px"
34
36
  >
35
- <span class="sticky left-(--g-label) inline-block px-2 pt-1.5 text-xs font-medium whitespace-nowrap text-foreground">
37
+ <span
38
+ class="sticky left-(--g-label) inline-block px-2 pt-1.5 text-xs font-medium whitespace-nowrap text-foreground"
39
+ >
36
40
  {month.label}
37
41
  </span>
38
42
  </div>
@@ -51,7 +55,9 @@
51
55
  style:width="{layout.dayWidth}px"
52
56
  >
53
57
  {#if layout.dayWidth >= 26}
54
- <span class="mb-0.5 opacity-70">{weekdayFormat.format(new Date(d.day * 86_400_000))}</span>
58
+ <span class="mb-0.5 opacity-70"
59
+ >{weekdayFormat.format(new Date(d.day * 86_400_000))}</span
60
+ >
55
61
  {/if}
56
62
  {#if showNumber}
57
63
  <span
@@ -3,7 +3,14 @@
3
3
  import { CaretRight, FlagPennant } from "phosphor-svelte";
4
4
  import type { GanttChange, GanttLabelContext } from "../task-types.js";
5
5
  import GanttBar from "./gantt-bar.svelte";
6
- import { barWidth, dayX, type Drag, type Layout, type Span, type TaskRow } from "./logic.js";
6
+ import {
7
+ barWidth,
8
+ dayX,
9
+ type Drag,
10
+ type Layout,
11
+ type Span,
12
+ type TaskRow,
13
+ } from "./logic.js";
7
14
  import "./gantt.css";
8
15
 
9
16
  interface Props<T> {
@@ -60,7 +67,12 @@
60
67
  let hot = $derived(hovered || drag?.id === row.id);
61
68
  </script>
62
69
 
63
- <div role="listitem" class="g-row relative flex border-b border-border/40 last:border-b-0" class:g-row-hot={hot} style:height="{layout.rowHeight}px">
70
+ <div
71
+ role="listitem"
72
+ class="g-row relative flex border-b border-border/40 last:border-b-0"
73
+ class:g-row-hot={hot}
74
+ style:height="{layout.rowHeight}px"
75
+ >
64
76
  <div
65
77
  class="sticky left-0 z-10 flex w-(--g-label) shrink-0 items-center gap-2 border-r border-border bg-card px-3 text-sm"
66
78
  style:padding-inline-start={nested ? `${row.depth * indent + 12}px` : undefined}
@@ -74,7 +86,11 @@
74
86
  onclick={() => onToggle(row.id)}
75
87
  class="-ml-1 grid size-5 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors duration-[var(--duration-quick,150ms)] hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
76
88
  >
77
- <span class="g-chevron size-3.5 [&>svg]:size-full" class:g-chevron-open={row.open} aria-hidden="true">
89
+ <span
90
+ class="g-chevron size-3.5 [&>svg]:size-full"
91
+ class:g-chevron-open={row.open}
92
+ aria-hidden="true"
93
+ >
78
94
  <CaretRight weight="bold" />
79
95
  </span>
80
96
  </button>
@@ -108,7 +124,9 @@
108
124
  onpointerenter={() => onhover(row.id, true)}
109
125
  onpointerleave={() => onhover(row.id, false)}
110
126
  >
111
- <span class="size-5 [&>svg]:size-full" aria-hidden="true"><FlagPennant weight="fill" /></span>
127
+ <span class="size-5 [&>svg]:size-full" aria-hidden="true"
128
+ ><FlagPennant weight="fill" /></span
129
+ >
112
130
  </button>
113
131
  {:else if !row.own}
114
132
  <!-- A parent with no dates of its own: the span of what is below it. Not draggable. -->
@@ -124,7 +142,8 @@
124
142
  style:width="{barWidth(span, layout)}px"
125
143
  style:height="{Math.max(layout.rowHeight - 24, 10)}px"
126
144
  onclick={() => onSelect?.(row.id)}
127
- onkeydown={(e) => (e.key === "Enter" || e.key === " ") && (e.preventDefault(), onSelect?.(row.id))}
145
+ onkeydown={(e) =>
146
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), onSelect?.(row.id))}
128
147
  onpointerenter={() => onhover(row.id, true)}
129
148
  onpointerleave={() => onhover(row.id, false)}
130
149
  ></div>
@@ -161,7 +180,8 @@
161
180
  }
162
181
 
163
182
  .g-chevron {
164
- transition: rotate var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
183
+ transition: rotate var(--duration-fast, 250ms)
184
+ var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
165
185
  }
166
186
 
167
187
  .g-chevron-open {
@@ -1,6 +1,7 @@
1
1
  /* Shared by the bar, the derived bar and the group summary: they all slide in the same way. */
2
2
  .g-enter {
3
- animation: g-bar-in var(--duration-slow, 400ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
3
+ animation: g-bar-in var(--duration-slow, 400ms)
4
+ var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
4
5
  animation-delay: calc(var(--g-i, 0) * var(--duration-stagger, 40ms));
5
6
  }
6
7
 
@@ -50,7 +50,9 @@ describe("TaskGantt", () => {
50
50
  const root = render({ onChange: (_id: string, change: GanttChange) => changes.push(change) });
51
51
  const bar = root.querySelector("[data-bar]");
52
52
  bar?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
53
- bar?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", shiftKey: true, bubbles: true }));
53
+ bar?.dispatchEvent(
54
+ new KeyboardEvent("keydown", { key: "ArrowRight", shiftKey: true, bubbles: true }),
55
+ );
54
56
  await tick();
55
57
  expect(changes).toEqual([
56
58
  { start: "2026-10-06", end: "2026-10-10" },
@@ -1,8 +1,28 @@
1
1
  import { describe, expect, it } from "vite-plus/test";
2
2
  import { dayNumber } from "../dates.js";
3
- import { barWidth, buildArrows, buildMonths, buildDays, buildRows, liveSpan, rangeLabel, rangeOf, roundedPath, spanOf, type Drag, type Layout } from "./logic.js";
3
+ import {
4
+ barWidth,
5
+ buildArrows,
6
+ buildMonths,
7
+ buildDays,
8
+ buildRows,
9
+ liveSpan,
10
+ rangeLabel,
11
+ rangeOf,
12
+ roundedPath,
13
+ spanOf,
14
+ type Drag,
15
+ type Layout,
16
+ } from "./logic.js";
4
17
 
5
- type Task = { id: string; start?: string; end?: string; parent?: string; group?: string; after?: string[] };
18
+ type Task = {
19
+ id: string;
20
+ start?: string;
21
+ end?: string;
22
+ parent?: string;
23
+ group?: string;
24
+ after?: string[];
25
+ };
6
26
 
7
27
  const dates = { getStart: (t: Task) => t.start, getEnd: (t: Task) => t.end };
8
28
  const source = (overrides: Partial<Parameters<typeof buildRows<Task>>[1]> = {}) => ({
@@ -19,12 +39,20 @@ describe("spanOf", () => {
19
39
  expect(span && span.end - span.start).toBe(2);
20
40
  });
21
41
  it("lets a lone date stand for a one-day task", () => {
22
- expect(spanOf({ id: "a", end: "2026-10-05" }, dates)).toEqual({ start: dayNumber("2026-10-05"), end: dayNumber("2026-10-05") });
23
- expect(spanOf({ id: "a", start: "2026-10-05" }, dates)).toEqual({ start: dayNumber("2026-10-05"), end: dayNumber("2026-10-05") });
42
+ expect(spanOf({ id: "a", end: "2026-10-05" }, dates)).toEqual({
43
+ start: dayNumber("2026-10-05"),
44
+ end: dayNumber("2026-10-05"),
45
+ });
46
+ expect(spanOf({ id: "a", start: "2026-10-05" }, dates)).toEqual({
47
+ start: dayNumber("2026-10-05"),
48
+ end: dayNumber("2026-10-05"),
49
+ });
24
50
  });
25
51
  it("gives an undated task no span, and never an end before the start", () => {
26
52
  expect(spanOf({ id: "a" }, dates)).toBeNull();
27
- expect(spanOf({ id: "a", start: "2026-10-09", end: "2026-10-05" }, dates)?.end).toBe(dayNumber("2026-10-09"));
53
+ expect(spanOf({ id: "a", start: "2026-10-09", end: "2026-10-05" }, dates)?.end).toBe(
54
+ dayNumber("2026-10-09"),
55
+ );
28
56
  });
29
57
  });
30
58
 
@@ -46,13 +74,20 @@ describe("liveSpan", () => {
46
74
  describe("rangeOf", () => {
47
75
  const today = dayNumber("2026-10-21");
48
76
  it("pads the tasks' span and starts on a Monday", () => {
49
- const { first, last } = rangeOf([{ id: "a", start: "2026-10-14", end: "2026-10-30" }], dates, today);
77
+ const { first, last } = rangeOf(
78
+ [{ id: "a", start: "2026-10-14", end: "2026-10-30" }],
79
+ dates,
80
+ today,
81
+ );
50
82
  expect(new Date(first * 86_400_000).getUTCDay()).toBe(1);
51
83
  expect(first).toBeLessThanOrEqual(dayNumber("2026-10-07"));
52
84
  expect(last).toBe(dayNumber("2026-10-30") + 21);
53
85
  });
54
86
  it("honours from and to, and never ends before it starts", () => {
55
- expect(rangeOf([], dates, today, "2026-10-21", "2026-10-01")).toEqual({ first: today, last: today });
87
+ expect(rangeOf([], dates, today, "2026-10-21", "2026-10-01")).toEqual({
88
+ first: today,
89
+ last: today,
90
+ });
56
91
  });
57
92
  });
58
93
 
@@ -91,15 +126,30 @@ describe("buildRows", () => {
91
126
  { id: "b", group: "one", start: "2026-10-01", end: "2026-10-02" },
92
127
  { id: "c", group: "one", start: "2026-10-10", end: "2026-10-12" },
93
128
  ];
94
- const rows = buildRows(grouped, source({
95
- groups: [{ id: "one", label: "One" }, { id: "two", label: "Two" }],
96
- getGroup: (t) => t.group ?? "",
97
- }));
98
- expect(rows.map((r) => (r.kind === "group" ? `#${r.group.id}:${r.count}` : r.id))).toEqual(["#one:2", "b", "c", "#two:1", "a"]);
129
+ const rows = buildRows(
130
+ grouped,
131
+ source({
132
+ groups: [
133
+ { id: "one", label: "One" },
134
+ { id: "two", label: "Two" },
135
+ ],
136
+ getGroup: (t) => t.group ?? "",
137
+ }),
138
+ );
139
+ expect(rows.map((r) => (r.kind === "group" ? `#${r.group.id}:${r.count}` : r.id))).toEqual([
140
+ "#one:2",
141
+ "b",
142
+ "c",
143
+ "#two:1",
144
+ "a",
145
+ ]);
99
146
  expect(rows[0].span).toEqual({ start: dayNumber("2026-10-01"), end: dayNumber("2026-10-12") });
100
147
  });
101
148
  it("applies a live drag to the row it moves", () => {
102
- const rows = buildRows([{ id: "a", start: "2026-10-05", end: "2026-10-06" }], source({ drag: { id: "a", mode: "move", originX: 0, dx: 2 } }));
149
+ const rows = buildRows(
150
+ [{ id: "a", start: "2026-10-05", end: "2026-10-06" }],
151
+ source({ drag: { id: "a", mode: "move", originX: 0, dx: 2 } }),
152
+ );
103
153
  const row = rows[0];
104
154
  if (row.kind !== "task") throw new Error("expected a task row");
105
155
  expect(row.span).toEqual({ start: dayNumber("2026-10-07"), end: dayNumber("2026-10-08") });
@@ -108,7 +158,15 @@ describe("buildRows", () => {
108
158
 
109
159
  describe("buildArrows", () => {
110
160
  const first = dayNumber("2026-10-05");
111
- const layout: Layout = { first, last: first + 30, dayWidth: 32, rowHeight: 40, inset: 3, timelineWidth: 31 * 32, todayDay: first };
161
+ const layout: Layout = {
162
+ first,
163
+ last: first + 30,
164
+ dayWidth: 32,
165
+ rowHeight: 40,
166
+ inset: 3,
167
+ timelineWidth: 31 * 32,
168
+ todayDay: first,
169
+ };
112
170
  const rows = (tasks: Task[]) => buildRows(tasks, source());
113
171
  it("draws one arrow per known dependency and skips unknown or undated ones", () => {
114
172
  const tasks: Task[] = [
@@ -124,7 +182,11 @@ describe("buildArrows", () => {
124
182
  { id: "a", start: "2026-10-05", end: "2026-10-05" },
125
183
  { id: "b", start: "2026-10-12", end: "2026-10-12", after: ["a"] },
126
184
  ];
127
- const [arrow] = buildArrows(rows(tasks), layout, (t) => tasks.find((x) => x.id === t.id)?.after);
185
+ const [arrow] = buildArrows(
186
+ rows(tasks),
187
+ layout,
188
+ (t) => tasks.find((x) => x.id === t.id)?.after,
189
+ );
128
190
  expect(arrow.d.match(/Q/g)).toHaveLength(2);
129
191
  });
130
192
  it("routes round the row gap when the bars touch", () => {
@@ -132,30 +194,58 @@ describe("buildArrows", () => {
132
194
  { id: "a", start: "2026-10-05", end: "2026-10-08" },
133
195
  { id: "b", start: "2026-10-06", end: "2026-10-09", after: ["a"] },
134
196
  ];
135
- const [arrow] = buildArrows(rows(tasks), layout, (t) => tasks.find((x) => x.id === t.id)?.after);
197
+ const [arrow] = buildArrows(
198
+ rows(tasks),
199
+ layout,
200
+ (t) => tasks.find((x) => x.id === t.id)?.after,
201
+ );
136
202
  expect(arrow.d.match(/Q/g)).toHaveLength(4);
137
203
  });
138
204
  });
139
205
 
140
206
  describe("roundedPath", () => {
141
207
  it("rounds each corner by up to the radius and ends on the last point", () => {
142
- expect(roundedPath([[0, 0], [20, 0], [20, 20]], 6)).toBe("M0,0 L14,0 Q20,0 20,6 L20,20");
208
+ expect(
209
+ roundedPath(
210
+ [
211
+ [0, 0],
212
+ [20, 0],
213
+ [20, 20],
214
+ ],
215
+ 6,
216
+ ),
217
+ ).toBe("M0,0 L14,0 Q20,0 20,6 L20,20");
143
218
  });
144
219
  });
145
220
 
146
221
  describe("timeline helpers", () => {
147
222
  it("groups days under their month", () => {
148
223
  const first = dayNumber("2026-10-30");
149
- const months = buildMonths(buildDays(first, 4), new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", timeZone: "UTC" }));
224
+ const months = buildMonths(
225
+ buildDays(first, 4),
226
+ new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", timeZone: "UTC" }),
227
+ );
150
228
  expect(months).toEqual([
151
229
  { key: "2026-10", label: "October 2026", i: 0, length: 2 },
152
230
  { key: "2026-11", label: "November 2026", i: 2, length: 2 },
153
231
  ]);
154
232
  });
155
233
  it("never draws a bar under 8px, and labels the span", () => {
156
- const layout: Layout = { first: 0, last: 9, dayWidth: 4, rowHeight: 40, inset: 0, timelineWidth: 40, todayDay: 0 };
234
+ const layout: Layout = {
235
+ first: 0,
236
+ last: 9,
237
+ dayWidth: 4,
238
+ rowHeight: 40,
239
+ inset: 0,
240
+ timelineWidth: 40,
241
+ todayDay: 0,
242
+ };
157
243
  expect(barWidth({ start: 1, end: 1 }, layout)).toBe(8);
158
- const format = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
244
+ const format = new Intl.DateTimeFormat("en-US", {
245
+ month: "short",
246
+ day: "numeric",
247
+ timeZone: "UTC",
248
+ });
159
249
  const start = dayNumber("2026-10-05");
160
250
  expect(rangeLabel({ start, end: start }, format)).toBe("Oct 5 to Oct 5, 1 day");
161
251
  expect(rangeLabel({ start, end: start + 2 }, format)).toBe("Oct 5 to Oct 7, 3 days");