eidotter 0.39.0 → 0.40.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 +19 -2
- package/dist/components/Chat/components/ChatInput.js +1 -1
- package/dist/components/Chat/components/ChatMessage.d.ts +1 -1
- package/dist/components/Chat/components/ChatMessage.js +2 -2
- package/dist/components/CommandPrompt/components/CommandPrompt.js +1 -1
- 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/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/InlineExpand/components/InlineExpand.d.ts +5 -3
- package/dist/components/InlineExpand/components/InlineExpand.js +15 -13
- 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/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/Terminal/components/Terminal.js +1 -1
- 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 +183 -0
- package/dist/eidotter.css +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.es.js +3068 -1804
- package/dist/index.umd.js +1 -1
- 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/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 +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. 47 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
|
|
|
@@ -111,7 +111,7 @@ for client apps and `'use client'` files.
|
|
|
111
111
|
|
|
112
112
|
## Components
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
47 components across six families — [complete catalog in Storybook](https://cinimody.github.io/eiDotter/).
|
|
115
115
|
|
|
116
116
|
<details>
|
|
117
117
|
<summary><strong>Windowing & shell</strong> — Terminal, Modal, Lightbox, CommandPrompt, CmdPalette, Header, Footer, Nav, Brand</summary>
|
|
@@ -130,6 +130,23 @@ for client apps and `'use client'` files.
|
|
|
130
130
|
|
|
131
131
|
</details>
|
|
132
132
|
|
|
133
|
+
<details>
|
|
134
|
+
<summary><strong>Health & metabolism</strong> — Gauge, Meter, MeterGroup, RangeBar, SeriesBars, DecayStrip, VitalsStrip</summary>
|
|
135
|
+
|
|
136
|
+
| Component | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `Gauge` | Quantised ring with a dominant percent, secondary value, tone, status, provenance tag and overflow marker |
|
|
139
|
+
| `Meter` | Single fill bar (vertical / horizontal) with an optimal tick or range and pattern fills |
|
|
140
|
+
| `MeterGroup` | Row or grid of Meters with an optional shared scale |
|
|
141
|
+
| `RangeBar` | Stacked range bar of ordered segments with legend and target tick (time-in-range style) |
|
|
142
|
+
| `SeriesBars` | Short bar series with a goal line and consumer-supplied threshold tones |
|
|
143
|
+
| `DecayStrip` | On-board decay over time from consumer-supplied samples — now marker, dimmed future, provenance required |
|
|
144
|
+
| `VitalsStrip` | Monitor-style vitals tiles with per-tile tone, status and paired values |
|
|
145
|
+
|
|
146
|
+
All six are presentational and server-safe; bands, targets and clinical copy are consumer props (no medical defaults), and tone is never the only signal.
|
|
147
|
+
|
|
148
|
+
</details>
|
|
149
|
+
|
|
133
150
|
<details>
|
|
134
151
|
<summary><strong>Content surfaces</strong> — Card, Accordion, Alert, Badge, Tag, Notification, Stat, Progress, Skeleton, Separator, Breadcrumb, LegalPage</summary>
|
|
135
152
|
|
|
@@ -33,5 +33,5 @@ export const ChatInput = ({ onSend, prompt = '>', placeholder, disabled = false,
|
|
|
33
33
|
textareaRef.current.focus();
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
-
return (_jsxs("div", { className: cn('flex items-start font-dos text-dos-text-md p-2 cursor-text', 'text-dos-text-brand', 'eidotter-chat-input', disabled && 'eidotter-chat-input--disabled', className), onClick: handleContainerClick, ...props, children: [_jsx("span", { className: "text-dos-text-brand mr-2 mt-0.5 whitespace-nowrap select-none", "aria-hidden": "true", children: prompt }), _jsxs("div", { className: "flex items-start flex-1 min-w-0", children: [_jsx("textarea", { ref: textareaRef, className: "eidotter-chat-input__textarea", value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, rows: 1, "aria-label": "Chat input" }), _jsx("span", { className: "eidotter-chat-input__cursor", "aria-hidden": "true", children: "
|
|
36
|
+
return (_jsxs("div", { className: cn('flex items-start font-dos text-dos-text-md p-2 cursor-text', 'text-dos-text-brand', 'eidotter-chat-input', disabled && 'eidotter-chat-input--disabled', className), onClick: handleContainerClick, ...props, children: [_jsx("span", { className: "text-dos-text-brand mr-2 mt-0.5 whitespace-nowrap select-none", "aria-hidden": "true", children: prompt }), _jsxs("div", { className: "flex items-start flex-1 min-w-0", children: [_jsx("textarea", { ref: textareaRef, className: "eidotter-chat-input__textarea", value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, rows: 1, "aria-label": "Chat input" }), _jsx("span", { className: "eidotter-chat-input__cursor", "aria-hidden": "true", children: "_" })] })] }));
|
|
37
37
|
};
|
|
@@ -18,6 +18,6 @@ export interface ChatMessageProps {
|
|
|
18
18
|
*
|
|
19
19
|
* User messages display with a `>` prefix, assistant messages with `C:\>`,
|
|
20
20
|
* and system messages appear dimmed. When `isStreaming` is true, a blinking
|
|
21
|
-
*
|
|
21
|
+
* underscore cursor appears after the content.
|
|
22
22
|
*/
|
|
23
23
|
export declare const ChatMessage: React.FC<ChatMessageProps & React.HTMLAttributes<HTMLDivElement>>;
|
|
@@ -10,9 +10,9 @@ const roleClasses = {
|
|
|
10
10
|
*
|
|
11
11
|
* User messages display with a `>` prefix, assistant messages with `C:\>`,
|
|
12
12
|
* and system messages appear dimmed. When `isStreaming` is true, a blinking
|
|
13
|
-
*
|
|
13
|
+
* underscore cursor appears after the content.
|
|
14
14
|
*/
|
|
15
15
|
export const ChatMessage = ({ role, content, isStreaming = false, userPrefix = '>', assistantPrefix = 'C:\\>', className, ...props }) => {
|
|
16
16
|
const prefix = role === 'user' ? userPrefix : role === 'assistant' ? assistantPrefix : '';
|
|
17
|
-
return (_jsxs("div", { className: cn('flex gap-2 font-dos text-dos-text-md leading-[1.4] py-1 whitespace-pre-wrap break-words', roleClasses[role], isStreaming && 'eidotter-chat-message--streaming', className), ...props, children: [prefix && (_jsx("span", { className: "shrink-0 select-none", "aria-hidden": "true", children: prefix })), _jsx("span", { className: "min-w-0", children: content }), isStreaming && (_jsx("span", { className: "eidotter-chat-message__cursor", "aria-hidden": "true", children: "
|
|
17
|
+
return (_jsxs("div", { className: cn('flex gap-2 font-dos text-dos-text-md leading-[1.4] py-1 whitespace-pre-wrap break-words', roleClasses[role], isStreaming && 'eidotter-chat-message--streaming', className), ...props, children: [prefix && (_jsx("span", { className: "shrink-0 select-none", "aria-hidden": "true", children: prefix })), _jsx("span", { className: "min-w-0", children: content }), isStreaming && (_jsx("span", { className: "eidotter-chat-message__cursor", "aria-hidden": "true", children: "_" }))] }));
|
|
18
18
|
};
|
|
@@ -30,5 +30,5 @@ export const CommandPrompt = ({ prompt = 'C:\\>', onCommand, autoFocus = false,
|
|
|
30
30
|
inputRef.current.focus();
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
-
return (_jsxs("div", { className: `command-prompt ${disabled ? 'command-prompt--disabled' : ''} ${className}`.trim(), onClick: handleContainerClick, children: [_jsx("span", { className: "command-prompt__prompt", "aria-hidden": "true", children: prompt }), _jsxs("div", { className: "command-prompt__input-wrapper", children: [_jsx("input", { ref: inputRef, className: "command-prompt__input", value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleKeyDown, autoFocus: autoFocus, spellCheck: false, autoComplete: "off", autoCapitalize: "off", autoCorrect: "off", placeholder: placeholder, disabled: disabled, "aria-label": "Command input", size: value.length || placeholder?.length || 1 }), _jsx("span", { className: "command-prompt__cursor", "aria-hidden": "true", children: "
|
|
33
|
+
return (_jsxs("div", { className: `command-prompt ${disabled ? 'command-prompt--disabled' : ''} ${className}`.trim(), onClick: handleContainerClick, children: [_jsx("span", { className: "command-prompt__prompt", "aria-hidden": "true", children: prompt }), _jsxs("div", { className: "command-prompt__input-wrapper", children: [_jsx("input", { ref: inputRef, className: "command-prompt__input", value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleKeyDown, autoFocus: autoFocus, spellCheck: false, autoComplete: "off", autoCapitalize: "off", autoCorrect: "off", placeholder: placeholder, disabled: disabled, "aria-label": "Command input", size: value.length || placeholder?.length || 1 }), _jsx("span", { className: "command-prompt__cursor", "aria-hidden": "true", children: "_" })] })] }));
|
|
34
34
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { HealthLabels, Provenance, Tone } from '../../../utils/health/index.js';
|
|
3
|
+
import type { DecaySample, TimeInput } from './resample.js';
|
|
4
|
+
export interface DecayStripItem {
|
|
5
|
+
/** Stable key for the row. */
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Starting amount, e.g. 40. `null` renders as unknown. */
|
|
9
|
+
amount: number | null;
|
|
10
|
+
unit: string;
|
|
11
|
+
/** Share still active, 0..1. `null`, `NaN` and `Infinity` render as unknown. */
|
|
12
|
+
remainingShare: number | null;
|
|
13
|
+
/** Required: where the share comes from. */
|
|
14
|
+
provenance: Provenance;
|
|
15
|
+
}
|
|
16
|
+
export interface DecayStripProps {
|
|
17
|
+
/** What is still active. An empty list renders EmptyState; the strip still renders. */
|
|
18
|
+
items: DecayStripItem[];
|
|
19
|
+
/**
|
|
20
|
+
* Consumer-computed decay curve. `t` is epoch ms or an ISO string;
|
|
21
|
+
* `remaining` is a 0..1 share. eidotter does no absorption math (KTD5).
|
|
22
|
+
*/
|
|
23
|
+
samples: DecaySample[];
|
|
24
|
+
/** Current time, epoch ms or ISO string. Pass it in; the strip never reads the clock. */
|
|
25
|
+
now: TimeInput;
|
|
26
|
+
/** Time span the strip covers. */
|
|
27
|
+
window: {
|
|
28
|
+
from: TimeInput;
|
|
29
|
+
to: TimeInput;
|
|
30
|
+
};
|
|
31
|
+
/** Required: where the samples come from. */
|
|
32
|
+
provenance: Provenance;
|
|
33
|
+
/** Size variant. `sm` renders items as a text list, `md` and `lg` as small Gauges. */
|
|
34
|
+
size?: 'sm' | 'md' | 'lg';
|
|
35
|
+
/** Tone of the strip and the item Gauges. */
|
|
36
|
+
tone?: Tone;
|
|
37
|
+
/** Number of strip cells (default 24). */
|
|
38
|
+
columns?: number;
|
|
39
|
+
/** BCP 47 locale for numbers and times (default `en`). */
|
|
40
|
+
locale?: string;
|
|
41
|
+
/**
|
|
42
|
+
* IANA time zone for the from, to and now labels (e.g. `Europe/Berlin`).
|
|
43
|
+
* Defaults to the runtime zone, which on a server render is the server's.
|
|
44
|
+
*/
|
|
45
|
+
timeZone?: string;
|
|
46
|
+
/** Overrides for the built-in label text. */
|
|
47
|
+
labels?: Partial<HealthLabels>;
|
|
48
|
+
/** Opt in to a polite live region and the now-marker pulse (R14). */
|
|
49
|
+
announce?: boolean;
|
|
50
|
+
/** Additional CSS class name. */
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Horizontal time strip of what is still active (a meal, a dose): past cells
|
|
55
|
+
* as stepped block glyphs scaled by the consumer's `remaining` samples, future
|
|
56
|
+
* cells dimmed, and a now marker. Item rows sit above the strip.
|
|
57
|
+
* The strip is `role="img"` with a composed summary, so the glyphs are never
|
|
58
|
+
* the only reading. Presentational and hook-free, so it stays server-safe.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DecayStrip: React.ForwardRefExoticComponent<DecayStripProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { Gauge } from '../../Gauge/index.js';
|
|
5
|
+
import { EmptyState } from '../../EmptyState/index.js';
|
|
6
|
+
import { formatValue, isKnown, PLACEHOLDER, provenanceLabel, resolveLabels, } from '../../../utils/health/index.js';
|
|
7
|
+
import { normaliseSamples, resample, toMs } from './resample.js';
|
|
8
|
+
const sizeClasses = {
|
|
9
|
+
sm: 'eidotter-decay-strip--sm text-dos-text-xs',
|
|
10
|
+
md: 'eidotter-decay-strip--md text-dos-text-sm',
|
|
11
|
+
lg: 'eidotter-decay-strip--lg text-dos-text-md',
|
|
12
|
+
};
|
|
13
|
+
const toneClasses = {
|
|
14
|
+
neutral: 'eidotter-tone--neutral',
|
|
15
|
+
info: 'eidotter-tone--info',
|
|
16
|
+
success: 'eidotter-tone--success',
|
|
17
|
+
warning: 'eidotter-tone--warning',
|
|
18
|
+
critical: 'eidotter-tone--critical',
|
|
19
|
+
};
|
|
20
|
+
/** Progress glyph ladder, empty to full. */
|
|
21
|
+
const LADDER = ['░', '▒', '▓', '█'];
|
|
22
|
+
const MARKER = '│';
|
|
23
|
+
const NO_DATA = '·';
|
|
24
|
+
function glyph(value) {
|
|
25
|
+
if (value === null)
|
|
26
|
+
return NO_DATA;
|
|
27
|
+
const v = Math.min(1, Math.max(0, value));
|
|
28
|
+
return LADDER[Math.round(v * (LADDER.length - 1))];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Time from `now` until the curve reaches zero for good (the first zero
|
|
32
|
+
* sample after the last positive one), as locale text. Null when the curve
|
|
33
|
+
* never reaches zero or is already at zero before `now`.
|
|
34
|
+
*/
|
|
35
|
+
function timeToZero(samples, now, locale) {
|
|
36
|
+
const points = normaliseSamples(samples);
|
|
37
|
+
let lastPositive = -1;
|
|
38
|
+
points.forEach((p, i) => {
|
|
39
|
+
if (p.v > 0)
|
|
40
|
+
lastPositive = i;
|
|
41
|
+
});
|
|
42
|
+
const zero = points.slice(lastPositive + 1).find((p) => p.v <= 0);
|
|
43
|
+
if (!zero || !Number.isFinite(now) || zero.t < now)
|
|
44
|
+
return null;
|
|
45
|
+
const minutes = (zero.t - now) / 60000;
|
|
46
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'always' });
|
|
47
|
+
return minutes < 60
|
|
48
|
+
? rtf.format(Math.round(minutes), 'minute')
|
|
49
|
+
: rtf.format(Math.round(minutes / 6) / 10, 'hour');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Horizontal time strip of what is still active (a meal, a dose): past cells
|
|
53
|
+
* as stepped block glyphs scaled by the consumer's `remaining` samples, future
|
|
54
|
+
* cells dimmed, and a now marker. Item rows sit above the strip.
|
|
55
|
+
* The strip is `role="img"` with a composed summary, so the glyphs are never
|
|
56
|
+
* the only reading. Presentational and hook-free, so it stays server-safe.
|
|
57
|
+
*/
|
|
58
|
+
export const DecayStrip = forwardRef(({ items, samples, now, window, provenance, size = 'md', tone = 'neutral', columns = 24, locale = 'en', timeZone, labels: labelOverrides, announce = false, className, ...props }, ref) => {
|
|
59
|
+
const labels = resolveLabels(labelOverrides);
|
|
60
|
+
const nowMs = toMs(now);
|
|
61
|
+
const cells = resample(samples, window, columns);
|
|
62
|
+
const time = new Intl.DateTimeFormat(locale, {
|
|
63
|
+
timeStyle: 'short',
|
|
64
|
+
...(timeZone && { timeZone }),
|
|
65
|
+
});
|
|
66
|
+
const formatTime = (ms) => (Number.isFinite(ms) ? time.format(ms) : PLACEHOLDER);
|
|
67
|
+
const fromText = formatTime(toMs(window.from));
|
|
68
|
+
const toText = formatTime(toMs(window.to));
|
|
69
|
+
const tag = (p) => provenanceLabel(labels, p);
|
|
70
|
+
const zeroPercent = formatValue(0, { locale, percent: true }).text;
|
|
71
|
+
const rows = items.map((item) => {
|
|
72
|
+
const amountText = formatValue(item.amount, { locale, unit: item.unit }).text;
|
|
73
|
+
const share = isKnown(item.remainingShare) ? item.remainingShare : null;
|
|
74
|
+
const shareText = share === null
|
|
75
|
+
? labels.unknown
|
|
76
|
+
: `${formatValue(share * 100, { locale, percent: true }).text} ${labels.remaining}`;
|
|
77
|
+
return { item, amountText, share, shareText, tagText: tag(item.provenance) };
|
|
78
|
+
});
|
|
79
|
+
const zeroText = timeToZero(samples, nowMs, locale);
|
|
80
|
+
const stripTag = tag(provenance);
|
|
81
|
+
const summary = [
|
|
82
|
+
...rows.map((r) => `${r.item.label} ${r.amountText}: ${r.shareText}${r.tagText ? `, ${r.tagText}` : ''}`),
|
|
83
|
+
`${fromText}–${toText}`,
|
|
84
|
+
Number.isFinite(nowMs) && `${labels.now} ${time.format(nowMs)}`,
|
|
85
|
+
zeroText && `${zeroPercent} ${zeroText}`,
|
|
86
|
+
stripTag,
|
|
87
|
+
]
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.join('; ');
|
|
90
|
+
const compact = size === 'sm';
|
|
91
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-decay-strip eidotter-health flex flex-col gap-2 w-full min-w-0 font-dos text-dos-text-brand', sizeClasses[size] ?? sizeClasses.md, toneClasses[tone], className), ...props, children: [items.length === 0 ? (_jsx(EmptyState, { title: `${zeroPercent} ${labels.remaining}`, className: "py-2" })) : (_jsx("ul", { className: cn('flex gap-2 min-w-0', compact ? 'flex-col' : 'flex-wrap'), children: rows.map((r) => (_jsx("li", { className: cn('eidotter-decay-strip__item min-w-0', compact ? 'flex flex-wrap items-baseline gap-2 tabular-nums' : 'flex-1'), children: compact ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "truncate text-dos-text-accent", title: r.item.label, children: r.item.label }), _jsx("span", { children: r.amountText }), _jsx("span", { children: r.shareText }), r.tagText && (_jsx("span", { className: "eidotter-decay-strip__item-provenance border border-dos-border-brand px-1", children: r.tagText }))] })) : (_jsx(Gauge, { label: r.item.label, value: r.share === null ? null : r.share * 100, secondary: r.item.amount === null ? undefined : r.amountText, size: "sm", tone: tone, provenance: r.item.provenance, locale: locale, labels: labelOverrides })) }, r.item.id))) })), _jsxs("div", { className: "eidotter-decay-strip__strip flex items-center gap-2 min-w-0 tabular-nums", role: "img", "aria-label": summary, children: [_jsx("span", { className: "eidotter-decay-strip__from shrink-0", children: fromText }), _jsx("span", { className: "eidotter-decay-strip__cells flex-1 min-w-0 overflow-hidden whitespace-nowrap leading-none", style: tone === 'neutral' ? undefined : { color: 'var(--eidotter-fill)' }, children: cells.map((cell, i) => {
|
|
92
|
+
const last = i === cells.length - 1;
|
|
93
|
+
const isNow = nowMs >= cell.start && (nowMs < cell.end || (last && nowMs === cell.end));
|
|
94
|
+
const future = !isNow && nowMs < cell.start && nowMs >= cells[0].start;
|
|
95
|
+
const state = isNow ? 'now' : future ? 'future' : 'past';
|
|
96
|
+
return (_jsx("span", { className: cn('eidotter-decay-strip__cell', `eidotter-decay-strip__cell--${state}`, future && 'opacity-50', isNow && 'text-dos-text-accent', isNow && announce && 'eidotter-health-pulse'), children: isNow ? MARKER : glyph(cell.value) }, i));
|
|
97
|
+
}) }), _jsx("span", { className: "eidotter-decay-strip__to shrink-0", children: toText })] }), stripTag && (_jsx("span", { className: "eidotter-decay-strip__provenance self-start border border-dos-border-brand px-1", children: stripTag })), announce && (_jsx("p", { className: "sr-only", "aria-live": "polite", children: summary }))] }));
|
|
98
|
+
});
|
|
99
|
+
DecayStrip.displayName = 'DecayStrip';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DecayStrip } from './DecayStrip.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resampling for DecayStrip. No absorption or insulin math lives here
|
|
3
|
+
* (KTD5): the consumer supplies `remaining` samples and this only bins them.
|
|
4
|
+
*/
|
|
5
|
+
/** Epoch milliseconds, or an ISO 8601 string. */
|
|
6
|
+
export type TimeInput = number | string;
|
|
7
|
+
export interface DecaySample {
|
|
8
|
+
t: TimeInput;
|
|
9
|
+
/** Share still active, 0..1 (0 = gone, 1 = all of it). */
|
|
10
|
+
remaining: number;
|
|
11
|
+
}
|
|
12
|
+
export interface DecayCell {
|
|
13
|
+
/** Cell start, epoch ms, inclusive. */
|
|
14
|
+
start: number;
|
|
15
|
+
/** Cell end, epoch ms, exclusive (the last cell includes the window end). */
|
|
16
|
+
end: number;
|
|
17
|
+
/** Mean `remaining` of the samples in the span, the nearest earlier sample when empty, or null. */
|
|
18
|
+
value: number | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function toMs(t: TimeInput): number;
|
|
21
|
+
/** Finite, time-sorted `{t, v}` points (stable for equal times); non-finite samples are dropped. */
|
|
22
|
+
export declare function normaliseSamples(samples: DecaySample[]): {
|
|
23
|
+
t: number;
|
|
24
|
+
v: number;
|
|
25
|
+
}[];
|
|
26
|
+
/**
|
|
27
|
+
* Split `window` into `columns` equal cells. A cell's value is the mean of the
|
|
28
|
+
* samples whose time falls in its span; an empty cell carries the value of
|
|
29
|
+
* the nearest earlier sample, or null when no earlier sample exists.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resample(samples: DecaySample[], window: {
|
|
32
|
+
from: TimeInput;
|
|
33
|
+
to: TimeInput;
|
|
34
|
+
}, columns: number): DecayCell[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resampling for DecayStrip. No absorption or insulin math lives here
|
|
3
|
+
* (KTD5): the consumer supplies `remaining` samples and this only bins them.
|
|
4
|
+
*/
|
|
5
|
+
export function toMs(t) {
|
|
6
|
+
return typeof t === 'number' ? t : Date.parse(t);
|
|
7
|
+
}
|
|
8
|
+
/** Finite, time-sorted `{t, v}` points (stable for equal times); non-finite samples are dropped. */
|
|
9
|
+
export function normaliseSamples(samples) {
|
|
10
|
+
return samples
|
|
11
|
+
.map((s) => ({ t: toMs(s.t), v: s.remaining }))
|
|
12
|
+
.filter((p) => Number.isFinite(p.t) && Number.isFinite(p.v))
|
|
13
|
+
.sort((a, b) => a.t - b.t);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Split `window` into `columns` equal cells. A cell's value is the mean of the
|
|
17
|
+
* samples whose time falls in its span; an empty cell carries the value of
|
|
18
|
+
* the nearest earlier sample, or null when no earlier sample exists.
|
|
19
|
+
*/
|
|
20
|
+
export function resample(samples, window, columns) {
|
|
21
|
+
const from = toMs(window.from);
|
|
22
|
+
const to = toMs(window.to);
|
|
23
|
+
const span = (to - from) / columns;
|
|
24
|
+
const points = normaliseSamples(samples);
|
|
25
|
+
const cells = [];
|
|
26
|
+
let next = 0;
|
|
27
|
+
let carry = null;
|
|
28
|
+
for (let i = 0; i < columns; i++) {
|
|
29
|
+
const start = from + i * span;
|
|
30
|
+
const end = i === columns - 1 ? to : from + (i + 1) * span;
|
|
31
|
+
const last = i === columns - 1;
|
|
32
|
+
while (next < points.length && points[next].t < start)
|
|
33
|
+
carry = points[next++].v;
|
|
34
|
+
let sum = 0;
|
|
35
|
+
let count = 0;
|
|
36
|
+
while (next < points.length && (points[next].t < end || (last && points[next].t === end))) {
|
|
37
|
+
sum += points[next].v;
|
|
38
|
+
carry = points[next++].v;
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
cells.push({ start, end, value: count > 0 ? sum / count : carry });
|
|
42
|
+
}
|
|
43
|
+
return cells;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DecayStrip } from './components/index.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Band, HealthLabels, Provenance, Tone, ValueStatus } from '../../../utils/health/index.js';
|
|
3
|
+
export interface GaugeProps {
|
|
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 of `value`, used only to flag mismatched band units in dev. */
|
|
13
|
+
unit?: string;
|
|
14
|
+
/** Absolute value text shown beneath the percent, e.g. "890 kcal". */
|
|
15
|
+
secondary?: string;
|
|
16
|
+
/** Size variant. */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
/** Explicit tone. Wins over `bands`. */
|
|
19
|
+
tone?: Tone;
|
|
20
|
+
/** Data status. */
|
|
21
|
+
status?: ValueStatus;
|
|
22
|
+
/** Tag shown for `estimated` and `modelled` values. */
|
|
23
|
+
provenance?: Provenance;
|
|
24
|
+
/** Consumer-supplied bands. Resolved on the raw value. */
|
|
25
|
+
bands?: Band[];
|
|
26
|
+
/** BCP 47 locale for number formatting (default `en`). */
|
|
27
|
+
locale?: string;
|
|
28
|
+
/** Overrides for the built-in label text. */
|
|
29
|
+
labels?: Partial<HealthLabels>;
|
|
30
|
+
/** Enable the phosphor glow on the ring. */
|
|
31
|
+
glow?: boolean;
|
|
32
|
+
/** Additional CSS class name. */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Replaces `label` in the composed accessible name. */
|
|
35
|
+
'aria-label'?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ring gauge: a quantised ProgressRing arc with a dominant percent, optional
|
|
39
|
+
* secondary value, tone, status, provenance tag and overflow marker.
|
|
40
|
+
* The root owns the `meter` semantics; the ring is decorative (KTD7).
|
|
41
|
+
* Presentational and hook-free, so it stays server-safe.
|
|
42
|
+
*/
|
|
43
|
+
export declare const Gauge: React.ForwardRefExoticComponent<GaugeProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { ProgressRing } from '../../ProgressRing/index.js';
|
|
5
|
+
import { Skeleton } from '../../Skeleton/index.js';
|
|
6
|
+
import { formatValue, isKnown, provenanceLabel, resolveBand, resolveLabels, PLACEHOLDER, } from '../../../utils/health/index.js';
|
|
7
|
+
const sizeClasses = {
|
|
8
|
+
sm: 'eidotter-gauge--sm text-dos-text-xs',
|
|
9
|
+
md: 'eidotter-gauge--md text-dos-text-sm',
|
|
10
|
+
lg: 'eidotter-gauge--lg text-dos-text-md',
|
|
11
|
+
};
|
|
12
|
+
const valueSizeClasses = {
|
|
13
|
+
sm: 'text-dos-text-sm',
|
|
14
|
+
md: 'text-dos-display-xs',
|
|
15
|
+
lg: 'text-dos-display-sm',
|
|
16
|
+
};
|
|
17
|
+
const ringSizes = { sm: 72, md: 104, lg: 136 };
|
|
18
|
+
const toneClasses = {
|
|
19
|
+
neutral: 'eidotter-tone--neutral',
|
|
20
|
+
info: 'eidotter-tone--info',
|
|
21
|
+
success: 'eidotter-tone--success',
|
|
22
|
+
warning: 'eidotter-tone--warning',
|
|
23
|
+
critical: 'eidotter-tone--critical',
|
|
24
|
+
};
|
|
25
|
+
/** Segment rhythm per filled tone: SVG strokes cannot take gradient fills (KTD3). */
|
|
26
|
+
const toneSegments = {
|
|
27
|
+
neutral: 16,
|
|
28
|
+
info: 12,
|
|
29
|
+
success: 16,
|
|
30
|
+
warning: 20,
|
|
31
|
+
critical: 24,
|
|
32
|
+
};
|
|
33
|
+
const statusClasses = {
|
|
34
|
+
ok: '',
|
|
35
|
+
loading: 'eidotter-gauge--loading',
|
|
36
|
+
empty: 'eidotter-gauge--empty',
|
|
37
|
+
stale: 'eidotter-gauge--stale',
|
|
38
|
+
unknown: 'eidotter-gauge--unknown',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Ring gauge: a quantised ProgressRing arc with a dominant percent, optional
|
|
42
|
+
* secondary value, tone, status, provenance tag and overflow marker.
|
|
43
|
+
* The root owns the `meter` semantics; the ring is decorative (KTD7).
|
|
44
|
+
* Presentational and hook-free, so it stays server-safe.
|
|
45
|
+
*/
|
|
46
|
+
export const Gauge = forwardRef(({ label, shortLabel, value, max = 100, unit, secondary, size = 'md', tone, status = 'ok', provenance, bands, locale = 'en', labels: labelOverrides, glow = false, className, 'aria-label': ariaLabel, ...props }, ref) => {
|
|
47
|
+
const labels = resolveLabels(labelOverrides);
|
|
48
|
+
const loading = status === 'loading';
|
|
49
|
+
const unknown = !isKnown(value) ||
|
|
50
|
+
!(max > 0) ||
|
|
51
|
+
!Number.isFinite(max) ||
|
|
52
|
+
status === 'empty' ||
|
|
53
|
+
status === 'unknown';
|
|
54
|
+
const pct = unknown ? 0 : (value / max) * 100;
|
|
55
|
+
const overflow = !unknown && value > max;
|
|
56
|
+
const resolved = resolveBand(unknown ? null : value, bands, unit);
|
|
57
|
+
const effectiveTone = tone ?? resolved.tone;
|
|
58
|
+
const valueText = unknown
|
|
59
|
+
? PLACEHOLDER
|
|
60
|
+
: formatValue(pct, { locale, percent: true }).text;
|
|
61
|
+
const stale = status === 'stale' && !unknown;
|
|
62
|
+
const valuetext = [unknown || loading ? labels.unknown : valueText, stale && labels.stale]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(', ');
|
|
65
|
+
const name = `${ariaLabel ?? label}: ${valuetext}`;
|
|
66
|
+
const diameter = ringSizes[size] ?? ringSizes.md;
|
|
67
|
+
const tagText = provenanceLabel(labels, provenance);
|
|
68
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-gauge eidotter-health flex flex-col items-center gap-1 w-full font-dos', sizeClasses[size] ?? sizeClasses.md, toneClasses[effectiveTone], statusClasses[status], className), style: { minWidth: diameter }, role: "meter", "aria-valuemin": 0, "aria-valuemax": unknown ? 100 : max, "aria-valuenow": unknown || loading ? 0 : Math.max(0, Math.min(value, max)), "aria-valuetext": valuetext, "aria-label": name, "aria-busy": loading || undefined, ...props, children: [_jsxs("div", { className: "relative inline-flex items-center justify-center", children: [_jsx(ProgressRing, { decorative: true, value: loading ? 0 : pct, max: 100, size: diameter, thickness: Math.round(diameter / 8), segments: toneSegments[effectiveTone] ?? 16, color: effectiveTone === 'neutral' ? undefined : 'var(--eidotter-fill)', glow: false, className: cn(glow && 'eidotter-health-glow') }), _jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center text-center leading-none", children: loading ? (_jsx("div", { "aria-hidden": "true", className: "eidotter-gauge__skeleton w-full overflow-hidden", children: _jsx(Skeleton, { variant: "figure", lines: 2, animated: false }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('eidotter-gauge__value text-dos-text-accent tabular-nums', valueSizeClasses[size] ?? valueSizeClasses.md, stale && 'opacity-60'), children: valueText }), secondary && !unknown && (_jsx("span", { className: "eidotter-gauge__secondary text-dos-text-brand text-dos-text-xs tabular-nums", children: secondary }))] })) })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-1 text-dos-text-brand uppercase tracking-wider leading-tight max-w-full", children: [effectiveTone === 'critical' && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__glyph text-dos-text-error", children: "\u25B2" })), _jsxs("span", { className: "eidotter-gauge__label truncate", title: label, children: [_jsx("span", { className: "eidotter-gauge__label--full", children: label }), shortLabel && _jsx("span", { className: "eidotter-gauge__label--short", children: shortLabel })] }), overflow && (_jsx("span", { "aria-hidden": "true", className: "eidotter-gauge__overflow text-dos-text-warning", children: "+" })), tagText !== null && (_jsx("span", { className: "eidotter-gauge__provenance border border-dos-border-brand px-1 normal-case", children: tagText })), stale && _jsx("span", { className: "eidotter-gauge__stale normal-case", children: labels.stale })] }), resolved.label && (_jsx("span", { className: "eidotter-gauge__band text-dos-text-brand normal-case", children: resolved.label }))] }));
|
|
69
|
+
});
|
|
70
|
+
Gauge.displayName = 'Gauge';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Gauge } from './Gauge.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Gauge } from './components/index.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
|
};
|
|
@@ -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>>;
|