@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
@@ -1,28 +1,33 @@
1
1
  // Turns a module's field declarations and a record's values into rows to draw.
2
2
  import { formatDay, formatMoney } from "./format.js";
3
3
  import type {
4
- FieldDef,
5
- FieldOption,
6
- FieldSectionDef,
7
- FieldValue,
8
- InfoRow,
4
+ FieldDef,
5
+ FieldOption,
6
+ FieldSectionDef,
7
+ FieldValue,
8
+ InfoRow,
9
9
  } from "./types.js";
10
10
 
11
+ /** A choice or a multi: the types that carry a list of options. */
12
+ function hasOptions(field: FieldDef): field is Extract<FieldDef, { type: "choice" | "multi" }> {
13
+ return field.type === "choice" || field.type === "multi";
14
+ }
15
+
11
16
  export type FieldValues = Record<string, FieldValue | undefined>;
12
17
 
13
18
  function isEmpty(value: FieldValue | undefined): boolean {
14
- return value == null || value === "" || (Array.isArray(value) && value.length === 0);
19
+ return value == null || value === "" || (Array.isArray(value) && value.length === 0);
15
20
  }
16
21
 
17
22
  /** A field shows unless its `showWhen` names a field that doesn't hold the wanted value. */
18
23
  export function isFieldVisible(field: FieldDef, values: FieldValues): boolean {
19
- const rule = field.showWhen;
20
- if (!rule) return true;
21
- const current = values[rule.key];
22
- if (rule.equals === undefined) return !isEmpty(current);
23
- const wanted = Array.isArray(rule.equals) ? rule.equals : [rule.equals];
24
- const held = Array.isArray(current) ? current : current == null ? [] : [String(current)];
25
- return held.some((v) => wanted.includes(v));
24
+ const rule = field.showWhen;
25
+ if (!rule) return true;
26
+ const current = values[rule.key];
27
+ if (rule.equals === undefined) return !isEmpty(current);
28
+ const wanted = Array.isArray(rule.equals) ? rule.equals : [rule.equals];
29
+ const held = Array.isArray(current) ? current : current == null ? [] : [String(current)];
30
+ return held.some((v) => wanted.includes(v));
26
31
  }
27
32
 
28
33
  /**
@@ -30,8 +35,10 @@ export function isFieldVisible(field: FieldDef, values: FieldValues): boolean {
30
35
  * record already holds, so an old value still reads and is never chosen again.
31
36
  */
32
37
  export function pickableOptions(field: FieldDef, value: FieldValue | undefined): FieldOption[] {
33
- const held = Array.isArray(value) ? value : value == null ? [] : [String(value)];
34
- return (field.options ?? []).filter((o) => !o.archived || held.includes(o.key));
38
+ const held = Array.isArray(value) ? value : value == null ? [] : [String(value)];
39
+ return (hasOptions(field) ? field.options : []).filter(
40
+ (o) => !o.archived || held.includes(o.key),
41
+ );
35
42
  }
36
43
 
37
44
  export type FieldGroup = { key: string; label: string | null; fields: FieldDef[] };
