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,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { formatValue, resolveLabels } from '../../../utils/health/index.js';
|
|
5
|
+
const orientationClasses = {
|
|
6
|
+
vertical: 'eidotter-range-bar--vertical',
|
|
7
|
+
horizontal: 'eidotter-range-bar--horizontal',
|
|
8
|
+
};
|
|
9
|
+
const sizeClasses = {
|
|
10
|
+
sm: 'eidotter-range-bar--sm text-dos-text-xs',
|
|
11
|
+
md: 'eidotter-range-bar--md text-dos-text-sm',
|
|
12
|
+
lg: 'eidotter-range-bar--lg text-dos-text-md',
|
|
13
|
+
};
|
|
14
|
+
/** Bar thickness and length per orientation and size. */
|
|
15
|
+
const trackClasses = {
|
|
16
|
+
'vertical-sm': 'w-[1rem] h-[8rem]',
|
|
17
|
+
'vertical-md': 'w-[1.5rem] h-[12rem]',
|
|
18
|
+
'vertical-lg': 'w-[2rem] h-[16rem]',
|
|
19
|
+
'horizontal-sm': 'h-[1rem] w-full',
|
|
20
|
+
'horizontal-md': 'h-[1.5rem] w-full',
|
|
21
|
+
'horizontal-lg': 'h-[2rem] w-full',
|
|
22
|
+
};
|
|
23
|
+
const toneClasses = {
|
|
24
|
+
neutral: 'eidotter-tone--neutral',
|
|
25
|
+
info: 'eidotter-tone--info',
|
|
26
|
+
success: 'eidotter-tone--success',
|
|
27
|
+
warning: 'eidotter-tone--warning',
|
|
28
|
+
critical: 'eidotter-tone--critical',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Neutral hue, set inline. Filled tones take `--eidotter-fill` from the
|
|
32
|
+
* `.eidotter-tone--*` class in health-patterns.css (KTD3), which sets none for
|
|
33
|
+
* neutral, so a neutral fill (e.g. with a pattern override) still paints.
|
|
34
|
+
*/
|
|
35
|
+
const NEUTRAL_FILL = 'var(--color-semantic-text-muted)';
|
|
36
|
+
/** Reserved id for the synthesized remainder, so it cannot collide with a consumer id. */
|
|
37
|
+
const OTHER_ID = '__eidotter-other';
|
|
38
|
+
// Neutral is intentionally unpatterned here (unlike Meter, which reuses solid).
|
|
39
|
+
const tonePatterns = {
|
|
40
|
+
neutral: 'none',
|
|
41
|
+
info: 'solid',
|
|
42
|
+
success: 'dither',
|
|
43
|
+
warning: 'hatch',
|
|
44
|
+
critical: 'dotted',
|
|
45
|
+
};
|
|
46
|
+
const patternClasses = {
|
|
47
|
+
none: 'eidotter-fill--none',
|
|
48
|
+
solid: 'eidotter-fill--solid',
|
|
49
|
+
dither: 'eidotter-fill--dither',
|
|
50
|
+
hatch: 'eidotter-fill--hatch',
|
|
51
|
+
dotted: 'eidotter-fill--dotted',
|
|
52
|
+
};
|
|
53
|
+
const resolveItem = (s, weight) => {
|
|
54
|
+
const toneKey = toneClasses[s.tone] ? s.tone : 'neutral';
|
|
55
|
+
const pattern = s.pattern ?? tonePatterns[toneKey];
|
|
56
|
+
return { ...s, weight, toneKey, patternClass: patternClasses[pattern] ?? patternClasses.solid };
|
|
57
|
+
};
|
|
58
|
+
const clean = (n) => Math.round(n * 1e6) / 1e6;
|
|
59
|
+
/**
|
|
60
|
+
* Stacked range bar: ordered segments sized by share, an optional target tick
|
|
61
|
+
* and a legend. Percent text sits in a lane beside the bar, never on a
|
|
62
|
+
* patterned fill (R13). The bar is a single `img` with a composed summary.
|
|
63
|
+
* Presentational and hook-free, so it stays server-safe.
|
|
64
|
+
*/
|
|
65
|
+
export const RangeBar = forwardRef(({ segments, orientation = 'vertical', size = 'md', showLegend = true, target, minSegmentShare = 0.6, labelThreshold = 8, locale = 'en', labels: labelOverrides, className, ...props }, ref) => {
|
|
66
|
+
const labels = resolveLabels(labelOverrides);
|
|
67
|
+
const shares = segments.map((s) => (Number.isFinite(s.share) && s.share > 0 ? s.share : 0));
|
|
68
|
+
const sum = clean(shares.reduce((a, b) => a + b, 0));
|
|
69
|
+
const items = segments.map((s, i) => resolveItem(s, clean(sum > 100 ? (shares[i] / sum) * 100 : shares[i])));
|
|
70
|
+
if (sum < 100 && !segments.some((s) => s.id === OTHER_ID)) {
|
|
71
|
+
items.push(resolveItem({ id: OTHER_ID, label: labels.other, share: 0, tone: 'neutral' }, clean(100 - sum)));
|
|
72
|
+
}
|
|
73
|
+
const pct = (n) => formatValue(n, { locale, percent: true }).text;
|
|
74
|
+
const summary = items.map((s) => `${s.label} ${pct(s.weight)}`).join(', ');
|
|
75
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-range-bar eidotter-health flex gap-3 font-dos', orientation === 'horizontal' ? 'flex-col' : 'flex-row items-start', orientationClasses[orientation] ?? orientationClasses.vertical, sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [_jsxs("div", { className: cn('eidotter-range-bar__track flex gap-1', orientation === 'horizontal' ? 'flex-col w-full' : 'flex-row'), children: [_jsx("div", { role: "img", "aria-label": summary, className: cn('eidotter-range-bar__bar flex shrink-0', orientation === 'horizontal' ? 'flex-row' : 'flex-col', trackClasses[`${orientation}-${size}`] ?? trackClasses['vertical-md']), children: items.map((s) => {
|
|
76
|
+
const isTarget = target?.segmentId === s.id;
|
|
77
|
+
return (_jsx("div", { "data-segment-id": s.id, className: cn('eidotter-range-bar__seg relative', toneClasses[s.toneKey], s.patternClass), style: {
|
|
78
|
+
flexGrow: Math.max(s.weight, minSegmentShare),
|
|
79
|
+
flexBasis: 0,
|
|
80
|
+
...(s.toneKey === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
|
|
81
|
+
}, children: isTarget && target && (_jsx("span", { "aria-hidden": "true", className: "eidotter-range-bar__target absolute", style: {
|
|
82
|
+
[orientation === 'horizontal' ? 'left' : 'top']: `${Math.min(Math.max(target.minShare / (s.weight || 1), 0), 1) * 100}%`,
|
|
83
|
+
} })) }, s.id));
|
|
84
|
+
}) }), _jsx("div", { "aria-hidden": "true", className: cn('eidotter-range-bar__lane flex tabular-nums text-dos-text-accent', orientation === 'horizontal' ? 'flex-row w-full' : 'flex-col'), children: items.map((s) => (_jsxs("div", { className: cn('eidotter-range-bar__cell flex items-center justify-center gap-1 min-w-0', orientation === 'horizontal' ? 'flex-col' : 'flex-row justify-start'), style: { flexGrow: Math.max(s.weight, minSegmentShare), flexBasis: 0 }, children: [s.weight >= labelThreshold && (_jsx("span", { className: "eidotter-range-bar__pct", children: pct(s.weight) })), target?.segmentId === s.id && (_jsx("span", { className: "eidotter-range-bar__target-label text-dos-text-brand whitespace-nowrap", children: target.label }))] }, s.id))) })] }), showLegend && (_jsx("ul", { className: "eidotter-range-bar__legend flex flex-col gap-1 list-none p-0 m-0 min-w-0", children: items.map((s) => {
|
|
85
|
+
return (_jsxs("li", { className: "eidotter-range-bar__row grid grid-cols-[auto_1fr_auto_auto] items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: cn('eidotter-range-bar__swatch inline-block w-[0.75rem] h-[0.75rem]', toneClasses[s.toneKey], s.patternClass), style: {
|
|
86
|
+
...(s.toneKey === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
|
|
87
|
+
} }), _jsxs("span", { className: "text-dos-text-primary truncate", title: s.label, children: [s.label, s.bound && _jsxs("span", { className: "text-dos-text-muted", children: [" ", s.bound] })] }), _jsx("span", { className: "text-dos-text-accent tabular-nums", children: pct(s.weight) }), _jsx("span", { className: "text-dos-text-muted tabular-nums text-right", children: s.duration ?? '' })] }, s.id));
|
|
88
|
+
}) }))] }));
|
|
89
|
+
});
|
|
90
|
+
RangeBar.displayName = 'RangeBar';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RangeBar } from './RangeBar.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RangeBar } from './components/index.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { HealthLabels, Tone } from '../../../utils/health/index.js';
|
|
3
|
+
export interface SeriesBarsPoint {
|
|
4
|
+
/** Tick and table label. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** `null`, `NaN` and `Infinity` render as a placeholder gap. */
|
|
7
|
+
value: number | null;
|
|
8
|
+
}
|
|
9
|
+
export interface SeriesBarsProps {
|
|
10
|
+
/** Short series, one vertical bar each. */
|
|
11
|
+
points: SeriesBarsPoint[];
|
|
12
|
+
/** Value at full bar height. Defaults to the largest finite value (or goal). */
|
|
13
|
+
max?: number;
|
|
14
|
+
/** Goal value, drawn as a line. */
|
|
15
|
+
goal?: number;
|
|
16
|
+
/** Text shown on the goal line. */
|
|
17
|
+
goalLabel?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Consumer-supplied tone thresholds. A point takes the tone of the entry with
|
|
20
|
+
* the largest `from` that is at or below its value. Without this prop, points
|
|
21
|
+
* at or above `goal` are success and the rest neutral.
|
|
22
|
+
*/
|
|
23
|
+
toneFor?: {
|
|
24
|
+
from: number;
|
|
25
|
+
tone: Tone;
|
|
26
|
+
}[];
|
|
27
|
+
/** Label every n-th bar (default 1). */
|
|
28
|
+
tickEvery?: number;
|
|
29
|
+
/** Size variant. */
|
|
30
|
+
size?: 'sm' | 'md' | 'lg';
|
|
31
|
+
/** BCP 47 locale for number formatting (default `en`). */
|
|
32
|
+
locale?: string;
|
|
33
|
+
/** Overrides for the built-in label text. */
|
|
34
|
+
labels?: Partial<HealthLabels>;
|
|
35
|
+
/** Additional CSS class name. */
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Short series as vertical bars with an optional goal line. The plot is one
|
|
40
|
+
* `img` with a composed summary; a visually hidden table beside it carries the
|
|
41
|
+
* values for assistive tech. Presentational and hook-free, so server-safe.
|
|
42
|
+
*/
|
|
43
|
+
export declare const SeriesBars: React.ForwardRefExoticComponent<SeriesBarsProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import { formatValue, isKnown, resolveLabels, PLACEHOLDER } from '../../../utils/health/index.js';
|
|
5
|
+
const sizeClasses = {
|
|
6
|
+
sm: 'eidotter-series-bars--sm text-dos-text-xs',
|
|
7
|
+
md: 'eidotter-series-bars--md text-dos-text-sm',
|
|
8
|
+
lg: 'eidotter-series-bars--lg text-dos-text-md',
|
|
9
|
+
};
|
|
10
|
+
const plotClasses = {
|
|
11
|
+
sm: 'h-[6rem]',
|
|
12
|
+
md: 'h-[10rem]',
|
|
13
|
+
lg: 'h-[14rem]',
|
|
14
|
+
};
|
|
15
|
+
const toneClasses = {
|
|
16
|
+
neutral: 'eidotter-tone--neutral',
|
|
17
|
+
info: 'eidotter-tone--info',
|
|
18
|
+
success: 'eidotter-tone--success',
|
|
19
|
+
warning: 'eidotter-tone--warning',
|
|
20
|
+
critical: 'eidotter-tone--critical',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Neutral hue, set inline. Filled tones take `--eidotter-fill` from the
|
|
24
|
+
* `.eidotter-tone--*` class in health-patterns.css (KTD3), which sets none for
|
|
25
|
+
* neutral, so a neutral fill (e.g. with a pattern override) still paints.
|
|
26
|
+
*/
|
|
27
|
+
const NEUTRAL_FILL = 'var(--color-semantic-text-muted)';
|
|
28
|
+
// Neutral is intentionally unpatterned here (unlike Meter, which reuses solid).
|
|
29
|
+
const patternClasses = {
|
|
30
|
+
neutral: 'eidotter-fill--none',
|
|
31
|
+
info: 'eidotter-fill--solid',
|
|
32
|
+
success: 'eidotter-fill--dither',
|
|
33
|
+
warning: 'eidotter-fill--hatch',
|
|
34
|
+
critical: 'eidotter-fill--dotted',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Short series as vertical bars with an optional goal line. The plot is one
|
|
38
|
+
* `img` with a composed summary; a visually hidden table beside it carries the
|
|
39
|
+
* values for assistive tech. Presentational and hook-free, so server-safe.
|
|
40
|
+
*/
|
|
41
|
+
export const SeriesBars = forwardRef(({ points, max, goal, goalLabel, toneFor, tickEvery = 1, size = 'md', locale = 'en', labels: labelOverrides, className, ...props }, ref) => {
|
|
42
|
+
const labels = resolveLabels(labelOverrides);
|
|
43
|
+
const known = points.map((p) => p.value).filter(isKnown);
|
|
44
|
+
const top = max ?? Math.max(...known, goal ?? 0, 0);
|
|
45
|
+
const scale = top > 0 ? top : 1;
|
|
46
|
+
const thresholds = toneFor ?? (goal !== undefined ? [{ from: goal, tone: 'success' }] : []);
|
|
47
|
+
const toneOf = (v) => thresholds
|
|
48
|
+
.filter((t) => v >= t.from)
|
|
49
|
+
.reduce((best, t) => (!best || t.from > best.from ? t : best), undefined)?.tone ?? 'neutral';
|
|
50
|
+
const text = (v) => (isKnown(v) ? formatValue(v, { locale }).text : labels.unknown);
|
|
51
|
+
const summary = [
|
|
52
|
+
...points.map((p) => `${p.label} ${text(p.value)}`),
|
|
53
|
+
...(goal !== undefined ? [`${goalLabel ?? ''} ${text(goal)}`.trim()] : []),
|
|
54
|
+
].join(', ');
|
|
55
|
+
return (_jsxs("div", { ref: ref, className: cn('eidotter-series-bars eidotter-health relative font-dos', sizeClasses[size] ?? sizeClasses.md, className), ...props, children: [_jsxs("div", { role: "img", "aria-label": summary, children: [_jsxs("div", { className: cn('eidotter-series-bars__plot relative', plotClasses[size] ?? plotClasses.md), children: [goal !== undefined && (_jsx("div", { className: "eidotter-series-bars__goal absolute inset-x-0", style: { bottom: `${(Math.min(goal, scale) / scale) * 100}%` }, children: goalLabel && (_jsx("span", { className: "eidotter-series-bars__goal-tag text-dos-text-brand", children: goalLabel })) })), _jsx("div", { className: "eidotter-series-bars__cols flex items-end gap-1 h-full", children: points.map((p, i) => {
|
|
56
|
+
const v = p.value;
|
|
57
|
+
const tone = isKnown(v) ? toneOf(v) : 'neutral';
|
|
58
|
+
return (_jsx("div", { className: "eidotter-series-bars__col flex flex-col justify-end flex-1 min-w-0 h-full", title: `${p.label}: ${text(v)}`, children: isKnown(v) ? (_jsx("div", { className: cn('eidotter-series-bars__bar w-full', toneClasses[tone], patternClasses[tone]), style: {
|
|
59
|
+
height: `${(Math.min(Math.max(v, 0), scale) / scale) * 100}%`,
|
|
60
|
+
...(tone === 'neutral' && { '--eidotter-fill': NEUTRAL_FILL }),
|
|
61
|
+
} })) : (_jsx("span", { className: "eidotter-series-bars__gap text-dos-text-muted text-center", children: PLACEHOLDER })) }, i));
|
|
62
|
+
}) })] }), _jsx("div", { "aria-hidden": "true", className: "flex gap-1 text-dos-text-muted", children: points.map((p, i) => (_jsx("span", { className: "eidotter-series-bars__tick flex-1 min-w-0 truncate text-center tabular-nums", children: i % Math.max(tickEvery, 1) === 0 ? p.label : '' }, i))) })] }), _jsx("table", { className: "sr-only", children: _jsxs("tbody", { children: [points.map((p, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: p.label }), _jsx("td", { children: text(p.value) })] }, i))), goal !== undefined && (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: goalLabel }), _jsx("td", { children: text(goal) })] }))] }) })] }));
|
|
63
|
+
});
|
|
64
|
+
SeriesBars.displayName = 'SeriesBars';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SeriesBars } from './SeriesBars.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SeriesBars } from './components/index.js';
|
|
@@ -10,6 +10,12 @@ export interface StatProps {
|
|
|
10
10
|
trendValue?: string;
|
|
11
11
|
/** Size variant. Use sm/md/lg — small/medium/large are @deprecated aliases. */
|
|
12
12
|
size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large';
|
|
13
|
+
/** Optional unit rendered after the value (e.g., "kg") */
|
|
14
|
+
unit?: string;
|
|
15
|
+
/** Optional muted note rendered below the value */
|
|
16
|
+
note?: string;
|
|
17
|
+
/** Optional target text rendered below the value; the consumer supplies the copy */
|
|
18
|
+
target?: string;
|
|
13
19
|
/** Whether to animate value changes with a text scramble effect */
|
|
14
20
|
scramble?: boolean;
|
|
15
21
|
/** Optional CSS class name */
|
|
@@ -30,13 +30,13 @@ const trendLabels = {
|
|
|
30
30
|
* DOS-styled Stat component for displaying metrics with optional trends.
|
|
31
31
|
* Pure presentational — no React Aria needed.
|
|
32
32
|
*/
|
|
33
|
-
export const Stat = forwardRef(({ label, value, trend, trendValue, size = 'md', scramble = false, className, ...props }, ref) => {
|
|
33
|
+
export const Stat = forwardRef(({ label, value, trend, trendValue, size = 'md', scramble = false, unit, note, target, className, ...props }, ref) => {
|
|
34
34
|
const { text: scrambledValue } = useTextScramble(String(value), {
|
|
35
35
|
speed: 30,
|
|
36
36
|
enabled: scramble,
|
|
37
37
|
});
|
|
38
38
|
const displayValue = scramble ? scrambledValue : String(value);
|
|
39
39
|
const s = sizeClasses[size] || sizeClasses.md;
|
|
40
|
-
return (_jsxs("div", { ref: ref, className: cn('flex flex-col font-dos', 'eidotter-stat', s.root, className), ...props, children: [_jsx("span", { className: cn('text-dos-text-brand uppercase tracking-wider leading-tight', s.label), children: label }),
|
|
40
|
+
return (_jsxs("div", { ref: ref, className: cn('flex flex-col font-dos', 'eidotter-stat', s.root, className), ...props, children: [_jsx("span", { className: cn('text-dos-text-brand uppercase tracking-wider leading-tight', s.label), children: label }), _jsxs("span", { className: cn('text-dos-text-accent font-bold leading-none tabular-nums', s.value), children: [displayValue, unit && (_jsx("span", { className: "eidotter-stat__unit ml-1 text-[0.5em] font-normal text-dos-text-brand", children: unit }))] }), note && (_jsx("span", { className: `eidotter-stat__note text-dos-text-muted leading-tight ${s.trend}`, children: note })), target && (_jsx("span", { className: `eidotter-stat__target text-dos-text-brand leading-tight ${s.trend}`, children: target })), trend && (_jsxs("span", { className: cn('inline-flex items-center gap-1 leading-none', trendColorClasses[trend], s.trend), "aria-label": `Trend: ${trendLabels[trend]}${trendValue ? `, ${trendValue}` : ''}`, children: [_jsx("span", { className: "text-[0.8em]", "aria-hidden": "true", children: trendIcons[trend] }), trendValue && (_jsx("span", { className: "tabular-nums", children: trendValue }))] }))] }));
|
|
41
41
|
});
|
|
42
42
|
Stat.displayName = 'Stat';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NavProps } from '../../Nav/index.js';
|
|
3
|
+
import type { TimelineEntryData } from '../../TimelineContainer/index.js';
|
|
4
|
+
/** A filterable category: a dot/chip colour and a human label. */
|
|
5
|
+
export interface TimelineCategory {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** CSS colour for the dot + active chip. Omit → amber (currentColor). */
|
|
9
|
+
color?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface TimelineFeedProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
|
|
12
|
+
/** Entries to list, newest-first is the consumer's responsibility. */
|
|
13
|
+
entries: TimelineEntryData[];
|
|
14
|
+
/** Categories for the filter chip row + dot colours. Omit to hide the filter. */
|
|
15
|
+
categories?: TimelineCategory[];
|
|
16
|
+
/** Resolve an entry's category key. Default: `entry.tags?.[0]`. */
|
|
17
|
+
categoryOf?: (entry: TimelineEntryData) => string | undefined;
|
|
18
|
+
/** Controlled active category (null = ALL). Leave undefined for uncontrolled. */
|
|
19
|
+
activeCategory?: string | null;
|
|
20
|
+
/** Uncontrolled initial category. Default null (ALL). */
|
|
21
|
+
defaultCategory?: string | null;
|
|
22
|
+
/** Fired when the filter changes. */
|
|
23
|
+
onCategoryChange?: (key: string | null) => void;
|
|
24
|
+
/** Custom link component for entry titles that carry an `article` href. */
|
|
25
|
+
linkComponent?: NavProps['linkComponent'];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* TimelineFeed — a dense, product-grade feed (the "J" layout): each row is a
|
|
29
|
+
* category dot · neutral date · title · a delicate outlined kind-tag, threaded
|
|
30
|
+
* by a thin amber spine. An optional category chip row culls the list
|
|
31
|
+
* ("navigation becomes a course through time"). Amber-mono; category colour
|
|
32
|
+
* lives only in the small dots + the active chip.
|
|
33
|
+
*
|
|
34
|
+
* Presentational — shares the `TimelineEntryData` model with TimelineContainer;
|
|
35
|
+
* the consumer owns ordering and data.
|
|
36
|
+
*/
|
|
37
|
+
export declare function TimelineFeed({ entries, categories, categoryOf, activeCategory, defaultCategory, onCategoryChange, linkComponent, className, ...rest }: TimelineFeedProps): React.JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
import { isSafeHref } from '../../../utils/isSafeHref.js';
|
|
6
|
+
const KIND_LABEL = {
|
|
7
|
+
milestone: 'milestone',
|
|
8
|
+
project: 'project',
|
|
9
|
+
event: 'event',
|
|
10
|
+
};
|
|
11
|
+
const defaultCategoryOf = (entry) => entry.tags?.[0];
|
|
12
|
+
/** Read an entry's optional read-more href (only the `article` kind has one). */
|
|
13
|
+
function entryHref(entry) {
|
|
14
|
+
return entry.kind === 'article' ? entry.href : undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* TimelineFeed — a dense, product-grade feed (the "J" layout): each row is a
|
|
18
|
+
* category dot · neutral date · title · a delicate outlined kind-tag, threaded
|
|
19
|
+
* by a thin amber spine. An optional category chip row culls the list
|
|
20
|
+
* ("navigation becomes a course through time"). Amber-mono; category colour
|
|
21
|
+
* lives only in the small dots + the active chip.
|
|
22
|
+
*
|
|
23
|
+
* Presentational — shares the `TimelineEntryData` model with TimelineContainer;
|
|
24
|
+
* the consumer owns ordering and data.
|
|
25
|
+
*/
|
|
26
|
+
export function TimelineFeed({ entries, categories, categoryOf = defaultCategoryOf, activeCategory, defaultCategory = null, onCategoryChange, linkComponent, className, ...rest }) {
|
|
27
|
+
const LinkTag = linkComponent ?? 'a';
|
|
28
|
+
const isControlled = activeCategory !== undefined;
|
|
29
|
+
const [internal, setInternal] = useState(defaultCategory);
|
|
30
|
+
const active = isControlled ? activeCategory : internal;
|
|
31
|
+
const setActive = (key) => {
|
|
32
|
+
if (!isControlled)
|
|
33
|
+
setInternal(key);
|
|
34
|
+
onCategoryChange?.(key);
|
|
35
|
+
};
|
|
36
|
+
const colorByKey = useMemo(() => {
|
|
37
|
+
const map = new Map();
|
|
38
|
+
categories?.forEach((c) => map.set(c.key, c.color));
|
|
39
|
+
return map;
|
|
40
|
+
}, [categories]);
|
|
41
|
+
const dateFormatter = useMemo(() => new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' }), []);
|
|
42
|
+
const formatDate = (iso) => {
|
|
43
|
+
try {
|
|
44
|
+
return dateFormatter.format(new Date(iso));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return iso;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const visible = useMemo(() => (active ? entries.filter((e) => categoryOf(e) === active) : entries), [active, entries, categoryOf]);
|
|
51
|
+
return (_jsxs("section", { className: cn('eidotter-feed font-dos', className), ...rest, children: [categories && categories.length > 0 && (_jsxs("div", { className: "eidotter-feed__filter", role: "group", "aria-label": "Filter by category", children: [_jsxs("button", { type: "button", className: cn('eidotter-feed__chip', active === null && 'eidotter-feed__chip--active'), "aria-pressed": active === null, onClick: () => setActive(null), children: ["all", active !== null && _jsxs("span", { className: "eidotter-feed__count", children: [" \u00B7 ", visible.length, " of ", entries.length] })] }), categories.map((c) => (_jsxs("button", { type: "button", className: cn('eidotter-feed__chip', active === c.key && 'eidotter-feed__chip--active'), "aria-pressed": active === c.key, onClick: () => setActive(c.key), style: c.color ? { '--eidotter-feed-chip': c.color } : undefined, children: [_jsx("span", { className: "eidotter-feed__chip-dot", "aria-hidden": "true" }), c.label] }, c.key)))] })), _jsx("ol", { className: "eidotter-feed__list", "aria-label": rest['aria-label'] ?? 'Timeline feed', children: visible.map((entry) => {
|
|
52
|
+
const catKey = categoryOf(entry);
|
|
53
|
+
const dotColor = catKey ? colorByKey.get(catKey) : undefined;
|
|
54
|
+
const href = entryHref(entry);
|
|
55
|
+
const safeHref = href && isSafeHref(href) ? href : undefined;
|
|
56
|
+
const kind = entry.type ? KIND_LABEL[entry.type] : undefined;
|
|
57
|
+
return (_jsxs("li", { className: "eidotter-feed__row", children: [_jsx("span", { className: "eidotter-feed__dot", "aria-hidden": "true", style: dotColor ? { '--eidotter-feed-dot': dotColor } : undefined }), _jsx("time", { className: "eidotter-feed__date", dateTime: entry.date, children: formatDate(entry.date) }), _jsx("span", { className: "eidotter-feed__title", children: safeHref ? (_jsx(LinkTag, { href: safeHref, className: "eidotter-feed__title-link", children: entry.title })) : (entry.title) }), kind && _jsx("span", { className: "eidotter-feed__kind", children: kind })] }, entry.id));
|
|
58
|
+
}) })] }));
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TimelineFeed } from './TimelineFeed.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TimelineFeed } from './components/index.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NavProps } from '../../Nav/index.js';
|
|
3
|
+
import type { TimelineCategory } from '../../TimelineFeed/index.js';
|
|
4
|
+
import type { TimelineEntryData } from '../../TimelineContainer/index.js';
|
|
5
|
+
export interface TimelinePageProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
|
|
6
|
+
/** Entries. Grouped by year and shown newest-first automatically. */
|
|
7
|
+
entries: TimelineEntryData[];
|
|
8
|
+
/** Categories for the filter chip row. Omit to hide the filter. */
|
|
9
|
+
categories?: TimelineCategory[];
|
|
10
|
+
/** Resolve an entry's category key. Default: `entry.tags?.[0]`. */
|
|
11
|
+
categoryOf?: (entry: TimelineEntryData) => string | undefined;
|
|
12
|
+
/** Resolve an entry's description line. Default: an `article` entry's `summary`. */
|
|
13
|
+
descriptionOf?: (entry: TimelineEntryData) => string | undefined;
|
|
14
|
+
/** Controlled active category (null = ALL). */
|
|
15
|
+
activeCategory?: string | null;
|
|
16
|
+
/** Uncontrolled initial category. Default null (ALL). */
|
|
17
|
+
defaultCategory?: string | null;
|
|
18
|
+
onCategoryChange?: (key: string | null) => void;
|
|
19
|
+
/** Brand slot at the start of the filter row (e.g. a `<Mark/>`). */
|
|
20
|
+
brand?: React.ReactNode;
|
|
21
|
+
/** Show the right-hand hover-preview panel. Default true. */
|
|
22
|
+
preview?: boolean;
|
|
23
|
+
/** Custom link component for entry titles that carry an `article` href. */
|
|
24
|
+
linkComponent?: NavProps['linkComponent'];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* TimelinePage — a Bret-Victor-style blog timeline (the "K2" layout). A filter
|
|
28
|
+
* chip row (with an optional Mark brand slot) sits above a year-grouped
|
|
29
|
+
* timeline: an AMBER-ONLY spine + dots (never category-rainbow), neutral grey
|
|
30
|
+
* dates, big titles and a description line — content leads. A right-hand panel
|
|
31
|
+
* previews the focused entry's thumbnail on hover/focus.
|
|
32
|
+
*
|
|
33
|
+
* Presentational; shares the `TimelineEntryData` model with TimelineContainer.
|
|
34
|
+
*/
|
|
35
|
+
export declare function TimelinePage({ entries, categories, categoryOf, descriptionOf, activeCategory, defaultCategory, onCategoryChange, brand, preview, linkComponent, className, ...rest }: TimelinePageProps): React.JSX.Element;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
import { isSafeHref } from '../../../utils/isSafeHref.js';
|
|
6
|
+
const defaultCategoryOf = (entry) => entry.tags?.[0];
|
|
7
|
+
const defaultDescriptionOf = (entry) => entry.kind === 'article' ? entry.summary : undefined;
|
|
8
|
+
function previewImage(entry) {
|
|
9
|
+
if (entry.kind === 'image')
|
|
10
|
+
return entry.image;
|
|
11
|
+
if (entry.kind === 'gallery')
|
|
12
|
+
return entry.images[0];
|
|
13
|
+
if (entry.kind === 'article')
|
|
14
|
+
return entry.images?.[0];
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function entryHref(entry) {
|
|
18
|
+
return entry.kind === 'article' ? entry.href : undefined;
|
|
19
|
+
}
|
|
20
|
+
function yearOf(iso) {
|
|
21
|
+
const y = new Date(iso).getFullYear();
|
|
22
|
+
return Number.isNaN(y) ? iso.slice(0, 4) : String(y);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* TimelinePage — a Bret-Victor-style blog timeline (the "K2" layout). A filter
|
|
26
|
+
* chip row (with an optional Mark brand slot) sits above a year-grouped
|
|
27
|
+
* timeline: an AMBER-ONLY spine + dots (never category-rainbow), neutral grey
|
|
28
|
+
* dates, big titles and a description line — content leads. A right-hand panel
|
|
29
|
+
* previews the focused entry's thumbnail on hover/focus.
|
|
30
|
+
*
|
|
31
|
+
* Presentational; shares the `TimelineEntryData` model with TimelineContainer.
|
|
32
|
+
*/
|
|
33
|
+
export function TimelinePage({ entries, categories, categoryOf = defaultCategoryOf, descriptionOf = defaultDescriptionOf, activeCategory, defaultCategory = null, onCategoryChange, brand, preview = true, linkComponent, className, ...rest }) {
|
|
34
|
+
const LinkTag = linkComponent ?? 'a';
|
|
35
|
+
const isControlled = activeCategory !== undefined;
|
|
36
|
+
const [internal, setInternal] = useState(defaultCategory);
|
|
37
|
+
const active = isControlled ? activeCategory : internal;
|
|
38
|
+
const setActive = (key) => {
|
|
39
|
+
if (!isControlled)
|
|
40
|
+
setInternal(key);
|
|
41
|
+
onCategoryChange?.(key);
|
|
42
|
+
};
|
|
43
|
+
const dateFormatter = useMemo(() => new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }), []);
|
|
44
|
+
const formatDate = (iso) => {
|
|
45
|
+
try {
|
|
46
|
+
return dateFormatter.format(new Date(iso));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return iso;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const visible = useMemo(() => (active ? entries.filter((e) => categoryOf(e) === active) : entries), [active, entries, categoryOf]);
|
|
53
|
+
const groups = useMemo(() => {
|
|
54
|
+
const byYear = new Map();
|
|
55
|
+
for (const e of visible) {
|
|
56
|
+
const y = yearOf(e.date);
|
|
57
|
+
const list = byYear.get(y);
|
|
58
|
+
if (list)
|
|
59
|
+
list.push(e);
|
|
60
|
+
else
|
|
61
|
+
byYear.set(y, [e]);
|
|
62
|
+
}
|
|
63
|
+
return Array.from(byYear.entries())
|
|
64
|
+
.map(([year, es]) => ({ year, entries: [...es].sort((a, b) => b.date.localeCompare(a.date)) }))
|
|
65
|
+
.sort((a, b) => b.year.localeCompare(a.year));
|
|
66
|
+
}, [visible]);
|
|
67
|
+
const [focusedId, setFocusedId] = useState(null);
|
|
68
|
+
const focused = useMemo(() => visible.find((e) => e.id === focusedId) ?? visible[0], [visible, focusedId]);
|
|
69
|
+
const focusedImage = focused ? previewImage(focused) : undefined;
|
|
70
|
+
return (_jsxs("section", { className: cn('eidotter-blog font-dos', className), ...rest, children: [(brand || (categories && categories.length > 0)) && (_jsxs("div", { className: "eidotter-blog__filter", children: [brand && _jsx("div", { className: "eidotter-blog__brand", children: brand }), categories && categories.length > 0 && (_jsxs("div", { className: "eidotter-blog__chips", role: "group", "aria-label": "Filter by category", children: [_jsx("button", { type: "button", className: cn('eidotter-blog__chip', active === null && 'eidotter-blog__chip--active'), "aria-pressed": active === null, onClick: () => setActive(null), children: "all" }), categories.map((c) => (_jsxs("button", { type: "button", className: cn('eidotter-blog__chip', active === c.key && 'eidotter-blog__chip--active'), "aria-pressed": active === c.key, onClick: () => setActive(c.key), style: c.color ? { '--eidotter-blog-chip': c.color } : undefined, children: [_jsx("span", { className: "eidotter-blog__chip-dot", "aria-hidden": "true" }), c.label] }, c.key))), active !== null && (_jsxs("span", { className: "eidotter-blog__count", children: [visible.length, " of ", entries.length] }))] }))] })), _jsxs("div", { className: "eidotter-blog__body", children: [_jsxs("div", { className: "eidotter-blog__timeline", children: [groups.map((group) => (_jsxs("div", { className: "eidotter-blog__year-group", children: [_jsx("h2", { className: "eidotter-blog__year", children: group.year }), _jsx("ol", { className: "eidotter-blog__entries", children: group.entries.map((entry) => {
|
|
71
|
+
const href = entryHref(entry);
|
|
72
|
+
const safeHref = href && isSafeHref(href) ? href : undefined;
|
|
73
|
+
const desc = descriptionOf(entry);
|
|
74
|
+
const focus = () => setFocusedId(entry.id);
|
|
75
|
+
return (_jsxs("li", { className: cn('eidotter-blog__entry', focused?.id === entry.id && 'eidotter-blog__entry--focused'), onMouseEnter: focus, onFocus: focus, children: [_jsx("span", { className: "eidotter-blog__dot", "aria-hidden": "true" }), _jsx("time", { className: "eidotter-blog__date", dateTime: entry.date, children: formatDate(entry.date) }), _jsx("h3", { className: "eidotter-blog__title", children: safeHref ? (_jsx(LinkTag, { href: safeHref, className: "eidotter-blog__title-link", children: entry.title })) : (entry.title) }), desc && _jsx("p", { className: "eidotter-blog__desc", children: desc })] }, entry.id));
|
|
76
|
+
}) })] }, group.year))), groups.length === 0 && _jsx("p", { className: "eidotter-blog__empty", children: "C:\\LOG> No entries." })] }), preview && (_jsx("aside", { className: "eidotter-blog__preview", "aria-hidden": "true", children: _jsxs("div", { className: "eidotter-blog__preview-inner", children: [focusedImage ? (_jsx("img", { className: "eidotter-blog__preview-img", src: focusedImage.thumbnail ?? focusedImage.src, alt: "", loading: "lazy" })) : (_jsx("div", { className: "eidotter-blog__preview-placeholder", children: "\u2593\u2592\u2591 \u00B7 \u2591\u2592\u2593" })), focused && (_jsx("div", { className: "eidotter-blog__preview-caption", children: _jsx("span", { className: "eidotter-blog__preview-title", children: focused.title }) }))] }) }))] })] }));
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TimelinePage } from './TimelinePage.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TimelinePage } from './components/index.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Band, HealthLabels, Tone, ValueStatus } from '../../../utils/health/index.js';
|
|
3
|
+
export interface VitalTile {
|
|
4
|
+
/** Stable key for the tile. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Tile label. Always part of the accessible name. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Single value. `null`, `NaN` and `Infinity` render as offline or unknown. Ignored when `pair` is set. */
|
|
9
|
+
value: number | null;
|
|
10
|
+
/** Unit shown after the value. */
|
|
11
|
+
unit?: string;
|
|
12
|
+
/** Muted line below the value, e.g. a reading time. */
|
|
13
|
+
secondary?: string;
|
|
14
|
+
/** Paired reading rendered as "a/b", e.g. [124, 78]. */
|
|
15
|
+
pair?: [number, number];
|
|
16
|
+
/** Explicit tone. Wins over `bands`. */
|
|
17
|
+
tone?: Tone;
|
|
18
|
+
/** Data status. */
|
|
19
|
+
status?: ValueStatus;
|
|
20
|
+
/** Consumer-supplied bands, resolved on the raw value (or the first pair part). */
|
|
21
|
+
bands?: Band[];
|
|
22
|
+
}
|
|
23
|
+
export interface VitalsStripProps {
|
|
24
|
+
/** Tiles to render, in order. */
|
|
25
|
+
tiles: VitalTile[];
|
|
26
|
+
/** Size variant. */
|
|
27
|
+
size?: 'sm' | 'md' | 'lg';
|
|
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
|
+
/** Additional CSS class name. */
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Monitor strip of vital tiles. Tiles are rendered here with Stat's size and
|
|
37
|
+
* text token classes rather than by importing Stat, which is a client
|
|
38
|
+
* component (KTD4). Presentational and hook-free, so it stays server-safe.
|
|
39
|
+
*/
|
|
40
|
+
export declare const VitalsStrip: React.ForwardRefExoticComponent<VitalsStripProps & React.RefAttributes<HTMLDivElement>>;
|