eidotter 0.42.0 → 0.44.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.
Files changed (33) hide show
  1. package/README.md +4 -4
  2. package/dist/components/Bench/components/Bench.d.ts +45 -0
  3. package/dist/components/Bench/components/Bench.js +46 -0
  4. package/dist/components/Bench/components/index.d.ts +2 -0
  5. package/dist/components/Bench/components/index.js +1 -0
  6. package/dist/components/Bench/index.d.ts +2 -0
  7. package/dist/components/Bench/index.js +1 -0
  8. package/dist/components/CmdPalette/components/CmdPalette.js +7 -3
  9. package/dist/components/Header/components/Header.d.ts +38 -2
  10. package/dist/components/Header/components/Header.js +23 -5
  11. package/dist/components/Icon/components/Icon.d.ts +13 -13
  12. package/dist/components/Icon/components/Icon.js +12 -12
  13. package/dist/components/LegalPage/components/LegalPage.d.ts +7 -0
  14. package/dist/components/LegalPage/components/LegalPage.js +1 -1
  15. package/dist/components/PageShell/components/PageSection.d.ts +25 -0
  16. package/dist/components/PageShell/components/PageSection.js +16 -0
  17. package/dist/components/PageShell/components/PageShell.d.ts +65 -0
  18. package/dist/components/PageShell/components/PageShell.js +33 -0
  19. package/dist/components/PageShell/components/index.d.ts +4 -0
  20. package/dist/components/PageShell/components/index.js +2 -0
  21. package/dist/components/PageShell/index.d.ts +2 -0
  22. package/dist/components/PageShell/index.js +1 -0
  23. package/dist/components/registry.js +30 -0
  24. package/dist/eidotter.css +1 -1
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.es.js +5517 -4720
  27. package/dist/index.umd.js +4 -4
  28. package/llms.txt +3 -3
  29. package/migrations/0.41.0.md +3 -2
  30. package/migrations/0.43.0.md +129 -0
  31. package/migrations/0.44.0.md +110 -0
  32. package/migrations/README.md +4 -0
  33. package/package.json +20 -20
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  [![license](https://img.shields.io/badge/license-CC--BY--NC--4.0-ffb000?labelColor=020003)](./LICENSE.md)
16
16
  [![React](https://img.shields.io/badge/React-18%20%7C%2019-ffb000?labelColor=020003)](https://react.dev/)
17
17
 
18
- **eiDotter** is a React component library that takes the DOS/CGA terminal aesthetic seriously: authentic 16-color palette, bitmap-accurate typography, phosphor glow physics, keyboard-first interactions. 59 components across forms, navigation, windowing, timelines, health displays, and terminal chrome — shipped as ES modules with a prebuilt stylesheet so consumers don't need Tailwind to use it.
18
+ **eiDotter** is a React component library that takes the DOS/CGA terminal aesthetic seriously: authentic 16-color palette, bitmap-accurate typography, phosphor glow physics, keyboard-first interactions. 61 components across forms, navigation, windowing, timelines, health displays, and terminal chrome — shipped as ES modules with a prebuilt stylesheet so consumers don't need Tailwind to use it.
19
19
 
20
20
  → **[Storybook](https://cinimody.github.io/eiDotter/)**  ·  **[CHANGELOG](./CHANGELOG.md)**  ·  **[Accessibility](./ACCESSIBILITY.md)**  ·  **[Issues](https://github.com/CinimoDY/eiDotter/issues)**  ·  **[npm](https://www.npmjs.com/package/eidotter)**
21
21
 
@@ -116,9 +116,9 @@ for client apps and `'use client'` files.
116
116
 
117
117
  ## Upgrading
118
118
 
119
- Every minor with breaking or visible changes ships an upgrade guide in the package, at `node_modules/eidotter/migrations/<version>.md` (source: [`migrations/`](./migrations/)). Read every guide between your installed version and the target, oldest first. Start with [0.41.0](./migrations/0.41.0.md).
119
+ Every minor with breaking or visible changes ships an upgrade guide in the package, at `node_modules/eidotter/migrations/<version>.md` (source: [`migrations/`](./migrations/)). Read every guide between your installed version and the target, oldest first. Start with [0.41.0](./migrations/0.41.0.md), then [0.43.0](./migrations/0.43.0.md) and [0.44.0](./migrations/0.44.0.md).
120
120
 
121
- 0.x caret ranges stop at the next minor (`^0.40.2` never resolves to 0.41.0), so bump the range explicitly: `npm install eidotter@^0.42.0` (0.41.0 shipped before its review fixes; use the latest 0.41.x, 0.41.3 or later).
121
+ 0.x caret ranges stop at the next minor (`^0.40.2` never resolves to 0.41.0), so bump the range explicitly: `npm install eidotter@^0.44.0`.
122
122
 
123
123
  ## Documentation
124
124
 
@@ -133,7 +133,7 @@ Every minor with breaking or visible changes ships an upgrade guide in the packa
133
133
 
134
134
  ## Components
135
135
 
136
- 59 components across six families — [complete catalog in Storybook](https://cinimody.github.io/eiDotter/). Also exported but not tabled below: CalendarHeatmap, Connector, CopyButton, ChecklistRow, EmptyState, HoldToComplete, LabeledProgress, LedgerRow, ProgressRing, SectionHeading, Select, TimelineFeed, TimelinePage and the `Mark` brand component.
136
+ 61 components across six families — [complete catalog in Storybook](https://cinimody.github.io/eiDotter/). Also exported but not tabled below: Bench, CalendarHeatmap, Connector, CopyButton, ChecklistRow, EmptyState, HoldToComplete, LabeledProgress, LedgerRow, PageShell (with PageSection), ProgressRing, SectionHeading, Select, TimelineFeed, TimelinePage and the `Mark` brand component.
137
137
 
138
138
  <details>
139
139
  <summary><strong>Windowing &amp; shell</strong> — Terminal, DosDialog, DosWindow, IconButton, Modal, Lightbox, CommandPrompt, CmdPalette, Header, Footer, Nav, Brand</summary>
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ export interface BenchPane {
3
+ /** Pane label (`Before`, `After · 390`, …), uppercase in the label row. */
4
+ label: React.ReactNode;
5
+ /** Pane body, usually a screenshot `<img>` (rendered block, full width). */
6
+ content: React.ReactNode;
7
+ /** `after` paints the label in the accent colour. Default `neutral`. */
8
+ tone?: 'before' | 'after' | 'neutral';
9
+ /**
10
+ * `desktop` (default): the scroll frame fills the pane. `phone`: a 390px
11
+ * frame centred with 12px of padding around it.
12
+ */
13
+ frame?: 'desktop' | 'phone';
14
+ /**
15
+ * Accessible name of the pane's scroll frame. Defaults to
16
+ * "<title>, <label>" when both are strings, else the label when it is a
17
+ * string, else "Scrollable pane".
18
+ */
19
+ frameLabel?: string;
20
+ }
21
+ export interface BenchProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
22
+ /** Strip title, rendered as a heading after DosWindow's `▸` marker. */
23
+ title: React.ReactNode;
24
+ /** Heading level of the title. Default `3` (inside a `PageSection` h2). */
25
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
26
+ /** Status chips (`Badge`s), right-aligned in the strip; they wrap. */
27
+ chips?: React.ReactNode;
28
+ /** One muted line under the strip, above the panes. */
29
+ note?: React.ReactNode;
30
+ /** One pane renders full width; two or more sit side by side in pairs. */
31
+ panes: BenchPane[];
32
+ /** Scroll-frame max height, any CSS length. Default `640px` (520px when stacked). */
33
+ maxHeight?: string;
34
+ className?: string;
35
+ }
36
+ /**
37
+ * Bench: a compare unit on DosWindow (0.42). Title strip with chips, an
38
+ * optional note, then panes side by side (before | after) separated by a 1px
39
+ * gap the grid background paints, so no divider elements. Each pane has an
40
+ * uppercase label row and a keyboard-scrollable frame. Below 760px of bench
41
+ * width the panes stack.
42
+ *
43
+ * Server-safe and presentational. Ref forwards to the DosWindow root.
44
+ */
45
+ export declare const Bench: React.ForwardRefExoticComponent<BenchProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { cn } from '../../../utils/cn.js';
4
+ import { DosWindow } from '../../DosWindow/index.js';
5
+ const toneClasses = {
6
+ before: 'eidotter-bench__pane--before',
7
+ after: 'eidotter-bench__pane--after',
8
+ neutral: 'eidotter-bench__pane--neutral',
9
+ };
10
+ function frameName(pane, title) {
11
+ if (pane.frameLabel)
12
+ return pane.frameLabel;
13
+ if (typeof pane.label === 'string') {
14
+ return typeof title === 'string' ? `${title}, ${pane.label}` : pane.label;
15
+ }
16
+ return 'Scrollable pane';
17
+ }
18
+ /**
19
+ * Bench: a compare unit on DosWindow (0.42). Title strip with chips, an
20
+ * optional note, then panes side by side (before | after) separated by a 1px
21
+ * gap the grid background paints, so no divider elements. Each pane has an
22
+ * uppercase label row and a keyboard-scrollable frame. Below 760px of bench
23
+ * width the panes stack.
24
+ *
25
+ * Server-safe and presentational. Ref forwards to the DosWindow root.
26
+ */
27
+ export const Bench = forwardRef(({ title, headingLevel = 3, chips, note, panes, maxHeight, className, style, ...rest }, ref) => {
28
+ const Heading = `h${headingLevel}`;
29
+ const hasChips = chips != null && chips !== false && chips !== '';
30
+ const hasNote = note != null && note !== false && note !== '';
31
+ const height = maxHeight?.trim();
32
+ const mergedStyle = height
33
+ ? { ...style, '--eidotter-bench-max-height': height }
34
+ : style;
35
+ return (_jsxs(DosWindow, { ref: ref, className: cn('eidotter-bench', className), style: mergedStyle, title: _jsx(Heading, { className: "eidotter-bench__title", children: title }), controls: hasChips ? _jsx("div", { className: "eidotter-bench__chips", children: chips }) : undefined, ...rest, children: [hasNote && _jsx("p", { className: "eidotter-bench__note", children: note }), _jsx("div", { className: cn('eidotter-bench__panes', panes.length === 1 && 'eidotter-bench__panes--single'), children: panes.map((pane, i) => {
36
+ const frame = (_jsx("div", { className: cn('eidotter-bench__frame', pane.frame === 'phone' && 'eidotter-bench__frame--phone'),
37
+ // Scroll containers must be keyboard-reachable (WCAG 2.1.1; the
38
+ // ChatHistory lesson). role=group carries the name without
39
+ // adding a landmark per pane.
40
+ tabIndex: 0, role: "group", "aria-label": frameName(pane, title), children: pane.content }));
41
+ return (_jsxs("div", { className: cn('eidotter-bench__pane', toneClasses[pane.tone ?? 'neutral']), children: [_jsx("div", { className: "eidotter-bench__label", children: pane.label }), pane.frame === 'phone'
42
+ ? _jsx("div", { className: "eidotter-bench__phone", children: frame })
43
+ : frame] }, i));
44
+ }) })] }));
45
+ });
46
+ Bench.displayName = 'Bench';
@@ -0,0 +1,2 @@
1
+ export { Bench } from './Bench.js';
2
+ export type { BenchProps, BenchPane } from './Bench.js';
@@ -0,0 +1 @@
1
+ export { Bench } from './Bench.js';
@@ -0,0 +1,2 @@
1
+ export { Bench } from './components/index.js';
2
+ export type { BenchProps, BenchPane } from './components/index.js';
@@ -0,0 +1 @@
1
+ export { Bench } from './components/index.js';
@@ -98,7 +98,12 @@ export const CmdPalette = forwardRef(({ open, onOpenChange, items, placeholder =
98
98
  useEffect(() => {
99
99
  setSelected(s => Math.min(s, Math.max(0, results.length - 1)));
100
100
  }, [results.length]);
101
- // Focus input when open (React Aria handles focus trap; we steer to input)
101
+ // Focus the input when the palette opens. `autoFocus` on the input focuses
102
+ // it during the commit of the opening click or keypress, which is what
103
+ // mobile Safari needs to raise the on-screen keyboard (a focus() call in a
104
+ // later timer is outside the user gesture, so iOS focuses without a
105
+ // keyboard). This timer is the fallback for environments where the dialog's
106
+ // own focus management lands after the mount.
102
107
  useEffect(() => {
103
108
  if (open) {
104
109
  const id = window.setTimeout(() => inputRef.current?.focus(), 10);
@@ -162,9 +167,8 @@ export const CmdPalette = forwardRef(({ open, onOpenChange, items, placeholder =
162
167
  };
163
168
  return (_jsxs(DosDialog, { ref: ref, open: open, onOpenChange: onOpenChange, placement: "top", size: "lg", "aria-label": ariaLabel, className: cn('eidotter-cmdpal__container', className), head: {
164
169
  title: 'JUMP TO',
165
- end: _jsx("span", { className: "eidotter-cmdpal__blink", children: "_" }),
166
170
  decorative: true,
167
- }, footer: footerHint ? (_jsx("div", { className: "eidotter-cmdpal__footer", "aria-hidden": "true", children: footerHint })) : undefined, children: [_jsx("input", { ref: inputRef, type: "text", role: "combobox", "aria-controls": listboxId, "aria-expanded": true, "aria-autocomplete": "list", "aria-activedescendant": results[selected] ? `${baseId}-item-${results[selected].id}` : undefined, className: "eidotter-cmdpal__input", value: query, placeholder: placeholder, onChange: e => setQuery(e.target.value), onKeyDown: handleKeyDown }), _jsxs("ul", { ref: listRef, id: listboxId, role: "listbox", "aria-label": ariaLabel, className: "eidotter-cmdpal__results", children: [results.length === 0 && (_jsx("li", { className: "eidotter-cmdpal__empty", role: "presentation", children: emptyMessage })), results.map((item, i) => {
171
+ }, footer: footerHint ? (_jsx("div", { className: "eidotter-cmdpal__footer", "aria-hidden": "true", children: footerHint })) : undefined, children: [_jsx("input", { ref: inputRef, autoFocus: true, type: "text", enterKeyHint: "go", autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, role: "combobox", "aria-controls": listboxId, "aria-expanded": true, "aria-autocomplete": "list", "aria-activedescendant": results[selected] ? `${baseId}-item-${results[selected].id}` : undefined, className: "eidotter-cmdpal__input", value: query, placeholder: placeholder, onChange: e => setQuery(e.target.value), onKeyDown: handleKeyDown }), _jsxs("ul", { ref: listRef, id: listboxId, role: "listbox", "aria-label": ariaLabel, className: "eidotter-cmdpal__results", children: [results.length === 0 && (_jsx("li", { className: "eidotter-cmdpal__empty", role: "presentation", children: emptyMessage })), results.map((item, i) => {
168
172
  const isSelected = i === selected;
169
173
  return (_jsx("li", { id: `${baseId}-item-${item.id}`, role: "option", "aria-selected": isSelected, "data-index": i, className: cn('eidotter-cmdpal__item', isSelected && 'eidotter-cmdpal__item--selected'), onMouseMove: () => {
170
174
  if (suppressHoverRef.current)
@@ -56,12 +56,48 @@ export interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
56
56
  * when `children` is provided, or when `brandName` is empty.
57
57
  */
58
58
  brand?: 'text' | 'lockup' | 'mark';
59
+ /**
60
+ * Narrow-header brand presentation for the icon brands (`brand` 'lockup' or
61
+ * 'mark'); ignored for `'text'`, `children` or an empty `brandName`.
62
+ * - `'none'` (default): the name always shows, wrapping onto more lines when
63
+ * it does not fit.
64
+ * - `'mark'`: below 480px of header width (a container query on the header)
65
+ * only the icon shows. The name stays in the DOM, visually hidden, so it
66
+ * remains the brand link's accessible name.
67
+ */
68
+ brandCollapse?: 'none' | 'mark';
69
+ /**
70
+ * Row arrangement:
71
+ * - `'inline'` (default): brand left, nav (+ `end`) right, one row.
72
+ * - `'stacked'`: brand row on top with `end` and the mobile MENU trigger on
73
+ * its right; the desktop nav on its own row below, left-aligned on the
74
+ * brand's edge. Below 768px the nav row is absent (MENU takes over), as
75
+ * in the inline layout.
76
+ */
77
+ layout?: 'inline' | 'stacked';
78
+ /**
79
+ * Bottom rule. `false` removes the 1px bottom border and the retro glow
80
+ * under it (for pages where the header sits in one block with the content).
81
+ * Default `true`.
82
+ */
83
+ rule?: boolean;
84
+ /**
85
+ * Centre the header's content in a max-width column so the brand's left edge
86
+ * lines up with a page column of the same width (e.g. `PageShell`). `true`
87
+ * uses 1240px (PageShell's default); a string is any CSS length
88
+ * (`'72rem'`, `'960px'`). The header itself (background, sticky) still spans
89
+ * the full width, and its inline padding follows the same
90
+ * `max(16px, safe-area inset)` gutter as PageShell. Default off.
91
+ */
92
+ contained?: boolean | string;
59
93
  /**
60
94
  * Trailing slot (theme toggle, ⌘K hint, …). Rendered once: after the desktop
61
95
  * nav on wide screens, and between the brand and the MENU trigger on mobile
62
96
  * (the desktop nav is hidden below 768px, the MENU trigger above it, so DOM
63
- * order gives both placements without duplicating the node). Keep end
64
- * content compact on mobile; it clips before the brand name does.
97
+ * order gives both placements without duplicating the node). With
98
+ * `layout="stacked"` it sits on the brand row at every width. Keep end
99
+ * content compact on mobile: when the row is short of room the brand wraps
100
+ * first, then the end slot clips.
65
101
  * The slot is `overflow: hidden` at every width, so dropdowns, menus and
66
102
  * tooltips opened from `end` must render in a portal (React Aria popovers,
67
103
  * e.g. `Select`, already do); an in-flow popover is clipped.
@@ -9,7 +9,7 @@ const variantClasses = {
9
9
  retro: 'eidotter-header--retro',
10
10
  modern: 'eidotter-header--modern',
11
11
  };
12
- export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHref, variant = 'retro', sticky = true, linkComponent, context, brand = 'lockup', end, skipLinkTarget, children, className, ...rest }, ref) => {
12
+ export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHref, variant = 'retro', sticky = true, linkComponent, context, brand = 'lockup', brandCollapse = 'none', layout = 'inline', rule = true, contained = false, end, skipLinkTarget, children, className, style, ...rest }, ref) => {
13
13
  const LinkTag = linkComponent ?? 'a';
14
14
  // The brand link is structural — an unsafe brandHref falls back to '/' rather
15
15
  // than unmounting the branding anchor.
@@ -20,6 +20,12 @@ export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHre
20
20
  const safeSkipTarget = skipLinkTarget && isSafeHref(skipLinkTarget) ? skipLinkTarget : undefined;
21
21
  // children always win; the icon variants only apply to a non-empty brandName.
22
22
  const iconBrand = !children && !!brandName && brand !== 'text';
23
+ // Mark-only collapse needs an icon to collapse to.
24
+ const collapseToMark = iconBrand && brandCollapse === 'mark';
25
+ const stacked = layout === 'stacked';
26
+ // contained: true → CSS default width; a non-blank string → that CSS length.
27
+ const containedWidth = typeof contained === 'string' ? contained.trim() : '';
28
+ const isContained = contained === true || containedWidth !== '';
23
29
  const brandContent = children || (iconBrand ? (_jsxs(_Fragment, { children: [brand === 'mark'
24
30
  ? _jsx(Mark, { size: 28, label: "", className: "eidotter-header__brand-icon" })
25
31
  : _jsx(Logo, { size: 28, glow: true, title: "", className: "eidotter-header__brand-icon" }), _jsx("span", { className: "eidotter-header__brand-name uppercase tracking-[0.3em]", children: brandName })] })) : brandName);
@@ -33,10 +39,22 @@ export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHre
33
39
  const hasItems = Array.isArray(items) && items.length > 0;
34
40
  const ActionsGroup = hasEnd ? 'div' : React.Fragment;
35
41
  const actionsProps = hasEnd ? { className: 'eidotter-header__actions flex items-center gap-4' } : {};
36
- const main = (_jsxs(_Fragment, { children: [_jsx(LinkTag, { href: safeBrandHref, className: cn('no-underline text-dos-text-base font-bold tracking-wide', 'eidotter-header__branding', iconBrand && 'eidotter-header__branding--icon inline-flex items-center gap-3'), children: brandContent }), _jsxs(ActionsGroup, { ...actionsProps, children: [hasItems && (_jsx(DesktopNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent })), hasEnd && _jsx("div", { className: "eidotter-header__end flex items-center", children: end }), hasItems && (_jsx(MobileNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent }))] })] }));
42
+ const desktopNav = hasItems ? (_jsx(DesktopNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent, className: stacked ? 'eidotter-header__nav-row' : undefined })) : null;
43
+ const main = (_jsxs(_Fragment, { children: [_jsx(LinkTag, { href: safeBrandHref, className: cn('no-underline text-dos-text-base font-bold tracking-wide', 'eidotter-header__branding', iconBrand && 'eidotter-header__branding--icon inline-flex items-start gap-3'), children: brandContent }), _jsxs(ActionsGroup, { ...actionsProps, children: [!stacked && desktopNav, hasEnd && _jsx("div", { className: "eidotter-header__end flex items-center", children: end }), hasItems && (_jsx(MobileNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent }))] })] }));
37
44
  const skipLink = safeSkipTarget ? (_jsx("a", { href: safeSkipTarget, className: "eidotter-header__skip", children: "Skip to content" })) : null;
38
- return (_jsxs("header", { ref: ref, className: cn(hasContext
39
- ? 'flex flex-col gap-2 px-4 py-3 font-dos'
40
- : 'flex items-center justify-between px-4 py-3 font-dos', 'eidotter-header', variantClasses[variant], sticky && 'sticky top-0 z-50 bg-dos-bg-primary', className), ...rest, children: [skipLink, hasContext ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "eidotter-header__main flex w-full items-center justify-between", children: main }), _jsx(HeaderContextRow, { context: context, linkComponent: linkComponent, activeHref: activeHref })] })) : (main)] }));
45
+ // Column layout whenever there is more than one row (context row, stacked nav
46
+ // row). The single-row inline header keeps its pre-0.42 DOM: no wrappers.
47
+ const columnLayout = hasContext || stacked;
48
+ const layoutClasses = columnLayout
49
+ ? 'flex flex-col gap-2'
50
+ : 'flex items-center justify-between';
51
+ const rows = columnLayout ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "eidotter-header__main flex w-full items-center justify-between", children: main }), stacked && desktopNav, hasContext && (_jsx(HeaderContextRow, { context: context, linkComponent: linkComponent, activeHref: activeHref }))] })) : (main);
52
+ const mergedStyle = containedWidth
53
+ ? { ...style, '--eidotter-header-max': containedWidth }
54
+ : style;
55
+ return (_jsxs("header", { ref: ref, className: cn(
56
+ // contained: the header is only the full-width band (padding, bg,
57
+ // sticky); the layout moves to the centred inner column.
58
+ isContained ? 'px-4 py-3 font-dos' : cn(layoutClasses, 'px-4 py-3 font-dos'), 'eidotter-header', variantClasses[variant], stacked && 'eidotter-header--stacked', isContained && 'eidotter-header--contained', !rule && 'eidotter-header--no-rule', collapseToMark && 'eidotter-header--collapse-mark', sticky && 'sticky top-0 z-50 bg-dos-bg-primary', className), style: mergedStyle, ...rest, children: [skipLink, isContained ? (_jsx("div", { className: cn('eidotter-header__inner w-full', layoutClasses), children: rows })) : (rows)] }));
41
59
  });
42
60
  Header.displayName = 'Header';
@@ -1,20 +1,20 @@
1
1
  import React, { FC } from 'react';
2
2
  type PixelIcon = FC<React.SVGProps<SVGSVGElement>>;
3
3
  declare const ICON_MAP: {
4
- Info: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
5
- Warning: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
6
- Error: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
7
- Done: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
8
- Check: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
4
+ Info: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
5
+ Warning: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
6
+ Error: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
7
+ Done: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
8
+ Check: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
9
9
  Close: PixelIcon;
10
- 'Chevron Up': (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
11
- 'Chevron Down': (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
12
- 'Chevron Left': (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
13
- 'Chevron Right': (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
14
- App: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
15
- Cancel: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
16
- Fullscreen: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
17
- Add: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
10
+ 'Chevron Up': (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
11
+ 'Chevron Down': (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
12
+ 'Chevron Left': (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
13
+ 'Chevron Right': (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
14
+ App: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
15
+ Cancel: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
16
+ Fullscreen: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
17
+ Add: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
18
18
  };
19
19
  export type IconName = keyof typeof ICON_MAP;
20
20
  /**
@@ -1,17 +1,17 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { InfoBox } from 'pixelarticons/react/InfoBox';
4
- import { WarningDiamond } from 'pixelarticons/react/WarningDiamond';
5
- import { SquareAlert } from 'pixelarticons/react/SquareAlert';
6
- import { Check } from 'pixelarticons/react/Check';
7
- import { ChevronUp } from 'pixelarticons/react/ChevronUp';
8
- import { ChevronDown } from 'pixelarticons/react/ChevronDown';
9
- import { ChevronLeft } from 'pixelarticons/react/ChevronLeft';
10
- import { ChevronRight } from 'pixelarticons/react/ChevronRight';
11
- import { WindowFrame } from 'pixelarticons/react/WindowFrame';
12
- import { Minus } from 'pixelarticons/react/Minus';
13
- import { Expand } from 'pixelarticons/react/Expand';
14
- import { Plus } from 'pixelarticons/react/Plus';
3
+ import { InfoBox } from 'pixelarticons/react/InfoBox.js';
4
+ import { WarningDiamond } from 'pixelarticons/react/WarningDiamond.js';
5
+ import { SquareAlert } from 'pixelarticons/react/SquareAlert.js';
6
+ import { Check } from 'pixelarticons/react/Check.js';
7
+ import { ChevronUp } from 'pixelarticons/react/ChevronUp.js';
8
+ import { ChevronDown } from 'pixelarticons/react/ChevronDown.js';
9
+ import { ChevronLeft } from 'pixelarticons/react/ChevronLeft.js';
10
+ import { ChevronRight } from 'pixelarticons/react/ChevronRight.js';
11
+ import { WindowFrame } from 'pixelarticons/react/WindowFrame.js';
12
+ import { Minus } from 'pixelarticons/react/Minus.js';
13
+ import { Expand } from 'pixelarticons/react/Expand.js';
14
+ import { Plus } from 'pixelarticons/react/Plus.js';
15
15
  import { cn } from '../../../utils/cn.js';
16
16
  /**
17
17
  * Custom pixel-art X mark — pixelarticons v2 has no standalone close/X glyph.
@@ -16,6 +16,13 @@ export interface LegalPageProps {
16
16
  home?: React.ReactNode;
17
17
  /** Body content — `<h2>`, `<p>`, `<ul>`, `<address>` children styled by the wrapper. */
18
18
  children: React.ReactNode;
19
+ /**
20
+ * Render inside the site's own page column (e.g. `PageShell`) instead of
21
+ * as a standalone page: no own 720px column, centring, padding, background
22
+ * or font, and a start-aligned hero without the rule under it. The type
23
+ * scale and body styles stay. Default `false`.
24
+ */
25
+ embedded?: boolean;
19
26
  /** Extra class on the outer wrapper */
20
27
  className?: string;
21
28
  }
@@ -8,4 +8,4 @@ import { cn } from '../../../utils/cn.js';
8
8
  * `<h2>`/`<p>`/`<ul>` body content (or pre-built clauses from this module)
9
9
  * and the wrapper handles spacing, type scale, and the `>` list bullet.
10
10
  */
11
- export const LegalPage = ({ title, date, intro, home, children, className, ...props }) => (_jsxs("div", { className: cn('eidotter-legal-page', className), ...props, children: [home && _jsx("div", { className: "eidotter-legal-page__home", children: home }), _jsxs("article", { children: [_jsxs("header", { className: "eidotter-legal-page__hero", children: [_jsx("p", { className: "eidotter-legal-page__date", children: date }), _jsx("h1", { children: title }), intro && _jsx("div", { className: "eidotter-legal-page__intro", children: intro })] }), _jsx("div", { className: "eidotter-legal-page__body", children: children })] })] }));
11
+ export const LegalPage = ({ title, date, intro, home, embedded = false, children, className, ...props }) => (_jsxs("div", { className: cn('eidotter-legal-page', embedded && 'eidotter-legal-page--embedded', className), ...props, children: [home && _jsx("div", { className: "eidotter-legal-page__home", children: home }), _jsxs("article", { children: [_jsxs("header", { className: "eidotter-legal-page__hero", children: [_jsx("p", { className: "eidotter-legal-page__date", children: date }), _jsx("h1", { children: title }), intro && _jsx("div", { className: "eidotter-legal-page__intro", children: intro })] }), _jsx("div", { className: "eidotter-legal-page__body", children: children })] })] }));
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ export interface PageSectionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
3
+ /**
4
+ * Section heading, in the display face and accent colour, with no rule under
5
+ * it. Omit for an untitled section.
6
+ */
7
+ title?: React.ReactNode;
8
+ /** Heading level. Default `2` (sections directly under the page `<h1>`). */
9
+ headingLevel?: 2 | 3 | 4;
10
+ /**
11
+ * Anchor id (TOC target). When set with a `title`, the heading gets
12
+ * `${id}-heading` and the section is labelled by it, so it becomes a named
13
+ * region.
14
+ */
15
+ id?: string;
16
+ children?: React.ReactNode;
17
+ className?: string;
18
+ }
19
+ /**
20
+ * PageSection: one block of a `PageShell` page (0.43). A `<section>` whose
21
+ * heading and children stack with a 16px gap. No divider above or below.
22
+ *
23
+ * Server-safe. Ref forwards to the `<section>`.
24
+ */
25
+ export declare const PageSection: React.ForwardRefExoticComponent<PageSectionProps & React.RefAttributes<HTMLElement>>;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { cn } from '../../../utils/cn.js';
4
+ /**
5
+ * PageSection: one block of a `PageShell` page (0.43). A `<section>` whose
6
+ * heading and children stack with a 16px gap. No divider above or below.
7
+ *
8
+ * Server-safe. Ref forwards to the `<section>`.
9
+ */
10
+ export const PageSection = forwardRef(({ title, headingLevel = 2, id, children, className, ...rest }, ref) => {
11
+ const hasTitle = title != null && title !== false && title !== '';
12
+ const Heading = `h${headingLevel}`;
13
+ const headingId = hasTitle && id ? `${id}-heading` : undefined;
14
+ return (_jsxs("section", { ref: ref, id: id, "aria-labelledby": headingId, className: cn('eidotter-page-section', className), ...rest, children: [hasTitle && (_jsx(Heading, { id: headingId, className: "eidotter-page-section__title", children: title })), children] }));
15
+ });
16
+ PageSection.displayName = 'PageSection';
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ export interface PageShellTocItem {
3
+ /** In-page anchor (`#findings`) or any safe URL. Unsafe schemes render no anchor. */
4
+ href: string;
5
+ /** Chip text. */
6
+ label: React.ReactNode;
7
+ }
8
+ export interface PageShellIntro {
9
+ /** Small uppercase line above the title (context, version, date). */
10
+ eyebrow?: React.ReactNode;
11
+ /** The page title, rendered as the page's `<h1>` in the display face. */
12
+ title: React.ReactNode;
13
+ /** Lead paragraph under the title (max 68ch). */
14
+ lead?: React.ReactNode;
15
+ /** A wrapping row of facts (author, branch, captured at, …). */
16
+ meta?: React.ReactNode;
17
+ /** Table-of-contents chips. Rendered as a labelled `<nav>` of links. */
18
+ toc?: PageShellTocItem[];
19
+ /** Accessible name of the TOC `<nav>`. Default `"On this page"`. */
20
+ tocLabel?: string;
21
+ }
22
+ export interface PageShellProps extends React.HTMLAttributes<HTMLDivElement> {
23
+ /**
24
+ * Top slot, inside the column so it shares the content's left edge.
25
+ * Typically `<Header layout="stacked" rule={false} …/>`; a Header placed
26
+ * here loses its own inline padding so its brand sits on the column edge.
27
+ */
28
+ header?: React.ReactNode;
29
+ /** Structured page intro: eyebrow, `<h1>` title, lead, meta row, TOC chips. */
30
+ intro?: PageShellIntro;
31
+ /** Bottom slot, inside the column (e.g. `<Footer>`). */
32
+ footer?: React.ReactNode;
33
+ /** Column max-width, any CSS length. Default `1240px`. */
34
+ maxWidth?: string;
35
+ /**
36
+ * Element that wraps `intro` and `children`. `'main'` (default) makes it the
37
+ * page's main landmark; use `'div'` when the app layout already renders a
38
+ * `<main>`.
39
+ */
40
+ contentAs?: 'main' | 'div';
41
+ /**
42
+ * `id` for the content element, e.g. a Header `skipLinkTarget` (`#content`).
43
+ * Also sets `tabIndex={-1}`, so following the skip link moves focus there.
44
+ */
45
+ contentId?: string;
46
+ /** Page sections (`PageSection`, Bench, …), stacked with a 40px gap. */
47
+ children?: React.ReactNode;
48
+ className?: string;
49
+ }
50
+ /**
51
+ * PageShell: the "one block" page layout (0.43).
52
+ *
53
+ * One centred column (1240px by default) with a `max(16px, safe-area)` side
54
+ * gutter and 24px / 64px block padding. Header, intro, sections and footer
55
+ * all live inside the column, so everything shares one left edge, stacked
56
+ * with a 40px gap. No dividers: the only lines on the page are the ones its
57
+ * children draw (window frames, pane gaps).
58
+ *
59
+ * Paints no background and sets no font: the page's own ground (a CRT tint,
60
+ * RetroEffects) and body font show through. Pass a `className` to paint
61
+ * an opaque ground.
62
+ *
63
+ * Server-safe and presentational. Ref forwards to the root `<div>`.
64
+ */
65
+ export declare const PageShell: React.ForwardRefExoticComponent<PageShellProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { cn } from '../../../utils/cn.js';
4
+ import { isSafeHref } from '../../../utils/isSafeHref.js';
5
+ function PageIntro({ intro }) {
6
+ const { eyebrow, title, lead, meta, toc, tocLabel = 'On this page' } = intro;
7
+ const has = (node) => node != null && node !== false && node !== '';
8
+ return (_jsxs("header", { className: "eidotter-page-shell__intro", children: [has(eyebrow) && _jsx("div", { className: "eidotter-page-shell__eyebrow", children: eyebrow }), _jsx("h1", { className: "eidotter-page-shell__title", children: title }), has(lead) && _jsx("p", { className: "eidotter-page-shell__lead", children: lead }), has(meta) && _jsx("div", { className: "eidotter-page-shell__meta", children: meta }), toc && toc.length > 0 && (_jsx("nav", { className: "eidotter-page-shell__toc", "aria-label": tocLabel, children: _jsx("ul", { className: "eidotter-page-shell__toc-list", children: toc.map((item, i) => (_jsx("li", { children: isSafeHref(item.href) ? (_jsx("a", { className: "eidotter-page-shell__toc-link", href: item.href, children: item.label })) : (_jsx("span", { className: "eidotter-page-shell__toc-link", children: item.label })) }, `${item.href}-${i}`))) }) }))] }));
9
+ }
10
+ /**
11
+ * PageShell: the "one block" page layout (0.43).
12
+ *
13
+ * One centred column (1240px by default) with a `max(16px, safe-area)` side
14
+ * gutter and 24px / 64px block padding. Header, intro, sections and footer
15
+ * all live inside the column, so everything shares one left edge, stacked
16
+ * with a 40px gap. No dividers: the only lines on the page are the ones its
17
+ * children draw (window frames, pane gaps).
18
+ *
19
+ * Paints no background and sets no font: the page's own ground (a CRT tint,
20
+ * RetroEffects) and body font show through. Pass a `className` to paint
21
+ * an opaque ground.
22
+ *
23
+ * Server-safe and presentational. Ref forwards to the root `<div>`.
24
+ */
25
+ export const PageShell = forwardRef(({ header, intro, footer, maxWidth, contentAs = 'main', contentId, children, className, style, ...rest }, ref) => {
26
+ const Content = contentAs;
27
+ const width = maxWidth?.trim();
28
+ const mergedStyle = width
29
+ ? { ...style, '--eidotter-page-shell-max': width }
30
+ : style;
31
+ return (_jsx("div", { ref: ref, className: cn('eidotter-page-shell', className), style: mergedStyle, ...rest, children: _jsxs("div", { className: "eidotter-page-shell__column", children: [header != null && header !== false && (_jsx("div", { className: "eidotter-page-shell__header", children: header })), _jsxs(Content, { id: contentId, tabIndex: contentId ? -1 : undefined, className: "eidotter-page-shell__content", children: [intro && _jsx(PageIntro, { intro: intro }), children] }), footer != null && footer !== false && (_jsx("div", { className: "eidotter-page-shell__footer", children: footer }))] }) }));
32
+ });
33
+ PageShell.displayName = 'PageShell';
@@ -0,0 +1,4 @@
1
+ export { PageShell } from './PageShell.js';
2
+ export type { PageShellProps, PageShellIntro, PageShellTocItem } from './PageShell.js';
3
+ export { PageSection } from './PageSection.js';
4
+ export type { PageSectionProps } from './PageSection.js';
@@ -0,0 +1,2 @@
1
+ export { PageShell } from './PageShell.js';
2
+ export { PageSection } from './PageSection.js';
@@ -0,0 +1,2 @@
1
+ export { PageShell, PageSection } from './components/index.js';
2
+ export type { PageShellProps, PageShellIntro, PageShellTocItem, PageSectionProps } from './components/index.js';
@@ -0,0 +1 @@
1
+ export { PageShell, PageSection } from './components/index.js';
@@ -688,8 +688,14 @@ export const componentRegistry = {
688
688
  'variant:modern': { description: 'Modern header without phosphor glow' },
689
689
  'context:categories': { description: 'Category badge row below the main row — measurable DOM target (stable eidotter-header__categories / __category classes + data-category-key) for the Mark arm-connector (DMNC-1325)', since: '0.39.0' },
690
690
  'context:returnTo': { description: 'Breadcrumb return pill — same-tab via linkComponent by default; reuseTab navigates via window.open(href, "rizomorf-shell") named-tab reuse', since: '0.39.0' },
691
+ 'layout:inline': { description: 'Brand left, nav (+ end) right, one row (default)', since: '0.43.0' },
692
+ 'layout:stacked': { description: 'Brand row on top (end + MENU on its right), desktop nav on its own row below, left-aligned on the brand edge', since: '0.43.0' },
693
+ 'brandCollapse:none': { description: 'Icon brands always show the name, wrapping when short of room (default)', since: '0.43.0' },
694
+ 'brandCollapse:mark': { description: 'Below 480px of header width (container query) only the icon shows; the name stays the accessible name', since: '0.43.0' },
691
695
  },
692
696
  changelog: [
697
+ { version: '0.43.0', type: 'added', description: '`layout` (inline | stacked), `rule` (false drops the bottom line and its glow), `contained` (true = 1240px column, or a CSS length, with the PageShell gutter, so the brand lines up with a page column) and `brandCollapse` (none | mark) props; all default to the previous rendering' },
698
+ { version: '0.43.0', type: 'changed', description: 'The brand name wraps instead of ellipsizing: the brand link shrinks first (the end slot clips only after the brand reaches its floor of icon + about 8 tracked characters), words break at spaces, a single over-long word breaks mid-word as a last resort; the icon top-aligns beside a two-line name' },
693
699
  { version: '0.41.3', type: 'changed', description: 'end slot clips the inline axis only (overflow-x: clip) with a 12px inline bleed, so focus rings and phosphor glows of controls in it are no longer cut; overflow: hidden fallback without clip support (DMNC-1611)' },
694
700
  { version: '0.41.3', type: 'changed', description: 'items={[]} renders no nav: no empty <nav> and no MENU trigger below 768px; the brand link gets the house focus ring (2px border-focus outline, 2px offset) in both variants (DMNC-1611)' },
695
701
  { version: '0.41.0', type: 'changed', description: 'Mobile Nav panel close is the shared IconButton (md 36x36, outline, hover invert) (DMNC-1592)' },
@@ -800,6 +806,30 @@ export const componentRegistry = {
800
806
  { version: '0.41.0', type: 'added', description: 'Initial DosWindow: title, marker, active, controls, footer, className; stable classes eidotter-window, __strip, __title, __controls, __body, __footer (DMNC-1592)' },
801
807
  ],
802
808
  },
809
+ Bench: {
810
+ origin: 'eidotter',
811
+ consumers: [],
812
+ since: '0.43.0',
813
+ originNote: 'Compare unit on DosWindow: title strip (heading after the ▸ marker) with wrapping status chips and an optional note, then before | after panes split by a 1px gap the grid background paints (no divider elements). Pane label row + keyboard-scrollable frame (max 640px, phone frame 390px); stacks below 760px of bench width (container query). From the review-page template (page-template-review-bench).',
814
+ platforms: { react: { path: 'src/components/Bench', status: 'canonical' } },
815
+ changelog: [
816
+ { version: '0.43.0', type: 'added', description: 'Initial Bench: title, headingLevel, chips, note, panes ({ label, content, tone, frame, frameLabel }), maxHeight; stable classes eidotter-bench, __panes, __pane, __label, __frame, __note' },
817
+ ],
818
+ },
819
+ PageShell: {
820
+ origin: 'eidotter',
821
+ consumers: [],
822
+ since: '0.43.0',
823
+ originNote: 'The "one block" page layout: one centred column (1240px default) with a max(16px, safe-area) gutter, 24/64px block padding, header/intro/sections/footer on one left edge with a 40px gap and no dividers. Structured intro (eyebrow, h1, lead, meta, TOC chips). PageSection (h2 in the display face, accent, no rule; 16px gap) ships alongside. Server-safe.',
824
+ variants: {
825
+ 'contentAs:main': { description: 'Content is the page main landmark (default)', since: '0.43.0' },
826
+ 'contentAs:div': { description: 'Content is a div, for app layouts that already render <main>', since: '0.43.0' },
827
+ },
828
+ platforms: { react: { path: 'src/components/PageShell', status: 'canonical' } },
829
+ changelog: [
830
+ { version: '0.43.0', type: 'added', description: 'Initial PageShell (header, intro, footer, maxWidth, contentAs, contentId) and PageSection (title, headingLevel, id); a Header in the header slot drops its inline padding so its brand sits on the column edge' },
831
+ ],
832
+ },
803
833
  Notification: {
804
834
  origin: 'eidotter',
805
835
  consumers: [],