eidotter 0.40.2 → 0.41.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 (97) hide show
  1. package/README.md +13 -5
  2. package/dist/components/Accordion/components/AccordionFill.js +1 -1
  3. package/dist/components/Accordion/components/Section.js +1 -1
  4. package/dist/components/Alert/components/Alert.js +3 -2
  5. package/dist/components/Brand/components/Logo.d.ts +7 -7
  6. package/dist/components/Brand/components/Logo.js +10 -15
  7. package/dist/components/Brand/components/LogoGeometry.d.ts +28 -0
  8. package/dist/components/Brand/components/LogoGeometry.js +78 -0
  9. package/dist/components/Brand/components/Mark.d.ts +13 -6
  10. package/dist/components/Brand/components/Mark.js +14 -10
  11. package/dist/components/Brand/components/Wordmark.js +1 -1
  12. package/dist/components/Brand/components/markGeometry.d.ts +15 -0
  13. package/dist/components/Brand/components/markGeometry.js +20 -7
  14. package/dist/components/Breadcrumb/components/Breadcrumb.js +2 -2
  15. package/dist/components/Button/components/Button.js +1 -1
  16. package/dist/components/Card/components/Card.d.ts +5 -0
  17. package/dist/components/Card/components/Card.js +8 -2
  18. package/dist/components/CmdPalette/components/CmdPalette.d.ts +4 -3
  19. package/dist/components/CmdPalette/components/CmdPalette.js +17 -12
  20. package/dist/components/DecayStrip/components/DecayStrip.js +6 -1
  21. package/dist/components/DosDialog/components/DosDialog.d.ts +77 -0
  22. package/dist/components/DosDialog/components/DosDialog.js +59 -0
  23. package/dist/components/DosDialog/components/index.d.ts +2 -0
  24. package/dist/components/DosDialog/components/index.js +1 -0
  25. package/dist/components/DosDialog/index.d.ts +2 -0
  26. package/dist/components/DosDialog/index.js +1 -0
  27. package/dist/components/DosFigure/components/DosFigure.d.ts +2 -2
  28. package/dist/components/DosFigure/components/DosFigure.js +14 -7
  29. package/dist/components/DosWindow/components/DosWindow.d.ts +39 -0
  30. package/dist/components/DosWindow/components/DosWindow.js +23 -0
  31. package/dist/components/DosWindow/components/index.d.ts +2 -0
  32. package/dist/components/DosWindow/components/index.js +1 -0
  33. package/dist/components/DosWindow/index.d.ts +2 -0
  34. package/dist/components/DosWindow/index.js +1 -0
  35. package/dist/components/FilterBar/components/FilterBar.d.ts +7 -4
  36. package/dist/components/Footer/components/Footer.js +1 -1
  37. package/dist/components/Gauge/components/Gauge.js +19 -2
  38. package/dist/components/Header/components/Header.d.ts +29 -1
  39. package/dist/components/Header/components/Header.js +20 -5
  40. package/dist/components/Icon/components/Icon.d.ts +2 -0
  41. package/dist/components/Icon/components/Icon.js +5 -1
  42. package/dist/components/IconButton/components/IconButton.d.ts +44 -0
  43. package/dist/components/IconButton/components/IconButton.js +19 -0
  44. package/dist/components/IconButton/components/index.d.ts +2 -0
  45. package/dist/components/IconButton/components/index.js +1 -0
  46. package/dist/components/IconButton/index.d.ts +2 -0
  47. package/dist/components/IconButton/index.js +1 -0
  48. package/dist/components/Input/components/Input.js +1 -1
  49. package/dist/components/Lightbox/components/Lightbox.d.ts +10 -4
  50. package/dist/components/Lightbox/components/Lightbox.js +19 -11
  51. package/dist/components/Meter/components/Meter.js +7 -3
  52. package/dist/components/Meter/components/MeterGroup.js +3 -3
  53. package/dist/components/Modal/components/Modal.d.ts +18 -6
  54. package/dist/components/Modal/components/Modal.js +18 -9
  55. package/dist/components/Nav/components/Nav.js +5 -4
  56. package/dist/components/Notification/components/Notification.js +3 -2
  57. package/dist/components/Progress/components/Progress.js +8 -6
  58. package/dist/components/SeriesBars/components/SeriesBars.js +9 -8
  59. package/dist/components/Tag/components/Tag.d.ts +8 -1
  60. package/dist/components/Tag/components/Tag.js +5 -5
  61. package/dist/components/VitalsStrip/components/VitalsStrip.js +10 -7
  62. package/dist/components/registry.js +221 -19
  63. package/dist/eidotter.css +1 -1
  64. package/dist/hooks/useAnimatedDismiss.js +5 -0
  65. package/dist/index.d.ts +8 -1
  66. package/dist/index.es.js +2174 -1600
  67. package/dist/index.umd.js +1 -1
  68. package/llms.txt +15 -7
  69. package/migrations/0.41.0.md +881 -0
  70. package/migrations/README.md +46 -0
  71. package/package.json +10 -2
  72. package/public/favicon.svg +1 -1
  73. package/src/styles/dos-utilities.css +6 -1
  74. package/src/styles/shadcn.css +52 -17
  75. package/src/styles/theme.amber-mono.css +4 -2
  76. package/src/styles/theme.amber-paper.css +106 -0
  77. package/src/styles/theme.cga-amber.css +3 -1
  78. package/src/styles/theme.cga-mode4-p0.css +3 -1
  79. package/src/styles/theme.cga-mode4-p1.css +3 -1
  80. package/src/styles/theme.cga-mode5.css +3 -1
  81. package/src/styles/theme.light.css +3 -1
  82. package/src/styles/tokens.css +5 -1
  83. package/tailwind.preset.cjs +23 -1
  84. package/dist/components/Brand/components/markGeometry.test.d.ts +0 -1
  85. package/dist/components/Connector/components/connectorGeometry.test.d.ts +0 -1
  86. package/dist/components/DecayStrip/components/resample.test.d.ts +0 -1
  87. package/dist/components/TimelineContainer/components/timelineUtils.test.d.ts +0 -1
  88. package/dist/components/TimelineContainer/components/useDrillDown.test.d.ts +0 -1
  89. package/dist/components/TimelineContainer/components/useSelection.test.d.ts +0 -1
  90. package/dist/components/TimelineContainer/components/useZoom.test.d.ts +0 -1
  91. package/dist/hooks/useTextScramble.test.d.ts +0 -1
  92. package/dist/hooks/useThemePortal.test.d.ts +0 -1
  93. package/dist/utils/health/formatValue.test.d.ts +0 -1
  94. package/dist/utils/health/healthPatterns.test.d.ts +0 -1
  95. package/dist/utils/health/resolveBand.test.d.ts +0 -1
  96. package/dist/utils/isSafeHref.test.d.ts +0 -1
  97. package/dist/utils/isSafeUrl.test.d.ts +0 -1
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
4
- import { ModalOverlay as AriaModalOverlay, Modal as AriaModal, Dialog as AriaDialog, } from 'react-aria-components';
4
+ import { DosDialog } from '../../DosDialog/components/DosDialog.js';
5
5
  import { cn } from '../../../utils/cn.js';
