eidotter 0.40.2 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +14 -6
  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 +1 -1
  21. package/dist/components/DosDialog/components/DosDialog.d.ts +90 -0
  22. package/dist/components/DosDialog/components/DosDialog.js +68 -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 +13 -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 +9 -0
  41. package/dist/components/Icon/components/Icon.js +7 -3
  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/RangeBar/components/RangeBar.js +8 -5
  59. package/dist/components/SeriesBars/components/SeriesBars.d.ts +9 -1
  60. package/dist/components/SeriesBars/components/SeriesBars.js +23 -11
  61. package/dist/components/Stat/components/Stat.js +1 -1
  62. package/dist/components/Tag/components/Tag.d.ts +8 -1
  63. package/dist/components/Tag/components/Tag.js +5 -5
  64. package/dist/components/VitalsStrip/components/VitalsStrip.js +9 -7
  65. package/dist/components/registry.js +230 -19
  66. package/dist/eidotter.css +3 -2
  67. package/dist/hooks/useAnimatedDismiss.js +5 -0
  68. package/dist/index.d.ts +9 -1
  69. package/dist/index.es.js +2414 -1747
  70. package/dist/index.umd.js +1 -1
  71. package/dist/utils/cn.d.ts +2 -0
  72. package/dist/utils/cn.js +33 -1
  73. package/dist/utils/dosFontSizes.d.ts +2 -0
  74. package/dist/utils/dosFontSizes.js +17 -0
  75. package/llms.txt +15 -7
  76. package/migrations/0.41.0.md +883 -0
  77. package/migrations/README.md +46 -0
  78. package/package.json +11 -3
  79. package/public/favicon.svg +1 -1
  80. package/src/styles/dos-utilities.css +9 -1
  81. package/src/styles/shadcn.css +63 -27
  82. package/src/styles/theme.amber-mono.css +4 -2
  83. package/src/styles/theme.amber-paper.css +106 -0
  84. package/src/styles/theme.cga-amber.css +3 -1
  85. package/src/styles/theme.cga-mode4-p0.css +3 -1
  86. package/src/styles/theme.cga-mode4-p1.css +3 -1
  87. package/src/styles/theme.cga-mode5.css +3 -1
  88. package/src/styles/theme.light.css +3 -1
  89. package/src/styles/tokens.css +5 -1
  90. package/tailwind.preset.cjs +23 -1
  91. package/dist/components/Brand/components/markGeometry.test.d.ts +0 -1
  92. package/dist/components/Connector/components/connectorGeometry.test.d.ts +0 -1
  93. package/dist/components/DecayStrip/components/resample.test.d.ts +0 -1
  94. package/dist/components/TimelineContainer/components/timelineUtils.test.d.ts +0 -1
  95. package/dist/components/TimelineContainer/components/useDrillDown.test.d.ts +0 -1
  96. package/dist/components/TimelineContainer/components/useSelection.test.d.ts +0 -1
  97. package/dist/components/TimelineContainer/components/useZoom.test.d.ts +0 -1
  98. package/dist/hooks/useTextScramble.test.d.ts +0 -1
  99. package/dist/hooks/useThemePortal.test.d.ts +0 -1
  100. package/dist/utils/health/formatValue.test.d.ts +0 -1
  101. package/dist/utils/health/healthPatterns.test.d.ts +0 -1
  102. package/dist/utils/health/resolveBand.test.d.ts +0 -1
  103. package/dist/utils/isSafeHref.test.d.ts +0 -1
  104. package/dist/utils/isSafeUrl.test.d.ts +0 -1
@@ -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,
@@ -58,7 +62,7 @@ const SIZE_MAP = { L: 56, S: 24 };
58
62
  * Renders inline SVG with proper viewBox scaling at any size.
59
63
  * The DOS pixel aesthetic matches eidotter's design language.
60
64
  */
