claude-artifact-framework 0.3.0 → 0.4.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/README.md +55 -3
- package/dist/index.esm.js +316 -6
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +40 -9
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +70 -4
- package/src/blocks.js +177 -3
- package/src/steps.js +106 -0
- package/src/theme.js +9 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ mirrors npm packages automatically.
|
|
|
12
12
|
### As a global script (`<script>` tag)
|
|
13
13
|
|
|
14
14
|
```html
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.4.0/dist/index.global.js"></script>
|
|
16
16
|
<script>
|
|
17
17
|
const { storage, createStore, createPersistedStore, createSharedStore } = ArtifactKit;
|
|
18
18
|
</script>
|
|
@@ -27,11 +27,11 @@ mirrors npm packages automatically.
|
|
|
27
27
|
createStore,
|
|
28
28
|
createPersistedStore,
|
|
29
29
|
createSharedStore,
|
|
30
|
-
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
30
|
+
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.4.0/dist/index.esm.js";
|
|
31
31
|
</script>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Pin a version (`@0.
|
|
34
|
+
Pin a version (`@0.4.0`) for reproducible artifacts, or drop the version
|
|
35
35
|
(`claude-artifact-framework/dist/...`) to always get the latest release.
|
|
36
36
|
|
|
37
37
|
### Inside a React artifact
|
|
@@ -123,6 +123,58 @@ persistence path as everything else. `id` is assigned by the framework and
|
|
|
123
123
|
reserved. Optional: `title(r)` / `subtitle(r)` for rows, `sort(a, b)`,
|
|
124
124
|
`summary(records)` for stats above the list, `empty` for the empty-state text.
|
|
125
125
|
|
|
126
|
+
### The `steps` layout — wizards, quizzes, staged calculators
|
|
127
|
+
|
|
128
|
+
Declare the sequence; next/back/progress/result are framework-owned. Step
|
|
129
|
+
fields write into the same flat `data` pool, so the result step is just a
|
|
130
|
+
function of data. Next is gated on the current step's validation; the current
|
|
131
|
+
step survives a reload.
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
ArtifactKit.createApp({
|
|
135
|
+
title: "Quiz de riesgo",
|
|
136
|
+
layout: "steps",
|
|
137
|
+
steps: [
|
|
138
|
+
{ title: "Tu edad", fields: [{ key: "edad", type: "number", required: true, min: 18 }] },
|
|
139
|
+
{ title: "Tu perfil", fields: [{ key: "perfil", type: "select", required: true, options: ["conservador", "moderado", "agresivo"] }] },
|
|
140
|
+
{ title: "Resultado", result: (d) => [{ label: "Puntaje", value: score(d), big: true }] },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Field keys must be unique across all steps (they share one data pool); a
|
|
146
|
+
duplicate throws at creation, as does an unknown step key.
|
|
147
|
+
|
|
148
|
+
### The `chart` block — bar, line, donut
|
|
149
|
+
|
|
150
|
+
Same vocabulary as `output` (items of `{ label, value }` plus an optional
|
|
151
|
+
`format`), so declaring one teaches nothing new. Colors derive from the theme
|
|
152
|
+
seed by golden-angle rotation — a chart can't clash with the app it lives in.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
blocks: [
|
|
156
|
+
{ chart: (d) => ({ type: "bar", format: "money", items: monthlyTotals(d) }), title: "Ventas", wide: true },
|
|
157
|
+
{ chart: { type: "donut", items: [{ label: "Comida", value: 45 }, { label: "Otros", value: 55 }] } },
|
|
158
|
+
]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### The `timer` block — countdowns that persist their effects
|
|
162
|
+
|
|
163
|
+
The interval is framework-owned and cleaned up on unmount (a leaked
|
|
164
|
+
`setInterval` is a classic half-built artifact bug). The countdown itself is
|
|
165
|
+
deliberately ephemeral; what you declare is what happens when it finishes —
|
|
166
|
+
`onDone` runs through `update()`, so its effects persist like any other change.
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
blocks: [
|
|
170
|
+
{ timer: { minutes: 25, label: "Focus", onDone: (d) => { d.completados = (d.completados || 0) + 1; } } },
|
|
171
|
+
{ output: (d) => [{ label: "Pomodoros", value: d.completados || 0, big: true }] },
|
|
172
|
+
]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`text` blocks also accept a function of data — `{ text: (d) => "Hola " + d.nombre }` —
|
|
176
|
+
with the same contract as `output`.
|
|
177
|
+
|
|
126
178
|
## `storage`
|
|
127
179
|
|
|
128
180
|
Thin wrapper over `window.storage`, the persistence API Claude injects into a
|
package/dist/index.esm.js
CHANGED
|
@@ -342,6 +342,11 @@ function themeCss(seed) {
|
|
|
342
342
|
:root[data-theme="light"] { ${vars(p.light)} }
|
|
343
343
|
`;
|
|
344
344
|
}
|
|
345
|
+
function seriesColors(seed, n) {
|
|
346
|
+
const [h0, s0] = rgbToHsl(hexToRgb(seed || "#3b6ef6"));
|
|
347
|
+
const s = clamp(s0, 45, 70);
|
|
348
|
+
return Array.from({ length: n }, (_, i) => hsl((h0 + i * 137.5) % 360, s, 52));
|
|
349
|
+
}
|
|
345
350
|
|
|
346
351
|
// src/blocks.js
|
|
347
352
|
function formatValue(value, format) {
|
|
@@ -476,8 +481,8 @@ function OutputBlock({ spec, ctx }) {
|
|
|
476
481
|
)
|
|
477
482
|
);
|
|
478
483
|
}
|
|
479
|
-
function TextBlock({ spec }) {
|
|
480
|
-
const body = typeof spec.text === "function" ? spec.text() : spec.text;
|
|
484
|
+
function TextBlock({ spec, ctx }) {
|
|
485
|
+
const body = typeof spec.text === "function" ? spec.text(ctx.data) : spec.text;
|
|
481
486
|
return createElement(
|
|
482
487
|
"div",
|
|
483
488
|
{ className: "caf-block caf-text" },
|
|
@@ -509,6 +514,161 @@ function ListBlock({ spec, ctx }) {
|
|
|
509
514
|
)
|
|
510
515
|
);
|
|
511
516
|
}
|
|
517
|
+
var CHART_TYPES = ["bar", "line", "donut"];
|
|
518
|
+
function ChartBlock({ spec, ctx }) {
|
|
519
|
+
const c = typeof spec.chart === "function" ? spec.chart(ctx.data) : spec.chart;
|
|
520
|
+
const items = c && c.items || [];
|
|
521
|
+
if (!items.length) {
|
|
522
|
+
return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "No data to chart yet.");
|
|
523
|
+
}
|
|
524
|
+
const type = c.type || "bar";
|
|
525
|
+
if (!CHART_TYPES.includes(type)) {
|
|
526
|
+
throw new Error(`unknown chart type "${type}". Valid types: ${CHART_TYPES.join(", ")}.`);
|
|
527
|
+
}
|
|
528
|
+
const colors = ctx.colors(items.length);
|
|
529
|
+
const body = type === "bar" ? barChart(items, c, colors) : type === "line" ? lineChart(items, c) : donutChart(items, c, colors);
|
|
530
|
+
return createElement(
|
|
531
|
+
"div",
|
|
532
|
+
{ className: "caf-block caf-chart" },
|
|
533
|
+
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
534
|
+
body
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
function barChart(items, c, colors) {
|
|
538
|
+
const max = Math.max(...items.map((it) => Number(it.value) || 0), 0) || 1;
|
|
539
|
+
return createElement(
|
|
540
|
+
"div",
|
|
541
|
+
{ className: "caf-bars" },
|
|
542
|
+
items.map(
|
|
543
|
+
(it, i) => createElement(
|
|
544
|
+
"div",
|
|
545
|
+
{ key: it.label ?? i, className: "caf-bar-row" },
|
|
546
|
+
createElement("span", { className: "caf-bar-label" }, it.label),
|
|
547
|
+
createElement(
|
|
548
|
+
"div",
|
|
549
|
+
{ className: "caf-bar-track" },
|
|
550
|
+
createElement("div", {
|
|
551
|
+
className: "caf-bar-fill",
|
|
552
|
+
style: { width: `${Math.max(2, (Number(it.value) || 0) / max * 100)}%`, background: colors[i] }
|
|
553
|
+
})
|
|
554
|
+
),
|
|
555
|
+
createElement("span", { className: "caf-bar-value" }, formatValue(it.value, c.format))
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
function lineChart(items, c) {
|
|
561
|
+
const values = items.map((it) => Number(it.value) || 0);
|
|
562
|
+
const min = Math.min(...values);
|
|
563
|
+
const max = Math.max(...values);
|
|
564
|
+
const span = max - min || 1;
|
|
565
|
+
const W = 100;
|
|
566
|
+
const H = 36;
|
|
567
|
+
const pts = values.map((v, i) => [
|
|
568
|
+
values.length === 1 ? W / 2 : i / (values.length - 1) * W,
|
|
569
|
+
H - 3 - (v - min) / span * (H - 6)
|
|
570
|
+
]);
|
|
571
|
+
const last = pts[pts.length - 1];
|
|
572
|
+
return createElement(
|
|
573
|
+
"div",
|
|
574
|
+
{ className: "caf-line" },
|
|
575
|
+
createElement(
|
|
576
|
+
"svg",
|
|
577
|
+
{ viewBox: `0 0 ${W} ${H}`, className: "caf-line-svg", preserveAspectRatio: "none", "aria-hidden": "true" },
|
|
578
|
+
[0.25, 0.5, 0.75].map(
|
|
579
|
+
(f) => createElement("line", { key: f, x1: 0, x2: W, y1: H * f, y2: H * f, className: "caf-line-grid" })
|
|
580
|
+
),
|
|
581
|
+
createElement("polyline", { points: pts.map((p) => p.join(",")).join(" "), className: "caf-line-path" }),
|
|
582
|
+
createElement("circle", { cx: last[0], cy: last[1], r: 1.6, className: "caf-line-dot" })
|
|
583
|
+
),
|
|
584
|
+
createElement(
|
|
585
|
+
"div",
|
|
586
|
+
{ className: "caf-line-meta" },
|
|
587
|
+
createElement("span", { className: "caf-hint" }, String(items[0].label ?? "")),
|
|
588
|
+
createElement("span", { className: "caf-line-last" }, formatValue(values[values.length - 1], c.format)),
|
|
589
|
+
createElement("span", { className: "caf-hint" }, String(items[items.length - 1].label ?? ""))
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
function donutChart(items, c, colors) {
|
|
594
|
+
const total = items.reduce((s, it) => s + (Number(it.value) || 0), 0) || 1;
|
|
595
|
+
const R = 15.9155;
|
|
596
|
+
let offset = 25;
|
|
597
|
+
const segments = items.map((it, i) => {
|
|
598
|
+
const pct = (Number(it.value) || 0) / total * 100;
|
|
599
|
+
const seg = createElement("circle", {
|
|
600
|
+
key: it.label ?? i,
|
|
601
|
+
cx: 21,
|
|
602
|
+
cy: 21,
|
|
603
|
+
r: R,
|
|
604
|
+
className: "caf-donut-seg",
|
|
605
|
+
stroke: colors[i],
|
|
606
|
+
strokeDasharray: `${pct} ${100 - pct}`,
|
|
607
|
+
strokeDashoffset: offset
|
|
608
|
+
});
|
|
609
|
+
offset -= pct;
|
|
610
|
+
return seg;
|
|
611
|
+
});
|
|
612
|
+
return createElement(
|
|
613
|
+
"div",
|
|
614
|
+
{ className: "caf-donut" },
|
|
615
|
+
createElement("svg", { viewBox: "0 0 42 42", className: "caf-donut-svg", "aria-hidden": "true" }, segments),
|
|
616
|
+
createElement(
|
|
617
|
+
"div",
|
|
618
|
+
{ className: "caf-donut-legend" },
|
|
619
|
+
items.map(
|
|
620
|
+
(it, i) => createElement(
|
|
621
|
+
"div",
|
|
622
|
+
{ key: it.label ?? i, className: "caf-legend-row" },
|
|
623
|
+
createElement("span", { className: "caf-legend-swatch", style: { background: colors[i] } }),
|
|
624
|
+
createElement("span", { className: "caf-legend-label" }, it.label),
|
|
625
|
+
createElement("span", { className: "caf-legend-value" }, formatValue(it.value, c.format))
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
function TimerBlock({ spec, ctx }) {
|
|
632
|
+
const t = spec.timer || {};
|
|
633
|
+
const total = Math.max(1, Math.round((t.minutes || 0) * 60 + (t.seconds || 0)) || 300);
|
|
634
|
+
const [left, setLeft] = useState(total);
|
|
635
|
+
const [running, setRunning] = useState(false);
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
if (!running) return;
|
|
638
|
+
const id = setInterval(() => setLeft((prev) => Math.max(0, prev - 1)), 1e3);
|
|
639
|
+
return () => clearInterval(id);
|
|
640
|
+
}, [running]);
|
|
641
|
+
useEffect(() => {
|
|
642
|
+
if (left === 0 && running) {
|
|
643
|
+
setRunning(false);
|
|
644
|
+
if (typeof t.onDone === "function") ctx.update((d) => t.onDone(d));
|
|
645
|
+
}
|
|
646
|
+
}, [left, running]);
|
|
647
|
+
const mm = String(Math.floor(left / 60)).padStart(2, "0");
|
|
648
|
+
const ss = String(left % 60).padStart(2, "0");
|
|
649
|
+
const done = left === 0;
|
|
650
|
+
return createElement(
|
|
651
|
+
"div",
|
|
652
|
+
{ className: "caf-block caf-timer" },
|
|
653
|
+
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
654
|
+
t.label ? createElement("span", { className: "caf-hint" }, t.label) : null,
|
|
655
|
+
createElement("div", { className: done ? "caf-timer-time caf-timer-done" : "caf-timer-time" }, done ? t.doneText || "Done!" : `${mm}:${ss}`),
|
|
656
|
+
createElement("div", { className: "caf-bar-track" }, createElement("div", { className: "caf-bar-fill caf-timer-fill", style: { width: `${left / total * 100}%` } })),
|
|
657
|
+
createElement(
|
|
658
|
+
"div",
|
|
659
|
+
{ className: "caf-timer-controls" },
|
|
660
|
+
createElement(
|
|
661
|
+
"button",
|
|
662
|
+
{ type: "button", className: "caf-btn caf-btn-primary", disabled: done, onClick: () => setRunning((r) => !r) },
|
|
663
|
+
running ? "Pause" : "Start"
|
|
664
|
+
),
|
|
665
|
+
createElement("button", { type: "button", className: "caf-btn", onClick: () => {
|
|
666
|
+
setRunning(false);
|
|
667
|
+
setLeft(total);
|
|
668
|
+
} }, "Reset")
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
}
|
|
512
672
|
function CustomBlock({ spec, ctx }) {
|
|
513
673
|
const rendered = spec.custom(ctx);
|
|
514
674
|
if (typeof rendered === "string") {
|
|
@@ -521,6 +681,8 @@ var BLOCKS = {
|
|
|
521
681
|
output: OutputBlock,
|
|
522
682
|
text: TextBlock,
|
|
523
683
|
list: ListBlock,
|
|
684
|
+
chart: ChartBlock,
|
|
685
|
+
timer: TimerBlock,
|
|
524
686
|
custom: CustomBlock
|
|
525
687
|
};
|
|
526
688
|
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
@@ -644,8 +806,91 @@ function RecordsLayout({ spec, ctx }) {
|
|
|
644
806
|
);
|
|
645
807
|
}
|
|
646
808
|
|
|
809
|
+
// src/steps.js
|
|
810
|
+
function stepErrors(step, data) {
|
|
811
|
+
return (step.fields || []).some((f) => validateField(f, data[f.key]) !== null);
|
|
812
|
+
}
|
|
813
|
+
function ResultScreen({ step, ctx }) {
|
|
814
|
+
const items = step.result(ctx.data) || [];
|
|
815
|
+
return createElement(
|
|
816
|
+
"div",
|
|
817
|
+
{ className: "caf-block caf-output" },
|
|
818
|
+
step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
|
|
819
|
+
createElement(
|
|
820
|
+
"div",
|
|
821
|
+
{ className: "caf-output-grid" },
|
|
822
|
+
items.map(
|
|
823
|
+
(item, i) => createElement(
|
|
824
|
+
"div",
|
|
825
|
+
{ key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
|
|
826
|
+
createElement("span", { className: "caf-stat-label" }, item.label),
|
|
827
|
+
createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
|
|
828
|
+
item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
),
|
|
832
|
+
createElement(
|
|
833
|
+
"div",
|
|
834
|
+
{ className: "caf-step-nav" },
|
|
835
|
+
createElement("button", { type: "button", className: "caf-btn", onClick: () => ctx.setStep(0) }, "Start over")
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
function StepsLayout({ spec, ctx }) {
|
|
840
|
+
const steps = spec.steps;
|
|
841
|
+
const i = Math.min(Math.max(ctx.view.step || 0, 0), steps.length - 1);
|
|
842
|
+
const step = steps[i];
|
|
843
|
+
const isResult = typeof step.result === "function";
|
|
844
|
+
const isLast = i === steps.length - 1;
|
|
845
|
+
return createElement(
|
|
846
|
+
"div",
|
|
847
|
+
{ className: "caf-panel caf-panel-single" },
|
|
848
|
+
createElement(
|
|
849
|
+
"div",
|
|
850
|
+
{ className: "caf-steps-progress" },
|
|
851
|
+
createElement("span", { className: "caf-hint" }, isResult ? "Result" : `Step ${i + 1} of ${steps.length}`),
|
|
852
|
+
createElement(
|
|
853
|
+
"div",
|
|
854
|
+
{ className: "caf-bar-track" },
|
|
855
|
+
createElement("div", { className: "caf-bar-fill caf-steps-fill", style: { width: `${(i + 1) / steps.length * 100}%` } })
|
|
856
|
+
)
|
|
857
|
+
),
|
|
858
|
+
isResult ? createElement(ResultScreen, { step, ctx }) : createElement(
|
|
859
|
+
"div",
|
|
860
|
+
{ className: "caf-block caf-fields" },
|
|
861
|
+
step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
|
|
862
|
+
step.text ? createElement("p", { className: "caf-step-text" }, typeof step.text === "function" ? step.text(ctx.data) : step.text) : null,
|
|
863
|
+
(step.fields || []).map(
|
|
864
|
+
(field) => createElement(Field, {
|
|
865
|
+
key: field.key,
|
|
866
|
+
field,
|
|
867
|
+
value: ctx.data[field.key],
|
|
868
|
+
onChange: (v) => ctx.update((d) => {
|
|
869
|
+
d[field.key] = v;
|
|
870
|
+
})
|
|
871
|
+
})
|
|
872
|
+
),
|
|
873
|
+
createElement(
|
|
874
|
+
"div",
|
|
875
|
+
{ className: "caf-step-nav" },
|
|
876
|
+
i > 0 ? createElement("button", { type: "button", className: "caf-btn", onClick: () => ctx.setStep(i - 1) }, "\u2039 Back") : createElement("span"),
|
|
877
|
+
!isLast ? createElement(
|
|
878
|
+
"button",
|
|
879
|
+
{
|
|
880
|
+
type: "button",
|
|
881
|
+
className: "caf-btn caf-btn-primary",
|
|
882
|
+
disabled: stepErrors(step, ctx.data),
|
|
883
|
+
onClick: () => ctx.setStep(i + 1)
|
|
884
|
+
},
|
|
885
|
+
"Next \u203A"
|
|
886
|
+
) : null
|
|
887
|
+
)
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
647
892
|
// src/app.js
|
|
648
|
-
var LAYOUTS = ["panel", "records"];
|
|
893
|
+
var LAYOUTS = ["panel", "records", "steps"];
|
|
649
894
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
650
895
|
function validate(spec) {
|
|
651
896
|
const layout = spec.layout || "panel";
|
|
@@ -654,6 +899,38 @@ function validate(spec) {
|
|
|
654
899
|
`claude-artifact-framework: unknown layout "${layout}". Valid layouts: ${LAYOUTS.join(", ")}.`
|
|
655
900
|
);
|
|
656
901
|
}
|
|
902
|
+
if (layout === "steps") {
|
|
903
|
+
const steps = spec.steps;
|
|
904
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
905
|
+
throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
|
|
906
|
+
}
|
|
907
|
+
const STEP_KEYS = ["title", "text", "fields", "result"];
|
|
908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
909
|
+
steps.forEach((st, i) => {
|
|
910
|
+
if (!st || typeof st !== "object") {
|
|
911
|
+
throw new Error(`claude-artifact-framework: steps[${i}] must be an object.`);
|
|
912
|
+
}
|
|
913
|
+
const unknown = Object.keys(st).filter((k) => !STEP_KEYS.includes(k));
|
|
914
|
+
if (unknown.length) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`claude-artifact-framework: steps[${i}] has unknown keys (${unknown.join(", ")}). Valid keys: ${STEP_KEYS.join(", ")}.`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (!st.fields && !st.text && !st.result) {
|
|
920
|
+
throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
|
|
921
|
+
}
|
|
922
|
+
for (const f of st.fields || []) {
|
|
923
|
+
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
|
|
924
|
+
if (seen.has(f.key)) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
`claude-artifact-framework: field key "${f.key}" appears in more than one step \u2014 keys share one data pool and must be unique.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
seen.add(f.key);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
return layout;
|
|
933
|
+
}
|
|
657
934
|
if (layout === "records") {
|
|
658
935
|
const r = spec.records;
|
|
659
936
|
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
@@ -700,7 +977,7 @@ function defaultsFrom(spec) {
|
|
|
700
977
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
701
978
|
data.records = [];
|
|
702
979
|
}
|
|
703
|
-
for (const block of spec.blocks || []) {
|
|
980
|
+
for (const block of [...spec.blocks || [], ...spec.steps || []]) {
|
|
704
981
|
for (const field of block.fields || []) {
|
|
705
982
|
if (field && field.key !== void 0 && !(field.key in data)) {
|
|
706
983
|
data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
|
|
@@ -753,7 +1030,7 @@ function Panel({ spec, ctx }) {
|
|
|
753
1030
|
)
|
|
754
1031
|
);
|
|
755
1032
|
}
|
|
756
|
-
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout };
|
|
1033
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout };
|
|
757
1034
|
function createApp(spec = {}) {
|
|
758
1035
|
const layout = validate(spec);
|
|
759
1036
|
const state = createAppState(defaultsFrom(spec));
|
|
@@ -776,7 +1053,9 @@ function createApp(spec = {}) {
|
|
|
776
1053
|
view: state.getView(),
|
|
777
1054
|
update: state.update,
|
|
778
1055
|
go: state.go,
|
|
779
|
-
back: state.back
|
|
1056
|
+
back: state.back,
|
|
1057
|
+
setStep: (n) => state.setView({ step: n }),
|
|
1058
|
+
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
|
|
780
1059
|
};
|
|
781
1060
|
const Layout = LAYOUT_COMPONENTS[layout];
|
|
782
1061
|
return createElement(
|
|
@@ -864,6 +1143,37 @@ var BASE_CSS = `
|
|
|
864
1143
|
.caf-btn:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
865
1144
|
.caf-btn-primary { background: var(--caf-accent); border-color: var(--caf-accent); color: var(--caf-accentText); }
|
|
866
1145
|
.caf-btn-danger { background: transparent; border-color: var(--caf-danger); color: var(--caf-danger); }
|
|
1146
|
+
.caf-bars { display: flex; flex-direction: column; gap: 0.55rem; }
|
|
1147
|
+
.caf-bar-row { display: grid; grid-template-columns: minmax(60px, 30%) 1fr auto; align-items: center; gap: 0.6rem; }
|
|
1148
|
+
.caf-bar-label { font-size: 0.82rem; color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1149
|
+
.caf-bar-track { height: 8px; border-radius: 4px; background: var(--caf-sunken); overflow: hidden; }
|
|
1150
|
+
.caf-bar-fill { height: 100%; border-radius: 4px; background: var(--caf-accent); }
|
|
1151
|
+
.caf-bar-value { font-size: 0.82rem; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
1152
|
+
.caf-line-svg { width: 100%; height: 110px; display: block; }
|
|
1153
|
+
.caf-line-grid { stroke: var(--caf-border); stroke-width: 0.3; }
|
|
1154
|
+
.caf-line-path { fill: none; stroke: var(--caf-accent); stroke-width: 1.1; stroke-linejoin: round; stroke-linecap: round; }
|
|
1155
|
+
.caf-line-dot { fill: var(--caf-accent); }
|
|
1156
|
+
.caf-line-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; }
|
|
1157
|
+
.caf-line-last { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--caf-accent); }
|
|
1158
|
+
.caf-donut { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
|
|
1159
|
+
.caf-donut-svg { width: 110px; height: 110px; flex: none; transform: rotate(0.001deg); }
|
|
1160
|
+
.caf-donut-seg { fill: none; stroke-width: 6; }
|
|
1161
|
+
.caf-donut-legend { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; flex: 1; }
|
|
1162
|
+
.caf-legend-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.84rem; }
|
|
1163
|
+
.caf-legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
|
1164
|
+
.caf-legend-label { color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1165
|
+
.caf-legend-value { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
1166
|
+
.caf-timer { align-items: center; text-align: center; }
|
|
1167
|
+
.caf-timer .caf-bar-track { width: 100%; }
|
|
1168
|
+
.caf-timer-time { font-size: 2.6rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
|
|
1169
|
+
.caf-timer-done { color: var(--caf-accent); }
|
|
1170
|
+
.caf-timer-fill { transition: width 0.9s linear; }
|
|
1171
|
+
.caf-timer-controls { display: flex; gap: 0.6rem; }
|
|
1172
|
+
.caf-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1173
|
+
.caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
|
|
1174
|
+
.caf-steps-fill { transition: width 0.25s ease; }
|
|
1175
|
+
.caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
|
|
1176
|
+
.caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
|
|
867
1177
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
868
1178
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
869
1179
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|