6
6
  const defaultFooterHint = (_jsx(_Fragment, { children: "\u2191/\u2193 NAVIGATE \u00B7 \u21B5 OPEN \u00B7 ESC CLOSE" }));
7
7
  /** Split hotkey string like "mod+shift+p" into a predicate. */
@@ -50,9 +50,10 @@ function scoreItem(item, query) {
50
50
  * Controlled component. Parent owns `open` and item list; CmdPalette handles
51
51
  * search, keyboard navigation, focus management, and hotkey binding.
52
52
  *
53
- * Built on React Aria `ModalOverlay` + `Modal` + `Dialog` — same primitives
54
- * as `<Modal>` — so focus trap, Esc-to-close, and backdrop dismissal are
55
- * standard.
53
+ * Built on the DosDialog shell (DMNC-1592), top placement, `lg` width: the
54
+ * shell supplies focus trap, Esc-to-close, backdrop dismissal, chrome, motion
55
+ * and the amber `▸ JUMP TO` strip (decorative; the dialog is named by
56
+ * `aria-label`).
56
57
  */
57
58
  export const CmdPalette = forwardRef(({ open, onOpenChange, items, placeholder = 'SEARCH · TYPE · ▸', hotkey = 'mod+k', emptyMessage = 'NO MATCHES', footerHint = defaultFooterHint, renderItem, maxResults = 20, className, 'aria-label': ariaLabel = 'Command palette', }, ref) => {
58
59
  const baseId = useId();
@@ -157,13 +158,17 @@ export const CmdPalette = forwardRef(({ open, onOpenChange, items, placeholder =
157
158
  }
158
159
  }
159
160
  };
160
- return (_jsx(AriaModalOverlay, { isOpen: open, onOpenChange: onOpenChange, isDismissable: true, className: ({ isEntering, isExiting }) => cn('eidotter-cmdpal-overlay', isEntering && 'eidotter-cmdpal-overlay--entering', isExiting && 'eidotter-cmdpal-overlay--exiting'), children: _jsx(AriaModal, { className: ({ isEntering, isExiting }) => cn('eidotter-cmdpal', isEntering && 'eidotter-cmdpal--entering', isExiting && 'eidotter-cmdpal--exiting'), children: _jsxs(AriaDialog, { "aria-label": ariaLabel, className: cn('eidotter-cmdpal__container outline-none', className), ref: ref, children: [_jsxs("div", { className: "eidotter-cmdpal__head", "aria-hidden": "true", children: [_jsx("span", { children: "\u25B8 JUMP TO" }), _jsx("span", { className: "eidotter-cmdpal__blink", 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) => {
161
- const isSelected = i === selected;
162
- 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: () => {
163
- if (suppressHoverRef.current)
164
- return;
165
- setSelected(i);
166
- }, onClick: () => handleSelect(item), children: renderItem ? renderItem(item, isSelected) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "eidotter-cmdpal__item-label", children: [_jsx("span", { className: "eidotter-cmdpal__item-caret", "aria-hidden": "true", children: isSelected ? '▸' : ' ' }), item.label] }), item.hint !== undefined && (_jsx("span", { className: "eidotter-cmdpal__item-hint", children: item.hint }))] })) }, item.id));
167
- })] }), footerHint && (_jsx("div", { className: "eidotter-cmdpal__footer", "aria-hidden": "true", children: footerHint }))] }) }) }));
161
+ return (_jsxs(DosDialog, { ref: ref, open: open, onOpenChange: onOpenChange, placement: "top", size: "lg", "aria-label": ariaLabel, className: cn('eidotter-cmdpal__container', className), head: {
162
+ title: 'JUMP TO',
163
+ end: _jsx("span", { className: "eidotter-cmdpal__blink", children: "_" }),
164
+ decorative: true,
165
+ }, 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) => {
166
+ const isSelected = i === selected;
167
+ 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: () => {
168
+ if (suppressHoverRef.current)
169
+ return;
170
+ 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));
172
+ })] })] }));
168
173
  });
169
174
  CmdPalette.displayName = 'CmdPalette';