@@ -41,102 +48,147 @@ export type FieldGroup = { key: string; label: string | null; fields: FieldDef[]
41
48
  * field is not drawn. Fields with no or an unknown section go last under no heading.
42
49
  */
43
50
  export function groupFields(
44
- fields: readonly FieldDef[],
45
- sections: readonly FieldSectionDef[],
46
- values: FieldValues,
51
+ fields: readonly FieldDef[],
52
+ sections: readonly FieldSectionDef[],
53
+ values: FieldValues,
47
54
  ): FieldGroup[] {
48
- const visible = fields.filter((f) => isFieldVisible(f, values));
49
- const known = new Set(sections.map((s) => s.key));
50
- const groups: FieldGroup[] = sections
51
- .map((s) => ({ key: s.key, label: s.label as string | null, fields: visible.filter((f) => f.section === s.key) }))
52
- .filter((g) => g.fields.length > 0);
53
- const loose = visible.filter((f) => !f.section || !known.has(f.section));
54
- if (loose.length > 0) groups.push({ key: "", label: null, fields: loose });
55
- return groups;
55
+ const visible = fields.filter((f) => isFieldVisible(f, values));
56
+ const known = new Set(sections.map((s) => s.key));
57
+ const groups: FieldGroup[] = sections
58
+ .map((s) => ({
59
+ key: s.key,
60
+ label: s.label as string | null,
61
+ fields: visible.filter((f) => f.section === s.key),
62
+ }))
63
+ .filter((g) => g.fields.length > 0);
64
+ const loose = visible.filter((f) => !f.section || !known.has(f.section));
65
+ if (loose.length > 0) groups.push({ key: "", label: null, fields: loose });
66
+ return groups;
56
67
  }
57
68
 
58
69
  /** What the editor hands back, as the type the field declares. Empty text is null. */
59
70
  export function parseSaved(field: FieldDef, raw: string | string[]): FieldValue {
60
- if (Array.isArray(raw)) return raw;
61
- const text = raw.trim();
62
- if (text === "") return null;
63
- if (field.type === "money") {
64
- const n = Number(text.replace(",", "."));
65
- return Number.isFinite(n) ? n : null;
66
- }
67
- return text;
71
+ if (Array.isArray(raw)) return raw;
72
+ const text = raw.trim();
73
+ if (text === "") return null;
74
+ if (field.type === "money") {
75
+ const n = Number(text.replace(",", "."));
76
+ return Number.isFinite(n) ? n : null;
77
+ }
78
+ return text;
68
79
  }
69
80
 
70
81
  export type RowContext = {
71
- locale?: string;
72
- /** The record's currency, for a money field with none of its own. */
73
- currency?: string;
74
- onSave?: (key: string, value: FieldValue) => void | Promise<void>;
82
+ locale?: string;
83
+ /** The record's currency, for a money field with none of its own. */
84
+ currency?: string;
85
+ /** The labels of an `"existing-values"` tags field, by field key: every value used across the records. */
86
+ tagOptions?: Readonly<Record<string, readonly string[]>>;
87
+ onSave?: (key: string, value: FieldValue) => void | Promise<void>;
75
88
  };
76
89
 
77
90
  const INPUT: Record<FieldDef["type"], NonNullable<InfoRow["input"]>> = {
78
- text: "text",
79
- longtext: "longtext",
80
- choice: "select",
81
- multi: "multi",
82
- money: "number",
83
- date: "date",
84
- url: "url",
85
- email: "email",
86
- timezone: "text",
91
+ text: "text",
92
+ longtext: "longtext",
93
+ choice: "select",
94
+ multi: "multi",
95
+ money: "number",
96
+ date: "date",
97
+ url: "url",
98
+ email: "email",
99
+ timezone: "text",
100
+ tags: "tags",
87
101
  };
88
102
 
89
103
  /** One field and its value as an `InfoRow`. */
90
- export function fieldRow(field: FieldDef, value: FieldValue | undefined, ctx: RowContext = {}): InfoRow {
91
- const row: InfoRow = {
92
- id: field.key,
93
- label: field.label,
94
- input: INPUT[field.type],
95
- placeholder: field.placeholder,
96
- required: field.required,
97
- };
98
- const optionOf = (key: string) => field.options?.find((o) => o.key === key);
104
+ export function fieldRow(
105
+ field: FieldDef,
106
+ value: FieldValue | undefined,
107
+ ctx: RowContext = {},
108
+ ): InfoRow {
109
+ const row: InfoRow = {
110
+ id: field.key,
111
+ label: field.label,
112
+ input: INPUT[field.type],
113
+ placeholder: field.placeholder,
114
+ required: field.required,
115
+ };
116
+ const optionOf = (key: string) =>
117
+ hasOptions(field) ? field.options.find((o) => o.key === key) : undefined;
118
+
119
+ if (!isEmpty(value)) {
120
+ switch (field.type) {
121
+ case "choice": {
122
+ const option = optionOf(String(value));
123
+ row.value = option?.label ?? String(value);
124
+ row.tone = option?.tone;
125
+ break;
126
+ }
127
+ case "multi": {
128
+ const keys = Array.isArray(value) ? value : [String(value)];
129
+ row.chips = keys.map((k) => ({ label: optionOf(k)?.label ?? k, tone: optionOf(k)?.tone }));
130
+ break;
131
+ }
132
+ case "tags": {
133
+ const labels = Array.isArray(value) ? value : [String(value)];
134
+ row.chips = labels.map((label) => ({ label }));
135
+ break;
136
+ }
137
+ case "money":
138
+ row.value = formatMoney(Number(value), field.currency ?? ctx.currency, ctx.locale);
139
+ break;
140
+ case "date":
141
+ row.value = formatDay(String(value), ctx.locale);
142
+ break;
143
+ case "url":
144
+ row.value = String(value)
145
+ .replace(/^https?:\/\/(www\.)?/, "")
146
+ .replace(/\/$/, "");
147
+ row.href = String(value);
148
+ break;
149
+ case "email":
150
+ row.value = String(value);
151
+ row.href = `mailto:${value}`;
152
+ break;
153
+ case "text":
154
+ case "longtext":
155
+ case "timezone":
156
+ row.value = String(value);
157
+ break;
158
+ default:
159
+ return field satisfies never;
160
+ }
161
+ }
99
162
 
100
- if (!isEmpty(value)) {
101
- switch (field.type) {
102
- case "choice": {
103
- const option = optionOf(String(value));
104
- row.value = option?.label ?? String(value);
105
- row.tone = option?.tone;
106
- break;
107
- }
108
- case "multi": {
109
- const keys = Array.isArray(value) ? value : [String(value)];
110
- row.chips = keys.map((k) => ({ label: optionOf(k)?.label ?? k, tone: optionOf(k)?.tone }));
111
- break;
112
- }
113
- case "money":
114
- row.value = formatMoney(Number(value), field.currency ?? ctx.currency, ctx.locale);
115
- break;
116
- case "date":
117
- row.value = formatDay(String(value), ctx.locale);
118
- break;
119
- case "url":
120
- row.value = String(value).replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
121
- row.href = String(value);
122
- break;
123
- case "email":
124
- row.value = String(value);
125
- row.href = `mailto:${value}`;
126
- break;
127
- default:
128
- row.value = String(value);
129
- }
130
- }
163
+ if (field.type === "choice" || field.type === "multi") {
164
+ row.options = pickableOptions(field, value).map((o) => ({
165
+ value: o.key,
166
+ label: o.label,
167
+ tone: o.tone,
168
+ }));
169
+ row.editValue = Array.isArray(value)
170
+ ? value
171
+ : value == null
172
+ ? field.type === "multi"
173
+ ? []
174
+ : ""
175
+ : String(value);
176
+ } else if (!isEmpty(value)) {
177
+ row.editValue = String(value);
178
+ }
131
179
 
132
- if (field.type === "choice" || field.type === "multi") {
133
- row.options = pickableOptions(field, value).map((o) => ({ value: o.key, label: o.label, tone: o.tone }));
134
- row.editValue = Array.isArray(value) ? value : value == null ? (field.type === "multi" ? [] : "") : String(value);
135
- } else if (!isEmpty(value)) {
136
- row.editValue = String(value);
137
- }
180
+ if (field.type === "tags") {
181
+ row.tags = {
182
+ // "existing-values" is the host's to fill: only it can see the other records.
183
+ options:
184
+ ctx.tagOptions?.[field.key] ?? (field.options === "existing-values" ? [] : field.options),
185
+ allowCreate: field.allowCreate,
186
+ max: field.max,
187
+ };
188
+ row.editValue = Array.isArray(value) ? value : [];
189
+ }
138
190
 
139
- const onSave = ctx.onSave;
140
- if (onSave) row.onSave = (raw) => onSave(field.key, parseSaved(field, raw));
141
- return row;
191
+ const onSave = ctx.onSave;
192
+ if (onSave) row.onSave = (raw) => onSave(field.key, parseSaved(field, raw));
193
+ return row;
142
194
  }
@@ -4,84 +4,91 @@
4
4
  const DAY_ONLY = /^\d{4}-\d{2}-\d{2}$/;
5
5
 
6
6
  function parse(iso: string): { date: Date; timeZone?: string } {
7
- return DAY_ONLY.test(iso)
8
- ? { date: new Date(`${iso}T00:00:00Z`), timeZone: "UTC" }
9
- : { date: new Date(iso) };
7
+ return DAY_ONLY.test(iso)
8
+ ? { date: new Date(`${iso}T00:00:00Z`), timeZone: "UTC" }
9
+ : { date: new Date(iso) };
10
10
  }
11
11
 
12
12
  /** "Mon 26 Oct". */
13
13
  export function formatDay(iso: string, locale = "en-GB"): string {
14
- const { date, timeZone } = parse(iso);
15
- return new Intl.DateTimeFormat(locale, {
16
- weekday: "short",
17
- day: "numeric",
18
- month: "short",
19
- timeZone,
20
- }).format(date);
14
+ const { date, timeZone } = parse(iso);
15
+ return new Intl.DateTimeFormat(locale, {
16
+ weekday: "short",
17
+ day: "numeric",
18
+ month: "short",
19
+ timeZone,
20
+ }).format(date);
21
21
  }
22
22
 
23
23
  /** "Sat 24 to Sun 25 Oct" in one month, two full days across months, one day when the ends match. */
24
- export function formatRange(start: string, end: string | null | undefined, locale = "en-GB"): string {
25
- if (!end || end.slice(0, 10) === start.slice(0, 10)) return formatDay(start, locale);
26
- const a = parse(start);
27
- const b = parse(end);
28
- const sameMonth =
29
- a.date.getUTCFullYear() === b.date.getUTCFullYear() && a.date.getUTCMonth() === b.date.getUTCMonth();
30
- if (!sameMonth) return `${formatDay(start, locale)} to ${formatDay(end, locale)}`;
31
- const part = (d: Date) =>
32
- new Intl.DateTimeFormat(locale, { weekday: "short", day: "numeric", timeZone: "UTC" }).format(d);
33
- const month = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" }).format(b.date);
34
- return `${part(a.date)} to ${part(b.date)} ${month}`;
24
+ export function formatRange(
25
+ start: string,
26
+ end: string | null | undefined,
27
+ locale = "en-GB",
28
+ ): string {
29
+ if (!end || end.slice(0, 10) === start.slice(0, 10)) return formatDay(start, locale);
30
+ const a = parse(start);
31
+ const b = parse(end);
32
+ const sameMonth =
33
+ a.date.getUTCFullYear() === b.date.getUTCFullYear() &&
34
+ a.date.getUTCMonth() === b.date.getUTCMonth();
35
+ if (!sameMonth) return `${formatDay(start, locale)} to ${formatDay(end, locale)}`;
36
+ const part = (d: Date) =>
37
+ new Intl.DateTimeFormat(locale, { weekday: "short", day: "numeric", timeZone: "UTC" }).format(
38
+ d,
39
+ );
40
+ const month = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" }).format(b.date);
41
+ return `${part(a.date)} to ${part(b.date)} ${month}`;
35
42
  }
36
43
 
37
44
  /** "Oct 21" without a weekday, for a timeline day header. */
38
45
  export function formatDayHeader(day: string, locale = "en-GB"): string {
39
- return new Intl.DateTimeFormat(locale, {
40
- weekday: "short",
41
- day: "numeric",
42
- month: "short",
43
- year: "numeric",
44
- timeZone: "UTC",
45
- }).format(new Date(`${day}T00:00:00Z`));
46
+ return new Intl.DateTimeFormat(locale, {
47
+ weekday: "short",
48
+ day: "numeric",
49
+ month: "short",
50
+ year: "numeric",
51
+ timeZone: "UTC",
52
+ }).format(new Date(`${day}T00:00:00Z`));
46
53
  }
47
54
 
48
55
  /** "14:30" in the viewer's zone, or the given one. */
49
56
  export function formatTime(iso: string, locale = "en-GB", timeZone?: string): string {
50
- return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone }).format(
51
- new Date(iso),
52
- );
57
+ return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone }).format(
58
+ new Date(iso),
59
+ );
53
60
  }
