@heyramzi/ui 1.54.2 → 2.0.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 (361) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +198 -6
  4. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  5. package/src/ambient.d.ts +8 -0
  6. package/src/brand/brand.test.ts +67 -0
  7. package/src/brand/index.ts +137 -0
  8. package/src/canvas/README.md +236 -0
  9. package/src/canvas/card/card-frame.svelte +61 -0
  10. package/src/canvas/card/card.css +39 -0
  11. package/src/canvas/card/resize-handles.svelte +119 -0
  12. package/src/canvas/card/resize.ts +33 -0
  13. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  14. package/src/canvas/chrome/chrome.css +231 -0
  15. package/src/canvas/chrome/lock-badge.svelte +18 -0
  16. package/src/canvas/chrome/side-handles.svelte +30 -0
  17. package/src/canvas/chrome/size-chip.svelte +45 -0
  18. package/src/canvas/chrome/snap-guides.svelte +69 -0
  19. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  20. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  21. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  22. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  23. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  24. package/src/canvas/edges/bend-handle.svelte +76 -0
  25. package/src/canvas/edges/edge-gradient.svelte +30 -0
  26. package/src/canvas/edges/edge-path.test.ts +58 -0
  27. package/src/canvas/edges/edge-path.ts +293 -0
  28. package/src/canvas/edges/gradient-edge.svelte +43 -0
  29. package/src/canvas/editor/context-menu.test.ts +65 -0
  30. package/src/canvas/editor/context-menu.ts +48 -0
  31. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  32. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  33. package/src/canvas/editor/editor-context.ts +62 -0
  34. package/src/canvas/editor/editor-mode.ts +67 -0
  35. package/src/canvas/editor/frame.ts +98 -0
  36. package/src/canvas/editor/hand-context.ts +15 -0
  37. package/src/canvas/editor/history-core.test.ts +239 -0
  38. package/src/canvas/editor/history-core.ts +247 -0
  39. package/src/canvas/editor/keymap.test.ts +161 -0
  40. package/src/canvas/editor/keymap.ts +151 -0
  41. package/src/canvas/editor/layout-actions.test.ts +156 -0
  42. package/src/canvas/editor/layout-actions.ts +219 -0
  43. package/src/canvas/editor/lock.test.ts +68 -0
  44. package/src/canvas/editor/lock.ts +87 -0
  45. package/src/canvas/editor/rebase.test.ts +208 -0
  46. package/src/canvas/editor/rebase.ts +272 -0
  47. package/src/canvas/editor/style.ts +184 -0
  48. package/src/canvas/editor/text-field-context.ts +41 -0
  49. package/src/canvas/editor/z-order.test.ts +75 -0
  50. package/src/canvas/editor/z-order.ts +87 -0
  51. package/src/canvas/headless/alignment.test.ts +59 -0
  52. package/src/canvas/headless/alignment.ts +23 -0
  53. package/src/canvas/headless/break-apart-find.ts +418 -0
  54. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  55. package/src/canvas/headless/break-apart-plan.ts +439 -0
  56. package/src/canvas/headless/break-apart-style.ts +106 -0
  57. package/src/canvas/headless/break-apart.ts +244 -0
  58. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  59. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  60. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  61. package/src/canvas/headless/edge-anchors.ts +143 -0
  62. package/src/canvas/headless/edge-memo.test.ts +58 -0
  63. package/src/canvas/headless/edge-memo.ts +32 -0
  64. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  65. package/src/canvas/headless/edge-serialization.ts +32 -0
  66. package/src/canvas/headless/figure-geometry.ts +293 -0
  67. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  68. package/src/canvas/headless/fit-viewport.ts +137 -0
  69. package/src/canvas/headless/grid.test.ts +34 -0
  70. package/src/canvas/headless/grid.ts +94 -0
  71. package/src/canvas/headless/icon-node.test.ts +23 -0
  72. package/src/canvas/headless/icon-node.ts +66 -0
  73. package/src/canvas/headless/index.ts +9 -0
  74. package/src/canvas/headless/linkify.test.ts +23 -0
  75. package/src/canvas/headless/linkify.ts +25 -0
  76. package/src/canvas/headless/pen-marks.ts +133 -0
  77. package/src/canvas/headless/sketch.test.ts +99 -0
  78. package/src/canvas/headless/sketch.ts +302 -0
  79. package/src/canvas/headless/snap-guides.test.ts +61 -0
  80. package/src/canvas/headless/snap-guides.ts +146 -0
  81. package/src/canvas/headless/svg-figure.test.ts +49 -0
  82. package/src/canvas/headless/svg-figure.ts +176 -0
  83. package/src/canvas/headless/table-layout.test.ts +158 -0
  84. package/src/canvas/headless/table-layout.ts +309 -0
  85. package/src/canvas/index.ts +35 -0
  86. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  87. package/src/canvas/lod/lod-switch.svelte +140 -0
  88. package/src/canvas/lod/render-all.svelte.ts +52 -0
  89. package/src/canvas/ops/arrange.ts +108 -0
  90. package/src/canvas/ops/ops.test.ts +98 -0
  91. package/src/canvas/ops/ops.ts +347 -0
  92. package/src/canvas/pen/PenField.svelte +128 -0
  93. package/src/canvas/pen/PenField.test.ts +87 -0
  94. package/src/canvas/pen/PenOverlay.svelte +172 -0
  95. package/src/canvas/pen/ShapeNode.svelte +64 -0
  96. package/src/canvas/pen/TextNode.svelte +97 -0
  97. package/src/canvas/pen/colors.ts +28 -0
  98. package/src/canvas/pen/eraser.ts +46 -0
  99. package/src/canvas/pen/geometry.ts +148 -0
  100. package/src/canvas/pen/host-field.test.svelte +13 -0
  101. package/src/canvas/pen/index.ts +40 -0
  102. package/src/canvas/pen/ops.ts +16 -0
  103. package/src/canvas/pen/pen-session.ts +177 -0
  104. package/src/canvas/pen/pen.css +149 -0
  105. package/src/canvas/pen/plugin.ts +139 -0
  106. package/src/canvas/pen/shapes.ts +104 -0
  107. package/src/canvas/pen/stroke-path.ts +54 -0
  108. package/src/canvas/pen/types.ts +96 -0
  109. package/src/canvas/pen/ui.ts +26 -0
  110. package/src/canvas/plugin.test.ts +45 -0
  111. package/src/canvas/plugin.ts +159 -0
  112. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  113. package/src/canvas/screen/PartView.svelte +176 -0
  114. package/src/canvas/screen/ScreenBody.svelte +142 -0
  115. package/src/canvas/screen/ScreenCard.svelte +32 -0
  116. package/src/canvas/screen/glyphs/button.svg +1 -0
  117. package/src/canvas/screen/glyphs/close.svg +1 -0
  118. package/src/canvas/screen/glyphs/down.svg +1 -0
  119. package/src/canvas/screen/glyphs/header.svg +1 -0
  120. package/src/canvas/screen/glyphs/hills.svg +1 -0
  121. package/src/canvas/screen/glyphs/image.svg +1 -0
  122. package/src/canvas/screen/glyphs/input.svg +1 -0
  123. package/src/canvas/screen/glyphs/list.svg +1 -0
  124. package/src/canvas/screen/glyphs/modal.svg +1 -0
  125. package/src/canvas/screen/glyphs/plus.svg +1 -0
  126. package/src/canvas/screen/glyphs/up.svg +1 -0
  127. package/src/canvas/screen/glyphs.ts +40 -0
  128. package/src/canvas/screen/labels.ts +41 -0
  129. package/src/canvas/screen/parts.ts +164 -0
  130. package/src/canvas/screen/plugin.ts +67 -0
  131. package/src/canvas/screen/screen.css +593 -0
  132. package/src/canvas/screen/types.ts +47 -0
  133. package/src/canvas/screen/ui.ts +13 -0
  134. package/src/canvas/test-utils.ts +18 -0
  135. package/src/canvas/types.ts +53 -0
  136. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  137. package/src/canvas/ui/FrameLayer.svelte +116 -0
  138. package/src/canvas/ui/PenLayer.svelte +39 -0
  139. package/src/canvas/ui/StylePanel.svelte +268 -0
  140. package/src/canvas/ui/canvas-grid.svelte +156 -0
  141. package/src/canvas/ui/canvas-ui.css +350 -0
  142. package/src/canvas/ui/forward-wheel.ts +29 -0
  143. package/src/canvas/ui/icons.ts +124 -0
  144. package/src/canvas/ui/place-panel.ts +80 -0
  145. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  146. package/src/canvas/ui/shortcut-tip.ts +17 -0
  147. package/src/canvas/ui/tools.ts +146 -0
  148. package/src/charts/README.md +104 -0
  149. package/src/charts/ambient.d.ts +4 -0
  150. package/src/charts/animated-value.css +7 -0
  151. package/src/charts/animated-value.svelte +110 -0
  152. package/src/charts/area-chart.css +29 -0
  153. package/src/charts/area-chart.svelte +204 -0
  154. package/src/charts/bar-chart.css +119 -0
  155. package/src/charts/bar-chart.svelte +275 -0
  156. package/src/charts/charts.css +201 -0
  157. package/src/charts/crosshair.svelte +35 -0
  158. package/src/charts/dashboard/chart.ts +5 -0
  159. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  160. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  161. package/src/charts/dashboard/data-panel.svelte +81 -0
  162. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  163. package/src/charts/dashboard/data-table.svelte +278 -0
  164. package/src/charts/dashboard/data-table.ts +26 -0
  165. package/src/charts/dashboard/index.ts +25 -0
  166. package/src/charts/dashboard/panel-grid.svelte +43 -0
  167. package/src/charts/dashboard/period-filter.svelte +53 -0
  168. package/src/charts/dashboard/rating-chip.svelte +35 -0
  169. package/src/charts/dashboard/stat-band.svelte +47 -0
  170. package/src/charts/dashboard/stat-tile.svelte +176 -0
  171. package/src/charts/dashboard/windows.ts +8 -0
  172. package/src/charts/donut-chart.css +56 -0
  173. package/src/charts/donut-chart.svelte +158 -0
  174. package/src/charts/edge-fade.svelte +43 -0
  175. package/src/charts/edge-fade.ts +16 -0
  176. package/src/charts/figure.css +100 -0
  177. package/src/charts/figure.svelte +122 -0
  178. package/src/charts/gauge-chart.css +94 -0
  179. package/src/charts/gauge-chart.svelte +302 -0
  180. package/src/charts/hover-band.test.ts +30 -0
  181. package/src/charts/hover-band.ts +21 -0
  182. package/src/charts/hover-focus.svelte +85 -0
  183. package/src/charts/index.ts +31 -0
  184. package/src/charts/legend.css +136 -0
  185. package/src/charts/legend.svelte +112 -0
  186. package/src/charts/line-chart.css +26 -0
  187. package/src/charts/line-chart.svelte +256 -0
  188. package/src/charts/motion.ts +19 -0
  189. package/src/charts/number-chart.css +179 -0
  190. package/src/charts/number-chart.svelte +187 -0
  191. package/src/charts/primitives/chart-container.svelte +57 -0
  192. package/src/charts/primitives/chart-style.svelte +36 -0
  193. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  194. package/src/charts/primitives/chart-utils.ts +54 -0
  195. package/src/charts/primitives/index.ts +11 -0
  196. package/src/charts/reference-line.css +18 -0
  197. package/src/charts/reference-line.svelte +31 -0
  198. package/src/charts/reveal.svelte +61 -0
  199. package/src/charts/ring-chart.css +104 -0
  200. package/src/charts/ring-chart.svelte +166 -0
  201. package/src/charts/sparkline.css +45 -0
  202. package/src/charts/sparkline.svelte +141 -0
  203. package/src/charts/stat-card.css +105 -0
  204. package/src/charts/stat-card.svelte +267 -0
  205. package/src/charts/time-series-chart.css +28 -0
  206. package/src/charts/time-series-chart.svelte +179 -0
  207. package/src/charts/trend-pill.css +38 -0
  208. package/src/charts/trend-pill.svelte +48 -0
  209. package/src/components/copy/copy-button.svelte +1 -1
  210. package/src/components/docs/doc-page.svelte +1 -1
  211. package/src/components/docs/on-this-page.svelte +2 -2
  212. package/src/components/list-page/empty-state.svelte +1 -1
  213. package/src/components/list-page/filter-popover.svelte +1 -1
  214. package/src/components/list-page/section-label.svelte +1 -1
  215. package/src/components/segmented/segment.svelte +4 -4
  216. package/src/components/theme/theme-provider.svelte +6 -4
  217. package/src/components/toolbar/index.ts +5 -0
  218. package/src/components/toolbar/toolbar.svelte +168 -0
  219. package/src/components/toolbar/toolbar.test.ts +66 -0
  220. package/src/editor/README.md +58 -0
  221. package/src/editor/ambient.d.ts +1 -0
  222. package/src/editor/block-id.ts +77 -0
  223. package/src/editor/block-ops.test.ts +237 -0
  224. package/src/editor/block-ops.ts +157 -0
  225. package/src/editor/content.css +418 -0
  226. package/src/editor/editor-toolbar.svelte +303 -0
  227. package/src/editor/editor.svelte +462 -0
  228. package/src/editor/extensions.ts +145 -0
  229. package/src/editor/image-toolbar.svelte +123 -0
  230. package/src/editor/index.ts +17 -0
  231. package/src/editor/inline-editor.svelte +261 -0
  232. package/src/editor/markdown-escaping.test.ts +158 -0
  233. package/src/editor/markdown-marks.test.ts +89 -0
  234. package/src/editor/markdown-test-helpers.ts +13 -0
  235. package/src/editor/markdown-youtube.test.ts +93 -0
  236. package/src/editor/markdown.test.ts +250 -0
  237. package/src/editor/markdown.ts +511 -0
  238. package/src/editor/renderer.svelte +92 -0
  239. package/src/editor/serializer.test.ts +133 -0
  240. package/src/editor/serializer.ts +370 -0
  241. package/src/editor/slash-items.ts +115 -0
  242. package/src/editor/slash-menu.svelte +159 -0
  243. package/src/editor/youtube-url.ts +35 -0
  244. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  245. package/src/lib/result.ts +19 -0
  246. package/src/onboarding/README.md +120 -0
  247. package/src/onboarding/define-tours.test.ts +50 -0
  248. package/src/onboarding/define-tours.ts +31 -0
  249. package/src/onboarding/demo.svelte +86 -0
  250. package/src/onboarding/index.ts +12 -0
  251. package/src/onboarding/onboarding-provider.svelte +120 -0
  252. package/src/onboarding/onboarding.svelte.ts +24 -0
  253. package/src/onboarding/pick.test.ts +79 -0
  254. package/src/onboarding/pick.ts +36 -0
  255. package/src/onboarding/stores.test.ts +103 -0
  256. package/src/onboarding/stores.ts +58 -0
  257. package/src/onboarding/tour-card.svelte +70 -0
  258. package/src/onboarding/tour-runner.svelte +197 -0
  259. package/src/onboarding/types.ts +34 -0
  260. package/src/onboarding/wait.ts +29 -0
  261. package/src/shadcn/command/command-input.svelte +1 -1
  262. package/src/slides/Controls.svelte +138 -0
  263. package/src/slides/Deck.svelte +271 -0
  264. package/src/slides/Fragment.svelte +71 -0
  265. package/src/slides/Overview.svelte +113 -0
  266. package/src/slides/Presenter.svelte +292 -0
  267. package/src/slides/README.md +89 -0
  268. package/src/slides/Slide.svelte +179 -0
  269. package/src/slides/SlideList.svelte +19 -0
  270. package/src/slides/ambient.d.ts +1 -0
  271. package/src/slides/bridges.ts +69 -0
  272. package/src/slides/deck-controller.svelte.ts +292 -0
  273. package/src/slides/demo-slides.svelte +358 -0
  274. package/src/slides/demo.svelte +176 -0
  275. package/src/slides/export/capture.ts +113 -0
  276. package/src/slides/export/ffmpeg.ts +120 -0
  277. package/src/slides/export/index.ts +9 -0
  278. package/src/slides/export/launch.ts +10 -0
  279. package/src/slides/export/pdf.ts +152 -0
  280. package/src/slides/export/types.ts +48 -0
  281. package/src/slides/index.ts +19 -0
  282. package/src/slides/labels.ts +48 -0
  283. package/src/slides/layout.ts +51 -0
  284. package/src/slides/model.ts +47 -0
  285. package/src/slides/navigation.ts +100 -0
  286. package/src/slides/theme.css +205 -0
  287. package/src/slides/types.ts +86 -0
  288. package/src/tasks/README.md +312 -0
  289. package/src/tasks/ambient.d.ts +3 -0
  290. package/src/tasks/board-order.ts +137 -0
  291. package/src/tasks/dates.ts +31 -0
  292. package/src/tasks/define-statuses.ts +32 -0
  293. package/src/tasks/demo.svelte +389 -0
  294. package/src/tasks/detail/detail-demo.svelte +186 -0
  295. package/src/tasks/detail/detail-types.ts +54 -0
  296. package/src/tasks/detail/task-chip.svelte +28 -0
  297. package/src/tasks/detail/task-detail.svelte +315 -0
  298. package/src/tasks/detail/task-property.svelte +17 -0
  299. package/src/tasks/detail/task-thread.svelte +258 -0
  300. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  301. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  302. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  303. package/src/tasks/gantt/gantt-header.svelte +71 -0
  304. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  305. package/src/tasks/gantt/gantt.css +22 -0
  306. package/src/tasks/gantt/gantt.test.ts +77 -0
  307. package/src/tasks/gantt/logic.test.ts +163 -0
  308. package/src/tasks/gantt/logic.ts +248 -0
  309. package/src/tasks/inbox/inbox-card.svelte +283 -0
  310. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  311. package/src/tasks/inbox/inbox-logic.ts +55 -0
  312. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  313. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  314. package/src/tasks/inbox/inbox-types.ts +108 -0
  315. package/src/tasks/inbox/inbox.svelte +203 -0
  316. package/src/tasks/inbox/index.ts +23 -0
  317. package/src/tasks/index.ts +85 -0
  318. package/src/tasks/kanban-board.svelte +561 -0
  319. package/src/tasks/kanban-column.svelte +117 -0
  320. package/src/tasks/kanban-drag.ts +38 -0
  321. package/src/tasks/kanban-types.ts +65 -0
  322. package/src/tasks/presets/sales.ts +16 -0
  323. package/src/tasks/record/demo-data.ts +286 -0
  324. package/src/tasks/record/field-section.svelte +53 -0
  325. package/src/tasks/record/fields.ts +142 -0
  326. package/src/tasks/record/format.ts +87 -0
  327. package/src/tasks/record/gutter.ts +2 -0
  328. package/src/tasks/record/index.ts +54 -0
  329. package/src/tasks/record/info-list.svelte +316 -0
  330. package/src/tasks/record/kinds.ts +42 -0
  331. package/src/tasks/record/logic.ts +324 -0
  332. package/src/tasks/record/next-up.svelte +70 -0
  333. package/src/tasks/record/project-list.svelte +133 -0
  334. package/src/tasks/record/record-demo.svelte +324 -0
  335. package/src/tasks/record/record-header.svelte +70 -0
  336. package/src/tasks/record/record-shell.svelte +138 -0
  337. package/src/tasks/record/record-stats.svelte +48 -0
  338. package/src/tasks/record/record-timeline.svelte +231 -0
  339. package/src/tasks/record/record.test.ts +350 -0
  340. package/src/tasks/record/steps-rail.svelte +474 -0
  341. package/src/tasks/record/types.ts +280 -0
  342. package/src/tasks/record/where-we-are.svelte +121 -0
  343. package/src/tasks/reorder.ts +19 -0
  344. package/src/tasks/side-panel-types.ts +16 -0
  345. package/src/tasks/side-panel.svelte +193 -0
  346. package/src/tasks/status-badge.svelte +63 -0
  347. package/src/tasks/status-dot.svelte +228 -0
  348. package/src/tasks/status-icon.svelte +30 -0
  349. package/src/tasks/status-types.ts +40 -0
  350. package/src/tasks/task-gantt.svelte +230 -0
  351. package/src/tasks/task-list.svelte +470 -0
  352. package/src/tasks/task-types.ts +33 -0
  353. package/src/tasks/time/duration.ts +69 -0
  354. package/src/tasks/time/time-chip.svelte +62 -0
  355. package/src/tasks/time/time-dot.svelte +58 -0
  356. package/src/tasks/time/time-ring.svelte +74 -0
  357. package/src/tasks/time/time-tracker.svelte +565 -0
  358. package/src/tasks/time/time-types.ts +74 -0
  359. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  360. package/src/tokens/base.css +5 -5
  361. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,280 @@
