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/llms.txt
CHANGED
|
@@ -32,7 +32,8 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
|
|
|
32
32
|
(+ key when used as a block)
|
|
33
33
|
- documents: label, title, blocks OR types, empty; machine: key, initial,
|
|
34
34
|
states, events (event: from, do, then); chat: system, greeting, placeholder,
|
|
35
|
-
tools, model, maxTokens; steps step: title, text, fields, result
|
|
35
|
+
tools, model, maxTokens; steps step: title, text, fields, result,
|
|
36
|
+
blocks (result steps only — end a wizard on charts/text under the stats)
|
|
36
37
|
- Formats: money, percent (0-100), number, date. Output/chart/compute items:
|
|
37
38
|
{ label, value, format, big }
|
|
38
39
|
|
|
@@ -50,8 +51,9 @@ https://cdn.jsdelivr.net/npm/claude-artifact-framework/dist/index.global.js
|
|
|
50
51
|
5. Phases (games, turns, processes) belong to `machine` — ctx.send(event,
|
|
51
52
|
payload) is the only door; events are no-ops outside their `from` states;
|
|
52
53
|
`after: {seconds, then, do}` timers are framework-owned.
|
|
53
|
-
6. Row
|
|
54
|
-
viewerId). Stop early if you like
|
|
54
|
+
6. Row callbacks (when AND run) receive (record, data, viewerId); block
|
|
55
|
+
actions (data, viewerId). Stop early if you like; a 2nd param literally
|
|
56
|
+
named viewerId is matched by name and gets the viewer id.
|
|
55
57
|
7. One big:true per screen; the constructive action takes kind:"primary"; a
|
|
56
58
|
counter changed by pressing is an action, not an editable number field.
|
|
57
59
|
8. Parts of a record are `items` — never JSON in a textarea.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
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
|
@@ -312,7 +312,7 @@ function validate(spec) {
|
|
|
312
312
|
if (!Array.isArray(steps) || steps.length === 0) {
|
|
313
313
|
throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
|
|
314
314
|
}
|
|
315
|
-
const STEP_KEYS = ["title", "text", "fields", "result"];
|
|
315
|
+
const STEP_KEYS = ["title", "text", "fields", "result", "blocks"];
|
|
316
316
|
const seen = new Set();
|
|
317
317
|
steps.forEach((st, i) => {
|
|
318
318
|
if (!st || typeof st !== "object") {
|
|
@@ -327,6 +327,15 @@ function validate(spec) {
|
|
|
327
327
|
if (!st.fields && !st.text && !st.result) {
|
|
328
328
|
throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
|
|
329
329
|
}
|
|
330
|
+
// A result step may close on more than stats: `blocks` renders any
|
|
331
|
+
// block set (charts, text, lists) under the result grid. Measured
|
|
332
|
+
// miss: a wizard that wanted to END on a chart had nowhere to put it.
|
|
333
|
+
if (st.blocks !== undefined) {
|
|
334
|
+
if (typeof st.result !== "function") {
|
|
335
|
+
throw new Error(`claude-artifact-framework: steps[${i}].blocks only renders on a result step — add \`result\` or move the blocks to the step's fields.`);
|
|
336
|
+
}
|
|
337
|
+
checkBlocks(st.blocks);
|
|
338
|
+
}
|
|
330
339
|
checkFields(st.fields, `steps[${i}]`);
|
|
331
340
|
for (const f of st.fields || []) {
|
|
332
341
|
if (seen.has(f.key)) {
|
|
@@ -920,7 +929,10 @@ function DocumentsLayout({ spec, ctx }) {
|
|
|
920
929
|
{ className: "caf-documents" },
|
|
921
930
|
h(DocTabbar, { docs, active, dspec, types, close, onAdd, label, ctx }),
|
|
922
931
|
typePicker,
|
|
923
|
-
|
|
932
|
+
// Keyed by document id: switching tabs REMOUNTS the pane, so DOM-held
|
|
933
|
+
// state can't leak across documents. Measured: the native file input
|
|
934
|
+
// kept showing the previous document's filename.
|
|
935
|
+
h(Panel, { key: active.id, spec: { blocks: aspec.blocks }, ctx: scoped })
|
|
924
936
|
);
|
|
925
937
|
}
|
|
926
938
|
|
|
@@ -1133,6 +1145,7 @@ export function createApp(spec = {}) {
|
|
|
1133
1145
|
colors: (n) => seriesColors(spec.theme && spec.theme.seed, n),
|
|
1134
1146
|
viewport: narrow ? "mobile" : "desktop",
|
|
1135
1147
|
_navTabs: narrow && spec.mobile && spec.mobile.nav === "bottom" ? findNavTabs(spec) : null,
|
|
1148
|
+
_Block: Block, // for layouts in other modules (steps) that render blocks
|
|
1136
1149
|
machine: spec.machine || null,
|
|
1137
1150
|
send: spec.machine
|
|
1138
1151
|
? (eventName, payload) => runMachineEvent(spec.machine, state.update, eventName, payload)
|
|
@@ -1157,13 +1170,15 @@ export function createApp(spec = {}) {
|
|
|
1157
1170
|
: h(
|
|
1158
1171
|
"span",
|
|
1159
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.
|
|
1160
1175
|
(spec.title || "?")
|
|
1161
1176
|
.split(/\s+/)
|
|
1162
|
-
.map((w) => w[0])
|
|
1177
|
+
.map((w) => (w.match(/[\p{L}\p{N}]/u) || [])[0])
|
|
1163
1178
|
.filter(Boolean)
|
|
1164
1179
|
.slice(0, 2)
|
|
1165
1180
|
.join("")
|
|
1166
|
-
.toUpperCase()
|
|
1181
|
+
.toUpperCase() || "?"
|
|
1167
1182
|
);
|
|
1168
1183
|
|
|
1169
1184
|
return h(
|
|
@@ -1192,7 +1207,11 @@ export function createApp(spec = {}) {
|
|
|
1192
1207
|
)
|
|
1193
1208
|
: !state.isHydrated() || libsState === "loading"
|
|
1194
1209
|
? h("div", { className: "caf-block caf-loading" }, libsState === "loading" ? "Loading libraries…" : "Loading…")
|
|
1195
|
-
:
|
|
1210
|
+
: // The same containment blocks get, one level up: a crash inside a
|
|
1211
|
+
// layout (a throwing sort, a bad row callback) shows a named error
|
|
1212
|
+
// under the header instead of killing the whole app. Measured: an
|
|
1213
|
+
// unhandled row-action throw once blanked an entire blind app.
|
|
1214
|
+
h(boundary(ctx.React), null, h(Layout, { spec, ctx })),
|
|
1196
1215
|
spec.brand && spec.brand.footer ? h("footer", { className: "caf-footer" }, spec.brand.footer) : null
|
|
1197
1216
|
);
|
|
1198
1217
|
};
|
|
@@ -1204,7 +1223,9 @@ const BASE_CSS = `
|
|
|
1204
1223
|
--caf-control-radius: 7px; /* one radius for every control: buttons, inputs, rows */
|
|
1205
1224
|
--caf-btn-pad-y: 0.45rem;
|
|
1206
1225
|
--caf-btn-pad-x: 0.8rem;
|
|
1207
|
-
--caf-num-primary: 2.
|
|
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) */
|
|
1208
1229
|
box-sizing: border-box;
|
|
1209
1230
|
min-height: 100vh;
|
|
1210
1231
|
padding: 2rem 1.25rem 4rem;
|
|
@@ -1220,7 +1241,9 @@ const BASE_CSS = `
|
|
|
1220
1241
|
.caf-header h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; text-wrap: balance; }
|
|
1221
1242
|
.caf-header p { margin: 0.4rem 0 0; color: var(--caf-muted); font-size: 0.95rem; line-height: 1.5; }
|
|
1222
1243
|
/* Between-card air must beat within-card padding (1.1rem), or groups dissolve. */
|
|
1223
|
-
|
|
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; }
|
|
1224
1247
|
.caf-slot { grid-column: span 2; min-width: 0; }
|
|
1225
1248
|
@media (min-width: 720px) { .caf-slot { grid-column: span 1; } .caf-slot-wide { grid-column: span 2; } }
|
|
1226
1249
|
.caf-block {
|
|
@@ -1314,8 +1337,11 @@ const BASE_CSS = `
|
|
|
1314
1337
|
.caf-timer-done { color: var(--caf-accent); }
|
|
1315
1338
|
.caf-timer-fill { transition: width 0.9s linear; }
|
|
1316
1339
|
.caf-timer-controls { display: flex; gap: 0.6rem; }
|
|
1317
|
-
|
|
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; }
|
|
1318
1343
|
.caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
|
|
1344
|
+
.caf-steps-result { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
1319
1345
|
.caf-steps-fill { transition: width 0.25s ease; }
|
|
1320
1346
|
.caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
|
|
1321
1347
|
.caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
|
|
@@ -1345,7 +1371,9 @@ const BASE_CSS = `
|
|
|
1345
1371
|
.caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
|
|
1346
1372
|
.caf-row-wrap .caf-row { flex: 1; min-width: 0; }
|
|
1347
1373
|
.caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
|
|
1348
|
-
|
|
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; }
|
|
1349
1377
|
.caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
1350
1378
|
.caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
|
|
1351
1379
|
.caf-item-row .caf-field { flex: 1; min-width: 0; }
|
|
@@ -1359,8 +1387,8 @@ const BASE_CSS = `
|
|
|
1359
1387
|
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
1360
1388
|
.caf-md-li::before { content: "•"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
1361
1389
|
.caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
|
|
1362
|
-
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap:
|
|
1363
|
-
.caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap:
|
|
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; }
|
|
1364
1392
|
.caf-ws-slot { min-width: 0; }
|
|
1365
1393
|
.caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
1366
1394
|
.caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
|
|
@@ -1445,6 +1473,11 @@ const BASE_CSS = `
|
|
|
1445
1473
|
/* Never-clip guarantee: content inside a custom block that is wider than the
|
|
1446
1474
|
phone (column grids, week views) must scroll, not disappear off-screen. */
|
|
1447
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); }
|
|
1448
1481
|
/* mobile.nav: "bottom" — the app-shell tab bar pinned to the thumb zone.
|
|
1449
1482
|
Tabs share the width evenly so every section stays visible; the root
|
|
1450
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
|
|
210
|
+
export function BannerBlock({ spec }) {
|
|
206
211
|
const b = typeof spec.banner === "string" ? { title: spec.banner } : spec.banner || {};
|
|
207
|
-
|
|
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,
|
|
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
|
@@ -54,6 +54,27 @@ function makeId() {
|
|
|
54
54
|
return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Call-by-name for row callbacks. The measured 50/50 trap: both
|
|
58
|
+
// `when: (r, viewerId)` (the old documented form) and `run: (r, viewerId)`
|
|
59
|
+
// (the mistake that killed an entire blind app — `data` skipped, so `d`
|
|
60
|
+
// held a viewer-id string) exist in the wild. Positions lie; names don't:
|
|
61
|
+
// if the second declared parameter is named like the viewer id, hand it
|
|
62
|
+
// the viewer id. Every callback gets what its own names ask for, and the
|
|
63
|
+
// uniform (record, data, viewerId) order in the docs stays the one truth.
|
|
64
|
+
const rowCallLegacy = new WeakMap();
|
|
65
|
+
function rowCall(fn, rec, data, viewerId) {
|
|
66
|
+
let legacy = rowCallLegacy.get(fn);
|
|
67
|
+
if (legacy === undefined) {
|
|
68
|
+
const src = String(fn);
|
|
69
|
+
const arrow = src.match(/^\s*(?:async\s+)?([\w$]+)\s*=>/); // one bare param
|
|
70
|
+
const parens = src.match(/^[^(]*\(([^)]*)\)/);
|
|
71
|
+
const params = arrow ? [arrow[1]] : parens ? parens[1].split(",").map((s) => s.trim().split(/[=\s:]/)[0]) : [];
|
|
72
|
+
legacy = /viewer|voter|usuario|user/i.test(params[1] || "");
|
|
73
|
+
rowCallLegacy.set(fn, legacy);
|
|
74
|
+
}
|
|
75
|
+
return legacy ? fn(rec, viewerId) : fn(rec, data, viewerId);
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
// avatar: true — an initials circle per row, colored deterministically from
|
|
58
79
|
// the row title through the theme's series palette. Identity without assets.
|
|
59
80
|
function Avatar({ title, ctx }) {
|
|
@@ -82,9 +103,13 @@ function Summary({ spec, records }) {
|
|
|
82
103
|
if (!spec.summary) return null;
|
|
83
104
|
const items = spec.summary(records) || [];
|
|
84
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);
|
|
85
110
|
return h(
|
|
86
111
|
"div",
|
|
87
|
-
{ className: "caf-block caf-output" },
|
|
112
|
+
{ className: hero ? "caf-block caf-output caf-block-feature" : "caf-block caf-output" },
|
|
88
113
|
h(
|
|
89
114
|
"div",
|
|
90
115
|
{ className: "caf-output-grid" },
|
|
@@ -184,7 +209,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
184
209
|
"div",
|
|
185
210
|
{ className: "caf-row-actions" },
|
|
186
211
|
spec.actions
|
|
187
|
-
.filter((a) => !a.when || a.when
|
|
212
|
+
.filter((a) => !a.when || rowCall(a.when, rec, ctx.data, ctx.viewerId))
|
|
188
213
|
.map((a) =>
|
|
189
214
|
h(
|
|
190
215
|
"button",
|
|
@@ -195,7 +220,7 @@ function ListScreen({ spec, ctx }) {
|
|
|
195
220
|
onClick: () =>
|
|
196
221
|
ctx.update((d) => {
|
|
197
222
|
const live = d.records.find((x) => x.id === rec.id);
|
|
198
|
-
if (live) a.run
|
|
223
|
+
if (live) rowCall(a.run, live, d, ctx.viewerId);
|
|
199
224
|
}),
|
|
200
225
|
},
|
|
201
226
|
a.label
|
package/src/steps.js
CHANGED
|
@@ -22,23 +22,32 @@ function stepErrors(step, data) {
|
|
|
22
22
|
|
|
23
23
|
function ResultScreen({ step, ctx }) {
|
|
24
24
|
const items = step.result(ctx.data) || [];
|
|
25
|
+
const Block = ctx._Block;
|
|
25
26
|
return h(
|
|
26
27
|
"div",
|
|
27
|
-
{ className: "caf-
|
|
28
|
-
step.title ? h("h2", { className: "caf-block-title" }, step.title) : null,
|
|
28
|
+
{ className: "caf-steps-result" },
|
|
29
29
|
h(
|
|
30
30
|
"div",
|
|
31
|
-
|
|
32
|
-
items.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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" },
|
|
33
|
+
step.title ? h("h2", { className: "caf-block-title" }, step.title) : null,
|
|
34
|
+
h(
|
|
35
|
+
"div",
|
|
36
|
+
{ className: "caf-output-grid" },
|
|
37
|
+
items.map((item, i) =>
|
|
38
|
+
h(
|
|
39
|
+
"div",
|
|
40
|
+
{ key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
|
|
41
|
+
h("span", { className: "caf-stat-label" }, item.label),
|
|
42
|
+
h("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
|
|
43
|
+
item.hint ? h("small", { className: "caf-hint" }, item.hint) : null
|
|
44
|
+
)
|
|
39
45
|
)
|
|
40
46
|
)
|
|
41
47
|
),
|
|
48
|
+
// A result can end on more than stats: any declared blocks (charts,
|
|
49
|
+
// text, lists) render under the grid with full block services.
|
|
50
|
+
(step.blocks || []).map((b, i) => h(Block, { key: "rb" + i, block: b, ctx })),
|
|
42
51
|
h(
|
|
43
52
|
"div",
|
|
44
53
|
{ className: "caf-step-nav" },
|