@cueplusplus/ui 0.11.1 → 0.12.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.
@@ -4,6 +4,7 @@ import { useIsomorphicLayoutEffect } from "../system/use-isomorphic-layout-effec
4
4
  import { AmbientThemeContext, DensityContext } from "../system/density.js";
5
5
  import { OverridesContext } from "../system/overrides.js";
6
6
  import { ThemeContext } from "../system/theme-provider.js";
7
+ import { GROUND_CLASSES } from "./_ground.js";
7
8
  import * as React from "react";
8
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
10
  //#region src/layout/preview.tsx
@@ -20,16 +21,29 @@ const BIG_STEP = 64;
20
21
  const ROOT_CLASS = "relative flex min-w-0 flex-col rounded-(--radius-surface) border border-border bg-surface-1 text-fg";
21
22
  const BENCH_CLASS = "relative flex min-w-0 flex-1 overflow-auto rounded-t-(--radius-surface)";
22
23
  /**
23
- * The stage: centred, dotted in the stage's own `--cue-hair-strong`, and at
24
- * least three top rungs of the space ladder tall so a preview reserves its room
25
- * before its specimen arrives. Static literals, because Tailwind scans text.
24
+ * The stage: centred, grounded, and at least three top rungs of the space
25
+ * ladder tall so a preview reserves its room before its specimen arrives.
26
+ * Static literals, because Tailwind scans text.
26
27
  *
27
28
  * Centred **safely** (`place-items: safe center`): a specimen wider than the
28
29
  * stage starts at the stage's edge and overflows at its end, where the bench
29
30
  * scrolls to it. Plain `center` split the overflow across both sides, and the
30
31
  * left half sat before the scroll origin, where nothing reaches it.
32
+ *
33
+ * **Three pieces rather than one literal, and the seam is where the ground
34
+ * goes.** `cn` preserves the order it is given, so a ground appended after the
35
+ * padding would emit the dot grid in a different place from the one every
36
+ * caller's markup has today — and `/`'s seventeen bare Previews and the group
37
+ * pages' 325 tooled ones are both measured to the byte. Head, ground, tail, in
38
+ * that order, is today's string.
31
39
  */
32
- const STAGE_CLASS = "grid h-full w-full min-h-[calc(var(--cue-space-8)*3)] place-items-center-safe bg-surface-1 bg-[radial-gradient(circle,var(--cue-hair-strong)_1px,transparent_1.3px)] bg-[length:14px_14px] p-(--cue-space-6) text-fg";
40
+ const STAGE_HEAD = "grid h-full w-full min-h-[calc(var(--cue-space-8)*3)] place-items-center-safe";
41
+ const STAGE_TAIL = "p-(--cue-space-6) text-fg";
42
+ /** Which ground a presentation paints when the caller names none (spec §4.1). */
43
+ const DEFAULT_GROUND = {
44
+ panel: "dots",
45
+ frame: "none"
46
+ };
33
47
  const TOOLS_CLASS = "absolute top-(--cue-space-2) right-(--cue-space-2) z-10 flex items-center gap-px rounded-(--radius-control) border border-border bg-surface-2 p-px";
