@tristan2828/ui-foundation 4.63.2 → 4.64.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/conventions/docs/design-language.md +15 -7
- package/dist/components/app/line-chart.d.ts +7 -1
- package/dist/components/app/line-chart.js +21 -7
- package/dist/components/app/widget-card.js +1 -1
- package/dist/lib/image-file.d.ts +5 -2
- package/dist/lib/image-file.js +18 -3
- package/dist/lib/line-chart-layout.d.ts +10 -1
- package/dist/lib/line-chart-layout.js +14 -3
- package/package.json +1 -1
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 516–630
|
|
19
19
|
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 632–701
|
|
20
20
|
- [Notices](#notices) · lines 703–731
|
|
21
|
-
- [A value over time](#a-value-over-time) · lines 733–
|
|
22
|
-
- [Several values over time](#several-values-over-time) · lines
|
|
23
|
-
- [A list as a board](#a-list-as-a-board) · lines
|
|
24
|
-
- [A list as cards](#a-list-as-cards) · lines
|
|
25
|
-
- [Key numbers on a record](#key-numbers-on-a-record) · lines
|
|
26
|
-
- [Picking at random](#picking-at-random) · lines
|
|
27
|
-
- [Filling in what's missing](#filling-in-whats-missing) · lines
|
|
21
|
+
- [A value over time](#a-value-over-time) · lines 733–819
|
|
22
|
+
- [Several values over time](#several-values-over-time) · lines 782–819
|
|
23
|
+
- [A list as a board](#a-list-as-a-board) · lines 821–872
|
|
24
|
+
- [A list as cards](#a-list-as-cards) · lines 874–920
|
|
25
|
+
- [Key numbers on a record](#key-numbers-on-a-record) · lines 922–944
|
|
26
|
+
- [Picking at random](#picking-at-random) · lines 946–977
|
|
27
|
+
- [Filling in what's missing](#filling-in-whats-missing) · lines 979–1046
|
|
28
28
|
|
|
29
29
|
<!-- /index -->
|
|
30
30
|
|
|
@@ -768,6 +768,14 @@ or `'last'`) with its value.
|
|
|
768
768
|
measured. Its points come after the last of `points`; the mark stays on
|
|
769
769
|
`points`, and a screen reader gets the projection's end in the summary
|
|
770
770
|
and its rows in the table, each named by `label`.
|
|
771
|
+
- **Levels** (4.64): for a value that is one of a few named steps and
|
|
772
|
+
holds until it changes (a confidence, a severity, a stage), pass
|
|
773
|
+
`levels`, lowest first, and give each point its step's index as y:
|
|
774
|
+
`levels={['Very low', 'Low', 'Moderate', 'High']}`. The steps are named
|
|
775
|
+
down the plot's left, the range is all of them (so one step looks like
|
|
776
|
+
one step), and the line steps instead of sloping. Values read by name
|
|
777
|
+
in the mark, the summary and the table; don't pass `formatY` unless the
|
|
778
|
+
names need more.
|
|
771
779
|
- One series and a few dozen points. Zoom or tooltips are a request for
|
|
772
780
|
the foundation, not a hand-drawn chart in the app.
|
|
773
781
|
|
|
@@ -21,6 +21,12 @@ export type LineChartProps = {
|
|
|
21
21
|
};
|
|
22
22
|
/** Which point to mark with a dot and its value: the lowest, the highest or the latest. */
|
|
23
23
|
mark?: LineChartMark;
|
|
24
|
+
/**
|
|
25
|
+
* Named steps, lowest first: `['Very low', 'Low', 'Moderate', 'High']`.
|
|
26
|
+
* Each point's y is its step's index. The steps are named on the plot's
|
|
27
|
+
* left, the range is all of them, and the line steps (4.64).
|
|
28
|
+
*/
|
|
29
|
+
levels?: readonly string[];
|
|
24
30
|
/** A value as shown: `usd`. Defaults to the number as the browser writes it. */
|
|
25
31
|
formatY?: (y: number) => string;
|
|
26
32
|
/** A point's x as shown: `formatPlainDate`. Defaults to x as given. */
|
|
@@ -32,4 +38,4 @@ export type LineChartProps = {
|
|
|
32
38
|
emptyMessage?: string;
|
|
33
39
|
className?: string;
|
|
34
40
|
};
|
|
35
|
-
export declare function LineChart({ label, points, target, projection, mark, formatY, formatX, height, isLoading, emptyMessage, className, }: LineChartProps): import("react").JSX.Element;
|
|
41
|
+
export declare function LineChart({ label, points, target, projection, mark, levels, formatY: formatYGiven, formatX, height, isLoading, emptyMessage, className, }: LineChartProps): import("react").JSX.Element;
|
|
@@ -18,12 +18,19 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
// A `projection` (4.51) carries the line on past its last point, dashed in
|
|
19
19
|
// the same colour: where a balance may go, not where it went. It shares
|
|
20
20
|
// the plot's x and y ranges, so the target and the projection meet where
|
|
21
|
-
// they would.
|
|
21
|
+
// they would.
|
|
22
|
+
//
|
|
23
|
+
// `levels` (4.64) is for a value that is one of a few named steps (a
|
|
24
|
+
// confidence, Very low to High): each y is its step's index. The steps
|
|
25
|
+
// are named down the plot's left with a faint rule at each, the range is
|
|
26
|
+
// every step, and the line holds each value until the next point, then
|
|
27
|
+
// steps. A value reads by its name, in the summary and the table too,
|
|
28
|
+
// unless `formatY` says otherwise. The four states: a skeleton at
|
|
22
29
|
// `height` while loading, one quiet line with no points, a dot (no line)
|
|
23
30
|
// for one point, and the chart.
|
|
24
31
|
import { cn } from 'cn';
|
|
25
32
|
import { Skeleton } from '../ui/skeleton.js';
|
|
26
|
-
import { chartLayout } from '../../lib/line-chart-layout.js';
|
|
33
|
+
import { chartLayout, steppedPlaces } from '../../lib/line-chart-layout.js';
|
|
27
34
|
const defaultY = (y) => y.toLocaleString();
|
|
28
35
|
const defaultX = (x) => String(x);
|
|
29
36
|
const percent = (fraction) => `${fraction * 100}%`;
|
|
@@ -34,7 +41,8 @@ const PLATE = 'rounded-sm bg-card px-1';
|
|
|
34
41
|
function besides(x) {
|
|
35
42
|
return x > 0.75 ? '-translate-x-full -ml-1.5' : 'ml-1.5';
|
|
36
43
|
}
|
|
37
|
-
export function LineChart({ label, points, target, projection, mark, formatY
|
|
44
|
+
export function LineChart({ label, points, target, projection, mark, levels, formatY: formatYGiven, formatX = defaultX, height = 160, isLoading = false, emptyMessage = 'No points yet', className, }) {
|
|
45
|
+
const formatY = formatYGiven ?? (levels ? (y) => levels[Math.round(y)] ?? String(y) : defaultY);
|
|
38
46
|
if (isLoading) {
|
|
39
47
|
return (_jsxs("div", { "data-slot": "line-chart", "data-state": "loading", "aria-busy": "true", className: cn('min-w-0', className), children: [_jsxs("span", { className: "sr-only", children: ["Loading ", label] }), _jsx(Skeleton, { style: { height }, className: "w-full" })] }));
|
|
40
48
|
}
|
|
@@ -42,13 +50,18 @@ export function LineChart({ label, points, target, projection, mark, formatY = d
|
|
|
42
50
|
return (_jsx("figure", { "data-slot": "line-chart", "data-state": "empty", "aria-label": label, className: cn('min-w-0', className), children: _jsx("p", { style: { height }, className: "flex items-center justify-center rounded-md border border-dashed border-border type-body text-muted-foreground", children: emptyMessage }) }));
|
|
43
51
|
}
|
|
44
52
|
const ahead = projection?.points ?? [];
|
|
45
|
-
const
|
|
53
|
+
const steps = levels && levels.length > 0 ? levels.length : undefined;
|
|
54
|
+
const layout = {
|
|
55
|
+
...chartLayout([...points, ...ahead], target?.y, undefined, steps),
|
|
56
|
+
markIndex: chartLayout(points, undefined, mark).markIndex,
|
|
57
|
+
};
|
|
58
|
+
const toLine = (places) => (steps ? steppedPlaces(places) : places).map((place) => `${place.x * 100},${place.y * 100}`).join(' ');
|
|
46
59
|
const projected = ahead.length > 0 ? layout.places.slice(points.length - 1) : [];
|
|
47
60
|
const lastX = ahead.length > 0 ? ahead[ahead.length - 1].x : points[points.length - 1].x;
|
|
48
61
|
const values = points.map((point) => point.y);
|
|
49
62
|
const lowest = values.indexOf(Math.min(...values));
|
|
50
63
|
const highest = values.indexOf(Math.max(...values));
|
|
51
|
-
const line = layout.places.slice(0, points.length)
|
|
64
|
+
const line = toLine(layout.places.slice(0, points.length));
|
|
52
65
|
const marked = layout.markIndex === undefined ? undefined : layout.places[layout.markIndex];
|
|
53
66
|
const single = points.length === 1 ? layout.places[0] : undefined;
|
|
54
67
|
const dot = marked ?? single;
|
|
@@ -62,7 +75,8 @@ export function LineChart({ label, points, target, projection, mark, formatY = d
|
|
|
62
75
|
]
|
|
63
76
|
.filter(Boolean)
|
|
64
77
|
.join('; ');
|
|
65
|
-
return (_jsxs("figure", { "data-slot": "line-chart", "data-state": "success", "aria-label": label, className: cn('
|
|
78
|
+
return (_jsxs("figure", { "data-slot": "line-chart", "data-state": "success", "aria-label": label, className: cn('grid min-w-0 gap-x-2 gap-y-1.5', steps ? 'grid-cols-[auto_minmax(0,1fr)]' : 'grid-cols-1', className), children: [steps && (_jsxs("div", { "aria-hidden": "true", "data-slot": "line-chart-levels", className: "relative", style: { height }, children: [levels.map((name, index) => (_jsx("span", { className: "absolute right-0 -translate-y-1/2 type-caption whitespace-nowrap text-muted-foreground", style: { top: percent(layout.levelYs[index]) }, children: name }, name))), _jsx("span", { className: "invisible flex h-0 flex-col overflow-hidden type-caption whitespace-nowrap", children: levels.map((name) => (_jsx("span", { children: name }, name))) })] })), _jsxs("div", { "aria-hidden": "true", className: "relative border-b border-border", style: { height }, children: [steps &&
|
|
79
|
+
layout.levelYs.map((y, index) => (_jsx("span", { "data-slot": "line-chart-level-rule", className: "absolute inset-x-0 border-t border-dashed border-border", style: { top: percent(y) } }, index))), target && layout.targetY !== undefined && (_jsxs(_Fragment, { children: [_jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 size-full overflow-visible text-success-text", children: _jsx("line", { x1: "0", x2: "100", y1: layout.targetY * 100, y2: layout.targetY * 100, strokeWidth: 1.5, strokeDasharray: "6 4", vectorEffect: "non-scaling-stroke", className: "stroke-current" }) }), _jsxs("span", { className: cn('absolute right-0 -translate-y-full type-caption text-success-text tabular-nums', PLATE), style: { top: percent(layout.targetY) }, children: [target.label, " ", formatY(target.y)] })] })), points.length > 1 && (_jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 size-full overflow-visible text-info-text", children: _jsx("polyline", { points: line, fill: "none", strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke", className: "stroke-current" }) })), projected.length > 1 && (_jsx("svg", { "data-slot": "line-chart-projection", viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 size-full overflow-visible text-info-text", children: _jsx("polyline", { points: toLine(projected), fill: "none", strokeWidth: 2, strokeDasharray: "6 4", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke", className: "stroke-current" }) })), dot && (_jsxs(_Fragment, { children: [_jsx("span", { "data-slot": "line-chart-mark", className: "absolute size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-info-text ring-2 ring-card", style: { left: percent(dot.x), top: percent(dot.y) } }), _jsx("span", { className: cn('absolute type-caption font-medium text-foreground tabular-nums', PLATE, besides(dot.x),
|
|
66
80
|
// Under the lowest point, where the line isn't; over any other.
|
|
67
|
-
mark === 'min' ? 'mt-1' : '-translate-y-full -mt-1'), style: { left: percent(dot.x), top: percent(dot.y) }, children: formatY(points[marked ? layout.markIndex : 0].y) })] }))] }), _jsxs("div", { "aria-hidden": "true", className:
|
|
81
|
+
mark === 'min' ? 'mt-1' : '-translate-y-full -mt-1'), style: { left: percent(dot.x), top: percent(dot.y) }, children: formatY(points[marked ? layout.markIndex : 0].y) })] }))] }), _jsxs("div", { "aria-hidden": "true", className: cn('flex justify-between gap-3 type-caption text-muted-foreground tabular-nums', steps && 'col-start-2'), children: [_jsx("span", { children: formatX(points[0].x) }), (points.length > 1 || ahead.length > 0) && _jsx("span", { children: formatX(lastX) })] }), _jsx("figcaption", { className: "sr-only", children: steps ? `${summary}; one of ${levels.join(', ')}` : summary }), _jsxs("table", { className: "sr-only", children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "When" }), _jsx("th", { scope: "col", children: "Value" })] }) }), _jsxs("tbody", { children: [points.map((point, index) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: formatX(point.x) }), _jsx("td", { children: formatY(point.y) })] }, index))), ahead.map((point, index) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: formatX(point.x) }), _jsxs("td", { children: [formatY(point.y), " (", projection.label, ")"] })] }, `ahead-${index}`)))] })] })] }));
|
|
68
82
|
}
|
|
@@ -90,7 +90,7 @@ export function WidgetCard({ id, title, icon: Icon, headingLevel = 2, link, isLo
|
|
|
90
90
|
return;
|
|
91
91
|
toggle();
|
|
92
92
|
};
|
|
93
|
-
return (_jsx("section", { id: id, "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className:
|
|
93
|
+
return (_jsx("section", { id: id, "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: cn('glow-edge density-compact:[--card-spacing:--spacing(3)]', collapsed && 'pb-0'), children: [_jsxs(CardHeader, { onClick: onHeaderClick, className: cn('flex items-center justify-between gap-3', inAccordion && 'cursor-pointer select-none', collapsed && 'border-b-0'), children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), (link || foldable || summaryShown) && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [summaryShown && (_jsx("span", { id: summaryId, "data-slot": "widget-card-summary", className: "mr-2 flex items-center", children: _jsx(WidgetCardSummary, { state: state, summary: summary, emptySummary: emptySummary }) })), link?.href !== undefined ? (_jsxs("a", { href: link.href, target: "_blank", rel: "noreferrer", "aria-describedby": headingId, className: cn(HEADER_LINK_CLASS, 'inline-flex items-center gap-1'), children: [link.label, _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] })) : (link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))), foldable && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-labelledby": headingId, "aria-describedby": summaryShown ? summaryId : undefined, "aria-expanded": !collapsed, "aria-controls": contentId, onClick: toggle,
|
|
94
94
|
// The button's own open look (a filled background) would
|
|
95
95
|
// read as pressed: the chevron says it.
|
|
96
96
|
className: "-my-1 -mr-1.5 aria-expanded:not-hover:bg-transparent", children: _jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('transition-transform', collapsed && '-rotate-90') }) }))] }))] }), _jsx(CardContent, { id: contentId, hidden: collapsed, "aria-busy": state === 'loading' || undefined, children: error ? (_jsx(ErrorState, { error: error, onRetry: onRetry, size: "compact" })) : state === 'loading' ? (_jsx(WidgetListSkeleton, { rows: skeletonRows })) : state === 'empty' ? (_jsx("p", { "data-state": "empty", className: "py-2 type-body text-muted-foreground", children: emptyMessage })) : (children) })] }) }));
|
package/dist/lib/image-file.d.ts
CHANGED
|
@@ -30,8 +30,11 @@ export declare function formatBytes(bytes: number): string;
|
|
|
30
30
|
export declare function acceptAttribute(accept?: readonly string[]): string;
|
|
31
31
|
/**
|
|
32
32
|
* Why a file can't be uploaded, as a sentence to show under the field, or
|
|
33
|
-
* null when it can: a type not accepted, or over the size limit.
|
|
33
|
+
* null when it can: a type not accepted, or over the size limit. A file
|
|
34
|
+
* with an empty type is judged by its name's extension (`.heic`, `.heif`).
|
|
34
35
|
*/
|
|
35
|
-
export declare function imageRefusal(file: Pick<File, 'type' | 'size'
|
|
36
|
+
export declare function imageRefusal(file: Pick<File, 'type' | 'size'> & {
|
|
37
|
+
name?: string;
|
|
38
|
+
}, rules?: ImageFileRules): string | null;
|
|
36
39
|
/** What a failed upload says: the rejection's own `message` (an `AppError`'s), else a generic sentence. */
|
|
37
40
|
export declare function uploadFailure(error: unknown, noun?: string): string;
|
package/dist/lib/image-file.js
CHANGED
|
@@ -42,22 +42,37 @@ export function formatBytes(bytes) {
|
|
|
42
42
|
function accepts(accept, type) {
|
|
43
43
|
return accept.some((allowed) => allowed === type || (allowed.endsWith('/*') && type.startsWith(allowed.slice(0, -1))));
|
|
44
44
|
}
|
|
45
|
+
// Some pickers (Windows, some Android) hand over a HEIC or HEIF photo with
|
|
46
|
+
// an empty `type`. Then the name's extension stands in for it, so the file
|
|
47
|
+
// is judged as if it had been typed.
|
|
48
|
+
const TYPES_BY_EXTENSION = {
|
|
49
|
+
heic: 'image/heic',
|
|
50
|
+
heif: 'image/heif',
|
|
51
|
+
};
|
|
52
|
+
function fileType(file) {
|
|
53
|
+
if (file.type || !file.name)
|
|
54
|
+
return file.type;
|
|
55
|
+
const extension = file.name.split('.').at(-1)?.toLowerCase();
|
|
56
|
+
return (file.name.includes('.') && extension && TYPES_BY_EXTENSION[extension]) || '';
|
|
57
|
+
}
|
|
45
58
|
/** The `accept` attribute for a file input: `image/jpeg,image/png,image/webp`. */
|
|
46
59
|
export function acceptAttribute(accept = DEFAULT_IMAGE_TYPES) {
|
|
47
60
|
return accept.join(',');
|
|
48
61
|
}
|
|
49
62
|
/**
|
|
50
63
|
* Why a file can't be uploaded, as a sentence to show under the field, or
|
|
51
|
-
* null when it can: a type not accepted, or over the size limit.
|
|
64
|
+
* null when it can: a type not accepted, or over the size limit. A file
|
|
65
|
+
* with an empty type is judged by its name's extension (`.heic`, `.heif`).
|
|
52
66
|
*/
|
|
53
67
|
export function imageRefusal(file, rules = {}) {
|
|
54
68
|
const accept = rules.accept ?? DEFAULT_IMAGE_TYPES;
|
|
55
|
-
|
|
69
|
+
const type = fileType(file);
|
|
70
|
+
if (!accepts(accept, type)) {
|
|
56
71
|
const wanted = listOr(accept.map(typeName));
|
|
57
72
|
const choose = accept.length === 1 && accept[0] === 'image/*' ? 'Choose an image.' : `Choose a ${wanted} image.`;
|
|
58
73
|
// A type with a name people know is named; any other file that isn't
|
|
59
74
|
// an image is just that.
|
|
60
|
-
const named = TYPE_NAMES[
|
|
75
|
+
const named = TYPE_NAMES[type] ?? (type.startsWith('image/') ? typeName(type) : null);
|
|
61
76
|
return named ? `That file is ${article(named)} ${named}. ${choose}` : `That file isn't an image. ${choose}`;
|
|
62
77
|
}
|
|
63
78
|
if (rules.maxSize !== undefined && file.size > rules.maxSize) {
|
|
@@ -19,8 +19,17 @@ export type ChartLayout = {
|
|
|
19
19
|
targetY?: number;
|
|
20
20
|
/** Which point the mark is on, if any. */
|
|
21
21
|
markIndex?: number;
|
|
22
|
+
/** With levels: each level's height, lowest level first. */
|
|
23
|
+
levelYs?: number[];
|
|
22
24
|
};
|
|
23
|
-
export declare function chartLayout(points: readonly LineChartPoint[], target?: number, mark?: LineChartMark): ChartLayout;
|
|
25
|
+
export declare function chartLayout(points: readonly LineChartPoint[], target?: number, mark?: LineChartMark, levels?: number): ChartLayout;
|
|
26
|
+
export declare function steppedPlaces(places: readonly {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
}[]): {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
}[];
|
|
24
33
|
export type SeriesLayout = {
|
|
25
34
|
/** Each series' places, in the order given, each in its points' order. */
|
|
26
35
|
places: {
|
|
@@ -10,11 +10,15 @@ function along(x) {
|
|
|
10
10
|
// Room above and below the values, as a share of their range, so the line
|
|
11
11
|
// never runs along the plot's edge.
|
|
12
12
|
const PAD = 0.1;
|
|
13
|
-
|
|
13
|
+
// `levels` (4.64): the count of named steps a value is one of (a
|
|
14
|
+
// confidence, Very low to High), each value its step's index. The range is
|
|
15
|
+
// then every step, whatever the values, so a move from one step to the
|
|
16
|
+
// next looks as big as it is, not the plot's whole height.
|
|
17
|
+
export function chartLayout(points, target, mark, levels) {
|
|
14
18
|
const values = points.map((point) => point.y);
|
|
15
19
|
const all = target === undefined ? values : [...values, target];
|
|
16
|
-
let min = Math.min(...all);
|
|
17
|
-
let max = Math.max(...all);
|
|
20
|
+
let min = levels ? 0 : Math.min(...all);
|
|
21
|
+
let max = levels ? levels - 1 : Math.max(...all);
|
|
18
22
|
// One value (or all the same): a band around it, so the line sits mid-plot.
|
|
19
23
|
const spread = max - min || Math.abs(max) || 1;
|
|
20
24
|
min -= spread * PAD;
|
|
@@ -40,8 +44,15 @@ export function chartLayout(points, target, mark) {
|
|
|
40
44
|
domain: { min, max },
|
|
41
45
|
targetY: target === undefined ? undefined : height(target),
|
|
42
46
|
markIndex,
|
|
47
|
+
levelYs: levels ? Array.from({ length: levels }, (_, index) => height(index)) : undefined,
|
|
43
48
|
};
|
|
44
49
|
}
|
|
50
|
+
// A line that holds each value until the next point, then steps to it:
|
|
51
|
+
// for values that change on a day and stay (a confidence), not drift.
|
|
52
|
+
// Places in, polyline places out.
|
|
53
|
+
export function steppedPlaces(places) {
|
|
54
|
+
return places.flatMap((place, index) => (index === 0 ? [place] : [{ x: place.x, y: places[index - 1].y }, place]));
|
|
55
|
+
}
|
|
45
56
|
// Several series on one plot (MultiLineChart): one y range over all their
|
|
46
57
|
// values and the target, and one x range over all their points, so a
|
|
47
58
|
// series that starts later starts further right. When an x isn't a time
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.64.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|