eidotter 0.42.0 → 0.43.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.
- package/README.md +4 -4
- package/dist/components/Bench/components/Bench.d.ts +45 -0
- package/dist/components/Bench/components/Bench.js +46 -0
- package/dist/components/Bench/components/index.d.ts +2 -0
- package/dist/components/Bench/components/index.js +1 -0
- package/dist/components/Bench/index.d.ts +2 -0
- package/dist/components/Bench/index.js +1 -0
- package/dist/components/Header/components/Header.d.ts +38 -2
- package/dist/components/Header/components/Header.js +23 -5
- package/dist/components/PageShell/components/PageSection.d.ts +25 -0
- package/dist/components/PageShell/components/PageSection.js +16 -0
- package/dist/components/PageShell/components/PageShell.d.ts +58 -0
- package/dist/components/PageShell/components/PageShell.js +29 -0
- package/dist/components/PageShell/components/index.d.ts +4 -0
- package/dist/components/PageShell/components/index.js +2 -0
- package/dist/components/PageShell/index.d.ts +2 -0
- package/dist/components/PageShell/index.js +1 -0
- package/dist/components/registry.js +30 -0
- package/dist/eidotter.css +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.es.js +440 -219
- package/dist/index.umd.js +4 -4
- package/llms.txt +3 -3
- package/migrations/0.41.0.md +3 -2
- package/migrations/0.43.0.md +129 -0
- package/migrations/README.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
[](./LICENSE.md)
|
|
16
16
|
[](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.
|
|
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).
|
|
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.
|
|
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.43.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
|
-
|
|
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 & 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 @@
|
|
|
1
|
+
export { Bench } from './Bench.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Bench } from './components/index.js';
|
|
@@ -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).
|
|
64
|
-
*
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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';
|
|
@@ -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,58 @@
|
|
|
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
|
+
/** `id` for the content element, e.g. a Header `skipLinkTarget` (`#content`). */
|
|
42
|
+
contentId?: string;
|
|
43
|
+
/** Page sections (`PageSection`, Bench, …), stacked with a 40px gap. */
|
|
44
|
+
children?: React.ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* PageShell: the "one block" page layout (0.43).
|
|
49
|
+
*
|
|
50
|
+
* One centred column (1240px by default) with a `max(16px, safe-area)` side
|
|
51
|
+
* gutter and 24px / 64px block padding. Header, intro, sections and footer
|
|
52
|
+
* all live inside the column, so everything shares one left edge, stacked
|
|
53
|
+
* with a 40px gap. No dividers: the only lines on the page are the ones its
|
|
54
|
+
* children draw (window frames, pane gaps).
|
|
55
|
+
*
|
|
56
|
+
* Server-safe and presentational. Ref forwards to the root `<div>`.
|
|
57
|
+
*/
|
|
58
|
+
export declare const PageShell: React.ForwardRefExoticComponent<PageShellProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
* Server-safe and presentational. Ref forwards to the root `<div>`.
|
|
20
|
+
*/
|
|
21
|
+
export const PageShell = forwardRef(({ header, intro, footer, maxWidth, contentAs = 'main', contentId, children, className, style, ...rest }, ref) => {
|
|
22
|
+
const Content = contentAs;
|
|
23
|
+
const width = maxWidth?.trim();
|
|
24
|
+
const mergedStyle = width
|
|
25
|
+
? { ...style, '--eidotter-page-shell-max': width }
|
|
26
|
+
: style;
|
|
27
|
+
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, 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 }))] }) }));
|
|
28
|
+
});
|
|
29
|
+
PageShell.displayName = 'PageShell';
|
|
@@ -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: [],
|