54
61
 
55
62
  /** "3 days ago", "in 2 weeks", "now". */
56
63
  export function formatAge(iso: string, now: Date | number = Date.now(), locale = "en-GB"): string {
57
- const seconds = Math.round((new Date(iso).getTime() - new Date(now).getTime()) / 1000);
58
- const abs = Math.abs(seconds);
59
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });
60
- const units: [number, Intl.RelativeTimeFormatUnit][] = [
61
- [31_536_000, "year"],
62
- [2_592_000, "month"],
63
- [604_800, "week"],
64
- [86_400, "day"],
65
- [3_600, "hour"],
66
- [60, "minute"],
67
- ];
68
- for (const [size, unit] of units) {
69
- if (abs >= size) return rtf.format(Math.round(seconds / size), unit);
70
- }
71
- return rtf.format(0, "second");
64
+ const seconds = Math.round((new Date(iso).getTime() - new Date(now).getTime()) / 1000);
65
+ const abs = Math.abs(seconds);
66
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });
67
+ const units: [number, Intl.RelativeTimeFormatUnit][] = [
68
+ [31_536_000, "year"],
69
+ [2_592_000, "month"],
70
+ [604_800, "week"],
71
+ [86_400, "day"],
72
+ [3_600, "hour"],
73
+ [60, "minute"],
74
+ ];
75
+ for (const [size, unit] of units) {
76
+ if (abs >= size) return rtf.format(Math.round(seconds / size), unit);
77
+ }
78
+ return rtf.format(0, "second");
72
79
  }
