eidotter 0.41.0 → 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 +2 -2
- package/dist/components/DecayStrip/components/DecayStrip.js +1 -6
- package/dist/components/DosDialog/components/DosDialog.d.ts +13 -0
- package/dist/components/DosDialog/components/DosDialog.js +13 -4
- package/dist/components/Gauge/components/Gauge.js +1 -7
- package/dist/components/Icon/components/Icon.d.ts +7 -0
- package/dist/components/Icon/components/Icon.js +2 -2
- package/dist/components/IconButton/components/IconButton.js +1 -1
- 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 +15 -4
- package/dist/components/Stat/components/Stat.js +1 -1
- package/dist/components/VitalsStrip/components/VitalsStrip.js +2 -3
- package/dist/components/registry.js +9 -0
- package/dist/eidotter.css +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +861 -768
- 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 +1 -1
- package/migrations/0.41.0.md +3 -1
- package/package.json +2 -2
- package/src/styles/dos-utilities.css +6 -3
- package/src/styles/shadcn.css +28 -27
- package/src/styles/tokens.css +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
[](./LICENSE.md)
|
|
16
16
|
[](https://react.dev/)
|
|
17
17
|
|
|
18
|
-
**eiDotter** is a React component library that takes the DOS/CGA terminal aesthetic seriously: authentic 16-color palette, bitmap-accurate typography, phosphor glow physics, keyboard-first interactions.
|
|
18
|
+
**eiDotter** is a React component library that takes the DOS/CGA terminal aesthetic seriously: authentic 16-color palette, bitmap-accurate typography, phosphor glow physics, keyboard-first interactions. 59 components across forms, navigation, windowing, timelines, health displays, and terminal chrome — shipped as ES modules with a prebuilt stylesheet so consumers don't need Tailwind to use it.
|
|
19
19
|
|
|
20
20
|
→ **[Storybook](https://cinimody.github.io/eiDotter/)** · **[CHANGELOG](./CHANGELOG.md)** · **[Accessibility](./ACCESSIBILITY.md)** · **[Issues](https://github.com/CinimoDY/eiDotter/issues)** · **[npm](https://www.npmjs.com/package/eidotter)**
|
|
21
21
|
|
|
@@ -104,7 +104,7 @@ for client apps and `'use client'` files.
|
|
|
104
104
|
|
|
105
105
|
Every minor with breaking or visible changes ships an upgrade guide in the package, at `node_modules/eidotter/migrations/<version>.md` (source: [`migrations/`](./migrations/)). Read every guide between your installed version and the target, oldest first. Start with [0.41.0](./migrations/0.41.0.md).
|
|
106
106
|
|
|
107
|
-
0.x caret ranges stop at the next minor (`^0.40.2` never resolves to 0.41.0), so bump the range explicitly: `npm install eidotter@^0.41.0
|
|
107
|
+
0.x caret ranges stop at the next minor (`^0.40.2` never resolves to 0.41.0), so bump the range explicitly: `npm install eidotter@^0.41.1` (0.41.0 shipped before its review fixes; use 0.41.1 or later).
|
|
108
108
|
|
|
109
109
|
## Documentation
|
|
110
110
|
|
|
@@ -88,12 +88,7 @@ export const DecayStrip = forwardRef(({ items, samples, now, window, provenance,
|
|
|
88
88
|
.filter(Boolean)
|
|
89
89
|
.join('; ');
|
|
90
90
|
const compact = size === 'sm';
|
|
91
|
-
return (_jsxs("div", { ref: ref, className: [
|
|
92
|
-
// The size class (a text-* font size) is joined outside cn(): twMerge
|
|
93
|
-
// reads it as a colour and would drop text-dos-text-brand.
|
|
94
|
-
cn('eidotter-decay-strip eidotter-health flex flex-col gap-2 w-full min-w-0 font-dos text-dos-text-brand', toneClasses[tone], className),
|
|
95
|
-
sizeClasses[size] ?? sizeClasses.md,
|
|
96
|
-
].join(' '), ...props, children: [items.length === 0 ? (_jsx(EmptyState, { title: `${zeroPercent} ${labels.remaining}`, className: "py-2" })) : (_jsx("ul", { className: cn('flex gap-2 min-w-0', compact ? 'flex-col' : 'flex-wrap'), children: rows.map((r) => (_jsx("li", { className: cn('eidotter-decay-strip__item min-w-0', compact ? 'flex flex-wrap items-baseline gap-2 tabular-nums' : 'flex-1'), children: compact ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-dos-text-accent", title: r.item.label, children: r.item.label }), _jsx("span", { children: r.amountText }), _jsx("span", { children: r.shareText }), r.tagText && (_jsx("span", { className: "eidotter-decay-strip__item-provenance border border-dos-border-brand px-1", children: r.tagText }))] })) : (_jsx(Gauge, { label: r.item.label, value: r.share === null ? null : r.share * 100, secondary: r.item.amount === null ? undefined : r.amountText, size: "sm", tone: tone, provenance: r.item.provenance, locale: locale, labels: labelOverrides })) }, r.item.id))) })), _jsxs("div", { className: "eidotter-decay-strip__strip flex items-center gap-2 min-w-0 tabular-nums", role: "img", "aria-label": summary, children: [_jsx("span", { className: "eidotter-decay-strip__from shrink-0", children: fromText }), _jsx("span", { className: "eidotter-decay-strip__cells flex flex-1 min-w-0 overflow-hidden whitespace-nowrap leading-none", style: tone === 'neutral' ? undefined : { color: 'var(--eidotter-fill)' }, children: cells.map((cell, i) => {
|
|
91
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-decay-strip eidotter-health flex flex-col gap-2 w-full min-w-0 font-dos text-dos-text-brand', toneClasses[tone], sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [items.length === 0 ? (_jsx(EmptyState, { title: `${zeroPercent} ${labels.remaining}`, className: "py-2" })) : (_jsx("ul", { className: cn('flex gap-2 min-w-0', compact ? 'flex-col' : 'flex-wrap'), children: rows.map((r) => (_jsx("li", { className: cn('eidotter-decay-strip__item min-w-0', compact ? 'flex flex-wrap items-baseline gap-2 tabular-nums' : 'flex-1'), children: compact ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-dos-text-accent", title: r.item.label, children: r.item.label }), _jsx("span", { children: r.amountText }), _jsx("span", { children: r.shareText }), r.tagText && (_jsx("span", { className: "eidotter-decay-strip__item-provenance border border-dos-border-brand px-1", children: r.tagText }))] })) : (_jsx(Gauge, { label: r.item.label, value: r.share === null ? null : r.share * 100, secondary: r.item.amount === null ? undefined : r.amountText, size: "sm", tone: tone, provenance: r.item.provenance, locale: locale, labels: labelOverrides })) }, r.item.id))) })), _jsxs("div", { className: "eidotter-decay-strip__strip flex items-center gap-2 min-w-0 tabular-nums", role: "img", "aria-label": summary, children: [_jsx("span", { className: "eidotter-decay-strip__from shrink-0", children: fromText }), _jsx("span", { className: "eidotter-decay-strip__cells flex flex-1 min-w-0 overflow-hidden whitespace-nowrap leading-none", style: tone === 'neutral' ? undefined : { color: 'var(--eidotter-fill)' }, children: cells.map((cell, i) => {
|
|
97
92
|
const last = i === cells.length - 1;
|
|
98
93
|
const isNow = nowMs >= cell.start && (nowMs < cell.end || (last && nowMs === cell.end));
|
|
99
94
|
const future = !isNow && nowMs < cell.start && nowMs >= cells[0].start;
|
|
@@ -20,6 +20,16 @@ export interface DosDialogHead {
|
|
|
20
20
|
export interface DosDialogProps {
|
|
21
21
|
/** Controlled open state. */
|
|
22
22
|
open: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Render the same chrome in place, in document flow: no `ModalOverlay`,
|
|
25
|
+
* portal, scrim, focus trap, scroll lock, Escape or backdrop dismissal, and
|
|
26
|
+
* no enter/exit motion. The frame, strip, head, body, footer and end control
|
|
27
|
+
* keep their markup and classes; the container is a `role="group"` named
|
|
28
|
+
* like the dialog (no `aria-modal`). For docs pages and static previews
|
|
29
|
+
* that must show the chrome without hydration. `open` still gates
|
|
30
|
+
* rendering; `onOpenChange` and `isDismissable` are unused. Defaults to `false`.
|
|
31
|
+
*/
|
|
32
|
+
inline?: boolean;
|
|
23
33
|
/** Called when the dialog requests open/close (Escape, backdrop click). */
|
|
24
34
|
onOpenChange?: (open: boolean) => void;
|
|
25
35
|
/** Vertical anchor: `center` (default) or `top` (~18vh, CmdPalette's anchor). */
|
|
@@ -72,6 +82,9 @@ export interface DosDialogProps {
|
|
|
72
82
|
*
|
|
73
83
|
* The strip title is one line with an ellipsis by default; Modal lets it wrap.
|
|
74
84
|
*
|
|
85
|
+
* `inline` renders the same chrome in place (no overlay, portal or focus
|
|
86
|
+
* trap) for docs and static previews.
|
|
87
|
+
*
|
|
75
88
|
* Ref forwards to the dialog element (`.eidotter-dialog__container`).
|
|
76
89
|
*/
|
|
77
90
|
export declare const DosDialog: React.ForwardRefExoticComponent<DosDialogProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, isValidElement, useEffect, useId } from 'react';
|
|
4
4
|
import { ModalOverlay as AriaModalOverlay, Modal as AriaModal, Dialog as AriaDialog, } from 'react-aria-components';
|
|
5
5
|
import { cn } from '../../../utils/cn.js';
|
|
@@ -30,15 +30,18 @@ function isHeadObject(head) {
|
|
|
30
30
|
*
|
|
31
31
|
* The strip title is one line with an ellipsis by default; Modal lets it wrap.
|
|
32
32
|
*
|
|
33
|
+
* `inline` renders the same chrome in place (no overlay, portal or focus
|
|
34
|
+
* trap) for docs and static previews.
|
|
35
|
+
*
|
|
33
36
|
* Ref forwards to the dialog element (`.eidotter-dialog__container`).
|
|
34
37
|
*/
|
|
35
|
-
export const DosDialog = forwardRef(({ open, onOpenChange, placement = 'center', size = 'md', head, footer, isDismissable = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }, ref) => {
|
|
38
|
+
export const DosDialog = forwardRef(({ open, onOpenChange, inline = false, placement = 'center', size = 'md', head, footer, isDismissable = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }, ref) => {
|
|
36
39
|
const titleId = useId();
|
|
37
40
|
const reduced = prefersReducedMotion();
|
|
38
41
|
const structured = isHeadObject(head) ? head : null;
|
|
39
42
|
const labelledBy = ariaLabelledBy ??
|
|
40
43
|
(!ariaLabel && structured && !structured.decorative ? titleId : undefined);
|
|
41
|
-
const unnamed = open && !ariaLabel && !labelledBy;
|
|
44
|
+
const unnamed = open && !inline && !ariaLabel && !labelledBy;
|
|
42
45
|
useEffect(() => {
|
|
43
46
|
if (unnamed && process.env.NODE_ENV !== 'production') {
|
|
44
47
|
console.warn('[eidotter] DosDialog has no accessible name. A ReactNode or decorative head ' +
|
|
@@ -54,6 +57,12 @@ export const DosDialog = forwardRef(({ open, onOpenChange, placement = 'center',
|
|
|
54
57
|
else if (head != null && head !== false) {
|
|
55
58
|
strip = _jsx("div", { className: "eidotter-dialog__head", children: head });
|
|
56
59
|
}
|
|
57
|
-
|
|
60
|
+
const content = (_jsxs(_Fragment, { children: [strip, _jsx("div", { className: "eidotter-dialog__body", children: children }), footer != null && footer !== false && (_jsx("div", { className: "eidotter-dialog__footer", children: footer }))] }));
|
|
61
|
+
if (inline) {
|
|
62
|
+
if (!open)
|
|
63
|
+
return null;
|
|
64
|
+
return (_jsx("div", { className: cn('eidotter-dialog', `eidotter-dialog--${size}`, `eidotter-dialog--${placement}`, 'eidotter-dialog--inline'), children: _jsx("div", { ref: ref, role: "group", "aria-label": ariaLabel, "aria-labelledby": labelledBy, className: cn('eidotter-dialog__container outline-none', className), children: content }) }));
|
|
65
|
+
}
|
|
66
|
+
return (_jsx(AriaModalOverlay, { isOpen: open, onOpenChange: onOpenChange, isDismissable: isDismissable, className: ({ isEntering, isExiting }) => cn('eidotter-dialog__overlay', `eidotter-dialog__overlay--${placement}`, !reduced && isEntering && 'eidotter-dialog__overlay--entering', !reduced && isExiting && 'eidotter-dialog__overlay--exiting'), children: _jsx(AriaModal, { className: ({ isEntering, isExiting }) => cn('eidotter-dialog', `eidotter-dialog--${size}`, `eidotter-dialog--${placement}`, !reduced && isEntering && 'eidotter-dialog--entering', !reduced && isExiting && 'eidotter-dialog--exiting'), children: _jsx(AriaDialog, { ref: ref, "aria-label": ariaLabel, "aria-labelledby": labelledBy, className: cn('eidotter-dialog__container outline-none', className), children: content }) }) }));
|
|
58
67
|
});
|
|
59
68
|
DosDialog.displayName = 'DosDialog';
|
|
@@ -76,12 +76,6 @@ export const Gauge = forwardRef(({ label, shortLabel, value, max = 100, unit, se
|
|
|
76
76
|
const [secondaryHead, secondaryTail] = splitSecondary(secondary ?? '');
|
|
77
77
|
const secondaryPart = secondary && !unknown && !loading ? (_jsxs("span", { className: "eidotter-gauge__secondary text-dos-text-brand text-dos-text-xs tabular-nums leading-none", children: [_jsx("span", { className: "eidotter-gauge__secondary-head", children: secondaryHead }), secondaryTail !== null && (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "eidotter-gauge__secondary-tail", children: secondaryTail })] }))] })) : null;
|
|
78
78
|
const tagText = provenanceLabel(labels, provenance);
|
|
79
|
-
return (_jsxs("div", { ref: ref, className: cn('eidotter-gauge eidotter-health flex flex-col items-center gap-1 w-full font-dos', sizeClasses[size] ?? sizeClasses.md, toneClasses[effectiveTone], statusClasses[status], className), style: { minWidth: diameter }, "data-size": sizeKey, "data-has-short": shortLabel ? 'true' : 'false', role: "meter", "aria-valuemin": 0, "aria-valuemax": unknown ? 100 : max, "aria-valuenow": unknown || loading ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, "aria-busy": loading || undefined, ...props, children: [_jsxs("div", { className: "relative inline-flex items-center justify-center", children: [_jsx(ProgressRing, { decorative: true, value: loading ? 0 : pct, max: 100, size: diameter, thickness: thickness, segments: toneSegments[effectiveTone] ?? 16, color: effectiveTone === 'neutral' ? undefined : 'var(--eidotter-fill)', glow: false, className: cn(glow && 'eidotter-health-glow') }), _jsx("div", { className: "eidotter-gauge__center absolute flex flex-col items-center justify-center min-w-0 text-center leading-none", style: { inset: thickness + 2 }, children: loading ? (_jsx("div", { "aria-hidden": "true", className: "eidotter-gauge__skeleton w-full overflow-hidden", children: _jsx(Skeleton, { variant: "figure", lines: 2, animated: false }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: [
|
|
80
|
-
'eidotter-gauge__value text-dos-text-accent tabular-nums',
|
|
81
|
-
valueSizeClasses[sizeKey],
|
|
82
|
-
stale && 'opacity-60',
|
|
83
|
-
]
|
|
84
|
-
.filter(Boolean)
|
|
85
|
-
.join(' '), children: valueText }), !secondaryBelow && secondaryPart] })) })] }), secondaryBelow && secondaryPart, _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight max-w-full", children: [effectiveTone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__glyph text-dos-text-error", children: "\u25B2" })), _jsxs("span", { className: "eidotter-gauge__label truncate", title: label, children: [_jsx("span", { className: "eidotter-gauge__label--full", children: label }), shortLabel && _jsx("span", { className: "eidotter-gauge__label--short", children: shortLabel })] }), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__overflow text-dos-text-warning", children: "+" })), tagText !== null && (_jsx("span", { className: "eidotter-gauge__provenance border border-dos-border-brand px-1 normal-case", children: tagText })), stale && _jsx("span", { className: "eidotter-gauge__stale normal-case", children: labels.stale })] }), resolved.label && (_jsx("span", { className: "eidotter-gauge__band text-dos-text-brand normal-case", children: resolved.label }))] }));
|
|
79
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-gauge eidotter-health flex flex-col items-center gap-1 w-full font-dos', sizeClasses[size] ?? sizeClasses.md, toneClasses[effectiveTone], statusClasses[status], className), style: { minWidth: diameter }, "data-size": sizeKey, "data-has-short": shortLabel ? 'true' : 'false', role: "meter", "aria-valuemin": 0, "aria-valuemax": unknown ? 100 : max, "aria-valuenow": unknown || loading ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, "aria-busy": loading || undefined, ...props, children: [_jsxs("div", { className: "relative inline-flex items-center justify-center", children: [_jsx(ProgressRing, { decorative: true, value: loading ? 0 : pct, max: 100, size: diameter, thickness: thickness, segments: toneSegments[effectiveTone] ?? 16, color: effectiveTone === 'neutral' ? undefined : 'var(--eidotter-fill)', glow: false, className: cn(glow && 'eidotter-health-glow') }), _jsx("div", { className: "eidotter-gauge__center absolute flex flex-col items-center justify-center min-w-0 text-center leading-none", style: { inset: thickness + 2 }, children: loading ? (_jsx("div", { "aria-hidden": "true", className: "eidotter-gauge__skeleton w-full overflow-hidden", children: _jsx(Skeleton, { variant: "figure", lines: 2, animated: false }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('eidotter-gauge__value text-dos-text-accent tabular-nums', valueSizeClasses[sizeKey], stale && 'opacity-60'), children: valueText }), !secondaryBelow && secondaryPart] })) })] }), secondaryBelow && secondaryPart, _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight max-w-full", children: [effectiveTone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__glyph text-dos-text-error", children: "\u25B2" })), _jsxs("span", { className: "eidotter-gauge__label truncate", title: label, children: [_jsx("span", { className: "eidotter-gauge__label--full", children: label }), shortLabel && _jsx("span", { className: "eidotter-gauge__label--short", children: shortLabel })] }), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__overflow text-dos-text-warning", children: "+" })), tagText !== null && (_jsx("span", { className: "eidotter-gauge__provenance border border-dos-border-brand px-1 normal-case", children: tagText })), stale && _jsx("span", { className: "eidotter-gauge__stale normal-case", children: labels.stale })] }), resolved.label && (_jsx("span", { className: "eidotter-gauge__band text-dos-text-brand normal-case", children: resolved.label }))] }));
|
|
86
80
|
});
|
|
87
81
|
Gauge.displayName = 'Gauge';
|
|
@@ -40,6 +40,13 @@ export interface IconProps {
|
|
|
40
40
|
role?: 'button' | 'img';
|
|
41
41
|
/** Accessible label override */
|
|
42
42
|
'aria-label'?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Decorative glyph: renders `aria-hidden="true"` with no `role` and no
|
|
45
|
+
* `aria-label`, for icons inside a control that already carries the
|
|
46
|
+
* accessible name (IconButton). Defaults to `false` (announced as an image).
|
|
47
|
+
* Not for `role="button"` icons.
|
|
48
|
+
*/
|
|
49
|
+
decorative?: boolean;
|
|
43
50
|
}
|
|
44
51
|
/**
|
|
45
52
|
* Icon component backed by pixelarticons (MIT licensed).
|
|
@@ -62,7 +62,7 @@ const SIZE_MAP = { L: 56, S: 24 };
|
|
|
62
62
|
* Renders inline SVG with proper viewBox scaling at any size.
|
|
63
63
|
* The DOS pixel aesthetic matches eidotter's design language.
|
|
64
64
|
*/
|
|
65
|
-
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, }) => {
|
|
66
66
|
// Runtime defense against JS consumers or dynamically-cast names.
|
|
67
67
|
// TypeScript users are already constrained to IconName by IconProps.
|
|
68
68
|
const IconComponent = ICON_MAP[name];
|
|
@@ -87,5 +87,5 @@ export const Icon = ({ name, size = 'L', className = '', onClick, color, role, '
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
: undefined;
|
|
90
|
-
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" }) }));
|
|
91
91
|
};
|
|
@@ -15,5 +15,5 @@ import { cn } from '../../../utils/cn.js';
|
|
|
15
15
|
* React Aria `Button` handles press, keyboard and focus-visible. Ref forwards
|
|
16
16
|
* to the `<button>`.
|
|
17
17
|
*/
|
|
18
|
-
export const IconButton = forwardRef(({ icon, label, size = 'md', variant = 'outline', isDisabled, onPress, excludeFromTabOrder, className, }, ref) => (_jsx(AriaButton, { ref: ref, "aria-label": label, isDisabled: isDisabled, onPress: onPress, excludeFromTabOrder: excludeFromTabOrder, className: cn('inline-flex items-center justify-center shrink-0', 'eidotter-icon-button', size === 'window' && 'eidotter-icon-button--window', variant === 'bare' && 'eidotter-icon-button--bare', className), children: _jsx("span", { className: "eidotter-icon-button__glyph", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: "S" }) }) })));
|
|
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
19
|
IconButton.displayName = 'IconButton';
|
|
@@ -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);
|
|
@@ -48,11 +48,22 @@ export const SeriesBars = forwardRef(({ points, max, goal, goalLabel, toneFor, t
|
|
|
48
48
|
.filter((t) => v >= t.from)
|
|
49
49
|
.reduce((best, t) => (!best || t.from > best.from ? t : best), undefined)?.tone ?? 'neutral';
|
|
50
50
|
const goalBand = goal !== undefined && !!goalLabel;
|
|
51
|
-
const text = (v) =>
|
|
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();
|
|
52
61
|
const summary = [
|
|
53
62
|
...points.map((p) => `${p.label} ${text(p.value)}`),
|
|
54
|
-
...(goal !== undefined ? [
|
|
63
|
+
...(goal !== undefined ? [goalSummary] : []),
|
|
55
64
|
].join(', ');
|
|
65
|
+
const step = Math.max(tickEvery, 1);
|
|
66
|
+
const last = points.length - 1;
|
|
56
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) => {
|
|
57
68
|
const v = p.value;
|
|
58
69
|
const tone = isKnown(v) ? toneOf(v) : 'neutral';
|
|
@@ -60,6 +71,6 @@ export const SeriesBars = forwardRef(({ points, max, goal, goalLabel, toneFor, t
|
|
|
60
71
|
height: `${(Math.min(Math.max(v, 0), scale) / scale) * 100}%`,
|
|
61
72
|
...(tone === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
|
|
62
73
|
} })) : (_jsx("span", { className: "eidotter-series-bars__gap text-dos-text-muted text-center", children: PLACEHOLDER })) }, i));
|
|
63
|
-
}) })] })] }), _jsx("div", { "aria-hidden": "true", className: "flex gap-1 text-dos-text-muted", children: points.map((p, i) => (_jsx("span", { className:
|
|
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) })] }))] }) })] }));
|
|
64
75
|
});
|
|
65
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';
|
|
@@ -64,9 +64,8 @@ export const VitalsStrip = forwardRef(({ tiles, size = 'md', locale = 'en', labe
|
|
|
64
64
|
return (_jsxs("div", { role: "group", "aria-label": name, "aria-busy": loading || undefined, className: cn('eidotter-vitals-strip__tile flex flex-col gap-0.5 border border-dos-border-default p-2', toneClasses[tone], statusClasses[status], tone === 'critical' &&
|
|
65
65
|
'eidotter-vitals-strip__marker border-l-4 border-dos-border-error'), children: [_jsxs("span", { className: "flex items-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight", children: [tone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "text-dos-text-error", children: "\u25B2" })), _jsx("span", { className: "truncate", title: t.label, children: t.label })] }), loading ? (_jsx("div", { "aria-hidden": "true", className: "w-full overflow-hidden", children: _jsx(Skeleton, { variant: "figure", lines: 1, animated: false }) })) : (
|
|
66
66
|
/* Display size comes from VitalsStrip.css (a per-size custom property that
|
|
67
|
-
steps down in narrow strips), not a Tailwind text-* utility
|
|
68
|
-
|
|
69
|
-
accent colour. The reading never wraps or widens the tile; the unit
|
|
67
|
+
steps down in narrow strips), not a Tailwind text-* utility, which
|
|
68
|
+
cannot follow the container width. The reading never wraps or widens the tile; the unit
|
|
70
69
|
moves under it when both do not fit on one line (DMNC-1573). */
|
|
71
70
|
_jsxs("span", { className: "eidotter-vitals-strip__value text-dos-text-accent leading-none tabular-nums flex flex-wrap items-baseline gap-x-1 min-w-0", children: [_jsx("span", { className: "eidotter-vitals-strip__reading min-w-0 max-w-full whitespace-nowrap overflow-hidden text-ellipsis", children: stale ? (_jsx("span", { className: "opacity-60", children: valueText })) : stateText !== null ? (
|
|
72
71
|
// State words are prose, not a reading: body size, not display size.
|
|
@@ -429,6 +429,7 @@ export const componentRegistry = {
|
|
|
429
429
|
swiftui: { status: 'planned' },
|
|
430
430
|
},
|
|
431
431
|
changelog: [
|
|
432
|
+
{ version: '0.42.0', type: 'changed', description: 'The unit can drop under the value in a narrow card (a space before the unit is the break opportunity; was a 4px margin with no break) (DMNC-1610)' },
|
|
432
433
|
{ version: '0.40.0', type: 'changed', description: 'Add optional unit, note and target slots (additive; existing consumers unchanged)' },
|
|
433
434
|
{ version: '0.19.0', type: 'changed', description: 'Replace hardcoded font-size pixels with V.37 dos tokens' },
|
|
434
435
|
{ version: '0.16.0', type: 'changed', description: 'Migrate to Tailwind-first (Wave 2); deprecate size aliases; add size:sm/lg' },
|
|
@@ -745,9 +746,13 @@ export const componentRegistry = {
|
|
|
745
746
|
'size:md': { description: '30rem, default (Modal historical 480px)', since: '0.41.0' },
|
|
746
747
|
'size:lg': { description: '40rem (CmdPalette historical 640px)', since: '0.41.0' },
|
|
747
748
|
'size:full': { description: 'Viewport width minus 2x spacing-4 (Lightbox)', since: '0.41.0' },
|
|
749
|
+
'inline:false': { description: 'Modal overlay: React Aria ModalOverlay, portal, scrim, focus trap (default)', since: '0.41.0' },
|
|
750
|
+
'inline:true': { description: 'Same chrome in document flow: no overlay, portal or focus trap; role=group, no aria-modal (docs, static previews)', since: '0.42.0' },
|
|
748
751
|
},
|
|
749
752
|
platforms: { react: { path: 'src/components/DosDialog', status: 'canonical' } },
|
|
750
753
|
changelog: [
|
|
754
|
+
{ version: '0.42.0', type: 'added', description: 'inline prop: renders the same frame, strip, head, body, footer and end control in place without ModalOverlay, portal or focus trap, as a role=group with no aria-modal; server HTML carries the chrome (DMNC-1605)' },
|
|
755
|
+
{ version: '0.42.0', type: 'changed', description: 'Last end IconButton docks flush in the frame corner (0px top, 0px right, was 0px top, 4px right), the same rule as DosWindow controls (DMNC-1605)' },
|
|
751
756
|
{ version: '0.41.0', type: 'added', description: 'Initial DosDialog: open/onOpenChange, placement, size, head strip (title/end/marker/decorative or ReactNode), footer, isDismissable, pop-in/out panel (150ms) over a fading scrim, reduced-motion bypass' },
|
|
752
757
|
],
|
|
753
758
|
},
|
|
@@ -778,6 +783,7 @@ export const componentRegistry = {
|
|
|
778
783
|
},
|
|
779
784
|
platforms: { react: { path: 'src/components/DosWindow', status: 'canonical' } },
|
|
780
785
|
changelog: [
|
|
786
|
+
{ version: '0.42.0', type: 'changed', description: 'Last control IconButton docks flush in the frame corner (0px top, 0px right, was 0px top, 4px right); the inline-end overhang moved from the controls container to the last IconButton, so non-IconButton controls keep the strip padding (DMNC-1605)' },
|
|
781
787
|
{ version: '0.41.0', type: 'added', description: 'Initial DosWindow: title, marker, active, controls, footer, className; stable classes eidotter-window, __strip, __title, __controls, __body, __footer (DMNC-1592)' },
|
|
782
788
|
],
|
|
783
789
|
},
|
|
@@ -1012,6 +1018,7 @@ export const componentRegistry = {
|
|
|
1012
1018
|
},
|
|
1013
1019
|
changelog: [
|
|
1014
1020
|
{ version: '0.40.0', type: 'added', description: 'Initial RangeBar (health metabolism family)' },
|
|
1021
|
+
{ version: '0.42.0', type: 'changed', description: 'Vertical legend wraps under the bar in a narrow container; legend names wrap instead of truncating and a bound moves to the next line whole; percent and target label separated by a gap and a space; lane and legend text print black on white (DMNC-1610)' },
|
|
1015
1022
|
],
|
|
1016
1023
|
},
|
|
1017
1024
|
SeriesBars: {
|
|
@@ -1036,6 +1043,8 @@ export const componentRegistry = {
|
|
|
1036
1043
|
changelog: [
|
|
1037
1044
|
{ version: '0.40.0', type: 'added', description: 'Initial SeriesBars (health metabolism family)' },
|
|
1038
1045
|
{ version: '0.41.0', type: 'changed', description: 'Goal label moved into a reserved band above the plot so it never paints over a bar (DMNC-1573)' },
|
|
1046
|
+
{ version: '0.42.0', type: 'added', description: 'Additive unit and format (formatValue digits/percent/signed) props: values and the goal carry the unit in the summary, value table and bar titles; a goal label that already holds the goal value is not repeated (DMNC-1610)' },
|
|
1047
|
+
{ version: '0.42.0', type: 'changed', description: 'Sparse ticks (tickEvery > 1) overflow into empty neighbour columns instead of truncating; first and last labels pinned to the plot edges; goal tag and ticks print black on white (DMNC-1610)' },
|
|
1039
1048
|
],
|
|
1040
1049
|
},
|
|
1041
1050
|
DecayStrip: {
|