claude-artifact-framework 0.18.0 → 0.19.1

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/llms.txt CHANGED
@@ -74,6 +74,21 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
74
74
  the viewers are on a phone. The framework never clips (wide custom
75
75
  content scrolls), but scrolling is the fallback, not the design.
76
76
 
77
+ ## Design principles (composition choices only the spec makes)
78
+
79
+ 1. ONE dominant element per screen: exactly one big:true — zero reads
80
+ flat, two read as none.
81
+ 2. Order blocks by importance, not data model: primary action first, hero
82
+ result next, breakdowns after; config/branding/help LAST or collapsed.
83
+ 3. Icons: all siblings or none, one style (emoji), never half a set.
84
+ 4. Mid-tone saturated seed that names the domain; no grays/near-blacks.
85
+ 5. Never duplicate what the framework renders (title, totals, empties).
86
+ 6. Group, don't multiply cards: related inputs in ONE fields block;
87
+ secondary content into tabs/collapsible; >5 cards on one screen = no
88
+ hierarchy.
89
+ 7. State via when:, not more blocks.
90
+ 8. Charts are support: ≤2 per screen, wide for time series, donut ≤5.
91
+
77
92
  ## Canonical example
78
93
 
79
94
  ArtifactKit.createApp({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-artifact-framework",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Utilities for building apps inside Claude Artifacts: platform storage, chat tool-calling loops, and other primitives verified against the real runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.esm.js",
package/src/app.js CHANGED
@@ -1170,13 +1170,15 @@ export function createApp(spec = {}) {
1170
1170
  : h(
1171
1171
  "span",
1172
1172
  { className: "caf-logo caf-logo-mono", "aria-hidden": true },
1173
+ // First letter/digit of each word — punctuation "words" like the
1174
+ // "-" in "Caja - Camión" must not become half the monogram.
1173
1175
  (spec.title || "?")
1174
1176
  .split(/\s+/)
1175
- .map((w) => w[0])
1177
+ .map((w) => (w.match(/[\p{L}\p{N}]/u) || [])[0])
1176
1178
  .filter(Boolean)
1177
1179
  .slice(0, 2)
1178
1180
  .join("")
1179
- .toUpperCase()
1181
+ .toUpperCase() || "?"
1180
1182
  );
1181
1183
 
1182
1184
  return h(
@@ -1221,7 +1223,9 @@ const BASE_CSS = `
1221
1223
  --caf-control-radius: 7px; /* one radius for every control: buttons, inputs, rows */
1222
1224
  --caf-btn-pad-y: 0.45rem;
1223
1225
  --caf-btn-pad-x: 0.8rem;
1224
- --caf-num-primary: 2.1rem; /* the one size for a block's dominant number */
1226
+ --caf-num-primary: 2.6rem; /* the one size for a block's dominant number — the largest thing on screen, bigger than the H1 */
1227
+ --caf-accent-tint: color-mix(in srgb, var(--caf-accent) 7%, var(--caf-surface)); /* soft accent surface for the feature card */
1228
+ --caf-block-gap: 1.75rem; /* ONE value for air between blocks, every layout (audited: workspace used a smaller gap for the same visual role) */
1225
1229
  box-sizing: border-box;
1226
1230
  min-height: 100vh;
1227
1231
  padding: 2rem 1.25rem 4rem;
@@ -1237,7 +1241,9 @@ const BASE_CSS = `
1237
1241
  .caf-header h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; text-wrap: balance; }
1238
1242
  .caf-header p { margin: 0.4rem 0 0; color: var(--caf-muted); font-size: 0.95rem; line-height: 1.5; }
1239
1243
  /* Between-card air must beat within-card padding (1.1rem), or groups dissolve. */
1240
- .caf-panel { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 1.75rem 1rem; }
1244
+ /* align-items: start a short card must not stretch to its tall neighbor's
1245
+ height; the stretched version reads as dead air (audited). */
1246
+ .caf-panel { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: var(--caf-block-gap) 1rem; align-items: start; }
1241
1247
  .caf-slot { grid-column: span 2; min-width: 0; }
1242
1248
  @media (min-width: 720px) { .caf-slot { grid-column: span 1; } .caf-slot-wide { grid-column: span 2; } }
1243
1249
  .caf-block {
@@ -1331,7 +1337,9 @@ const BASE_CSS = `
1331
1337
  .caf-timer-done { color: var(--caf-accent); }
1332
1338
  .caf-timer-fill { transition: width 0.9s linear; }
1333
1339
  .caf-timer-controls { display: flex; gap: 0.6rem; }
1334
- .caf-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1340
+ /* Disabled goes NEUTRAL, not pale-accent: a washed-out primary reads as
1341
+ broken (audited on the chat Send with an empty input). */
1342
+ .caf-btn:disabled { cursor: not-allowed; background: var(--caf-sunken); color: var(--caf-muted); border-color: var(--caf-border); opacity: 1; }
1335
1343
  .caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
1336
1344
  .caf-steps-result { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
1337
1345
  .caf-steps-fill { transition: width 0.25s ease; }
@@ -1363,7 +1371,9 @@ const BASE_CSS = `
1363
1371
  .caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
1364
1372
  .caf-row-wrap .caf-row { flex: 1; min-width: 0; }
1365
1373
  .caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
1366
- .caf-btn-sm { font-size: 0.78rem; padding: calc(var(--caf-btn-pad-y) * 0.67) calc(var(--caf-btn-pad-x) * 0.75); }
1374
+ /* min-width keeps row actions' left edges aligned across rows even when
1375
+ labels differ ("Hecho" vs "Desmarcar" — audited misalignment). */
1376
+ .caf-btn-sm { font-size: 0.78rem; padding: calc(var(--caf-btn-pad-y) * 0.67) calc(var(--caf-btn-pad-x) * 0.75); min-width: 76px; text-align: center; }
1367
1377
  .caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
1368
1378
  .caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
1369
1379
  .caf-item-row .caf-field { flex: 1; min-width: 0; }
@@ -1377,8 +1387,8 @@ const BASE_CSS = `
1377
1387
  .caf-md-li { display: block; padding-left: 1rem; position: relative; }
1378
1388
  .caf-md-li::before { content: "•"; position: absolute; left: 0.2rem; opacity: 0.6; }
1379
1389
  .caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
1380
- .caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
1381
- .caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
1390
+ .caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--caf-block-gap); }
1391
+ .caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: var(--caf-block-gap); position: sticky; top: 1rem; }
1382
1392
  .caf-ws-slot { min-width: 0; }
1383
1393
  .caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
1384
1394
  .caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
@@ -1463,6 +1473,11 @@ const BASE_CSS = `
1463
1473
  /* Never-clip guarantee: content inside a custom block that is wider than the
1464
1474
  phone (column grids, week views) must scroll, not disappear off-screen. */
1465
1475
  .caf-custom { overflow-x: auto; }
1476
+ /* The one differentiated surface per screen: accent top rule + tinted bg on
1477
+ the block holding the hero number. Everything-is-the-same-card was the
1478
+ audited root cause of the "generic template" look. (Late in the sheet on
1479
+ purpose — it must win over .caf-block's border/background.) */
1480
+ .caf-block-feature { border-top: 3px solid var(--caf-accent); background: var(--caf-accent-tint); }
1466
1481
  /* mobile.nav: "bottom" — the app-shell tab bar pinned to the thumb zone.
1467
1482
  Tabs share the width evenly so every section stays visible; the root
1468
1483
  reserves space so the bar never covers the end of the content. */
package/src/blocks.js CHANGED
@@ -178,9 +178,14 @@ export function OutputBlock({ spec, ctx }) {
178
178
  if (!items.length) {
179
179
  return h("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
180
180
  }
181
+ // The block holding the screen's hero number (a `big` stat) gets the one
182
+ // differentiated surface — an accent-tinted feature card. Audited miss:
183
+ // with a single card chrome, the most important number on screen weighed
184
+ // exactly the same as everything else.
185
+ const hero = items.some((it) => it && it.big);
181
186
  return h(
182
187
  "div",
183
- { className: "caf-block caf-output" },
188
+ { className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
184
189
  spec.title ? h("h2", { className: "caf-block-title" }, spec.title) : null,
185
190
  h(
186
191
  "div",
@@ -202,12 +207,14 @@ export function OutputBlock({ spec, ctx }) {
202
207
  // with nothing uploaded (artifacts can't persist real images — the ~5MB
203
208
  // pool). banner's default face is a gradient derived from the theme seed;
204
209
  // image accepts a URL or an inline SVG string (LLMs generate SVG well).
205
- export function BannerBlock({ spec, ctx }) {
210
+ export function BannerBlock({ spec }) {
206
211
  const b = typeof spec.banner === "string" ? { title: spec.banner } : spec.banner || {};
207
- const [c1, c2] = ctx.colors(2);
212
+ // Tonal gradient, one hue family: the golden-angle series palette is for
213
+ // CHARTS (categorical distinction) — on a brand surface its second hue
214
+ // clashes (audited: an orange seed produced an orange→green banner).
208
215
  const background = b.image
209
216
  ? `linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)), url(${JSON.stringify(b.image)}) center / cover`
210
- : `linear-gradient(135deg, ${c1}, ${c2})`;
217
+ : `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))`;
211
218
  return h(
212
219
  "div",
213
220
  { className: "caf-banner", style: { background } },
package/src/records.js CHANGED
@@ -103,9 +103,13 @@ function Summary({ spec, records }) {
103
103
  if (!spec.summary) return null;
104
104
  const items = spec.summary(records) || [];
105
105
  if (!items.length) return null;
106
+ // Same rule as OutputBlock: the hero (big) stat promotes its card to the
107
+ // screen's feature surface. Re-audit caught this second render path
108
+ // missing the treatment — the rule must hold wherever big stats render.
109
+ const hero = items.some((it) => it && it.big);
106
110
  return h(
107
111
  "div",
108
- { className: "caf-block caf-output" },
112
+ { className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
109
113
  h(
110
114
  "div",
111
115
  { className: "caf-output-grid" },
package/src/steps.js CHANGED
@@ -28,7 +28,8 @@ function ResultScreen({ step, ctx }) {
28
28
  { className: "caf-steps-result" },
29
29
  h(
30
30
  "div",
31
- { className: "caf-block caf-output" },
31
+ // Same rule as OutputBlock: the hero (big) result gets the feature card.
32
+ { className: items.some((it) => it && it.big) ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
32
33
  step.title ? h("h2", { className: "caf-block-title" }, step.title) : null,
33
34
  h(
34
35
  "div",