@@ -88,7 +88,12 @@ export const DecayStrip = forwardRef(({ items, samples, now, window, provenance,
88
88
  .filter(Boolean)
89
89
  .join('; ');
90
90
  const compact = size === 'sm';
91
- return (_jsxs("div", { ref: ref, className: cn('eidotter-decay-strip eidotter-health flex flex-col gap-2 w-full min-w-0 font-dos text-dos-text-brand', sizeClasses[size] ?? sizeClasses.md, toneClasses[tone], className), ...props, children: [items.length === 0 ? (_jsx(EmptyState, { title: `${zeroPercent} ${labels.remaining}`, className: "py-2" })) : (_jsx("ul", { className: cn('flex gap-2 min-w-0', compact ? 'flex-col' : 'flex-wrap'), children: rows.map((r) => (_jsx("li", { className: cn('eidotter-decay-strip__item min-w-0', compact ? 'flex flex-wrap items-baseline gap-2 tabular-nums' : 'flex-1'), children: compact ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-dos-text-accent", title: r.item.label, children: r.item.label }), _jsx("span", { children: r.amountText }), _jsx("span", { children: r.shareText }), r.tagText && (_jsx("span", { className: "eidotter-decay-strip__item-provenance border border-dos-border-brand px-1", children: r.tagText }))] })) : (_jsx(Gauge, { label: r.item.label, value: r.share === null ? null : r.share * 100, secondary: r.item.amount === null ? undefined : r.amountText, size: "sm", tone: tone, provenance: r.item.provenance, locale: locale, labels: labelOverrides })) }, r.item.id))) })), _jsxs("div", { className: "eidotter-decay-strip__strip flex items-center gap-2 min-w-0 tabular-nums", role: "img", "aria-label": summary, children: [_jsx("span", { className: "eidotter-decay-strip__from shrink-0", children: fromText }), _jsx("span", { className: "eidotter-decay-strip__cells flex-1 min-w-0 overflow-hidden whitespace-nowrap leading-none", style: tone === 'neutral' ? undefined : { color: 'var(--eidotter-fill)' }, children: cells.map((cell, i) => {
91
+ return (_jsxs("div", { ref: ref, className: [
92
+ // The size class (a text-* font size) is joined outside cn(): twMerge
93
+ // reads it as a colour and would drop text-dos-text-brand.
94
+ cn('eidotter-decay-strip eidotter-health flex flex-col gap-2 w-full min-w-0 font-dos text-dos-text-brand', toneClasses[tone], className),
95
+ sizeClasses[size] ?? sizeClasses.md,
96
+ ].join(' '), ...props, children: [items.length === 0 ? (_jsx(EmptyState, { title: `${zeroPercent} ${labels.remaining}`, className: "py-2" })) : (_jsx("ul", { className: cn('flex gap-2 min-w-0', compact ? 'flex-col' : 'flex-wrap'), children: rows.map((r) => (_jsx("li", { className: cn('eidotter-decay-strip__item min-w-0', compact ? 'flex flex-wrap items-baseline gap-2 tabular-nums' : 'flex-1'), children: compact ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-dos-text-accent", title: r.item.label, children: r.item.label }), _jsx("span", { children: r.amountText }), _jsx("span", { children: r.shareText }), r.tagText && (_jsx("span", { className: "eidotter-decay-strip__item-provenance border border-dos-border-brand px-1", children: r.tagText }))] })) : (_jsx(Gauge, { label: r.item.label, value: r.share === null ? null : r.share * 100, secondary: r.item.amount === null ? undefined : r.amountText, size: "sm", tone: tone, provenance: r.item.provenance, locale: locale, labels: labelOverrides })) }, r.item.id))) })), _jsxs("div", { className: "eidotter-decay-strip__strip flex items-center gap-2 min-w-0 tabular-nums", role: "img", "aria-label": summary, children: [_jsx("span", { className: "eidotter-decay-strip__from shrink-0", children: fromText }), _jsx("span", { className: "eidotter-decay-strip__cells flex flex-1 min-w-0 overflow-hidden whitespace-nowrap leading-none", style: tone === 'neutral' ? undefined : { color: 'var(--eidotter-fill)' }, children: cells.map((cell, i) => {
92
97
  const last = i === cells.length - 1;
93
98
  const isNow = nowMs >= cell.start && (nowMs < cell.end || (last && nowMs === cell.end));
94
99
  const future = !isNow && nowMs < cell.start && nowMs >= cells[0].start;
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ export type DosDialogPlacement = 'center' | 'top';
3
+ export type DosDialogSize = 'sm' | 'md' | 'lg' | 'full';
4
+ /** Structured head for the title strip. */
5
+ export interface DosDialogHead {
6
+ /** Title text. Rendered as the dialog's `<h2>` unless `decorative` is set. */
7
+ title: React.ReactNode;
8
+ /** Trailing slot on the right of the strip: close button, status, controls. */
9
+ end?: React.ReactNode;
10
+ /** Show the leading `▸` marker. Defaults to `true`. */
11
+ marker?: boolean;
12
+ /**
13
+ * Visual-only strip: hidden from assistive tech, and the title renders as a
14
+ * `<span>` instead of an `<h2>`. Use when the dialog is named by `aria-label`
15
+ * and the strip repeats nothing a screen reader needs (CmdPalette's
16
+ * `JUMP TO` banner). Do not put focusable controls in `end` when this is set.
17
+ */
18
+ decorative?: boolean;
19
+ }
20
+ export interface DosDialogProps {
21
+ /** Controlled open state. */
22
+ open: boolean;
23
+ /** Called when the dialog requests open/close (Escape, backdrop click). */
24
+ onOpenChange?: (open: boolean) => void;
25
+ /** Vertical anchor: `center` (default) or `top` (~18vh, CmdPalette's anchor). */
26
+ placement?: DosDialogPlacement;
27
+ /**
28
+ * Width step, shared by every dialog: `sm` 24rem, `md` 30rem (default),
29
+ * `lg` 40rem, `full` = viewport width minus 2 × spacing-4. Every step is
30
+ * capped at the viewport minus 32px.
31
+ */
32
+ size?: DosDialogSize;
33
+ /**
34
+ * The title strip (dark, yellow title, dim-amber rule). Pass `{ title, end?, marker?, decorative? }` for the
35
+ * standard strip, or any ReactNode to fill the strip yourself. Omit for no strip.
36
+ */
37
+ head?: React.ReactNode | DosDialogHead;
38
+ /** Footer row, separated from the body by a 1px rule. Rendered in a `<div>` (no contentinfo landmark). */
39
+ footer?: React.ReactNode;
40
+ /** Close on backdrop click / outside interaction. Escape always closes. Defaults to `true`. */
41
+ isDismissable?: boolean;
42
+ /**
43
+ * Accessible name. When omitted, a non-decorative `head.title` names the dialog.
44
+ * A ReactNode head, a `decorative` head or no head names nothing: pass
45
+ * `aria-label` or `aria-labelledby` then, or the dialog has no accessible
46
+ * name (outside production builds, an open unnamed dialog logs a `console.warn`).
47
+ */
48
+ 'aria-label'?: string;
49
+ /** Id of the element that names the dialog. */
50
+ 'aria-labelledby'?: string;
51
+ /** Extra classes merged onto the visible container (`.eidotter-dialog__container`). */
52
+ className?: string;
53
+ /** Dialog body. */
54
+ children?: React.ReactNode;
55
+ }
56
+ /**
57
+ * DosDialog: the one overlay shell (DMNC-1592).
58
+ *
59
+ * The CmdPalette chrome, generalised: brand-coloured medium border with an
60
+ * amber phosphor glow, square corners, a blurred scrim, and a dark title
61
+ * strip (yellow title, `▸` marker, dim-amber rule below). Modal, Lightbox and
62
+ * CmdPalette are built on it.
63
+ *
64
+ * React Aria `ModalOverlay > Modal > Dialog` provides the focus trap, scroll
65
+ * lock, Escape and backdrop dismissal. The panel pops in/out with the shared
66
+ * `eidotter-pop-in/out` keyframes (scale 0.8 to 1, the CRT turn-on) and the
67
+ * scrim fades; under reduced motion the entering/exiting classes are not applied at all.
68
+ *
69
+ * Accessible name: a non-decorative structured `head.title` names the dialog;
70
+ * otherwise pass `aria-label` / `aria-labelledby`. Outside production builds
71
+ * an open dialog with no name logs a `console.warn`.
72
+ *
73
+ * The strip title is one line with an ellipsis by default; Modal lets it wrap.
74
+ *
75
+ * Ref forwards to the dialog element (`.eidotter-dialog__container`).
76
+ */
77
+ export declare const DosDialog: React.ForwardRefExoticComponent<DosDialogProps & React.RefAttributes<HTMLElement>>;
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, isValidElement, useEffect, useId } from 'react';
4
+ import { ModalOverlay as AriaModalOverlay, Modal as AriaModal, Dialog as AriaDialog, } from 'react-aria-components';
5
+ import { cn } from '../../../utils/cn.js';
6
+ import { prefersReducedMotion } from '../../../utils/prefersReducedMotion.js';
7
+ function isHeadObject(head) {
8
+ return (typeof head === 'object' &&
9
+ head !== null &&
10
+ !isValidElement(head) &&
11
+ !Array.isArray(head) &&
12
+ 'title' in head);
13
+ }
14
+ /**
15
+ * DosDialog: the one overlay shell (DMNC-1592).
16
+ *
17
+ * The CmdPalette chrome, generalised: brand-coloured medium border with an
18
+ * amber phosphor glow, square corners, a blurred scrim, and a dark title
19
+ * strip (yellow title, `▸` marker, dim-amber rule below). Modal, Lightbox and
20
+ * CmdPalette are built on it.
21
+ *
22
+ * React Aria `ModalOverlay > Modal > Dialog` provides the focus trap, scroll
23
+ * lock, Escape and backdrop dismissal. The panel pops in/out with the shared
24
+ * `eidotter-pop-in/out` keyframes (scale 0.8 to 1, the CRT turn-on) and the
25
+ * scrim fades; under reduced motion the entering/exiting classes are not applied at all.
26
+ *
27
+ * Accessible name: a non-decorative structured `head.title` names the dialog;
28
+ * otherwise pass `aria-label` / `aria-labelledby`. Outside production builds
29
+ * an open dialog with no name logs a `console.warn`.
30
+ *
31
+ * The strip title is one line with an ellipsis by default; Modal lets it wrap.
32
+ *
33
+ * Ref forwards to the dialog element (`.eidotter-dialog__container`).
34
+ */
35
+ export const DosDialog = forwardRef(({ open, onOpenChange, placement = 'center', size = 'md', head, footer, isDismissable = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }, ref) => {
36
+ const titleId = useId();
37
+ const reduced = prefersReducedMotion();
38
+ const structured = isHeadObject(head) ? head : null;
39
+ const labelledBy = ariaLabelledBy ??
40
+ (!ariaLabel && structured && !structured.decorative ? titleId : undefined);
41
+ const unnamed = open && !ariaLabel && !labelledBy;
42
+ useEffect(() => {
43
+ if (unnamed && process.env.NODE_ENV !== 'production') {
44
+ console.warn('[eidotter] DosDialog has no accessible name. A ReactNode or decorative head ' +
45
+ 'does not name the dialog: pass aria-label or aria-labelledby.');
46
+ }
47
+ }, [unnamed]);
48
+ let strip = null;
49
+ if (structured) {
50
+ const { title, end, marker = true, decorative = false } = structured;
51
+ const TitleTag = decorative ? 'span' : 'h2';
52
+ strip = (_jsxs("div", { className: "eidotter-dialog__head", "aria-hidden": decorative || undefined, children: [_jsxs("div", { className: "eidotter-dialog__lead", children: [marker && (_jsx("span", { className: "eidotter-dialog__marker", "aria-hidden": "true", children: "\u25B8" })), _jsx(TitleTag, { id: titleId, className: "eidotter-dialog__title", children: title })] }), end != null && end !== false && (_jsx("div", { className: "eidotter-dialog__end", children: end }))] }));
53
+ }
54
+ else if (head != null && head !== false) {
55
+ strip = _jsx("div", { className: "eidotter-dialog__head", children: head });
56
+ }
57
+ return (_jsx(AriaModalOverlay, { isOpen: open, onOpenChange: onOpenChange, isDismissable: isDismissable, className: ({ isEntering, isExiting }) => cn('eidotter-dialog__overlay', `eidotter-dialog__overlay--${placement}`, !reduced && isEntering && 'eidotter-dialog__overlay--entering', !reduced && isExiting && 'eidotter-dialog__overlay--exiting'), children: _jsx(AriaModal, { className: ({ isEntering, isExiting }) => cn('eidotter-dialog', `eidotter-dialog--${size}`, `eidotter-dialog--${placement}`, !reduced && isEntering && 'eidotter-dialog--entering', !reduced && isExiting && 'eidotter-dialog--exiting'), children: _jsxs(AriaDialog, { ref: ref, "aria-label": ariaLabel, "aria-labelledby": labelledBy, className: cn('eidotter-dialog__container outline-none', className), children: [strip, _jsx("div", { className: "eidotter-dialog__body", children: children }), footer != null && footer !== false && (_jsx("div", { className: "eidotter-dialog__footer", children: footer }))] }) }) }));
58
+ });
59
+ DosDialog.displayName = 'DosDialog';
@@ -0,0 +1,2 @@
1
+ export { DosDialog } from './DosDialog.js';
2
+ export type { DosDialogProps, DosDialogHead, DosDialogPlacement, DosDialogSize, } from './DosDialog.js';
@@ -0,0 +1 @@
1
+ export { DosDialog } from './DosDialog.js';
@@ -0,0 +1,2 @@
1
+ export { DosDialog } from './components/index.js';
2
+ export type { DosDialogProps, DosDialogHead, DosDialogPlacement, DosDialogSize, } from './components/index.js';
@@ -0,0 +1 @@
1
+ export { DosDialog } from './components/index.js';
@@ -30,8 +30,8 @@ export interface DosFigureProps {
30
30
  *
31
31
  * Sierra / LucasArts / demoscene title cards painted the screen with a
32
32
  * limited palette, dithering, and annotated frames. DosFigure recreates
33
- * that aesthetic: amber chrome, scanline sweep, optional pins, and a
34
- * resolution tag. Use as a lead-in for blog articles or as a placeholder
33
+ * that aesthetic: the DosWindow chrome (title strip, brand border, drop
34
+ * shadow), scanline sweep, optional pins, and a resolution tag. Use as a lead-in for blog articles or as a placeholder
35
35
  * where a photograph would otherwise go.
36
36
  *
37
37
  * Not intended as a replacement for semantic imagery — pass `aria-label`
@@ -2,13 +2,14 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useId } from 'react';
4
4
  import { cn } from '../../../utils/cn.js';
5
+ import { DosWindow } from '../../DosWindow/components/DosWindow.js';
5
6
  /**
6
7
  * DosFigure — demoscene-style placeholder for media.
7
8
  *
8
9
  * Sierra / LucasArts / demoscene title cards painted the screen with a
9
10
  * limited palette, dithering, and annotated frames. DosFigure recreates
10
- * that aesthetic: amber chrome, scanline sweep, optional pins, and a
11
- * resolution tag. Use as a lead-in for blog articles or as a placeholder
11
+ * that aesthetic: the DosWindow chrome (title strip, brand border, drop
12
+ * shadow), scanline sweep, optional pins, and a resolution tag. Use as a lead-in for blog articles or as a placeholder
12
13
  * where a photograph would otherwise go.
13
14
  *
14
15
  * Not intended as a replacement for semantic imagery — pass `aria-label`
@@ -17,10 +18,16 @@ import { cn } from '../../../utils/cn.js';
17
18
  export const DosFigure = forwardRef(({ title, resolution, subject, caption, pins = [], animated = true, 'aria-label': ariaLabel, className, }, ref) => {
18
19
  const captionId = useId();
19
20
  const describedById = caption ? captionId : undefined;
20
- return (_jsxs("figure", { ref: ref, className: cn('eidotter-dos-figure', animated && 'eidotter-dos-figure--animated', className), role: "img", "aria-label": ariaLabel, "aria-describedby": describedById, children: [title && (_jsx("div", { className: "eidotter-dos-figure__title", "aria-hidden": "true", children: _jsx("span", { children: title }) })), _jsxs("div", { className: "eidotter-dos-figure__frame", children: [_jsx("div", { className: "eidotter-dos-figure__subject", children: subject }), pins.length > 0 && (_jsx("ul", { className: "eidotter-dos-figure__pins", role: "list", children: pins.map((pin, idx) => {
21
- const x = Number.isFinite(pin.x) ? Math.max(0, Math.min(100, pin.x)) : 0;
22
- const y = Number.isFinite(pin.y) ? Math.max(0, Math.min(100, pin.y)) : 0;
23
- return (_jsxs("li", { className: "eidotter-dos-figure__pin", style: { left: `${x}%`, top: `${y}%` }, children: [_jsx("span", { className: "eidotter-dos-figure__pin-dot", "aria-hidden": "true" }), _jsx("span", { className: "eidotter-dos-figure__pin-label", children: pin.label })] }, `${x}-${y}-${idx}`));
24
- }) })), _jsx("span", { className: "eidotter-dos-figure__scanline", "aria-hidden": "true" }), resolution && (_jsx("span", { className: "eidotter-dos-figure__resolution", "aria-hidden": "true", children: resolution }))] }), caption && (_jsx("figcaption", { id: captionId, className: "eidotter-dos-figure__caption", children: caption }))] }));
21
+ return (_jsxs("figure", { ref: ref, className: cn('eidotter-dos-figure', animated && 'eidotter-dos-figure--animated', className), role: "img", "aria-label": ariaLabel, "aria-describedby": describedById, children: [_jsx(DosWindow
22
+ // Decorative chrome: the figure is named by its aria-label, so the
23
+ // strip text stays out of the accessible content.
24
+ , {
25
+ // Decorative chrome: the figure is named by its aria-label, so the
26
+ // strip text stays out of the accessible content.
27
+ title: title && _jsx("span", { "aria-hidden": "true", children: title }), className: "eidotter-dos-figure__window", children: _jsxs("div", { className: "eidotter-dos-figure__frame", children: [_jsx("div", { className: "eidotter-dos-figure__subject", children: subject }), pins.length > 0 && (_jsx("ul", { className: "eidotter-dos-figure__pins", role: "list", children: pins.map((pin, idx) => {
28
+ const x = Number.isFinite(pin.x) ? Math.max(0, Math.min(100, pin.x)) : 0;
29
+ const y = Number.isFinite(pin.y) ? Math.max(0, Math.min(100, pin.y)) : 0;
30
+ return (_jsxs("li", { className: "eidotter-dos-figure__pin", style: { left: `${x}%`, top: `${y}%` }, children: [_jsx("span", { className: "eidotter-dos-figure__pin-dot", "aria-hidden": "true" }), _jsx("span", { className: "eidotter-dos-figure__pin-label", children: pin.label })] }, `${x}-${y}-${idx}`));
31
+ }) })), _jsx("span", { className: "eidotter-dos-figure__scanline", "aria-hidden": "true" }), resolution && (_jsx("span", { className: "eidotter-dos-figure__resolution", "aria-hidden": "true", children: resolution }))] }) }), caption && (_jsx("figcaption", { id: captionId, className: "eidotter-dos-figure__caption", children: caption }))] }));
25
32
  });
26
33
  DosFigure.displayName = 'DosFigure';
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ export interface DosWindowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
3
+ /** Strip title. Omit (with no `controls`) for a frame without a strip. */
4
+ title?: React.ReactNode;
5
+ /** Show the leading `▸` marker in the strip. Defaults to `true`. */
6
+ marker?: boolean;
7
+ /**
8
+ * Active window: strip title in the active colour. Inactive dims the
9
+ * strip title to `text-muted`. Defaults to `true`.
10
+ */
11
+ active?: boolean;
12
+ /**
13
+ * Window controls for the strip's end slot, usually `IconButton`s at the
14
+ * default `md` size (36px; they overhang the strip padding so the strip
15
+ * keeps its height). Consumer-supplied so DosWindow stays presentational.
16
+ */
17
+ controls?: React.ReactNode;
18
+ /** Footer row, separated from the body by a 1px rule. */
19
+ footer?: React.ReactNode;
20
+ /** Extra classes merged onto the root (`.eidotter-window`). */
21
+ className?: string;
22
+ /** Window body. */
23
+ children?: React.ReactNode;
24
+ }
25
+ /**
26
+ * DosWindow: the window chrome for in-page panels (DMNC-1592).
27
+ *
28
+ * The same title strip as DosDialog (dark, uppercase xs title, `▸` marker,
29
+ * 1px dim-amber rule) on a frame of brand border plus `--shadow-drop`, with
30
+ * no glow and no backdrop. DosFigure and Card are built on it.
31
+ *
32
+ * Server-safe and presentational: no hooks, no handlers. Controls are passed
33
+ * in by the consumer. The strip title is a plain `<span>`; it is not a
34
+ * heading, so pass your own heading element as `title` if the window needs
35
+ * one in the document outline.
36
+ *
37
+ * Ref forwards to the root `<div>`.
38
+ */
39
+ export declare const DosWindow: React.ForwardRefExoticComponent<DosWindowProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,23 @@
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
+ * DosWindow: the window chrome for in-page panels (DMNC-1592).
6
+ *
7
+ * The same title strip as DosDialog (dark, uppercase xs title, `▸` marker,
8
+ * 1px dim-amber rule) on a frame of brand border plus `--shadow-drop`, with
9
+ * no glow and no backdrop. DosFigure and Card are built on it.
10
+ *
11
+ * Server-safe and presentational: no hooks, no handlers. Controls are passed
12
+ * in by the consumer. The strip title is a plain `<span>`; it is not a
13
+ * heading, so pass your own heading element as `title` if the window needs
14
+ * one in the document outline.
15
+ *
16
+ * Ref forwards to the root `<div>`.
17
+ */
18
+ export const DosWindow = forwardRef(({ title, marker = true, active = true, controls, footer, className, children, ...rest }, ref) => {
19
+ const hasTitle = title != null && title !== false && title !== '';
20
+ const hasControls = controls != null && controls !== false;
21
+ return (_jsxs("div", { ref: ref, className: cn('eidotter-window', !active && 'eidotter-window--inactive', className), "data-active": active ? 'true' : 'false', ...rest, children: [(hasTitle || hasControls) && (_jsxs("div", { className: "eidotter-window__strip", children: [_jsxs("div", { className: "eidotter-window__lead", children: [marker && hasTitle && (_jsx("span", { className: "eidotter-window__marker", "aria-hidden": "true", children: "\u25B8" })), hasTitle && _jsx("span", { className: "eidotter-window__title", children: title })] }), hasControls && _jsx("div", { className: "eidotter-window__controls", children: controls })] })), _jsx("div", { className: "eidotter-window__body", children: children }), footer != null && footer !== false && (_jsx("div", { className: "eidotter-window__footer", children: footer }))] }));
22
+ });
23
+ DosWindow.displayName = 'DosWindow';
@@ -0,0 +1,2 @@
1
+ export { DosWindow } from './DosWindow.js';
2
+ export type { DosWindowProps } from './DosWindow.js';
@@ -0,0 +1 @@
1
+ export { DosWindow } from './DosWindow.js';
@@ -0,0 +1,2 @@
1
+ export { DosWindow } from './components/index.js';
2
+ export type { DosWindowProps } from './components/index.js';
@@ -0,0 +1 @@
1
+ export { DosWindow } from './components/index.js';
@@ -9,10 +9,13 @@ export interface FilterBarItem {
9
9
  /** Whether this item is disabled */
10
10
  disabled?: boolean;
11
11
  /**
12
- * Optional colour-role token for the active state, applied as a CSS custom
13
- * property. Prefer a semantic role (e.g. `--color-semantic-text-accent`) so
14
- * the active colour follows the active theme; raw `--color-cga-*` primitives
15
- * are accepted but won't re-theme. (A themeable per-category palette is
12
+ * Optional fill token for the active state, applied as a CSS custom
13
+ * property. The active label is drawn in `text-on-brand` ink, so pass a
14
+ * bright fill: `--color-semantic-background-brand` (follows the theme), or a
15
+ * bright CGA primitive (`--color-cga-bright-cyan`, `-bright-green`,
16
+ * `-yellow`, `-light-gray`). Each clears 4.5:1 in every theme. Text roles
17
+ * such as `--color-semantic-text-accent` are dark in the light theme and
18
+ * drop to about 3:1 as a fill. (A themeable per-category palette is
16
19
  * tracked with the token rationalization, DMNC-922/1059.)
17
20
  */
18
21
  color?: string;
@@ -15,7 +15,7 @@ export const Footer = ({ copyright, links, linkComponent, children, className, .
15
15
  const resolvedLinks = links ?? defaultLegalLinks;
16
16
  const LinkTag = linkComponent ?? 'a';
17
17
  const linkClassName = 'eidotter-footer__link text-dos-text-brand no-underline';
18
- return (_jsxs("footer", { className: cn('font-dos text-sm py-4 px-2 text-center', 'eidotter-footer', className), ...props, children: [_jsx("div", { className: "h-px bg-dos-border-default mb-4", role: "separator" }), children && _jsx("div", { className: "mb-3", children: children }), resolvedLinks.length > 0 && (_jsx("nav", { className: "flex justify-center items-center flex-wrap gap-2 mb-2", "aria-label": "Footer links", children: resolvedLinks.map((link, index) => {
18
+ return (_jsxs("footer", { className: cn('font-dos text-dos-text-base py-4 px-2 text-center', 'eidotter-footer', className), ...props, children: [_jsx("div", { className: "h-px bg-dos-border-default mb-4", role: "separator" }), children && _jsx("div", { className: "mb-3", children: children }), resolvedLinks.length > 0 && (_jsx("nav", { className: "flex justify-center items-center flex-wrap gap-2 mb-2", "aria-label": "Footer links", children: resolvedLinks.map((link, index) => {
19
19
  // Unsafe hrefs (javascript, data, vbscript, …) render the label without an anchor.
20
20
  const safeHref = isSafeHref(link.href) ? link.href : undefined;
21
21
  return (_jsxs(React.Fragment, { children: [index > 0 && _jsx("span", { className: "text-dos-text-muted select-none eidotter-footer__dot", "aria-hidden": "true", children: "\u00B7" }), !safeHref ? (_jsx("span", { className: linkClassName, children: link.label })) : link.external ? (_jsx("a", { className: linkClassName, href: safeHref, target: "_blank", rel: "noopener noreferrer", children: link.label })) : (_jsx(LinkTag, { className: linkClassName, href: safeHref, children: link.label }))] }, link.href));
@@ -15,6 +15,11 @@ const valueSizeClasses = {
15
15
  lg: 'text-dos-display-sm',
16
16
  };
17
17
  const ringSizes = { sm: 72, md: 104, lg: 136 };
18
+ /** Splits "720 units" at the first space into the reading and the rest, so the rest can wrap. */
19
+ function splitSecondary(text) {
20
+ const i = text.indexOf(' ');
21
+ return i > 0 ? [text.slice(0, i), text.slice(i + 1)] : [text, null];
22
+ }
18
23
  const toneClasses = {
19
24
  neutral: 'eidotter-tone--neutral',
20
25
  info: 'eidotter-tone--info',
@@ -63,8 +68,20 @@ export const Gauge = forwardRef(({ label, shortLabel, value, max = 100, unit, se
63
68
  .filter(Boolean)
64
69
  .join(', ');
65
70
  const name = `${ariaLabel ?? label}: ${valuetext}`;
66
- const diameter = ringSizes[size] ?? ringSizes.md;
71
+ const sizeKey = size in ringSizes ? size : 'md';
72
+ const diameter = ringSizes[sizeKey];
73
+ const thickness = Math.round(diameter / 8);
74
+ // The small ring's hole cannot hold a secondary line, so it sits under the ring.
75
+ const secondaryBelow = sizeKey === 'sm';
76
+ const [secondaryHead, secondaryTail] = splitSecondary(secondary ?? '');
77
+ const secondaryPart = secondary && !unknown && !loading ? (_jsxs("span", { className: "eidotter-gauge__secondary text-dos-text-brand text-dos-text-xs tabular-nums leading-none", children: [_jsx("span", { className: "eidotter-gauge__secondary-head", children: secondaryHead }), secondaryTail !== null && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "eidotter-gauge__secondary-tail", children: secondaryTail })] }))] })) : null;
67
78
  const tagText = provenanceLabel(labels, provenance);
68
- return (_jsxs("div", { ref: ref, className: cn('eidotter-gauge eidotter-health flex flex-col items-center gap-1 w-full font-dos', sizeClasses[size] ?? sizeClasses.md, toneClasses[effectiveTone], statusClasses[status], className), style: { minWidth: diameter }, role: "meter", "aria-valuemin": 0, "aria-valuemax": unknown ? 100 : max, "aria-valuenow": unknown || loading ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, "aria-busy": loading || undefined, ...props, children: [_jsxs("div", { className: "relative inline-flex items-center justify-center", children: [_jsx(ProgressRing, { decorative: true, value: loading ? 0 : pct, max: 100, size: diameter, thickness: Math.round(diameter / 8), segments: toneSegments[effectiveTone] ?? 16, color: effectiveTone === 'neutral' ? undefined : 'var(--eidotter-fill)', glow: false, className: cn(glow && 'eidotter-health-glow') }), _jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center text-center leading-none", children: loading ? (_jsx("div", { "aria-hidden": "true", className: "eidotter-gauge__skeleton w-full overflow-hidden", children: _jsx(Skeleton, { variant: "figure", lines: 2, animated: false }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('eidotter-gauge__value text-dos-text-accent tabular-nums', valueSizeClasses[size] ?? valueSizeClasses.md, stale && 'opacity-60'), children: valueText }), secondary && !unknown && (_jsx("span", { className: "eidotter-gauge__secondary text-dos-text-brand text-dos-text-xs tabular-nums", children: secondary }))] })) })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight max-w-full", children: [effectiveTone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__glyph text-dos-text-error", children: "\u25B2" })), _jsxs("span", { className: "eidotter-gauge__label truncate", title: label, children: [_jsx("span", { className: "eidotter-gauge__label--full", children: label }), shortLabel && _jsx("span", { className: "eidotter-gauge__label--short", children: shortLabel })] }), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__overflow text-dos-text-warning", children: "+" })), tagText !== null && (_jsx("span", { className: "eidotter-gauge__provenance border border-dos-border-brand px-1 normal-case", children: tagText })), stale && _jsx("span", { className: "eidotter-gauge__stale normal-case", children: labels.stale })] }), resolved.label && (_jsx("span", { className: "eidotter-gauge__band text-dos-text-brand normal-case", children: resolved.label }))] }));
79
+ return (_jsxs("div", { ref: ref, className: cn('eidotter-gauge eidotter-health flex flex-col items-center gap-1 w-full font-dos', sizeClasses[size] ?? sizeClasses.md, toneClasses[effectiveTone], statusClasses[status], className), style: { minWidth: diameter }, "data-size": sizeKey, "data-has-short": shortLabel ? 'true' : 'false', role: "meter", "aria-valuemin": 0, "aria-valuemax": unknown ? 100 : max, "aria-valuenow": unknown || loading ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, "aria-busy": loading || undefined, ...props, children: [_jsxs("div", { className: "relative inline-flex items-center justify-center", children: [_jsx(ProgressRing, { decorative: true, value: loading ? 0 : pct, max: 100, size: diameter, thickness: thickness, segments: toneSegments[effectiveTone] ?? 16, color: effectiveTone === 'neutral' ? undefined : 'var(--eidotter-fill)', glow: false, className: cn(glow && 'eidotter-health-glow') }), _jsx("div", { className: "eidotter-gauge__center absolute flex flex-col items-center justify-center min-w-0 text-center leading-none", style: { inset: thickness + 2 }, children: loading ? (_jsx("div", { "aria-hidden": "true", className: "eidotter-gauge__skeleton w-full overflow-hidden", children: _jsx(Skeleton, { variant: "figure", lines: 2, animated: false }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: [
80
+ 'eidotter-gauge__value text-dos-text-accent tabular-nums',
81
+ valueSizeClasses[sizeKey],
82
+ stale && 'opacity-60',
83
+ ]
84
+ .filter(Boolean)
85
+ .join(' '), children: valueText }), !secondaryBelow && secondaryPart] })) })] }), secondaryBelow && secondaryPart, _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight max-w-full", children: [effectiveTone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__glyph text-dos-text-error", children: "\u25B2" })), _jsxs("span", { className: "eidotter-gauge__label truncate", title: label, children: [_jsx("span", { className: "eidotter-gauge__label--full", children: label }), shortLabel && _jsx("span", { className: "eidotter-gauge__label--short", children: shortLabel })] }), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__overflow text-dos-text-warning", children: "+" })), tagText !== null && (_jsx("span", { className: "eidotter-gauge__provenance border border-dos-border-brand px-1 normal-case", children: tagText })), stale && _jsx("span", { className: "eidotter-gauge__stale normal-case", children: labels.stale })] }), resolved.label && (_jsx("span", { className: "eidotter-gauge__band text-dos-text-brand normal-case", children: resolved.label }))] }));
69
86
  });
70
87
  Gauge.displayName = 'Gauge';
@@ -46,7 +46,35 @@ export interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
46
46
  * is identical to a header without this prop.
47
47
  */
48
48
  context?: HeaderContext;
49
- /** Custom branding content — replaces brandName when provided */
49
+ /**
50
+ * How `brandName` is presented when no `children` are passed:
51
+ * - `'lockup'` (default): the eiDotter Logo (28px, glow) + the name in
52
+ * uppercase, wide-tracked DOS text.
53
+ * - `'mark'`: the morphing `Mark` (28px) + the same name text.
54
+ * - `'text'`: the name as plain text (the pre-0.41 behaviour).
55
+ * The icon is decorative (the name is the link's accessible name). Ignored
56
+ * when `children` is provided, or when `brandName` is empty.
57
+ */
58
+ brand?: 'text' | 'lockup' | 'mark';
59
+ /**
60
+ * Trailing slot (theme toggle, ⌘K hint, …). Rendered once: after the desktop
61
+ * nav on wide screens, and between the brand and the MENU trigger on mobile
62
+ * (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.
65
+ * The slot is `overflow: hidden` at every width, so dropdowns, menus and
66
+ * tooltips opened from `end` must render in a portal (React Aria popovers,
67
+ * e.g. `Select`, already do); an in-flow popover is clipped.
68
+ */
69
+ end?: React.ReactNode;
70
+ /**
71
+ * When set (e.g. `'#main'`), renders a "SKIP TO CONTENT" anchor as the
72
+ * header's first child, visually hidden until keyboard-focused (WCAG 2.4.1).
73
+ * Off by default — sites with their own skip link leave it unset. Unsafe
74
+ * schemes render no anchor.
75
+ */
76
+ skipLinkTarget?: string;
77
+ /** Custom branding content — replaces brandName (and `brand`) when provided */
50
78
  children?: React.ReactNode;
51
79
  /** Additional CSS class name */
52
80
  className?: string;
@@ -1,14 +1,15 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef } from 'react';
2
+ import React, { forwardRef } from 'react';
3
3
  import { cn } from '../../../utils/cn.js';
4
4
  import { isSafeHref } from '../../../utils/isSafeHref.js';
5
5
  import { DesktopNav, MobileNav } from '../../Nav/index.js';
6
+ import { Logo, Mark } from '../../Brand/index.js';
6
7
  import { HeaderContextRow } from './HeaderContextRow.js';
7
8
  const variantClasses = {
8
9
  retro: 'eidotter-header--retro',
9
10
  modern: 'eidotter-header--modern',
10
11
  };
11
- export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHref, variant = 'retro', sticky = true, linkComponent, context, children, className, ...rest }, ref) => {
12
+ export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHref, variant = 'retro', sticky = true, linkComponent, context, brand = 'lockup', end, skipLinkTarget, children, className, ...rest }, ref) => {
12
13
  const LinkTag = linkComponent ?? 'a';
13
14
  // The brand link is structural — an unsafe brandHref falls back to '/' rather
14
15
  // than unmounting the branding anchor.
@@ -16,9 +17,23 @@ export const Header = forwardRef(({ brandName, brandHref = '/', items, activeHre
16
17
  // An empty context (no categories, no returnTo) is treated as absent so the
17
18
  // rendered DOM stays byte-identical to a header without the prop.
18
19
  const hasContext = !!context && (context.categories.length > 0 || !!context.returnTo);
19
- const main = (_jsxs(_Fragment, { children: [_jsx(LinkTag, { href: safeBrandHref, className: cn('no-underline text-base font-bold tracking-wide', 'eidotter-header__branding'), children: children || brandName }), _jsx(DesktopNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent }), _jsx(MobileNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent })] }));
20
- return (_jsx("header", { ref: ref, className: cn(hasContext
20
+ const safeSkipTarget = skipLinkTarget && isSafeHref(skipLinkTarget) ? skipLinkTarget : undefined;
21
+ // children always win; the icon variants only apply to a non-empty brandName.
22
+ const iconBrand = !children && !!brandName && brand !== 'text';
23
+ const brandContent = children || (iconBrand ? (_jsxs(_Fragment, { children: [brand === 'mark'
24
+ ? _jsx(Mark, { size: 28, label: "", className: "eidotter-header__brand-icon" })
25
+ : _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);
26
+ // `end` is rendered once: DesktopNav is hidden <768px and the MENU trigger
27
+ // ≥768px, so this DOM order gives "nav · end" on desktop and "end · MENU" on
28
+ // mobile. The right-hand group wrapper only exists when `end` is passed, so
29
+ // headers without it keep their previous DOM.
30
+ const hasEnd = end != null && end !== false && end !== '';
31
+ const ActionsGroup = hasEnd ? 'div' : React.Fragment;
32
+ const actionsProps = hasEnd ? { className: 'eidotter-header__actions flex items-center gap-4' } : {};
33
+ 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: [_jsx(DesktopNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent }), hasEnd && _jsx("div", { className: "eidotter-header__end flex items-center", children: end }), _jsx(MobileNav, { items: items, activeHref: activeHref, variant: variant, linkComponent: linkComponent })] })] }));
34
+ const skipLink = safeSkipTarget ? (_jsx("a", { href: safeSkipTarget, className: "eidotter-header__skip", children: "Skip to content" })) : null;
35
+ return (_jsxs("header", { ref: ref, className: cn(hasContext
21
36
  ? 'flex flex-col gap-2 px-4 py-3 font-dos'
22
- : '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: 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) }));
37
+ : '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)] }));
23
38
  });
24
39
  Header.displayName = 'Header';
@@ -9,6 +9,8 @@ declare const ICON_MAP: {
9
9
  Close: PixelIcon;
10
10
  'Chevron Up': (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
11
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;
12
14
  App: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
13
15
  Cancel: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
14
16
  Fullscreen: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
@@ -6,6 +6,8 @@ import { SquareAlert } from 'pixelarticons/react/SquareAlert';
6
6
  import { Check } from 'pixelarticons/react/Check';
7
7
  import { ChevronUp } from 'pixelarticons/react/ChevronUp';
8
8
  import { ChevronDown } from 'pixelarticons/react/ChevronDown';
9
+ import { ChevronLeft } from 'pixelarticons/react/ChevronLeft';
10
+ import { ChevronRight } from 'pixelarticons/react/ChevronRight';
9
11
  import { WindowFrame } from 'pixelarticons/react/WindowFrame';
10
12
  import { Minus } from 'pixelarticons/react/Minus';
11
13
  import { Expand } from 'pixelarticons/react/Expand';
@@ -17,7 +19,7 @@ import { cn } from '../../../utils/cn.js';
17
19
  * currentColor inheritance, 2×2 unit "pixels" on the grid.
18
20
  */
19
21
  const PixelX = (props) => (_jsx("svg", { viewBox: "0 0 24 24", width: "24", height: "24", fill: "currentColor", xmlns: "http://www.w3.org/2000/svg", ...props, children: _jsx("path", { d: "M5 5h2v2H5zM7 7h2v2H7zM9 9h2v2H9zM11 11h2v2h-2zM13 13h2v2h-2zM15 15h2v2h-2zM17 17h2v2h-2zM17 5h2v2h-2zM15 7h2v2h-2zM13 9h2v2h-2zM9 13h2v2H9zM7 15h2v2H7zM5 17h2v2H5z" }) }));
20
- // 12 public icon names mapping to 11 unique pixelarticons components.
22
+ // 14 public icon names mapping to 13 unique glyphs (12 pixelarticons + PixelX).
21
23
  // Done and Check both map to the Check component intentionally — consumers
22
24
  // can use either semantic name for a checkmark glyph.
23
25
  //
@@ -38,6 +40,8 @@ const ICON_MAP = {
38
40
  'Close': PixelX, // custom pixel-art X mark (pixelarticons has no X glyph)
39
41
  'Chevron Up': ChevronUp,
40
42
  'Chevron Down': ChevronDown,
43
+ 'Chevron Left': ChevronLeft, // Lightbox prev (IconButton, DMNC-1592)
44
+ 'Chevron Right': ChevronRight, // Lightbox next
41
45
  'App': WindowFrame,
42
46
  'Cancel': Minus, // window minimize control, not abort
43
47
  'Fullscreen': Expand,