eidotter 0.38.1 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/dist/components/Brand/components/Mark.d.ts +55 -0
- package/dist/components/Brand/components/Mark.js +87 -0
- package/dist/components/Brand/components/index.d.ts +4 -0
- package/dist/components/Brand/components/index.js +2 -0
- package/dist/components/Brand/components/markGeometry.d.ts +26 -0
- package/dist/components/Brand/components/markGeometry.js +54 -0
- package/dist/components/Brand/components/markGeometry.test.d.ts +1 -0
- package/dist/components/Breadcrumb/components/Breadcrumb.js +7 -3
- package/dist/components/Connector/components/Connector.d.ts +41 -0
- package/dist/components/Connector/components/Connector.js +165 -0
- package/dist/components/Connector/components/connectorGeometry.d.ts +50 -0
- package/dist/components/Connector/components/connectorGeometry.js +102 -0
- package/dist/components/Connector/components/connectorGeometry.test.d.ts +1 -0
- package/dist/components/Connector/components/index.d.ts +4 -0
- package/dist/components/Connector/components/index.js +2 -0
- package/dist/components/Connector/index.d.ts +2 -0
- package/dist/components/Connector/index.js +1 -0
- package/dist/components/DecayStrip/components/DecayStrip.d.ts +60 -0
- package/dist/components/DecayStrip/components/DecayStrip.js +99 -0
- package/dist/components/DecayStrip/components/index.d.ts +3 -0
- package/dist/components/DecayStrip/components/index.js +1 -0
- package/dist/components/DecayStrip/components/resample.d.ts +34 -0
- package/dist/components/DecayStrip/components/resample.js +44 -0
- package/dist/components/DecayStrip/components/resample.test.d.ts +1 -0
- package/dist/components/DecayStrip/index.d.ts +2 -0
- package/dist/components/DecayStrip/index.js +1 -0
- package/dist/components/Footer/components/Footer.js +6 -1
- package/dist/components/Gauge/components/Gauge.d.ts +43 -0
- package/dist/components/Gauge/components/Gauge.js +70 -0
- package/dist/components/Gauge/components/index.d.ts +2 -0
- package/dist/components/Gauge/components/index.js +1 -0
- package/dist/components/Gauge/index.d.ts +2 -0
- package/dist/components/Gauge/index.js +1 -0
- package/dist/components/Header/components/Header.d.ts +31 -0
- package/dist/components/Header/components/Header.js +14 -3
- package/dist/components/Header/components/HeaderContextRow.d.ts +33 -0
- package/dist/components/Header/components/HeaderContextRow.js +58 -0
- package/dist/components/Header/components/index.d.ts +1 -1
- package/dist/components/Header/index.d.ts +1 -1
- package/dist/components/Icon/components/Icon.d.ts +7 -0
- package/dist/components/Icon/components/Icon.js +9 -0
- package/dist/components/Icon/components/index.d.ts +1 -1
- package/dist/components/Icon/components/index.js +1 -1
- package/dist/components/InlineExpand/components/InlineExpand.d.ts +5 -3
- package/dist/components/InlineExpand/components/InlineExpand.js +15 -13
- package/dist/components/InlineLink/components/InlineLink.js +5 -9
- package/dist/components/Meter/components/Meter.d.ts +45 -0
- package/dist/components/Meter/components/Meter.js +79 -0
- package/dist/components/Meter/components/MeterGroup.d.ts +26 -0
- package/dist/components/Meter/components/MeterGroup.js +28 -0
- package/dist/components/Meter/components/index.d.ts +4 -0
- package/dist/components/Meter/components/index.js +2 -0
- package/dist/components/Meter/index.d.ts +2 -0
- package/dist/components/Meter/index.js +1 -0
- package/dist/components/Nav/components/Nav.js +14 -2
- package/dist/components/ProgressRing/components/ProgressRing.d.ts +5 -0
- package/dist/components/ProgressRing/components/ProgressRing.js +10 -2
- package/dist/components/RangeBar/components/RangeBar.d.ts +50 -0
- package/dist/components/RangeBar/components/RangeBar.js +90 -0
- package/dist/components/RangeBar/components/index.d.ts +2 -0
- package/dist/components/RangeBar/components/index.js +1 -0
- package/dist/components/RangeBar/index.d.ts +2 -0
- package/dist/components/RangeBar/index.js +1 -0
- package/dist/components/SeriesBars/components/SeriesBars.d.ts +43 -0
- package/dist/components/SeriesBars/components/SeriesBars.js +64 -0
- package/dist/components/SeriesBars/components/index.d.ts +2 -0
- package/dist/components/SeriesBars/components/index.js +1 -0
- package/dist/components/SeriesBars/index.d.ts +2 -0
- package/dist/components/SeriesBars/index.js +1 -0
- package/dist/components/Stat/components/Stat.d.ts +6 -0
- package/dist/components/Stat/components/Stat.js +2 -2
- package/dist/components/TimelineFeed/components/TimelineFeed.d.ts +37 -0
- package/dist/components/TimelineFeed/components/TimelineFeed.js +59 -0
- package/dist/components/TimelineFeed/components/index.d.ts +2 -0
- package/dist/components/TimelineFeed/components/index.js +1 -0
- package/dist/components/TimelineFeed/index.d.ts +2 -0
- package/dist/components/TimelineFeed/index.js +1 -0
- package/dist/components/TimelinePage/components/TimelinePage.d.ts +35 -0
- package/dist/components/TimelinePage/components/TimelinePage.js +77 -0
- package/dist/components/TimelinePage/components/index.d.ts +2 -0
- package/dist/components/TimelinePage/components/index.js +1 -0
- package/dist/components/TimelinePage/index.d.ts +2 -0
- package/dist/components/TimelinePage/index.js +1 -0
- package/dist/components/VitalsStrip/components/VitalsStrip.d.ts +40 -0
- package/dist/components/VitalsStrip/components/VitalsStrip.js +73 -0
- package/dist/components/VitalsStrip/components/index.d.ts +2 -0
- package/dist/components/VitalsStrip/components/index.js +1 -0
- package/dist/components/VitalsStrip/index.d.ts +2 -0
- package/dist/components/VitalsStrip/index.js +1 -0
- package/dist/components/registry.d.ts +1 -1
- package/dist/components/registry.js +254 -2
- package/dist/eidotter.css +1 -1
- package/dist/index.d.ts +25 -5
- package/dist/index.es.js +3897 -1902
- package/dist/index.umd.js +3 -3
- package/dist/utils/health/formatValue.d.ts +29 -0
- package/dist/utils/health/formatValue.js +46 -0
- package/dist/utils/health/formatValue.test.d.ts +1 -0
- package/dist/utils/health/healthPatterns.test.d.ts +1 -0
- package/dist/utils/health/index.d.ts +6 -0
- package/dist/utils/health/index.js +3 -0
- package/dist/utils/health/labels.d.ts +6 -0
- package/dist/utils/health/labels.js +18 -0
- package/dist/utils/health/resolveBand.d.ts +12 -0
- package/dist/utils/health/resolveBand.js +29 -0
- package/dist/utils/health/resolveBand.test.d.ts +1 -0
- package/dist/utils/health/types.d.ts +25 -0
- package/dist/utils/health/types.js +2 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/isSafeHref.d.ts +26 -1
- package/dist/utils/isSafeHref.js +27 -15
- package/dist/utils/isSafeHref.test.d.ts +1 -0
- package/dist/utils/isSafeUrl.d.ts +5 -0
- package/dist/utils/isSafeUrl.js +5 -0
- package/guidelines/README.md +1 -0
- package/guidelines/components.md +25 -0
- package/guidelines/health-metabolism-patterns.md +212 -0
- package/llms.txt +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NavProps } from '../../Nav/index.js';
|
|
3
|
+
import type { HeaderContext } from './Header.js';
|
|
4
|
+
export interface HeaderContextRowProps {
|
|
5
|
+
/** Category badge row + optional returnTo pill (DMNC-1326). */
|
|
6
|
+
context: HeaderContext;
|
|
7
|
+
/** Same custom link component threaded from Header for framework routing. */
|
|
8
|
+
linkComponent?: NavProps['linkComponent'];
|
|
9
|
+
/**
|
|
10
|
+
* Currently active href — the category whose href matches gets
|
|
11
|
+
* aria-current="page" + the underline active class (WCAG 1.4.1 non-color
|
|
12
|
+
* state, mirroring Nav). This is the non-SVG active state the future
|
|
13
|
+
* tentacle mode keys off (PLAN-tentacle-menu).
|
|
14
|
+
*/
|
|
15
|
+
activeHref?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The measurable second row of the Header (DMNC-1326).
|
|
19
|
+
*
|
|
20
|
+
* Emits the stable measurement/transform contract the Mark arm-connector
|
|
21
|
+
* (DMNC-1325) and the future tentacle overlay (PLAN-tentacle-menu) anchor off:
|
|
22
|
+
* `.eidotter-header__context` / `.eidotter-header__categories` /
|
|
23
|
+
* `.eidotter-header__category` + `li[data-category-key]`. Do not rename them.
|
|
24
|
+
*
|
|
25
|
+
* Internal only — never exported from a barrel. Carries `'use client'` because
|
|
26
|
+
* the reuseTab pill attaches an onClick handler (moves the RSC boundary here,
|
|
27
|
+
* keeping Header.tsx server-safe).
|
|
28
|
+
*
|
|
29
|
+
* Nav-link boundary safety (per #470's shared policy): category + returnTo
|
|
30
|
+
* hrefs pass through `isSafeHref` and fall back to a non-anchor span when an
|
|
31
|
+
* unsafe scheme (javascript:, data:, …) is supplied — same guard as Nav/Footer.
|
|
32
|
+
*/
|
|
33
|
+
export declare function HeaderContextRow({ context, linkComponent, activeHref }: HeaderContextRowProps): React.JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { isSafeHref } from '../../../utils/isSafeHref.js';
|
|
5
|
+
import { Badge } from '../../Badge/index.js';
|
|
6
|
+
import { Icon, isIconName } from '../../Icon/index.js';
|
|
7
|
+
const returnPillClasses = cn('eidotter-header__return', 'inline-flex items-center gap-1 no-underline', 'border border-dos-border-default rounded-dos-base', 'px-2 py-0.5 text-dos-text-xs uppercase tracking-wider');
|
|
8
|
+
/**
|
|
9
|
+
* The measurable second row of the Header (DMNC-1326).
|
|
10
|
+
*
|
|
11
|
+
* Emits the stable measurement/transform contract the Mark arm-connector
|
|
12
|
+
* (DMNC-1325) and the future tentacle overlay (PLAN-tentacle-menu) anchor off:
|
|
13
|
+
* `.eidotter-header__context` / `.eidotter-header__categories` /
|
|
14
|
+
* `.eidotter-header__category` + `li[data-category-key]`. Do not rename them.
|
|
15
|
+
*
|
|
16
|
+
* Internal only — never exported from a barrel. Carries `'use client'` because
|
|
17
|
+
* the reuseTab pill attaches an onClick handler (moves the RSC boundary here,
|
|
18
|
+
* keeping Header.tsx server-safe).
|
|
19
|
+
*
|
|
20
|
+
* Nav-link boundary safety (per #470's shared policy): category + returnTo
|
|
21
|
+
* hrefs pass through `isSafeHref` and fall back to a non-anchor span when an
|
|
22
|
+
* unsafe scheme (javascript:, data:, …) is supplied — same guard as Nav/Footer.
|
|
23
|
+
*/
|
|
24
|
+
export function HeaderContextRow({ context, linkComponent, activeHref }) {
|
|
25
|
+
// Cast to ElementType so href/aria-current/className all type-check whether
|
|
26
|
+
// LinkTag resolves to a custom component or the intrinsic anchor.
|
|
27
|
+
const LinkTag = linkComponent ?? 'a';
|
|
28
|
+
const { categories, returnTo } = context;
|
|
29
|
+
const safeReturnHref = returnTo && isSafeHref(returnTo.href) ? returnTo.href : undefined;
|
|
30
|
+
const handleReuseTabClick = (event, href) => {
|
|
31
|
+
if (event.defaultPrevented)
|
|
32
|
+
return;
|
|
33
|
+
// Preserve native Cmd/Ctrl/Shift/Alt/middle-click "open in new tab".
|
|
34
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
35
|
+
return;
|
|
36
|
+
const win = window.open(href, 'rizomorf-shell');
|
|
37
|
+
if (win) {
|
|
38
|
+
// Only cancel same-tab navigation when the named tab actually opened;
|
|
39
|
+
// a popup blocker returns null → fall back to normal same-tab nav.
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
// Best-effort focus. Same-origin only — browsers ignore cross-origin focus.
|
|
42
|
+
win.focus();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const returnLabel = returnTo && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: "\u2190" }), _jsx("span", { children: returnTo.label })] }));
|
|
46
|
+
return (_jsxs("div", { className: cn('eidotter-header__context', 'flex w-full flex-wrap items-center gap-x-4 gap-y-2'), children: [returnTo && (safeReturnHref === undefined ? (
|
|
47
|
+
// Unsafe href → render the label without an anchor (Nav's fallback).
|
|
48
|
+
_jsx("span", { className: returnPillClasses, children: returnLabel })) : returnTo.reuseTab ? (
|
|
49
|
+
// reuseTab bypasses linkComponent — a router link would intercept the
|
|
50
|
+
// click and window.open would never fire.
|
|
51
|
+
_jsx("a", { href: safeReturnHref, onClick: (e) => handleReuseTabClick(e, safeReturnHref), className: returnPillClasses, children: returnLabel })) : (_jsx(LinkTag, { href: safeReturnHref, className: returnPillClasses, children: returnLabel }))), categories.length > 0 && (_jsx("nav", { "aria-label": "Categories", className: "eidotter-header__categories-nav", children: _jsx("ul", { className: cn('eidotter-header__categories', 'm-0 flex list-none flex-wrap items-center gap-2 p-0'), children: categories.map((category) => {
|
|
52
|
+
const safeHref = isSafeHref(category.href) ? category.href : undefined;
|
|
53
|
+
const isActive = activeHref !== undefined && category.href === activeHref;
|
|
54
|
+
const linkClass = cn('eidotter-header__category inline-flex no-underline', isActive && 'eidotter-header__category--active');
|
|
55
|
+
const badge = (_jsxs(Badge, { size: "sm", variant: "default", children: [isIconName(category.icon) && (_jsx("span", { "aria-hidden": "true", className: "me-1 inline-flex", children: _jsx(Icon, { name: category.icon, size: "S", className: "eidotter-header__category-icon" }) })), category.label] }));
|
|
56
|
+
return (_jsx("li", { "data-category-key": category.key, className: "eidotter-header__category-item", children: safeHref !== undefined ? (_jsx(LinkTag, { href: safeHref, className: linkClass, "aria-current": isActive ? 'page' : undefined, children: badge })) : (_jsx("span", { className: linkClass, children: badge })) }, category.key));
|
|
57
|
+
}) }) }))] }));
|
|
58
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Header } from './Header.js';
|
|
2
|
-
export type { HeaderProps } from './Header.js';
|
|
2
|
+
export type { HeaderProps, HeaderContext, HeaderCategory, HeaderReturnTo } from './Header.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Header } from './components/index.js';
|
|
2
|
-
export type { HeaderProps } from './components/index.js';
|
|
2
|
+
export type { HeaderProps, HeaderContext, HeaderCategory, HeaderReturnTo } from './components/index.js';
|
|
@@ -15,6 +15,13 @@ declare const ICON_MAP: {
|
|
|
15
15
|
Add: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
|
|
16
16
|
};
|
|
17
17
|
export type IconName = keyof typeof ICON_MAP;
|
|
18
|
+
/**
|
|
19
|
+
* Runtime guard for arbitrary icon-name strings (e.g. Header context
|
|
20
|
+
* categories whose `icon` is developer-supplied and not statically typed).
|
|
21
|
+
* Narrows to `IconName` so `<Icon>` can render only when the name is real —
|
|
22
|
+
* unknown strings should render label-only, never an empty icon wrapper.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isIconName(name: string): name is IconName;
|
|
18
25
|
export type IconSize = 'L' | 'S';
|
|
19
26
|
export interface IconProps {
|
|
20
27
|
/** The name of the icon to display */
|
|
@@ -43,6 +43,15 @@ const ICON_MAP = {
|
|
|
43
43
|
'Fullscreen': Expand,
|
|
44
44
|
'Add': Plus,
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Runtime guard for arbitrary icon-name strings (e.g. Header context
|
|
48
|
+
* categories whose `icon` is developer-supplied and not statically typed).
|
|
49
|
+
* Narrows to `IconName` so `<Icon>` can render only when the name is real —
|
|
50
|
+
* unknown strings should render label-only, never an empty icon wrapper.
|
|
51
|
+
*/
|
|
52
|
+
export function isIconName(name) {
|
|
53
|
+
return name in ICON_MAP;
|
|
54
|
+
}
|
|
46
55
|
const SIZE_MAP = { L: 56, S: 24 };
|
|
47
56
|
/**
|
|
48
57
|
* Icon component backed by pixelarticons (MIT licensed).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Icon } from './Icon.js';
|
|
1
|
+
export { Icon, isIconName } from './Icon.js';
|
|
2
2
|
export type { IconProps, IconName, IconSize } from './Icon.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Icon } from './Icon.js';
|
|
1
|
+
export { Icon, isIconName } from './Icon.js';
|
|
@@ -50,8 +50,10 @@ export interface InlineExpandProps {
|
|
|
50
50
|
* - DOS-authentic styling with phosphor glow and CGA tokens
|
|
51
51
|
* - WCAG 2.1 AA compliant
|
|
52
52
|
*
|
|
53
|
-
* Content
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Content is conditionally rendered — it mounts on expand and unmounts on
|
|
54
|
+
* collapse (like Section), so it occupies no inline space when closed. The
|
|
55
|
+
* expanded text continues the trigger's line with a marker-pen highlight per
|
|
56
|
+
* wrapped fragment; a trailing collapse control and a compact favicon-only
|
|
57
|
+
* sources cluster follow the last word. Reveal is an in-place opacity fade.
|
|
56
58
|
*/
|
|
57
59
|
export declare const InlineExpand: React.FC<InlineExpandProps>;
|
|
@@ -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 { useState, useId, useRef } from 'react';
|
|
4
4
|
import { Button as AriaButton } from 'react-aria-components';
|
|
5
5
|
import { cn } from '../../../utils/cn.js';
|
|
@@ -26,23 +26,21 @@ function getGoogleFaviconUrl(sourceUrl) {
|
|
|
26
26
|
* - DOS-authentic styling with phosphor glow and CGA tokens
|
|
27
27
|
* - WCAG 2.1 AA compliant
|
|
28
28
|
*
|
|
29
|
-
* Content
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Content is conditionally rendered — it mounts on expand and unmounts on
|
|
30
|
+
* collapse (like Section), so it occupies no inline space when closed. The
|
|
31
|
+
* expanded text continues the trigger's line with a marker-pen highlight per
|
|
32
|
+
* wrapped fragment; a trailing collapse control and a compact favicon-only
|
|
33
|
+
* sources cluster follow the last word. Reveal is an in-place opacity fade.
|
|
32
34
|
*/
|
|
33
35
|
export const InlineExpand = ({ children, content, defaultExpanded = false, expanded, onToggle, sources = [], className, ...props }) => {
|
|
34
36
|
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
35
37
|
// Tracks the current fallback stage per source URL:
|
|
36
38
|
// undefined = try primary; 'google' = primary failed, try Google Favicons; 'icon' = both failed
|
|
37
39
|
const [faviconFallbacks, setFaviconFallbacks] = useState({});
|
|
38
|
-
const hasBeenExpanded = useRef(defaultExpanded);
|
|
39
40
|
const contentId = useId();
|
|
40
41
|
const triggerRef = useRef(null);
|
|
41
42
|
const isControlled = expanded !== undefined;
|
|
42
43
|
const isExpanded = isControlled ? expanded : internalExpanded;
|
|
43
|
-
if (isExpanded) {
|
|
44
|
-
hasBeenExpanded.current = true;
|
|
45
|
-
}
|
|
46
44
|
const handleToggle = () => {
|
|
47
45
|
const next = !isExpanded;
|
|
48
46
|
if (!isControlled) {
|
|
@@ -50,6 +48,13 @@ export const InlineExpand = ({ children, content, defaultExpanded = false, expan
|
|
|
50
48
|
}
|
|
51
49
|
onToggle?.(next);
|
|
52
50
|
};
|
|
51
|
+
// The trailing [-] control unmounts when it collapses the content; return
|
|
52
|
+
// focus to the trigger so a keyboard/screen-reader user isn't dropped to
|
|
53
|
+
// <body>. Routes through handleToggle so controlled mode still works.
|
|
54
|
+
const handleCollapseFromTrailing = () => {
|
|
55
|
+
handleToggle();
|
|
56
|
+
triggerRef.current?.focus();
|
|
57
|
+
};
|
|
53
58
|
const handleKeyDown = (event) => {
|
|
54
59
|
if (event.key === 'Escape' && isExpanded) {
|
|
55
60
|
event.stopPropagation();
|
|
@@ -57,10 +62,7 @@ export const InlineExpand = ({ children, content, defaultExpanded = false, expan
|
|
|
57
62
|
triggerRef.current?.focus();
|
|
58
63
|
}
|
|
59
64
|
};
|
|
60
|
-
return (_jsxs("span", { className: cn('eidotter-inline-expand', isExpanded && 'eidotter-inline-expand--expanded', className), onKeyDown: handleKeyDown, ...props, children: [_jsxs(AriaButton, { ref: triggerRef, className: "eidotter-inline-expand__trigger", "aria-expanded": isExpanded, "aria-controls": contentId, onPress: handleToggle, children: [children, _jsx("span", { className: "eidotter-inline-expand__indicator", "aria-hidden": "true", children: isExpanded ? '[-]' : '[+]' })] }),
|
|
61
|
-
if (!hasBeenExpanded.current) {
|
|
62
|
-
return _jsx("span", { className: "eidotter-inline-expand__source-icon", "aria-hidden": "true", children: "[\u2192]" });
|
|
63
|
-
}
|
|
65
|
+
return (_jsxs("span", { className: cn('eidotter-inline-expand', isExpanded && 'eidotter-inline-expand--expanded', className), "aria-live": "polite", onKeyDown: handleKeyDown, ...props, children: [_jsxs(AriaButton, { ref: triggerRef, className: "eidotter-inline-expand__trigger", "aria-expanded": isExpanded, "aria-controls": contentId, onPress: handleToggle, children: [children, _jsx("span", { className: "eidotter-inline-expand__indicator", "aria-hidden": "true", children: isExpanded ? '[-]' : '[+]' })] }), isExpanded && (_jsxs(_Fragment, { children: [' ', _jsxs("span", { id: contentId, className: "eidotter-inline-expand__content", "data-ai-skip": "true", children: [content, _jsx(AriaButton, { className: "eidotter-inline-expand__collapse", "aria-expanded": true, "aria-controls": contentId, "aria-label": "Collapse", onPress: handleCollapseFromTrailing, children: "[-]" }), sources.length > 0 && (_jsx("span", { className: "eidotter-inline-expand__sources", role: "list", children: sources.map((source) => (_jsx("span", { className: "eidotter-inline-expand__source-item", role: "listitem", children: _jsx("a", { href: isSafeUrl(source.url) ? source.url : undefined, className: "eidotter-inline-expand__source-link", target: "_blank", rel: "noopener noreferrer", title: source.title, "aria-label": `${source.title} (opens external website)`, children: (() => {
|
|
64
66
|
const stage = faviconFallbacks[source.url];
|
|
65
67
|
if (stage === 'icon') {
|
|
66
68
|
return _jsx("span", { className: "eidotter-inline-expand__source-icon", "aria-hidden": "true", children: "[\u2192]" });
|
|
@@ -74,5 +76,5 @@ export const InlineExpand = ({ children, content, defaultExpanded = false, expan
|
|
|
74
76
|
return (_jsx("img", { className: "eidotter-inline-expand__source-favicon", src: googleUrl, alt: "", width: 16, height: 16, decoding: "async", onError: () => setFaviconFallbacks(prev => ({ ...prev, [source.url]: 'icon' })) }));
|
|
75
77
|
}
|
|
76
78
|
return _jsx("span", { className: "eidotter-inline-expand__source-icon", "aria-hidden": "true", children: "[\u2192]" });
|
|
77
|
-
})()
|
|
79
|
+
})() }) }, source.url))) }))] })] }))] }));
|
|
78
80
|
};
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { isSafeHref } from '../../../utils/isSafeHref.js';
|
|
4
5
|
// Relative paths, hash/query fragments, and standard nav schemes are allowed.
|
|
5
6
|
// Everything else (javascript:, data:, vbscript:, unknown schemes) collapses
|
|
6
|
-
// to a safe "#" sentinel.
|
|
7
|
+
// to a safe "#" sentinel. Delegates to the shared isSafeHref util with
|
|
8
|
+
// InlineLink's wider allowlist (tel/ftp/sms); behavior is unchanged — the empty
|
|
9
|
+
// passthrough and "#" fallback are InlineLink-specific and preserved here.
|
|
7
10
|
function sanitizeHref(href) {
|
|
8
11
|
const trimmed = href.trim();
|
|
9
12
|
if (trimmed === '')
|
|
10
13
|
return trimmed;
|
|
11
|
-
|
|
12
|
-
return trimmed;
|
|
13
|
-
const schemeMatch = /^([a-z][a-z0-9+\-.]*):/i.exec(trimmed);
|
|
14
|
-
if (!schemeMatch)
|
|
15
|
-
return trimmed;
|
|
16
|
-
const scheme = schemeMatch[1].toLowerCase();
|
|
17
|
-
const allowed = ['http', 'https', 'mailto', 'tel', 'ftp', 'sms'];
|
|
18
|
-
return allowed.includes(scheme) ? trimmed : '#';
|
|
14
|
+
return isSafeHref(trimmed, { extraSchemes: ['tel', 'ftp', 'sms'] }) ? trimmed : '#';
|
|
19
15
|
}
|
|
20
16
|
// Union-merge caller rel tokens with the required safety tokens so
|
|
21
17
|
// consumer-supplied `rel="external"` still gets `noopener noreferrer` when the
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Band, HealthLabels, Tone } from '../../../utils/health/index.js';
|
|
3
|
+
export interface MeterProps {
|
|
4
|
+
/** Full label. Always part of the accessible name. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Shown instead of `label` below the medium container width. */
|
|
7
|
+
shortLabel?: string;
|
|
8
|
+
/** Current value. `null`, `NaN` and `Infinity` render as unknown. */
|
|
9
|
+
value: number | null;
|
|
10
|
+
/** Maximum value (default 100). Zero or negative renders as unknown. */
|
|
11
|
+
max?: number;
|
|
12
|
+
/** Unit appended to the value text and used to flag mismatched band units in dev. */
|
|
13
|
+
unit?: string;
|
|
14
|
+
/** Optimal value (tick) or `[lo, hi]` range (band), on the same scale as `value`. */
|
|
15
|
+
optimal?: number | [number, number];
|
|
16
|
+
/** Bar direction. */
|
|
17
|
+
orientation?: 'vertical' | 'horizontal';
|
|
18
|
+
/** Size variant. */
|
|
19
|
+
size?: 'xs' | 'sm' | 'md';
|
|
20
|
+
/** Explicit tone. Wins over `bands`. */
|
|
21
|
+
tone?: Tone;
|
|
22
|
+
/** Fill pattern. Defaults to the pattern paired with the tone. */
|
|
23
|
+
pattern?: 'solid' | 'dither' | 'hatch' | 'dotted';
|
|
24
|
+
/** Render the bar as this many quantised blocks (round half away). */
|
|
25
|
+
segments?: number;
|
|
26
|
+
/** Consumer-supplied bands. Resolved on the raw value. */
|
|
27
|
+
bands?: Band[];
|
|
28
|
+
/** BCP 47 locale for number formatting (default `en`). */
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** Overrides for the built-in label text. */
|
|
31
|
+
labels?: Partial<HealthLabels>;
|
|
32
|
+
/** Show the value text (default true). The accessible value text is unaffected. */
|
|
33
|
+
showValue?: boolean;
|
|
34
|
+
/** Additional CSS class name. */
|
|
35
|
+
className?: string;
|
|
36
|
+
/** Replaces `label` in the composed accessible name. */
|
|
37
|
+
'aria-label'?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Single-bar meter, vertical or horizontal, with label, value text, optional
|
|
41
|
+
* optimal tick or range, tone with a paired fill pattern, and optional
|
|
42
|
+
* quantised blocks. Text never sits on the fill (R13). Presentational and
|
|
43
|
+
* hook-free, so it stays server-safe.
|
|
44
|
+
*/
|
|
45
|
+
export declare const Meter: React.ForwardRefExoticComponent<MeterProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { formatValue, isKnown, resolveBand, resolveLabels, PLACEHOLDER } from '../../../utils/health/index.js';
|
|
5
|
+
const orientationClasses = {
|
|
6
|
+
vertical: 'eidotter-meter--vertical flex-col items-center gap-1',
|
|
7
|
+
horizontal: 'eidotter-meter--horizontal flex-row items-center gap-2',
|
|
8
|
+
};
|
|
9
|
+
const sizeClasses = {
|
|
10
|
+
xs: 'eidotter-meter--xs text-dos-text-xs',
|
|
11
|
+
sm: 'eidotter-meter--sm text-dos-text-xs',
|
|
12
|
+
md: 'eidotter-meter--md text-dos-text-sm',
|
|
13
|
+
};
|
|
14
|
+
const toneClasses = {
|
|
15
|
+
neutral: 'eidotter-tone--neutral',
|
|
16
|
+
info: 'eidotter-tone--info',
|
|
17
|
+
success: 'eidotter-tone--success',
|
|
18
|
+
warning: 'eidotter-tone--warning',
|
|
19
|
+
critical: 'eidotter-tone--critical',
|
|
20
|
+
};
|
|
21
|
+
const patternClasses = {
|
|
22
|
+
solid: 'eidotter-fill--solid',
|
|
23
|
+
dither: 'eidotter-fill--dither',
|
|
24
|
+
hatch: 'eidotter-fill--hatch',
|
|
25
|
+
dotted: 'eidotter-fill--dotted',
|
|
26
|
+
};
|
|
27
|
+
/** Default pattern per tone (KTD3): neutral reuses solid on its own hue. */
|
|
28
|
+
const tonePatterns = {
|
|
29
|
+
neutral: 'solid',
|
|
30
|
+
info: 'solid',
|
|
31
|
+
success: 'dither',
|
|
32
|
+
warning: 'hatch',
|
|
33
|
+
critical: 'dotted',
|
|
34
|
+
};
|
|
35
|
+
const round4 = (n) => Math.round(n * 10000) / 10000;
|
|
36
|
+
const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
37
|
+
/** CSS custom properties for the optimal tick or range; `max` must be valid. */
|
|
38
|
+
function optimalStyle(optimal, max) {
|
|
39
|
+
if (optimal === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (Array.isArray(optimal)) {
|
|
42
|
+
const lo = clamp01(optimal[0] / max);
|
|
43
|
+
const hi = clamp01(optimal[1] / max);
|
|
44
|
+
return {
|
|
45
|
+
'--eidotter-meter-optimal-start': String(round4(lo)),
|
|
46
|
+
'--eidotter-meter-optimal-size': String(round4(hi - lo)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { '--eidotter-meter-optimal': String(round4(clamp01(optimal / max))) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Single-bar meter, vertical or horizontal, with label, value text, optional
|
|
53
|
+
* optimal tick or range, tone with a paired fill pattern, and optional
|
|
54
|
+
* quantised blocks. Text never sits on the fill (R13). Presentational and
|
|
55
|
+
* hook-free, so it stays server-safe.
|
|
56
|
+
*/
|
|
57
|
+
export const Meter = forwardRef(({ label, shortLabel, value, max = 100, unit, optimal, orientation = 'vertical', size = 'sm', tone, pattern, segments, bands, locale = 'en', labels: labelOverrides, showValue = true, className, 'aria-label': ariaLabel, ...props }, ref) => {
|
|
58
|
+
const labels = resolveLabels(labelOverrides);
|
|
59
|
+
const maxValid = max > 0 && Number.isFinite(max);
|
|
60
|
+
const unknown = !isKnown(value) || !maxValid;
|
|
61
|
+
const ratio = unknown ? 0 : clamp01(value / max);
|
|
62
|
+
const overflow = !unknown && value > max;
|
|
63
|
+
const resolved = resolveBand(unknown ? null : value, bands, unit);
|
|
64
|
+
const effectiveTone = tone ?? resolved.tone;
|
|
65
|
+
const effectivePattern = pattern ?? tonePatterns[effectiveTone] ?? 'solid';
|
|
66
|
+
const fillClass = patternClasses[effectivePattern] ?? patternClasses.solid;
|
|
67
|
+
const valueText = unknown ? PLACEHOLDER : formatValue(value, { locale, unit }).text;
|
|
68
|
+
const valuetext = unknown ? labels.unknown : valueText;
|
|
69
|
+
const name = `${ariaLabel ?? label}: ${valuetext}`;
|
|
70
|
+
const isRange = Array.isArray(optimal);
|
|
71
|
+
const optimalProps = maxValid ? optimalStyle(optimal, max) : undefined;
|
|
72
|
+
const blockCount = segments !== undefined ? Math.max(0, Math.floor(segments)) : 0;
|
|
73
|
+
const filledBlocks = Math.round(ratio * blockCount);
|
|
74
|
+
const valuePart = showValue || overflow ? (_jsxs("div", { className: "eidotter-meter__readout flex items-center gap-1 leading-none", children: [showValue && (_jsx("span", { className: "eidotter-meter__value text-dos-text-accent tabular-nums", children: valueText })), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-meter__overflow text-dos-text-warning", children: "+" }))] })) : null;
|
|
75
|
+
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
|
+
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] })) }));
|
|
78
|
+
});
|
|
79
|
+
Meter.displayName = 'Meter';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { HealthLabels } from '../../../utils/health/index.js';
|
|
3
|
+
import type { MeterProps } from './Meter.js';
|
|
4
|
+
export interface MeterGroupProps {
|
|
5
|
+
/** Row wraps under the medium container width; grid uses `columns`. */
|
|
6
|
+
layout?: 'row' | 'grid';
|
|
7
|
+
/** Fixed column count for `layout="grid"`. Omit to auto-fit. */
|
|
8
|
+
columns?: number;
|
|
9
|
+
/** Scale every bar to the largest `max` in `items`. */
|
|
10
|
+
sharedMax?: boolean;
|
|
11
|
+
/** Meters to render. Takes precedence over `children`. */
|
|
12
|
+
items?: MeterProps[];
|
|
13
|
+
/** Meter elements, used when `items` is not given. */
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
/** Overrides for built-in text (the empty state uses `unknown`). */
|
|
16
|
+
labels?: Partial<HealthLabels>;
|
|
17
|
+
/** Additional CSS class name. */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Accessible name for the group. */
|
|
20
|
+
'aria-label'?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lays Meters out as a row or grid, optionally on one shared scale.
|
|
24
|
+
* Renders EmptyState when there is nothing to show. Hook-free.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MeterGroup: React.ForwardRefExoticComponent<MeterGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { forwardRef } from 'react';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { EmptyState } from '../../EmptyState/index.js';
|
|
5
|
+
import { resolveLabels } from '../../../utils/health/index.js';
|
|
6
|
+
import { Meter } from './Meter.js';
|
|
7
|
+
const layoutClasses = {
|
|
8
|
+
row: 'eidotter-meter-group--row',
|
|
9
|
+
grid: 'eidotter-meter-group--grid',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Lays Meters out as a row or grid, optionally on one shared scale.
|
|
13
|
+
* Renders EmptyState when there is nothing to show. Hook-free.
|
|
14
|
+
*/
|
|
15
|
+
export const MeterGroup = forwardRef(({ layout = 'row', columns, sharedMax = false, items, children, labels: labelOverrides, className, ...props }, ref) => {
|
|
16
|
+
const hasChildren = React.Children.count(children) > 0;
|
|
17
|
+
if ((!items || items.length === 0) && !hasChildren) {
|
|
18
|
+
return _jsx(EmptyState, { title: resolveLabels(labelOverrides).unknown, className: className });
|
|
19
|
+
}
|
|
20
|
+
const scale = sharedMax && items ? Math.max(...items.map((i) => i.max ?? 100)) : undefined;
|
|
21
|
+
const fixedColumns = layout === 'grid' && columns !== undefined;
|
|
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
|
+
? { '--eidotter-meter-columns': String(columns) }
|
|
24
|
+
: undefined, ...props, children: items
|
|
25
|
+
? items.map((item, i) => (_jsx(Meter, { ...item, ...(scale !== undefined && { max: scale }) }, `${item.label}-${i}`)))
|
|
26
|
+
: children }));
|
|
27
|
+
});
|
|
28
|
+
MeterGroup.displayName = 'MeterGroup';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Meter, MeterGroup } from './components/index.js';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
import { isSafeHref } from '../../../utils/isSafeHref.js';
|
|
5
6
|
import { Icon } from '../../Icon/index.js';
|
|
6
7
|
const variantClasses = {
|
|
7
8
|
retro: 'eidotter-nav--retro',
|
|
@@ -9,7 +10,13 @@ const variantClasses = {
|
|
|
9
10
|
};
|
|
10
11
|
export const DesktopNav = ({ items, activeHref, variant = 'retro', linkComponent, className, }) => {
|
|
11
12
|
const LinkTag = linkComponent || 'a';
|
|
12
|
-
return (_jsx("nav", { className: cn('eidotter-nav eidotter-nav--desktop', variantClasses[variant], className), "aria-label": "Main navigation", children: _jsx("ul", { className: "eidotter-nav__desktop-list", children: items.map((item) =>
|
|
13
|
+
return (_jsx("nav", { className: cn('eidotter-nav eidotter-nav--desktop', variantClasses[variant], className), "aria-label": "Main navigation", children: _jsx("ul", { className: "eidotter-nav__desktop-list", children: items.map((item) => {
|
|
14
|
+
// Unsafe hrefs (javascript, data, vbscript, …) render the label as a
|
|
15
|
+
// plain span instead of an anchor.
|
|
16
|
+
const safeHref = isSafeHref(item.href) ? item.href : undefined;
|
|
17
|
+
const linkClassName = cn('eidotter-nav__link', activeHref === item.href && 'eidotter-nav__link--active');
|
|
18
|
+
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
|
+
}) }) }));
|
|
13
20
|
};
|
|
14
21
|
export const MobileNav = ({ items, activeHref, variant = 'retro', linkComponent, className, }) => {
|
|
15
22
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -62,7 +69,12 @@ export const MobileNav = ({ items, activeHref, variant = 'retro', linkComponent,
|
|
|
62
69
|
first.focus();
|
|
63
70
|
}
|
|
64
71
|
};
|
|
65
|
-
return (_jsxs("div", { className: cn('eidotter-nav eidotter-nav--mobile', variantClasses[variant], className), children: [_jsx("button", { ref: triggerRef, onClick: toggle, className: "eidotter-nav__menu-trigger", "aria-label": isOpen ? 'Close menu' : 'Open menu', "aria-expanded": isOpen, "aria-controls": "eidotter-mobile-nav-panel", children: "MENU" }), isOpen && (_jsx("div", { className: "eidotter-nav__overlay", onClick: close, "aria-hidden": "true" })), _jsxs("nav", { ref: panelRef, id: "eidotter-mobile-nav-panel", className: cn('eidotter-nav__panel', isOpen && 'eidotter-nav__panel--open'), "aria-label": "Mobile navigation", inert: !isOpen, onKeyDown: handlePanelKeyDown, children: [_jsx("div", { className: "eidotter-nav__panel-header", children: _jsx("button", { ref: closeButtonRef, onClick: close, className: "eidotter-nav__close", "aria-label": "Close menu", children: _jsx(Icon, { name: "Close", size: "S" }) }) }), _jsx("ul", { className: "eidotter-nav__list", children: items.map((item) =>
|
|
72
|
+
return (_jsxs("div", { className: cn('eidotter-nav eidotter-nav--mobile', variantClasses[variant], className), children: [_jsx("button", { ref: triggerRef, onClick: toggle, className: "eidotter-nav__menu-trigger", "aria-label": isOpen ? 'Close menu' : 'Open menu', "aria-expanded": isOpen, "aria-controls": "eidotter-mobile-nav-panel", children: "MENU" }), isOpen && (_jsx("div", { className: "eidotter-nav__overlay", onClick: close, "aria-hidden": "true" })), _jsxs("nav", { ref: panelRef, id: "eidotter-mobile-nav-panel", className: cn('eidotter-nav__panel', isOpen && 'eidotter-nav__panel--open'), "aria-label": "Mobile navigation", inert: !isOpen, onKeyDown: handlePanelKeyDown, children: [_jsx("div", { className: "eidotter-nav__panel-header", children: _jsx("button", { ref: closeButtonRef, onClick: close, className: "eidotter-nav__close", "aria-label": "Close menu", children: _jsx(Icon, { name: "Close", size: "S" }) }) }), _jsx("ul", { className: "eidotter-nav__list", children: items.map((item) => {
|
|
73
|
+
// Same guard as desktop — mobile panel is a second render path.
|
|
74
|
+
const safeHref = isSafeHref(item.href) ? item.href : undefined;
|
|
75
|
+
const linkClassName = cn('eidotter-nav__link', activeHref === item.href && 'eidotter-nav__link--active');
|
|
76
|
+
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
|
+
}) })] })] }));
|
|
66
78
|
};
|
|
67
79
|
export const Nav = (props) => {
|
|
68
80
|
return (_jsxs(_Fragment, { children: [_jsx(MobileNav, { ...props }), _jsx(DesktopNav, { ...props })] }));
|
|
@@ -24,6 +24,11 @@ export interface ProgressRingProps {
|
|
|
24
24
|
className?: string;
|
|
25
25
|
/** Accessible label for screen readers. */
|
|
26
26
|
'aria-label'?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Render as a purely visual ring: `aria-hidden`, no `progressbar` role and no
|
|
29
|
+
* aria value attributes. Use when a parent owns the semantics (e.g. Gauge).
|
|
30
|
+
*/
|
|
31
|
+
decorative?: boolean;
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* Blocky DOS-style progress ring: the arc is split into `segments` chunky
|
|
@@ -9,7 +9,7 @@ const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
|
9
9
|
* presentational and swappable. Ported from the Tracker habit app (DMNC-1040),
|
|
10
10
|
* where it powers the hold-to-complete feedback ring.
|
|
11
11
|
*/
|
|
12
|
-
export const ProgressRing = forwardRef(({ value = 0, max = 100, size = 64, thickness = 8, segments = 16, color, showLabel = false, glow = false, className, 'aria-label': ariaLabel, ...props }, ref) => {
|
|
12
|
+
export const ProgressRing = forwardRef(({ value = 0, max = 100, size = 64, thickness = 8, segments = 16, color, showLabel = false, glow = false, decorative = false, className, 'aria-label': ariaLabel, ...props }, ref) => {
|
|
13
13
|
const segmentCount = Math.max(1, Math.floor(segments));
|
|
14
14
|
const fraction = clamp(max === 0 ? 0 : value / max, 0, 1);
|
|
15
15
|
const filled = Math.floor(fraction * segmentCount);
|
|
@@ -29,6 +29,14 @@ export const ProgressRing = forwardRef(({ value = 0, max = 100, size = 64, thick
|
|
|
29
29
|
// Draw exactly one segment arc: skip i*per, draw `dash`, then gap.
|
|
30
30
|
strokeDasharray: `0 ${i * per} ${dash} ${circumference}`, className: cn('eidotter-progress-ring__seg', isFilled && 'eidotter-progress-ring__seg--filled') }, i));
|
|
31
31
|
});
|
|
32
|
-
return (_jsxs("div", { ref: ref, className: cn('eidotter-progress-ring', glow && 'eidotter-progress-ring--glow', className), style: { width: size, height: size },
|
|
32
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-progress-ring', glow && 'eidotter-progress-ring--glow', className), style: { width: size, height: size }, ...(decorative
|
|
33
|
+
? { 'aria-hidden': true }
|
|
34
|
+
: {
|
|
35
|
+
role: 'progressbar',
|
|
36
|
+
'aria-valuenow': quantizedPct,
|
|
37
|
+
'aria-valuemin': 0,
|
|
38
|
+
'aria-valuemax': 100,
|
|
39
|
+
'aria-label': ariaLabel ?? `Progress: ${quantizedPct}%`,
|
|
40
|
+
}), ...props, children: [_jsx("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", focusable: "false", children: _jsx("g", { transform: `rotate(-90 ${cx} ${cy})`, children: arcs }) }), showLabel && (_jsxs("span", { className: "eidotter-progress-ring__label font-dos", children: [quantizedPct, "%"] }))] }));
|
|
33
41
|
});
|
|
34
42
|
ProgressRing.displayName = 'ProgressRing';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { HealthLabels, Tone } from '../../../utils/health/index.js';
|
|
3
|
+
export interface RangeBarSegment {
|
|
4
|
+
/** Stable id, used by `target.segmentId`. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Segment name, shown in the legend and the accessible summary. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Share of the whole, 0-100. Non-finite counts as 0. */
|
|
9
|
+
share: number;
|
|
10
|
+
tone: Tone;
|
|
11
|
+
/** Fill pattern. Defaults to the pattern paired with `tone`. */
|
|
12
|
+
pattern?: 'solid' | 'dither' | 'hatch' | 'dotted' | 'none';
|
|
13
|
+
/** Bound text shown in the legend, e.g. "< 1". */
|
|
14
|
+
bound?: string;
|
|
15
|
+
/** Duration text shown in the legend, e.g. "14 h". */
|
|
16
|
+
duration?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RangeBarProps {
|
|
19
|
+
/** Ordered segments. Shares are normalised to 100. */
|
|
20
|
+
segments: RangeBarSegment[];
|
|
21
|
+
/** Stack direction (default vertical). */
|
|
22
|
+
orientation?: 'vertical' | 'horizontal';
|
|
23
|
+
/** Size variant. */
|
|
24
|
+
size?: 'sm' | 'md' | 'lg';
|
|
25
|
+
/** Render the legend list (default true). */
|
|
26
|
+
showLegend?: boolean;
|
|
27
|
+
/** Target tick on one segment. `minShare` is the share the segment should reach; the tick sits at that point along the segment, clamped to its end. */
|
|
28
|
+
target?: {
|
|
29
|
+
segmentId: string;
|
|
30
|
+
minShare: number;
|
|
31
|
+
label: string;
|
|
32
|
+
};
|
|
33
|
+
/** Floor for a segment's flex weight so tiny segments stay visible (default 0.6). */
|
|
34
|
+
minSegmentShare?: number;
|
|
35
|
+
/** Inline percent shows only at or above this share (default 8). */
|
|
36
|
+
labelThreshold?: number;
|
|
37
|
+
/** BCP 47 locale for number formatting (default `en`). */
|
|
38
|
+
locale?: string;
|
|
39
|
+
/** Overrides for the built-in label text. */
|
|
40
|
+
labels?: Partial<HealthLabels>;
|
|
41
|
+
/** Additional CSS class name. */
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Stacked range bar: ordered segments sized by share, an optional target tick
|
|
46
|
+
* and a legend. Percent text sits in a lane beside the bar, never on a
|
|
47
|
+
* patterned fill (R13). The bar is a single `img` with a composed summary.
|
|
48
|
+
* Presentational and hook-free, so it stays server-safe.
|
|
49
|
+
*/
|
|
50
|
+
export declare const RangeBar: React.ForwardRefExoticComponent<RangeBarProps & React.RefAttributes<HTMLDivElement>>;
|