61
- export const Icon = ({ name, size = 'L', className = '', onClick, color, role, 'aria-label': ariaLabel, }) => {
65
+ export const Icon = ({ name, size = 'L', className = '', onClick, color, role, 'aria-label': ariaLabel, decorative = false, }) => {
62
66
  // Runtime defense against JS consumers or dynamically-cast names.
63
67
  // TypeScript users are already constrained to IconName by IconProps.
64
68
  const IconComponent = ICON_MAP[name];
@@ -83,5 +87,5 @@ export const Icon = ({ name, size = 'L', className = '', onClick, color, role, '
83
87
  }
84
88
  }
85
89
  : undefined;
86
- return (_jsx("span", { className: cn('icon', isButton && 'icon--button', className), onClick: onClick, onKeyDown: handleKeyDown, role: resolvedRole, tabIndex: isButton ? 0 : undefined, "aria-label": ariaLabel || `${name} icon`, style: color ? { color } : undefined, children: _jsx(IconComponent, { width: pixelSize, height: pixelSize, className: "icon__svg" }) }));
90
+ return (_jsx("span", { className: cn('icon', isButton && 'icon--button', className), onClick: onClick, onKeyDown: handleKeyDown, role: decorative ? undefined : resolvedRole, tabIndex: isButton ? 0 : undefined, "aria-label": decorative ? undefined : ariaLabel || `${name} icon`, "aria-hidden": decorative ? 'true' : undefined, style: color ? { color } : undefined, children: _jsx(IconComponent, { width: pixelSize, height: pixelSize, className: "icon__svg" }) }));
87
91
  };
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import type { PressEvent } from 'react-aria-components';
3
+ import type { IconName } from '../../Icon/components/Icon.js';
4
+ /** `md` = 36×36 hit area, 20px glyph (every built-in adopter). `window` = 28×28, 16px glyph, for dense chrome. */
5
+ export type IconButtonSize = 'md' | 'window';
6
+ /** `outline` = 1px square outline at rest (default). `bare` = no resting outline. */
7
+ export type IconButtonVariant = 'outline' | 'bare';
8
+ export interface IconButtonProps {
9
+ /** Glyph from the `<Icon>` set: `Close` (X), `Cancel` (minus, minimise), `Fullscreen` (maximise), `Chevron Left/Right/Up/Down`. */
10
+ icon: IconName;
11
+ /** Accessible name. Required: the button has no visible text. Rendered as `aria-label`. */
12
+ label: string;
13
+ /** Hit-area size. Defaults to `md` (36×36). */
14
+ size?: IconButtonSize;
15
+ /**
16
+ * `outline` (default) shows the thin square outline at rest. `bare` drops
17
+ * it, for places where an outline would fight a neighbouring rule.
18
+ */
19
+ variant?: IconButtonVariant;
20
+ /** Disables the button (opacity 0.5, no hover invert). */
21
+ isDisabled?: boolean;
22
+ /** Called on press (mouse, touch, Enter, Space). */
23
+ onPress?: (e: PressEvent) => void;
24
+ /**
25
+ * Keep the button out of the Tab order. Only for controls whose action is
26
+ * reachable another way (Tag's remove, reachable via Delete/Backspace on
27
+ * the focused tag).
28
+ */
29
+ excludeFromTabOrder?: boolean;
30
+ /** Extra classes (placement) merged onto the button. */
31
+ className?: string;
32
+ }
33
+ /**
34
+ * IconButton: the one icon-only control (DMNC-1592).
35
+ *
36
+ * A pixel glyph from `<Icon>` inside a thin square outline: transparent fill,
37
+ * `text-accent` glyph at rest; hover inverts (amber fill, dark glyph); focus
38
+ * is a 2px brand outline offset 2px. Every close, prev/next and window
39
+ * control in the library uses it, so the glyph and states match everywhere.
40
+ *
41
+ * React Aria `Button` handles press, keyboard and focus-visible. Ref forwards
42
+ * to the `<button>`.
43
+ */
44
+ export declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { forwardRef } from 'react';
4
+ import { Button as AriaButton } from 'react-aria-components';
5
+ import { Icon } from '../../Icon/components/Icon.js';
6
+ import { cn } from '../../../utils/cn.js';
7
+ /**
8
+ * IconButton: the one icon-only control (DMNC-1592).
9
+ *
10
+ * A pixel glyph from `<Icon>` inside a thin square outline: transparent fill,
11
+ * `text-accent` glyph at rest; hover inverts (amber fill, dark glyph); focus
12
+ * is a 2px brand outline offset 2px. Every close, prev/next and window
13
+ * control in the library uses it, so the glyph and states match everywhere.
14
+ *
15
+ * React Aria `Button` handles press, keyboard and focus-visible. Ref forwards
16
+ * to the `<button>`.
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 }) }) })));
19
+ IconButton.displayName = 'IconButton';
@@ -0,0 +1,2 @@
1
+ export { IconButton } from './IconButton.js';
2
+ export type { IconButtonProps, IconButtonSize, IconButtonVariant } from './IconButton.js';
@@ -0,0 +1 @@
1
+ export { IconButton } from './IconButton.js';
@@ -0,0 +1,2 @@
1
+ export { IconButton } from './components/index.js';
2
+ export type { IconButtonProps, IconButtonSize, IconButtonVariant } from './components/index.js';
@@ -0,0 +1 @@
1
+ export { IconButton } from './components/index.js';
@@ -13,6 +13,6 @@ const variantClasses = {
13
13
  */
14
14
  export const Input = forwardRef(({ variant = 'default', label, description, errorMessage, disabled, className, onChange, isRequired, ...props }, ref) => {
15
15
  const isInvalid = variant === 'error';
16
- return (_jsxs(AriaTextField, { isDisabled: disabled, isInvalid: isInvalid, isRequired: isRequired, className: cn('eidotter-text-field', 'flex flex-col gap-1', className), children: [label && (_jsx(AriaLabel, { className: "text-dos-text-brand font-dos text-xs uppercase tracking-wider", children: label })), _jsx(AriaInput, { ref: ref, className: cn('w-full bg-transparent text-dos-text-brand font-dos text-base p-2', 'border-2 border-dos-border-default outline-none box-border', 'eidotter-input', variantClasses[variant] || variantClasses.default, disabled && 'eidotter-input--disabled'), onChange: onChange, ...props }), description && !isInvalid && (_jsx(AriaText, { slot: "description", className: "text-dos-text-muted font-dos text-xs", children: description })), errorMessage && isInvalid && (_jsx(AriaText, { slot: "errorMessage", className: "text-dos-text-error font-dos text-xs", children: errorMessage }))] }));
16
+ return (_jsxs(AriaTextField, { isDisabled: disabled, isInvalid: isInvalid, isRequired: isRequired, className: cn('eidotter-text-field', 'flex flex-col gap-1', className), children: [label && (_jsx(AriaLabel, { className: "text-dos-text-brand font-dos text-dos-text-base uppercase tracking-wider", children: label })), _jsx(AriaInput, { ref: ref, className: cn('w-full bg-transparent text-dos-text-brand font-dos text-dos-text-base p-2', 'border-2 border-dos-border-default outline-none box-border', 'eidotter-input', variantClasses[variant] || variantClasses.default, disabled && 'eidotter-input--disabled'), onChange: onChange, ...props }), description && !isInvalid && (_jsx(AriaText, { slot: "description", className: "text-dos-text-muted font-dos text-dos-text-base", children: description })), errorMessage && isInvalid && (_jsx(AriaText, { slot: "errorMessage", className: "text-dos-text-error font-dos text-dos-text-base", children: errorMessage }))] }));
17
17
  });
18
18
  Input.displayName = 'Input';
@@ -13,10 +13,16 @@ export interface LightboxProps {
13
13
  onIndexChange?: (index: number) => void;
14
14
  }
15
15
  /**
16
- * Fullscreen image viewer built on the same React Aria primitives as Modal.
16
+ * Fullscreen image viewer on the DosDialog shell (DMNC-1592).
17
17
  *
18
- * Reuses Modal's CRT phosphor enter/exit animations, overlay tokens, and close
19
- * button styling. Adds prev/next, counter, caption, keyboard nav, and touch
20
- * swipe — see Tasks 4 and 5.
18
+ * The shell supplies the overlay, chrome, motion and the title strip;
19
+ * the `[ n / N ]` counter is the strip's title and the close control sits at
20
+ * its end (an IconButton). Lightbox adds prev/next (IconButtons with the pixel
21
+ * chevrons), caption, keyboard nav, and touch swipe.
22
+ * It shares the shell's z-index (`--z-index-modal`) with Modal; stacking a
23
+ * Lightbox over an open Modal is not a supported case.
24
+ *
25
+ * The close IconButton carries `eidotter-lightbox__close`. eidotter ships no
26
+ * CSS for it; it is kept as a stable consumer hook for targeting the control.
21
27
  */
22
28
  export declare const Lightbox: React.FC<LightboxProps>;
@@ -1,15 +1,20 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React, { useCallback, useEffect, useState } from 'react';
4
- import { Button as AriaButton, ModalOverlay as AriaModalOverlay, Modal as AriaModal, Dialog as AriaDialog, } from 'react-aria-components';
5
- import { Icon } from '../../Icon/components/Icon.js';
6
- import { cn } from '../../../utils/cn.js';
4
+ import { IconButton } from '../../IconButton/components/IconButton.js';
5
+ import { DosDialog } from '../../DosDialog/components/DosDialog.js';
7
6
  /**
8
- * Fullscreen image viewer built on the same React Aria primitives as Modal.
7
+ * Fullscreen image viewer on the DosDialog shell (DMNC-1592).
9
8
  *
10
- * Reuses Modal's CRT phosphor enter/exit animations, overlay tokens, and close
11
- * button styling. Adds prev/next, counter, caption, keyboard nav, and touch
12
- * swipe — see Tasks 4 and 5.
9
+ * The shell supplies the overlay, chrome, motion and the title strip;
10
+ * the `[ n / N ]` counter is the strip's title and the close control sits at
11
+ * its end (an IconButton). Lightbox adds prev/next (IconButtons with the pixel
12
+ * chevrons), caption, keyboard nav, and touch swipe.
13
+ * It shares the shell's z-index (`--z-index-modal`) with Modal; stacking a
14
+ * Lightbox over an open Modal is not a supported case.
15
+ *
16
+ * The close IconButton carries `eidotter-lightbox__close`. eidotter ships no
17
+ * CSS for it; it is kept as a stable consumer hook for targeting the control.
13
18
  */
14
19
  export const Lightbox = ({ images, initialIndex = 0, isOpen, onClose, onIndexChange, }) => {
15
20
  // Clamp the requested initial index so callers passing stale indices
@@ -78,8 +83,8 @@ export const Lightbox = ({ images, initialIndex = 0, isOpen, onClose, onIndexCha
78
83
  return () => window.removeEventListener('keydown', onKeyDown);
79
84
  }, [isOpen, goPrev, goNext]);
80
85
  // Empty array: nothing to show. (Note: do NOT early-return on !isOpen here —
81
- // React Aria's AriaModalOverlay needs to stay mounted across isOpen
82
- // transitions so it can play modal-crt-exit. The overlay handles its own
86
+ // the DosDialog overlay needs to stay mounted across isOpen transitions so
87
+ // it can play its exit animation. The overlay handles its own
83
88
  // visibility based on the `isOpen` prop.)
84
89
  if (images.length === 0) {
85
90
  return null;
@@ -91,7 +96,10 @@ export const Lightbox = ({ images, initialIndex = 0, isOpen, onClose, onIndexCha
91
96
  const current = images[safeIndex];
92
97
  const hasPrev = safeIndex > 0;
93
98
  const hasNext = safeIndex < images.length - 1;
94
- return (_jsx(AriaModalOverlay, { isOpen: isOpen, onOpenChange: (open) => { if (!open)
95
- onClose(); }, isDismissable: true, className: ({ isEntering, isExiting }) => cn('eidotter-lightbox-overlay', isEntering && 'eidotter-lightbox-overlay--entering', isExiting && 'eidotter-lightbox-overlay--exiting'), children: _jsx(AriaModal, { className: ({ isEntering, isExiting }) => cn('eidotter-lightbox', isEntering && 'eidotter-lightbox--entering', isExiting && 'eidotter-lightbox--exiting'), children: _jsxs(AriaDialog, { "aria-label": current.alt || 'Image viewer', className: "eidotter-lightbox__container outline-none", children: [_jsx(AriaButton, { className: "eidotter-lightbox__close", onPress: onClose, "aria-label": "Close lightbox", children: _jsx(Icon, { name: "Close", size: "S" }) }), _jsx(AriaButton, { className: "eidotter-lightbox__nav eidotter-lightbox__nav--prev", onPress: goPrev, isDisabled: !hasPrev, "aria-label": "Previous image", children: "\u25C0" }), _jsx(AriaButton, { className: "eidotter-lightbox__nav eidotter-lightbox__nav--next", onPress: goNext, isDisabled: !hasNext, "aria-label": "Next image", children: "\u25B6" }), _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 }), current.caption && (_jsx("figcaption", { className: "eidotter-lightbox__caption", children: current.caption }))] }), _jsxs("span", { className: "eidotter-lightbox__counter", "aria-live": "polite", children: ["[ ", safeIndex + 1, " / ", images.length, " ]"] })] }) }) }));
99
+ return (_jsx(DosDialog, { open: isOpen, onOpenChange: (open) => { if (!open)
100
+ onClose(); }, placement: "center", size: "full", "aria-label": current.alt || 'Image viewer', className: "eidotter-lightbox__container", head: {
101
+ title: (_jsxs("span", { className: "eidotter-lightbox__counter", "aria-live": "polite", children: ["[ ", safeIndex + 1, " / ", images.length, " ]"] })),
102
+ 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 }))] }) }));
96
104
  };
