claude-artifact-framework 0.8.1 → 0.9.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 +24 -3
- package/dist/index.esm.js +76 -31
- package/dist/index.esm.js.map +2 -2
- package/dist/index.global.js +68 -39
- package/dist/index.global.js.map +3 -3
- package/package.json +1 -1
- package/src/app.js +76 -29
- package/src/blocks.js +1 -1
- package/src/chat.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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
|
@@ -419,7 +419,9 @@ function Panel({ spec, ctx }) {
|
|
|
419
419
|
(spec.blocks || []).map((block, i) =>
|
|
420
420
|
h(
|
|
421
421
|
"section",
|
|
422
|
-
|
|
422
|
+
// A collapsible is always full-width: half-width accordions leave an
|
|
423
|
+
// orphaned right edge on desktop and change width across breakpoints.
|
|
424
|
+
{ key: i, className: block.wide || block.collapsible ? "caf-slot caf-slot-wide" : "caf-slot" },
|
|
423
425
|
h(Block, { block, ctx })
|
|
424
426
|
)
|
|
425
427
|
)
|
|
@@ -444,7 +446,9 @@ function RecordsWithBlocks({ spec, ctx }) {
|
|
|
444
446
|
spec.blocks.map((block, i) =>
|
|
445
447
|
h(
|
|
446
448
|
"section",
|
|
447
|
-
|
|
449
|
+
// A collapsible is always full-width: half-width accordions leave an
|
|
450
|
+
// orphaned right edge on desktop and change width across breakpoints.
|
|
451
|
+
{ key: i, className: block.wide || block.collapsible ? "caf-slot caf-slot-wide" : "caf-slot" },
|
|
448
452
|
h(Block, { block, ctx })
|
|
449
453
|
)
|
|
450
454
|
)
|
|
@@ -482,7 +486,19 @@ function loadScript(src) {
|
|
|
482
486
|
const tag = document.createElement("script");
|
|
483
487
|
tag.src = src;
|
|
484
488
|
tag.onload = () => { tag.dataset.cafReady = "1"; resolve(); };
|
|
485
|
-
tag.onerror = () =>
|
|
489
|
+
tag.onerror = () => {
|
|
490
|
+
// Models guess file paths inside packages and get them wrong more often
|
|
491
|
+
// than they get versions wrong (measured: marked@13's UMD lives at
|
|
492
|
+
// lib/marked.umd.js, not the root the model wrote). jsDelivr serves a
|
|
493
|
+
// package's default build at the bare npm/<pkg>@<version> URL, so one
|
|
494
|
+
// retry there absorbs the whole failure class.
|
|
495
|
+
const m = src.match(/^(https:\/\/cdn\.jsdelivr\.net\/npm\/(?:@[^/]+\/)?[^/]+)\/.+/);
|
|
496
|
+
if (m && m[1] !== src) {
|
|
497
|
+
loadScript(m[1]).then(resolve, () => reject(new Error(`failed to load ${src} (also tried package default ${m[1]})`)));
|
|
498
|
+
} else {
|
|
499
|
+
reject(new Error(`failed to load ${src}`));
|
|
500
|
+
}
|
|
501
|
+
};
|
|
486
502
|
document.head.appendChild(tag);
|
|
487
503
|
});
|
|
488
504
|
}
|
|
@@ -540,7 +556,9 @@ export function createApp(spec = {}) {
|
|
|
540
556
|
|
|
541
557
|
return h(
|
|
542
558
|
"div",
|
|
543
|
-
|
|
559
|
+
// The layout class lets shared CSS follow the active container width —
|
|
560
|
+
// e.g. the header must share the workspace's wider axis, not panel's.
|
|
561
|
+
{ className: `caf-root caf-layout-${layout}` },
|
|
544
562
|
h("style", { dangerouslySetInnerHTML: { __html: css } }),
|
|
545
563
|
h(
|
|
546
564
|
"header",
|
|
@@ -565,6 +583,10 @@ export function createApp(spec = {}) {
|
|
|
565
583
|
const BASE_CSS = `
|
|
566
584
|
.caf-root {
|
|
567
585
|
--caf-radius: 10px;
|
|
586
|
+
--caf-control-radius: 7px; /* one radius for every control: buttons, inputs, rows */
|
|
587
|
+
--caf-btn-pad-y: 0.45rem;
|
|
588
|
+
--caf-btn-pad-x: 0.8rem;
|
|
589
|
+
--caf-num-primary: 2.1rem; /* the one size for a block's dominant number */
|
|
568
590
|
box-sizing: border-box;
|
|
569
591
|
min-height: 100vh;
|
|
570
592
|
padding: 2rem 1.25rem 4rem;
|
|
@@ -575,9 +597,12 @@ const BASE_CSS = `
|
|
|
575
597
|
}
|
|
576
598
|
.caf-root *, .caf-root *::before, .caf-root *::after { box-sizing: inherit; }
|
|
577
599
|
.caf-header { max-width: 760px; margin: 0 auto 1.5rem; }
|
|
600
|
+
/* One left axis per screen: the header adopts the workspace's wider container. */
|
|
601
|
+
.caf-layout-workspace .caf-header { max-width: 1200px; }
|
|
578
602
|
.caf-header h1 { margin: 0; font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; text-wrap: balance; }
|
|
579
603
|
.caf-header p { margin: 0.4rem 0 0; color: var(--caf-muted); font-size: 0.95rem; line-height: 1.5; }
|
|
580
|
-
|
|
604
|
+
/* Between-card air must beat within-card padding (1.1rem), or groups dissolve. */
|
|
605
|
+
.caf-panel { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 1.75rem 1rem; }
|
|
581
606
|
.caf-slot { grid-column: span 2; min-width: 0; }
|
|
582
607
|
@media (min-width: 720px) { .caf-slot { grid-column: span 1; } .caf-slot-wide { grid-column: span 2; } }
|
|
583
608
|
.caf-block {
|
|
@@ -590,12 +615,14 @@ const BASE_CSS = `
|
|
|
590
615
|
gap: 0.8rem;
|
|
591
616
|
height: 100%;
|
|
592
617
|
}
|
|
593
|
-
.caf-block-title { margin: 0; font-size: 0.
|
|
618
|
+
.caf-block-title { margin: 0; font-size: 0.78rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em; color: var(--caf-muted); }
|
|
594
619
|
.caf-field { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
595
|
-
|
|
620
|
+
/* A full size+weight step below .caf-block-title — the uppercase alone was
|
|
621
|
+
not enough to tell "card title" from "field label" at a glance. */
|
|
622
|
+
.caf-field label { font-size: 0.68rem; font-weight: 500; color: var(--caf-muted); }
|
|
596
623
|
.caf-field input, .caf-field select, .caf-field textarea {
|
|
597
624
|
font: inherit; font-size: 0.95rem; padding: 0.5rem 0.6rem;
|
|
598
|
-
border-radius:
|
|
625
|
+
border-radius: var(--caf-control-radius); border: 1px solid var(--caf-border);
|
|
599
626
|
background: var(--caf-sunken); color: var(--caf-text); width: 100%;
|
|
600
627
|
}
|
|
601
628
|
.caf-field input:focus-visible, .caf-field select:focus-visible, .caf-field textarea:focus-visible,
|
|
@@ -604,17 +631,21 @@ const BASE_CSS = `
|
|
|
604
631
|
.caf-check { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
|
|
605
632
|
.caf-hint { color: var(--caf-muted); font-size: 0.75rem; }
|
|
606
633
|
.caf-error { color: var(--caf-danger); font-size: 0.75rem; font-weight: 600; }
|
|
607
|
-
.caf-output-grid { display:
|
|
608
|
-
|
|
609
|
-
.caf-stat
|
|
610
|
-
.caf-stat-
|
|
611
|
-
.caf-stat-
|
|
612
|
-
.caf-stat-big
|
|
634
|
+
.caf-output-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.9rem 1.1rem; }
|
|
635
|
+
/* Every stat shares one axis: label above value, both flush left. */
|
|
636
|
+
.caf-stat { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; }
|
|
637
|
+
.caf-stat-label { color: var(--caf-muted); font-size: 0.78rem; font-weight: 600; letter-spacing: 0.02em; }
|
|
638
|
+
.caf-stat-value { font-size: 1.05rem; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
639
|
+
.caf-stat-big { grid-column: 1 / -1; }
|
|
640
|
+
/* The divider exists to separate the hero from stats below it — alone in the
|
|
641
|
+
grid it would be an orphaned rule, so it only renders when followed. */
|
|
642
|
+
.caf-stat-big:not(:last-child) { padding-bottom: 0.5rem; border-bottom: 1px solid var(--caf-border); }
|
|
643
|
+
.caf-stat-big .caf-stat-value { font-size: var(--caf-num-primary); font-weight: 700; letter-spacing: -0.02em; color: var(--caf-accent); line-height: 1.15; }
|
|
613
644
|
.caf-list { padding: 0.4rem; gap: 0; }
|
|
614
645
|
.caf-row {
|
|
615
646
|
font: inherit; text-align: left; cursor: pointer;
|
|
616
647
|
display: flex; flex-direction: column; gap: 0.15rem;
|
|
617
|
-
padding: 0.65rem 0.7rem; border: none; border-radius:
|
|
648
|
+
padding: 0.65rem 0.7rem; border: none; border-radius: var(--caf-control-radius);
|
|
618
649
|
background: transparent; color: inherit; width: 100%;
|
|
619
650
|
}
|
|
620
651
|
.caf-row:hover { background: var(--caf-sunken); }
|
|
@@ -622,12 +653,15 @@ const BASE_CSS = `
|
|
|
622
653
|
.caf-row-sub { font-size: 0.78rem; color: var(--caf-muted); }
|
|
623
654
|
.caf-panel-single { grid-template-columns: 1fr; }
|
|
624
655
|
.caf-panel-single .caf-slot, .caf-panel-single > * { grid-column: span 1; }
|
|
625
|
-
|
|
656
|
+
/* Bottom padding matches the inter-row rhythm so the toolbar doesn't sit
|
|
657
|
+
closer to the first row than rows sit to each other. */
|
|
658
|
+
.caf-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.3rem 0.3rem 0.75rem; }
|
|
626
659
|
.caf-count { font-size: 0.8rem; color: var(--caf-muted); font-variant-numeric: tabular-nums; }
|
|
627
660
|
.caf-btn {
|
|
628
661
|
font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer;
|
|
629
|
-
padding:
|
|
630
|
-
border: 1px solid var(--caf-border); background: var(--caf-
|
|
662
|
+
padding: var(--caf-btn-pad-y) var(--caf-btn-pad-x); border-radius: var(--caf-control-radius);
|
|
663
|
+
border: 1px solid var(--caf-border); background: var(--caf-surface); color: var(--caf-text);
|
|
664
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
631
665
|
}
|
|
632
666
|
.caf-btn:hover { filter: brightness(0.97); }
|
|
633
667
|
.caf-btn:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
@@ -646,16 +680,19 @@ const BASE_CSS = `
|
|
|
646
680
|
.caf-line-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; }
|
|
647
681
|
.caf-line-last { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--caf-accent); }
|
|
648
682
|
.caf-donut { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
|
|
649
|
-
|
|
683
|
+
/* -3px optical correction: a circle beside flush-left text reads indented. */
|
|
684
|
+
.caf-donut-svg { width: 110px; height: 110px; flex: none; transform: rotate(0.001deg); margin-left: -3px; }
|
|
650
685
|
.caf-donut-seg { fill: none; stroke-width: 6; }
|
|
651
|
-
|
|
686
|
+
/* Capped width keeps each value NEXT TO its label in wide cards — the pair
|
|
687
|
+
must read as a pair regardless of container width. */
|
|
688
|
+
.caf-donut-legend { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; flex: 1; max-width: 260px; }
|
|
652
689
|
.caf-legend-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.84rem; }
|
|
653
690
|
.caf-legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
|
654
691
|
.caf-legend-label { color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
655
692
|
.caf-legend-value { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
656
693
|
.caf-timer { align-items: center; text-align: center; }
|
|
657
694
|
.caf-timer .caf-bar-track { width: 100%; }
|
|
658
|
-
.caf-timer-time { font-size:
|
|
695
|
+
.caf-timer-time { font-size: var(--caf-num-primary); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; color: var(--caf-accent); }
|
|
659
696
|
.caf-timer-done { color: var(--caf-accent); }
|
|
660
697
|
.caf-timer-fill { transition: width 0.9s linear; }
|
|
661
698
|
.caf-timer-controls { display: flex; gap: 0.6rem; }
|
|
@@ -672,8 +709,11 @@ const BASE_CSS = `
|
|
|
672
709
|
.caf-msg-tools { align-self: flex-start; font-size: 0.74rem; color: var(--caf-muted); padding: 0.15rem 0.55rem; border: 1px solid var(--caf-border); border-radius: 999px; }
|
|
673
710
|
.caf-msg-error { align-self: stretch; max-width: none; display: flex; flex-direction: column; gap: 0.2rem; background: transparent; border: 1px solid var(--caf-danger); }
|
|
674
711
|
.caf-msg-error strong { color: var(--caf-danger); font-size: 0.85rem; }
|
|
675
|
-
.caf-chat-input {
|
|
676
|
-
|
|
712
|
+
.caf-chat-input {
|
|
713
|
+
display: flex; gap: 0.6rem; margin-top: 1rem; padding: 0.6rem;
|
|
714
|
+
background: var(--caf-surface); border: 1px solid var(--caf-border); border-radius: var(--caf-radius);
|
|
715
|
+
}
|
|
716
|
+
.caf-chat-input input { flex: 1; font: inherit; font-size: 0.95rem; padding: 0.55rem 0.7rem; border-radius: var(--caf-control-radius); border: 1px solid var(--caf-border); background: var(--caf-sunken); color: var(--caf-text); }
|
|
677
717
|
.caf-chat-input input:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
678
718
|
.caf-search {
|
|
679
719
|
font: inherit; font-size: 0.9rem; width: 100%;
|
|
@@ -687,7 +727,7 @@ const BASE_CSS = `
|
|
|
687
727
|
.caf-row-wrap { display: flex; align-items: center; gap: 0.4rem; }
|
|
688
728
|
.caf-row-wrap .caf-row { flex: 1; min-width: 0; }
|
|
689
729
|
.caf-row-actions { display: flex; gap: 0.35rem; flex: none; padding-right: 0.3rem; }
|
|
690
|
-
.caf-btn-sm { font-size: 0.78rem; padding:
|
|
730
|
+
.caf-btn-sm { font-size: 0.78rem; padding: calc(var(--caf-btn-pad-y) * 0.67) calc(var(--caf-btn-pad-x) * 0.75); }
|
|
691
731
|
.caf-items { display: flex; flex-direction: column; gap: 0.5rem; border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
692
732
|
.caf-item-row { display: flex; align-items: flex-end; gap: 0.5rem; }
|
|
693
733
|
.caf-item-row .caf-field { flex: 1; min-width: 0; }
|
|
@@ -719,17 +759,24 @@ const BASE_CSS = `
|
|
|
719
759
|
}
|
|
720
760
|
.caf-tab:hover { color: var(--caf-text); }
|
|
721
761
|
.caf-tab:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: -2px; }
|
|
722
|
-
|
|
762
|
+
/* :hover on the active tab must not win the specificity race and mute it. */
|
|
763
|
+
.caf-tab-active, .caf-tab-active:hover { color: var(--caf-accent); border-bottom-color: var(--caf-accent); }
|
|
723
764
|
.caf-tabs-body { display: flex; flex-direction: column; gap: 0.8rem; }
|
|
724
765
|
.caf-collapse { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
|
|
725
766
|
.caf-collapse-head {
|
|
726
|
-
|
|
767
|
+
/* Same treatment as .caf-block-title: a collapsible header IS a card title. */
|
|
768
|
+
font: inherit; font-size: 0.78rem; font-weight: 650; text-transform: uppercase;
|
|
769
|
+
letter-spacing: 0.06em; color: var(--caf-muted); cursor: pointer;
|
|
727
770
|
display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
|
|
728
|
-
padding: 0.7rem 1rem; border: 1px solid var(--caf-border); border-radius: var(--caf-radius);
|
|
729
|
-
background: var(--caf-surface);
|
|
771
|
+
padding: 0.7rem 1.1rem; border: 1px solid var(--caf-border); border-radius: var(--caf-radius);
|
|
772
|
+
background: var(--caf-surface); width: 100%; text-align: left;
|
|
730
773
|
}
|
|
774
|
+
.caf-collapse-head { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); }
|
|
775
|
+
.caf-collapse-head:hover { filter: brightness(0.97); }
|
|
731
776
|
.caf-collapse-head:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
732
|
-
|
|
777
|
+
/* Accent chevron: the one interaction cue that distinguishes a collapsible
|
|
778
|
+
header from a static card title without breaking their shared typography. */
|
|
779
|
+
.caf-chevron { transition: transform 0.15s ease; color: var(--caf-accent); }
|
|
733
780
|
.caf-chevron-closed { transform: rotate(-90deg); }
|
|
734
781
|
.caf-block-fill { border: none; background: transparent; padding: 0; overflow: auto; min-height: 320px; }
|
|
735
782
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
package/src/blocks.js
CHANGED
|
@@ -464,7 +464,7 @@ export const BLOCKS = {
|
|
|
464
464
|
chart: ChartBlock,
|
|
465
465
|
timer: TimerBlock,
|
|
466
466
|
actions: ActionsBlock,
|
|
467
|
-
chat: ({ spec, ctx }) => h(ChatPanel, { cfg: spec.chat, ctx }),
|
|
467
|
+
chat: ({ spec, ctx }) => h(ChatPanel, { cfg: spec.chat, ctx, title: spec.title }),
|
|
468
468
|
custom: CustomBlock,
|
|
469
469
|
};
|
|
470
470
|
|
package/src/chat.js
CHANGED
|
@@ -171,7 +171,7 @@ function Bubble({ m }) {
|
|
|
171
171
|
// The embeddable core: everything the chat layout does, as a single column
|
|
172
172
|
// that can live in a workspace sidebar (or any block slot) as well as fill
|
|
173
173
|
// the whole app. One conversation per app — it persists under data.chat.
|
|
174
|
-
export function ChatPanel({ cfg, ctx }) {
|
|
174
|
+
export function ChatPanel({ cfg, ctx, title }) {
|
|
175
175
|
const c = cfg;
|
|
176
176
|
const [draft, setDraft] = useState("");
|
|
177
177
|
const [busy, setBusy] = useState(false);
|
|
@@ -250,6 +250,9 @@ export function ChatPanel({ cfg, ctx }) {
|
|
|
250
250
|
h(
|
|
251
251
|
"div",
|
|
252
252
|
{ className: "caf-block caf-chat-log" },
|
|
253
|
+
// Every other block opens with its title label — a chat card that
|
|
254
|
+
// starts straight at a bubble was the one heading-less block on screen.
|
|
255
|
+
title ? h("h2", { className: "caf-block-title" }, title) : null,
|
|
253
256
|
c.greeting
|
|
254
257
|
? h("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } })
|
|
255
258
|
: null,
|