@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.
- package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- 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} {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,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,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
|
+
}
|