division-ds 0.3.1 → 0.4.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.
@@ -1231,6 +1231,18 @@
1231
1231
  "type": "ReactNode",
1232
1232
  "description": "The key's name, for example \"Esc\" or \"⌘\"."
1233
1233
  },
1234
+ {
1235
+ "name": "size",
1236
+ "type": "'md' | 'sm'",
1237
+ "default": "'md'",
1238
+ "description": "md is 24px tall, for a cap beside text or in a menu row. sm is 20px tall, for a cap inside a 32px control: a small Button or a search field."
1239
+ },
1240
+ {
1241
+ "name": "cut",
1242
+ "type": "boolean",
1243
+ "default": "false",
1244
+ "description": "Cuts the bottom-right corner (5px at sm, 8px at md) so the cap nests in the corner of a bevelled control, its diagonal parallel to the control's. Only for a cap inside such a control."
1245
+ },
1234
1246
  {
1235
1247
  "name": "className",
1236
1248
  "type": "string",
@@ -1251,10 +1263,11 @@
1251
1263
  "dos": [
1252
1264
  "Use one Kbd per physical key, and put them side by side for a chord (keys pressed together).",
1253
1265
  "Put it next to the action it triggers, in the same row or the same tooltip.",
1254
- "Write the key name the way the keyboard prints it."
1266
+ "Write the key name the way the keyboard prints it.",
1267
+ "Inside a 32px control, use size=\"sm\"; inside a bevelled one (a Button), add cut and place the cap at the end, near the cut corner."
1255
1268
  ],
1256
1269
  "donts": [
1257
- "Don’t bevel it. Cut corners are for surfaces and controls, not 24px chrome.",
1270
+ "Don’t set cut on a cap that stands alone in text or a menu row. The cut answers a bevelled corner around it; without one it reads as a button.",
1258
1271
  "Don’t put a whole chord inside one Kbd (\"Ctrl+K\" in a single cap).",
1259
1272
  "Don’t use it for anything that isn’t a key. It’s not a badge."
1260
1273
  ],
@@ -1265,11 +1278,11 @@
1265
1278
  },
1266
1279
  {
1267
1280
  "part": "Hairline border",
1268
- "description": "A one-pixel border in border.hairline-strong, with no bevel (cut corners)."
1281
+ "description": "A one-pixel border in border.hairline-strong. Square corners by default; with cut, the bottom-right corner is cut at 45 degrees and the border follows the diagonal."
1269
1282
  },
1270
1283
  {
1271
1284
  "part": "Mono legend",
1272
- "description": "JetBrains Mono, uppercase, 11px (pixels): the tertiary type role."
1285
+ "description": "JetBrains Mono, uppercase, 11px (pixels): the tertiary type role, at both sizes."
1273
1286
  }
1274
1287
  ],
1275
1288
  "examples": [
@@ -1280,6 +1293,14 @@
1280
1293
  {
1281
1294
  "caption": "A chord: one cap per key",
1282
1295
  "source": "<span className=\"flex items-center gap-1\">\n <Kbd>Ctrl</Kbd>\n <span className=\"text-[var(--color-text-muted)] text-sm\">+</span>\n <Kbd>K</Kbd>\n</span>\n<span className=\"text-sm text-[var(--color-text-secondary)]\">Open the command palette</span>"
1296
+ },
1297
+ {
1298
+ "caption": "Sizes",
1299
+ "source": "<Kbd>Esc</Kbd>\n<Kbd size=\"sm\">Esc</Kbd>"
1300
+ },
1301
+ {
1302
+ "caption": "Small and cut, inside a small button",
1303
+ "source": "<Button variant=\"secondary\" size=\"sm\" iconStart={<Search />} aria-keyshortcuts=\"Control+K\">\n Search\n <Kbd aria-hidden=\"true\" size=\"sm\" cut className=\"-mr-1.5 ml-2 -translate-y-[var(--cap-shift,0px)]\">\n Ctrl K\n </Kbd>\n</Button>"
1283
1304
  }
1284
1305
  ],
1285
1306
  "closed": false
@@ -1503,6 +1524,155 @@
1503
1524
  ],
1504
1525
  "closed": false
1505
1526
  },
