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/README.md +38 -3
- package/dist/index.esm.js +30 -13
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +28 -15
- package/dist/index.global.js.map +3 -3
- package/llms.txt +15 -0
- package/package.json +1 -1
- package/src/app.js +23 -8
- package/src/blocks.js +11 -4
- package/src/records.js +5 -1
- package/src/steps.js +2 -1
package/README.md
CHANGED
|
@@ -909,9 +909,10 @@ keys must be unique across steps.
|
|
|
909
909
|
|
|
910
910
|
**Formats** (`format:` on output/chart/compute items): `money`, `percent`,
|
|
911
911
|
`number`, `date`. `percent` expects 0–100 (pass `ratio * 100`, not the raw
|
|
912
|
-
ratio). `big: true` renders an item at hero size
|
|
913
|
-
|
|
914
|
-
|
|
912
|
+
ratio). `big: true` renders an item at hero size AND promotes its block to
|
|
913
|
+
the screen's one feature surface (accent-tinted card) — reserve it for the
|
|
914
|
+
ONE number that is the screen's main result (a total, a final score), not
|
|
915
|
+
for every stat; more than one hero per screen means no hero at all.
|
|
915
916
|
|
|
916
917
|
**Quick-add pattern** — a lightweight alternative to `records` when you just
|
|
917
918
|
need "type something, press a button, it lands in a list":
|
|
@@ -987,6 +988,40 @@ Conflict handling is last-write-wins: if two viewers change the same key
|
|
|
987
988
|
within one poll window, whichever write reaches storage last overwrites the
|
|
988
989
|
other. There is no merge/CRDT logic.
|
|
989
990
|
|
|
991
|
+
## Design principles — composing a screen that reads
|
|
992
|
+
|
|
993
|
+
The framework owns the CSS, but whether a screen has visual hierarchy is
|
|
994
|
+
decided by choices only the SPEC can make. Every beautiful app follows
|
|
995
|
+
these; every generic-looking one breaks at least two:
|
|
996
|
+
|
|
997
|
+
1. **One dominant element per screen.** Exactly one `big: true` stat — the
|
|
998
|
+
screen's answer (the total, the score, the net). It automatically
|
|
999
|
+
becomes the highlighted feature card. Zero heroes reads flat; two
|
|
1000
|
+
heroes read as none.
|
|
1001
|
+
2. **Order blocks by importance, not by data model.** First what the user
|
|
1002
|
+
DOES most (the quick-add, the main fields), then the hero result, then
|
|
1003
|
+
breakdowns and charts, then history. Configuration, branding text, and
|
|
1004
|
+
help go LAST — or inside `collapsible: "collapsed"` — never first.
|
|
1005
|
+
3. **Icons: all siblings or none.** If one tab/section gets an `icon`,
|
|
1006
|
+
every tab/section in that same set gets one, same style (all emoji). A
|
|
1007
|
+
half-iconed set reads as an accident. Don't use emoji as bullet points
|
|
1008
|
+
inside stat labels or titles that already live in an iconed set.
|
|
1009
|
+
4. **Pick a mid-tone, saturated seed that names the domain** — warm
|
|
1010
|
+
orange/red for food, teal/blue for finance, green for health. Grays and
|
|
1011
|
+
near-blacks kill the entire accent system; neons burn it.
|
|
1012
|
+
5. **Never duplicate what the framework already renders.** No text block
|
|
1013
|
+
repeating the app's own title (the header shows it), no hand-built
|
|
1014
|
+
totals when `summary`/`output` computes them, no "empty list" messages.
|
|
1015
|
+
6. **Group; don't multiply cards.** Related inputs belong in ONE `fields`
|
|
1016
|
+
block with one title. Secondary content goes into `tabs` or a
|
|
1017
|
+
collapsible. A first screen with more than ~5 cards has no hierarchy
|
|
1018
|
+
left — and a block whose only content is one lonely button probably
|
|
1019
|
+
belongs next to the fields it acts on.
|
|
1020
|
+
7. **Say state with `when`, not with more blocks.** Hide what doesn't
|
|
1021
|
+
apply right now instead of stacking every possibility on screen.
|
|
1022
|
+
8. **Charts are support, not wallpaper.** At most two per screen; `wide:
|
|
1023
|
+
true` for time series; donuts only up to ~5 slices.
|
|
1024
|
+
|
|
990
1025
|
## Hard rules — the short list
|
|
991
1026
|
|
|
992
1027
|
The rules that decide whether a generated app works, all in one place
|
package/dist/index.esm.js
CHANGED
|
@@ -761,9 +761,10 @@ function OutputBlock({ spec, ctx }) {
|
|
|
761
761
|
if (!items.length) {
|
|
762
762
|
return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
|
|
763
763
|
}
|
|
764
|
+
const hero = items.some((it) => it && it.big);
|
|
764
765
|
return createElement(
|
|
765
766
|
"div",
|
|
766
|
-
{ className: "caf-block caf-output" },
|
|
767
|
+
{ className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
|
|
767
768
|
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
768
769
|
createElement(
|
|
769
770
|
"div",
|
|
@@ -780,10 +781,9 @@ function OutputBlock({ spec, ctx }) {
|
|
|
780
781
|
)
|
|
781
782
|
);
|
|
782
783
|
}
|
|
783
|
-
function BannerBlock({ spec
|
|
784
|
+
function BannerBlock({ spec }) {
|
|
784
785
|
const b = typeof spec.banner === "string" ? { title: spec.banner } : spec.banner || {};
|
|
785
|
-
const
|
|
786
|
-
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, ${c1}, ${c2})`;
|
|
786
|
+
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))`;
|
|
787
787
|
return createElement(
|
|
788
788
|
"div",
|
|
789
789
|
{ className: "caf-banner", style: { background } },
|
|
@@ -1100,9 +1100,10 @@ function Summary({ spec, records }) {
|
|
|
1100
1100
|
if (!spec.summary) return null;
|
|
1101
1101
|
const items = spec.summary(records) || [];
|
|
1102
1102
|
if (!items.length) return null;
|
|
1103
|
+
const hero = items.some((it) => it && it.big);
|
|
1103
1104
|
return createElement(
|
|
1104
1105
|
"div",
|
|
1105
|
-
{ className: "caf-block caf-output" },
|
|
1106
|
+
{ className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
|
|
1106
1107
|
createElement(
|
|
1107
1108
|
"div",
|
|
1108
1109
|
{ className: "caf-output-grid" },
|
|
@@ -1373,7 +1374,8 @@ function ResultScreen({ step, ctx }) {
|
|
|
1373
1374
|
{ className: "caf-steps-result" },
|
|
1374
1375
|
createElement(
|
|
1375
1376
|
"div",
|
|
1376
|
-
|
|
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" },
|
|
1377
1379
|
step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
|
|
1378
1380
|
createElement(
|
|
1379
1381
|
"div",
|
|
@@ -2446,7 +2448,9 @@ function createApp(spec = {}) {
|
|
|
2446
2448
|
const logoEl = !spec.brand ? null : logo && logo.trim().startsWith("<svg") ? createElement("span", { className: "caf-logo caf-logo-svg", dangerouslySetInnerHTML: { __html: logo } }) : logo && /^https?:\/\//.test(logo) ? createElement("img", { className: "caf-logo", src: logo, alt: "" }) : logo ? createElement("span", { className: "caf-logo caf-logo-emoji", "aria-hidden": true }, logo) : createElement(
|
|
2447
2449
|
"span",
|
|
2448
2450
|
{ className: "caf-logo caf-logo-mono", "aria-hidden": true },
|
|
2449
|
-
|
|
2451
|
+
// First letter/digit of each word — punctuation "words" like the
|
|
2452
|
+
// "-" in "Caja - Camión" must not become half the monogram.
|
|
2453
|
+
(spec.title || "?").split(/\s+/).map((w) => (w.match(/[\p{L}\p{N}]/u) || [])[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?"
|
|
2450
2454
|
);
|
|
2451
2455
|
return createElement(
|
|
2452
2456
|
"div",
|
|
@@ -2487,7 +2491,9 @@ var BASE_CSS = `
|
|
|
2487
2491
|
--caf-control-radius: 7px; /* one radius for every control: buttons, inputs, rows */
|
|
2488
2492
|
--caf-btn-pad-y: 0.45rem;
|
|
2489
2493
|
--caf-btn-pad-x: 0.8rem;
|
|
2490
|
-
--caf-num-primary: 2.
|
|
2494
|
+
--caf-num-primary: 2.6rem; /* the one size for a block's dominant number \u2014 the largest thing on screen, bigger than the H1 */
|
|
2495
|
+
--caf-accent-tint: color-mix(in srgb, var(--caf-accent) 7%, var(--caf-surface)); /* soft accent surface for the feature card */
|
|
2496
|
+
--caf-block-gap: 1.75rem; /* ONE value for air between blocks, every layout (audited: workspace used a smaller gap for the same visual role) */
|
|
2491
2497
|
box-sizing: border-box;
|
|
2492
2498
|
min-height: 100vh;
|
|
2493
2499
|
padding: 2rem 1.25rem 4rem;
|
|
@@ -2503,7 +2509,9 @@ var BASE_CSS = `
|
|
|
2503
2509
|
.caf-header h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; text-wrap: balance; }
|
|
2504
2510
|
.caf-header p { margin: 0.4rem 0 0; color: var(--caf-muted); font-size: 0.95rem; line-height: 1.5; }
|
|
2505
2511
|
/* Between-card air must beat within-card padding (1.1rem), or groups dissolve. */
|
|
2506
|
-
|
|
2512
|
+
/* align-items: start \u2014 a short card must not stretch to its tall neighbor's
|
|
2513
|
+
height; the stretched version reads as dead air (audited). */
|
|
2514
|
+
.caf-panel { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: var(--caf-block-gap) 1rem; align-items: start; }
|
|
2507
2515
|
.caf-slot { grid-column: span 2; min-width: 0; }
|
|
2508
2516
|
@media (min-width: 720px) { .caf-slot { grid-column: span 1; } .caf-slot-wide { grid-column: span 2; } }
|
|
2509
2517
|
.caf-block {
|
|
@@ -2597,7 +2605,9 @@ var BASE_CSS = `
|
|
|
2597
2605
|
.caf-timer-done { color: var(--caf-accent); }
|
|
2598
2606
|
.caf-timer-fill { transition: width 0.9s linear; }
|
|
2599
2607
|
.caf-timer-controls { display: flex; gap: 0.6rem; }
|
|
2600
|
-
|
|
2608
|
+
/* Disabled goes NEUTRAL, not pale-accent: a washed-out primary reads as
|
|
2609
|
+
broken (audited on the chat Send with an empty input). */
|
|
2610
|
+
.caf-btn:disabled { cursor: not-allowed; background: var(--caf-sunken); color: var(--caf-muted); border-color: var(--caf-border); opacity: 1; }
|
|
2601
2611
|
.caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
|
|
2602
2612
|
.caf-steps-result { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
2603
2613
|
.caf-steps-fill { transition: width 0.25s ease; }
|
|
@@ -2629,7 +2639,9 @@ var BASE_CSS = `
|
|
|
2629
2639
|
.caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
|
|
2630
2640
|
.caf-row-wrap .caf-row { flex: 1; min-width: 0; }
|
|
2631
2641
|
.caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
|
|
2632
|
-
|
|
2642
|
+
/* min-width keeps row actions' left edges aligned across rows even when
|
|
2643
|
+
labels differ ("Hecho" vs "Desmarcar" \u2014 audited misalignment). */
|
|
2644
|
+
.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; }
|
|
2633
2645
|
.caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
2634
2646
|
.caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
|
|
2635
2647
|
.caf-item-row .caf-field { flex: 1; min-width: 0; }
|
|
@@ -2643,8 +2655,8 @@ var BASE_CSS = `
|
|
|
2643
2655
|
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
2644
2656
|
.caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
2645
2657
|
.caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
|
|
2646
|
-
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap:
|
|
2647
|
-
.caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap:
|
|
2658
|
+
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--caf-block-gap); }
|
|
2659
|
+
.caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: var(--caf-block-gap); position: sticky; top: 1rem; }
|
|
2648
2660
|
.caf-ws-slot { min-width: 0; }
|
|
2649
2661
|
.caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
2650
2662
|
.caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
|
|
@@ -2729,6 +2741,11 @@ var BASE_CSS = `
|
|
|
2729
2741
|
/* Never-clip guarantee: content inside a custom block that is wider than the
|
|
2730
2742
|
phone (column grids, week views) must scroll, not disappear off-screen. */
|
|
2731
2743
|
.caf-custom { overflow-x: auto; }
|
|
2744
|
+
/* The one differentiated surface per screen: accent top rule + tinted bg on
|
|
2745
|
+
the block holding the hero number. Everything-is-the-same-card was the
|
|
2746
|
+
audited root cause of the "generic template" look. (Late in the sheet on
|
|
2747
|
+
purpose \u2014 it must win over .caf-block's border/background.) */
|
|
2748
|
+
.caf-block-feature { border-top: 3px solid var(--caf-accent); background: var(--caf-accent-tint); }
|
|
2732
2749
|
/* mobile.nav: "bottom" \u2014 the app-shell tab bar pinned to the thumb zone.
|
|
2733
2750
|
Tabs share the width evenly so every section stays visible; the root
|
|
2734
2751
|
reserves space so the bar never covers the end of the content. */
|