@joewinke/jatui 0.37.0 → 0.38.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 (266) hide show
  1. package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
  2. package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
  3. package/dist/artifact-blocks/BlockComments.svelte +309 -0
  4. package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
  5. package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
  6. package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
  7. package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
  8. package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
  9. package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
  10. package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
  11. package/dist/artifact-blocks/CONTRACT.md +225 -0
  12. package/dist/artifact-blocks/CommentPill.svelte +49 -0
  13. package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
  14. package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
  15. package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
  16. package/dist/artifact-blocks/Glyph.svelte +54 -0
  17. package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
  18. package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
  19. package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
  20. package/dist/artifact-blocks/Markdown.svelte +186 -0
  21. package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
  22. package/dist/artifact-blocks/NoteComposer.svelte +31 -0
  23. package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
  24. package/dist/artifact-blocks/PlainComposer.svelte +200 -0
  25. package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
  26. package/dist/artifact-blocks/SectionTools.svelte +102 -0
  27. package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
  28. package/dist/artifact-blocks/adapter.d.ts +13 -0
  29. package/dist/artifact-blocks/adapter.js +31 -0
  30. package/dist/artifact-blocks/autoSend.d.ts +49 -0
  31. package/dist/artifact-blocks/autoSend.js +103 -0
  32. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
  33. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
  34. package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
  35. package/dist/artifact-blocks/blocks/actions/block.js +74 -0
  36. package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
  37. package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
  38. package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
  39. package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
  40. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
  41. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
  42. package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
  43. package/dist/artifact-blocks/blocks/callout/block.js +39 -0
  44. package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
  45. package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
  46. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
  47. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
  48. package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
  49. package/dist/artifact-blocks/blocks/chart/block.js +70 -0
  50. package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
  51. package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
  52. package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
  53. package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
  54. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
  55. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
  56. package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
  57. package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
  58. package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
  59. package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
  60. package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
  61. package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
  62. package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
  63. package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
  64. package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
  65. package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
  66. package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
  67. package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
  68. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
  69. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
  70. package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
  71. package/dist/artifact-blocks/blocks/code/block.js +31 -0
  72. package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
  73. package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
  74. package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
  75. package/dist/artifact-blocks/blocks/code/schema.js +39 -0
  76. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
  77. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
  78. package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
  79. package/dist/artifact-blocks/blocks/collage/block.js +29 -0
  80. package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
  81. package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
  82. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
  83. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
  84. package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
  85. package/dist/artifact-blocks/blocks/compare/block.js +32 -0
  86. package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
  87. package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
  88. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
  89. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
  90. package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
  91. package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
  92. package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
  93. package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
  94. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
  95. package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
  96. package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
  97. package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
  98. package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
  99. package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
  100. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
  101. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
  102. package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
  103. package/dist/artifact-blocks/blocks/diff/block.js +36 -0
  104. package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
  105. package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
  106. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
  107. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
  108. package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
  109. package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
  110. package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
  111. package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
  112. package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
  113. package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
  114. package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
  115. package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
  116. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
  117. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
  118. package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
  119. package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
  120. package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
  121. package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
  122. package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
  123. package/dist/artifact-blocks/blocks/group-content.js +16 -0
  124. package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
  125. package/dist/artifact-blocks/blocks/group-data.js +7 -0
  126. package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
  127. package/dist/artifact-blocks/blocks/group-review.js +12 -0
  128. package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
  129. package/dist/artifact-blocks/blocks/group-visual.js +14 -0
  130. package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
  131. package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
  132. package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
  133. package/dist/artifact-blocks/blocks/map/block.js +62 -0
  134. package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
  135. package/dist/artifact-blocks/blocks/map/schema.js +113 -0
  136. package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
  137. package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
  138. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
  139. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
  140. package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
  141. package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
  142. package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
  143. package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
  144. package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
  145. package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
  146. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
  147. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
  148. package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
  149. package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
  150. package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
  151. package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
  152. package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
  153. package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
  154. package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
  155. package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
  156. package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
  157. package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
  158. package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
  159. package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
  160. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
  161. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
  162. package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
  163. package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
  164. package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
  165. package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
  166. package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
  167. package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
  168. package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
  169. package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
  170. package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
  171. package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
  172. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
  173. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
  174. package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
  175. package/dist/artifact-blocks/blocks/program/block.js +68 -0
  176. package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
  177. package/dist/artifact-blocks/blocks/program/schema.js +268 -0
  178. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
  179. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
  180. package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
  181. package/dist/artifact-blocks/blocks/stats/block.js +49 -0
  182. package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
  183. package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
  184. package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
  185. package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
  186. package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
  187. package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
  188. package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
  189. package/dist/artifact-blocks/blocks/table/block.js +100 -0
  190. package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
  191. package/dist/artifact-blocks/blocks/table/schema.js +278 -0
  192. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
  193. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
  194. package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
  195. package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
  196. package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
  197. package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
  198. package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
  199. package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
  200. package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
  201. package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
  202. package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
  203. package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
  204. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
  205. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
  206. package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
  207. package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
  208. package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
  209. package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
  210. package/dist/artifact-blocks/builtins.d.ts +40 -0
  211. package/dist/artifact-blocks/builtins.js +22 -0
  212. package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
  213. package/dist/artifact-blocks/contractBlocks.js +279 -0
  214. package/dist/artifact-blocks/history.d.ts +140 -0
  215. package/dist/artifact-blocks/history.js +363 -0
  216. package/dist/artifact-blocks/index.d.ts +40 -0
  217. package/dist/artifact-blocks/index.js +46 -0
  218. package/dist/artifact-blocks/listen/context.d.ts +30 -0
  219. package/dist/artifact-blocks/listen/context.js +30 -0
  220. package/dist/artifact-blocks/liveData.d.ts +78 -0
  221. package/dist/artifact-blocks/liveData.js +137 -0
  222. package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
  223. package/dist/artifact-blocks/liveDataPoller.js +140 -0
  224. package/dist/artifact-blocks/openNav.d.ts +46 -0
  225. package/dist/artifact-blocks/openNav.js +106 -0
  226. package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
  227. package/dist/artifact-blocks/pageReviewKeys.js +115 -0
  228. package/dist/artifact-blocks/params.d.ts +98 -0
  229. package/dist/artifact-blocks/params.js +159 -0
  230. package/dist/artifact-blocks/registry.d.ts +27 -0
  231. package/dist/artifact-blocks/registry.js +83 -0
  232. package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
  233. package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
  234. package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
  235. package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
  236. package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
  237. package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
  238. package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
  239. package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
  240. package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
  241. package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
  242. package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
  243. package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
  244. package/dist/artifact-blocks/review/attachments.d.ts +10 -0
  245. package/dist/artifact-blocks/review/attachments.js +37 -0
  246. package/dist/artifact-blocks/review/bulk.d.ts +52 -0
  247. package/dist/artifact-blocks/review/bulk.js +61 -0
  248. package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
  249. package/dist/artifact-blocks/review/itemReview.js +112 -0
  250. package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
  251. package/dist/artifact-blocks/review/roundLines.js +20 -0
  252. package/dist/artifact-blocks/review/rounds.d.ts +86 -0
  253. package/dist/artifact-blocks/review/rounds.js +302 -0
  254. package/dist/artifact-blocks/review/saver.d.ts +23 -0
  255. package/dist/artifact-blocks/review/saver.js +48 -0
  256. package/dist/artifact-blocks/review/team.d.ts +31 -0
  257. package/dist/artifact-blocks/review/team.js +42 -0
  258. package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
  259. package/dist/artifact-blocks/review/visualOrder.js +66 -0
  260. package/dist/artifact-blocks/review.d.ts +180 -0
  261. package/dist/artifact-blocks/review.js +716 -0
  262. package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
  263. package/dist/artifact-blocks/thumb-kit.js +149 -0
  264. package/dist/artifact-blocks/types.d.ts +513 -0
  265. package/dist/artifact-blocks/types.js +1 -0
  266. package/package.json +11 -2
