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.
- package/README.md +14 -6
- package/dist/components/Accordion/components/AccordionFill.js +1 -1
- package/dist/components/Accordion/components/Section.js +1 -1
- package/dist/components/Alert/components/Alert.js +3 -2
- package/dist/components/Brand/components/Logo.d.ts +7 -7
- package/dist/components/Brand/components/Logo.js +10 -15
- package/dist/components/Brand/components/LogoGeometry.d.ts +28 -0
- package/dist/components/Brand/components/LogoGeometry.js +78 -0
- package/dist/components/Brand/components/Mark.d.ts +13 -6
- package/dist/components/Brand/components/Mark.js +14 -10
- package/dist/components/Brand/components/Wordmark.js +1 -1
- package/dist/components/Brand/components/markGeometry.d.ts +15 -0
- package/dist/components/Brand/components/markGeometry.js +20 -7
- package/dist/components/Breadcrumb/components/Breadcrumb.js +2 -2
- package/dist/components/Button/components/Button.js +1 -1
- package/dist/components/Card/components/Card.d.ts +5 -0
- package/dist/components/Card/components/Card.js +8 -2
- package/dist/components/CmdPalette/components/CmdPalette.d.ts +4 -3
- package/dist/components/CmdPalette/components/CmdPalette.js +17 -12
- package/dist/components/DecayStrip/components/DecayStrip.js +1 -1
- package/dist/components/DosDialog/components/DosDialog.d.ts +90 -0
- package/dist/components/DosDialog/components/DosDialog.js +68 -0
- package/dist/components/DosDialog/components/index.d.ts +2 -0
- package/dist/components/DosDialog/components/index.js +1 -0
- package/dist/components/DosDialog/index.d.ts +2 -0
- package/dist/components/DosDialog/index.js +1 -0
- package/dist/components/DosFigure/components/DosFigure.d.ts +2 -2
- package/dist/components/DosFigure/components/DosFigure.js +14 -7
- package/dist/components/DosWindow/components/DosWindow.d.ts +39 -0
- package/dist/components/DosWindow/components/DosWindow.js +23 -0
- package/dist/components/DosWindow/components/index.d.ts +2 -0
- package/dist/components/DosWindow/components/index.js +1 -0
- package/dist/components/DosWindow/index.d.ts +2 -0
- package/dist/components/DosWindow/index.js +1 -0
- package/dist/components/FilterBar/components/FilterBar.d.ts +7 -4
- package/dist/components/Footer/components/Footer.js +1 -1
- package/dist/components/Gauge/components/Gauge.js +13 -2
- package/dist/components/Header/components/Header.d.ts +29 -1
- package/dist/components/Header/components/Header.js +20 -5
- package/dist/components/Icon/components/Icon.d.ts +9 -0
- package/dist/components/Icon/components/Icon.js +7 -3
- package/dist/components/IconButton/components/IconButton.d.ts +44 -0
- package/dist/components/IconButton/components/IconButton.js +19 -0
- package/dist/components/IconButton/components/index.d.ts +2 -0
- package/dist/components/IconButton/components/index.js +1 -0
- package/dist/components/IconButton/index.d.ts +2 -0
- package/dist/components/IconButton/index.js +1 -0
- package/dist/components/Input/components/Input.js +1 -1
- package/dist/components/Lightbox/components/Lightbox.d.ts +10 -4
- package/dist/components/Lightbox/components/Lightbox.js +19 -11
- package/dist/components/Meter/components/Meter.js +7 -3
- package/dist/components/Meter/components/MeterGroup.js +3 -3
- package/dist/components/Modal/components/Modal.d.ts +18 -6
- package/dist/components/Modal/components/Modal.js +18 -9
- package/dist/components/Nav/components/Nav.js +5 -4
- package/dist/components/Notification/components/Notification.js +3 -2
- package/dist/components/Progress/components/Progress.js +8 -6
- package/dist/components/RangeBar/components/RangeBar.js +8 -5
- package/dist/components/SeriesBars/components/SeriesBars.d.ts +9 -1
- package/dist/components/SeriesBars/components/SeriesBars.js +23 -11
- package/dist/components/Stat/components/Stat.js +1 -1
- package/dist/components/Tag/components/Tag.d.ts +8 -1
- package/dist/components/Tag/components/Tag.js +5 -5
- package/dist/components/VitalsStrip/components/VitalsStrip.js +9 -7
- package/dist/components/registry.js +230 -19
- package/dist/eidotter.css +3 -2
- package/dist/hooks/useAnimatedDismiss.js +5 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.es.js +2414 -1747
- package/dist/index.umd.js +1 -1
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +33 -1
- package/dist/utils/dosFontSizes.d.ts +2 -0
- package/dist/utils/dosFontSizes.js +17 -0
- package/llms.txt +15 -7
- package/migrations/0.41.0.md +883 -0
- package/migrations/README.md +46 -0
- package/package.json +11 -3
- package/public/favicon.svg +1 -1
- package/src/styles/dos-utilities.css +9 -1
- package/src/styles/shadcn.css +63 -27
- package/src/styles/theme.amber-mono.css +4 -2
- package/src/styles/theme.amber-paper.css +106 -0
- package/src/styles/theme.cga-amber.css +3 -1
- package/src/styles/theme.cga-mode4-p0.css +3 -1
- package/src/styles/theme.cga-mode4-p1.css +3 -1
- package/src/styles/theme.cga-mode5.css +3 -1
- package/src/styles/theme.light.css +3 -1
- package/src/styles/tokens.css +5 -1
- package/tailwind.preset.cjs +23 -1
- package/dist/components/Brand/components/markGeometry.test.d.ts +0 -1
- package/dist/components/Connector/components/connectorGeometry.test.d.ts +0 -1
- package/dist/components/DecayStrip/components/resample.test.d.ts +0 -1
- package/dist/components/TimelineContainer/components/timelineUtils.test.d.ts +0 -1
- package/dist/components/TimelineContainer/components/useDrillDown.test.d.ts +0 -1
- package/dist/components/TimelineContainer/components/useSelection.test.d.ts +0 -1
- package/dist/components/TimelineContainer/components/useZoom.test.d.ts +0 -1
- package/dist/hooks/useTextScramble.test.d.ts +0 -1
- package/dist/hooks/useThemePortal.test.d.ts +0 -1
- package/dist/utils/health/formatValue.test.d.ts +0 -1
- package/dist/utils/health/healthPatterns.test.d.ts +0 -1
- package/dist/utils/health/resolveBand.test.d.ts +0 -1
- package/dist/utils/isSafeHref.test.d.ts +0 -1
- 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
|
-
//
|
|
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 @@
|
|
|
1
|
+
export { IconButton } from './IconButton.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-
|
|
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
|
|
16
|
+
* Fullscreen image viewer on the DosDialog shell (DMNC-1592).
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
|
7
|
+
* Fullscreen image viewer on the DosDialog shell (DMNC-1592).
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
//
|
|
82
|
-
//
|
|
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(
|
|
95
|
-
onClose(); },
|
|
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,
|
|
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 && (
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
/**
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, forwardRef } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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('
|
|
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
|
|
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-
|
|
10
|
-
md: 'text-
|
|
11
|
-
lg: 'text-
|
|
12
|
-
small: 'text-
|
|
13
|
-
medium: 'text-
|
|
14
|
-
large: 'text-
|
|
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',
|
|
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-[
|
|
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
|
|
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
|
|
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 ? [
|
|
63
|
+
...(goal !== undefined ? [goalSummary] : []),
|
|
54
64
|
].join(', ');
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 && (
|
|
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';
|