eidotter 0.41.3 → 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 +6 -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/CmdPalette/components/CmdPalette.js +5 -3
- package/dist/components/Header/components/Header.d.ts +38 -2
- package/dist/components/Header/components/Header.js +23 -5
- package/dist/components/IconButton/components/IconButton.d.ts +20 -3
- package/dist/components/IconButton/components/IconButton.js +1 -1
- package/dist/components/IconButton/components/index.d.ts +1 -1
- package/dist/components/IconButton/index.d.ts +1 -1
- package/dist/components/Lightbox/components/Lightbox.d.ts +2 -1
- package/dist/components/Lightbox/components/Lightbox.js +8 -3
- package/dist/components/Nav/components/Nav.d.ts +20 -7
- package/dist/components/Nav/components/index.d.ts +1 -1
- package/dist/components/Nav/index.d.ts +1 -1
- 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/RangeBar/components/RangeBar.js +2 -2
- package/dist/components/registry.js +39 -0
- package/dist/eidotter.css +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.es.js +1047 -1176
- package/dist/index.umd.js +4 -4
- package/guidelines/README.md +37 -0
- package/guidelines/components.md +2 -2
- package/llms.txt +4 -4
- package/migrations/0.41.0.md +7 -4
- package/migrations/0.43.0.md +129 -0
- package/migrations/README.md +2 -0
- package/package.json +4 -4
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
|
|
|
@@ -59,6 +59,8 @@ import 'eidotter/utilities'; // Optional: .dos-* typography clas
|
|
|
59
59
|
|
|
60
60
|
Granular imports are still available (`eidotter/fonts.css`, `eidotter/tokens.css`) if you want to override the font or tokens independently.
|
|
61
61
|
|
|
62
|
+
**Fonts in your build output:** your bundler emits the four JetBrains Mono Nerd Font woff2 files (body tier, about 4 MB) even if you never use the body tier. Browsers only download faces a page uses, so this is deploy size, not page weight. To leave them out, see "Fonts: what your build emits" in [`guidelines/README.md`](guidelines/README.md).
|
|
63
|
+
|
|
62
64
|
**Favicon:** the logomark favicon ships as `eidotter/favicon.svg` (SVG only). Copy it into your site's public dir or point a bundler at it, then `<link rel="icon" href="/favicon.svg" type="image/svg+xml" />`.
|
|
63
65
|
|
|
64
66
|
### With Tailwind
|
|
@@ -114,9 +116,9 @@ for client apps and `'use client'` files.
|
|
|
114
116
|
|
|
115
117
|
## Upgrading
|
|
116
118
|
|
|
117
|
-
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).
|
|
118
120
|
|
|
119
|
-
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`.
|
|
120
122
|
|
|
121
123
|
## Documentation
|
|
122
124
|
|
|
@@ -131,7 +133,7 @@ Every minor with breaking or visible changes ships an upgrade guide in the packa
|
|
|
131
133
|
|
|
132
134
|
## Components
|
|
133
135
|
|
|
134
|
-
|
|
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.
|
|
135
137
|
|
|
136
138
|
<details>
|
|
137
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';
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
4
4
|
import { DosDialog } from '../../DosDialog/components/DosDialog.js';
|
|
5
5
|
import { cn } from '../../../utils/cn.js';
|
|
6
|
-
|
|
6
|
+
// Each key hint is one no-wrap unit, so a narrow palette wraps between hints
|
|
7
|
+
// ("ESC CLOSE" never splits across lines, DMNC-1612). Text is unchanged.
|
|
8
|
+
const defaultFooterHint = (_jsxs(_Fragment, { children: [_jsx("span", { className: "eidotter-cmdpal__key-hint", children: "\u2191/\u2193 NAVIGATE" }), ' · ', _jsx("span", { className: "eidotter-cmdpal__key-hint", children: "\u21B5 OPEN" }), ' · ', _jsx("span", { className: "eidotter-cmdpal__key-hint", children: "ESC CLOSE" })] }));
|
|
7
9
|
/** Split hotkey string like "mod+shift+p" into a predicate. */
|
|
8
10
|
function parseHotkey(hotkey) {
|
|
9
11
|
const parts = hotkey.toLowerCase().split('+').map(p => p.trim());
|
|
@@ -168,7 +170,7 @@ export const CmdPalette = forwardRef(({ open, onOpenChange, items, placeholder =
|
|
|
168
170
|
if (suppressHoverRef.current)
|
|
169
171
|
return;
|
|
170
172
|
setSelected(i);
|
|
171
|
-
}, onClick: () => handleSelect(item), children: renderItem ? renderItem(item, isSelected) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: cn('eidotter-cmdpal__item-label', isSelected && 'eidotter-cmdpal__text--bright'), children: [_jsx("span", { className: "eidotter-cmdpal__item-caret", "aria-hidden": "true", children: isSelected ? '▸' : ' ' }), item.label] }), item.hint !== undefined && (_jsx("span", { className: cn('eidotter-cmdpal__item-hint', isSelected ? 'eidotter-cmdpal__text--bright' : 'eidotter-cmdpal__text--muted'), children: item.hint }))] })) }, item.id));
|
|
173
|
+
}, onClick: () => handleSelect(item), children: renderItem ? renderItem(item, isSelected) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: cn('eidotter-cmdpal__item-label', isSelected && 'eidotter-cmdpal__text--bright'), children: [_jsx("span", { className: "eidotter-cmdpal__item-caret", "aria-hidden": "true", children: isSelected ? '▸' : ' ' }), _jsx("span", { className: "eidotter-cmdpal__item-text", children: item.label })] }), item.hint !== undefined && (_jsx("span", { className: cn('eidotter-cmdpal__item-hint', isSelected ? 'eidotter-cmdpal__text--bright' : 'eidotter-cmdpal__text--muted'), children: item.hint }))] })) }, item.id));
|
|
172
174
|
})] })] }));
|
|
173
175
|
});
|
|
174
176
|
CmdPalette.displayName = 'CmdPalette';
|
|
@@ -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';
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { PressEvent } from 'react-aria-components';
|
|
3
3
|
import type { IconName } from '../../Icon/components/Icon.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `md` = 36×36 hit area, 20px glyph (every built-in adopter). `window` = 28×28,
|
|
6
|
+
* 16px glyph, for dense chrome. `touch` = 44×44, 24px glyph, a full-size touch
|
|
7
|
+
* target for stage and kiosk shells.
|
|
8
|
+
*/
|
|
9
|
+
export type IconButtonSize = 'md' | 'window' | 'touch';
|
|
6
10
|
/** `outline` = 1px square outline at rest (default). `bare` = no resting outline. */
|
|
7
11
|
export type IconButtonVariant = 'outline' | 'bare';
|
|
12
|
+
/**
|
|
13
|
+
* `accent` = the house `text-accent` glyph with the amber hover invert
|
|
14
|
+
* (default). `inherit` = glyph, outline and focus ring take the surrounding
|
|
15
|
+
* `currentColor`, for coloured bars; hover and press tint the button with that
|
|
16
|
+
* colour instead of the amber fill.
|
|
17
|
+
*/
|
|
18
|
+
export type IconButtonTone = 'accent' | 'inherit';
|
|
8
19
|
export interface IconButtonProps {
|
|
9
20
|
/** Glyph from the `<Icon>` set: `Close` (X), `Cancel` (minus, minimise), `Fullscreen` (maximise), `Chevron Left/Right/Up/Down`. */
|
|
10
21
|
icon: IconName;
|
|
11
22
|
/** Accessible name. Required: the button has no visible text. Rendered as `aria-label`. */
|
|
12
23
|
label: string;
|
|
13
|
-
/** Hit-area size. Defaults to `md` (36×36). */
|
|
24
|
+
/** Hit-area size. Defaults to `md` (36×36); `touch` is 44×44. */
|
|
14
25
|
size?: IconButtonSize;
|
|
15
26
|
/**
|
|
16
27
|
* `outline` (default) shows the thin square outline at rest. `bare` drops
|
|
17
28
|
* it, for places where an outline would fight a neighbouring rule.
|
|
18
29
|
*/
|
|
19
30
|
variant?: IconButtonVariant;
|
|
31
|
+
/**
|
|
32
|
+
* Colour source. `accent` (default) is the house look. `inherit` uses the
|
|
33
|
+
* parent's text colour (`currentColor`) for the glyph, outline and focus
|
|
34
|
+
* ring, so the button reads on a coloured status bar; set the bar's `color`.
|
|
35
|
+
*/
|
|
36
|
+
tone?: IconButtonTone;
|
|
20
37
|
/** Disables the button (opacity 0.5, no hover invert). */
|
|
21
38
|
isDisabled?: boolean;
|
|
22
39
|
/** Called on press (mouse, touch, Enter, Space). */
|
|
@@ -15,5 +15,5 @@ import { cn } from '../../../utils/cn.js';
|
|
|
15
15
|
* React Aria `Button` handles press, keyboard and focus-visible. Ref forwards
|
|
16
16
|
* to the `<button>`.
|
|
17
17
|
*/
|
|
18
|
-
export const IconButton = forwardRef(({ icon, label, size = 'md', variant = 'outline', isDisabled, onPress, excludeFromTabOrder, className, }, ref) => (_jsx(AriaButton, { ref: ref, "aria-label": label, isDisabled: isDisabled, onPress: onPress, excludeFromTabOrder: excludeFromTabOrder, className: cn('inline-flex items-center justify-center shrink-0', 'eidotter-icon-button', size === 'window' && 'eidotter-icon-button--window', variant === 'bare' && 'eidotter-icon-button--bare', className), children: _jsx("span", { className: "eidotter-icon-button__glyph", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: "S", decorative: true }) }) })));
|
|
18
|
+
export const IconButton = forwardRef(({ icon, label, size = 'md', variant = 'outline', tone = 'accent', isDisabled, onPress, excludeFromTabOrder, className, }, ref) => (_jsx(AriaButton, { ref: ref, "aria-label": label, isDisabled: isDisabled, onPress: onPress, excludeFromTabOrder: excludeFromTabOrder, className: cn('inline-flex items-center justify-center shrink-0', 'eidotter-icon-button', size === 'window' && 'eidotter-icon-button--window', size === 'touch' && 'eidotter-icon-button--touch', variant === 'bare' && 'eidotter-icon-button--bare', tone === 'inherit' && 'eidotter-icon-button--inherit', className), children: _jsx("span", { className: "eidotter-icon-button__glyph", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: "S", decorative: true }) }) })));
|
|
19
19
|
IconButton.displayName = 'IconButton';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { IconButton } from './IconButton.js';
|
|
2
|
-
export type { IconButtonProps, IconButtonSize, IconButtonVariant } from './IconButton.js';
|
|
2
|
+
export type { IconButtonProps, IconButtonSize, IconButtonVariant, IconButtonTone } from './IconButton.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { IconButton } from './components/index.js';
|
|
2
|
-
export type { IconButtonProps, IconButtonSize, IconButtonVariant } from './components/index.js';
|
|
2
|
+
export type { IconButtonProps, IconButtonSize, IconButtonVariant, IconButtonTone } from './components/index.js';
|
|
@@ -18,7 +18,8 @@ export interface LightboxProps {
|
|
|
18
18
|
* The shell supplies the overlay, chrome, motion and the title strip;
|
|
19
19
|
* the `[ n / N ]` counter is the strip's title and the close control sits at
|
|
20
20
|
* its end (an IconButton). Lightbox adds prev/next (IconButtons with the pixel
|
|
21
|
-
* chevrons), caption, keyboard nav, and
|
|
21
|
+
* chevrons; omitted when there is only one image), caption, keyboard nav, and
|
|
22
|
+
* touch swipe. A figure taller than the body scrolls from its top edge.
|
|
22
23
|
* It shares the shell's z-index (`--z-index-modal`) with Modal; stacking a
|
|
23
24
|
* Lightbox over an open Modal is not a supported case.
|
|
24
25
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
4
4
|
import { IconButton } from '../../IconButton/components/IconButton.js';
|
|
5
5
|
import { DosDialog } from '../../DosDialog/components/DosDialog.js';
|
|
@@ -9,7 +9,8 @@ import { DosDialog } from '../../DosDialog/components/DosDialog.js';
|
|
|
9
9
|
* The shell supplies the overlay, chrome, motion and the title strip;
|
|
10
10
|
* the `[ n / N ]` counter is the strip's title and the close control sits at
|
|
11
11
|
* its end (an IconButton). Lightbox adds prev/next (IconButtons with the pixel
|
|
12
|
-
* chevrons), caption, keyboard nav, and
|
|
12
|
+
* chevrons; omitted when there is only one image), caption, keyboard nav, and
|
|
13
|
+
* touch swipe. A figure taller than the body scrolls from its top edge.
|
|
13
14
|
* It shares the shell's z-index (`--z-index-modal`) with Modal; stacking a
|
|
14
15
|
* Lightbox over an open Modal is not a supported case.
|
|
15
16
|
*
|
|
@@ -96,10 +97,14 @@ export const Lightbox = ({ images, initialIndex = 0, isOpen, onClose, onIndexCha
|
|
|
96
97
|
const current = images[safeIndex];
|
|
97
98
|
const hasPrev = safeIndex > 0;
|
|
98
99
|
const hasNext = safeIndex < images.length - 1;
|
|
100
|
+
// One image: nothing to navigate, so no prev/next at all (two permanently
|
|
101
|
+
// disabled buttons are noise). The `[ 1 / 1 ]` counter stays as the strip
|
|
102
|
+
// title, so the strip keeps its heading and the position stays explicit.
|
|
103
|
+
const canNavigate = images.length > 1;
|
|
99
104
|
return (_jsx(DosDialog, { open: isOpen, onOpenChange: (open) => { if (!open)
|
|
100
105
|
onClose(); }, placement: "center", size: "full", "aria-label": current.alt || 'Image viewer', className: "eidotter-lightbox__container", head: {
|
|
101
106
|
title: (_jsxs("span", { className: "eidotter-lightbox__counter", "aria-live": "polite", children: ["[ ", safeIndex + 1, " / ", images.length, " ]"] })),
|
|
102
107
|
end: (_jsx(IconButton, { icon: "Close", label: "Close lightbox", className: "eidotter-lightbox__close", onPress: onClose })),
|
|
103
|
-
}, children: _jsxs("figure", { className: "eidotter-lightbox__figure", children: [_jsx("img", { className: "eidotter-lightbox__image", src: current.src, alt: current.alt, width: current.width, height: current.height, onTouchStart: onTouchStart, onTouchEnd: onTouchEnd }), _jsx(IconButton, { icon: "Chevron Left", label: "Previous image", className: "eidotter-lightbox__nav eidotter-lightbox__nav--prev", onPress: goPrev, isDisabled: !hasPrev }), _jsx(IconButton, { icon: "Chevron Right", label: "Next image", className: "eidotter-lightbox__nav eidotter-lightbox__nav--next", onPress: goNext, isDisabled: !hasNext }), current.caption && (_jsx("figcaption", { className: "eidotter-lightbox__caption", children: current.caption }))] }) }));
|
|
108
|
+
}, children: _jsxs("figure", { className: "eidotter-lightbox__figure", children: [_jsx("img", { className: "eidotter-lightbox__image", src: current.src, alt: current.alt, width: current.width, height: current.height, onTouchStart: onTouchStart, onTouchEnd: onTouchEnd }), canNavigate && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: "Chevron Left", label: "Previous image", className: "eidotter-lightbox__nav eidotter-lightbox__nav--prev", onPress: goPrev, isDisabled: !hasPrev }), _jsx(IconButton, { icon: "Chevron Right", label: "Next image", className: "eidotter-lightbox__nav eidotter-lightbox__nav--next", onPress: goNext, isDisabled: !hasNext })] })), current.caption && (_jsx("figcaption", { className: "eidotter-lightbox__caption", children: current.caption }))] }) }));
|
|
104
109
|
};
|
|
105
110
|
Lightbox.displayName = 'Lightbox';
|
|
@@ -5,6 +5,24 @@ export interface NavItem {
|
|
|
5
5
|
/** URL/path for the link */
|
|
6
6
|
href: string;
|
|
7
7
|
}
|
|
8
|
+
/** Props eidotter passes to a custom link component. */
|
|
9
|
+
export interface NavLinkProps {
|
|
10
|
+
href: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
'aria-current'?: 'page';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A router link component for `linkComponent`: any component that accepts
|
|
18
|
+
* {@link NavLinkProps}. Its own props may be wider (Next.js `Link` takes
|
|
19
|
+
* `href: string | UrlObject`). Function and class components match
|
|
20
|
+
* `ComponentType`; `forwardRef`/`memo` components such as `next/link` match
|
|
21
|
+
* `ExoticComponent`, which has no `propTypes` member, so the `propTypes`
|
|
22
|
+
* comparison that rejected `next/link` under @types/react 18 does not apply
|
|
23
|
+
* (DMNC-1612). Works with @types/react 18 and 19, no wrapper needed.
|
|
24
|
+
*/
|
|
25
|
+
export type NavLinkComponent = React.ComponentType<NavLinkProps> | React.ExoticComponent<NavLinkProps>;
|
|
8
26
|
export interface NavProps {
|
|
9
27
|
/** Navigation items */
|
|
10
28
|
items: NavItem[];
|
|
@@ -13,15 +31,10 @@ export interface NavProps {
|
|
|
13
31
|
/** Visual variant */
|
|
14
32
|
variant?: 'retro' | 'modern';
|
|
15
33
|
/**
|
|
16
|
-
* Custom link component (e.g
|
|
34
|
+
* Custom link component (e.g. Next.js `Link`, passed directly).
|
|
17
35
|
* If not provided, uses regular anchor tags.
|
|
18
36
|
*/
|
|
19
|
-
linkComponent?:
|
|
20
|
-
href: string;
|
|
21
|
-
className?: string;
|
|
22
|
-
children: React.ReactNode;
|
|
23
|
-
onClick?: () => void;
|
|
24
|
-
}>;
|
|
37
|
+
linkComponent?: NavLinkComponent;
|
|
25
38
|
/** Additional CSS class name */
|
|
26
39
|
className?: string;
|
|
27
40
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Nav, DesktopNav, MobileNav } from './Nav.js';
|
|
2
|
-
export type { NavProps, NavItem } from './Nav.js';
|
|
2
|
+
export type { NavProps, NavItem, NavLinkProps, NavLinkComponent } from './Nav.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Nav, DesktopNav, MobileNav } from './components/index.js';
|
|
2
|
-
export type { NavProps, NavItem } from './components/index.js';
|
|
2
|
+
export type { NavProps, NavItem, NavLinkProps, NavLinkComponent } from './components/index.js';
|
|
@@ -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';
|