73
80
 
74
81
  /** "€12,000". `compact` gives "€12K". */
75
82
  export function formatMoney(
76
- amount: number,
77
- currency = "EUR",
78
- locale = "en-GB",
79
- compact = false,
83
+ amount: number,
84
+ currency = "EUR",
85
+ locale = "en-GB",
86
+ compact = false,
80
87
  ): string {
81
- return new Intl.NumberFormat(locale, {
82
- style: "currency",
83
- currency,
84
- maximumFractionDigits: compact || Number.isInteger(amount) ? 0 : 2,
85
- notation: compact ? "compact" : "standard",
86
- }).format(amount);
88
+ return new Intl.NumberFormat(locale, {
89
+ style: "currency",
90
+ currency,
91
+ maximumFractionDigits: compact || Number.isInteger(amount) ? 0 : 2,
92
+ notation: compact ? "compact" : "standard",
93
+ }).format(amount);
87
94
  }
@@ -12,43 +12,57 @@ export { default as InfoList } from "./info-list.svelte";
12
12
  export { DEFAULT_RECORD_FILTERS, DEFAULT_RECORD_KINDS } from "./kinds.js";
13
13
  export { DEFAULT_RECORD_LABELS, resolveLabels } from "./types.js";
14
14
  export {
15
- activeProjects,
16
- dayRelation,
17
- eventsForFilter,
18
- filterCounts,
19
- foldGroups,
20
- groupEventsByDay,
21
- groupSteps,
22
- latestWhereWeAre,
23
- localDay,
24
- mergeClientEvents,
25
- nextAcrossProjects,
26
- nudgeStep,
27
- reorderIds,
28
- rollupProjects,
29
- stepMarkers,
30
- stepProgress,
15
+ activeProjects,
16
+ dayRelation,
17
+ eventsForFilter,
18
+ filterCounts,
19
+ foldGroups,
20
+ groupEventsByDay,
21
+ groupSteps,
22
+ latestWhereWeAre,
23
+ localDay,
24
+ mergeClientEvents,
25
+ nextAcrossProjects,
26
+ nudgeStep,
27
+ reorderIds,
28
+ rollupProjects,
29
+ stepMarkers,
30
+ stepProgress,
31
31
  } from "./logic.js";
