claude-artifact-framework 0.2.0 → 0.3.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 +185 -0
- package/dist/index.esm.js +776 -1
- package/dist/index.esm.js.map +4 -4
- package/dist/index.global.js +89 -1
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +271 -0
- package/src/appState.js +127 -0
- package/src/blocks.js +215 -0
- package/src/index.js +3 -0
- package/src/react-runtime.js +80 -0
- package/src/records.js +158 -0
- package/src/theme.js +148 -0
package/dist/index.esm.js
CHANGED
|
@@ -100,10 +100,785 @@ function createSharedStore(key, initialState, { pollMs = 2e3, debounceMs = 300 }
|
|
|
100
100
|
const timer = setInterval(poll, pollMs);
|
|
101
101
|
return { ...store, ready, stop: () => clearInterval(timer) };
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
// src/react-runtime.js
|
|
105
|
+
var React = typeof globalThis !== "undefined" ? globalThis.React : void 0;
|
|
106
|
+
function useReact(instance) {
|
|
107
|
+
React = instance;
|
|
108
|
+
return instance;
|
|
109
|
+
}
|
|
110
|
+
function hasReact() {
|
|
111
|
+
return Boolean(React);
|
|
112
|
+
}
|
|
113
|
+
function getReact() {
|
|
114
|
+
return required();
|
|
115
|
+
}
|
|
116
|
+
function required() {
|
|
117
|
+
if (!React) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'claude-artifact-framework: React not found. Hand it over once, before createApp:\n\n import React from "react";\n ArtifactKit.useReact(React);\n'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return React;
|
|
123
|
+
}
|
|
124
|
+
var createElement = (...args) => required().createElement(...args);
|
|
125
|
+
var useState = (...args) => required().useState(...args);
|
|
126
|
+
var useEffect = (...args) => required().useEffect(...args);
|
|
127
|
+
var Component = React && React.Component || class ReactMissing {
|
|
128
|
+
render() {
|
|
129
|
+
return required();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/appState.js
|
|
134
|
+
var DATA_KEY = "caf:data";
|
|
135
|
+
var VIEW_KEY = "caf:view";
|
|
136
|
+
var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null };
|
|
137
|
+
function isPlainObject(v) {
|
|
138
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
139
|
+
}
|
|
140
|
+
function mergeDefaults(defaults, stored) {
|
|
141
|
+
if (!isPlainObject(defaults) || !isPlainObject(stored)) {
|
|
142
|
+
return stored === void 0 ? defaults : stored;
|
|
143
|
+
}
|
|
144
|
+
const out = { ...defaults };
|
|
145
|
+
for (const key of Object.keys(stored)) {
|
|
146
|
+
out[key] = isPlainObject(defaults[key]) ? mergeDefaults(defaults[key], stored[key]) : stored[key];
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function debounce2(fn, ms) {
|
|
151
|
+
let timer;
|
|
152
|
+
return (...args) => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createAppState(defaults, { debounceMs = 400 } = {}) {
|
|
158
|
+
let data = structuredCloneish(defaults);
|
|
159
|
+
let view = { ...EMPTY_VIEW };
|
|
160
|
+
let hydrated = false;
|
|
161
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
162
|
+
let queued = false;
|
|
163
|
+
function notify() {
|
|
164
|
+
if (queued) return;
|
|
165
|
+
queued = true;
|
|
166
|
+
Promise.resolve().then(() => {
|
|
167
|
+
queued = false;
|
|
168
|
+
for (const fn of listeners) fn();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const persistData = debounce2(() => storage.set(DATA_KEY, data), debounceMs);
|
|
172
|
+
const persistView = debounce2(() => storage.set(VIEW_KEY, view), debounceMs);
|
|
173
|
+
const ready = (async () => {
|
|
174
|
+
const [storedData, storedView] = await Promise.all([
|
|
175
|
+
storage.get(DATA_KEY, void 0),
|
|
176
|
+
storage.get(VIEW_KEY, void 0)
|
|
177
|
+
]);
|
|
178
|
+
if (storedData !== void 0) data = mergeDefaults(defaults, storedData);
|
|
179
|
+
if (storedView !== void 0) view = { ...EMPTY_VIEW, ...storedView };
|
|
180
|
+
hydrated = true;
|
|
181
|
+
notify();
|
|
182
|
+
})();
|
|
183
|
+
function update(fn) {
|
|
184
|
+
const result = fn(data);
|
|
185
|
+
if (result !== void 0) data = result;
|
|
186
|
+
data = { ...data };
|
|
187
|
+
persistData();
|
|
188
|
+
notify();
|
|
189
|
+
}
|
|
190
|
+
function setView(patch) {
|
|
191
|
+
view = { ...view, ...patch };
|
|
192
|
+
persistView();
|
|
193
|
+
notify();
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ready,
|
|
197
|
+
isHydrated: () => hydrated,
|
|
198
|
+
getData: () => data,
|
|
199
|
+
getView: () => view,
|
|
200
|
+
update,
|
|
201
|
+
setView,
|
|
202
|
+
go(screen, arg) {
|
|
203
|
+
setView({ stack: [...view.stack, { screen: view.screen, arg: view.arg }], screen, arg });
|
|
204
|
+
},
|
|
205
|
+
back() {
|
|
206
|
+
const stack = view.stack.slice();
|
|
207
|
+
const previous = stack.pop() || { screen: null, arg: null };
|
|
208
|
+
setView({ stack, screen: previous.screen, arg: previous.arg });
|
|
209
|
+
},
|
|
210
|
+
subscribe(fn) {
|
|
211
|
+
listeners.add(fn);
|
|
212
|
+
return () => listeners.delete(fn);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function structuredCloneish(value) {
|
|
217
|
+
if (Array.isArray(value)) return value.map(structuredCloneish);
|
|
218
|
+
if (isPlainObject(value)) {
|
|
219
|
+
const out = {};
|
|
220
|
+
for (const k of Object.keys(value)) out[k] = structuredCloneish(value[k]);
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/theme.js
|
|
227
|
+
function clamp(n, lo, hi) {
|
|
228
|
+
return Math.min(hi, Math.max(lo, n));
|
|
229
|
+
}
|
|
230
|
+
function hexToRgb(hex) {
|
|
231
|
+
let h = String(hex).replace("#", "").trim();
|
|
232
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
233
|
+
const n = parseInt(h, 16);
|
|
234
|
+
if (!Number.isFinite(n)) return [59, 110, 246];
|
|
235
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
236
|
+
}
|
|
237
|
+
function rgbToHex(rgb) {
|
|
238
|
+
return "#" + rgb.map((v) => clamp(Math.round(v), 0, 255).toString(16).padStart(2, "0")).join("");
|
|
239
|
+
}
|
|
240
|
+
function rgbToHsl([r, g, b]) {
|
|
241
|
+
r /= 255;
|
|
242
|
+
g /= 255;
|
|
243
|
+
b /= 255;
|
|
244
|
+
const max = Math.max(r, g, b);
|
|
245
|
+
const min = Math.min(r, g, b);
|
|
246
|
+
const l = (max + min) / 2;
|
|
247
|
+
const d = max - min;
|
|
248
|
+
let h = 0;
|
|
249
|
+
let s = 0;
|
|
250
|
+
if (d) {
|
|
251
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
252
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
253
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
254
|
+
else h = (r - g) / d + 4;
|
|
255
|
+
h /= 6;
|
|
256
|
+
}
|
|
257
|
+
return [h * 360, s * 100, l * 100];
|
|
258
|
+
}
|
|
259
|
+
function hslToRgb(h, s, l) {
|
|
260
|
+
h = (h % 360 + 360) % 360 / 360;
|
|
261
|
+
s = clamp(s, 0, 100) / 100;
|
|
262
|
+
l = clamp(l, 0, 100) / 100;
|
|
263
|
+
if (!s) return [l * 255, l * 255, l * 255];
|
|
264
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
265
|
+
const p = 2 * l - q;
|
|
266
|
+
const channel = (t) => {
|
|
267
|
+
if (t < 0) t += 1;
|
|
268
|
+
if (t > 1) t -= 1;
|
|
269
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
270
|
+
if (t < 1 / 2) return q;
|
|
271
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
272
|
+
return p;
|
|
273
|
+
};
|
|
274
|
+
return [channel(h + 1 / 3) * 255, channel(h) * 255, channel(h - 1 / 3) * 255];
|
|
275
|
+
}
|
|
276
|
+
function hsl(h, s, l) {
|
|
277
|
+
return rgbToHex(hslToRgb(h, s, l));
|
|
278
|
+
}
|
|
279
|
+
function luminance(rgb) {
|
|
280
|
+
const a = rgb.map((v) => {
|
|
281
|
+
v /= 255;
|
|
282
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
283
|
+
});
|
|
284
|
+
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
|
285
|
+
}
|
|
286
|
+
function contrast(a, b) {
|
|
287
|
+
const la = luminance(hexToRgb(a));
|
|
288
|
+
const lb = luminance(hexToRgb(b));
|
|
289
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
290
|
+
}
|
|
291
|
+
function accentAt(h, s, against, min, from, step) {
|
|
292
|
+
for (let l = from; l >= 4 && l <= 96; l += step) {
|
|
293
|
+
const candidate = hsl(h, s, l);
|
|
294
|
+
if (contrast(candidate, against) >= min) return candidate;
|
|
295
|
+
}
|
|
296
|
+
return hsl(h, s, step < 0 ? 24 : 82);
|
|
297
|
+
}
|
|
298
|
+
function palette(seed = "#3b6ef6") {
|
|
299
|
+
const [h, rawSat] = rgbToHsl(hexToRgb(seed));
|
|
300
|
+
const sat = clamp(rawSat, 30, 88);
|
|
301
|
+
const tint = clamp(sat * 0.1, 3, 9);
|
|
302
|
+
const lightSurface = hsl(h, tint * 0.4, 100);
|
|
303
|
+
const darkSurface = hsl(h, tint * 1.4, 13);
|
|
304
|
+
return {
|
|
305
|
+
light: {
|
|
306
|
+
bg: hsl(h, tint, 96.5),
|
|
307
|
+
surface: lightSurface,
|
|
308
|
+
sunken: hsl(h, tint, 93),
|
|
309
|
+
border: hsl(h, tint, 86),
|
|
310
|
+
text: hsl(h, tint * 1.6, 11),
|
|
311
|
+
muted: hsl(h, tint * 1.2, 41),
|
|
312
|
+
accent: accentAt(h, sat, "#ffffff", 4.5, 54, -2),
|
|
313
|
+
accentText: "#ffffff",
|
|
314
|
+
danger: accentAt(2, 68, "#ffffff", 4.5, 54, -2),
|
|
315
|
+
ok: accentAt(148, 62, "#ffffff", 4.5, 44, -2),
|
|
316
|
+
warn: accentAt(38, 88, "#ffffff", 4.5, 44, -2)
|
|
317
|
+
},
|
|
318
|
+
dark: {
|
|
319
|
+
bg: hsl(h, tint * 1.5, 9),
|
|
320
|
+
surface: darkSurface,
|
|
321
|
+
sunken: hsl(h, tint * 1.5, 7),
|
|
322
|
+
border: hsl(h, tint * 1.3, 24),
|
|
323
|
+
text: hsl(h, tint * 0.6, 95),
|
|
324
|
+
muted: hsl(h, tint * 0.8, 66),
|
|
325
|
+
accent: accentAt(h, clamp(sat, 40, 80), darkSurface, 4.5, 62, 2),
|
|
326
|
+
accentText: hsl(h, tint * 2, 8),
|
|
327
|
+
danger: accentAt(2, 70, darkSurface, 4.5, 62, 2),
|
|
328
|
+
ok: accentAt(148, 55, darkSurface, 4.5, 62, 2),
|
|
329
|
+
warn: accentAt(38, 80, darkSurface, 4.5, 62, 2)
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function vars(scheme) {
|
|
334
|
+
return Object.entries(scheme).map(([k, v]) => `--caf-${k}: ${v};`).join("");
|
|
335
|
+
}
|
|
336
|
+
function themeCss(seed) {
|
|
337
|
+
const p = palette(seed);
|
|
338
|
+
return `
|
|
339
|
+
:root { ${vars(p.light)} }
|
|
340
|
+
@media (prefers-color-scheme: dark) { :root { ${vars(p.dark)} } }
|
|
341
|
+
:root[data-theme="dark"] { ${vars(p.dark)} }
|
|
342
|
+
:root[data-theme="light"] { ${vars(p.light)} }
|
|
343
|
+
`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/blocks.js
|
|
347
|
+
function formatValue(value, format) {
|
|
348
|
+
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
349
|
+
const n = Number(value);
|
|
350
|
+
switch (format) {
|
|
351
|
+
case "money":
|
|
352
|
+
return Number.isFinite(n) ? n.toLocaleString(void 0, { style: "currency", currency: "USD", maximumFractionDigits: 2 }) : String(value);
|
|
353
|
+
case "percent":
|
|
354
|
+
return Number.isFinite(n) ? `${n.toLocaleString(void 0, { maximumFractionDigits: 2 })}%` : String(value);
|
|
355
|
+
case "number":
|
|
356
|
+
return Number.isFinite(n) ? n.toLocaleString(void 0, { maximumFractionDigits: 2 }) : String(value);
|
|
357
|
+
case "date":
|
|
358
|
+
return value instanceof Date ? value.toLocaleDateString() : String(value);
|
|
359
|
+
default:
|
|
360
|
+
return String(value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function validateField(field, value) {
|
|
364
|
+
if (field.required && (value === "" || value === null || value === void 0)) {
|
|
365
|
+
return "Required";
|
|
366
|
+
}
|
|
367
|
+
if (value === "" || value === null || value === void 0) return null;
|
|
368
|
+
if (field.type === "number" || field.type === "money" || field.type === "percent") {
|
|
369
|
+
const n = Number(value);
|
|
370
|
+
if (!Number.isFinite(n)) return "Must be a number";
|
|
371
|
+
if (field.min !== void 0 && n < field.min) return `Must be at least ${field.min}`;
|
|
372
|
+
if (field.max !== void 0 && n > field.max) return `Must be at most ${field.max}`;
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
function Field({ field, value, onChange }) {
|
|
377
|
+
const error = validateField(field, value);
|
|
378
|
+
const id = `caf-f-${field.key}`;
|
|
379
|
+
const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
|
|
380
|
+
if (field.type === "check") {
|
|
381
|
+
return createElement(
|
|
382
|
+
"label",
|
|
383
|
+
{ className: "caf-check" },
|
|
384
|
+
createElement("input", {
|
|
385
|
+
id,
|
|
386
|
+
type: "checkbox",
|
|
387
|
+
checked: Boolean(value),
|
|
388
|
+
onChange: (e) => onChange(e.target.checked)
|
|
389
|
+
}),
|
|
390
|
+
createElement("span", null, field.label || field.key)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const common = {
|
|
394
|
+
id,
|
|
395
|
+
value: value === null || value === void 0 ? "" : value,
|
|
396
|
+
"aria-invalid": error ? "true" : void 0,
|
|
397
|
+
onChange: (e) => onChange(numeric ? toNumber(e.target.value) : e.target.value)
|
|
398
|
+
};
|
|
399
|
+
let control;
|
|
400
|
+
if (field.type === "textarea") {
|
|
401
|
+
control = createElement("textarea", { ...common, rows: field.rows || 3 });
|
|
402
|
+
} else if (field.type === "select") {
|
|
403
|
+
control = createElement(
|
|
404
|
+
"select",
|
|
405
|
+
common,
|
|
406
|
+
createElement("option", { value: "" }, field.placeholder || "Select\u2026"),
|
|
407
|
+
(field.options || []).map((opt) => {
|
|
408
|
+
const val = typeof opt === "string" ? opt : opt.value;
|
|
409
|
+
const label = typeof opt === "string" ? opt : opt.label;
|
|
410
|
+
return createElement("option", { key: val, value: val }, label);
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
} else {
|
|
414
|
+
control = createElement("input", {
|
|
415
|
+
...common,
|
|
416
|
+
type: field.type === "date" ? "date" : numeric ? "number" : "text",
|
|
417
|
+
min: field.min,
|
|
418
|
+
max: field.max,
|
|
419
|
+
step: field.step || (field.type === "money" || field.type === "percent" ? "any" : void 0),
|
|
420
|
+
placeholder: field.placeholder
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return createElement(
|
|
424
|
+
"div",
|
|
425
|
+
{ className: "caf-field" },
|
|
426
|
+
createElement("label", { htmlFor: id }, field.label || field.key),
|
|
427
|
+
control,
|
|
428
|
+
field.hint && !error ? createElement("small", { className: "caf-hint" }, field.hint) : null,
|
|
429
|
+
error ? createElement("small", { className: "caf-error" }, error) : null
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
function toNumber(raw) {
|
|
433
|
+
if (raw === "") return "";
|
|
434
|
+
const n = Number(raw);
|
|
435
|
+
return Number.isFinite(n) ? n : raw;
|
|
436
|
+
}
|
|
437
|
+
function FieldsBlock({ spec, ctx }) {
|
|
438
|
+
const fields = spec.fields || [];
|
|
439
|
+
return createElement(
|
|
440
|
+
"div",
|
|
441
|
+
{ className: "caf-block caf-fields" },
|
|
442
|
+
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
443
|
+
fields.map(
|
|
444
|
+
(field) => createElement(Field, {
|
|
445
|
+
key: field.key,
|
|
446
|
+
field,
|
|
447
|
+
value: ctx.data[field.key],
|
|
448
|
+
onChange: (v) => ctx.update((d) => {
|
|
449
|
+
d[field.key] = v;
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
function OutputBlock({ spec, ctx }) {
|
|
456
|
+
const items = typeof spec.output === "function" ? spec.output(ctx.data) : spec.output || [];
|
|
457
|
+
if (!items.length) {
|
|
458
|
+
return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing to show yet.");
|
|
459
|
+
}
|
|
460
|
+
return createElement(
|
|
461
|
+
"div",
|
|
462
|
+
{ className: "caf-block caf-output" },
|
|
463
|
+
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
464
|
+
createElement(
|
|
465
|
+
"div",
|
|
466
|
+
{ className: "caf-output-grid" },
|
|
467
|
+
items.map(
|
|
468
|
+
(item, i) => createElement(
|
|
469
|
+
"div",
|
|
470
|
+
{ key: item.label || i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
|
|
471
|
+
createElement("span", { className: "caf-stat-label" }, item.label),
|
|
472
|
+
createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
|
|
473
|
+
item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
function TextBlock({ spec }) {
|
|
480
|
+
const body = typeof spec.text === "function" ? spec.text() : spec.text;
|
|
481
|
+
return createElement(
|
|
482
|
+
"div",
|
|
483
|
+
{ className: "caf-block caf-text" },
|
|
484
|
+
spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
|
|
485
|
+
createElement("p", null, body)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
function ListBlock({ spec, ctx }) {
|
|
489
|
+
const rows = (typeof spec.list === "function" ? spec.list(ctx.data) : spec.list) || [];
|
|
490
|
+
if (!rows.length) {
|
|
491
|
+
return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing here yet.");
|
|
492
|
+
}
|
|
493
|
+
const title = spec.title || ((r) => r.title ?? r.name ?? r.label ?? String(r));
|
|
494
|
+
return createElement(
|
|
495
|
+
"div",
|
|
496
|
+
{ className: "caf-block caf-list" },
|
|
497
|
+
rows.map(
|
|
498
|
+
(row, i) => createElement(
|
|
499
|
+
"button",
|
|
500
|
+
{
|
|
501
|
+
key: row.id ?? i,
|
|
502
|
+
className: "caf-row",
|
|
503
|
+
type: "button",
|
|
504
|
+
onClick: spec.onSelect ? () => spec.onSelect(row, ctx) : void 0
|
|
505
|
+
},
|
|
506
|
+
createElement("span", { className: "caf-row-title" }, title(row)),
|
|
507
|
+
spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(row)) : null
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
function CustomBlock({ spec, ctx }) {
|
|
513
|
+
const rendered = spec.custom(ctx);
|
|
514
|
+
if (typeof rendered === "string") {
|
|
515
|
+
return createElement("div", { className: "caf-block", dangerouslySetInnerHTML: { __html: rendered } });
|
|
516
|
+
}
|
|
517
|
+
return createElement("div", { className: "caf-block" }, rendered);
|
|
518
|
+
}
|
|
519
|
+
var BLOCKS = {
|
|
520
|
+
fields: FieldsBlock,
|
|
521
|
+
output: OutputBlock,
|
|
522
|
+
text: TextBlock,
|
|
523
|
+
list: ListBlock,
|
|
524
|
+
custom: CustomBlock
|
|
525
|
+
};
|
|
526
|
+
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
527
|
+
|
|
528
|
+
// src/records.js
|
|
529
|
+
function recordDefaults(fields) {
|
|
530
|
+
const rec = {};
|
|
531
|
+
for (const f of fields) {
|
|
532
|
+
rec[f.key] = f.value !== void 0 ? f.value : f.type === "check" ? false : "";
|
|
533
|
+
}
|
|
534
|
+
return rec;
|
|
535
|
+
}
|
|
536
|
+
function makeId() {
|
|
537
|
+
return "r" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
538
|
+
}
|
|
539
|
+
function rowTitle(spec, rec) {
|
|
540
|
+
if (spec.title) return spec.title(rec);
|
|
541
|
+
const f = spec.fields.find((f2) => !f2.type || f2.type === "text" || f2.type === "textarea" || f2.type === "select");
|
|
542
|
+
const v = f && rec[f.key];
|
|
543
|
+
return v === "" || v === void 0 || v === null ? "(untitled)" : String(v);
|
|
544
|
+
}
|
|
545
|
+
function Summary({ spec, records }) {
|
|
546
|
+
if (!spec.summary) return null;
|
|
547
|
+
const items = spec.summary(records) || [];
|
|
548
|
+
if (!items.length) return null;
|
|
549
|
+
return createElement(
|
|
550
|
+
"div",
|
|
551
|
+
{ className: "caf-block caf-output" },
|
|
552
|
+
createElement(
|
|
553
|
+
"div",
|
|
554
|
+
{ className: "caf-output-grid" },
|
|
555
|
+
items.map(
|
|
556
|
+
(item, i) => createElement(
|
|
557
|
+
"div",
|
|
558
|
+
{ key: item.label || i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
|
|
559
|
+
createElement("span", { className: "caf-stat-label" }, item.label),
|
|
560
|
+
createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format))
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
function ListScreen({ spec, ctx }) {
|
|
567
|
+
const label = spec.label || "item";
|
|
568
|
+
let records = ctx.data.records;
|
|
569
|
+
if (spec.sort) records = records.slice().sort(spec.sort);
|
|
570
|
+
function add() {
|
|
571
|
+
const rec = { id: makeId(), ...recordDefaults(spec.fields) };
|
|
572
|
+
ctx.update((d) => {
|
|
573
|
+
d.records = [...d.records, rec];
|
|
574
|
+
});
|
|
575
|
+
ctx.go("record", rec.id);
|
|
576
|
+
}
|
|
577
|
+
return createElement(
|
|
578
|
+
"div",
|
|
579
|
+
{ className: "caf-block caf-list" },
|
|
580
|
+
createElement(
|
|
581
|
+
"div",
|
|
582
|
+
{ className: "caf-toolbar" },
|
|
583
|
+
createElement("span", { className: "caf-count" }, `${records.length} ${label}${records.length === 1 ? "" : "s"}`),
|
|
584
|
+
createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `Add ${label}`)
|
|
585
|
+
),
|
|
586
|
+
records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
|
|
587
|
+
(rec) => createElement(
|
|
588
|
+
"button",
|
|
589
|
+
{
|
|
590
|
+
key: rec.id,
|
|
591
|
+
type: "button",
|
|
592
|
+
className: "caf-row",
|
|
593
|
+
onClick: () => ctx.go("record", rec.id)
|
|
594
|
+
},
|
|
595
|
+
createElement("span", { className: "caf-row-title" }, rowTitle(spec, rec)),
|
|
596
|
+
spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(rec)) : null
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
function DetailScreen({ spec, ctx, record }) {
|
|
602
|
+
const label = spec.label || "item";
|
|
603
|
+
function remove() {
|
|
604
|
+
ctx.update((d) => {
|
|
605
|
+
d.records = d.records.filter((r) => r.id !== record.id);
|
|
606
|
+
});
|
|
607
|
+
ctx.back();
|
|
608
|
+
}
|
|
609
|
+
return createElement(
|
|
610
|
+
"div",
|
|
611
|
+
{ className: "caf-block caf-fields" },
|
|
612
|
+
createElement(
|
|
613
|
+
"div",
|
|
614
|
+
{ className: "caf-toolbar" },
|
|
615
|
+
createElement("button", { type: "button", className: "caf-btn", onClick: ctx.back }, "\u2039 Back"),
|
|
616
|
+
createElement("button", { type: "button", className: "caf-btn caf-btn-danger", onClick: remove }, `Delete ${label}`)
|
|
617
|
+
),
|
|
618
|
+
spec.fields.map(
|
|
619
|
+
(field) => createElement(Field, {
|
|
620
|
+
key: field.key,
|
|
621
|
+
field,
|
|
622
|
+
value: record[field.key],
|
|
623
|
+
onChange: (v) => ctx.update((d) => {
|
|
624
|
+
const rec = d.records.find((r) => r.id === record.id);
|
|
625
|
+
if (rec) rec[field.key] = v;
|
|
626
|
+
})
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
function RecordsLayout({ spec, ctx }) {
|
|
632
|
+
const rspec = spec.records;
|
|
633
|
+
if (ctx.view.screen === "record") {
|
|
634
|
+
const record = ctx.data.records.find((r) => r.id === ctx.view.arg);
|
|
635
|
+
if (record) {
|
|
636
|
+
return createElement("div", { className: "caf-panel caf-panel-single" }, createElement(DetailScreen, { spec: rspec, ctx, record }));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return createElement(
|
|
640
|
+
"div",
|
|
641
|
+
{ className: "caf-panel caf-panel-single" },
|
|
642
|
+
createElement(Summary, { spec: rspec, records: ctx.data.records }),
|
|
643
|
+
createElement(ListScreen, { spec: rspec, ctx })
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/app.js
|
|
648
|
+
var LAYOUTS = ["panel", "records"];
|
|
649
|
+
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
650
|
+
function validate(spec) {
|
|
651
|
+
const layout = spec.layout || "panel";
|
|
652
|
+
if (!LAYOUTS.includes(layout)) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`claude-artifact-framework: unknown layout "${layout}". Valid layouts: ${LAYOUTS.join(", ")}.`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (layout === "records") {
|
|
658
|
+
const r = spec.records;
|
|
659
|
+
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
665
|
+
for (const f of r.fields) {
|
|
666
|
+
if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
|
|
667
|
+
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
|
|
668
|
+
if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
|
|
669
|
+
seen.add(f.key);
|
|
670
|
+
}
|
|
671
|
+
return layout;
|
|
672
|
+
}
|
|
673
|
+
const blocks = spec.blocks || [];
|
|
674
|
+
if (!Array.isArray(blocks)) {
|
|
675
|
+
throw new Error("claude-artifact-framework: `blocks` must be an array.");
|
|
676
|
+
}
|
|
677
|
+
blocks.forEach((block, i) => {
|
|
678
|
+
if (!block || typeof block !== "object") {
|
|
679
|
+
throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
|
|
680
|
+
}
|
|
681
|
+
const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
|
|
682
|
+
const known = keys.filter((k) => BLOCK_NAMES.includes(k));
|
|
683
|
+
if (known.length === 0) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
`claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (known.length > 1) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
|
|
691
|
+
", "
|
|
692
|
+
)}). Split them into separate entries of \`blocks\`.`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
return layout;
|
|
697
|
+
}
|
|
698
|
+
function defaultsFrom(spec) {
|
|
699
|
+
const data = { ...spec.data || {} };
|
|
700
|
+
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
701
|
+
data.records = [];
|
|
702
|
+
}
|
|
703
|
+
for (const block of spec.blocks || []) {
|
|
704
|
+
for (const field of block.fields || []) {
|
|
705
|
+
if (field && field.key !== void 0 && !(field.key in data)) {
|
|
706
|
+
data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return data;
|
|
711
|
+
}
|
|
712
|
+
var BlockBoundary = null;
|
|
713
|
+
function boundary(React2) {
|
|
714
|
+
if (!BlockBoundary) {
|
|
715
|
+
BlockBoundary = class extends React2.Component {
|
|
716
|
+
constructor(props) {
|
|
717
|
+
super(props);
|
|
718
|
+
this.state = { error: null };
|
|
719
|
+
}
|
|
720
|
+
static getDerivedStateFromError(error) {
|
|
721
|
+
return { error };
|
|
722
|
+
}
|
|
723
|
+
render() {
|
|
724
|
+
if (this.state.error) {
|
|
725
|
+
return createElement(
|
|
726
|
+
"div",
|
|
727
|
+
{ className: "caf-block caf-block-error" },
|
|
728
|
+
createElement("strong", null, "This section failed to render"),
|
|
729
|
+
createElement("code", null, String(this.state.error.message || this.state.error))
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
return this.props.children;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return BlockBoundary;
|
|
737
|
+
}
|
|
738
|
+
function Block({ block, ctx }) {
|
|
739
|
+
const name = Object.keys(block).find((k) => BLOCK_NAMES.includes(k));
|
|
740
|
+
const Component2 = BLOCKS[name];
|
|
741
|
+
return createElement(boundary(ctx.React), null, createElement(Component2, { spec: block, ctx }));
|
|
742
|
+
}
|
|
743
|
+
function Panel({ spec, ctx }) {
|
|
744
|
+
return createElement(
|
|
745
|
+
"div",
|
|
746
|
+
{ className: "caf-panel" },
|
|
747
|
+
(spec.blocks || []).map(
|
|
748
|
+
(block, i) => createElement(
|
|
749
|
+
"section",
|
|
750
|
+
{ key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
|
|
751
|
+
createElement(Block, { block, ctx })
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout };
|
|
757
|
+
function createApp(spec = {}) {
|
|
758
|
+
const layout = validate(spec);
|
|
759
|
+
const state = createAppState(defaultsFrom(spec));
|
|
760
|
+
const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
|
|
761
|
+
return function App() {
|
|
762
|
+
if (!hasReact()) {
|
|
763
|
+
throw new Error(
|
|
764
|
+
'claude-artifact-framework: React not found. Hand it over once, before rendering:\n\n import React from "react";\n ArtifactKit.useReact(React);\n'
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const [, force] = useState(0);
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
const unsubscribe = state.subscribe(() => force((n) => n + 1));
|
|
770
|
+
if (state.isHydrated()) force((n) => n + 1);
|
|
771
|
+
return unsubscribe;
|
|
772
|
+
}, []);
|
|
773
|
+
const ctx = {
|
|
774
|
+
React: getReact(),
|
|
775
|
+
data: state.getData(),
|
|
776
|
+
view: state.getView(),
|
|
777
|
+
update: state.update,
|
|
778
|
+
go: state.go,
|
|
779
|
+
back: state.back
|
|
780
|
+
};
|
|
781
|
+
const Layout = LAYOUT_COMPONENTS[layout];
|
|
782
|
+
return createElement(
|
|
783
|
+
"div",
|
|
784
|
+
{ className: "caf-root" },
|
|
785
|
+
createElement("style", { dangerouslySetInnerHTML: { __html: css } }),
|
|
786
|
+
createElement(
|
|
787
|
+
"header",
|
|
788
|
+
{ className: "caf-header" },
|
|
789
|
+
spec.title ? createElement("h1", null, spec.title) : null,
|
|
790
|
+
spec.subtitle ? createElement("p", null, spec.subtitle) : null
|
|
791
|
+
),
|
|
792
|
+
state.isHydrated() ? createElement(Layout, { spec, ctx }) : createElement("div", { className: "caf-block caf-loading" }, "Loading\u2026")
|
|
793
|
+
);
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
var BASE_CSS = `
|
|
797
|
+
.caf-root {
|
|
798
|
+
--caf-radius: 10px;
|
|
799
|
+
box-sizing: border-box;
|
|
800
|
+
min-height: 100vh;
|
|
801
|
+
padding: 2rem 1.25rem 4rem;
|
|
802
|
+
background: var(--caf-bg);
|
|
803
|
+
color: var(--caf-text);
|
|
804
|
+
font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
805
|
+
-webkit-font-smoothing: antialiased;
|
|
806
|
+
}
|
|
807
|
+
.caf-root *, .caf-root *::before, .caf-root *::after { box-sizing: inherit; }
|
|
808
|
+
.caf-header { max-width: 760px; margin: 0 auto 1.5rem; }
|
|
809
|
+
.caf-header h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; text-wrap: balance; }
|
|
810
|
+
.caf-header p { margin: 0.4rem 0 0; color: var(--caf-muted); font-size: 0.95rem; line-height: 1.5; }
|
|
811
|
+
.caf-panel { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
812
|
+
.caf-slot { grid-column: span 2; min-width: 0; }
|
|
813
|
+
@media (min-width: 720px) { .caf-slot { grid-column: span 1; } .caf-slot-wide { grid-column: span 2; } }
|
|
814
|
+
.caf-block {
|
|
815
|
+
background: var(--caf-surface);
|
|
816
|
+
border: 1px solid var(--caf-border);
|
|
817
|
+
border-radius: var(--caf-radius);
|
|
818
|
+
padding: 1.1rem;
|
|
819
|
+
display: flex;
|
|
820
|
+
flex-direction: column;
|
|
821
|
+
gap: 0.8rem;
|
|
822
|
+
height: 100%;
|
|
823
|
+
}
|
|
824
|
+
.caf-block-title { margin: 0; font-size: 0.95rem; font-weight: 650; }
|
|
825
|
+
.caf-field { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
826
|
+
.caf-field label { font-size: 0.78rem; font-weight: 600; color: var(--caf-muted); letter-spacing: 0.02em; }
|
|
827
|
+
.caf-field input, .caf-field select, .caf-field textarea {
|
|
828
|
+
font: inherit; font-size: 0.95rem; padding: 0.5rem 0.6rem;
|
|
829
|
+
border-radius: 7px; border: 1px solid var(--caf-border);
|
|
830
|
+
background: var(--caf-sunken); color: var(--caf-text); width: 100%;
|
|
831
|
+
}
|
|
832
|
+
.caf-field input:focus-visible, .caf-field select:focus-visible, .caf-field textarea:focus-visible,
|
|
833
|
+
.caf-row:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
834
|
+
.caf-field input[aria-invalid="true"] { border-color: var(--caf-danger); }
|
|
835
|
+
.caf-check { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
|
|
836
|
+
.caf-hint { color: var(--caf-muted); font-size: 0.75rem; }
|
|
837
|
+
.caf-error { color: var(--caf-danger); font-size: 0.75rem; font-weight: 600; }
|
|
838
|
+
.caf-output-grid { display: flex; flex-direction: column; gap: 0.7rem; }
|
|
839
|
+
.caf-stat { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
|
|
840
|
+
.caf-stat-label { color: var(--caf-muted); font-size: 0.82rem; }
|
|
841
|
+
.caf-stat-value { font-size: 1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
842
|
+
.caf-stat-big { flex-direction: column; align-items: flex-start; gap: 0.15rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--caf-border); }
|
|
843
|
+
.caf-stat-big .caf-stat-value { font-size: 1.9rem; font-weight: 680; letter-spacing: -0.02em; color: var(--caf-accent); }
|
|
844
|
+
.caf-list { padding: 0.4rem; gap: 0; }
|
|
845
|
+
.caf-row {
|
|
846
|
+
font: inherit; text-align: left; cursor: pointer;
|
|
847
|
+
display: flex; flex-direction: column; gap: 0.15rem;
|
|
848
|
+
padding: 0.65rem 0.7rem; border: none; border-radius: 7px;
|
|
849
|
+
background: transparent; color: inherit; width: 100%;
|
|
850
|
+
}
|
|
851
|
+
.caf-row:hover { background: var(--caf-sunken); }
|
|
852
|
+
.caf-row-title { font-size: 0.92rem; font-weight: 550; }
|
|
853
|
+
.caf-row-sub { font-size: 0.78rem; color: var(--caf-muted); }
|
|
854
|
+
.caf-panel-single { grid-template-columns: 1fr; }
|
|
855
|
+
.caf-panel-single .caf-slot, .caf-panel-single > * { grid-column: span 1; }
|
|
856
|
+
.caf-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.3rem 0.3rem 0.5rem; }
|
|
857
|
+
.caf-count { font-size: 0.8rem; color: var(--caf-muted); font-variant-numeric: tabular-nums; }
|
|
858
|
+
.caf-btn {
|
|
859
|
+
font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer;
|
|
860
|
+
padding: 0.45rem 0.8rem; border-radius: 7px;
|
|
861
|
+
border: 1px solid var(--caf-border); background: var(--caf-sunken); color: var(--caf-text);
|
|
862
|
+
}
|
|
863
|
+
.caf-btn:hover { filter: brightness(0.97); }
|
|
864
|
+
.caf-btn:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
865
|
+
.caf-btn-primary { background: var(--caf-accent); border-color: var(--caf-accent); color: var(--caf-accentText); }
|
|
866
|
+
.caf-btn-danger { background: transparent; border-color: var(--caf-danger); color: var(--caf-danger); }
|
|
867
|
+
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
868
|
+
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
869
|
+
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|
|
870
|
+
.caf-block-error code { font-size: 0.78rem; color: var(--caf-muted); word-break: break-word; }
|
|
871
|
+
`;
|
|
103
872
|
export {
|
|
873
|
+
contrast,
|
|
874
|
+
createApp,
|
|
104
875
|
createPersistedStore,
|
|
105
876
|
createSharedStore,
|
|
106
877
|
createStore,
|
|
107
|
-
|
|
878
|
+
hasReact,
|
|
879
|
+
palette,
|
|
880
|
+
storage,
|
|
881
|
+
themeCss,
|
|
882
|
+
useReact
|
|
108
883
|
};
|
|
109
884
|
//# sourceMappingURL=index.esm.js.map
|