@heyramzi/ui 1.54.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +205 -13
  4. package/scripts/sync-shadcn.mjs +2 -0
  5. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  6. package/src/ambient.d.ts +8 -0
  7. package/src/brand/brand.test.ts +67 -0
  8. package/src/brand/index.ts +137 -0
  9. package/src/canvas/README.md +236 -0
  10. package/src/canvas/card/card-frame.svelte +61 -0
  11. package/src/canvas/card/card.css +39 -0
  12. package/src/canvas/card/resize-handles.svelte +119 -0
  13. package/src/canvas/card/resize.ts +33 -0
  14. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  15. package/src/canvas/chrome/chrome.css +231 -0
  16. package/src/canvas/chrome/lock-badge.svelte +18 -0
  17. package/src/canvas/chrome/side-handles.svelte +30 -0
  18. package/src/canvas/chrome/size-chip.svelte +45 -0
  19. package/src/canvas/chrome/snap-guides.svelte +69 -0
  20. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  21. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  22. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  23. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  24. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  25. package/src/canvas/edges/bend-handle.svelte +76 -0
  26. package/src/canvas/edges/edge-gradient.svelte +30 -0
  27. package/src/canvas/edges/edge-path.test.ts +58 -0
  28. package/src/canvas/edges/edge-path.ts +293 -0
  29. package/src/canvas/edges/gradient-edge.svelte +43 -0
  30. package/src/canvas/editor/context-menu.test.ts +65 -0
  31. package/src/canvas/editor/context-menu.ts +48 -0
  32. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  33. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  34. package/src/canvas/editor/editor-context.ts +62 -0
  35. package/src/canvas/editor/editor-mode.ts +67 -0
  36. package/src/canvas/editor/frame.ts +98 -0
  37. package/src/canvas/editor/hand-context.ts +15 -0
  38. package/src/canvas/editor/history-core.test.ts +239 -0
  39. package/src/canvas/editor/history-core.ts +247 -0
  40. package/src/canvas/editor/keymap.test.ts +161 -0
  41. package/src/canvas/editor/keymap.ts +151 -0
  42. package/src/canvas/editor/layout-actions.test.ts +156 -0
  43. package/src/canvas/editor/layout-actions.ts +219 -0
  44. package/src/canvas/editor/lock.test.ts +68 -0
  45. package/src/canvas/editor/lock.ts +87 -0
  46. package/src/canvas/editor/rebase.test.ts +208 -0
  47. package/src/canvas/editor/rebase.ts +272 -0
  48. package/src/canvas/editor/style.ts +184 -0
  49. package/src/canvas/editor/text-field-context.ts +41 -0
  50. package/src/canvas/editor/z-order.test.ts +75 -0
  51. package/src/canvas/editor/z-order.ts +87 -0
  52. package/src/canvas/headless/alignment.test.ts +59 -0
  53. package/src/canvas/headless/alignment.ts +23 -0
  54. package/src/canvas/headless/break-apart-find.ts +418 -0
  55. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  56. package/src/canvas/headless/break-apart-plan.ts +439 -0
  57. package/src/canvas/headless/break-apart-style.ts +106 -0
  58. package/src/canvas/headless/break-apart.ts +244 -0
  59. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  60. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  61. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  62. package/src/canvas/headless/edge-anchors.ts +143 -0
  63. package/src/canvas/headless/edge-memo.test.ts +58 -0
  64. package/src/canvas/headless/edge-memo.ts +32 -0
  65. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  66. package/src/canvas/headless/edge-serialization.ts +32 -0
  67. package/src/canvas/headless/figure-geometry.ts +293 -0
  68. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  69. package/src/canvas/headless/fit-viewport.ts +137 -0
  70. package/src/canvas/headless/grid.test.ts +34 -0
  71. package/src/canvas/headless/grid.ts +94 -0
  72. package/src/canvas/headless/icon-node.test.ts +23 -0
  73. package/src/canvas/headless/icon-node.ts +66 -0
  74. package/src/canvas/headless/index.ts +9 -0
  75. package/src/canvas/headless/linkify.test.ts +23 -0
  76. package/src/canvas/headless/linkify.ts +25 -0
  77. package/src/canvas/headless/pen-marks.ts +133 -0
  78. package/src/canvas/headless/sketch.test.ts +99 -0
  79. package/src/canvas/headless/sketch.ts +302 -0
  80. package/src/canvas/headless/snap-guides.test.ts +61 -0
  81. package/src/canvas/headless/snap-guides.ts +146 -0
  82. package/src/canvas/headless/svg-figure.test.ts +49 -0
  83. package/src/canvas/headless/svg-figure.ts +176 -0
  84. package/src/canvas/headless/table-layout.test.ts +158 -0
  85. package/src/canvas/headless/table-layout.ts +309 -0
  86. package/src/canvas/index.ts +35 -0
  87. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  88. package/src/canvas/lod/lod-switch.svelte +140 -0
  89. package/src/canvas/lod/render-all.svelte.ts +52 -0
  90. package/src/canvas/ops/arrange.ts +108 -0
  91. package/src/canvas/ops/ops.test.ts +98 -0
  92. package/src/canvas/ops/ops.ts +347 -0
  93. package/src/canvas/pen/PenField.svelte +128 -0
  94. package/src/canvas/pen/PenField.test.ts +87 -0
  95. package/src/canvas/pen/PenOverlay.svelte +172 -0
  96. package/src/canvas/pen/ShapeNode.svelte +64 -0
  97. package/src/canvas/pen/TextNode.svelte +97 -0
  98. package/src/canvas/pen/colors.ts +28 -0
  99. package/src/canvas/pen/eraser.ts +46 -0
  100. package/src/canvas/pen/geometry.ts +148 -0
  101. package/src/canvas/pen/host-field.test.svelte +13 -0
  102. package/src/canvas/pen/index.ts +40 -0
  103. package/src/canvas/pen/ops.ts +16 -0
  104. package/src/canvas/pen/pen-session.ts +177 -0
  105. package/src/canvas/pen/pen.css +149 -0
  106. package/src/canvas/pen/plugin.ts +139 -0
  107. package/src/canvas/pen/shapes.ts +104 -0
  108. package/src/canvas/pen/stroke-path.ts +54 -0
  109. package/src/canvas/pen/types.ts +96 -0
  110. package/src/canvas/pen/ui.ts +26 -0
  111. package/src/canvas/plugin.test.ts +45 -0
  112. package/src/canvas/plugin.ts +159 -0
  113. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  114. package/src/canvas/screen/PartView.svelte +176 -0
  115. package/src/canvas/screen/ScreenBody.svelte +142 -0
  116. package/src/canvas/screen/ScreenCard.svelte +32 -0
  117. package/src/canvas/screen/glyphs/button.svg +1 -0
  118. package/src/canvas/screen/glyphs/close.svg +1 -0
  119. package/src/canvas/screen/glyphs/down.svg +1 -0
  120. package/src/canvas/screen/glyphs/header.svg +1 -0
  121. package/src/canvas/screen/glyphs/hills.svg +1 -0
  122. package/src/canvas/screen/glyphs/image.svg +1 -0
  123. package/src/canvas/screen/glyphs/input.svg +1 -0
  124. package/src/canvas/screen/glyphs/list.svg +1 -0
  125. package/src/canvas/screen/glyphs/modal.svg +1 -0
  126. package/src/canvas/screen/glyphs/plus.svg +1 -0
  127. package/src/canvas/screen/glyphs/up.svg +1 -0
  128. package/src/canvas/screen/glyphs.ts +40 -0
  129. package/src/canvas/screen/labels.ts +41 -0
  130. package/src/canvas/screen/parts.ts +164 -0
  131. package/src/canvas/screen/plugin.ts +67 -0
  132. package/src/canvas/screen/screen.css +593 -0
  133. package/src/canvas/screen/types.ts +47 -0
  134. package/src/canvas/screen/ui.ts +13 -0
  135. package/src/canvas/test-utils.ts +18 -0
  136. package/src/canvas/types.ts +53 -0
  137. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  138. package/src/canvas/ui/FrameLayer.svelte +116 -0
  139. package/src/canvas/ui/PenLayer.svelte +39 -0
  140. package/src/canvas/ui/StylePanel.svelte +268 -0
  141. package/src/canvas/ui/canvas-grid.svelte +156 -0
  142. package/src/canvas/ui/canvas-ui.css +350 -0
  143. package/src/canvas/ui/forward-wheel.ts +29 -0
  144. package/src/canvas/ui/icons.ts +124 -0
  145. package/src/canvas/ui/place-panel.ts +80 -0
  146. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  147. package/src/canvas/ui/shortcut-tip.ts +17 -0
  148. package/src/canvas/ui/tools.ts +146 -0
  149. package/src/charts/README.md +104 -0
  150. package/src/charts/ambient.d.ts +4 -0
  151. package/src/charts/animated-value.css +7 -0
  152. package/src/charts/animated-value.svelte +110 -0
  153. package/src/charts/area-chart.css +29 -0
  154. package/src/charts/area-chart.svelte +204 -0
  155. package/src/charts/bar-chart.css +119 -0
  156. package/src/charts/bar-chart.svelte +275 -0
  157. package/src/charts/charts.css +201 -0
  158. package/src/charts/crosshair.svelte +35 -0
  159. package/src/charts/dashboard/chart.ts +5 -0
  160. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  161. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  162. package/src/charts/dashboard/data-panel.svelte +81 -0
  163. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  164. package/src/charts/dashboard/data-table.svelte +278 -0
  165. package/src/charts/dashboard/data-table.ts +26 -0
  166. package/src/charts/dashboard/index.ts +25 -0
  167. package/src/charts/dashboard/panel-grid.svelte +43 -0
  168. package/src/charts/dashboard/period-filter.svelte +53 -0
  169. package/src/charts/dashboard/rating-chip.svelte +35 -0
  170. package/src/charts/dashboard/stat-band.svelte +47 -0
  171. package/src/charts/dashboard/stat-tile.svelte +176 -0
  172. package/src/charts/dashboard/windows.ts +8 -0
  173. package/src/charts/donut-chart.css +56 -0
  174. package/src/charts/donut-chart.svelte +158 -0
  175. package/src/charts/edge-fade.svelte +43 -0
  176. package/src/charts/edge-fade.ts +16 -0
  177. package/src/charts/figure.css +100 -0
  178. package/src/charts/figure.svelte +122 -0
  179. package/src/charts/gauge-chart.css +94 -0
  180. package/src/charts/gauge-chart.svelte +302 -0
  181. package/src/charts/hover-band.test.ts +30 -0
  182. package/src/charts/hover-band.ts +21 -0
  183. package/src/charts/hover-focus.svelte +85 -0
  184. package/src/charts/index.ts +31 -0
  185. package/src/charts/legend.css +136 -0
  186. package/src/charts/legend.svelte +112 -0
  187. package/src/charts/line-chart.css +26 -0
  188. package/src/charts/line-chart.svelte +256 -0
  189. package/src/charts/motion.ts +19 -0
  190. package/src/charts/number-chart.css +179 -0
  191. package/src/charts/number-chart.svelte +187 -0
  192. package/src/charts/primitives/chart-container.svelte +57 -0
  193. package/src/charts/primitives/chart-style.svelte +36 -0
  194. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  195. package/src/charts/primitives/chart-utils.ts +54 -0
  196. package/src/charts/primitives/index.ts +11 -0
  197. package/src/charts/reference-line.css +18 -0
  198. package/src/charts/reference-line.svelte +31 -0
  199. package/src/charts/reveal.svelte +61 -0
  200. package/src/charts/ring-chart.css +104 -0
  201. package/src/charts/ring-chart.svelte +166 -0
  202. package/src/charts/sparkline.css +45 -0
  203. package/src/charts/sparkline.svelte +141 -0
  204. package/src/charts/stat-card.css +105 -0
  205. package/src/charts/stat-card.svelte +267 -0
  206. package/src/charts/time-series-chart.css +28 -0
  207. package/src/charts/time-series-chart.svelte +179 -0
  208. package/src/charts/trend-pill.css +38 -0
  209. package/src/charts/trend-pill.svelte +48 -0
  210. package/src/components/copy/copy-button.svelte +1 -1
  211. package/src/components/docs/doc-page.svelte +1 -1
  212. package/src/components/docs/on-this-page.svelte +2 -2
  213. package/src/components/list-page/empty-state.svelte +1 -1
  214. package/src/components/list-page/filter-popover.svelte +1 -1
  215. package/src/components/list-page/section-label.svelte +1 -1
  216. package/src/components/segmented/segment.svelte +4 -4
  217. package/src/components/theme/theme-provider.svelte +6 -4
  218. package/src/components/toolbar/index.ts +5 -0
  219. package/src/components/toolbar/toolbar.svelte +168 -0
  220. package/src/components/toolbar/toolbar.test.ts +66 -0
  221. package/src/editor/README.md +58 -0
  222. package/src/editor/ambient.d.ts +1 -0
  223. package/src/editor/block-id.ts +77 -0
  224. package/src/editor/block-ops.test.ts +237 -0
  225. package/src/editor/block-ops.ts +157 -0
  226. package/src/editor/content.css +418 -0
  227. package/src/editor/editor-toolbar.svelte +303 -0
  228. package/src/editor/editor.svelte +469 -0
  229. package/src/editor/extensions.ts +145 -0
  230. package/src/editor/image-toolbar.svelte +123 -0
  231. package/src/editor/index.ts +17 -0
  232. package/src/editor/inline-editor.svelte +261 -0
  233. package/src/editor/markdown-escaping.test.ts +158 -0
  234. package/src/editor/markdown-marks.test.ts +89 -0
  235. package/src/editor/markdown-test-helpers.ts +13 -0
  236. package/src/editor/markdown-youtube.test.ts +93 -0
  237. package/src/editor/markdown.test.ts +250 -0
  238. package/src/editor/markdown.ts +511 -0
  239. package/src/editor/renderer.svelte +92 -0
  240. package/src/editor/serializer.test.ts +133 -0
  241. package/src/editor/serializer.ts +370 -0
  242. package/src/editor/slash-items.ts +115 -0
  243. package/src/editor/slash-menu.svelte +159 -0
  244. package/src/editor/youtube-url.ts +35 -0
  245. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  246. package/src/gallery.test.ts +20 -1
  247. package/src/lib/result.ts +19 -0
  248. package/src/onboarding/README.md +120 -0
  249. package/src/onboarding/define-tours.test.ts +50 -0
  250. package/src/onboarding/define-tours.ts +31 -0
  251. package/src/onboarding/demo.svelte +86 -0
  252. package/src/onboarding/index.ts +12 -0
  253. package/src/onboarding/onboarding-provider.svelte +120 -0
  254. package/src/onboarding/onboarding.svelte.ts +24 -0
  255. package/src/onboarding/pick.test.ts +79 -0
  256. package/src/onboarding/pick.ts +36 -0
  257. package/src/onboarding/stores.test.ts +103 -0
  258. package/src/onboarding/stores.ts +58 -0
  259. package/src/onboarding/tour-card.svelte +70 -0
  260. package/src/onboarding/tour-runner.svelte +197 -0
  261. package/src/onboarding/types.ts +34 -0
  262. package/src/onboarding/wait.ts +29 -0
  263. package/src/shadcn/command/command-input.svelte +1 -1
  264. package/src/slides/Controls.svelte +138 -0
  265. package/src/slides/Deck.svelte +271 -0
  266. package/src/slides/Fragment.svelte +71 -0
  267. package/src/slides/Overview.svelte +113 -0
  268. package/src/slides/Presenter.svelte +292 -0
  269. package/src/slides/README.md +89 -0
  270. package/src/slides/Slide.svelte +179 -0
  271. package/src/slides/SlideList.svelte +19 -0
  272. package/src/slides/ambient.d.ts +1 -0
  273. package/src/slides/bridges.ts +69 -0
  274. package/src/slides/deck-controller.svelte.ts +292 -0
  275. package/src/slides/demo-slides.svelte +358 -0
  276. package/src/slides/demo.svelte +176 -0
  277. package/src/slides/export/capture.ts +113 -0
  278. package/src/slides/export/ffmpeg.ts +120 -0
  279. package/src/slides/export/index.ts +9 -0
  280. package/src/slides/export/launch.ts +10 -0
  281. package/src/slides/export/pdf.ts +152 -0
  282. package/src/slides/export/types.ts +48 -0
  283. package/src/slides/index.ts +19 -0
  284. package/src/slides/labels.ts +48 -0
  285. package/src/slides/layout.ts +51 -0
  286. package/src/slides/model.ts +47 -0
  287. package/src/slides/navigation.ts +100 -0
  288. package/src/slides/theme.css +205 -0
  289. package/src/slides/types.ts +86 -0
  290. package/src/tasks/README.md +312 -0
  291. package/src/tasks/ambient.d.ts +3 -0
  292. package/src/tasks/board-order.ts +137 -0
  293. package/src/tasks/dates.ts +31 -0
  294. package/src/tasks/define-statuses.ts +32 -0
  295. package/src/tasks/demo.svelte +389 -0
  296. package/src/tasks/detail/detail-demo.svelte +186 -0
  297. package/src/tasks/detail/detail-types.ts +54 -0
  298. package/src/tasks/detail/task-chip.svelte +28 -0
  299. package/src/tasks/detail/task-detail.svelte +315 -0
  300. package/src/tasks/detail/task-property.svelte +17 -0
  301. package/src/tasks/detail/task-thread.svelte +258 -0
  302. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  303. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  304. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  305. package/src/tasks/gantt/gantt-header.svelte +71 -0
  306. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  307. package/src/tasks/gantt/gantt.css +22 -0
  308. package/src/tasks/gantt/gantt.test.ts +77 -0
  309. package/src/tasks/gantt/logic.test.ts +163 -0
  310. package/src/tasks/gantt/logic.ts +248 -0
  311. package/src/tasks/inbox/inbox-card.svelte +283 -0
  312. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  313. package/src/tasks/inbox/inbox-logic.ts +55 -0
  314. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  315. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  316. package/src/tasks/inbox/inbox-types.ts +108 -0
  317. package/src/tasks/inbox/inbox.svelte +203 -0
  318. package/src/tasks/inbox/index.ts +23 -0
  319. package/src/tasks/index.ts +85 -0
  320. package/src/tasks/kanban-board.svelte +561 -0
  321. package/src/tasks/kanban-column.svelte +117 -0
  322. package/src/tasks/kanban-drag.ts +38 -0
  323. package/src/tasks/kanban-types.ts +65 -0
  324. package/src/tasks/presets/sales.ts +16 -0
  325. package/src/tasks/record/demo-data.ts +286 -0
  326. package/src/tasks/record/field-section.svelte +53 -0
  327. package/src/tasks/record/fields.ts +142 -0
  328. package/src/tasks/record/format.ts +87 -0
  329. package/src/tasks/record/gutter.ts +2 -0
  330. package/src/tasks/record/index.ts +54 -0
  331. package/src/tasks/record/info-list.svelte +316 -0
  332. package/src/tasks/record/kinds.ts +42 -0
  333. package/src/tasks/record/logic.ts +324 -0
  334. package/src/tasks/record/next-up.svelte +70 -0
  335. package/src/tasks/record/project-list.svelte +133 -0
  336. package/src/tasks/record/record-demo.svelte +324 -0
  337. package/src/tasks/record/record-header.svelte +70 -0
  338. package/src/tasks/record/record-shell.svelte +138 -0
  339. package/src/tasks/record/record-stats.svelte +48 -0
  340. package/src/tasks/record/record-timeline.svelte +231 -0
  341. package/src/tasks/record/record.test.ts +350 -0
  342. package/src/tasks/record/steps-rail.svelte +474 -0
  343. package/src/tasks/record/types.ts +280 -0
  344. package/src/tasks/record/where-we-are.svelte +121 -0
  345. package/src/tasks/reorder.ts +19 -0
  346. package/src/tasks/side-panel-types.ts +16 -0
  347. package/src/tasks/side-panel.svelte +193 -0
  348. package/src/tasks/status-badge.svelte +63 -0
  349. package/src/tasks/status-dot.svelte +228 -0
  350. package/src/tasks/status-icon.svelte +30 -0
  351. package/src/tasks/status-types.ts +40 -0
  352. package/src/tasks/task-gantt.svelte +230 -0
  353. package/src/tasks/task-list.svelte +470 -0
  354. package/src/tasks/task-types.ts +33 -0
  355. package/src/tasks/time/duration.ts +69 -0
  356. package/src/tasks/time/time-chip.svelte +62 -0
  357. package/src/tasks/time/time-dot.svelte +58 -0
  358. package/src/tasks/time/time-ring.svelte +74 -0
  359. package/src/tasks/time/time-tracker.svelte +565 -0
  360. package/src/tasks/time/time-types.ts +74 -0
  361. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  362. package/src/tokens/base.css +5 -5
  363. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,389 @@