@@ -0,0 +1,315 @@
1
+ <script lang="ts">
2
+ /**
3
+ * stats block — 1-8 KPIs as a typographic scorecard row (campaign-blocks
4
+ * Gate A, J5): no tiles, no accent stripe, no tinted delta pill. Each
5
+ * metric is a column under a hairline rule — label, value (+unit), delta
6
+ * in words, target bar, sparkline.
7
+ *
8
+ * Columns come from a CONTAINER query on the block, not the viewport: the
9
+ * block lives in a ~720px page column, so a viewport `xl:grid-cols-4`
10
+ * produced 171px tiles and truncated every label to one letter. Labels
11
+ * wrap; they never truncate.
12
+ *
13
+ * Tone resolution: the explicit `status` token wins; without one the tone
14
+ * is derived from `delta` vs its `goodDirection` (a drop can be good —
15
+ * e.g. error rate), else neutral. `warn` is explicit-only. Tone is carried
16
+ * by a status dot + glyph (graphics, ≥3:1) and spelled out for screen
17
+ * readers; the delta TEXT stays base-content/75 so it clears 4.5:1 in
18
+ * every theme (text-success on a soft tint measured ~1.8:1).
19
+ */
20
+ import ProgressBar from "../../../components/progress/ProgressBar.svelte"
21
+ import Sparkline from "../../../components/Sparkline.svelte"
22
+ import type { BlockProps } from "../../types.js"
23
+ import {
24
+ type StatCard,
25
+ type StatDelta,
26
+ type StatStatus,
27
+ type StatsParams,
28
+ } from "./schema.js"
29
+ import SectionTools from "../../SectionTools.svelte"
30
+
31
+ let { params, ctx }: BlockProps<StatsParams> = $props()
32
+
33
+ const cards = $derived(params?.cards ?? [])
34
+
35
+ /** Column count follows the card count, capped at 4 (container widths). */
36
+ const gridCols = $derived(
37
+ cards.length <= 1
38
+ ? ""
39
+ : cards.length === 2
40
+ ? "@sm:grid-cols-2"
41
+ : cards.length === 3
42
+ ? "@sm:grid-cols-2 @2xl:grid-cols-3"
43
+ : "@sm:grid-cols-2 @2xl:grid-cols-4",
44
+ )
45
+
46
+ // ---- tone tokens (CSS vars / semantic classes only) ----------------------
47
+ const TONE_VAR: Record<StatStatus, string> = {
48
+ good: "var(--color-success)",
49
+ warn: "var(--color-warning)",
50
+ bad: "var(--color-error)",
51
+ neutral: "var(--color-primary)",
52
+ }
53
+ const TONE_DOT: Record<StatStatus, string> = {
54
+ good: "bg-success",
55
+ warn: "bg-warning",
56
+ bad: "bg-error",
57
+ neutral: "",
58
+ }
59
+ const TONE_WORD: Record<StatStatus, string> = {
60
+ good: "on track",
61
+ warn: "watch",
62
+ bad: "off track",
63
+ neutral: "",
64
+ }
65
+ const DELTA_ICON_TONE: Record<"good" | "bad" | "neutral", string> = {
66
+ good: "text-success",
67
+ bad: "text-error",
68
+ neutral: "text-base-content/50",
69
+ }
70
+ const TONE_PROGRESS: Record<
71
+ StatStatus,
72
+ "primary" | "success" | "warning" | "error"
73
+ > = {
74
+ good: "success",
75
+ warn: "warning",
76
+ bad: "error",
77
+ neutral: "primary",
78
+ }
79
+
80
+ function toneOf(card: StatCard): StatStatus {
81
+ if (card.status) return card.status
82
+ if (!card.delta || card.delta.value === 0) return "neutral"
83
+ const good = card.delta.goodDirection ?? "up"
84
+ const positive = card.delta.value > 0
85
+ return (good === "up") === positive ? "good" : "bad"
86
+ }
87
+
88
+ function deltaTone(d: StatDelta): "good" | "bad" | "neutral" {
89
+ if (d.value === 0) return "neutral"
90
+ const good = d.goodDirection ?? "up"
91
+ return (good === "up") === d.value > 0 ? "good" : "bad"
92
+ }
93
+
94
+ // ---- formatting ----------------------------------------------------------
95
+ function fmt(n: number, decimals?: number): string {
96
+ if (!Number.isFinite(n)) return "n/a"
97
+ const nf = new Intl.NumberFormat(
98
+ "en-US",
99
+ decimals === undefined
100
+ ? { maximumFractionDigits: 2 }
101
+ : { minimumFractionDigits: decimals, maximumFractionDigits: decimals },
102
+ )
103
+ return nf.format(n)
104
+ }
105
+
106
+ function deltaText(d: StatDelta, decimals?: number): string {
107
+ const sign = d.value > 0 ? "+" : d.value < 0 ? "−" : ""
108
+ return `${sign}${fmt(Math.abs(d.value), decimals)}`
109
+ }
110
+
111
+ function deltaSr(d: StatDelta): string {
112
+ const t = deltaTone(d)
113
+ const dir = d.value > 0 ? "up" : d.value < 0 ? "down" : "unchanged"
114
+ return t === "neutral"
115
+ ? dir
116
+ : `${dir}, ${t === "good" ? "better" : "worse"}`
117
+ }
118
+
119
+ function targetPct(card: StatCard): number {
120
+ if (!card.target || card.target.value <= 0) return 0
121
+ return Math.min(100, Math.max(0, (card.value / card.target.value) * 100))
122
+ }
123
+
124
+ function targetCaption(card: StatCard): string {
125
+ if (!card.target) return ""
126
+ const unit = card.unit ?? ""
127
+ const num = `${fmt(card.target.value, card.decimals)}${unit}`
128
+ return card.target.label ? `${card.target.label} (${num})` : num
129
+ }
130
+
131
+ function sparkSr(values: number[]): string {
132
+ const first = values[0]
133
+ const last = values[values.length - 1]
134
+ const trend = last > first ? "rising" : last < first ? "falling" : "flat"
135
+ return `Trend over ${values.length} points: ${fmt(first)} to ${fmt(last)}, ${trend}.`
136
+ }
137
+
138
+ const browser = typeof window !== "undefined"
139
+
140
+ // ---- sparkline -----------------------------------------------------------
141
+ // The jatui Sparkline animates its entrance; under prefers-reduced-motion
142
+ // the same bars are drawn as a static SVG instead (same band math, no motion).
143
+ let reduceMotion = $state(
144
+ browser
145
+ ? window.matchMedia("(prefers-reduced-motion: reduce)").matches
146
+ : false,
147
+ )
148
+ $effect(() => {
149
+ if (!browser) return
150
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)")
151
+ const on = () => (reduceMotion = mq.matches)
152
+ mq.addEventListener?.("change", on)
153
+ return () => mq.removeEventListener?.("change", on)
154
+ })
155
+
156
+ const SPARK_W = 112
157
+ const SPARK_H = 24
158
+
159
+ interface StaticBar {
160
+ x: number
161
+ y: number
162
+ w: number
163
+ h: number
164
+ o: number
165
+ }
166
+
167
+ function staticBars(values: number[]): StaticBar[] {
168
+ const pad = 1
169
+ const innerW = SPARK_W - pad * 2
170
+ const innerH = SPARK_H - pad * 2
171
+ const min = Math.min(...values) * 0.95
172
+ const max = Math.max(...values) * 1.05
173
+ const span = max - min || 1
174
+ const bandW = innerW / values.length
175
+ const w = Math.max(1, bandW * 0.85)
176
+ return values.map((v, i) => {
177
+ const h = ((v - min) / span) * innerH
178
+ return {
179
+ x: pad + i * bandW + (bandW - w) / 2,
180
+ y: innerH - h,
181
+ w,
182
+ h,
183
+ o: 0.15 + 0.85 * (i / Math.max(1, values.length - 1)),
184
+ }
185
+ })
186
+ }
187
+ </script>
188
+
189
+ <section class="artifact-stats @container w-full">
190
+ {#if params?.title}
191
+ <div class="mb-3 flex items-center gap-2">
192
+ <h2 class="min-w-0 text-sm font-semibold text-base-content">
193
+ {params.title}
194
+ </h2>
195
+ <SectionTools {ctx} inline />
196
+ </div>
197
+ {/if}
198
+
199
+ <dl class="grid grid-cols-1 gap-x-6 gap-y-5 {gridCols}">
200
+ {#each cards as card, i (`${i}:${card.label}`)}
201
+ {@const tone = toneOf(card)}
202
+ <div
203
+ class="stat-metric flex min-w-0 flex-col gap-1 border-t border-base-content/15 pt-3"
204
+ data-tone={tone}
205
+ >
206
+ <dt
207
+ class="flex min-w-0 items-start gap-1.5 text-[0.8125rem] leading-snug text-base-content/70"
208
+ >
209
+ {#if TONE_DOT[tone]}
210
+ <span
211
+ class="mt-1.5 size-2 shrink-0 rounded-full {TONE_DOT[tone]}"
212
+ aria-hidden="true"
213
+ ></span>
214
+ {/if}
215
+ <span class="min-w-0 text-pretty break-words">{card.label}</span>
216
+ {#if TONE_WORD[tone]}
217
+ <span class="sr-only">({TONE_WORD[tone]})</span>
218
+ {/if}
219
+ </dt>
220
+
221
+ <dd class="flex flex-wrap items-baseline gap-x-1">
222
+ <span
223
+ class="text-3xl font-semibold leading-tight tracking-tight tabular-nums text-base-content"
224
+ >{fmt(card.value, card.decimals)}</span
225
+ >
226
+ {#if card.unit}
227
+ <span class="text-sm text-base-content/70">{card.unit}</span>
228
+ {/if}
229
+ </dd>
230
+
231
+ {#if card.delta}
232
+ {@const dt = deltaTone(card.delta)}
233
+ <dd
234
+ class="stat-delta flex items-start gap-1 text-[0.8125rem] leading-snug text-base-content/75"
235
+ data-delta-tone={dt}
236
+ >
237
+ <span
238
+ class="mt-px shrink-0 {DELTA_ICON_TONE[dt]}"
239
+ aria-hidden="true"
240
+ >
241
+ {#if card.delta.value > 0}
242
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
243
+ {:else if card.delta.value < 0}
244
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 7 10 10" /><path d="M17 7v10H7" /></svg>
245
+ {:else}
246
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /></svg>
247
+ {/if}
248
+ </span>
249
+ <span class="min-w-0">
250
+ <span class="font-medium tabular-nums text-base-content"
251
+ >{deltaText(card.delta, card.decimals)}</span
252
+ >{#if card.delta.label}&nbsp;{card.delta.label}{/if}
253
+ <span class="sr-only">({deltaSr(card.delta)})</span>
254
+ </span>
255
+ </dd>
256
+ {/if}
257
+
258
+ {#if card.target && card.target.value > 0}
259
+ {@const pct = targetPct(card)}
260
+ <dd class="mt-1">
261
+ <ProgressBar
262
+ value={pct}
263
+ color={TONE_PROGRESS[tone]}
264
+ size="xs"
265
+ label="{card.label}: {fmt(card.value, card.decimals)} of {fmt(
266
+ card.target.value,
267
+ card.decimals,
268
+ )}"
269
+ />
270
+ <p class="mt-1 text-xs text-base-content/70">
271
+ {fmt(card.value, card.decimals)}{card.unit ?? ""} toward {targetCaption(
272
+ card,
273
+ )}
274
+ </p>
275
+ </dd>
276
+ {/if}
277
+
278
+ {#if card.sparkline && card.sparkline.length >= 2}
279
+ <dd class="mt-1">
280
+ {#if reduceMotion}
281
+ <svg
282
+ viewBox="0 0 {SPARK_W} {SPARK_H}"
283
+ width={SPARK_W}
284
+ height={SPARK_H}
285
+ aria-hidden="true"
286
+ >
287
+ {#each staticBars(card.sparkline) as b}
288
+ <rect
289
+ x={b.x}
290
+ y={b.y}
291
+ width={b.w}
292
+ height={b.h}
293
+ rx="1"
294
+ fill={TONE_VAR[tone]}
295
+ fill-opacity={b.o}
296
+ />
297
+ {/each}
298
+ </svg>
299
+ {:else}
300
+ <div aria-hidden="true">
301
+ <Sparkline
302
+ values={card.sparkline}
303
+ width={SPARK_W}
304
+ height={SPARK_H}
305
+ color={TONE_VAR[tone]}
306
+ />
307
+ </div>
308
+ {/if}
309
+ <span class="sr-only">{sparkSr(card.sparkline)}</span>
310
+ </dd>
311
+ {/if}
312
+ </div>
313
+ {/each}
314
+ </dl>
315
+ </section>
@@ -0,0 +1,24 @@
1
+ import type { BlockProps } from "../../types.js";
2
+ declare const StatsBlock: import("svelte").Component<BlockProps<{
3
+ cards: {
4
+ label: string;
5
+ value: number;
6
+ unit?: string | undefined;
7
+ decimals?: number | undefined;
8
+ delta?: {
9
+ value: number;
10
+ goodDirection?: "up" | "down" | undefined;
11
+ label?: string | undefined;
12
+ } | undefined;
13
+ target?: {
14
+ value: number;
15
+ label?: string | undefined;
16
+ } | undefined;
17
+ sparkline?: number[] | undefined;
18
+ status?: "warn" | "neutral" | "good" | "bad" | undefined;
19
+ }[];
20
+ title?: string | undefined;
21
+ dataKey?: string | undefined;
22
+ }>, {}, "">;
23
+ type StatsBlock = ReturnType<typeof StatsBlock>;
24
+ export default StatsBlock;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * stats block — 1-8 KPI cards with delta, target progress and sparkline.
3
+ */
4
+ import type { ArtifactBlockDef } from "../../types.js";
5
+ import { type StatsParams } from "./schema.js";
6
+ declare const def: ArtifactBlockDef<StatsParams>;
7
+ export default def;
@@ -0,0 +1,49 @@
1
+ import StatsBlock from "./StatsBlock.svelte";
2
+ import { statsSchema } from "./schema.js";
3
+ const EXAMPLE = {
4
+ title: "Ruby launch: coverage scorecard",
5
+ cards: [
6
+ {
7
+ label: "Ruby coverage",
8
+ value: 81.5,
9
+ unit: "%",
10
+ delta: { value: 4.2, goodDirection: "up", label: "vs last week" },
11
+ target: { value: 98 },
12
+ sparkline: [62, 65, 69, 71, 74, 77, 81.5],
13
+ status: "good",
14
+ },
15
+ {
16
+ label: "Error rate",
17
+ value: 1.9,
18
+ unit: "%",
19
+ delta: { value: -0.4, goodDirection: "down", label: "vs last week" },
20
+ status: "good",
21
+ },
22
+ {
23
+ label: "P95 latency",
24
+ value: 412,
25
+ unit: "ms",
26
+ delta: { value: 38, goodDirection: "down", label: "vs last week" },
27
+ sparkline: [355, 361, 370, 381, 374, 412],
28
+ status: "warn",
29
+ },
30
+ {
31
+ label: "Signups",
32
+ value: 1284,
33
+ delta: { value: -52, goodDirection: "up", label: "vs last week" },
34
+ status: "bad",
35
+ },
36
+ ],
37
+ };
38
+ const def = {
39
+ type: "stats",
40
+ title: "Stats (KPI cards)",
41
+ description: "1-8 KPI cards for a scorecard row. Each card: label, value (+ small unit like %/ms), optional delta (signed change with goodDirection — a negative delta is GOOD when goodDirection is 'down', e.g. error rate; tone: success/warning/error), optional target (renders a progress bar of value toward target, e.g. '81.5% toward 98%'), optional sparkline of 2-31 recent points, and a status tone token good|warn|bad|neutral (omit it to derive the tone from the delta; 'warn' is explicit-only). Tip: lead with the card that needs a decision, keep numbers small enough to read at a glance. Live data (mos-0ninw): set params.dataKey to re-render the whole card grid from a published data key (mos-artifact data) without a republish.",
42
+ schema: statsSchema,
43
+ component: StatsBlock,
44
+ // mos-8fvt9: the section's Listen + comment sit on the heading row.
45
+ hostsTools: (p) => Boolean(p.title),
46
+ example: EXAMPLE,
47
+ dataParam: "dataKey",
48
+ };
49
+ export default def;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Params schema for the stats block (1-8 KPI cards).
3
+ */
4
+ import { z } from "zod";
5
+ declare const statStatusSchema: z.ZodEnum<{
6
+ warn: "warn";
7
+ neutral: "neutral";
8
+ good: "good";
9
+ bad: "bad";
10
+ }>;
11
+ declare const statDeltaSchema: z.ZodObject<{
12
+ value: z.ZodNumber;
13
+ goodDirection: z.ZodOptional<z.ZodEnum<{
14
+ up: "up";
15
+ down: "down";
16
+ }>>;
17
+ label: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>;
19
+ declare const statTargetSchema: z.ZodObject<{
20
+ value: z.ZodNumber;
21
+ label: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>;
23
+ declare const statCardSchema: z.ZodObject<{
24
+ label: z.ZodString;
25
+ value: z.ZodNumber;
26
+ unit: z.ZodOptional<z.ZodString>;
27
+ decimals: z.ZodOptional<z.ZodNumber>;
28
+ delta: z.ZodOptional<z.ZodObject<{
29
+ value: z.ZodNumber;
30
+ goodDirection: z.ZodOptional<z.ZodEnum<{
31
+ up: "up";
32
+ down: "down";
33
+ }>>;
34
+ label: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strip>>;
36
+ target: z.ZodOptional<z.ZodObject<{
37
+ value: z.ZodNumber;
38
+ label: z.ZodOptional<z.ZodString>;
39
+ }, z.core.$strip>>;
40
+ sparkline: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
41
+ status: z.ZodOptional<z.ZodEnum<{
42
+ warn: "warn";
43
+ neutral: "neutral";
44
+ good: "good";
45
+ bad: "bad";
46
+ }>>;
47
+ }, z.core.$strip>;
48
+ export declare const statsSchema: z.ZodObject<{
49
+ title: z.ZodOptional<z.ZodString>;
50
+ cards: z.ZodArray<z.ZodObject<{
51
+ label: z.ZodString;
52
+ value: z.ZodNumber;
53
+ unit: z.ZodOptional<z.ZodString>;
54
+ decimals: z.ZodOptional<z.ZodNumber>;
55
+ delta: z.ZodOptional<z.ZodObject<{
56
+ value: z.ZodNumber;
57
+ goodDirection: z.ZodOptional<z.ZodEnum<{
58
+ up: "up";
59
+ down: "down";
60
+ }>>;
61
+ label: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strip>>;
63
+ target: z.ZodOptional<z.ZodObject<{
64
+ value: z.ZodNumber;
65
+ label: z.ZodOptional<z.ZodString>;
66
+ }, z.core.$strip>>;
67
+ sparkline: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
68
+ status: z.ZodOptional<z.ZodEnum<{
69
+ warn: "warn";
70
+ neutral: "neutral";
71
+ good: "good";
72
+ bad: "bad";
73
+ }>>;
74
+ }, z.core.$strip>>;
75
+ dataKey: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strip>;
77
+ export type StatStatus = z.infer<typeof statStatusSchema>;
78
+ export type StatDelta = z.infer<typeof statDeltaSchema>;
79
+ export type StatTarget = z.infer<typeof statTargetSchema>;
80
+ export type StatCard = z.infer<typeof statCardSchema>;
81
+ export type StatsParams = z.infer<typeof statsSchema>;
82
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Params schema for the stats block (1-8 KPI cards).
3
+ */
4
+ import { z } from "zod";
5
+ const statStatusSchema = z.enum(["good", "warn", "bad", "neutral"]);
6
+ const statDeltaSchema = z.object({
7
+ /** Signed change, e.g. 4.2 or -1.3. Sign + goodDirection set the tone. */
8
+ value: z.number(),
9
+ /** Which direction of change is GOOD for this metric. Default up. */
10
+ goodDirection: z.enum(["up", "down"]).optional(),
11
+ /** Comparison phrase, e.g. "vs last week" (max 24 chars). */
12
+ label: z.string().max(24).optional(),
13
+ });
14
+ const statTargetSchema = z.object({
15
+ /** The number the value is measured toward, e.g. 98 for 98%. */
16
+ value: z.number(),
17
+ /** Optional phrase for the caption, e.g. "goal" → "81.5% toward goal (98%)". */
18
+ label: z.string().max(60).optional(),
19
+ });
20
+ const statCardSchema = z.object({
21
+ label: z.string().min(1).max(80),
22
+ value: z.number(),
23
+ /** Rendered small after the value, e.g. "%", "ms", "/mo" (max 8 chars). */
24
+ unit: z.string().max(8).optional(),
25
+ /** Fraction digits shown (0-4). Default: up to 2, trailing zeros trimmed. */
26
+ decimals: z.number().int().min(0).max(4).optional(),
27
+ delta: statDeltaSchema.optional(),
28
+ target: statTargetSchema.optional(),
29
+ /** 2-31 recent points for the sparkline (oldest → newest). */
30
+ sparkline: z.array(z.number()).min(2).max(31).optional(),
31
+ /** Status tone token. Default: derived from delta, else neutral. */
32
+ status: statStatusSchema.optional(),
33
+ });
34
+ export const statsSchema = z.object({
35
+ /** Optional heading above the card grid. */
36
+ title: z.string().max(120).optional(),
37
+ cards: z
38
+ .array(statCardSchema)
39
+ .min(1, "at least one card")
40
+ .max(8, "at most 8 cards"),
41
+ /** Live data (mos-0ninw): key into ctx.data whose value replaces params. */
42
+ dataKey: z.string().max(80).optional(),
43
+ });
@@ -0,0 +1,2 @@
1
+ import { type ThumbPalette } from "../../thumb-kit.js";
2
+ export declare function drawStatsThumb(raw: unknown, palette: ThumbPalette): string | null;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Stats block thumbnail composer (mos-qkpva).
3
+ *
4
+ * The KPI scorecard as a silhouette: one column per card (up to four fit the
5
+ * 1.6:1 frame) — hairline top rule, the VALUE (large, bold), its unit, the
6
+ * label under it and the status dot. Sparklines draw as tiny polylines; the
7
+ * delta prose and target captions are text the cell size cannot carry.
8
+ *
9
+ * Status color derivation mirrors the component: an explicit `status` wins,
10
+ * else the delta's sign against its goodDirection, else neutral.
11
+ */
12
+ import { statsSchema, } from "./schema.js";
13
+ import { r1, thumbText, thumbTruncate, } from "../../thumb-kit.js";
14
+ const W = 560;
15
+ const H = 200;
16
+ const PAD = 16;
17
+ function statusColor(palette, s) {
18
+ switch (s) {
19
+ case "good":
20
+ return palette.success;
21
+ case "warn":
22
+ return palette.warning;
23
+ case "bad":
24
+ return palette.error;
25
+ default:
26
+ return palette.primary;
27
+ }
28
+ }
29
+ function toneOf(card) {
30
+ if (card.status)
31
+ return card.status;
32
+ if (!card.delta)
33
+ return "neutral";
34
+ const good = card.delta.goodDirection ?? "up";
35
+ const up = card.delta.value > 0;
36
+ if (card.delta.value === 0)
37
+ return "neutral";
38
+ return up === (good === "up") ? "good" : "bad";
39
+ }
40
+ export function drawStatsThumb(raw, palette) {
41
+ const parsed = statsSchema.safeParse(raw);
42
+ if (!parsed.success)
43
+ return null;
44
+ const params = parsed.data;
45
+ const cards = params.cards;
46
+ if (cards.length === 0)
47
+ return null;
48
+ const cols = Math.min(cards.length, 4);
49
+ const colW = (W - PAD * 2) / cols;
50
+ const out = [];
51
+ cards.slice(0, 4).forEach((card, ci) => {
52
+ const x = PAD + ci * colW;
53
+ const status = toneOf(card);
54
+ const color = statusColor(palette, status);
55
+ // hairline rule on top of each column
56
+ out.push(`<line x1="${r1(x)}" x2="${r1(x + colW - 14)}" y1="${PAD}" y2="${PAD}" stroke="${palette.ink}" stroke-opacity="0.25" stroke-width="1"/>`);
57
+ // status dot + value
58
+ out.push(`<circle cx="${r1(x + 4)}" cy="${r1(PAD + 30)}" r="3.5" fill="${color}"/>`);
59
+ const valueStr = formatValue(card);
60
+ out.push(thumbText(x + 14, PAD + 36, valueStr, {
61
+ size: 24,
62
+ fill: palette.ink,
63
+ weight: 700,
64
+ }));
65
+ // label (wrapped to two lines)
66
+ const label = thumbTruncate(card.label, 20);
67
+ out.push(thumbText(x, PAD + 58, label, {
68
+ size: 11,
69
+ fill: palette.ink,
70
+ opacity: 0.6,
71
+ }));
72
+ // sparkline (last 12 points, bottom band)
73
+ if (card.sparkline && card.sparkline.length >= 2) {
74
+ const pts = card.sparkline.slice(-12);
75
+ const min = Math.min(...pts);
76
+ const max = Math.max(...pts);
77
+ const span = max - min || 1;
78
+ const sw = colW - 20;
79
+ const sh = 22;
80
+ const sy = PAD + 92;
81
+ const d = pts
82
+ .map((v, i) => `${i ? "L" : "M"} ${r1(x + (i / (pts.length - 1)) * sw)} ${r1(sy + sh - ((v - min) / span) * sh)}`)
83
+ .join(" ");
84
+ out.push(`<path d="${d}" fill="none" stroke="${color}" stroke-width="1.5"/>`);
85
+ }
86
+ // target bar
87
+ if (card.target) {
88
+ const pct = Math.max(0, Math.min(1, card.value / (card.target.value || 1)));
89
+ out.push(`<rect x="${r1(x)}" y="${r1(PAD + 124)}" width="${r1(colW - 20)}" height="4" rx="2" fill="${palette.ink}" fill-opacity="0.1"/>`);
90
+ out.push(`<rect x="${r1(x)}" y="${r1(PAD + 124)}" width="${r1(Math.max(2, (colW - 20) * pct))}" height="4" rx="2" fill="${palette.primary}" fill-opacity="0.8"/>`);
91
+ }
92
+ });
93
+ return `<svg viewBox="0 0 ${W} ${H}">${out.join("")}</svg>`;
94
+ }
95
+ /** value + unit, trailing-zero trimmed (the component's formatValue shape). */
96
+ function formatValue(card) {
97
+ const decimals = card.decimals ?? 2;
98
+ let v = card.value.toFixed(decimals);
99
+ if (v.includes("."))
100
+ v = v.replace(/\.?0+$/, "");
101
+ return v + (card.unit ?? "");
102
+ }