97
105
  Lightbox.displayName = 'Lightbox';
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
3
  import { cn } from '../../../utils/cn.js';
4
4
  import { formatValue, isKnown, resolveBand, resolveLabels, PLACEHOLDER } from '../../../utils/health/index.js';
@@ -65,15 +65,19 @@ export const Meter = forwardRef(({ label, shortLabel, value, max = 100, unit, op
65
65
  const effectivePattern = pattern ?? tonePatterns[effectiveTone] ?? 'solid';
66
66
  const fillClass = patternClasses[effectivePattern] ?? patternClasses.solid;
67
67
  const valueText = unknown ? PLACEHOLDER : formatValue(value, { locale, unit }).text;
68
+ // Number and unit render in two spans so a narrow MeterGroup can drop the
69
+ // unit onto its own line; the accessible value text stays one string.
70
+ const numberText = unknown ? PLACEHOLDER : formatValue(value, { locale }).text;
71
+ const unitText = unknown ? undefined : unit || undefined;
68
72
  const valuetext = unknown ? labels.unknown : valueText;
69
73
  const name = `${ariaLabel ?? label}: ${valuetext}`;
70
74
  const isRange = Array.isArray(optimal);
71
75
  const optimalProps = maxValid ? optimalStyle(optimal, max) : undefined;
72
76
  const blockCount = segments !== undefined ? Math.max(0, Math.floor(segments)) : 0;
73
77
  const filledBlocks = Math.round(ratio * blockCount);
74
- const valuePart = showValue || overflow ? (_jsxs("div", { className: "eidotter-meter__readout flex items-center gap-1 leading-none", children: [showValue && (_jsx("span", { className: "eidotter-meter__value text-dos-text-accent tabular-nums", children: valueText })), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-meter__overflow text-dos-text-warning", children: "+" }))] })) : null;
78
+ const valuePart = showValue || overflow ? (_jsxs("div", { className: "eidotter-meter__readout flex items-center gap-1 leading-none", children: [showValue && (_jsxs("span", { className: "eidotter-meter__value text-dos-text-accent tabular-nums text-center", children: [_jsx("span", { className: "eidotter-meter__number", children: numberText }), unitText && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "eidotter-meter__unit", children: unitText })] }))] })), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-meter__overflow text-dos-text-warning", children: "+" }))] })) : null;
75
79
  const trackPart = (_jsxs("div", { className: "eidotter-meter__track relative bg-dos-bg-primary border border-dos-border-disabled", children: [blockCount > 0 ? (_jsx("div", { className: "eidotter-meter__segments", "aria-hidden": "true", children: Array.from({ length: blockCount }, (_, i) => (_jsx("span", { className: cn('eidotter-meter__segment', i < filledBlocks && `eidotter-meter__segment--filled ${fillClass}`) }, i))) })) : (_jsx("div", { className: cn('eidotter-meter__fill', fillClass), style: { '--eidotter-meter-fill': String(round4(ratio)) } })), optimalProps && (_jsx("div", { "aria-hidden": "true", className: cn('eidotter-meter__optimal', isRange && 'eidotter-meter__optimal--range'), style: optimalProps }))] }));
76
80
  const labelPart = (_jsxs("div", { className: "eidotter-meter__caption flex flex-wrap items-center justify-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight min-w-0 max-w-full", children: [effectiveTone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "eidotter-meter__glyph text-dos-text-error", children: "\u25B2" })), _jsxs("span", { className: "eidotter-meter__label truncate", title: label, children: [_jsx("span", { className: "eidotter-meter__label--full", children: label }), shortLabel && _jsx("span", { className: "eidotter-meter__label--short", children: shortLabel })] }), resolved.label && (_jsx("span", { className: "eidotter-meter__band normal-case", children: resolved.label }))] }));
77
- return (_jsx("div", { ref: ref, className: cn('eidotter-meter eidotter-health flex font-dos', orientationClasses[orientation] ?? orientationClasses.vertical, sizeClasses[size] ?? sizeClasses.sm, toneClasses[effectiveTone], className), role: "meter", "aria-valuemin": 0, "aria-valuemax": maxValid ? max : 100, "aria-valuenow": unknown ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, ...props, children: orientation === 'horizontal' ? (_jsxs(_Fragment, { children: [labelPart, trackPart, valuePart] })) : (_jsxs(_Fragment, { children: [valuePart, trackPart, labelPart] })) }));
81
+ return (_jsx("div", { ref: ref, className: cn('eidotter-meter eidotter-health flex font-dos', orientationClasses[orientation] ?? orientationClasses.vertical, sizeClasses[size] ?? sizeClasses.sm, toneClasses[effectiveTone], className), "data-has-short": shortLabel ? 'true' : 'false', role: "meter", "aria-valuemin": 0, "aria-valuemax": maxValid ? max : 100, "aria-valuenow": unknown ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, ...props, children: orientation === 'horizontal' ? (_jsxs(_Fragment, { children: [labelPart, trackPart, valuePart] })) : (_jsxs(_Fragment, { children: [valuePart, trackPart, labelPart] })) }));
78
82
  });