34
48
  /**
35
49
  * The room the tools take, kept clear on the stage — only when there are tools.
@@ -52,6 +66,79 @@ const TOOLS_CLASS = "absolute top-(--cue-space-2) right-(--cue-space-2) z-10 fle
52
66
  const TOOLS_RESERVE_BENCH = "[--preview-tools-clear:calc(var(--cue-space-2)*2+var(--cue-control-sm)+4px)]";
53
67
  const TOOLS_RESERVE_STAGE = "pt-(--preview-tools-clear)";
54
68
  const FOOTER_CLASS = "flex min-w-0 items-center gap-(--cue-space-2) border-t border-border px-(--cue-space-4) py-(--cue-space-2)";
69
+ /**
70
+ * A frame's root: no card, no border, no fill. The frame is a thing on a
71
+ * canvas, and the canvas is what paints. `shrink-0` because a canvas is a flex
72
+ * row that scrolls, and a frame that shrank to fit would defeat the scrolling.
73
+ */
74
+ const FRAME_ROOT_CLASS = "relative flex min-w-0 shrink-0 flex-col gap-(--cue-space-2) text-fg";
75
+ /**
76
+ * A frame's footer: the same row as a panel's, without the rule across the top
77
+ * or the card's horizontal padding — there is no card for either to belong to.
78
+ */
79
+ const FRAME_FOOTER_CLASS = "flex min-w-0 items-center gap-(--cue-space-2) text-(length:--cue-text-micro) text-fg-subtle";
80
+ /** The row above a frame's stage: the name (or its control), the count, and the tools. */
81
+ const FRAME_LABEL_CLASS = "flex min-w-0 items-center gap-(--cue-space-2) text-(length:--cue-text-label) text-fg-muted";
82
+ /** The name when the caller passed no control for it — the frame's `label`, as text. */
83
+ const FRAME_NAME_CLASS = "min-w-0 truncate font-mono text-(length:--cue-text-label) text-fg";
84
+ /** The count beside it: quieter, and never allowed to shrink the name's truncation away. */
85
+ const FRAME_COUNT_CLASS = "shrink-0 font-mono text-(length:--cue-text-micro) text-fg-subtle";
86
+ /**
87
+ * A frame's tools: pushed to the row's end, and carrying none of the panel's
88
+ * overlay chrome. A frame's stage is transparent, so a toolbar drawn over it
89
+ * would float on the canvas ground with nothing behind it (spec §4.1 Decision 2).
90
+ */
91
+ const FRAME_TOOLS_CLASS = "ml-auto flex shrink-0 items-center gap-px";
92
+ /**
93
+ * The box that holds the bench and the four marks.
94
+ *
95
+ * One more element than the spec's Decision 3 priced, and it is not avoidable.
96
+ * The bench is `overflow-auto`: a mark positioned inside it is clipped at the
97
+ * padding box and scrolls with the content. The frame's root is no good either
98
+ * — its first child is the label row, so the root's top-left corner is the
99
+ * label's and not the stage's. So the frame gets one `relative` box whose
100
+ * borders are exactly the stage's, and the marks hang off that.
101
+ */
102
+ const FRAME_BOX_CLASS = "relative flex min-w-0 flex-col";
103
+ /**
104
+ * Captions off, as properties the item reads rather than a prop each item takes.
105
+ *
106
+ * A reader turns captions off for a whole frame from its settings popover,
107
+ * never for one item — so the switch is on the frame and the item obeys it
108
+ * through inheritance.
109
+ *
110
+ * **`opacity: 0`, and deliberately not `visibility: hidden`.** §4.1 Decision 4
111
+ * refuses "removing a hidden caption from the accessibility tree", and
112
+ * `visibility: hidden` does exactly that — a browser drops such a node as
113
+ * surely as it drops a `display: none` one. Zero opacity keeps the box, so
114
+ * nothing re-flows, *and* keeps the caption announced, which is what the
115
+ * refusal is for: the caption is what names the specimen for a reader who
116
+ * cannot see the difference between two of them.
117
+ *
118
+ * **Two properties, declared together here so the pair is never half-applied.**
119
+ * Custom properties are untyped text: `opacity` wants a number and
120
+ * `pointer-events` wants a keyword, so one property cannot carry both. The
121
+ * second is what stops an unpainted caption being clicked, or a drag across the
122
+ * stage selecting words nobody can see.
123
+ */
124
+ const CAPTIONS_OFF = "[--preview-captions:0] [--preview-captions-pe:none]";
125
+ /**
126
+ * One corner mark: an 11×11 box with a one-pixel cross drawn as two linear
127
+ * gradients — a vertical hairline and a horizontal one — so the mark is a
128
+ * background and not four more elements.
129
+ *
130
+ * Not a border and not an SVG. A border would have to be on the stage, and the
131
+ * stage is the theme island: a frame's chrome that moved with a stage theme is
132
+ * the one thing ruling 7 forbids.
133
+ */
134
+ const CROSS_CLASS = "pointer-events-none absolute z-10 size-[11px] bg-[linear-gradient(var(--cue-fg-subtle),var(--cue-fg-subtle)),linear-gradient(var(--cue-fg-subtle),var(--cue-fg-subtle))] bg-[length:1px_11px,11px_1px] bg-[position:center,center] bg-no-repeat";
135
+ /** The four corners, each half outside the box so the cross straddles the corner. */
136
+ const CROSS_CORNERS = [
137
+ "-top-[5px] -left-[5px]",
138
+ "-top-[5px] -right-[5px]",
139
+ "-bottom-[5px] -left-[5px]",
140
+ "-bottom-[5px] -right-[5px]"
141
+ ];
55
142
  const GRIP_FOCUS = "touch-none outline-none hover:bg-(--cue-border-strong) focus-visible:bg-accent";
