claude-artifact-framework 0.19.0 → 0.19.2

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 CHANGED
@@ -908,8 +908,10 @@ keys must be unique across steps.
908
908
  `{ description, input, run(input, ctx) }`.
909
909
 
910
910
  **Formats** (`format:` on output/chart/compute items): `money`, `percent`,
911
- `number`, `date`. `percent` expects 0–100 (pass `ratio * 100`, not the raw
912
- ratio). `big: true` renders an item at hero size AND promotes its block to
911
+ `number`, `date`, `text` (explicit no-op). Anything else throws, contained
912
+ to the block. `percent` expects 0–100 (pass `ratio * 100`, not the raw
913
+ ratio); a computed `NaN`/`Infinity` renders as "—", never as "NaN".
914
+ `big: true` renders an item at hero size AND promotes its block to
913
915
  the screen's one feature surface (accent-tinted card) — reserve it for the
914
916
  ONE number that is the screen's main result (a total, a final score), not
915
917
  for every stat; more than one hero per screen means no hero at all.
@@ -988,6 +990,40 @@ Conflict handling is last-write-wins: if two viewers change the same key
988
990
  within one poll window, whichever write reaches storage last overwrites the
989
991
  other. There is no merge/CRDT logic.
990
992
 
993
+ ## Design principles — composing a screen that reads
994
+
995
+ The framework owns the CSS, but whether a screen has visual hierarchy is
996
+ decided by choices only the SPEC can make. Every beautiful app follows
997
+ these; every generic-looking one breaks at least two:
998
+
999
+ 1. **One dominant element per screen.** Exactly one `big: true` stat — the
1000
+ screen's answer (the total, the score, the net). It automatically
1001
+ becomes the highlighted feature card. Zero heroes reads flat; two
1002
+ heroes read as none.
1003
+ 2. **Order blocks by importance, not by data model.** First what the user
1004
+ DOES most (the quick-add, the main fields), then the hero result, then
1005
+ breakdowns and charts, then history. Configuration, branding text, and
1006
+ help go LAST — or inside `collapsible: "collapsed"` — never first.
1007
+ 3. **Icons: all siblings or none.** If one tab/section gets an `icon`,
1008
+ every tab/section in that same set gets one, same style (all emoji). A
1009
+ half-iconed set reads as an accident. Don't use emoji as bullet points
1010
+ inside stat labels or titles that already live in an iconed set.
1011
+ 4. **Pick a mid-tone, saturated seed that names the domain** — warm
1012
+ orange/red for food, teal/blue for finance, green for health. Grays and
1013
+ near-blacks kill the entire accent system; neons burn it.
1014
+ 5. **Never duplicate what the framework already renders.** No text block
1015
+ repeating the app's own title (the header shows it), no hand-built
1016
+ totals when `summary`/`output` computes them, no "empty list" messages.
1017
+ 6. **Group; don't multiply cards.** Related inputs belong in ONE `fields`
1018
+ block with one title. Secondary content goes into `tabs` or a
1019
+ collapsible. A first screen with more than ~5 cards has no hierarchy
1020
+ left — and a block whose only content is one lonely button probably
1021
+ belongs next to the fields it acts on.
1022
+ 7. **Say state with `when`, not with more blocks.** Hide what doesn't
1023
+ apply right now instead of stacking every possibility on screen.
1024
+ 8. **Charts are support, not wallpaper.** At most two per screen; `wide:
1025
+ true` for time series; donuts only up to ~5 slices.
1026
+
991
1027
  ## Hard rules — the short list
992
1028
 
993
1029
  The rules that decide whether a generated app works, all in one place
