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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-artifact-framework",
3
- "version": "0.8.1",
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
- { key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
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
- { key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
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 = () => reject(new Error(`failed to load ${src}`));
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
- { className: "caf-root" },
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
- .caf-panel { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
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.95rem; font-weight: 650; }
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
- .caf-field label { font-size: 0.78rem; font-weight: 600; color: var(--caf-muted); letter-spacing: 0.02em; }
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: 7px; border: 1px solid var(--caf-border);
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: flex; flex-direction: column; gap: 0.7rem; }
608
- .caf-stat { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
609
- .caf-stat-label { color: var(--caf-muted); font-size: 0.82rem; }
610
- .caf-stat-value { font-size: 1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
611
- .caf-stat-big { flex-direction: column; align-items: flex-start; gap: 0.15rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--caf-border); }
612
- .caf-stat-big .caf-stat-value { font-size: 1.9rem; font-weight: 680; letter-spacing: -0.02em; color: var(--caf-accent); }
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: 7px;
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
- .caf-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.3rem 0.3rem 0.5rem; }
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: 0.45rem 0.8rem; border-radius: 7px;
630
- border: 1px solid var(--caf-border); background: var(--caf-sunken); color: var(--caf-text);
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
- .caf-donut-svg { width: 110px; height: 110px; flex: none; transform: rotate(0.001deg); }
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
- .caf-donut-legend { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; flex: 1; }
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: 2.6rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
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 { display: flex; gap: 0.6rem; margin-top: 0.8rem; }
676
- .caf-chat-input input { flex: 1; font: inherit; font-size: 0.95rem; padding: 0.55rem 0.7rem; border-radius: 8px; border: 1px solid var(--caf-border); background: var(--caf-surface); color: var(--caf-text); }
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: 0.3rem 0.6rem; border-radius: 6px; }
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
- .caf-tab-active { color: var(--caf-accent); border-bottom-color: var(--caf-accent); }
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
- font: inherit; font-size: 0.95rem; font-weight: 650; cursor: pointer;
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); color: var(--caf-text); width: 100%; text-align: left;
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
- .caf-chevron { transition: transform 0.15s ease; color: var(--caf-muted); }
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,