1
+ <script lang="ts">
2
+ import KanbanBoard from "./kanban-board.svelte";
3
+ import TaskList from "./task-list.svelte";
4
+ import TaskGantt from "./task-gantt.svelte";
5
+ import { SegmentedFilter } from "../components/segmented/index.js";
6
+ import { ChartBarHorizontal, Kanban, ListBullets } from "phosphor-svelte";
7
+ import StatusBadge from "./status-badge.svelte";
8
+ import StatusDot from "./status-dot.svelte";
9
+ import { defineStatuses } from "./define-statuses.js";
10
+ import { addDays, dayNumber, todayIso } from "./dates.js";
11
+ import { salesStatuses } from "./presets/sales.js";
12
+ import type { KanbanMove } from "./kanban-types.js";
13
+ import type { GanttChange } from "./task-types.js";
14
+ import TimeTracker from "./time/time-tracker.svelte";
15
+ import TimeChip from "./time/time-chip.svelte";
16
+ import type { TimeLogInput, TimeStopInfo } from "./time/time-types.js";
17
+
18
+ type Priority = "low" | "medium" | "high";
19
+
20
+ type Task = {
21
+ id: string;
22
+ status: string;
23
+ title: string;
24
+ assignee: string;
25
+ priority: Priority;
26
+ boardOrder: string;
27
+ start: string;
28
+ end: string;
29
+ dependsOn?: string[];
30
+ };
31
+
32
+ const statuses = defineStatuses({
33
+ todo: { label: "To do", tone: "neutral", variant: "open" },
34
+ doing: { label: "In progress", tone: "info", variant: "progress", progress: 0.5 },
35
+ review: { label: "Review", tone: "warning", variant: "progress", progress: 0.75 },
36
+ done: { label: "Done", tone: "success", variant: "completed" },
37
+ });
38
+
39
+ const columns = [
40
+ { id: "todo", label: "To do", hue: "var(--color-muted-foreground)" },
41
+ { id: "doing", label: "In progress", hue: "var(--color-info)" },
42
+ { id: "review", label: "Review", hue: "var(--color-warning)" },
43
+ { id: "done", label: "Done", hue: "var(--color-success)" },
44
+ ];
45
+
46
+ const progressByStatus: Record<string, number> = { todo: 0, doing: 0.45, review: 0.8, done: 1 };
47
+
48
+ // Dates hang off today, so the timeline always opens on live work.
49
+ const today = todayIso();
50
+ const on = (days: number) => addDays(today, days);
51
+
52
+ let tasks = $state<Task[]>([
53
+ { id: "t1", status: "done", title: "Set up analytics", assignee: "Bo", priority: "low", boardOrder: "a0", start: on(-12), end: on(-8) },
54
+ { id: "t2", status: "done", title: "Interview five customers", assignee: "Cleo", priority: "medium", boardOrder: "a1", start: on(-10), end: on(-4) },
55
+ { id: "t3", status: "review", title: "Write the pricing page", assignee: "Ada", priority: "high", boardOrder: "a0", start: on(-5), end: on(1), dependsOn: ["t2"] },
56
+ { id: "t4", status: "doing", title: "Build the onboarding flow", assignee: "Dana", priority: "high", boardOrder: "a0", start: on(-3), end: on(5), dependsOn: ["t1"] },
57
+ { id: "t5", status: "doing", title: "Draft the launch email", assignee: "Bo", priority: "medium", boardOrder: "a1", start: on(0), end: on(3), dependsOn: ["t3"] },
58
+ { id: "t6", status: "todo", title: "Record the demo video", assignee: "Cleo", priority: "medium", boardOrder: "a0", start: on(4), end: on(8), dependsOn: ["t4"] },
59
+ { id: "t7", status: "todo", title: "Pick a shipping date", assignee: "Ada", priority: "low", boardOrder: "a1", start: on(6), end: on(6) },
60
+ { id: "t8", status: "todo", title: "Launch on Product Hunt", assignee: "Dana", priority: "high", boardOrder: "a2", start: on(10), end: on(11), dependsOn: ["t5", "t6"] },
61
+ ]);
62
+
63
+ const views = [
64
+ { value: "board", label: "Board", icon: Kanban },
65
+ { value: "list", label: "List", icon: ListBullets },
66
+ { value: "gantt", label: "Gantt", icon: ChartBarHorizontal },
67
+ ] as const;
68
+ const scales = [
69
+ { value: "days", label: "Days" },
70
+ { value: "weeks", label: "Weeks" },
71
+ ] as const;
72
+
73
+ let view = $state<(typeof views)[number]["value"]>("board");
74
+ let sortKey = $state("manual");
75
+ let scale = $state<"days" | "weeks">("days");
76
+
77
+ const sortKeys = [
78
+ { id: "title", label: "Title", compare: (a: Task, b: Task) => a.title.localeCompare(b.title) },
79
+ { id: "assignee", label: "Assignee", compare: (a: Task, b: Task) => a.assignee.localeCompare(b.assignee) },
80
+ { id: "due", label: "Due date", compare: (a: Task, b: Task) => a.end.localeCompare(b.end) },
81
+ ];
82
+
83
+ const priorityClass: Record<Priority, string> = {
84
+ low: "bg-muted text-muted-foreground",
85
+ medium: "bg-warning/15 text-warning-700 dark:text-warning-400",
86
+ high: "bg-destructive/15 text-destructive",
87
+ };
88
+
89
+ const people: Record<string, string> = {
90
+ Ada: "oklch(0.72 0.12 280)",
91
+ Bo: "oklch(0.74 0.12 160)",
92
+ Cleo: "oklch(0.76 0.12 60)",
93
+ Dana: "oklch(0.72 0.13 20)",
94
+ };
95
+
96
+ const dueFormat = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
97
+
98
+ function due(task: Task): { label: string; late: boolean } {
99
+ const late = task.status !== "done" && dayNumber(task.end) < dayNumber(today);
100
+ return { label: dueFormat.format(new Date(`${task.end}T00:00:00Z`)), late };
101
+ }
102
+
103
+ function onMove(id: string, move: KanbanMove) {
104
+ tasks = tasks.map((task) =>
105
+ task.id === id
106
+ ? { ...task, status: move.columnId, boardOrder: move.boardOrder ?? task.boardOrder }
107
+ : task,
108
+ );
109
+ }
110
+
111
+ function onChange(id: string, change: GanttChange) {
112
+ tasks = tasks.map((task) => (task.id === id ? { ...task, ...change } : task));
113
+ }
114
+
115
+ // ---- time tracking, an option the host turns on
116
+ type Entry = { id: string; taskId: string; person: string; day: string; minutes: number; note?: string };
117
+
118
+ const estimates: Record<string, number> = { t3: 360, t4: 480, t5: 180, t6: 240 };
119
+
120
+ let tracking = $state(true);
121
+ let timeTaskId = $state("t4");
122
+ let nextEntry = 100;
123
+ let entries = $state<Entry[]>([
124
+ { id: "e1", taskId: "t4", person: "Dana", day: on(-3), minutes: 150, note: "Signup and email step" },
125
+ { id: "e2", taskId: "t4", person: "Ada", day: on(-2), minutes: 95, note: "Copy for the welcome screen" },
126
+ { id: "e3", taskId: "t4", person: "Dana", day: on(-2), minutes: 125, note: "Progress bar and empty states" },
127
+ { id: "e4", taskId: "t4", person: "Dana", day: on(-1), minutes: 80 },
128
+ { id: "e5", taskId: "t3", person: "Ada", day: on(-5), minutes: 210, note: "First draft" },
129
+ { id: "e6", taskId: "t3", person: "Bo", day: on(-3), minutes: 95, note: "Pricing table" },
130
+ { id: "e7", taskId: "t3", person: "Ada", day: on(-1), minutes: 85, note: "Review changes" },
131
+ { id: "e8", taskId: "t5", person: "Bo", day: on(0), minutes: 40 },
132
+ { id: "e9", taskId: "t1", person: "Bo", day: on(-12), minutes: 60 },
133
+ ]);
134
+ // The host keeps the running timer. A reload would read it back from storage.
135
+ let timer = $state<{ taskId: string; startedAt: number } | null>({
136
+ taskId: "t4",
137
+ startedAt: Date.now() - (12 * 60 + 41) * 1000,
138
+ });
139
+
140
+ let minutesByTask = $derived.by(() => {
141
+ const totals: Record<string, number> = {};
142
+ for (const entry of entries) totals[entry.taskId] = (totals[entry.taskId] ?? 0) + entry.minutes;
143
+ return totals;
144
+ });
145
+ let timeEntries = $derived(entries.filter((entry) => entry.taskId === timeTaskId));
146
+ let timeRunning = $derived(timer?.taskId === timeTaskId ? { startedAt: timer.startedAt } : null);
147
+
148
+ function addEntry(taskId: string, day: string, minutes: number, note?: string) {
149
+ entries = [{ id: `e${nextEntry++}`, taskId, person: "Dana", day, minutes, note }, ...entries];
150
+ }
151
+
152
+ function onLog(input: TimeLogInput) {
153
+ addEntry(timeTaskId, input.day, input.minutes, input.note);
154
+ }
155
+
156
+ function onStop(info: TimeStopInfo) {
157
+ if (timer) addEntry(timer.taskId, info.day, info.minutes);
158
+ timer = null;
159
+ }
160
+
161
+ const listGrid = "grid grid-cols-[minmax(0,1fr)_7rem_5.5rem_4.5rem] items-center gap-3 max-sm:grid-cols-[minmax(0,1fr)_4.5rem]";
162
+ </script>
163
+
164
+ {#snippet avatar(name: string)}
165
+ <span
166
+ class="grid size-5 shrink-0 place-items-center rounded-full text-[10px] font-semibold text-white"
167
+ style:background={people[name] ?? "var(--color-muted-foreground)"}
168
+ aria-hidden="true"
169
+ >
170
+ {name[0]}
171
+ </span>
172
+ {/snippet}
173
+
174
+ <div class="flex flex-col gap-6 rounded-xl border border-border bg-background p-4">
175
+ <div class="flex h-[600px] flex-col gap-3">
176
+ <div class="flex flex-wrap items-center gap-2">
177
+ <SegmentedFilter options={views} bind:value={view} ariaLabel="Task view" block={false} />
178
+ <div class="ml-auto flex items-center gap-3">
179
+ <label class="flex cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground">
180
+ <input type="checkbox" bind:checked={tracking} class="size-3.5 accent-[var(--tone-accent,var(--color-primary))]" />
181
+ Time tracking
182
+ </label>
183
+ {#if view === "gantt"}
184
+ <SegmentedFilter options={scales} bind:value={scale} ariaLabel="Timeline scale" block={false} />
185
+ {:else}
186
+ <label for="tasks-sort" class="text-xs font-medium text-muted-foreground">Sort</label>
187
+ <select
188
+ id="tasks-sort"
189
+ bind:value={sortKey}
190
+ class="h-8 rounded-md border border-border bg-background px-2 text-xs text-foreground"
191
+ >
192
+ <option value="manual">Manual</option>
193
+ {#each sortKeys as key (key.id)}
194
+ <option value={key.id}>{key.label}</option>
195
+ {/each}
196
+ </select>
197
+ {/if}
198
+ </div>
199
+ </div>
200
+
201
+ {#key view}
202
+ <div class="tasks-view flex min-h-0 flex-1 flex-col">
203
+ {#if view === "board"}
204
+ <KanbanBoard
205
+ {columns}
206
+ items={tasks}
207
+ getId={(task) => task.id}
208
+ getColumn={(task) => task.status}
209
+ {sortKey}
210
+ {sortKeys}
211
+ boardOrder={(task) => task.boardOrder}
212
+ {onMove}
213
+ >
214
+ {#snippet renderCard(task)}
215
+ {@const d = due(task)}
216
+ <article class="rounded-xl border border-border bg-card p-2.5 text-card-foreground shadow-sm">
217
+ <p class="text-sm leading-snug">{task.title}</p>
218
+ <div class="mt-2.5 flex items-center gap-2 text-xs">
219
+ {@render avatar(task.assignee)}
220
+ <span class={d.late ? "font-medium text-destructive" : "text-muted-foreground"}>{d.label}</span>
221
+ {#if tracking}
222
+ <TimeChip minutes={minutesByTask[task.id] ?? 0} estimate={estimates[task.id]} running={timer?.taskId === task.id} />
223
+ {/if}
224
+ <span class="ml-auto rounded-full px-1.5 py-0.5 font-medium capitalize {priorityClass[task.priority]}">
225
+ {task.priority}
226
+ </span>
227
+ </div>
228
+ </article>
229
+ {/snippet}
230
+ </KanbanBoard>
231
+ {:else if view === "list"}
232
+ <TaskList
233
+ groups={columns}
234
+ items={tasks}
235
+ getId={(task) => task.id}
236
+ getGroup={(task) => task.status}
237
+ {sortKey}
238
+ {sortKeys}
239
+ boardOrder={(task) => task.boardOrder}
240
+ {onMove}
241
+ >
242
+ {#snippet head()}
243
+ <div class="{listGrid} w-full px-3.5">
244
+ <span class="pl-6">Task</span>
245
+ <span class="max-sm:hidden">Assignee</span>
246
+ <span class="max-sm:hidden">Priority</span>
247
+ <span class="text-right">Due</span>
248
+ </div>
249
+ {/snippet}
250
+ {#snippet renderRow(task)}
251
+ {@const d = due(task)}
252
+ <div class="{listGrid} h-10 px-2 text-sm">
253
+ <span class="flex min-w-0 items-center gap-2.5">
254
+ {#key task.status}
255
+ <StatusDot statuses={statuses} status={task.status} />
256
+ {/key}
257
+ <span class="truncate" class:text-muted-foreground={task.status === "done"} class:line-through={task.status === "done"}>
258
+ {task.title}
259
+ </span>
260
+ {#if tracking}
261
+ <TimeChip minutes={minutesByTask[task.id] ?? 0} estimate={estimates[task.id]} running={timer?.taskId === task.id} />
262
+ {/if}
263
+ </span>
264
+ <span class="flex min-w-0 items-center gap-2 text-muted-foreground max-sm:hidden">
265
+ {@render avatar(task.assignee)}
266
+ <span class="truncate">{task.assignee}</span>
267
+ </span>
268
+ <span class="max-sm:hidden">
269
+ <span class="rounded-full px-1.5 py-0.5 text-xs font-medium capitalize {priorityClass[task.priority]}">
270
+ {task.priority}
271
+ </span>
272
+ </span>
273
+ <span class="text-right text-xs tabular-nums {d.late ? 'font-medium text-destructive' : 'text-muted-foreground'}">
274
+ {d.label}
275
+ </span>
276
+ </div>
277
+ {/snippet}
278
+ </TaskList>
279
+ {:else}
280
+ <TaskGantt
281
+ items={tasks}
282
+ getId={(task) => task.id}
283
+ getTitle={(task) => task.title}
284
+ getStart={(task) => task.start}
285
+ getEnd={(task) => task.end}
286
+ getProgress={(task) => progressByStatus[task.status]}
287
+ getDependsOn={(task) => task.dependsOn}
288
+ groups={columns}
289
+ getGroup={(task) => task.status}
290
+ {today}
291
+ dayWidth={scale === "days" ? 32 : 14}
292
+ labelWidth={220}
293
+ {onChange}
294
+ >
295
+ {#snippet renderLabel(task)}
296
+ <StatusDot statuses={statuses} status={task.status} />
297
+ <span class="min-w-0 flex-1 truncate">{task.title}</span>
298
+ {@render avatar(task.assignee)}
299
+ {/snippet}
300
+ </TaskGantt>
301
+ {/if}
302
+ </div>
303
+ {/key}
304
+
305
+ <p class="text-xs text-muted-foreground/70">
306
+ {#if view === "board"}
307
+ Drag a card between columns, or focus one and press Alt plus Left or Right.
308
+ {:else if view === "list"}
309
+ Drag a row between groups, or focus one and press Alt plus Up or Down.
310
+ {:else}
311
+ Drag a bar to move it, or its edges to resize. Arrow keys nudge a focused bar a day.
312
+ {/if}
313
+ </p>
314
+ </div>
315
+
316
+ {#if tracking}
317
+ <section class="flex flex-col gap-3 border-t border-border pt-4">
318
+ <div class="flex items-center justify-between gap-3">
319
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Task time</h3>
320
+ <select
321
+ bind:value={timeTaskId}
322
+ aria-label="Task"
323
+ class="h-8 max-w-[60%] rounded-md border border-border bg-background px-2 text-xs text-foreground"
324
+ >
325
+ {#each tasks as task (task.id)}
326
+ <option value={task.id}>{task.title}</option>
327
+ {/each}
328
+ </select>
329
+ </div>
330
+ <TimeTracker
331
+ class="mx-auto w-full max-w-xl"
332
+ entries={timeEntries}
333
+ getId={(entry) => entry.id}
334
+ getMinutes={(entry) => entry.minutes}
335
+ getDay={(entry) => entry.day}
336
+ getPerson={(entry) => entry.person}
337
+ getNote={(entry) => entry.note}
338
+ estimate={estimates[timeTaskId]}
339
+ running={timeRunning}
340
+ onStart={() => {
341
+ timer = { taskId: timeTaskId, startedAt: Date.now() };
342
+ }}
343
+ {onStop}
344
+ {onLog}
345
+ onDelete={(entry) => {
346
+ entries = entries.filter((row) => row.id !== entry.id);
347
+ }}
348
+ />
349
+ </section>
350
+ {/if}
351
+
352
+ <section class="flex flex-col gap-2 border-t border-border pt-4">
353
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Status badges</h3>
354
+ <div class="flex flex-wrap items-center gap-2">
355
+ {#each salesStatuses.ids as id (id)}
356
+ <StatusBadge statuses={salesStatuses} status={id} />
357
+ {/each}
358
+ </div>
359
+ <ul class="mt-1 flex flex-col gap-1.5">
360
+ {#each salesStatuses.ids.slice(0, 3) as id (id)}
361
+ <li class="flex items-center gap-3 text-sm">
362
+ <StatusDot statuses={salesStatuses} status={id} />
363
+ <span class="text-foreground">{salesStatuses.label(id)}</span>
364
+ </li>
365
+ {/each}
366
+ </ul>
367
+ </section>
368
+ </div>
369
+
370
+ <style>
371
+ /* Switching views: the new one rises in with a touch of blur, the old one is gone at once. */
372
+ .tasks-view {
373
+ animation: tasks-view-in var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
374
+ }
375
+
376
+ @keyframes tasks-view-in {
377
+ from {
378
+ opacity: 0;
379
+ transform: translateY(var(--distance-base, 8px));
380
+ filter: blur(var(--blur-small, 2px));
381
+ }
382
+ }
383
+
384
+ @media (prefers-reduced-motion: reduce) {
385
+ .tasks-view {
386
+ animation: none;
387
+ }
388
+ }
389
+ </style>
@@ -0,0 +1,186 @@
1
+ <script lang="ts">
2
+ import { CalendarBlank, CircleDashed, Flag, Folder, User } from "phosphor-svelte";
3
+ import TaskDetail from "./task-detail.svelte";
4
+ import TaskProperty from "./task-property.svelte";
5
+ import TaskChip from "./task-chip.svelte";
6
+ import TimeTracker from "../time/time-tracker.svelte";
7
+ import StatusDot from "../status-dot.svelte";
8
+ import { defineStatuses } from "../define-statuses.js";
9
+ import type { TaskComment } from "./detail-types.js";
10
+ import type { TimeLogInput, TimeStopInfo } from "../time/time-types.js";
11
+
12
+ type Scenario = "default" | "long" | "emptyDescription" | "unassigned" | "running" | "busy" | "closed" | "loading";
13
+
14
+ const statuses = defineStatuses({
15
+ todo: { label: "To do", tone: "neutral", variant: "open" },
16
+ doing: { label: "In progress", tone: "info", variant: "progress", progress: 0.5 },
17
+ done: { label: "Done", tone: "success", variant: "completed" },
18
+ });
19
+
20
+ const scenarios: { id: Scenario; label: string }[] = [
21
+ { id: "default", label: "Default" },
22
+ { id: "long", label: "Long title" },
23
+ { id: "emptyDescription", label: "Empty description" },
24
+ { id: "unassigned", label: "No assignee" },
25
+ { id: "running", label: "Running timer" },
26
+ { id: "busy", label: "24 comments" },
27
+ { id: "closed", label: "Closed" },
28
+ { id: "loading", label: "Loading" },
29
+ ];
30
+
31
+ const people = ["Ada Lovelace", "Bo Chen", "Cy Park"];
32
+ const now = Date.now();
33
+ const hour = 3_600_000;
34
+ const stamp = (agoHours: number) => new Date(now - agoHours * hour).toISOString();
35
+
36
+ const seed: TaskComment[] = [
37
+ { id: "c1", author: "Ada Lovelace", body: "Calls with Yoan: the format that sold the template was simple. A board on why, then the system, then the demo.", createdAt: stamp(30) },
38
+ { id: "c2", author: "Planner", agent: true, body: "Reused that shape for the outline. Three sections, the demo last, one call to action at the end.", createdAt: stamp(26) },
39
+ { id: "c3", author: "Bo Chen", body: "Looks right. I'll record Thursday.", createdAt: stamp(5), edited: true },
40
+ ];
41
+ const crowd: TaskComment[] = Array.from({ length: 24 }, (_, i) => ({
42
+ id: `b${i}`,
43
+ author: i % 4 === 1 ? "Planner" : people[i % people.length] ?? "Ada Lovelace",
44
+ agent: i % 4 === 1,
45
+ body: i % 5 === 0 ? "Pushed the second cut. The intro runs 40 seconds, so I trimmed the first example and kept the one with numbers." : "Noted, moving on to the next step.",
46
+ createdAt: stamp(60 - i * 2),
47
+ }));
48
+
49
+ const longTitle =
50
+ "Run one awareness-only live session for an entrepreneur group, record it, and hand the raw take to the editor with a thumbnail brief";
51
+ const brief =
52
+ "Record the video about how the notebook is orchestrated.\n\nSteps: pick the angle from the notebook's own list, record, drop the raw take in the drive, and open one task per handoff (editor cut, thumbnail, storage link).\n\nDone when the raw take is in the drive and the three handoff tasks exist.";
53
+
54
+ let scenario = $state<Scenario>("default");
55
+ let narrow = $state(false);
56
+ let title = $state("Record the video and hand it off");
57
+ let description = $state<string | null>(brief);
58
+ let thread = $state<TaskComment[]>([...seed]);
59
+ let done = $state(false);
60
+ let timer = $state<{ startedAt: number } | null>(null);
61
+ let entries = $state([
62
+ { id: "e1", minutes: 90, day: new Date(now - 24 * hour).toISOString().slice(0, 10), person: "Ada Lovelace", note: "Outline" },
63
+ ]);
64
+
65
+ $effect(() => {
66
+ const s = scenario;
67
+ title = s === "long" ? longTitle : "Record the video and hand it off";
68
+ description = s === "emptyDescription" ? null : brief;
69
+ thread = s === "busy" ? [...crowd] : s === "loading" ? [] : [...seed];
70
+ done = s === "closed";
71
+ timer = s === "running" ? { startedAt: Date.now() - 754_000 } : null;
72
+ });
73
+
74
+ let assignee = $derived(scenario === "unassigned" ? null : "Ada Lovelace");
75
+ let status = $derived(done ? "done" : scenario === "running" ? "doing" : "todo");
76
+ let statusLabel = $derived(statuses.label(status));
77
+
78
+ function onLog(input: TimeLogInput) {
79
+ entries = [{ id: `e${entries.length + 1}`, minutes: input.minutes, day: input.day, person: "Ada Lovelace", note: input.note ?? "" }, ...entries];
80
+ return true;
81
+ }
82
+
83
+ function onStop(info: TimeStopInfo) {
84
+ onLog({ minutes: info.minutes, day: info.day });
85
+ timer = null;
86
+ }
87
+
88
+ function onSend(body: string) {
89
+ thread = [...thread, { id: `n${thread.length}`, author: "Ada Lovelace", body, createdAt: new Date().toISOString() }];
90
+ }
91
+ </script>
92
+
93
+ <div class="flex flex-col gap-3">
94
+ <div class="flex flex-wrap items-center gap-1.5" role="group" aria-label="Scenario">
95
+ {#each scenarios as item (item.id)}
96
+ <button
97
+ type="button"
98
+ aria-pressed={scenario === item.id}
99
+ onclick={() => (scenario = item.id)}
100
+ class="h-7 rounded-md border border-border px-2.5 text-[13px] font-medium transition-colors hover:bg-muted aria-pressed:bg-foreground aria-pressed:text-background"
101
+ >
102
+ {item.label}
103
+ </button>
104
+ {/each}
105
+ <button
106
+ type="button"
107
+ aria-pressed={narrow}
108
+ onclick={() => (narrow = !narrow)}
109
+ class="ms-auto h-7 rounded-md border border-border px-2.5 text-[13px] font-medium transition-colors hover:bg-muted aria-pressed:bg-foreground aria-pressed:text-background"
110
+ >
111
+ 390 wide
112
+ </button>
113
+ </div>
114
+
115
+ <div class="mx-auto w-full overflow-hidden rounded-xl border border-border bg-background" style:max-width={narrow ? "390px" : "none"}>
116
+ <TaskDetail
117
+ {title}
118
+ onTitleChange={(next) => {
119
+ title = next;
120
+ }}
121
+ {done}
122
+ closed={done}
123
+ onToggleDone={() => (done = !done)}
124
+ {description}
125
+ onDescriptionChange={(next) => {
126
+ description = next;
127
+ }}
128
+ comments={thread}
129
+ loadingComments={scenario === "loading"}
130
+ {onSend}
131
+ onDeleteComment={(comment) => {
132
+ thread = thread.filter((row) => row.id !== comment.id);
133
+ }}
134
+ canEditComment={(comment) => !comment.agent && comment.author === "Ada Lovelace"}
135
+ >
136
+ {#snippet meta()}
137
+ <span>From a call</span>
138
+ <span class="tabular-nums">Created Sep 9, 2026</span>
139
+ {/snippet}
140
+ {#snippet properties()}
141
+ <TaskProperty label="Status">
142
+ <TaskChip aria-label="Status: {statusLabel}"><StatusDot {statuses} {status} size={14} animate={false} />{statusLabel}</TaskChip>
143
+ </TaskProperty>
144
+ <TaskProperty label="Priority">
145
+ <TaskChip><Flag weight="fill" class="text-destructive" aria-hidden="true" />High</TaskChip>
146
+ </TaskProperty>
147
+ <TaskProperty label="Assignee">
148
+ {#if assignee}
149
+ <TaskChip>
150
+ <span class="grid size-4 place-items-center rounded-full bg-muted text-[0px]" aria-hidden="true"><User /></span>
151
+ {assignee}
152
+ </TaskChip>
153
+ {:else}
154
+ <TaskChip empty><CircleDashed aria-hidden="true" />Assign</TaskChip>
155
+ {/if}
156
+ </TaskProperty>
157
+ <TaskProperty label="Due date">
158
+ <TaskChip><CalendarBlank aria-hidden="true" />Oct 9, 2026</TaskChip>
159
+ </TaskProperty>
160
+ <TaskProperty label="Project">
161
+ <TaskChip><Folder aria-hidden="true" />Agency OS</TaskChip>
162
+ </TaskProperty>
163
+ <TaskProperty label="Time">
164
+ <TimeTracker
165
+ {entries}
166
+ running={timer}
167
+ estimate={240}
168
+ getId={(entry) => entry.id}
169
+ getMinutes={(entry) => entry.minutes}
170
+ getDay={(entry) => entry.day}
171
+ getPerson={(entry) => entry.person}
172
+ getNote={(entry) => entry.note}
173
+ onStart={() => {
174
+ timer = { startedAt: Date.now() };
175
+ }}
176
+ {onStop}
177
+ {onLog}
178
+ onDelete={(entry) => {
179
+ entries = entries.filter((row) => row.id !== entry.id);
180
+ }}
181
+ />
182
+ </TaskProperty>
183
+ {/snippet}
184
+ </TaskDetail>
185
+ </div>
186
+ </div>
@@ -0,0 +1,54 @@
1
+ /** One entry in a task's thread. The host resolves the author's name. */
2
+ export type TaskComment = {
3
+ id: string;
4
+ author: string;
5
+ body: string;
6
+ /** ISO timestamp. */
7
+ createdAt: string;
8
+ /** An agent wrote it: the thread draws the agent avatar and an "AI" label. */
9
+ agent?: boolean;
10
+ /** Set when the body changed after posting. */
11
+ edited?: boolean;
12
+ };
13
+
14
+ export type TaskDetailLabels = {
15
+ markComplete: string;
16
+ markIncomplete: string;
17
+ editTitle: string;
18
+ description: string;
19
+ addDescription: string;
20
+ editDescription: string;
21
+ save: string;
22
+ saving: string;
23
+ cancel: string;
24
+ comments: string;
25
+ commentPlaceholder: string;
26
+ commentSend: string;
27
+ commentEdit: string;
28
+ commentDelete: string;
29
+ commentEdited: string;
30
+ commentsEmpty: string;
31
+ ai: string;
32
+ properties: string;
33
+ };
34
+
35
+ export const DEFAULT_TASK_DETAIL_LABELS: TaskDetailLabels = {
36
+ markComplete: "Mark complete",
37
+ markIncomplete: "Mark incomplete",
38
+ editTitle: "Edit title",
39
+ description: "Description",
40
+ addDescription: "Add a description",
41
+ editDescription: "Edit description",
42
+ save: "Save",
43
+ saving: "Saving",
44
+ cancel: "Cancel",
45
+ comments: "Activity",
46
+ commentPlaceholder: "Leave a comment",
47
+ commentSend: "Comment",
48
+ commentEdit: "Edit comment",
49
+ commentDelete: "Delete comment",
50
+ commentEdited: "edited",
51
+ commentsEmpty: "No comments yet",
52
+ ai: "AI",
53
+ properties: "Properties",
54
+ };
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLButtonAttributes } from "svelte/elements";
4
+ import { cn } from "../../lib/utils.js";
5
+
6
+ interface Props extends Omit<HTMLButtonAttributes, "children"> {
7
+ /** No value yet: the label is a prompt and recedes until hovered. */
8
+ empty?: boolean;
9
+ children: Snippet;
10
+ }
11
+
12
+ let { empty = false, class: className, children, ...rest }: Props = $props();
13
+ </script>
14
+
15
+ <!-- The one box every property value sits in: 28px, 40 under a coarse pointer. -->
16
+ <button
17
+ type="button"
18
+ class={cn(
19
+ "inline-flex h-7 min-w-0 items-center gap-1.5 rounded-md ps-1.5 pe-2.5 text-[13px] whitespace-nowrap outline-none select-none any-pointer-coarse:h-10",
20
+ "transition-colors hover:bg-muted data-[state=open]:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/50",
21
+ "disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3.5 [&_svg]:shrink-0",
22
+ empty && "text-muted-foreground hover:text-foreground",
23
+ className,
24
+ )}
25
+ {...rest}
26
+ >
27
+ {@render children()}
28
+ </button>