1527
+ {
1528
+ "name": "Panel",
1529
+ "title": "Panel",
1530
+ "tier": "atoms",
1531
+ "slug": "panel",
1532
+ "summary": "The system's framed surface: a thin border around a fill, with the crosshair corner marks and the cut corner as options. Card is a Panel with a title, body and footer; Panel is the frame alone, for a feature tile, a demo frame or a tile that is one link.",
1533
+ "useWhen": "Framing content that has no title, body and footer structure: a feature tile, a demo area, or a tile that is one link.",
1534
+ "notFor": "Content with a title, a body and actions: use Card. A page-width layout wrapper: use a plain div with max-w-*.",
1535
+ "source": "src/components/atoms/panel/Panel.tsx",
1536
+ "docs": "/docs/components/panel.md",
1537
+ "imports": [
1538
+ {
1539
+ "names": [
1540
+ "Panel"
1541
+ ],
1542
+ "from": "src/components/atoms/panel/Panel"
1543
+ }
1544
+ ],
1545
+ "props": [
1546
+ {
1547
+ "name": "padding",
1548
+ "type": "'none' | 'sm' | 'md' | 'lg'",
1549
+ "default": "'md'",
1550
+ "description": "Inner spacing: 0, 12px, 20px or 32px."
1551
+ },
1552
+ {
1553
+ "name": "tone",
1554
+ "type": "'base' | 'raised' | 'signal'",
1555
+ "default": "'raised'",
1556
+ "description": "base sits flush with the page, raised is one step up, signal is the brand wash for the one panel in a group that leads."
1557
+ },
1558
+ {
1559
+ "name": "crosshair",
1560
+ "type": "boolean",
1561
+ "default": "false",
1562
+ "description": "Draws the crosshair marks at the corners."
1563
+ },
1564
+ {
1565
+ "name": "cut",
1566
+ "type": "boolean",
1567
+ "default": "false",
1568
+ "description": "Cuts the bottom-right corner, with the border following the cut."
1569
+ },
1570
+ {
1571
+ "name": "interactive",
1572
+ "type": "boolean",
1573
+ "default": "false",
1574
+ "description": "Hover and focus states for a panel that is one link or button. Use it with asChild; it adds no behaviour of its own."
1575
+ },
1576
+ {
1577
+ "name": "asChild",
1578
+ "type": "boolean",
1579
+ "default": "false",
1580
+ "description": "Renders the single child element (an a, a router Link, a button) with the panel classes merged onto it, instead of a div."
1581
+ },
1582
+ {
1583
+ "name": "className",
1584
+ "type": "string",
1585
+ "description": "Merged with the component's classes. Use it for layout (a grid span, flex, gap), not for the fill or the border."
1586
+ },
1587
+ {
1588
+ "name": "ref",
1589
+ "type": "Ref<HTMLDivElement>",
1590
+ "description": "Passed through to the div, or to the child with asChild."
1591
+ },
1592
+ {
1593
+ "name": "...rest",
1594
+ "type": "HTMLAttributes<HTMLDivElement>",
1595
+ "description": "Every native attribute, passed onto the element."
1596
+ }
1597
+ ],
1598
+ "keyboard": [
1599
+ {
1600
+ "keys": [
1601
+ "Tab"
1602
+ ],
1603
+ "does": "With interactive and asChild on a link or button: moves focus to the panel and shows the focus ring."
1604
+ },
1605
+ {
1606
+ "keys": [
1607
+ "Enter"
1608
+ ],
1609
+ "does": "Follows the link, or presses the button, that the panel was rendered as."
1610
+ }
1611
+ ],
1612
+ "dos": [
1613
+ "Use signal on at most one panel in a group: the one that leads.",
1614
+ "Give a clickable panel interactive and asChild, with a real link or button as the child.",
1615
+ "Leave 8px around a panel with the crosshair, so a parent that clips does not cut the marks off."
1616
+ ],
1617
+ "donts": [
1618
+ "Don’t put a button near the bottom-right corner of a cut panel; the cut can clip it.",
1619
+ "Don’t set a fill or border colour with a class. Choose a tone.",
1620
+ "Don’t put a second link or button inside an interactive panel."
1621
+ ],
1622
+ "pitfalls": [
1623
+ {
1624
+ "wrong": "<Panel className=\"bg-[var(--color-surface-base)] border-[var(--color-border-selection)]\">",
1625
+ "right": "<Panel tone=\"base\">",
1626
+ "why": "The panel's fill and border come from its tone, through CSS that sits outside Tailwind's layers, so a bg- or border- class never wins. Pick the tone."
1627
+ },
1628
+ {
1629
+ "wrong": "<Panel interactive onClick={open}>Replicas</Panel>",
1630
+ "right": "<Panel interactive asChild><Link to=\"/replicas\">Replicas</Link></Panel>",
1631
+ "why": "A div with a click handler cannot be reached by keyboard and is not announced as a link. interactive styles a panel that already is a link or a button; asChild makes it one."
1632
+ }
1633
+ ],
1634
+ "anatomy": [
1635
+ {
1636
+ "part": "Frame",
1637
+ "description": "A div (or, with asChild, your element) with the .panel class: a one-pixel border and a fill, both set by the tone."
1638
+ },
1639
+ {
1640
+ "part": "Tone",
1641
+ "description": "base: surface.base fill, border.hairline border. raised: surface.raised fill, border.hairline border. signal: accent.signal.wash fill, border.selection border. All tokens, so each one is right in both themes."
1642
+ },
1643
+ {
1644
+ "part": "Crosshair",
1645
+ "description": "Optional. Short marks drawn 5px outside each corner, in text.muted. The parent must not clip its overflow."
1646
+ },
1647
+ {
1648
+ "part": "Cut corner",
1649
+ "description": "Optional. The bottom-right corner cut at 45 degrees (14px), with the border drawn along the cut, so the outline does not break at that corner. With the crosshair on, the bottom-right marks fall inside the cut and are not drawn."
1650
+ },
1651
+ {
1652
+ "part": "Interactive state",
1653
+ "description": "Optional. On hover and on keyboard focus the border, diagonal included, turns accent.signal.indicator; keyboard focus also draws the focus ring 2px outside. Nothing moves."
1654
+ }
1655
+ ],
1656
+ "examples": [
1657
+ {
1658
+ "caption": "Tones",
1659
+ "source": "<Panel tone=\"base\">\n <p className=\"font-display font-semibold\">Base</p>\n <p className=\"text-sm text-[var(--color-text-secondary)]\">Flush with the page.</p>\n</Panel>\n<Panel tone=\"raised\">\n <p className=\"font-display font-semibold\">Raised</p>\n <p className=\"text-sm text-[var(--color-text-secondary)]\">One step up.</p>\n</Panel>\n<Panel tone=\"signal\">\n <p className=\"font-display font-semibold\">Signal</p>\n <p className=\"text-sm text-[var(--color-text-secondary)]\">The one that leads.</p>\n</Panel>"
1660
+ },
1661
+ {
1662
+ "caption": "Padding",
1663
+ "source": "<Panel padding=\"none\">none</Panel>\n<Panel padding=\"sm\">sm</Panel>\n<Panel padding=\"md\">md</Panel>\n<Panel padding=\"lg\">lg</Panel>"
1664
+ },
1665
+ {
1666
+ "caption": "Crosshair and cut corner",
1667
+ "source": "<Panel crosshair>Crosshair</Panel>\n<Panel cut>Cut corner</Panel>\n<Panel crosshair cut tone=\"signal\">Both</Panel>"
1668
+ },
1669
+ {
1670
+ "caption": "Interactive, as a router link",
1671
+ "source": "<Panel interactive cut padding=\"lg\" asChild>\n <RouterLink to=\"/components/card\" className=\"flex flex-col gap-3\">\n <span className=\"font-display text-lg font-semibold\">Card</span>\n <span className=\"text-sm text-[var(--color-text-secondary)]\">A panel with a title, a body and a footer.</span>\n <span className=\"inline-flex items-center gap-2 font-semibold text-[var(--color-accent-signal-text)]\">\n Open <ArrowRight aria-hidden=\"true\" className=\"size-4\" />\n </span>\n </RouterLink>\n</Panel>"
1672
+ }
1673
+ ],
1674
+ "closed": false
1675
+ },
1506
1676
  {
1507
1677
  "name": "Progress",
1508
1678
  "title": "Progress",
@@ -1548,7 +1718,7 @@
1548
1718
  "name": "tone",
1549
1719
  "type": "'signal' | 'ok' | 'warning' | 'hostile'",
1550
1720
  "default": "'signal'",
1551
- "description": "The fill colour. signal paints accent.signal.indicator to match Slider. On the track in light theme that’s a 2.15:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there."
1721
+ "description": "The fill colour. signal paints accent.signal.indicator to match Slider. On the track in light theme that’s a 2.80:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there."
1552
1722
  },
1553
1723
  {
1554
1724
  "name": "className",
@@ -4373,6 +4543,153 @@
4373
4543
  ],
4374
4544
  "closed": true
4375
4545
  },