79
83
  Meter.displayName = 'Meter';
@@ -21,8 +21,8 @@ export const MeterGroup = forwardRef(({ layout = 'row', columns, sharedMax = fal
21
21
  const fixedColumns = layout === 'grid' && columns !== undefined;
22
22
  return (_jsx("div", { ref: ref, role: "group", className: cn('eidotter-meter-group', layoutClasses[layout] ?? layoutClasses.row, fixedColumns && 'eidotter-meter-group--columns', className), style: fixedColumns
23
23
  ? { '--eidotter-meter-columns': String(columns) }
24
- : undefined, ...props, children: items
25
- ? items.map((item, i) => (_jsx(Meter, { ...item, ...(scale !== undefined && { max: scale }) }, `${item.label}-${i}`)))
26
- : children }));
24
+ : undefined, ...props, children: _jsx("div", { className: "eidotter-meter-group__layout", children: items
25
+ ? items.map((item, i) => (_jsx(Meter, { ...item, ...(scale !== undefined && { max: scale }) }, `${item.label}-${i}`)))
26
+ : children }) }));
27
27
  });
28
28
  MeterGroup.displayName = 'MeterGroup';
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import type { DosDialogSize } from '../../DosDialog/components/DosDialog.js';
2
3
  export interface ModalProps {
3
4
  /** Whether the modal is open */
4
5
  isOpen: boolean;
@@ -6,19 +7,30 @@ export interface ModalProps {
6
7
  onOpenChange?: (isOpen: boolean) => void;
7
8
  /** @deprecated Use `onOpenChange` instead. Called when modal should close. */
8
9
  onClose?: () => void;
9
- /** Modal title (required for accessibility) */
10
+ /** Modal title (required for accessibility). Rendered in the title strip. */
10
11
  title: string;
11
12
  /** Modal body content */
12
13
  children: React.ReactNode;
13
14
  /** Footer content, typically action buttons */
14
15
  footer?: React.ReactNode;
15
- /** Optional CSS class name */
16
+ /**
17
+ * Width step from the shared dialog map (DosDialog): `sm` 24rem,
18
+ * `md` 30rem (default, the historical 480px), `lg` 40rem, `full`.
19
+ */
20
+ size?: DosDialogSize;
21
+ /** Close on backdrop click. Escape and the close button always close. Defaults to `true`. */
22
+ isDismissable?: boolean;
23
+ /** Optional CSS class name, merged onto the dialog container */
16
24
  className?: string;
17
25
  }
18
26
  /**
19
- * DOS-styled Modal with React Aria Dialog.
20
- * React Aria provides focus trapping, scroll lock, backdrop dismiss, and escape handling.
21
- * CRT phosphor enter/exit animations via isEntering/isExiting render props.
22
- * Ref forwards to the inner AriaDialog element (not the overlay).
27
+ * DOS-styled Modal on the DosDialog shell (DMNC-1592).
28
+ * The title sits in the title strip with the close control at its end; the
29
+ * footer keeps its 1px top rule. React Aria provides focus trapping, scroll
30
+ * lock, backdrop dismiss, and escape handling.
31
+ * Ref forwards to the dialog element (not the overlay).
32
+ *
33
+ * The close IconButton carries `eidotter-modal__close`. eidotter ships no CSS
34
+ * for it; it is kept as a stable consumer hook for targeting the control.
23
35
  */
24
36
  export declare const Modal: React.ForwardRefExoticComponent<ModalProps & React.RefAttributes<HTMLElement>>;
@@ -1,16 +1,20 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, forwardRef } from 'react';
4
- import { Button as AriaButton, ModalOverlay as AriaModalOverlay, Modal as AriaModal, Dialog as AriaDialog, } from 'react-aria-components';
5
- import { Icon } from '../../Icon/components/Icon.js';
4
+ import { IconButton } from '../../IconButton/components/IconButton.js';
5
+ import { DosDialog } from '../../DosDialog/components/DosDialog.js';
6
6
  import { cn } from '../../../utils/cn.js';
7
7
  /**
8
- * DOS-styled Modal with React Aria Dialog.
9
- * React Aria provides focus trapping, scroll lock, backdrop dismiss, and escape handling.
10
- * CRT phosphor enter/exit animations via isEntering/isExiting render props.
11
- * Ref forwards to the inner AriaDialog element (not the overlay).
8
+ * DOS-styled Modal on the DosDialog shell (DMNC-1592).
9
+ * The title sits in the title strip with the close control at its end; the
10
+ * footer keeps its 1px top rule. React Aria provides focus trapping, scroll
11
+ * lock, backdrop dismiss, and escape handling.
12
+ * Ref forwards to the dialog element (not the overlay).
13
+ *
14
+ * The close IconButton carries `eidotter-modal__close`. eidotter ships no CSS
15
+ * for it; it is kept as a stable consumer hook for targeting the control.
12
16
  */
13
- export const Modal = forwardRef(({ isOpen, onOpenChange, onClose, title, children, footer, className, }, ref) => {
17
+ export const Modal = forwardRef(({ isOpen, onOpenChange, onClose, title, children, footer, size = 'md', isDismissable = true, className, }, ref) => {
14
18
  const prevOpenRef = useRef(isOpen);
15
19
  const interactionRef = useRef(false);
16
20
  // Notify onOpenChange on prop-driven state changes only (observer pattern for agents).
@@ -32,6 +36,11 @@ export const Modal = forwardRef(({ isOpen, onOpenChange, onClose, title, childre
32
36
  if (!open)
33
37
  onClose?.();
34
38
  };
35
- return (_jsx(AriaModalOverlay, { isOpen: isOpen, onOpenChange: handleOpenChange, isDismissable: true, className: ({ isEntering, isExiting }) => cn('eidotter-modal-overlay', isEntering && 'eidotter-modal-overlay--entering', isExiting && 'eidotter-modal-overlay--exiting'), children: _jsx(AriaModal, { className: ({ isEntering, isExiting }) => cn('eidotter-modal', isEntering && 'eidotter-modal--entering', isExiting && 'eidotter-modal--exiting'), children: _jsxs(AriaDialog, { ref: ref, "aria-label": title, className: cn('eidotter-modal__container outline-none', className), children: [_jsxs("header", { className: "eidotter-modal__header", children: [_jsx("h2", { className: "eidotter-modal__title", children: title }), _jsx(AriaButton, { className: "eidotter-modal__close", onPress: () => handleOpenChange(false), "aria-label": "Close modal", children: _jsx(Icon, { name: "Close", size: "S" }) })] }), _jsx("div", { className: "eidotter-modal__body", children: children }), footer && (_jsx("footer", { className: "eidotter-modal__footer", children: footer }))] }) }) }));
39
+ return (_jsx(DosDialog, { ref: ref, open: isOpen, onOpenChange: handleOpenChange, isDismissable: isDismissable, size: size, "aria-label": title, className: cn('eidotter-modal__container', className), head: {
40
+ title,
41
+ end: (_jsx(IconButton, { icon: "Close", label: "Close modal", className: "eidotter-modal__close", onPress: () => handleOpenChange(false) })),
42
+ },
43
+ /* Modal keeps its <footer> element (role=contentinfo) inside the shell slot. */
44
+ footer: footer ? _jsx("footer", { className: "eidotter-modal__footer", children: footer }) : undefined, children: _jsx("div", { className: "eidotter-modal__body", children: children }) }));
36
45
  });
37
46
  Modal.displayName = 'Modal';
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useState, useCallback, useEffect, useRef } from 'react';
4
4
  import { cn } from '../../../utils/cn.js';
5
5
  import { isSafeHref } from '../../../utils/isSafeHref.js';
6
- import { Icon } from '../../Icon/index.js';
6
+ import { IconButton } from '../../IconButton/components/IconButton.js';
7
7
  const variantClasses = {
8
8
  retro: 'eidotter-nav--retro',
9
9
  modern: 'eidotter-nav--modern',
@@ -14,7 +14,8 @@ export const DesktopNav = ({ items, activeHref, variant = 'retro', linkComponent
14
14
  // Unsafe hrefs (javascript, data, vbscript, …) render the label as a
15
15
  // plain span instead of an anchor.
16
16
  const safeHref = isSafeHref(item.href) ? item.href : undefined;
17
- const linkClassName = cn('eidotter-nav__link', activeHref === item.href && 'eidotter-nav__link--active');
17
+ // Retro labels render uppercase DOS, like the MENU trigger (DMNC-1584).
18
+ const linkClassName = cn('eidotter-nav__link', variant === 'retro' && 'uppercase', activeHref === item.href && 'eidotter-nav__link--active');
18
19
  return (_jsx("li", { className: "eidotter-nav__desktop-item", children: safeHref ? (_jsx(LinkTag, { href: safeHref, className: linkClassName, "aria-current": activeHref === item.href ? 'page' : undefined, children: item.label })) : (_jsx("span", { className: linkClassName, children: item.label })) }, item.href));
19
20
  }) }) }));