56
143
  const GRIP_X_CLASS = `absolute top-0 right-0 z-10 h-full w-(--cue-space-2) cursor-col-resize ${GRIP_FOCUS}`;
57
144
  const GRIP_Y_CLASS = `absolute bottom-0 left-0 z-10 h-(--cue-space-2) w-full cursor-row-resize ${GRIP_FOCUS}`;
@@ -357,9 +444,18 @@ function Grips({ label, options, size }) {
357
444
  ] });
358
445
  }
359
446
  /**
360
- * A frame for one component: a stage that is its own theme island, a slot for
447
+ * A card for one component: a stage that is its own theme island, a slot for
361
448
  * tools at the top right, and a footer.
362
449
  *
450
+ * **Two presentations, one stage.** `"panel"`, the default and what every
451
+ * existing caller gets, is that card: a border, a fill, a dot grid under the
452
+ * specimen, and the tools floating over the stage's top right. `"frame"` is a
453
+ * design-canvas frame — no card, no border, no fill; the name, the count and
454
+ * the tools sit in a row *above* the stage rather than over it, four crosshairs
455
+ * mark the stage's outer corners, and the stage paints nothing by default, so
456
+ * whatever canvas the frame sits on shows through. Everything below holds in
457
+ * both.
458
+ *
363
459
  * **The stage is the island; the chrome is not.** `theme`, `mode`, `density`
364
460
  * and `font` land on the stage element only, and the stage republishes the
365
461
  * theme and density contexts so an overlay opened on it follows it. The tools,
@@ -375,7 +471,7 @@ function Grips({ label, options, size }) {
375
471
  * that is under `maxWidth`. Not `Resizable`, which is split panes on an
376
472
  * optional peer.
377
473
  *
378
- * Props, not a compound: one stage and two slots, each saying what it is.
474
+ * Props, not a compound: one stage and its slots, each saying what it is.
379
475
  *
380
476
  * @example
381
477
  * <Preview label="Button preview" tools={<IconButton aria-label="Configure Button" icon={Wrench} />}>
@@ -385,8 +481,12 @@ function Grips({ label, options, size }) {
385
481
  * <Preview label="Chip preview" theme="terminal" density="compact" resizable>
386
482
  * <Chip tone="accent">Standby</Chip>
387
483
  * </Preview>
484
+ * @example
485
+ * <Preview label="Button" presentation="frame" count={3}>
486
+ * <Button variant="primary">Take cue</Button>
487
+ * </Preview>
388
488
  */