4546
+ {
4547
+ "name": "CodeBlock",
4548
+ "title": "Code block",
4549
+ "tier": "molecules",
4550
+ "slug": "code-block",
4551
+ "summary": "Code or a command in a panel, with a title bar that names the file or language and a button that copies the text. Long lines scroll sideways inside it, and code longer than one line gets line numbers.",
4552
+ "useWhen": "Showing code or a command someone will read or copy: an install step, a config file, an example.",
4553
+ "notFor": "A word of code inside a sentence: use a code element. A key to press: use Kbd.",
4554
+ "source": "src/components/molecules/code-block/CodeBlock.tsx",
4555
+ "docs": "/docs/components/code-block.md",
4556
+ "imports": [
4557
+ {
4558
+ "names": [
4559
+ "CodeBlock"
4560
+ ],
4561
+ "from": "src/components/molecules/code-block/CodeBlock"
4562
+ }
4563
+ ],
4564
+ "requires": [
4565
+ "The app’s one Toaster at the root: the copy is confirmed with a toast."
4566
+ ],
4567
+ "props": [
4568
+ {
4569
+ "name": "code",
4570
+ "type": "string",
4571
+ "description": "The text shown and copied, exactly."
4572
+ },
4573
+ {
4574
+ "name": "label",
4575
+ "type": "string",
4576
+ "description": "What the code is, in a few words (\"the theme CSS\"). Names the region and the copy button (\"Copy the theme CSS\")."
4577
+ },
4578
+ {
4579
+ "name": "title",
4580
+ "type": "string",
4581
+ "description": "Shown in the title bar: a file name (\"index.css\") or a language (\"bash\")."
4582
+ },
4583
+ {
4584
+ "name": "language",
4585
+ "type": "'css' | 'text'",
4586
+ "default": "'text'",
4587
+ "description": "css colours comments, at-rules, selectors and property names; text is plain."
4588
+ },
4589
+ {
4590
+ "name": "lineNumbers",
4591
+ "type": "boolean",
4592
+ "description": "Line numbers in a gutter. Unset, they show when the code has more than one line."
4593
+ },
4594
+ {
4595
+ "name": "copyable",
4596
+ "type": "boolean",
4597
+ "default": "true",
4598
+ "description": "The copy button. Turn it off for code nobody should paste, such as a wrong example."
4599
+ },
4600
+ {
4601
+ "name": "wrap",
4602
+ "type": "boolean",
4603
+ "default": "false",
4604
+ "description": "Wraps long lines instead of scrolling them sideways. For prose, such as a prompt; leave code unwrapped."
4605
+ },
4606
+ {
4607
+ "name": "className",
4608
+ "type": "string",
4609
+ "description": "Merged onto the frame. Use it for width (max-w-3xl) and spacing."
4610
+ },
4611
+ {
4612
+ "name": "ref",
4613
+ "type": "Ref<HTMLDivElement>",
4614
+ "description": "Passed through to the frame."
4615
+ }
4616
+ ],
4617
+ "keyboard": [
4618
+ {
4619
+ "keys": [
4620
+ "Tab"
4621
+ ],
4622
+ "does": "Moves to the copy button, then to the code region."
4623
+ },
4624
+ {
4625
+ "keys": [
4626
+ "←",
4627
+ "→"
4628
+ ],
4629
+ "does": "In the focused code region: scrolls a long line sideways."
4630
+ },
4631
+ {
4632
+ "keys": [
4633
+ "Enter"
4634
+ ],
4635
+ "does": "On the copy button: copies the code."
4636
+ }
4637
+ ],
4638
+ "dos": [
4639
+ "Write a label that says what the code is, so the copy button reads \"Copy the install command\".",
4640
+ "Give it a title: the file name it belongs in, or the language.",
4641
+ "Set a max width with className; long lines scroll inside it, so it never widens the page."
4642
+ ],
4643
+ "donts": [
4644
+ "Don’t put several unrelated commands in one block. One block per thing to copy.",
4645
+ "Don’t set copyable on an example of what not to write.",
4646
+ "Don’t wrap it in another bordered panel; it brings its own frame."
4647
+ ],
4648
+ "pitfalls": [
4649
+ {
4650
+ "wrong": "<pre className=\"font-mono text-sm\">{command}</pre>",
4651
+ "right": "<CodeBlock code={command} label=\"the install command\" title=\"bash\" />",
4652
+ "why": "A bare pre has no copy button, no name, and cannot be scrolled with a keyboard when a line runs past the edge."
4653
+ }
4654
+ ],
4655
+ "anatomy": [
4656
+ {
4657
+ "part": "Frame",
4658
+ "description": "A thin border around a surface.base fill. The syntax colours are measured for contrast on surface.base."
4659
+ },
4660
+ {
4661
+ "part": "Title bar",
4662
+ "description": "The title on the left, in 12px uppercase mono, and the copy button on the right, above a thin line."
4663
+ },
4664
+ {
4665
+ "part": "Code region",
4666
+ "description": "A pre with role region, named by the title and the label, focusable so a keyboard can scroll it. Up to 32rem tall, then it scrolls."
4667
+ },
4668
+ {
4669
+ "part": "Line numbers",
4670
+ "description": "A gutter in text.muted, hidden from screen readers and from text selection."
4671
+ },
4672
+ {
4673
+ "part": "Copy button",
4674
+ "description": "An IconButton named \"Copy\" plus the label. After copying, its icon turns into a tick for a moment and a toast confirms it."
4675
+ }
4676
+ ],
4677
+ "examples": [
4678
+ {
4679
+ "caption": "A command",
4680
+ "source": "<CodeBlock title=\"bash\" label=\"the check command\" code=\"npx division-ds check src\" className=\"max-w-3xl\" />"
4681
+ },
4682
+ {
4683
+ "caption": "A file, with CSS colours and line numbers",
4684
+ "source": "<CodeBlock\n title=\"index.css\"\n language=\"css\"\n label=\"the theme CSS\"\n className=\"max-w-3xl\"\n code={'@import \"./division-ds/styles/division-ds.css\";\\n\\n/* Square corners */\\n:root {\\n --shape-bevel: 0;\\n --shape-radius: 4px;\\n}\\n'}\n/>"
4685
+ },
4686
+ {
4687
+ "caption": "Not copyable, for code nobody should paste",
4688
+ "source": "<CodeBlock title=\"Wrong\" label=\"a bare pre\" copyable={false} code={'<pre>{command}</pre>'} className=\"max-w-3xl\" />"
4689
+ }
4690
+ ],
4691
+ "closed": true
4692
+ },
4376
4693
  {
4377
4694
  "name": "DatePicker",
4378
4695
  "title": "Date picker",
@@ -5222,6 +5539,12 @@
5222
5539
  "default": "'default'",
5223
5540
  "description": "Use hostile for a destructive action: hostile-coloured text at rest, the hostile fill with ink text when highlighted."
5224
5541
  },
