claude-artifact-framework 0.17.0 → 0.19.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 +26 -11
- package/dist/index.esm.js +83 -27
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +34 -20
- package/dist/index.global.js.map +3 -3
- package/llms.txt +5 -3
- package/package.json +1 -1
- package/src/app.js +44 -11
- package/src/blocks.js +11 -4
- package/src/records.js +28 -3
- package/src/steps.js +19 -10
package/README.md
CHANGED
|
@@ -168,6 +168,18 @@ ArtifactKit.createApp({
|
|
|
168
168
|
Field keys must be unique across all steps (they share one data pool); a
|
|
169
169
|
duplicate throws at creation, as does an unknown step key.
|
|
170
170
|
|
|
171
|
+
A result step can end on more than stats: give it `blocks` and any block
|
|
172
|
+
set (a chart of the answers, explanatory text, a list) renders under the
|
|
173
|
+
result grid:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
{ title: "Resultado",
|
|
177
|
+
result: (d) => [{ label: "Libre por mes", value: libre(d), format: "money", big: true }],
|
|
178
|
+
blocks: [{ title: "Distribución", chart: (d) => ({ type: "donut", format: "money", items: porRubro(d) }) }] }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`blocks` on a non-result step throws.
|
|
182
|
+
|
|
171
183
|
### The `chart` block — bar, line, donut
|
|
172
184
|
|
|
173
185
|
Same vocabulary as `output` (items of `{ label, value }` plus an optional
|
|
@@ -343,12 +355,13 @@ blocks: [
|
|
|
343
355
|
```
|
|
344
356
|
|
|
345
357
|
Action keys: `label`, `run`, optional `when` (hides the button when false)
|
|
346
|
-
and `kind` (`"primary"` / `"danger"`). Row actions
|
|
347
|
-
`(record, data, viewerId)
|
|
348
|
-
early is fine (`run: (r) => ...`),
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
358
|
+
and `kind` (`"primary"` / `"danger"`). Row actions — `when` and `run`
|
|
359
|
+
alike — receive `(record, data, viewerId)`; block actions
|
|
360
|
+
`(data, viewerId)`. Stopping early is fine (`run: (r) => ...`), and the
|
|
361
|
+
framework matches the viewer id **by name**: a second parameter literally
|
|
362
|
+
named `viewerId` receives the viewer id even though `data` was skipped —
|
|
363
|
+
`run: (r, viewerId) => ...` works as written. Still, prefer the full
|
|
364
|
+
order: `run: (r, d, viewerId)`. Changes persist like any other update.
|
|
352
365
|
Give the constructive action `kind: "primary"` — a "Vote"/"Add"/"Done" left
|
|
353
366
|
as the neutral default reads weaker than a `"danger"` next to it. And a
|
|
354
367
|
smell to avoid: **a number that changes by pressing is an `action`, not an
|
|
@@ -896,9 +909,10 @@ keys must be unique across steps.
|
|
|
896
909
|
|
|
897
910
|
**Formats** (`format:` on output/chart/compute items): `money`, `percent`,
|
|
898
911
|
`number`, `date`. `percent` expects 0–100 (pass `ratio * 100`, not the raw
|
|
899
|
-
ratio). `big: true` renders an item at hero size
|
|
900
|
-
|
|
901
|
-
|
|
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.
|
|
902
916
|
|
|
903
917
|
**Quick-add pattern** — a lightweight alternative to `records` when you just
|
|
904
918
|
need "type something, press a button, it lands in a list":
|
|
@@ -993,8 +1007,9 @@ The rules that decide whether a generated app works, all in one place
|
|
|
993
1007
|
or `ctx.files`.
|
|
994
1008
|
5. **Phases belong to the `machine`** — never track game/process phases
|
|
995
1009
|
with hand-rolled flags and `setTimeout`; `ctx.send` is the only door.
|
|
996
|
-
6. **
|
|
997
|
-
|
|
1010
|
+
6. **Row callbacks are `(record, data, viewerId)`** — `when` and `run`
|
|
1011
|
+
alike. Prefer the full order; a second parameter literally named
|
|
1012
|
+
`viewerId` is matched by name and still gets the viewer id.
|
|
998
1013
|
7. **One `big: true` per screen** — it marks THE result, not every stat.
|
|
999
1014
|
8. **The constructive action takes `kind: "primary"`**; a counter changed
|
|
1000
1015
|
by pressing is an `action`, not an editable `number` field.
|
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 } },
|
|
@@ -1070,6 +1070,19 @@ function recordDefaults(fields) {
|
|
|
1070
1070
|
function makeId() {
|
|
1071
1071
|
return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
1072
1072
|
}
|
|
1073
|
+
var rowCallLegacy = /* @__PURE__ */ new WeakMap();
|
|
1074
|
+
function rowCall(fn, rec, data, viewerId) {
|
|
1075
|
+
let legacy = rowCallLegacy.get(fn);
|
|
1076
|
+
if (legacy === void 0) {
|
|
1077
|
+
const src = String(fn);
|
|
1078
|
+
const arrow = src.match(/^\s*(?:async\s+)?([\w$]+)\s*=>/);
|
|
1079
|
+
const parens = src.match(/^[^(]*\(([^)]*)\)/);
|
|
1080
|
+
const params = arrow ? [arrow[1]] : parens ? parens[1].split(",").map((s) => s.trim().split(/[=\s:]/)[0]) : [];
|
|
1081
|
+
legacy = /viewer|voter|usuario|user/i.test(params[1] || "");
|
|
1082
|
+
rowCallLegacy.set(fn, legacy);
|
|
1083
|
+
}
|
|
1084
|
+
return legacy ? fn(rec, viewerId) : fn(rec, data, viewerId);
|
|
1085
|
+
}
|
|
1073
1086
|
function Avatar({ title, ctx }) {
|
|
1074
1087
|
const initials = String(title).split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";
|
|
1075
1088
|
let hash = 0;
|
|
@@ -1087,9 +1100,10 @@ function Summary({ spec, records }) {
|
|
|
1087
1100
|
if (!spec.summary) return null;
|
|
1088
1101
|
const items = spec.summary(records) || [];
|
|
1089
1102
|
if (!items.length) return null;
|
|
1103
|
+
const hero = items.some((it) => it && it.big);
|
|
1090
1104
|
return createElement(
|
|
1091
1105
|
"div",
|
|
1092
|
-
{ className: "caf-block caf-output" },
|
|
1106
|
+
{ className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
|
|
1093
1107
|
createElement(
|
|
1094
1108
|
"div",
|
|
1095
1109
|
{ className: "caf-output-grid" },
|
|
@@ -1174,7 +1188,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
1174
1188
|
spec.actions ? createElement(
|
|
1175
1189
|
"div",
|
|
1176
1190
|
{ className: "caf-row-actions" },
|
|
1177
|
-
spec.actions.filter((a) => !a.when || a.when
|
|
1191
|
+
spec.actions.filter((a) => !a.when || rowCall(a.when, rec, ctx.data, ctx.viewerId)).map(
|
|
1178
1192
|
(a) => createElement(
|
|
1179
1193
|
"button",
|
|
1180
1194
|
{
|
|
@@ -1183,7 +1197,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
1183
1197
|
className: a.kind === "danger" ? "caf-btn caf-btn-danger caf-btn-sm" : "caf-btn caf-btn-sm",
|
|
1184
1198
|
onClick: () => ctx.update((d) => {
|
|
1185
1199
|
const live = d.records.find((x) => x.id === rec.id);
|
|
1186
|
-
if (live) a.run
|
|
1200
|
+
if (live) rowCall(a.run, live, d, ctx.viewerId);
|
|
1187
1201
|
})
|
|
1188
1202
|
},
|
|
1189
1203
|
a.label
|
|
@@ -1354,23 +1368,32 @@ function stepErrors(step, data) {
|
|
|
1354
1368
|
}
|
|
1355
1369
|
function ResultScreen({ step, ctx }) {
|
|
1356
1370
|
const items = step.result(ctx.data) || [];
|
|
1371
|
+
const Block2 = ctx._Block;
|
|
1357
1372
|
return createElement(
|
|
1358
1373
|
"div",
|
|
1359
|
-
{ className: "caf-
|
|
1360
|
-
step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
|
|
1374
|
+
{ className: "caf-steps-result" },
|
|
1361
1375
|
createElement(
|
|
1362
1376
|
"div",
|
|
1363
|
-
|
|
1364
|
-
items.
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
item
|
|
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
|
+
)
|
|
1371
1391
|
)
|
|
1372
1392
|
)
|
|
1373
1393
|
),
|
|
1394
|
+
// A result can end on more than stats: any declared blocks (charts,
|
|
1395
|
+
// text, lists) render under the grid with full block services.
|
|
1396
|
+
(step.blocks || []).map((b, i) => createElement(Block2, { key: "rb" + i, block: b, ctx })),
|
|
1374
1397
|
createElement(
|
|
1375
1398
|
"div",
|
|
1376
1399
|
{ className: "caf-step-nav" },
|
|
@@ -1701,7 +1724,7 @@ function validate(spec) {
|
|
|
1701
1724
|
if (!Array.isArray(steps) || steps.length === 0) {
|
|
1702
1725
|
throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
|
|
1703
1726
|
}
|
|
1704
|
-
const STEP_KEYS = ["title", "text", "fields", "result"];
|
|
1727
|
+
const STEP_KEYS = ["title", "text", "fields", "result", "blocks"];
|
|
1705
1728
|
const seen = /* @__PURE__ */ new Set();
|
|
1706
1729
|
steps.forEach((st, i) => {
|
|
1707
1730
|
if (!st || typeof st !== "object") {
|
|
@@ -1716,6 +1739,12 @@ function validate(spec) {
|
|
|
1716
1739
|
if (!st.fields && !st.text && !st.result) {
|
|
1717
1740
|
throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
|
|
1718
1741
|
}
|
|
1742
|
+
if (st.blocks !== void 0) {
|
|
1743
|
+
if (typeof st.result !== "function") {
|
|
1744
|
+
throw new Error(`claude-artifact-framework: steps[${i}].blocks only renders on a result step \u2014 add \`result\` or move the blocks to the step's fields.`);
|
|
1745
|
+
}
|
|
1746
|
+
checkBlocks(st.blocks);
|
|
1747
|
+
}
|
|
1719
1748
|
checkFields(st.fields, `steps[${i}]`);
|
|
1720
1749
|
for (const f of st.fields || []) {
|
|
1721
1750
|
if (seen.has(f.key)) {
|
|
@@ -2221,7 +2250,10 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
2221
2250
|
{ className: "caf-documents" },
|
|
2222
2251
|
createElement(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
|
|
2223
2252
|
typePicker,
|
|
2224
|
-
|
|
2253
|
+
// Keyed by document id: switching tabs REMOUNTS the pane, so DOM-held
|
|
2254
|
+
// state can't leak across documents. Measured: the native file input
|
|
2255
|
+
// kept showing the previous document's filename.
|
|
2256
|
+
createElement(Panel, { key: active.id, spec: { blocks: aspec.blocks }, ctx: scoped })
|
|
2225
2257
|
);
|
|
2226
2258
|
}
|
|
2227
2259
|
function DocTabbar({ docs, active, dspec, types, close, onAdd, label, ctx }) {
|
|
@@ -2404,6 +2436,8 @@ function createApp(spec = {}) {
|
|
|
2404
2436
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
2405
2437
|
viewport: narrow ? "mobile" : "desktop",
|
|
2406
2438
|
_navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
|
|
2439
|
+
_Block: Block,
|
|
2440
|
+
// for layouts in other modules (steps) that render blocks
|
|
2407
2441
|
machine: spec.machine || null,
|
|
2408
2442
|
send: spec.machine ? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload) : () => {
|
|
2409
2443
|
throw new Error("claude-artifact-framework: ctx.send() needs a `machine` declared at the top level.");
|
|
@@ -2414,7 +2448,9 @@ function createApp(spec = {}) {
|
|
|
2414
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(
|
|
2415
2449
|
"span",
|
|
2416
2450
|
{ className: "caf-logo caf-logo-mono", "aria-hidden": true },
|
|
2417
|
-
|
|
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() || "?"
|
|
2418
2454
|
);
|
|
2419
2455
|
return createElement(
|
|
2420
2456
|
"div",
|
|
@@ -2438,7 +2474,13 @@ function createApp(spec = {}) {
|
|
|
2438
2474
|
{ className: "caf-block caf-block-error" },
|
|
2439
2475
|
createElement("strong", null, "A library failed to load"),
|
|
2440
2476
|
createElement("code", null, libsError)
|
|
2441
|
-
) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") :
|
|
2477
|
+
) : !state.isHydrated() || libsState === "loading" ? createElement("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries\u2026" : "Loading\u2026") : (
|
|
2478
|
+
// The same containment blocks get, one level up: a crash inside a
|
|
2479
|
+
// layout (a throwing sort, a bad row callback) shows a named error
|
|
2480
|
+
// under the header instead of killing the whole app. Measured: an
|
|
2481
|
+
// unhandled row-action throw once blanked an entire blind app.
|
|
2482
|
+
createElement(boundary(ctx.React), null, createElement(Layout, { spec, ctx }))
|
|
2483
|
+
),
|
|
2442
2484
|
spec.brand && spec.brand.footer ? createElement("footer", { className: "caf-footer" }, spec.brand.footer) : null
|
|
2443
2485
|
);
|
|
2444
2486
|
};
|
|
@@ -2449,7 +2491,9 @@ var BASE_CSS = `
|
|
|
2449
2491
|
--caf-control-radius: 7px; /* one radius for every control: buttons, inputs, rows */
|
|
2450
2492
|
--caf-btn-pad-y: 0.45rem;
|
|
2451
2493
|
--caf-btn-pad-x: 0.8rem;
|
|
2452
|
-
--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) */
|
|
2453
2497
|
box-sizing: border-box;
|
|
2454
2498
|
min-height: 100vh;
|
|
2455
2499
|
padding: 2rem 1.25rem 4rem;
|
|
@@ -2465,7 +2509,9 @@ var BASE_CSS = `
|
|
|
2465
2509
|
.caf-header h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; text-wrap: balance; }
|
|
2466
2510
|
.caf-header p { margin: 0.4rem 0 0; color: var(--caf-muted); font-size: 0.95rem; line-height: 1.5; }
|
|
2467
2511
|
/* Between-card air must beat within-card padding (1.1rem), or groups dissolve. */
|
|
2468
|
-
|
|
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; }
|
|
2469
2515
|
.caf-slot { grid-column: span 2; min-width: 0; }
|
|
2470
2516
|
@media (min-width: 720px) { .caf-slot { grid-column: span 1; } .caf-slot-wide { grid-column: span 2; } }
|
|
2471
2517
|
.caf-block {
|
|
@@ -2559,8 +2605,11 @@ var BASE_CSS = `
|
|
|
2559
2605
|
.caf-timer-done { color: var(--caf-accent); }
|
|
2560
2606
|
.caf-timer-fill { transition: width 0.9s linear; }
|
|
2561
2607
|
.caf-timer-controls { display: flex; gap: 0.6rem; }
|
|
2562
|
-
|
|
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; }
|
|
2563
2611
|
.caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
|
|
2612
|
+
.caf-steps-result { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
2564
2613
|
.caf-steps-fill { transition: width 0.25s ease; }
|
|
2565
2614
|
.caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
|
|
2566
2615
|
.caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
|
|
@@ -2590,7 +2639,9 @@ var BASE_CSS = `
|
|
|
2590
2639
|
.caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
|
|
2591
2640
|
.caf-row-wrap .caf-row { flex: 1; min-width: 0; }
|
|
2592
2641
|
.caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
|
|
2593
|
-
|
|
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; }
|
|
2594
2645
|
.caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
2595
2646
|
.caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
|
|
2596
2647
|
.caf-item-row .caf-field { flex: 1; min-width: 0; }
|
|
@@ -2604,8 +2655,8 @@ var BASE_CSS = `
|
|
|
2604
2655
|
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
2605
2656
|
.caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
2606
2657
|
.caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
|
|
2607
|
-
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap:
|
|
2608
|
-
.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; }
|
|
2609
2660
|
.caf-ws-slot { min-width: 0; }
|
|
2610
2661
|
.caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
2611
2662
|
.caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
|
|
@@ -2690,6 +2741,11 @@ var BASE_CSS = `
|
|
|
2690
2741
|
/* Never-clip guarantee: content inside a custom block that is wider than the
|
|
2691
2742
|
phone (column grids, week views) must scroll, not disappear off-screen. */
|
|
2692
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); }
|
|
2693
2749
|
/* mobile.nav: "bottom" \u2014 the app-shell tab bar pinned to the thumb zone.
|
|
2694
2750
|
Tabs share the width evenly so every section stays visible; the root
|
|
2695
2751
|
reserves space so the bar never covers the end of the content. */
|