20
21
  };
@@ -69,10 +70,10 @@ export const MobileNav = ({ items, activeHref, variant = 'retro', linkComponent,
69
70
  first.focus();
70
71
  }
71
72
  };
72
- return (_jsxs("div", { className: cn('eidotter-nav eidotter-nav--mobile', variantClasses[variant], className), children: [_jsx("button", { ref: triggerRef, onClick: toggle, className: "eidotter-nav__menu-trigger", "aria-label": isOpen ? 'Close menu' : 'Open menu', "aria-expanded": isOpen, "aria-controls": "eidotter-mobile-nav-panel", children: "MENU" }), isOpen && (_jsx("div", { className: "eidotter-nav__overlay", onClick: close, "aria-hidden": "true" })), _jsxs("nav", { ref: panelRef, id: "eidotter-mobile-nav-panel", className: cn('eidotter-nav__panel', isOpen && 'eidotter-nav__panel--open'), "aria-label": "Mobile navigation", inert: !isOpen, onKeyDown: handlePanelKeyDown, children: [_jsx("div", { className: "eidotter-nav__panel-header", children: _jsx("button", { ref: closeButtonRef, onClick: close, className: "eidotter-nav__close", "aria-label": "Close menu", children: _jsx(Icon, { name: "Close", size: "S" }) }) }), _jsx("ul", { className: "eidotter-nav__list", children: items.map((item) => {
73
+ return (_jsxs("div", { className: cn('eidotter-nav eidotter-nav--mobile', variantClasses[variant], className), children: [_jsx("button", { ref: triggerRef, onClick: toggle, className: "eidotter-nav__menu-trigger", "aria-label": isOpen ? 'Close menu' : 'Open menu', "aria-expanded": isOpen, "aria-controls": "eidotter-mobile-nav-panel", children: "MENU" }), isOpen && (_jsx("div", { className: "eidotter-nav__overlay", onClick: close, "aria-hidden": "true" })), _jsxs("nav", { ref: panelRef, id: "eidotter-mobile-nav-panel", className: cn('eidotter-nav__panel', isOpen && 'eidotter-nav__panel--open'), "aria-label": "Mobile navigation", inert: !isOpen, onKeyDown: handlePanelKeyDown, children: [_jsx("div", { className: "eidotter-nav__panel-header", children: _jsx(IconButton, { ref: closeButtonRef, icon: "Close", label: "Close menu", className: "eidotter-nav__close", onPress: close }) }), _jsx("ul", { className: "eidotter-nav__list", children: items.map((item) => {
73
74
  // Same guard as desktop — mobile panel is a second render path.
74
75
  const safeHref = isSafeHref(item.href) ? item.href : undefined;
75
- const linkClassName = cn('eidotter-nav__link', activeHref === item.href && 'eidotter-nav__link--active');
76
+ const linkClassName = cn('eidotter-nav__link', variant === 'retro' && 'uppercase', activeHref === item.href && 'eidotter-nav__link--active');
76
77
  return (_jsx("li", { className: "eidotter-nav__item", children: safeHref ? (_jsx(LinkTag, { href: safeHref, className: linkClassName, "aria-current": activeHref === item.href ? 'page' : undefined, onClick: close, children: item.label })) : (_jsx("span", { className: linkClassName, children: item.label })) }, item.href));
77
78
  }) })] })] }));
78
79
  };
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
4
4
  import { Button as AriaButton } from 'react-aria-components';
5
5
  import { cn } from '../../../utils/cn.js';
6
6
  import { Icon } from '../../Icon/components/Icon.js';
7
+ import { IconButton } from '../../IconButton/components/IconButton.js';
7
8
  import { Progress } from '../../Progress/components/Progress.js';
8
9
  import { useAnimatedDismiss } from '../../../hooks/useAnimatedDismiss.js';
9
10
  const TYPE_ICONS = {
@@ -20,7 +21,7 @@ const TYPE_ICONS = {
20
21
  * Purely presentational — consumers handle positioning.
21
22
  */
22
23
  export const Notification = ({ type = 'primary', title, children, actions, onClose, avatar, image, progress, progressLabel, duration, showIcon = true, className, }) => {
23
- const { isClosing, triggerClose, handleAnimationEnd } = useAnimatedDismiss('notification-exit', onClose);
24
+ const { isClosing, triggerClose, handleAnimationEnd } = useAnimatedDismiss('eidotter-pop-out', onClose);
24
25
  // Auto-dismiss timer
25
26
  useEffect(() => {
26
27
  if (duration == null || !onClose)
@@ -30,5 +31,5 @@ export const Notification = ({ type = 'primary', title, children, actions, onClo
30
31
  }, [duration, onClose, triggerClose]);
31
32
  const hasIcon = showIcon && !avatar;
32
33
  const hasProgress = progress != null;
33
- return (_jsxs("div", { className: cn('w-full max-w-[400px]', 'font-dos text-dos-text-sm', 'border border-dos-border-default rounded-dos-base', 'eidotter-notification', `eidotter-notification--${type}`, isClosing && 'eidotter-notification--closing', className), onAnimationEnd: handleAnimationEnd, role: type === 'error' ? 'alert' : 'status', "data-type": type, children: [image && (_jsx("div", { className: "eidotter-notification__image", children: image })), _jsxs("div", { className: "flex flex-row gap-4 p-4", children: [avatar && (_jsx("div", { className: "eidotter-notification__avatar flex-shrink-0", children: avatar })), hasIcon && (_jsx("div", { className: "eidotter-notification__icon", children: _jsx(Icon, { name: TYPE_ICONS[type] || 'Info', size: "S", "aria-label": `${type} notification` }) })), _jsxs("div", { className: cn('flex-1 flex flex-col gap-3 min-w-0', onClose && !hasIcon && !avatar && 'pr-8'), children: [(title || children) && (_jsxs("div", { className: "flex flex-col gap-1", children: [title && (_jsx("div", { className: "eidotter-notification__title font-dos leading-[140%]", children: title })), children && (_jsx("div", { className: "eidotter-notification__text font-dos leading-[140%]", children: children }))] })), hasProgress && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Progress, { value: progress, max: 100, size: "sm", fullWidth: true, showLabel: false }), progressLabel && (_jsx("div", { className: "eidotter-notification__text font-dos text-dos-text-xs", children: progressLabel }))] })), actions && actions.length > 0 && (_jsx("div", { className: "flex flex-row gap-3", children: actions.map((action, i) => (_jsxs(AriaButton, { className: "eidotter-notification__action", onPress: action.onClick, children: [action.label, action.icon] }, i))) }))] }), onClose && (_jsx(AriaButton, { className: "eidotter-notification__close", onPress: triggerClose, "aria-label": "Close notification", children: _jsx(Icon, { name: "Close", size: "S" }) }))] })] }));
34
+ return (_jsxs("div", { className: cn('w-full max-w-[400px]', 'font-dos text-dos-text-sm', 'border border-dos-border-default', 'eidotter-notification', `eidotter-notification--${type}`, isClosing && 'eidotter-notification--closing', className), onAnimationEnd: handleAnimationEnd, role: type === 'error' ? 'alert' : 'status', "data-type": type, children: [image && (_jsx("div", { className: "eidotter-notification__image", children: image })), _jsxs("div", { className: "flex flex-row gap-4 p-4", children: [avatar && (_jsx("div", { className: "eidotter-notification__avatar flex-shrink-0", children: avatar })), hasIcon && (_jsx("div", { className: "eidotter-notification__icon", children: _jsx(Icon, { name: TYPE_ICONS[type] || 'Info', size: "S", "aria-label": `${type} notification` }) })), _jsxs("div", { className: cn('flex-1 flex flex-col gap-3 min-w-0', onClose && !hasIcon && !avatar && 'pr-8'), children: [(title || children) && (_jsxs("div", { className: "flex flex-col gap-1", children: [title && (_jsx("div", { className: "eidotter-notification__title font-dos leading-[140%]", children: title })), children && (_jsx("div", { className: "eidotter-notification__text font-dos leading-[140%]", children: children }))] })), hasProgress && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Progress, { value: progress, max: 100, size: "sm", fullWidth: true, showLabel: false }), progressLabel && (_jsx("div", { className: "eidotter-notification__text font-dos text-dos-text-xs", children: progressLabel }))] })), actions && actions.length > 0 && (_jsx("div", { className: "flex flex-row gap-3", children: actions.map((action, i) => (_jsxs(AriaButton, { className: "eidotter-notification__action", onPress: action.onClick, children: [action.label, action.icon] }, i))) }))] }), onClose && (_jsx(IconButton, { icon: "Close", label: "Close notification", className: "eidotter-notification__close", onPress: triggerClose }))] })] }));
34
35
  };
@@ -5,13 +5,15 @@ const FILLED = '\u2588'; // █
5
5
  const EMPTY = '\u2591'; // ░
6
6
  const DARK_SHADE = '\u2593'; // ▓
7
7
  const MED_SHADE = '\u2592'; // ▒
8
+ // eidotter tokens only: raw Tailwind sizes (text-xs/base/lg) would compile
9
+ // into dist/eidotter.css and override consumers' own utilities (DMNC-1579).
8
10
  const sizeClasses = {
9
- sm: 'text-xs',
10
- md: 'text-base',
11
- lg: 'text-lg',
12
- small: 'text-xs',
13
- medium: 'text-base',
14
- large: 'text-lg',
11
+ sm: 'text-dos-text-base', // 16px
12
+ md: 'text-dos-text-sm', // 20px
13
+ lg: 'text-dos-text-md', // 22px
14
+ small: 'text-dos-text-base',
15
+ medium: 'text-dos-text-sm',
16
+ large: 'text-dos-text-md',
15
17
  };
16
18
  const variantClasses = {
17
19
  default: 'eidotter-progress--default',
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
3
  import { cn } from '../../../utils/cn.js';
4
4
  import { formatValue, resolveLabels } from '../../../utils/health/index.js';
@@ -72,7 +72,10 @@ export const RangeBar = forwardRef(({ segments, orientation = 'vertical', size =
72
72
  }
73
73
  const pct = (n) => formatValue(n, { locale, percent: true }).text;
74
74
  const summary = items.map((s) => `${s.label} ${pct(s.weight)}`).join(', ');
75
- return (_jsxs("div", { ref: ref, className: cn('eidotter-range-bar eidotter-health flex gap-3 font-dos', orientation === 'horizontal' ? 'flex-col' : 'flex-row items-start', orientationClasses[orientation] ?? orientationClasses.vertical, sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [_jsxs("div", { className: cn('eidotter-range-bar__track flex gap-1', orientation === 'horizontal' ? 'flex-col w-full' : 'flex-row'), children: [_jsx("div", { role: "img", "aria-label": summary, className: cn('eidotter-range-bar__bar flex shrink-0', orientation === 'horizontal' ? 'flex-row' : 'flex-col', trackClasses[`${orientation}-${size}`] ?? trackClasses['vertical-md']), children: items.map((s) => {
75
+ return (_jsxs("div", { ref: ref, className: cn('eidotter-range-bar eidotter-health flex gap-3 font-dos',
76
+ // Vertical: the legend wraps under the bar when bar and legend do not
77
+ // fit side by side in the containing width (DMNC-1610).
78
+ orientation === 'horizontal' ? 'flex-col' : 'flex-row flex-wrap items-start', orientationClasses[orientation] ?? orientationClasses.vertical, sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [_jsxs("div", { className: cn('eidotter-range-bar__track flex gap-1', orientation === 'horizontal' ? 'flex-col w-full' : 'flex-row'), children: [_jsx("div", { role: "img", "aria-label": summary, className: cn('eidotter-range-bar__bar flex shrink-0', orientation === 'horizontal' ? 'flex-row' : 'flex-col', trackClasses[`${orientation}-${size}`] ?? trackClasses['vertical-md']), children: items.map((s) => {
76
79
  const isTarget = target?.segmentId === s.id;
77
80
  return (_jsx("div", { "data-segment-id": s.id, className: cn('eidotter-range-bar__seg relative', toneClasses[s.toneKey], s.patternClass), style: {
78
81
  flexGrow: Math.max(s.weight, minSegmentShare),
@@ -81,10 +84,10 @@ export const RangeBar = forwardRef(({ segments, orientation = 'vertical', size =
81
84
  }, children: isTarget && target && (_jsx("span", { "aria-hidden": "true", className: "eidotter-range-bar__target absolute", style: {
82
85
  [orientation === 'horizontal' ? 'left' : 'top']: `${Math.min(Math.max(target.minShare / (s.weight || 1), 0), 1) * 100}%`,
83
86
  } })) }, s.id));
84
- }) }), _jsx("div", { "aria-hidden": "true", className: cn('eidotter-range-bar__lane flex tabular-nums text-dos-text-accent', orientation === 'horizontal' ? 'flex-row w-full' : 'flex-col'), children: items.map((s) => (_jsxs("div", { className: cn('eidotter-range-bar__cell flex items-center justify-center gap-1 min-w-0', orientation === 'horizontal' ? 'flex-col' : 'flex-row justify-start'), style: { flexGrow: Math.max(s.weight, minSegmentShare), flexBasis: 0 }, children: [s.weight >= labelThreshold && (_jsx("span", { className: "eidotter-range-bar__pct", children: pct(s.weight) })), target?.segmentId === s.id && (_jsx("span", { className: "eidotter-range-bar__target-label text-dos-text-brand whitespace-nowrap", children: target.label }))] }, s.id))) })] }), showLegend && (_jsx("ul", { className: "eidotter-range-bar__legend flex flex-col gap-1 list-none p-0 m-0 min-w-0", children: items.map((s) => {
85
- return (_jsxs("li", { className: "eidotter-range-bar__row grid grid-cols-[auto_1fr_auto_auto] items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: cn('eidotter-range-bar__swatch inline-block w-[0.75rem] h-[0.75rem]', toneClasses[s.toneKey], s.patternClass), style: {
87
+ }) }), _jsx("div", { "aria-hidden": "true", className: cn('eidotter-range-bar__lane flex tabular-nums text-dos-text-accent', orientation === 'horizontal' ? 'flex-row w-full' : 'flex-col'), children: items.map((s) => (_jsxs("div", { className: cn('eidotter-range-bar__cell flex items-center justify-center gap-x-2 gap-y-1 min-w-0', orientation === 'horizontal' ? 'flex-col' : 'flex-row justify-start'), style: { flexGrow: Math.max(s.weight, minSegmentShare), flexBasis: 0 }, children: [s.weight >= labelThreshold && (_jsx("span", { className: "eidotter-range-bar__pct", children: pct(s.weight) })), s.weight >= labelThreshold && target?.segmentId === s.id && ' ', target?.segmentId === s.id && (_jsx("span", { className: "eidotter-range-bar__target-label text-dos-text-brand whitespace-nowrap", children: target.label }))] }, s.id))) })] }), showLegend && (_jsx("ul", { className: "eidotter-range-bar__legend flex flex-col gap-1 list-none p-0 m-0 min-w-0 max-w-full", children: items.map((s) => {
88
+ return (_jsxs("li", { className: "eidotter-range-bar__row grid grid-cols-[auto_minmax(0,1fr)_auto_auto] items-baseline gap-2", children: [_jsx("span", { "aria-hidden": "true", className: cn('eidotter-range-bar__swatch inline-block w-[0.75rem] h-[0.75rem]', toneClasses[s.toneKey], s.patternClass), style: {
86
89
  ...(s.toneKey === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
87
- } }), _jsxs("span", { className: "text-dos-text-primary truncate", title: s.label, children: [s.label, s.bound && _jsxs("span", { className: "text-dos-text-muted", children: [" ", s.bound] })] }), _jsx("span", { className: "text-dos-text-accent tabular-nums", children: pct(s.weight) }), _jsx("span", { className: "text-dos-text-muted tabular-nums text-right", children: s.duration ?? '' })] }, s.id));
90
+ } }), _jsxs("span", { className: "eidotter-range-bar__name text-dos-text-primary min-w-0 break-words", children: [s.label, s.bound && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "eidotter-range-bar__bound text-dos-text-muted whitespace-nowrap", children: s.bound })] }))] }), _jsx("span", { className: "text-dos-text-accent tabular-nums", children: pct(s.weight) }), _jsx("span", { className: "text-dos-text-muted tabular-nums text-right", children: s.duration ?? '' })] }, s.id));
88
91
  }) }))] }));
89
92
  });
90
93
  RangeBar.displayName = 'RangeBar';
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { HealthLabels, Tone } from '../../../utils/health/index.js';
2
+ import type { FormatValueOptions, HealthLabels, Tone } from '../../../utils/health/index.js';
3
3
  export interface SeriesBarsPoint {
4
4
  /** Tick and table label. */
5
5
  label: string;
@@ -30,6 +30,14 @@ export interface SeriesBarsProps {
30
30
  size?: 'sm' | 'md' | 'lg';
31
31
  /** BCP 47 locale for number formatting (default `en`). */
32
32
  locale?: string;
33
+ /**
34
+ * Unit appended to every value and the goal in the accessible summary, the
35
+ * value table and the bar titles (e.g. `%` gives "70 %"). Use either this
36
+ * or `format.percent`, not both.
37
+ */
38
+ unit?: string;
39
+ /** Number formatting, the family's `formatValue` options (`digits`, `percent`, `signed`). */
40
+ format?: Pick<FormatValueOptions, 'digits' | 'percent' | 'signed'>;
33
41
  /** Overrides for the built-in label text. */
34
42
  labels?: Partial<HealthLabels>;
35
43
  /** Additional CSS class name. */
@@ -38,7 +38,7 @@ const patternClasses = {
38
38
  * `img` with a composed summary; a visually hidden table beside it carries the
39
39
  * values for assistive tech. Presentational and hook-free, so server-safe.
40
40
  */
41
- export const SeriesBars = forwardRef(({ points, max, goal, goalLabel, toneFor, tickEvery = 1, size = 'md', locale = 'en', labels: labelOverrides, className, ...props }, ref) => {
41
+ export const SeriesBars = forwardRef(({ points, max, goal, goalLabel, toneFor, tickEvery = 1, size = 'md', locale = 'en', unit, format, labels: labelOverrides, className, ...props }, ref) => {
42
42
  const labels = resolveLabels(labelOverrides);
43
43
  const known = points.map((p) => p.value).filter(isKnown);
44
44
  const top = max ?? Math.max(...known, goal ?? 0, 0);
@@ -47,18 +47,30 @@ export const SeriesBars = forwardRef(({ points, max, goal, goalLabel, toneFor, t
47
47
  const toneOf = (v) => thresholds
48
48
  .filter((t) => v >= t.from)
49
49
  .reduce((best, t) => (!best || t.from > best.from ? t : best), undefined)?.tone ?? 'neutral';
50
- const text = (v) => (isKnown(v) ? formatValue(v, { locale }).text : labels.unknown);
50
+ const goalBand = goal !== undefined && !!goalLabel;
51
+ const text = (v) => isKnown(v)
52
+ ? formatValue(v, { digits: format?.digits, percent: format?.percent, signed: format?.signed, locale, unit }).text
53
+ : labels.unknown;
54
+ // A goal label that already carries the formatted goal as a whole word
55
+ // ("Goal 70 %") is not repeated in the summary ("Goal 70 % 70 %").
56
+ const goalText = goal !== undefined ? text(goal) : '';
57
+ const words = (t) => ` ${t.replace(/\s+/g, ' ')} `;
58
+ const goalSummary = goalLabel && words(goalLabel).includes(words(goalText))
59
+ ? goalLabel
60
+ : `${goalLabel ?? ''} ${goalText}`.trim();
51
61
  const summary = [
52
62
  ...points.map((p) => `${p.label} ${text(p.value)}`),
53
- ...(goal !== undefined ? [`${goalLabel ?? ''} ${text(goal)}`.trim()] : []),
63
+ ...(goal !== undefined ? [goalSummary] : []),
54
64
  ].join(', ');
55
- return (_jsxs("div", { ref: ref, className: cn('eidotter-series-bars eidotter-health relative font-dos', sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [_jsxs("div", { role: "img", "aria-label": summary, children: [_jsxs("div", { className: cn('eidotter-series-bars__plot relative', plotClasses[size] ?? plotClasses.md), children: [goal !== undefined && (_jsx("div", { className: "eidotter-series-bars__goal absolute inset-x-0", style: { bottom: `${(Math.min(goal, scale) / scale) * 100}%` }, children: goalLabel && (_jsx("span", { className: "eidotter-series-bars__goal-tag text-dos-text-brand", children: goalLabel })) })), _jsx("div", { className: "eidotter-series-bars__cols flex items-end gap-1 h-full", children: points.map((p, i) => {
56
- const v = p.value;
57
- const tone = isKnown(v) ? toneOf(v) : 'neutral';
58
- return (_jsx("div", { className: "eidotter-series-bars__col flex flex-col justify-end flex-1 min-w-0 h-full", title: `${p.label}: ${text(v)}`, children: isKnown(v) ? (_jsx("div", { className: cn('eidotter-series-bars__bar w-full', toneClasses[tone], patternClasses[tone]), style: {
59
- height: `${(Math.min(Math.max(v, 0), scale) / scale) * 100}%`,
60
- ...(tone === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
61
- } })) : (_jsx("span", { className: "eidotter-series-bars__gap text-dos-text-muted text-center", children: PLACEHOLDER })) }, i));
62
- }) })] }), _jsx("div", { "aria-hidden": "true", className: "flex gap-1 text-dos-text-muted", children: points.map((p, i) => (_jsx("span", { className: "eidotter-series-bars__tick flex-1 min-w-0 truncate text-center tabular-nums", children: i % Math.max(tickEvery, 1) === 0 ? p.label : '' }, i))) })] }), _jsx("table", { className: "sr-only", children: _jsxs("tbody", { children: [points.map((p, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: p.label }), _jsx("td", { children: text(p.value) })] }, i))), goal !== undefined && (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: goalLabel }), _jsx("td", { children: text(goal) })] }))] }) })] }));
65
+ const step = Math.max(tickEvery, 1);
66
+ const last = points.length - 1;
67
+ return (_jsxs("div", { ref: ref, className: cn('eidotter-series-bars eidotter-health relative font-dos', sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [_jsxs("div", { role: "img", "aria-label": summary, children: [_jsxs("div", { className: cn('eidotter-series-bars__frame relative', goalBand && 'eidotter-series-bars__frame--goal-band'), children: [goalBand && (_jsx("span", { className: "eidotter-series-bars__goal-tag text-dos-text-brand", children: goalLabel })), _jsxs("div", { className: cn('eidotter-series-bars__plot relative', plotClasses[size] ?? plotClasses.md), children: [goal !== undefined && (_jsx("div", { className: "eidotter-series-bars__goal absolute inset-x-0", style: { bottom: `${(Math.min(goal, scale) / scale) * 100}%` } })), _jsx("div", { className: "eidotter-series-bars__cols flex items-end gap-1 h-full", children: points.map((p, i) => {
68
+ const v = p.value;
69
+ const tone = isKnown(v) ? toneOf(v) : 'neutral';
70
+ return (_jsx("div", { className: "eidotter-series-bars__col flex flex-col justify-end flex-1 min-w-0 h-full", title: `${p.label}: ${text(v)}`, children: isKnown(v) ? (_jsx("div", { className: cn('eidotter-series-bars__bar w-full', toneClasses[tone], patternClasses[tone]), style: {
71
+ height: `${(Math.min(Math.max(v, 0), scale) / scale) * 100}%`,
72
+ ...(tone === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
73
+ } })) : (_jsx("span", { className: "eidotter-series-bars__gap text-dos-text-muted text-center", children: PLACEHOLDER })) }, i));
74
+ }) })] })] }), _jsx("div", { "aria-hidden": "true", className: "eidotter-series-bars__ticks flex gap-1 text-dos-text-muted", children: points.map((p, i) => (_jsx("span", { className: cn('eidotter-series-bars__tick-cell flex flex-1 min-w-0', i === 0 ? 'justify-start' : i === last ? 'justify-end' : 'justify-center'), children: i % step === 0 && (_jsx("span", { className: cn('eidotter-series-bars__tick whitespace-nowrap tabular-nums', step === 1 && 'min-w-0 truncate'), children: p.label })) }, i))) })] }), _jsx("table", { className: "sr-only", children: _jsxs("tbody", { children: [points.map((p, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: p.label }), _jsx("td", { children: text(p.value) })] }, i))), goal !== undefined && (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: goalLabel }), _jsx("td", { children: text(goal) })] }))] }) })] }));
63
75
  });
64
76
  SeriesBars.displayName = 'SeriesBars';
@@ -37,6 +37,6 @@ export const Stat = forwardRef(({ label, value, trend, trendValue, size = 'md',
37
37
  });
38
38
  const displayValue = scramble ? scrambledValue : String(value);
39
39
  const s = sizeClasses[size] || sizeClasses.md;
40
- return (_jsxs("div", { ref: ref, className: cn('flex flex-col font-dos', 'eidotter-stat', s.root, className), ...props, children: [_jsx("span", { className: cn('text-dos-text-brand uppercase tracking-wider leading-tight', s.label), children: label }), _jsxs("span", { className: cn('text-dos-text-accent font-bold leading-none tabular-nums', s.value), children: [displayValue, unit && (_jsx("span", { className: "eidotter-stat__unit ml-1 text-[0.5em] font-normal text-dos-text-brand", children: unit }))] }), note && (_jsx("span", { className: `eidotter-stat__note text-dos-text-muted leading-tight ${s.trend}`, children: note })), target && (_jsx("span", { className: `eidotter-stat__target text-dos-text-brand leading-tight ${s.trend}`, children: target })), trend && (_jsxs("span", { className: cn('inline-flex items-center gap-1 leading-none', trendColorClasses[trend], s.trend), "aria-label": `Trend: ${trendLabels[trend]}${trendValue ? `, ${trendValue}` : ''}`, children: [_jsx("span", { className: "text-[0.8em]", "aria-hidden": "true", children: trendIcons[trend] }), trendValue && (_jsx("span", { className: "tabular-nums", children: trendValue }))] }))] }));
40
+ return (_jsxs("div", { ref: ref, className: cn('flex flex-col font-dos', 'eidotter-stat', s.root, className), ...props, children: [_jsx("span", { className: cn('text-dos-text-brand uppercase tracking-wider leading-tight', s.label), children: label }), _jsxs("span", { className: cn('text-dos-text-accent font-bold leading-none tabular-nums', s.value), children: [displayValue, unit && (_jsxs("span", { className: "eidotter-stat__unit text-[0.5em] font-normal text-dos-text-brand", children: [' ', unit] }))] }), note && (_jsx("span", { className: `eidotter-stat__note text-dos-text-muted leading-tight ${s.trend}`, children: note })), target && (_jsx("span", { className: `eidotter-stat__target text-dos-text-brand leading-tight ${s.trend}`, children: target })), trend && (_jsxs("span", { className: cn('inline-flex items-center gap-1 leading-none', trendColorClasses[trend], s.trend), "aria-label": `Trend: ${trendLabels[trend]}${trendValue ? `, ${trendValue}` : ''}`, children: [_jsx("span", { className: "text-[0.8em]", "aria-hidden": "true", children: trendIcons[trend] }), trendValue && (_jsx("span", { className: "tabular-nums", children: trendValue }))] }))] }));
41
41
  });
42
42
  Stat.displayName = 'Stat';