5542
+ {
5543
+ "name": "asChild (Item)",
5544
+ "type": "boolean",
5545
+ "default": "false",
5546
+ "description": "Makes the one child (a link) the item, for a menu of links. icon and shortcut are ignored then; put the content inside the link."
5547
+ },
5225
5548
  {
5226
5549
  "name": "icon (Item)",
5227
5550
  "type": "ReactNode",
@@ -9547,7 +9870,7 @@
9547
9870
  "examples": [
9548
9871
  {
9549
9872
  "caption": "Line, two series",
9550
- "source": "type Week = { week: string; online: number; store: number }\n\nconst WEEKS: Week[] = [\n { week: 'W1', online: 120, store: 160 },\n { week: 'W2', online: 131, store: 158 },\n { week: 'W3', online: 142, store: 163 },\n { week: 'W4', online: 150, store: 155 },\n { week: 'W5', online: 171, store: 162 },\n { week: 'W6', online: 188, store: 159 },\n { week: 'W7', online: 197, store: 166 },\n { week: 'W8', online: 214, store: 161 },\n]\n\n<ChartPanel<Week>\n title=\"Weekly orders\"\n summary=\"Online orders rose from 120 to 214 over eight weeks and passed in-store orders in week 5.\"\n kind=\"line\"\n data={WEEKS}\n xKey=\"week\"\n xLabel=\"Week\"\n series={[\n { key: 'online', label: 'Online' },\n { key: 'store', label: 'In store' },\n ]}\n headingLevel={4}\n/>"
9873
+ "source": "type Week = { week: string; online: number; store: number }\n\nconst WEEKS: Week[] = [\n { week: 'W1', online: 118, store: 150 },\n { week: 'W2', online: 149, store: 172 },\n { week: 'W3', online: 168, store: 158 },\n { week: 'W4', online: 141, store: 131 },\n { week: 'W5', online: 126, store: 128 },\n { week: 'W6', online: 174, store: 150 },\n { week: 'W7', online: 231, store: 141 },\n { week: 'W8', online: 183, store: 122 },\n { week: 'W9', online: 139, store: 149 },\n { week: 'W10', online: 162, store: 185 },\n { week: 'W11', online: 196, store: 163 },\n { week: 'W12', online: 170, store: 140 },\n]\n\n<ChartPanel<Week>\n title=\"Weekly orders\"\n summary=\"Online orders peaked three times in twelve weeks, highest at 231 in week 7; in-store orders peaked highest at 185 in week 10.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n data={WEEKS}\n xKey=\"week\"\n xLabel=\"Week\"\n series={[\n { key: 'online', label: 'Online' },\n { key: 'store', label: 'In store' },\n ]}\n headingLevel={4}\n/>"
9551
9874
  },
9552
9875
  {
9553
9876
  "caption": "Bar, one series",
@@ -9555,11 +9878,11 @@
9555
9878
  },
9556
9879
  {
9557
9880
  "caption": "Area under a smooth line",
9558
- "source": "type Day = { day: string; visits: number; orders: number }\n\nconst DAYS: Day[] = [\n { day: 'Mon', visits: 1840, orders: 61 },\n { day: 'Tue', visits: 2210, orders: 74 },\n { day: 'Wed', visits: 1960, orders: 58 },\n { day: 'Thu', visits: 2650, orders: 92 },\n { day: 'Fri', visits: 3120, orders: 118 },\n { day: 'Sat', visits: 3890, orders: 141 },\n { day: 'Sun', visits: 3370, orders: 126 },\n]\n\n<ChartPanel<Day>\n title=\"Visits this week\"\n summary=\"Visits climbed from 1,840 on Monday to a Saturday peak of 3,890.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n data={DAYS}\n xKey=\"day\"\n xLabel=\"Day\"\n series={[{ key: 'visits', label: 'Visits' }]}\n headline=\"19,040\"\n headingLevel={4}\n/>"
9881
+ "source": "type Day = { day: string; visits: number; orders: number }\n\nconst DAYS: Day[] = [\n { day: 'Mon', visits: 1840, orders: 61 },\n { day: 'Tue', visits: 2960, orders: 104 },\n { day: 'Wed', visits: 2210, orders: 74 },\n { day: 'Thu', visits: 1720, orders: 55 },\n { day: 'Fri', visits: 2650, orders: 92 },\n { day: 'Sat', visits: 3890, orders: 141 },\n { day: 'Sun', visits: 3120, orders: 118 },\n]\n\n<ChartPanel<Day>\n title=\"Visits this week\"\n summary=\"Visits peaked on Tuesday at 2,960, fell to a Thursday low of 1,720, then peaked higher on Saturday at 3,890.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n data={DAYS}\n xKey=\"day\"\n xLabel=\"Day\"\n series={[{ key: 'visits', label: 'Visits' }]}\n headline=\"18,390\"\n headingLevel={4}\n/>"
9559
9882
  },
9560
9883
  {
9561
9884
  "caption": "No grid lines: only the labels along the axes",
9562
- "source": "type Week = { week: string; online: number; store: number }\n\nconst WEEKS: Week[] = [\n { week: 'W1', online: 120, store: 160 },\n { week: 'W2', online: 131, store: 158 },\n { week: 'W3', online: 142, store: 163 },\n { week: 'W4', online: 150, store: 155 },\n { week: 'W5', online: 171, store: 162 },\n { week: 'W6', online: 188, store: 159 },\n { week: 'W7', online: 197, store: 166 },\n { week: 'W8', online: 214, store: 161 },\n]\n\n<ChartPanel<Week>\n title=\"Weekly orders, gridless\"\n summary=\"Online orders rose from 120 to 214 over eight weeks and passed in-store orders in week 5.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n grid={false}\n data={WEEKS}\n xKey=\"week\"\n xLabel=\"Week\"\n series={[\n { key: 'online', label: 'Online' },\n { key: 'store', label: 'In store' },\n ]}\n headingLevel={4}\n/>"
9885
+ "source": "type Week = { week: string; online: number; store: number }\n\nconst WEEKS: Week[] = [\n { week: 'W1', online: 118, store: 150 },\n { week: 'W2', online: 149, store: 172 },\n { week: 'W3', online: 168, store: 158 },\n { week: 'W4', online: 141, store: 131 },\n { week: 'W5', online: 126, store: 128 },\n { week: 'W6', online: 174, store: 150 },\n { week: 'W7', online: 231, store: 141 },\n { week: 'W8', online: 183, store: 122 },\n { week: 'W9', online: 139, store: 149 },\n { week: 'W10', online: 162, store: 185 },\n { week: 'W11', online: 196, store: 163 },\n { week: 'W12', online: 170, store: 140 },\n]\n\n<ChartPanel<Week>\n title=\"Weekly orders, gridless\"\n summary=\"Online orders peaked three times in twelve weeks, highest at 231 in week 7; in-store orders peaked highest at 185 in week 10.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n grid={false}\n data={WEEKS}\n xKey=\"week\"\n xLabel=\"Week\"\n series={[\n { key: 'online', label: 'Online' },\n { key: 'store', label: 'In store' },\n ]}\n headingLevel={4}\n/>"
9563
9886
  },
9564
9887
  {
9565
9888
  "caption": "A shaded band: night hours on a 24-hour series (the band wraps past midnight)",
@@ -9567,7 +9890,7 @@
9567
9890
  },
9568
9891
  {
9569
9892
  "caption": "Small: stat cards, each with a sparkline (a tiny line chart with no axes)",
9570
- "source": "type Week = { week: string; online: number; store: number }\n\ntype Day = { day: string; visits: number; orders: number }\n\nconst WEEKS: Week[] = [\n { week: 'W1', online: 120, store: 160 },\n { week: 'W2', online: 131, store: 158 },\n { week: 'W3', online: 142, store: 163 },\n { week: 'W4', online: 150, store: 155 },\n { week: 'W5', online: 171, store: 162 },\n { week: 'W6', online: 188, store: 159 },\n { week: 'W7', online: 197, store: 166 },\n { week: 'W8', online: 214, store: 161 },\n]\n\nconst DAYS: Day[] = [\n { day: 'Mon', visits: 1840, orders: 61 },\n { day: 'Tue', visits: 2210, orders: 74 },\n { day: 'Wed', visits: 1960, orders: 58 },\n { day: 'Thu', visits: 2650, orders: 92 },\n { day: 'Fri', visits: 3120, orders: 118 },\n { day: 'Sat', visits: 3890, orders: 141 },\n { day: 'Sun', visits: 3370, orders: 126 },\n]\n\n<ChartPanel<Day>\n title=\"Visits\"\n headline=\"19,040\"\n summary=\"Up every day but Wednesday and Sunday.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n size=\"sm\"\n data={DAYS}\n xKey=\"day\"\n xLabel=\"Day\"\n series={[{ key: 'visits', label: 'Visits' }]}\n headingLevel={4}\n/>\n<ChartPanel<Day>\n title=\"Orders\"\n headline=\"670\"\n summary=\"Saturday took 141, the most of the week.\"\n kind=\"bar\"\n size=\"sm\"\n data={DAYS}\n xKey=\"day\"\n xLabel=\"Day\"\n series={[{ key: 'orders', label: 'Orders' }]}\n headingLevel={4}\n/>\n<ChartPanel<Week>\n title=\"Online orders\"\n headline=\"214\"\n summary=\"Eight weeks of growth, from 120 a week.\"\n kind=\"line\"\n curve=\"smooth\"\n size=\"sm\"\n data={WEEKS}\n xKey=\"week\"\n xLabel=\"Week\"\n series={[{ key: 'online', label: 'Online' }]}\n headingLevel={4}\n/>"
9893
+ "source": "type Week = { week: string; online: number; store: number }\n\ntype Day = { day: string; visits: number; orders: number }\n\nconst WEEKS: Week[] = [\n { week: 'W1', online: 118, store: 150 },\n { week: 'W2', online: 149, store: 172 },\n { week: 'W3', online: 168, store: 158 },\n { week: 'W4', online: 141, store: 131 },\n { week: 'W5', online: 126, store: 128 },\n { week: 'W6', online: 174, store: 150 },\n { week: 'W7', online: 231, store: 141 },\n { week: 'W8', online: 183, store: 122 },\n { week: 'W9', online: 139, store: 149 },\n { week: 'W10', online: 162, store: 185 },\n { week: 'W11', online: 196, store: 163 },\n { week: 'W12', online: 170, store: 140 },\n]\n\nconst DAYS: Day[] = [\n { day: 'Mon', visits: 1840, orders: 61 },\n { day: 'Tue', visits: 2960, orders: 104 },\n { day: 'Wed', visits: 2210, orders: 74 },\n { day: 'Thu', visits: 1720, orders: 55 },\n { day: 'Fri', visits: 2650, orders: 92 },\n { day: 'Sat', visits: 3890, orders: 141 },\n { day: 'Sun', visits: 3120, orders: 118 },\n]\n\n<ChartPanel<Day>\n title=\"Visits\"\n headline=\"18,390\"\n summary=\"Two peaks: Tuesday, then a higher Saturday.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n size=\"sm\"\n data={DAYS}\n xKey=\"day\"\n xLabel=\"Day\"\n series={[{ key: 'visits', label: 'Visits' }]}\n headingLevel={4}\n/>\n<ChartPanel<Day>\n title=\"Orders\"\n headline=\"670\"\n summary=\"Saturday took 141, the most of the week.\"\n kind=\"bar\"\n size=\"sm\"\n data={DAYS}\n xKey=\"day\"\n xLabel=\"Day\"\n series={[{ key: 'orders', label: 'Orders' }]}\n headingLevel={4}\n/>\n<ChartPanel<Week>\n title=\"Online orders\"\n headline=\"170\"\n summary=\"Three peaks in twelve weeks; the highest, 231, in week 7.\"\n kind=\"line\"\n area\n curve=\"smooth\"\n size=\"sm\"\n data={WEEKS}\n xKey=\"week\"\n xLabel=\"Week\"\n series={[{ key: 'online', label: 'Online' }]}\n headingLevel={4}\n/>"
9571
9894
  },
9572
9895
  {
9573
9896
  "caption": "Donut with a centre total",
@@ -10984,7 +11307,7 @@
10984
11307
  {
10985
11308
  "name": "items",
10986
11309
  "type": "NavbarItem[]",
10987
- "description": "Required. Each item has label, href and current."
11310
+ "description": "Required. A link is { label, href, current }. A menu among the links is { label, menu, current }: menu lists { label, href, description } entries, opened from a dropdown on a wide bar and listed as a labelled group in the menu sheet on a narrow one."
10988
11311
  },
10989
11312
  {
10990
11313
  "name": "actions",
@@ -11006,7 +11329,13 @@
11006
11329
  {
11007
11330
  "name": "menuFooter",
11008
11331
  "type": "ReactNode",
11009
- "description": "Shown in the menu sheet under the links. Use it for an action that doesn't fit in actions on a narrow bar. Hide the matching copy in actions below the breakpoint by wrapping it in an element with the classes hidden @2xl:flex."
11332
+ "description": "Shown in the menu sheet under the links. Use it for an action that doesn't fit in actions on a narrow bar. Hide the matching copy in actions below the breakpoint by wrapping it in an element with the classes hidden @2xl:flex (hidden @4xl:flex when collapseBelow is '4xl')."
11333
+ },
11334
+ {
11335
+ "name": "collapseBelow",
11336
+ "type": "'2xl' | '4xl'",
11337
+ "default": "'2xl'",
11338
+ "description": "The bar's own width below which the links fold into the menu sheet: 2xl is 42rem, 4xl is 56rem. Pick the one at which the brand, every link and the actions still fit on one row with room to spare."
11010
11339
  },
11011
11340
  {
11012
11341
  "name": "className",
@@ -11023,7 +11352,7 @@
11023
11352
  {
11024
11353
  "name": "NavbarItem",
11025
11354
  "kind": "type",
11026
- "code": "export type NavbarItem = { label: string; href: string; current?: boolean }",
11355
+ "code": "export type NavbarItem = NavbarLink | NavbarMenu",
11027
11356
  "from": "src/components/organisms/navbar/Navbar"
11028
11357
  }
11029
11358
  ],
@@ -11084,6 +11413,10 @@
11084
11413
  "caption": "Store navigation",
11085
11414
  "source": "const ITEMS = [\n { label: 'Replicas', href: '#replicas' },\n { label: 'Optics', href: '#optics', current: true },\n { label: 'Parts', href: '#parts' },\n { label: 'Care', href: '#care' },\n { label: 'Field notes', href: '#field-notes' },\n]\n\nconst BRAND = (\n <a\n href=\"#home\"\n className=\"inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]\"\n >\n Hexline\n </a>\n)\n\nconst ACTIONS = (\n <>\n <IconButton label=\"Search\" variant=\"ghost\">\n <Search aria-hidden=\"true\" className=\"w-5 h-5\" />\n </IconButton>\n <IconButton label=\"Cart, 2 items\" variant=\"ghost\" asChild>\n <a href=\"#cart\">\n <ShoppingBag aria-hidden=\"true\" className=\"w-5 h-5\" />\n </a>\n </IconButton>\n </>\n)\n\n<Navbar label=\"Store navigation\" brand={BRAND} items={ITEMS} actions={ACTIONS} />"
11086
11415
  },
11416
+ {
11417
+ "caption": "A menu among the links",
11418
+ "source": "const ITEMS = [\n { label: 'Replicas', href: '#replicas' },\n { label: 'Optics', href: '#optics', current: true },\n { label: 'Parts', href: '#parts' },\n { label: 'Care', href: '#care' },\n { label: 'Field notes', href: '#field-notes' },\n]\n\nconst BRAND = (\n <a\n href=\"#home\"\n className=\"inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]\"\n >\n Hexline\n </a>\n)\n\nconst ACTIONS = (\n <>\n <IconButton label=\"Search\" variant=\"ghost\">\n <Search aria-hidden=\"true\" className=\"w-5 h-5\" />\n </IconButton>\n <IconButton label=\"Cart, 2 items\" variant=\"ghost\" asChild>\n <a href=\"#cart\">\n <ShoppingBag aria-hidden=\"true\" className=\"w-5 h-5\" />\n </a>\n </IconButton>\n </>\n)\n\n<Navbar\n label=\"Store navigation with a menu\"\n brand={BRAND}\n items={[\n ...ITEMS.slice(0, 3),\n {\n label: 'Guides',\n menu: [\n { label: 'Sizing', href: '#sizing', description: 'Fit charts for every replica.' },\n { label: 'Returns', href: '#returns', description: 'Thirty days, no questions.' },\n ],\n },\n ]}\n actions={ACTIONS}\n/>"
11419
+ },
11087
11420
  {
11088
11421
  "caption": "A narrow container",
11089
11422
  "source": "const ITEMS = [\n { label: 'Replicas', href: '#replicas' },\n { label: 'Optics', href: '#optics', current: true },\n { label: 'Parts', href: '#parts' },\n { label: 'Care', href: '#care' },\n { label: 'Field notes', href: '#field-notes' },\n]\n\nconst BRAND = (\n <a\n href=\"#home\"\n className=\"inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]\"\n >\n Hexline\n </a>\n)\n\nconst ACTIONS = (\n <>\n <IconButton label=\"Search\" variant=\"ghost\">\n <Search aria-hidden=\"true\" className=\"w-5 h-5\" />\n </IconButton>\n <IconButton label=\"Cart, 2 items\" variant=\"ghost\" asChild>\n <a href=\"#cart\">\n <ShoppingBag aria-hidden=\"true\" className=\"w-5 h-5\" />\n </a>\n </IconButton>\n </>\n)\n\n<div className=\"max-w-sm\">\n <Navbar label=\"Store navigation, narrow\" brand={BRAND} items={ITEMS} actions={ACTIONS} />\n</div>"
@@ -11708,6 +12041,242 @@
11708
12041
  }
11709
12042
  ],
11710
12043
  "closed": true
12044
+ },
12045
+ {
12046
+ "name": "Terminal",
12047
+ "title": "Terminal",
12048
+ "tier": "organisms",
12049
+ "slug": "terminal",
12050
+ "summary": "A live terminal session in the page, built on xterm.js. It renders the output you write to it and reports what the person types. It knows nothing about the connection: your app owns that.",
12051
+ "useWhen": "A live, interactive process that needs real terminal behaviour: colour, cursor movement, full-screen programs.",
12052
+ "notFor": "Static code or a finished log: use CodeBlock. A conversation shown as messages: use ChatThread.",
12053
+ "source": "src/components/organisms/terminal/Terminal.tsx",
12054
+ "docs": "/docs/components/terminal.md",
12055
+ "imports": [
12056
+ {
12057
+ "names": [
12058
+ "Terminal"
12059
+ ],
12060
+ "from": "src/components/organisms/terminal/Terminal"
12061
+ }
12062
+ ],
12063
+ "requires": [
12064
+ "Import it from its own file and load the screen that uses it with React.lazy. It adds about 100 kB of JavaScript after gzip (about 380 kB before) and 1 kB of CSS, so it is not in the organisms barrel.",
12065
+ "A height. It is 24rem by default; set another with className (h-[32rem]), or flex-1 inside a column."
12066
+ ],
12067
+ "props": [
12068
+ {
12069
+ "name": "label",
12070
+ "type": "string",
12071
+ "description": "Required. What this session is (\"Build agent\"). Names the terminal, its input and its status announcements."
12072
+ },
12073
+ {
12074
+ "name": "status",
12075
+ "type": "'connecting' | 'live' | 'reconnecting' | 'exited' | 'error'",
12076
+ "description": "Required. connecting and reconnecting are info, live is ok, exited is neutral for code 0 and hostile otherwise, error is hostile. Buffered output stays visible in all of them."
12077
+ },
12078
+ {
12079
+ "name": "exitCode",
12080
+ "type": "number",
12081
+ "description": "With status exited: shown beside the word, and a non-zero code turns it hostile."
12082
+ },
12083
+ {
12084
+ "name": "statusMessage",
12085
+ "type": "string",
12086
+ "description": "Detail beside the status, and announced with it: the reason for an error, the attempt while reconnecting."
12087
+ },
12088
+ {
12089
+ "name": "readOnly",
12090
+ "type": "boolean",
12091
+ "default": "false",
12092
+ "description": "Blocks input and never fires onData. Selecting and copying keep working, and Tab and Escape leave the terminal as they do anywhere else."
12093
+ },
12094
+ {
12095
+ "name": "announceOutput",
12096
+ "type": "boolean",
12097
+ "default": "false",
12098
+ "description": "Reads output aloud through xterm’s live region, up to 20 lines at a time. For a single session a screen reader user is working in. Leave it off when several terminals share a page."
12099
+ },
12100
+ {
12101
+ "name": "onData",
12102
+ "type": "(data: string) => void",
12103
+ "description": "What the person typed or pasted, plus the replies a program asks the terminal for. Send it to the process."
12104
+ },
12105
+ {
12106
+ "name": "onResize",
12107
+ "type": "(size: { cols: number; rows: number }) => void",
12108
+ "description": "The new grid size, debounced by 100ms and only when cols or rows changed. It fires once after mounting."
12109
+ },
12110
+ {
12111
+ "name": "scrollback",
12112
+ "type": "number",
12113
+ "default": "5000",
12114
+ "description": "Lines kept above the screen. xterm’s own default is 1000, which a busy agent passes in seconds."
12115
+ },
12116
+ {
12117
+ "name": "className",
12118
+ "type": "string",
12119
+ "description": "Merged onto the frame. Set the height here."
12120
+ },
12121
+ {
12122
+ "name": "ref",
12123
+ "type": "Ref<TerminalHandle>",
12124
+ "description": "The handle with the four methods below."
12125
+ },
12126
+ {
12127
+ "name": "write (TerminalHandle)",
12128
+ "type": "(data: string | Uint8Array, callback?: () => void) => void",
12129
+ "description": "Sends output to the terminal, without a React render. The callback runs when it has been parsed: use it for flow control, because xterm throws data away past 50 MB waiting."
12130
+ },
12131
+ {
12132
+ "name": "clear (TerminalHandle)",
12133
+ "type": "() => void",
12134
+ "description": "Clears the screen and the scrollback, keeping the line the cursor is on."
12135
+ },
12136
+ {
12137
+ "name": "focus (TerminalHandle)",
12138
+ "type": "() => void",
12139
+ "description": "Moves keyboard focus into the terminal."
12140
+ },
12141
+ {
12142
+ "name": "fit (TerminalHandle)",
12143
+ "type": "() => void",
12144
+ "description": "Fits the grid to the frame now. Resizing already does this."
12145
+ }
12146
+ ],
12147
+ "api": [
12148
+ {
12149
+ "name": "TerminalStatus",
12150
+ "kind": "type",
12151
+ "code": "export type TerminalStatus = 'connecting' | 'live' | 'reconnecting' | 'exited' | 'error'",
12152
+ "from": "src/components/organisms/terminal/Terminal"
12153
+ },
12154
+ {
12155
+ "name": "TerminalHandle",
12156
+ "kind": "type",
12157
+ "code": "export type TerminalHandle = {\n /** Sends output to the terminal, without a React render. The callback runs once it is parsed: use it for flow control. */\n write(data: string | Uint8Array, callback?: () => void): void\n /** Clears the screen and the scrollback, keeping the cursor's line. */\n clear(): void\n /** Moves keyboard focus into the terminal. */\n focus(): void\n /** Fits the grid to the container now. Resizing already does this; use it after a change no resize reports. */\n fit(): void\n}",
12158
+ "from": "src/components/organisms/terminal/Terminal"
12159
+ }
12160
+ ],
12161
+ "keyboard": [
12162
+ {
12163
+ "keys": [
12164
+ "Tab"
12165
+ ],
12166
+ "does": "From the page, moves into the terminal. Inside it, goes to the program, unless readOnly is set, when it moves on."
12167
+ },
12168
+ {
12169
+ "keys": [
12170
+ "Ctrl",
12171
+ "Shift",
12172
+ "L"
12173
+ ],
12174
+ "does": "Moves focus out of the terminal to the next tab stop. Tab and Escape belong to the program, so this is the way out."
12175
+ },
12176
+ {
12177
+ "keys": [
12178
+ "Ctrl",
12179
+ "C"
12180
+ ],
12181
+ "does": "With text selected, copies it and clears the selection. With nothing selected, goes to the program as an interrupt."
12182
+ },
12183
+ {
12184
+ "keys": [
12185
+ "Ctrl",
12186
+ "Shift",
12187
+ "C"
12188
+ ],
12189
+ "does": "Copies the selection and keeps it."
12190
+ },
12191
+ {
12192
+ "keys": [
12193
+ "Ctrl",
12194
+ "V"
12195
+ ],
12196
+ "does": "Pastes. Ctrl+Shift+V does the same. A program that wants a literal Ctrl+V does not get it."
12197
+ }
12198
+ ],
12199
+ "dos": [
12200
+ "Give each terminal a label that says which session it is, so a page of them can be told apart.",
12201
+ "Set readOnly when nobody can usefully type: a finished process, a session someone else owns.",
12202
+ "Say why in statusMessage when status is error.",
12203
+ "Use write’s callback to pause the source when output arrives faster than the page can draw it."
12204
+ ],
12205
+ "donts": [
12206
+ "Don’t import it from the organisms barrel, and don’t load it on a screen that does not show it.",
12207
+ "Don’t put a terminal inside another bordered panel; it brings its own frame.",
12208
+ "Don’t keep output in React state to pass down. Write it through the ref.",
12209
+ "Don’t expect the status to block input by itself: set readOnly when a process has exited."
12210
+ ],
12211
+ "pitfalls": [
12212
+ {
12213
+ "wrong": "<Terminal label=\"Agent\" status=\"live\" output={text} />",
12214
+ "right": "const term = useRef<TerminalHandle>(null)\nconst socket = useRef<WebSocket>(null)\nconst [status, setStatus] = useState<TerminalStatus>('connecting')\n\nuseEffect(() => {\n const ws = new WebSocket(url)\n ws.binaryType = 'arraybuffer'\n socket.current = ws\n ws.onopen = () => setStatus('live')\n ws.onmessage = event =>\n term.current?.write(typeof event.data === 'string' ? event.data : new Uint8Array(event.data))\n ws.onclose = () => setStatus('reconnecting')\n return () => ws.close()\n}, [url])\n\n<Terminal\n ref={term}\n label=\"Build agent\"\n status={status}\n onData={data => socket.current?.send(data)}\n onResize={size => socket.current?.send(JSON.stringify({ type: 'resize', ...size }))}\n/>",
12215
+ "why": "Output goes through the ref, so a fast stream never re-renders React. The connection lives in your app, not in the component."
12216
+ },
12217
+ {
12218
+ "wrong": "<Suspense fallback={null}><LazyTerminal ref={term} ... /></Suspense> // with the socket in this component's effect",
12219
+ "right": "const Screen = lazy(() => import('./AgentScreen')) // AgentScreen imports Terminal directly and owns the socket",
12220
+ "why": "If only the Terminal is lazy, your effect runs while the ref is still empty and the first output is lost. Lazy-load the screen that holds both."
12221
+ }
12222
+ ],
12223
+ "anatomy": [
12224
+ {
12225
+ "part": "Frame",
12226
+ "description": "A Panel on surface.base, which is the surface all sixteen ANSI colours are measured on. It shows the focus ring while the terminal has focus."
12227
+ },
12228
+ {
12229
+ "part": "Title bar",
12230
+ "description": "The label, the status Badge with a word and a tone, a Read only Badge when set, statusMessage, and the shortcut that leaves the terminal."
12231
+ },
12232
+ {
12233
+ "part": "Status announcement",
12234
+ "description": "A visually hidden status region. It says only the label and the status, so a screen reader hears changes and not output."
12235
+ },
12236
+ {
12237
+ "part": "Grid",
12238
+ "description": "xterm.js in the mono font, fitted to the frame. Colours are read from tokens at runtime and updated when the theme changes, keeping the buffer."
12239
+ },
12240
+ {
12241
+ "part": "Screen reader rows",
12242
+ "description": "xterm’s accessibility tree: the visible rows as a list a screen reader can browse. Scrolling the terminal changes which rows it holds."
12243
+ }
12244
+ ],
12245
+ "examples": [
12246
+ {
12247
+ "caption": "Connecting",
12248
+ "source": "<Terminal label=\"Build agent\" status=\"connecting\" className=\"h-40 max-w-3xl\" />"
12249
+ },
12250
+ {
12251
+ "caption": "Live, with a simulated stream",
12252
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nconst STREAM = ['compiling', 'linking', 'checking types', 'running tests', 'writing bundle']\n\nfunction LiveDemo() {\n const term = useRef<TerminalHandle>(null)\n const [streaming, setStreaming] = useState(false)\n\n useEffect(() => {\n term.current?.write(`${TRANSCRIPT}Type here, or start the stream.\\r\\n$ `)\n }, [])\n\n useEffect(() => {\n if (!streaming) return\n let line = 0\n const timer = window.setInterval(() => {\n const step = STREAM[line % STREAM.length]\n const code = 31 + (line % 6)\n term.current?.write(`${paint(code, `[${String(line + 1).padStart(3, '0')}]`)} ${step}\\r\\n`)\n line += 1\n if (line === 40) setStreaming(false)\n }, 120)\n return () => window.clearInterval(timer)\n }, [streaming])\n\n return (\n <div className=\"flex max-w-3xl flex-col gap-3\">\n <Terminal\n ref={term}\n label=\"Sample build\"\n status=\"live\"\n className=\"h-80\"\n onData={data => term.current?.write(data === '\\r' ? '\\r\\n$ ' : data)}\n />\n <div className=\"flex flex-wrap gap-3\">\n <Button variant=\"secondary\" onClick={() => setStreaming(on => !on)}>{streaming ? 'Stop the stream' : 'Start the stream'}</Button>\n <Button variant=\"secondary\" onClick={() => term.current?.clear()}>Clear</Button>\n </div>\n </div>\n )\n}\n\n<LiveDemo />"
12253
+ },
12254
+ {
12255
+ "caption": "Read only: select and copy, but no typing",
12256
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nfunction SampleTerminal(props: Omit<TerminalProps, 'ref'>) {\n const term = useRef<TerminalHandle>(null)\n useEffect(() => term.current?.write(TRANSCRIPT), [])\n return <Terminal ref={term} className=\"h-64 max-w-3xl\" {...props} />\n}\n\n<SampleTerminal label=\"Deploy log\" status=\"live\" readOnly />"
12257
+ },
12258
+ {
12259
+ "caption": "Reconnecting, with the output kept",
12260
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nfunction SampleTerminal(props: Omit<TerminalProps, 'ref'>) {\n const term = useRef<TerminalHandle>(null)\n useEffect(() => term.current?.write(TRANSCRIPT), [])\n return <Terminal ref={term} className=\"h-64 max-w-3xl\" {...props} />\n}\n\n<SampleTerminal label=\"Test agent\" status=\"reconnecting\" statusMessage=\"Attempt 2 of 5\" />"
12261
+ },
12262
+ {
12263
+ "caption": "Exited with code 0",
12264
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nfunction SampleTerminal(props: Omit<TerminalProps, 'ref'>) {\n const term = useRef<TerminalHandle>(null)\n useEffect(() => term.current?.write(TRANSCRIPT), [])\n return <Terminal ref={term} className=\"h-64 max-w-3xl\" {...props} />\n}\n\n<SampleTerminal label=\"Migration\" status=\"exited\" exitCode={0} readOnly />"
12265
+ },
12266
+ {
12267
+ "caption": "Exited with an error code",
12268
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nfunction SampleTerminal(props: Omit<TerminalProps, 'ref'>) {\n const term = useRef<TerminalHandle>(null)\n useEffect(() => term.current?.write(TRANSCRIPT), [])\n return <Terminal ref={term} className=\"h-64 max-w-3xl\" {...props} />\n}\n\n<SampleTerminal label=\"Type check\" status=\"exited\" exitCode={2} readOnly />"
12269
+ },
12270
+ {
12271
+ "caption": "Connection error",
12272
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nfunction SampleTerminal(props: Omit<TerminalProps, 'ref'>) {\n const term = useRef<TerminalHandle>(null)\n useEffect(() => term.current?.write(TRANSCRIPT), [])\n return <Terminal ref={term} className=\"h-64 max-w-3xl\" {...props} />\n}\n\n<SampleTerminal label=\"Review agent\" status=\"error\" statusMessage=\"The connection was refused.\" readOnly />"
12273
+ },
12274
+ {
12275
+ "caption": "Switching theme updates the colours and keeps the buffer",
12276
+ "source": "const ESC = '\\u001b'\n\nconst paint = (code: number, text: string) => `${ESC}[${code}m${text}${ESC}[0m`\n\nconst COLOURS = [31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97]\n .map(code => paint(code, `${code} `.padEnd(4)))\n .join('')\n\nconst TRANSCRIPT = [\n paint(1, 'Sample session, written by this page'),\n '┌──────────────────────────────┐',\n `│ ${paint(32, '✔')} tests passed 宽字符 🚀 │`,\n '└──────────────────────────────┘',\n COLOURS,\n 'Docs at https://example.com/terminal',\n '',\n].join('\\r\\n')\n\nfunction SampleTerminal(props: Omit<TerminalProps, 'ref'>) {\n const term = useRef<TerminalHandle>(null)\n useEffect(() => term.current?.write(TRANSCRIPT), [])\n return <Terminal ref={term} className=\"h-64 max-w-3xl\" {...props} />\n}\n\n<div className=\"flex max-w-3xl flex-col gap-3\">\n <SampleTerminal label=\"Theme check\" status=\"live\" readOnly />\n <div className=\"flex flex-wrap gap-3\">\n <Button variant=\"secondary\" onClick={() => setTheme('light')}>Light theme</Button>\n <Button variant=\"secondary\" onClick={() => setTheme('dark')}>Dark theme</Button>\n </div>\n</div>"
12277
+ }
12278
+ ],
12279
+ "closed": true
11711
12280
  }
11712
12281
  ]
11713
12282
  }