package/dist/index.esm.js CHANGED
@@ -625,6 +625,7 @@ function ChatLayout({ spec, ctx }) {
625
625
  var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select", "file"];
626
626
  function formatValue(value, format) {
627
627
  if (value === null || value === void 0 || value === "") return "\u2014";
628
+ if (typeof value === "number" && !Number.isFinite(value)) return "\u2014";
628
629
  const n = Number(value);
629
630
  switch (format) {
630
631
  case "money":
@@ -635,8 +636,15 @@ function formatValue(value, format) {
635
636
  return Number.isFinite(n) ? n.toLocaleString(void 0, { maximumFractionDigits: 2 }) : String(value);
636
637
  case "date":
637
638
  return value instanceof Date ? value.toLocaleDateString() : String(value);
638
- default:
639
+ case "text":
640
+ // observed natural usage (round 21) — an explicit no-op
641
+ case void 0:
642
+ case null:
639
643
  return String(value);
644
+ default:
645
+ throw new Error(
646
+ `claude-artifact-framework: unknown format "${format}". Valid formats: money, percent, number, date, text.`
647
+ );
640
648
  }
641
649
  }
642
650
  function validateField(field, value) {
@@ -756,16 +764,12 @@ function FieldsBlock({ spec, ctx }) {
756
764
  )
757
765
  );
758
766
  }
759
- function OutputBlock({ spec, ctx }) {
760
- const items = typeof spec.output === "function" ? spec.output(ctx.data) : spec.output || [];
761
- if (!items.length) {
762
- return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
763
- }
767
+ function StatGrid({ items, title }) {
764
768
  const hero = items.some((it) => it && it.big);
765
769
  return createElement(
766
770
  "div",
767
771
  { className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
768
- spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
772
+ title ? createElement("h2", { className: "caf-block-title" }, title) : null,
769
773
  createElement(
770
774
  "div",
771
775
  { className: "caf-output-grid" },
@@ -781,6 +785,13 @@ function OutputBlock({ spec, ctx }) {
781
785
  )
782
786
  );
783
787
  }
788
+ function OutputBlock({ spec, ctx }) {
789
+ const items = typeof spec.output === "function" ? spec.output(ctx.data) : spec.output || [];
790
+ if (!items.length) {
791
+ return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
792
+ }
793
+ return createElement(StatGrid, { items, title: spec.title });
794
+ }
784
795
  function BannerBlock({ spec }) {
785
796
  const b = typeof spec.banner === "string" ? { title: spec.banner } : spec.banner || {};
786
797
  const background = b.image ? `linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)), url(${JSON.stringify(b.image)}) center / cover` : `linear-gradient(135deg, color-mix(in srgb, var(--caf-accent) 82%, #000), var(--caf-accent) 55%, color-mix(in srgb, var(--caf-accent) 72%, #fff))`;
@@ -1100,23 +1111,7 @@ function Summary({ spec, records }) {
1100
1111
  if (!spec.summary) return null;
1101
1112
  const items = spec.summary(records) || [];
1102
1113
  if (!items.length) return null;
1103
- const hero = items.some((it) => it && it.big);
1104
- return createElement(
1105
- "div",
1106
- { className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
1107
- createElement(
1108
- "div",
1109
- { className: "caf-output-grid" },
1110
- items.map(
1111
- (item, i) => createElement(
1112
- "div",
1113
- { key: item.label || i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
1114
- createElement("span", { className: "caf-stat-label" }, item.label),
1115
- createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format))
1116
- )
1117
- )
1118
- )
1119
- );
1114
+ return createElement(StatGrid, { items });
1120
1115
  }
1121
1116
  function ListScreen({ spec, ctx }) {
1122
1117
  const label = spec.label || "item";
@@ -1372,25 +1367,7 @@ function ResultScreen({ step, ctx }) {
1372
1367
  return createElement(
1373
1368
  "div",
1374
1369
  { className: "caf-steps-result" },
1375
- createElement(
1376
- "div",
1377
- // Same rule as OutputBlock: the hero (big) result gets the feature card.
1378
- { className: items.some((it) => it && it.big) ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
1379
- step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
1380
- createElement(
1381
- "div",
1382
- { className: "caf-output-grid" },
1383
- items.map(
1384
- (item, i) => createElement(
1385
- "div",
1386
- { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
1387
- createElement("span", { className: "caf-stat-label" }, item.label),
1388
- createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
1389
- item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
1390
- )
1391
- )
1392
- )
1393
- ),
1370
+ createElement(StatGrid, { items, title: step.title }),
1394
1371
  // A result can end on more than stats: any declared blocks (charts,
1395
1372
  // text, lists) render under the grid with full block services.
1396
1373
  (step.blocks || []).map((b, i) => createElement(Block2, { key: "rb" + i, block: b, ctx })),