32
- export { fieldRow, groupFields, isFieldVisible, parseSaved, pickableOptions } from "./fields.js";
32
+ export {
33
+ fieldRow,
34
+ groupFields,
35
+ isFieldVisible,
36
+ parseSaved,
37
+ pickableOptions,
38
+ } from "./fields.js";
33
39
  export { formatAge, formatDay, formatMoney, formatRange } from "./format.js";
34
- export type { ClientRollup, DayGroup, LatestStatus, MoneyTotals, ProjectEvents } from "./logic.js";
40
+ export type {
41
+ ClientRollup,
42
+ DayGroup,
43
+ LatestStatus,
44
+ MoneyTotals,
45
+ ProjectEvents,
46
+ } from "./logic.js";
35
47
  export type { FieldValues } from "./fields.js";
36
48
  export type {
37
- FieldDef,
38
- FieldOption,
39
- FieldSectionDef,
40
- FieldType,
41
- FieldValue,
42
- InfoRow,
43
- RecordEvent,
44
- RecordFilterDef,
45
- RecordKindDef,
46
- RecordLabels,
47
- RecordNext,
48
- RecordSummary,
49
- StageCategory,
50
- StageDef,
51
- StepDef,
52
- StepGroupDef,
53
- StepMarker,
49
+ FieldDef,
50
+ FieldOption,
51
+ FieldSectionDef,
52
+ TagsConfig,
53
+ FieldType,
54
+ FieldTypes,
55
+ FieldValue,
56
+ InfoRow,
57
+ RecordEvent,
58
+ RecordFilterDef,
59
+ RecordKindDef,
60
+ RecordLabels,
61
+ RecordNext,
62
+ RecordSummary,
63
+ StageCategory,
64
+ StageDef,
65
+ StepDef,
66
+ StepGroupDef,
67
+ StepMarker,
54
68
  } from "./types.js";