1
+ // The view types of a client or project record. Plain data in, nothing loaded:
2
+ // the host maps its own rows onto these and passes them down.
3
+ import type { PhosphorIcon } from "../../lib/icon-types.js";
4
+ import type { Tone } from "../../lib/tone.js";
5
+
6
+ /** Where a stage sits in the pipeline. Structure, not an agency's own names. */
7
+ export type StageCategory = "open" | "proposal" | "ongoing" | "completed" | "lost";
8
+
9
+ /** One stage an agency declares. The record draws its label and tone. */
10
+ export type StageDef = {
11
+ key: string;
12
+ label: string;
13
+ tone: Tone;
14
+ category: StageCategory;
15
+ };
16
+
17
+ /**
18
+ * One thing that happened. Append-only. It belongs to a project, or to a client
19
+ * when no project exists yet. `occurredAt` is an ISO timestamp.
20
+ */
21
+ export type RecordEvent = {
22
+ id: string;
23
+ /** A registry key: `call`, `note`, `stage`, or a module's own `slug:name`. */
24
+ kind: string;
25
+ occurredAt: string;
26
+ title: string;
27
+ body?: string | null;
28
+ /** Money, in the currency's major unit. */
29
+ amount?: number | null;
30
+ currency?: string | null;
31
+ /** Who or what wrote it. The host resolves the name. */
32
+ actor?: string | null;
33
+ /** A note that changed after posting. */
34
+ edited?: boolean;
35
+ /** The kind's own declared fields, already formatted by the host for display. */
36
+ details?: { label: string; value: string }[];
37
+ /** The project it belongs to. Set in client mode, so each entry can say which. */
38
+ projectId?: string | null;
39
+ projectName?: string | null;
40
+ projectHref?: string | null;
41
+ /** Where the event's source lives: the call, the invoice, the deck. */
42
+ href?: string | null;
43
+ };
44
+
45
+ /** The numbers and lines one project rolls up to. The host computes them. */
46
+ export type RecordSummary = {
47
+ id: string;
48
+ name: string;
49
+ href?: string;
50
+ stageKey: string;
51
+ /** Short qualifier drawn after the stage, such as a loss reason. */
52
+ stageDetail?: string | null;
53
+ /** Money, in the currency's major unit. */
54
+ value?: number | null;
55
+ currency?: string | null;
56
+ /** Money received, from payment events. */
57
+ paid?: number | null;
58
+ startDate?: string | null;
59
+ endDate?: string | null;
60
+ whereWeAre?: {
61
+ text: string;
62
+ /** ISO timestamp of the status event. */
63
+ at?: string | null;
64
+ actor?: string | null;
65
+ health?: { label: string; tone: Tone } | null;
66
+ } | null;
67
+ next?: RecordNext | null;
68
+ lastCallAt?: string | null;
69
+ lastTouchAt?: string | null;
70
+ progress?: { done: number; total: number } | null;
71
+ /** A cancelled or archived project: dropped from "active" and "next". */
72
+ closed?: boolean;
73
+ };
74
+
75
+ /** The next milestone, or the next dated task when there are no milestones. */
76
+ export type RecordNext = {
77
+ label: string;
78
+ /** ISO date. */
79
+ date?: string | null;
80
+ /** ISO date, for a range. */
81
+ endDate?: string | null;
82
+ owner?: string | null;
83
+ href?: string | null;
84
+ projectId?: string | null;
85
+ projectName?: string | null;
86
+ };
87
+
88
+ export type FieldType =
89
+ | "text"
90
+ | "longtext"
91
+ | "choice"
92
+ | "multi"
93
+ | "money"
94
+ | "date"
95
+ | "url"
96
+ | "email"
97
+ | "timezone";
98
+
99
+ export type FieldOption = {
100
+ key: string;
101
+ label: string;
102
+ tone?: Tone;
103
+ /** Still reads on old records, cannot be picked. */
104
+ archived?: boolean;
105
+ };
106
+
107
+ /** What a field holds. A choice is its option key, a multi is its keys. */
108
+ export type FieldValue = string | number | string[] | null;
109
+
110
+ /** One attribute a module declares. The record draws it and calls `onSave`. */
111
+ export type FieldDef = {
112
+ key: string;
113
+ label: string;
114
+ type: FieldType;
115
+ options?: FieldOption[];
116
+ section?: string;
117
+ required?: boolean;
118
+ /** Shown only while another field has this value. Without `equals`, any value. */
119
+ showWhen?: { key: string; equals?: string | string[] };
120
+ /** For money. Defaults to the record's currency. */
121
+ currency?: string;
122
+ placeholder?: string;
123
+ };
124
+
125
+ export type FieldSectionDef = { key: string; label: string };
126
+
127
+ /** One step of the rail. A step is a milestone task. */
128
+ export type StepDef = {
129
+ id: string;
130
+ label: string;
131
+ done: boolean;
132
+ /** Which group header it sits under, such as Sales or Delivery. */
133
+ group?: string;
134
+ /** ISO dates. Both set draws a range, one draws a day. */
135
+ start?: string | null;
136
+ end?: string | null;
137
+ /** Hidden from the rail, still in the edit menu. */
138
+ hidden?: boolean;
139
+ /** The label was renamed: the menu offers a reset. */
140
+ renamed?: boolean;
141
+ };
142
+
143
+ export type StepGroupDef = { key: string; label: string };
144
+
145
+ export type StepMarker = "complete" | "current" | "late" | "pending";
146
+
147
+ /** One row of an `InfoList`. The value is already formatted for display. */
148
+ export type InfoRow = {
149
+ /** Names the row for a host to find it. Falls back to the label. */
150
+ id?: string;
151
+ label: string;
152
+ icon?: PhosphorIcon;
153
+ value?: string | null;
154
+ /** A coloured chip in place of plain text. */
155
+ tone?: Tone;
156
+ /** Chips for a multi value. Wins over `value`. */
157
+ chips?: { label: string; tone?: Tone }[];
158
+ /** What the editor starts from, when it differs from what is drawn. */
159
+ editValue?: string | string[] | null;
160
+ href?: string;
161
+ muted?: boolean;
162
+ /** `select` becomes a searchable list past seven options. */
163
+ input?: "text" | "longtext" | "select" | "multi" | "date" | "number" | "url" | "email";
164
+ options?: { value: string; label: string; tone?: Tone }[];
165
+ placeholder?: string;
166
+ required?: boolean;
167
+ /** Without it the row is read-only. A multi row passes the picked keys. */
168
+ onSave?: (value: string | string[]) => void | Promise<void>;
169
+ divider?: boolean;
170
+ };
171
+
172
+ /** A kind of event: how its filter chip and its default row read. */
173
+ export type RecordKindDef = {
174
+ key: string;
175
+ label: string;
176
+ icon?: PhosphorIcon;
177
+ tone?: Tone;
178
+ };
179
+
180
+ /** A filter chip: a label over the kinds it shows. */
181
+ export type RecordFilterDef = { key: string; label: string; kinds: string[] };
182
+
183
+ export type RecordLabels = {
184
+ whereWeAre: string;
185
+ more: string;
186
+ less: string;
187
+ next: string;
188
+ late: string;
189
+ steps: string;
190
+ stepsCount: (done: number, total: number) => string;
191
+ editSteps: string;
192
+ rename: string;
193
+ resetName: string;
194
+ hide: string;
195
+ show: string;
196
+ moveUp: string;
197
+ moveDown: string;
198
+ shownCount: (shown: number, total: number) => string;
199
+ openSteps: string;
200
+ activity: string;
201
+ all: string;
202
+ noEvents: string;
203
+ older: (count: number) => string;
204
+ today: string;
205
+ yesterday: string;
206
+ by: (name: string) => string;
207
+ projects: string;
208
+ projectCount: (count: number) => string;
209
+ noProjects: string;
210
+ details: string;
211
+ edit: (field: string) => string;
212
+ add: (field: string) => string;
213
+ save: string;
214
+ cancel: string;
215
+ search: string;
216
+ noMatch: string;
217
+ pipeline: string;
218
+ won: string;
219
+ paid: string;
220
+ lastCall: string;
221
+ lastTouch: string;
222
+ noCall: string;
223
+ open: string;
224
+ edited: string;
225
+ noSteps: string;
226
+ saveFailed: string;
227
+ required: string;
228
+ };
229
+
230
+ export const DEFAULT_RECORD_LABELS: RecordLabels = {
231
+ whereWeAre: "Where we are",
232
+ more: "More",
233
+ less: "Less",
234
+ next: "Next",
235
+ late: "Late",
236
+ steps: "Steps",
237
+ stepsCount: (done, total) => `${done}/${total}`,
238
+ editSteps: "Choose visible steps",
239
+ rename: "Rename",
240
+ resetName: "Reset name",
241
+ hide: "Hide",
242
+ show: "Show",
243
+ moveUp: "Move up",
244
+ moveDown: "Move down",
245
+ shownCount: (shown, total) => `${shown} of ${total} shown`,
246
+ openSteps: "Open steps",
247
+ activity: "Activity",
248
+ all: "All",
249
+ noEvents: "Nothing logged yet",
250
+ older: (count) => `Show ${count} older`,
251
+ today: "Today",
252
+ yesterday: "Yesterday",
253
+ by: (name) => `by ${name}`,
254
+ projects: "Projects",
255
+ projectCount: (count) => (count === 1 ? "1 project" : `${count} projects`),
256
+ noProjects: "No projects",
257
+ details: "Details",
258
+ edit: (field) => `Edit ${field}`,
259
+ add: (field) => `Add ${field.toLowerCase()}`,
260
+ save: "Save",
261
+ cancel: "Cancel",
262
+ search: "Search",
263
+ noMatch: "No match",
264
+ pipeline: "Pipeline",
265
+ won: "Won",
266
+ paid: "Paid",
267
+ lastCall: "Last call",
268
+ lastTouch: "Last touch",
269
+ noCall: "None yet",
270
+ open: "Open",
271
+ edited: "edited",
272
+ noSteps: "No steps yet",
273
+ saveFailed: "Couldn't save",
274
+ required: "Required",
275
+ };
276
+
277
+ /** Merge a partial label set over the defaults. */
278
+ export function resolveLabels(labels?: Partial<RecordLabels>): RecordLabels {
279
+ return labels ? { ...DEFAULT_RECORD_LABELS, ...labels } : DEFAULT_RECORD_LABELS;
280
+ }
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { NavigationArrow } from "phosphor-svelte";
6
+ import { cn } from "../../lib/utils.js";
7
+ import { Button } from "../../shadcn/button/index.js";
8
+ import StatusBadge from "../status-badge.svelte";
9
+ import type { Tone } from "../../lib/tone.js";
10
+ import { gutterClass } from "./gutter.js";
11
+ import { formatAge } from "./format.js";
12
+ import { resolveLabels, type RecordLabels } from "./types.js";
13
+
14
+ // =========================
15
+ // PROPS
16
+ // =========================
17
+ interface Props {
18
+ /** The status note. Nothing draws without it. */
19
+ note?: string | null;
20
+ /** ISO timestamp of the note. */
21
+ at?: string | null;
22
+ actor?: string | null;
23
+ health?: { label: string; tone: Tone } | null;
24
+ /** Client mode: the project this note belongs to. */
25
+ project?: { name: string; href?: string | null } | null;
26
+ labels?: Partial<RecordLabels>;
27
+ locale?: string;
28
+ now?: Date | number;
29
+ class?: string;
30
+ }
31
+ let { note, at, actor, health, project, labels, locale, now, class: className }: Props = $props();
32
+
33
+ // =========================
34
+ // STATE
35
+ // =========================
36
+ let expanded = $state(false);
37
+ let clamped = $state(false);
38
+ let text = $state<HTMLElement | null>(null);
39
+
40
+ // =========================
41
+ // DERIVED
42
+ // =========================
43
+ let t = $derived(resolveLabels(labels));
44
+ let age = $derived(at ? formatAge(at, now, locale) : null);
45
+ let hasMeta = $derived(Boolean(health || actor || age));
46
+
47
+ // =========================
48
+ // EFFECTS
49
+ // =========================
50
+ // WHY measure instead of guessing from length: the clamp is two lines, so the
51
+ // same note fits at 1280px and overflows at 390px. "More" shows only when text is cut.
52
+ $effect(() => {
53
+ if (!text) return;
54
+ const el = text;
55
+ const measure = () => {
56
+ if (!expanded) clamped = el.scrollHeight > el.clientHeight + 1;
57
+ };
58
+ measure();
59
+ const observer = new ResizeObserver(measure);
60
+ observer.observe(el);
61
+ return () => observer.disconnect();
62
+ });
63
+ </script>
64
+
65
+ <!-- =========================== -->
66
+ <!-- MARKUP -->
67
+ <!-- =========================== -->
68
+ {#if note}
69
+ <section
70
+ aria-label={t.whereWeAre}
71
+ class={cn("flex items-start gap-2.5 border-b border-border bg-muted/40 py-2.5", gutterClass, className)}
72
+ >
73
+ <NavigationArrow
74
+ class="mt-0.5 size-3.5 shrink-0 text-[color:var(--tone-accent,var(--color-primary))]"
75
+ weight="fill"
76
+ aria-hidden="true"
77
+ />
78
+ <div class="min-w-0 flex-1">
79
+ <!-- No `title` attribute: a native tooltip would repeat the whole note
80
+ over the steps list. "More" opens it in place. -->
81
+ <p
82
+ bind:this={text}
83
+ class={cn("text-sm leading-snug text-muted-foreground", !expanded && "line-clamp-2")}
84
+ >
85
+ <span class="font-medium text-foreground">{t.whereWeAre}</span>
86
+ {#if project}
87
+ {#if project.href}
88
+ <a
89
+ href={project.href}
90
+ class="font-medium text-foreground underline-offset-2 hover:underline"
91
+ >{project.name}</a
92
+ >
93
+ {:else}
94
+ <span class="font-medium text-foreground">{project.name}</span>
95
+ {/if}
96
+ {/if}
97
+ <span class="whitespace-pre-line">{note}</span>
98
+ </p>
99
+ {#if hasMeta}
100
+ <p class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
101
+ {#if health}
102
+ <StatusBadge tone={health.tone} dot capitalize={false}>{health.label}</StatusBadge>
103
+ {/if}
104
+ {#if actor}<span>{t.by(actor)}</span>{/if}
105
+ {#if age}<span class="tabular-nums">{age}</span>{/if}
106
+ </p>
107
+ {/if}
108
+ </div>
109
+ {#if clamped || expanded}
110
+ <Button
111
+ variant="ghost"
112
+ size="sm"
113
+ class="-mr-2 shrink-0 text-muted-foreground"
114
+ aria-expanded={expanded}
115
+ onclick={() => (expanded = !expanded)}
116
+ >
117
+ {expanded ? t.less : t.more}
118
+ </Button>
119
+ {/if}
120
+ </section>
121
+ {/if}
@@ -0,0 +1,19 @@
1
+ type Identifiable = { id: string };
2
+
3
+ /** Move `activeId` to `overId`'s index. Returns a copy; unknown ids pass through. */
4
+ export function reorderByIds<Item extends Identifiable>(
5
+ items: Item[],
6
+ activeId: Item["id"],
7
+ overId: Item["id"],
8
+ ): Item[] {
9
+ const activeIndex = items.findIndex((item) => item.id === activeId);
10
+ const overIndex = items.findIndex((item) => item.id === overId);
11
+
12
+ if (activeIndex === -1 || overIndex === -1 || activeIndex === overIndex) {
13
+ return items.slice();
14
+ }
15
+
16
+ const next = items.slice();
17
+ next.splice(overIndex, 0, ...next.splice(activeIndex, 1));
18
+ return next;
19
+ }
@@ -0,0 +1,16 @@
1
+ // In a .ts file, not the panel's module script, so a plain `tsc` consumer of the
2
+ // barrel can read them. tsc sees a .svelte file as a default export only.
3
+
4
+ /** How wide the panel opens from `sm` (a short form) to `2xl` (a reader). Under
5
+ the `sm` breakpoint every panel is full width. */
6
+ export type SidePanelWidth = "sm" | "md" | "lg" | "xl" | "2xl";
7
+
8
+ /** One number the record is judged on. They sit in the header, in the order given. */
9
+ export type PanelStat = {
10
+ label: string;
11
+ value: string;
12
+ /** A quiet note beside the value: the rate behind a count, or what the figure is. */
13
+ sub?: string;
14
+ /** One severity vocabulary: a count that should be zero is `bad`. */
15
+ tone?: "default" | "muted" | "good" | "bad";
16
+ };
@@ -0,0 +1,193 @@
1
+ <script lang="ts" module>
2
+ import type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
3
+ export type { PanelStat, SidePanelWidth };
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ // =========================
8
+ // IMPORTS
9
+ // =========================
10
+ import type { Snippet } from "svelte";
11
+ import * as Sheet from "../shadcn/sheet/index.js";
12
+ import { OpenIn, type OpenInTool } from "../components/open-in/index.js";
13
+ import { CopyButton } from "../components/copy/index.js";
14
+ import type { Tone } from "../lib/tone.js";
15
+ import { cn } from "../lib/utils.js";
16
+ import StatusBadge from "./status-badge.svelte";
17
+
18
+ // =========================
19
+ // PROPS
20
+ // =========================
21
+ interface Props {
22
+ open: boolean;
23
+ /** Names the dialog for screen readers and heads the panel. */
24
+ title: string;
25
+ /** Merged onto the title. A short path is set `font-mono`, a subject is not. */
26
+ titleClass?: string;
27
+ /** The facts the record is filed under, read out under the title. */
28
+ description?: string;
29
+ /** Status chips, left of the action row. */
30
+ badges?: { label: string; tone: Tone }[];
31
+ /** Doors out to another tool. `text` replaces the tool's name on the button. */
32
+ openIn?: { tool: OpenInTool; href: string; label: string; text?: string }[];
33
+ /** A prompt for an AI about this record, copied on click. */
34
+ handoff?: { value: () => string | Promise<string>; label: string; copiedLabel: string };
35
+ /** The record's numbers, as a strip under the badges. Same place in every panel. */
36
+ stats?: PanelStat[];
37
+ /** Anything else the record can do, drawn last in the action row. */
38
+ actions?: Snippet;
39
+ /** Pinned actions. The body scrolls above it; the caller aligns the row. */
40
+ footer?: Snippet;
41
+ children: Snippet;
42
+ width?: SidePanelWidth;
43
+ /** The body brings its own header: the title is read out but not drawn, and
44
+ the body is neither padded nor scrolled by the panel. */
45
+ bare?: boolean;
46
+ /** Fires once when the panel closes, from the close button, Escape or the overlay. */
47
+ onClose?: () => void;
48
+ };
49
+ let {
50
+ open = $bindable(),
51
+ title,
52
+ titleClass,
53
+ description,
54
+ badges = [],
55
+ openIn = [],
56
+ handoff,
57
+ stats = [],
58
+ actions,
59
+ footer,
60
+ children,
61
+ width = "sm",
62
+ bare = false,
63
+ onClose,
64
+ }: Props = $props();
65
+
66
+ // =========================
67
+ // STATE
68
+ // =========================
69
+ let panel = $state<HTMLElement | null>(null);
70
+
71
+ // =========================
72
+ // DERIVED
73
+ // =========================
74
+ let hasRow = $derived(badges.length > 0 || openIn.length > 0 || Boolean(handoff || actions));
75
+ const STAT_TONE = {
76
+ default: "",
77
+ muted: "text-muted-foreground",
78
+ good: "text-success",
79
+ bad: "text-destructive",
80
+ } as const;
81
+ const WIDTH: Record<SidePanelWidth, string> = {
82
+ sm: "sm:max-w-sm",
83
+ md: "sm:max-w-md",
84
+ lg: "sm:max-w-xl",
85
+ xl: "sm:max-w-2xl",
86
+ "2xl": "sm:max-w-3xl",
87
+ };
88
+
89
+ // =========================
90
+ // HANDLERS
91
+ // =========================
92
+ // WHY focus goes to the panel unless a text field comes first: the dialog otherwise
93
+ // opens on its first control, which draws a ring on a record nobody has touched.
94
+ // A form that opens on its field keeps the default, since that is where you type.
95
+ const FIRST_CONTROL =
96
+ 'a[href], button:not([disabled]), input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])';
97
+ function onOpenAutoFocus(event: Event) {
98
+ if (!panel || panel.querySelector(FIRST_CONTROL)?.matches("input, textarea, select")) return;
99
+ event.preventDefault();
100
+ panel.focus();
101
+ }
102
+ </script>
103
+
104
+ <!-- =========================== -->
105
+ <!-- MARKUP -->
106
+ <!-- =========================== -->
107
+ <Sheet.Root
108
+ bind:open
109
+ onOpenChange={(next) => {
110
+ if (!next) onClose?.();
111
+ }}
112
+ >
113
+ <Sheet.Content
114
+ side="right"
115
+ bind:ref={panel}
116
+ tabindex={-1}
117
+ class={cn("flex w-full flex-col gap-0 p-0", WIDTH[width], bare && "overflow-hidden")}
118
+ {onOpenAutoFocus}
119
+ >
120
+ {#if bare}
121
+ <Sheet.Header class="sr-only">
122
+ <Sheet.Title>{title}</Sheet.Title>
123
+ </Sheet.Header>
124
+ {@render children()}
125
+ {:else}
126
+ <Sheet.Header class="gap-1 border-b border-border">
127
+ <!-- WHY the right padding: the close button sits over this corner and
128
+ a long title runs straight under it. -->
129
+ <Sheet.Title class={cn("pr-9 text-base leading-snug", titleClass)}>{title}</Sheet.Title>
130
+ {#if description}
131
+ <Sheet.Description>{description}</Sheet.Description>
132
+ {/if}
133
+ {#if hasRow}
134
+ <div class="flex items-center justify-between gap-3 pt-1.5">
135
+ <div class="flex min-w-0 flex-wrap items-center gap-2">
136
+ {#each badges as badge (`${badge.tone}:${badge.label}`)}
137
+ <StatusBadge tone={badge.tone}>{badge.label}</StatusBadge>
138
+ {/each}
139
+ </div>
140
+ <div class="flex shrink-0 items-center gap-1">
141
+ {#each openIn as door (door.href)}
142
+ <OpenIn
143
+ tool={door.tool}
144
+ href={door.href}
145
+ title={door.label}
146
+ label={door.text}
147
+ variant="button"
148
+ size="sm"
149
+ />
150
+ {/each}
151
+ {#if handoff}
152
+ <CopyButton
153
+ agent
154
+ value={handoff.value}
155
+ label={handoff.label}
156
+ copiedLabel={handoff.copiedLabel}
157
+ />
158
+ {/if}
159
+ {@render actions?.()}
160
+ </div>
161
+ </div>
162
+ {/if}
163
+ {#if stats.length > 0}
164
+ <dl class="flex flex-wrap gap-x-6 gap-y-3 pt-3">
165
+ {#each stats as stat (stat.label)}
166
+ <div class="min-w-0">
167
+ <dt class="text-xs text-muted-foreground">{stat.label}</dt>
168
+ <dd class={cn("text-sm font-medium tabular-nums", STAT_TONE[stat.tone ?? "default"])}>
169
+ {stat.value}
170
+ {#if stat.sub}
171
+ <span class="text-xs font-normal text-muted-foreground">{stat.sub}</span>
172
+ {/if}
173
+ </dd>
174
+ </div>
175
+ {/each}
176
+ </dl>
177
+ {/if}
178
+ </Sheet.Header>
179
+
180
+ <!-- WHY the body pads and spaces its sections itself: a panel that lets the
181
+ caller choose drifts, one with a grey band and one with a white page. -->
182
+ <div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
183
+ {@render children()}
184
+ </div>
185
+
186
+ {#if footer}
187
+ <Sheet.Footer class="mt-0 flex-row items-center justify-end border-t border-border">
188
+ {@render footer()}
189
+ </Sheet.Footer>
190
+ {/if}
191
+ {/if}
192
+ </Sheet.Content>
193
+ </Sheet.Root>
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import { cn } from "../lib/utils.js";
5
+ import { Badge } from "../shadcn/badge/index.js";
6
+ import { Spinner } from "../shadcn/spinner/index.js";
7
+ import { TONE_COLOR, type Tone } from "../lib/tone.js";
8
+ import type { Statuses } from "./status-types.js";
9
+
10
+ interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "class" | "children"> {
11
+ /** A registry from `defineStatuses`. Supplies the label and tone. */
12
+ statuses?: Statuses;
13
+ /** The status id to look up in `statuses`. Leave it out when you pass `children`. */
14
+ status?: string;
15
+ /** Override the registry label. */
16
+ label?: string;
17
+ /** Override the registry tone. */
18
+ tone?: Tone;
19
+ /** Shows a dot in the tone's colour before the label. */
20
+ dot?: boolean;
21
+ /** Shows a spinner: a background job is still working on the record. */
22
+ busy?: boolean;
23
+ /** Capitalizes the first letter of each word. Turn off for a name or a number. */
24
+ capitalize?: boolean;
25
+ /** The chip's text, for a caller that already has the word. Wins over `label` and the registry. */
26
+ children?: Snippet;
27
+ class?: string;
28
+ }
29
+
30
+ let {
31
+ statuses,
32
+ status,
33
+ label,
34
+ tone,
35
+ dot = false,
36
+ busy = false,
37
+ capitalize = true,
38
+ children,
39
+ class: className,
40
+ ...restProps
41
+ }: Props = $props();
42
+
43
+ let resolvedTone = $derived(tone ?? (statuses && status ? statuses.tone(status) : "neutral"));
44
+ let resolvedLabel = $derived(label ?? (statuses && status ? statuses.label(status) : (status ?? "")));
45
+ </script>
46
+
47
+ <Badge tone={resolvedTone} class={cn(capitalize && "capitalize", className)} {...restProps}>
48
+ {#if dot}
49
+ <span
50
+ class="size-1.5 shrink-0 rounded-full"
51
+ style:background-color={TONE_COLOR[resolvedTone]}
52
+ aria-hidden="true"
53
+ ></span>
54
+ {/if}
55
+ {#if busy}
56
+ <Spinner class="size-3" aria-hidden="true" />
57
+ {/if}
58
+ {#if children}
59
+ {@render children()}
60
+ {:else}
61
+ {resolvedLabel}
62
+ {/if}
63
+ </Badge>