389
- const Preview = React.forwardRef(function Preview({ children, label, theme, mode, density, font, resizable, tools, footer, className, ...elementProps }, ref) {
489
+ const Preview = React.forwardRef(function Preview({ children, label, theme, mode, density, font, resizable, tools, footer, presentation = "panel", name, count, ground, captions = true, className, ...elementProps }, ref) {
390
490
  const outer = React.useContext(ThemeContext);
391
491
  const bench = React.useRef(null);
392
492
  const given = resolveResize(resizable);
@@ -399,10 +499,11 @@ const Preview = React.forwardRef(function Preview({ children, label, theme, mode
399
499
  density,
400
500
  font
401
501
  }, outer);
502
+ const framed = presentation === "frame";
402
503
  const stage = /* @__PURE__ */ jsx("div", {
403
504
  "data-slot": "preview-stage",
404
505
  ...attributes,
405
- className: cn(STAGE_CLASS, options !== null && "flex-1", tools !== void 0 && TOOLS_RESERVE_STAGE),
506
+ className: cn(STAGE_HEAD, GROUND_CLASSES[ground ?? DEFAULT_GROUND[presentation]], STAGE_TAIL, options !== null && "flex-1", !framed && tools !== void 0 && TOOLS_RESERVE_STAGE, !captions && CAPTIONS_OFF),
406
507
  style: size.height === null ? void 0 : { minHeight: 0 },
407
508
  children: /* @__PURE__ */ jsx(StageScope, {
408
509
  theme,
@@ -411,6 +512,75 @@ const Preview = React.forwardRef(function Preview({ children, label, theme, mode
411
512
  children
412
513
  })
413
514
  });
515
+ const benched = /* @__PURE__ */ jsx("div", {
516
+ ref: bench,
517
+ "data-slot": "preview-bench",
518
+ className: cn(BENCH_CLASS, footer === void 0 && "rounded-b-(--radius-surface)", !framed && tools !== void 0 && TOOLS_RESERVE_BENCH),
519
+ children: options === null ? stage : /* @__PURE__ */ jsxs("div", {
520
+ ref: size.sizer,
521
+ "data-slot": "preview-sizer",
522
+ className: cn("relative flex flex-col", size.width === null && "w-full"),
523
+ style: {
524
+ width: size.width === null ? void 0 : `${size.shownWidth}px`,
525
+ height: size.height === null ? void 0 : `${size.shownHeight}px`,
526
+ minWidth: `${options.minWidth}px`,
527
+ maxWidth: `${options.maxWidth}px`,
528
+ minHeight: `${options.minHeight}px`,
529
+ maxHeight: `${options.maxHeight}px`
530
+ },
531
+ children: [stage, /* @__PURE__ */ jsx(Grips, {
532
+ label,
533
+ options,
534
+ size
535
+ })]
536
+ })
537
+ });
538
+ if (framed) return /* @__PURE__ */ jsxs("div", {
539
+ ...elementProps,
540
+ ref,
541
+ role: "group",
542
+ "aria-label": label,
543
+ "data-slot": "preview",
544
+ "data-presentation": "frame",
545
+ className: cn(FRAME_ROOT_CLASS, options !== null && "w-full", className),
546
+ children: [
547
+ /* @__PURE__ */ jsxs("div", {
548
+ "data-slot": "preview-frame-label",
549
+ className: FRAME_LABEL_CLASS,
550
+ children: [
551
+ name === void 0 ? /* @__PURE__ */ jsx("span", {
552
+ "data-slot": "preview-name",
553
+ className: FRAME_NAME_CLASS,
554
+ children: label
555
+ }) : name,
556
+ count === void 0 ? null : /* @__PURE__ */ jsx("span", {
557
+ "data-slot": "preview-count",
558
+ className: FRAME_COUNT_CLASS,
559
+ children: count
560
+ }),
561
+ tools === void 0 ? null : /* @__PURE__ */ jsx("div", {
562
+ "data-slot": "preview-tools",
563
+ className: FRAME_TOOLS_CLASS,
564
+ children: tools
565
+ })
566
+ ]
567
+ }),
568
+ /* @__PURE__ */ jsxs("div", {
569
+ "data-slot": "preview-frame",
570
+ className: FRAME_BOX_CLASS,
571
+ children: [benched, CROSS_CORNERS.map((corner) => /* @__PURE__ */ jsx("span", {
572
+ "aria-hidden": "true",
573
+ "data-slot": "preview-crosshair",
574
+ className: cn(CROSS_CLASS, corner)
575
+ }, corner))]
576
+ }),
577
+ footer === void 0 ? null : /* @__PURE__ */ jsx("div", {
578
+ "data-slot": "preview-footer",
579
+ className: FRAME_FOOTER_CLASS,
580
+ children: footer
581
+ })
582
+ ]
583
+ });
414
584
  return /* @__PURE__ */ jsxs("div", {
415
585
  ...elementProps,
416
586
  ref,
@@ -419,29 +589,7 @@ const Preview = React.forwardRef(function Preview({ children, label, theme, mode
419
589
  "data-slot": "preview",
420
590
  className: cn(ROOT_CLASS, options !== null && "w-full", className),
421
591
  children: [
422
- /* @__PURE__ */ jsx("div", {
423
- ref: bench,
424
- "data-slot": "preview-bench",
425
- className: cn(BENCH_CLASS, footer === void 0 && "rounded-b-(--radius-surface)", tools !== void 0 && TOOLS_RESERVE_BENCH),
426
- children: options === null ? stage : /* @__PURE__ */ jsxs("div", {
427
- ref: size.sizer,
428
- "data-slot": "preview-sizer",
429
- className: cn("relative flex flex-col", size.width === null && "w-full"),
430
- style: {
431
- width: size.width === null ? void 0 : `${size.shownWidth}px`,
432
- height: size.height === null ? void 0 : `${size.shownHeight}px`,
433
- minWidth: `${options.minWidth}px`,
434
- maxWidth: `${options.maxWidth}px`,
435
- minHeight: `${options.minHeight}px`,
436
- maxHeight: `${options.maxHeight}px`
437
- },
438
- children: [stage, /* @__PURE__ */ jsx(Grips, {
439
- label,
440
- options,
441
- size
442
- })]
443
- })
444
- }),
592
+ benched,
445
593
  tools === void 0 ? null : /* @__PURE__ */ jsx("div", {
446
594
  "data-slot": "preview-tools",
447
595
  className: TOOLS_CLASS,
@@ -104,7 +104,7 @@ const Drawer$1 = {
104
104
  children: [grabHandle ? /* @__PURE__ */ jsx("div", {
105
105
  "data-slot": "drawer-grab-handle",
106
106
  "aria-hidden": "true",
107
- className: cn("shrink-0 rounded-(--radius-full) bg-border-strong", vertical ? "mx-auto my-(--cue-space-2) h-1 w-9" : "my-auto mx-(--cue-space-2) h-9 w-1")
107
+ className: cn("shrink-0 rounded-full bg-border-strong", vertical ? "mx-auto my-(--cue-space-2) h-1 w-9" : "my-auto mx-(--cue-space-2) h-9 w-1")
108
108
  }) : null, /* @__PURE__ */ jsx(Drawer.Content, {
109
109
  "data-slot": "drawer-content",
110
110
  className: "flex min-h-0 flex-1 flex-col gap-(--cue-space-4) p-(--cue-space-6)",
@@ -30,7 +30,7 @@ import * as React from "react";
30
30
  * <span className={chipVariants({ variant: "tag" })}>mcp</span>
31
31
  */
32
32
  declare const chipVariants: (props?: ({
33
- tone?: "info" | "warn" | "danger" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
33
+ tone?: "danger" | "info" | "warn" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
34
34
  variant?: "outline" | "tinted" | "tag" | null | undefined;
35
35
  interactive?: boolean | null | undefined;
36
36
  } & ClassProp) | undefined) => string;
@@ -254,7 +254,7 @@
254
254
  "--cue-surface-1",
255
255
  "--cue-surface-2",
256
256
  "--cue-text-emphasis",
257
- "--cue-text-meta"
257
+ "--cue-text-micro"
258
258
  ],
259
259
  "summary": "The unequal grid: tiles of different sizes on one set of tracks, from a closed set of spans.",
260
260
  "examples": [
@@ -0,0 +1,210 @@
1
+ {
2
+ "name": "Frames",
3
+ "slug": "frames",
4
+ "group": "layout",
5
+ "importPath": "@cueplusplus/ui",
6
+ "peerDependencies": [],
7
+ "clientOnly": true,
8
+ "description": "A design canvas: frames side by side, each holding one axis of one component.\n\n`Frames.Root` is the canvas — short, wide, scrollable sideways, focusable,\nand pannable with the pointer. `Frames.Item` is one captioned cell, and it is\nused **inside a `Preview`** rather than inside the Root. The two are apart\nbecause that is how they are used, and a props-only API cannot say it.\n\n**The canvas knows nothing about its children.** It does not count them, does\nnot index them and does not select one; `hint` is where a caller puts what it\nknows. A canvas that knew its frames would be a manifest API, and this is a\nslot API.",
9
+ "props": [],
10
+ "typeReferences": [],
11
+ "subcomponents": [
12
+ "Root",
13
+ "Item"
14
+ ],
15
+ "parts": [
16
+ {
17
+ "name": "Root",
18
+ "usage": "Frames.Root",
19
+ "description": "The canvas: a short, wide, focusable scroll region that holds frames.\n\n**It knows nothing about its children.** It does not count them, index them\nor select one — a canvas that knew its frames would be a manifest API, and\nthis is a slot API. `hint` is where a caller puts the count it knows and this\ndoes not.",
20
+ "props": [
21
+ {
22
+ "name": "children",
23
+ "type": "ReactNode",
24
+ "required": true,
25
+ "defaultValue": null,
26
+ "description": "The frames. Anything — this never inspects them.",
27
+ "control": {
28
+ "kind": "node",
29
+ "fixture": "children.text"
30
+ }
31
+ },
32
+ {
33
+ "name": "label",
34
+ "type": "string",
35
+ "required": true,
36
+ "defaultValue": null,
37
+ "description": "The canvas's accessible name — \"Button frames\". Required: the canvas is a\nfocusable scroll region, and a scroll region with no name is one a screen\nreader announces as nothing.",
38
+ "control": {
39
+ "kind": "string"
40
+ }
41
+ },
42
+ {
43
+ "name": "height",
44
+ "type": "\"short\" | \"tall\"",
45
+ "required": false,
46
+ "defaultValue": "short",
47
+ "description": "`\"short\"` is 18.5rem, `\"tall\"` 27rem. A CSS length in `style` overrides both.",
48
+ "control": {
49
+ "kind": "enum",
50
+ "members": [
51
+ "short",
52
+ "tall"
53
+ ]
54
+ }
55
+ },
56
+ {
57
+ "name": "ground",
58
+ "type": "\"none\" | \"plain\" | \"dots\"",
59
+ "required": false,
60
+ "defaultValue": "dots",
61
+ "description": "The canvas ground. Defaults to `\"dots\"`.",
62
+ "control": {
63
+ "kind": "enum",
64
+ "members": [
65
+ "none",
66
+ "plain",
67
+ "dots"
68
+ ]
69
+ }
70
+ },
71
+ {
72
+ "name": "hint",
73
+ "type": "ReactNode",
74
+ "required": false,
75
+ "defaultValue": null,
76
+ "description": "The line in the bottom-right corner — \"drag to pan · 8 frames\". Unset, none is drawn.",
77
+ "control": {
78
+ "kind": "node",
79
+ "fixture": "children.text"
80
+ }
81
+ },
82
+ {
83
+ "name": "pan",
84
+ "type": "boolean",
85
+ "required": false,
86
+ "defaultValue": "true",
87
+ "description": "Drag-to-pan with the pointer. Defaults to `true`. A drag that starts on a\ncontrol is never a pan, so a frame's own controls stay usable — and \"on a\ncontrol\" counts the control's own contents: the words inside a\n`role=\"menuitem\"` row, the icon inside a `button`. A drag inside a\nrole-bearing box that is not itself a control — a `role=\"group\"` frame, a\n`role=\"region\"` — still pans, because a frame is such a box and a board is\nmostly frames. Only the primary button drags; `false` binds no pointer\nlistener at all; and the keyboard scrolls the canvas either way.",
88
+ "control": {
89
+ "kind": "boolean"
90
+ }
91
+ }
92
+ ],
93
+ "typeReferences": [
94
+ "Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\">"
95
+ ]
96
+ },
97
+ {
98
+ "name": "Item",
99
+ "usage": "Frames.Item",
100
+ "description": "One captioned cell of a frame.\n\n**It is used inside a `Preview`, not inside `Frames.Root`** — that is the\nwhole reason this is a compound rather than a props API: the canvas holds\nframes, a frame holds a stage, and the stage holds these. `Frames.Root` never\nlooks for one.\n\n**A stage does not arrange them, and the caller does.** A stage is a centred\ngrid in both presentations, so two items left to it stack; the row every\nexample here draws is `<div className=\"flex flex-wrap items-end gap-(--cue-space-6)\">`,\nand `items-end` is what lines the captions up when the specimens are\ndifferent heights.",
101
+ "props": [
102
+ {
103
+ "name": "children",
104
+ "type": "ReactNode",
105
+ "required": true,
106
+ "defaultValue": null,
107
+ "description": "The specimen.",
108
+ "control": {
109
+ "kind": "node",
110
+ "fixture": "children.text"
111
+ }
112
+ },
113
+ {
114
+ "name": "caption",
115
+ "type": "ReactNode",
116
+ "required": false,
117
+ "defaultValue": null,
118
+ "description": "The line under it. Hidden — not removed — when the frame sets `captions={false}`.",
119
+ "control": {
120
+ "kind": "node",
121
+ "fixture": "children.text"
122
+ }
123
+ },
124
+ {
125
+ "name": "selected",
126
+ "type": "boolean",
127
+ "required": false,
128
+ "defaultValue": "false",
129
+ "description": "Draws the selection outline the frame gives the item a reader is inspecting.",
130
+ "control": {
131
+ "kind": "boolean"
132
+ }
133
+ }
134
+ ],
135
+ "typeReferences": [
136
+ "React.ComponentPropsWithoutRef<\"div\">"
137
+ ]
138
+ }
139
+ ],
140
+ "variants": {},
141
+ "defaultVariants": {},
142
+ "tokensUsed": [
143
+ "--cue-accent",
144
+ "--cue-border",
145
+ "--cue-fg-subtle",
146
+ "--cue-font-mono",
147
+ "--cue-hair-strong",
148
+ "--cue-radius-control",
149
+ "--cue-radius-surface",
150
+ "--cue-space-2",
151
+ "--cue-space-3",
152
+ "--cue-space-4",
153
+ "--cue-space-6",
154
+ "--cue-space-8",
155
+ "--cue-surface-1",
156
+ "--cue-text-micro"
157
+ ],
158
+ "summary": "A design canvas: frames side by side, each holding one axis of one component. Short, wide, scrollable sideways, focusable, and pannable with the pointer.",
159
+ "examples": [
160
+ {
161
+ "title": "A short board that pans",
162
+ "code": "<Frames.Root label=\"Button frames\" hint=\"drag to pan · 2 frames\">\n <Preview presentation=\"frame\" label=\"Tone\" count={2}>\n <div className=\"flex flex-wrap items-end gap-(--cue-space-6)\">\n <Frames.Item caption=\"accent\">\n <Button variant=\"primary\">Take cue</Button>\n </Frames.Item>\n <Frames.Item caption=\"default\">\n <Button>Take cue</Button>\n </Frames.Item>\n </div>\n </Preview>\n</Frames.Root>",
163
+ "language": "tsx"
164
+ },
165
+ {
166
+ "title": "Tall, flat, and still",
167
+ "code": "<Frames.Root label=\"Grid frames\" height=\"tall\" ground=\"plain\" pan={false}>\n <Preview presentation=\"frame\" label=\"Columns\" count={3} captions={false}>\n …\n </Preview>\n</Frames.Root>",
168
+ "language": "tsx"
169
+ },
170
+ {
171
+ "title": "Usage",
172
+ "code": "<Frames.Root label=\"Button frames\" hint=\"drag to pan · 3 frames\">\n <Preview presentation=\"frame\" label=\"Tone\" count={3} name={<ToneMenu />}>\n <div className=\"flex flex-wrap items-end gap-(--cue-space-6)\">\n <Frames.Item caption=\"accent\"><Button tone=\"accent\">Take cue</Button></Frames.Item>\n <Frames.Item caption=\"ok\"><Button tone=\"ok\">Take cue</Button></Frames.Item>\n <Frames.Item caption=\"warn\"><Button tone=\"warn\">Take cue</Button></Frames.Item>\n </div>\n </Preview>\n</Frames.Root>",
173
+ "language": "tsx"
174
+ }
175
+ ],
176
+ "status": "stable",
177
+ "url": "/docs/components/frames",
178
+ "mdUrl": "/docs/components/frames.md",
179
+ "jsonUrl": "/r/components/frames.json",
180
+ "whenToUse": [
181
+ "Showing a component's axes at once — one frame per axis, each with its own name row and its own settings — instead of one preview a reader has to re-configure.",
182
+ "Any short, wide board of specimens a reader scans rather than reads: a theme sheet, a size ladder, a state matrix.",
183
+ "Beside `Preview presentation=\"frame\"`, which is what a frame on this canvas is."
184
+ ],
185
+ "whenNotToUse": [
186
+ "Laying components out in a page. That is `Grid` or `Bento`; a canvas is a viewport that scrolls, and everything in it is off-screen by design.",
187
+ "A board a reader zooms. This does not zoom, and it will not: a transformed subtree breaks every popover anchored inside it.",
188
+ "Scrolling down. The canvas is `overflow-y: hidden` on purpose — it is short and wide, not a second page."
189
+ ],
190
+ "commonMistakes": [
191
+ "Putting `Frames.Item` inside `Frames.Root`. The item belongs on a frame's stage — `Frames.Root` holds frames, and a frame is a `Preview`.",
192
+ "Expecting a frame to arrange its items. A stage is a centred grid in both presentations, so two items stack; put them in a row yourself — the row is `<div className=\"flex flex-wrap items-end gap-(--cue-space-6)\">`, and every example here uses exactly that one.",
193
+ "Leaving `label` off, or repeating the page's heading in it. The canvas is a focusable scroll region, and its name is what a screen reader announces when a reader tabs into it.",
194
+ "Expecting the canvas to count its frames. It never inspects its children — pass the count yourself, in `hint`.",
195
+ "Expecting a long `caption` to wrap or to ellipsise on its own. A cell does not shrink, so a caption longer than its specimen widens the cell instead, and the wrapping row it sits in wraps a cell sooner — the `truncate` on the caption has no narrower box to fire in. Bound the cell yourself when a caption can run long: `<Frames.Item className=\"max-w-[12rem]\" caption=\"…\">`.",
196
+ "Reading `captions={false}` as a way to take a caption out of the page. It is visual only — the caption goes to `opacity: 0` and stops taking pointer events, keeping its box so nothing re-flows and keeping its place in the accessibility tree, so a screen reader still reads it. Delete the `caption` prop if the words should not be there at all."
197
+ ],
198
+ "specimens": [
199
+ {
200
+ "title": "Frames",
201
+ "group": "layout",
202
+ "components": [
203
+ "Frames"
204
+ ],
205
+ "code": "<Frames.Root\n label=\"Button frames\"\n hint=\"drag to pan · 2 frames\"\n className=\"w-full\"\n>\n <Preview\n label=\"Button tone\"\n presentation=\"frame\"\n count={3}\n tools={<Chip variant=\"outline\">tone</Chip>}\n >\n <div className=\"flex flex-wrap items-end gap-(--cue-space-6)\">\n <Frames.Item caption=\"accent\" selected>\n <Button variant=\"primary\">Take cue</Button>\n </Frames.Item>\n <Frames.Item caption=\"default\">\n <Button>Take cue</Button>\n </Frames.Item>\n <Frames.Item caption=\"subtle\">\n <Button variant=\"ghost\">Take cue</Button>\n </Frames.Item>\n </div>\n </Preview>\n <Preview label=\"Button density\" presentation=\"frame\" count={2} captions={false}>\n <div className=\"flex flex-wrap items-end gap-(--cue-space-6)\">\n <Frames.Item caption=\"compact\">\n <Button size=\"sm\">Go</Button>\n </Frames.Item>\n <Frames.Item caption=\"normal\">\n <Button>Go</Button>\n </Frames.Item>\n </div>\n </Preview>\n</Frames.Root>",
206
+ "note": "A design canvas: frames side by side, each holding one axis of one component. The canvas is a focusable scroll region; a frame is a `Preview` in its `frame` presentation, with its name row above the stage and four crosshairs at the corners; an item is one captioned cell on that stage.",
207
+ "interaction": "the canvas scrolls sideways: click it and Left and Right step one rung of the space ladder, PageUp and PageDown a viewport, Home and End the two ends. Dragging it pans it — except a drag that starts on a control, which stays a click. `pan={false}` turns the dragging off and leaves the keyboard."
208
+ }
209
+ ]
210
+ }