@nadicodeai/ui 0.5.2 → 0.6.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/AGENTS.md +12 -3
- package/README.md +18 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert-dialog.js +5 -5
- package/dist/components/alert.js +1 -1
- package/dist/components/badge.js +4 -4
- package/dist/components/button-group.js +4 -4
- package/dist/components/button.d.ts.map +1 -1
- package/dist/components/button.js +12 -12
- package/dist/components/calendar.js +2 -2
- package/dist/components/card.js +3 -3
- package/dist/components/chart-breakdown-table.d.ts +39 -0
- package/dist/components/chart-breakdown-table.d.ts.map +1 -0
- package/dist/components/chart-breakdown-table.js +29 -0
- package/dist/components/chart-bullet-bar.d.ts +36 -0
- package/dist/components/chart-bullet-bar.d.ts.map +1 -0
- package/dist/components/chart-bullet-bar.js +30 -0
- package/dist/components/chart-date-range-control.d.ts +12 -0
- package/dist/components/chart-date-range-control.d.ts.map +1 -0
- package/dist/components/chart-date-range-control.js +59 -0
- package/dist/components/chart-date-range.d.ts +38 -0
- package/dist/components/chart-date-range.d.ts.map +1 -0
- package/dist/components/chart-date-range.js +63 -0
- package/dist/components/chart-delta.d.ts +28 -0
- package/dist/components/chart-delta.d.ts.map +1 -0
- package/dist/components/chart-delta.js +32 -0
- package/dist/components/chart-entity-colors.d.ts +37 -0
- package/dist/components/chart-entity-colors.d.ts.map +1 -0
- package/dist/components/chart-entity-colors.js +68 -0
- package/dist/components/chart-kpi-stat.d.ts +33 -0
- package/dist/components/chart-kpi-stat.d.ts.map +1 -0
- package/dist/components/chart-kpi-stat.js +7 -0
- package/dist/components/chart-sparkline.d.ts +45 -0
- package/dist/components/chart-sparkline.d.ts.map +1 -0
- package/dist/components/chart-sparkline.js +44 -0
- package/dist/components/chart-time-series.d.ts +33 -0
- package/dist/components/chart-time-series.d.ts.map +1 -0
- package/dist/components/chart-time-series.js +136 -0
- package/dist/components/chart.js +1 -1
- package/dist/components/checkbox.js +1 -1
- package/dist/components/combobox.js +5 -5
- package/dist/components/command.js +4 -4
- package/dist/components/context-menu.js +6 -6
- package/dist/components/dialog.d.ts +8 -4
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +6 -6
- package/dist/components/drawer.d.ts.map +1 -1
- package/dist/components/drawer.js +4 -4
- package/dist/components/dropdown-menu.js +7 -7
- package/dist/components/empty.js +2 -2
- package/dist/components/field.js +3 -3
- package/dist/components/hover-card.js +1 -1
- package/dist/components/input-group.js +5 -5
- package/dist/components/input-otp.d.ts.map +1 -1
- package/dist/components/input-otp.js +8 -3
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts.map +1 -1
- package/dist/components/item.js +5 -5
- package/dist/components/menubar.js +8 -8
- package/dist/components/native-select.js +1 -1
- package/dist/components/navigation-menu.js +5 -5
- package/dist/components/popover.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/resizable.js +1 -1
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/select.js +4 -4
- package/dist/components/sheet.d.ts +4 -2
- package/dist/components/sheet.d.ts.map +1 -1
- package/dist/components/sheet.js +5 -5
- package/dist/components/sidebar.d.ts +7 -3
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +16 -16
- package/dist/components/slider.js +1 -1
- package/dist/components/spinner.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/textarea.js +1 -1
- package/dist/components/theme-mode-switcher.js +1 -1
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle.js +5 -5
- package/dist/components/tooltip.js +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/styles/globals.css +25 -3
- package/dist/styles/shadcn.tailwind.css +629 -0
- package/docs/agents/nadicodeai-ui.md +24 -1
- package/docs/component-classification.md +3 -3
- package/docs/product-compositions.md +2 -2
- package/docs/product-sections.md +2 -2
- package/llms.txt +11 -1
- package/package.json +5 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chart-date-range — the date-range value model and URL-state codec for the
|
|
3
|
+
* console chart kit (issue #166).
|
|
4
|
+
*
|
|
5
|
+
* This module is intentionally server-safe (no "use client" directive): the
|
|
6
|
+
* package owns no routing, so consumers wire `encodeDateRange` /
|
|
7
|
+
* `decodeDateRange` to their own searchParams — often in a Server Component,
|
|
8
|
+
* where importing a pure function from a client module would break. The
|
|
9
|
+
* interactive picker lives in `chart-date-range-control`.
|
|
10
|
+
*/
|
|
11
|
+
const DATE_RANGE_PRESETS = ["7d", "14d", "30d", "cycle"];
|
|
12
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
13
|
+
const CUSTOM_SEPARATOR = "..";
|
|
14
|
+
/**
|
|
15
|
+
* Encodes a date-range value for URL state: a preset encodes as its token
|
|
16
|
+
* ("7d", "cycle", …) and a custom range as "yyyy-mm-dd..yyyy-mm-dd".
|
|
17
|
+
*/
|
|
18
|
+
function encodeDateRange(value) {
|
|
19
|
+
return value.kind === "preset"
|
|
20
|
+
? value.preset
|
|
21
|
+
: `${value.from}${CUSTOM_SEPARATOR}${value.to}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decodes a URL-state string back into a date-range value. Returns null for
|
|
25
|
+
* anything malformed (unknown preset, bad date shape, inverted range) so
|
|
26
|
+
* consumers fall back to their own default.
|
|
27
|
+
*/
|
|
28
|
+
function decodeDateRange(raw) {
|
|
29
|
+
if (!raw) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (DATE_RANGE_PRESETS.includes(raw)) {
|
|
33
|
+
return { kind: "preset", preset: raw };
|
|
34
|
+
}
|
|
35
|
+
const parts = raw.split(CUSTOM_SEPARATOR);
|
|
36
|
+
if (parts.length !== 2) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const [from, to] = parts;
|
|
40
|
+
if (!ISO_DAY.test(from) || !ISO_DAY.test(to) || from > to) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!isRealIsoDay(from) || !isRealIsoDay(to)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return { kind: "custom", from, to };
|
|
47
|
+
}
|
|
48
|
+
/** Parses a "yyyy-mm-dd" day as a LOCAL date (no timezone shifting). */
|
|
49
|
+
function parseIsoDay(iso) {
|
|
50
|
+
const [year, month, day] = iso.split("-").map(Number);
|
|
51
|
+
return new Date(year, month - 1, day);
|
|
52
|
+
}
|
|
53
|
+
/** Formats a local date as "yyyy-mm-dd". */
|
|
54
|
+
function formatIsoDay(date) {
|
|
55
|
+
const year = String(date.getFullYear()).padStart(4, "0");
|
|
56
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
57
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
58
|
+
return `${year}-${month}-${day}`;
|
|
59
|
+
}
|
|
60
|
+
function isRealIsoDay(iso) {
|
|
61
|
+
return formatIsoDay(parseIsoDay(iso)) === iso;
|
|
62
|
+
}
|
|
63
|
+
export { DATE_RANGE_PRESETS, decodeDateRange, encodeDateRange, formatIsoDay, parseIsoDay, };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* chart-delta — shared delta/trend encoding for the console chart kit
|
|
4
|
+
* (issue #166; DESIGN.md "Delta and trend encoding").
|
|
5
|
+
*
|
|
6
|
+
* A change reads as a direction glyph (▲ ▼) plus a tone token, never bare
|
|
7
|
+
* signed text and never color alone. Tone follows direction crossed with
|
|
8
|
+
* whether up is good in the surrounding context (`upIsGood`).
|
|
9
|
+
*/
|
|
10
|
+
type DeltaDirection = "up" | "down" | "flat";
|
|
11
|
+
type DeltaTone = "positive" | "negative" | "neutral";
|
|
12
|
+
/**
|
|
13
|
+
* Maps a delta direction onto its tone: direction crossed with whether up is
|
|
14
|
+
* good in this context. A flat delta is always neutral.
|
|
15
|
+
*/
|
|
16
|
+
declare function deltaTone(direction: DeltaDirection, upIsGood?: boolean): DeltaTone;
|
|
17
|
+
declare function Delta({ value, direction, upIsGood, vsLabel, className, ...props }: Omit<React.ComponentProps<"span">, "children"> & {
|
|
18
|
+
/** The formatted change, e.g. "+12%" or "3.4k". */
|
|
19
|
+
value: React.ReactNode;
|
|
20
|
+
direction: DeltaDirection;
|
|
21
|
+
/** Whether an upward move is good in this context. Defaults to true. */
|
|
22
|
+
upIsGood?: boolean;
|
|
23
|
+
/** The named comparison period, rendered as "vs {vsLabel}". */
|
|
24
|
+
vsLabel?: string;
|
|
25
|
+
}): React.JSX.Element;
|
|
26
|
+
export { Delta, deltaTone };
|
|
27
|
+
export type { DeltaDirection, DeltaTone };
|
|
28
|
+
//# sourceMappingURL=chart-delta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-delta.d.ts","sourceRoot":"","sources":["../../src/components/chart-delta.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B;;;;;;;GAOG;AAEH,KAAK,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAA;AAC5C,KAAK,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAA;AAoBpD;;;GAGG;AACH,iBAAS,SAAS,CAAC,SAAS,EAAE,cAAc,EAAE,QAAQ,UAAO,GAAG,SAAS,CAMxE;AAED,iBAAS,KAAK,CAAC,EACb,KAAK,EACL,SAAS,EACT,QAAe,EACf,OAAO,EACP,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG;IAClD,mDAAmD;IACnD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,SAAS,EAAE,cAAc,CAAA;IACzB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,qBAuBA;AAED,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AAC3B,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
const DELTA_GLYPHS = {
|
|
4
|
+
up: "▲",
|
|
5
|
+
down: "▼",
|
|
6
|
+
flat: "–",
|
|
7
|
+
};
|
|
8
|
+
const DELTA_DIRECTION_TEXT = {
|
|
9
|
+
up: "up",
|
|
10
|
+
down: "down",
|
|
11
|
+
flat: "unchanged",
|
|
12
|
+
};
|
|
13
|
+
const DELTA_TONE_CLASSES = {
|
|
14
|
+
positive: "text-(--nc-state-complete)",
|
|
15
|
+
negative: "text-(--nc-state-blocked)",
|
|
16
|
+
neutral: "text-muted-foreground",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Maps a delta direction onto its tone: direction crossed with whether up is
|
|
20
|
+
* good in this context. A flat delta is always neutral.
|
|
21
|
+
*/
|
|
22
|
+
function deltaTone(direction, upIsGood = true) {
|
|
23
|
+
if (direction === "flat") {
|
|
24
|
+
return "neutral";
|
|
25
|
+
}
|
|
26
|
+
return (direction === "up") === upIsGood ? "positive" : "negative";
|
|
27
|
+
}
|
|
28
|
+
function Delta({ value, direction, upIsGood = true, vsLabel, className, ...props }) {
|
|
29
|
+
const tone = deltaTone(direction, upIsGood);
|
|
30
|
+
return (_jsxs("span", { "data-slot": "delta", "data-direction": direction, "data-tone": tone, className: cn("inline-flex items-baseline gap-1 text-sm font-medium", DELTA_TONE_CLASSES[tone], className), ...props, children: [_jsx("span", { "aria-hidden": "true", children: DELTA_GLYPHS[direction] }), _jsx("span", { className: "sr-only", children: DELTA_DIRECTION_TEXT[direction] }), _jsx("span", { children: value }), vsLabel ? (_jsxs("span", { className: "font-normal text-muted-foreground", children: ["vs ", vsLabel] })) : null] }));
|
|
31
|
+
}
|
|
32
|
+
export { Delta, deltaTone };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chart-entity-colors — the entity-color assignment helper for the console
|
|
3
|
+
* chart kit (issue #166; DESIGN.md "Entity-color assignment").
|
|
4
|
+
*
|
|
5
|
+
* Categorical hues assign in fixed order to entities per page context, and the
|
|
6
|
+
* assignment follows the entity identity, never rank, sort position, or array
|
|
7
|
+
* index. Callers build ONE map from the full page-context entity list (not a
|
|
8
|
+
* filtered or re-sorted view) and pass it to every chart widget on that
|
|
9
|
+
* surface, so a filter or re-sort never repaints the entities that remain on
|
|
10
|
+
* screen. Beyond six entities the tail folds into a single neutral "other"
|
|
11
|
+
* register, never a seventh hue.
|
|
12
|
+
*
|
|
13
|
+
* The map is a plain serializable record so a Server Component can compute it
|
|
14
|
+
* and pass it across the RSC boundary to client chart widgets.
|
|
15
|
+
*/
|
|
16
|
+
declare const CHART_ENTITY_COLOR_TOKENS: readonly ["var(--nc-chart-1)", "var(--nc-chart-2)", "var(--nc-chart-3)", "var(--nc-chart-4)", "var(--nc-chart-5)", "var(--nc-chart-6)"];
|
|
17
|
+
/**
|
|
18
|
+
* The neutral register for folded "everything else" entities. A muted ink
|
|
19
|
+
* token, never an extra categorical hue.
|
|
20
|
+
*/
|
|
21
|
+
declare const CHART_ENTITY_OTHER_COLOR = "var(--muted-foreground)";
|
|
22
|
+
type EntityColorMap = Readonly<Record<string, string>>;
|
|
23
|
+
/**
|
|
24
|
+
* Assigns `var(--nc-chart-1..6)` in fixed order by FIRST APPEARANCE of each
|
|
25
|
+
* stable entity id. Duplicate ids keep their first assignment. Entities past
|
|
26
|
+
* the sixth unique id fold to the neutral "other" register.
|
|
27
|
+
*/
|
|
28
|
+
declare function createEntityColorMap(entityIds: readonly string[]): EntityColorMap;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves an entity's assigned color. Ids missing from the map (an entity the
|
|
31
|
+
* page context never registered) read as the neutral "other" register instead
|
|
32
|
+
* of silently minting a new hue.
|
|
33
|
+
*/
|
|
34
|
+
declare function entityColor(map: EntityColorMap, entityId: string): string;
|
|
35
|
+
export { CHART_ENTITY_COLOR_TOKENS, CHART_ENTITY_OTHER_COLOR, createEntityColorMap, entityColor, };
|
|
36
|
+
export type { EntityColorMap };
|
|
37
|
+
//# sourceMappingURL=chart-entity-colors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-entity-colors.d.ts","sourceRoot":"","sources":["../../src/components/chart-entity-colors.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,QAAA,MAAM,yBAAyB,yIAOrB,CAAA;AAEV;;;GAGG;AACH,QAAA,MAAM,wBAAwB,4BAA4B,CAAA;AAE1D,KAAK,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAEtD;;;;GAIG;AACH,iBAAS,oBAAoB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CA2B1E;AAED;;;;GAIG;AACH,iBAAS,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMlE;AAED,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,WAAW,GACZ,CAAA;AACD,YAAY,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chart-entity-colors — the entity-color assignment helper for the console
|
|
3
|
+
* chart kit (issue #166; DESIGN.md "Entity-color assignment").
|
|
4
|
+
*
|
|
5
|
+
* Categorical hues assign in fixed order to entities per page context, and the
|
|
6
|
+
* assignment follows the entity identity, never rank, sort position, or array
|
|
7
|
+
* index. Callers build ONE map from the full page-context entity list (not a
|
|
8
|
+
* filtered or re-sorted view) and pass it to every chart widget on that
|
|
9
|
+
* surface, so a filter or re-sort never repaints the entities that remain on
|
|
10
|
+
* screen. Beyond six entities the tail folds into a single neutral "other"
|
|
11
|
+
* register, never a seventh hue.
|
|
12
|
+
*
|
|
13
|
+
* The map is a plain serializable record so a Server Component can compute it
|
|
14
|
+
* and pass it across the RSC boundary to client chart widgets.
|
|
15
|
+
*/
|
|
16
|
+
const CHART_ENTITY_COLOR_TOKENS = [
|
|
17
|
+
"var(--nc-chart-1)",
|
|
18
|
+
"var(--nc-chart-2)",
|
|
19
|
+
"var(--nc-chart-3)",
|
|
20
|
+
"var(--nc-chart-4)",
|
|
21
|
+
"var(--nc-chart-5)",
|
|
22
|
+
"var(--nc-chart-6)",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* The neutral register for folded "everything else" entities. A muted ink
|
|
26
|
+
* token, never an extra categorical hue.
|
|
27
|
+
*/
|
|
28
|
+
const CHART_ENTITY_OTHER_COLOR = "var(--muted-foreground)";
|
|
29
|
+
/**
|
|
30
|
+
* Assigns `var(--nc-chart-1..6)` in fixed order by FIRST APPEARANCE of each
|
|
31
|
+
* stable entity id. Duplicate ids keep their first assignment. Entities past
|
|
32
|
+
* the sixth unique id fold to the neutral "other" register.
|
|
33
|
+
*/
|
|
34
|
+
function createEntityColorMap(entityIds) {
|
|
35
|
+
// Assign through a Map so ids that collide with Object.prototype members
|
|
36
|
+
// ("toString", "__proto__", "constructor") key and dedupe safely during the
|
|
37
|
+
// build — a Map never confuses an entity id with an inherited prototype
|
|
38
|
+
// method, and never triggers the `__proto__` accessor.
|
|
39
|
+
const assignments = new Map();
|
|
40
|
+
for (const entityId of entityIds) {
|
|
41
|
+
if (assignments.has(entityId)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
assignments.set(entityId, assignments.size < CHART_ENTITY_COLOR_TOKENS.length
|
|
45
|
+
? CHART_ENTITY_COLOR_TOKENS[assignments.size]
|
|
46
|
+
: CHART_ENTITY_OTHER_COLOR);
|
|
47
|
+
}
|
|
48
|
+
// Return a PLAIN, serializable object so a Server Component can compute the
|
|
49
|
+
// map and pass it across the RSC boundary to client chart widgets (React
|
|
50
|
+
// rejects a null-prototype object). `Object.fromEntries` writes OWN data
|
|
51
|
+
// properties, so an id like "__proto__" becomes an own key with its hue
|
|
52
|
+
// instead of mutating the object's prototype — and `entityColor` reads
|
|
53
|
+
// membership with `Object.hasOwn`, so inherited members never leak.
|
|
54
|
+
return Object.fromEntries(assignments);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves an entity's assigned color. Ids missing from the map (an entity the
|
|
58
|
+
* page context never registered) read as the neutral "other" register instead
|
|
59
|
+
* of silently minting a new hue.
|
|
60
|
+
*/
|
|
61
|
+
function entityColor(map, entityId) {
|
|
62
|
+
// Object.hasOwn, not `map[entityId] ??`: the map is serialized across the RSC
|
|
63
|
+
// boundary and rehydrates with Object.prototype, so a bare index on an id like
|
|
64
|
+
// "toString" or "constructor" would return an inherited method instead of
|
|
65
|
+
// folding to the neutral register.
|
|
66
|
+
return Object.hasOwn(map, entityId) ? map[entityId] : CHART_ENTITY_OTHER_COLOR;
|
|
67
|
+
}
|
|
68
|
+
export { CHART_ENTITY_COLOR_TOKENS, CHART_ENTITY_OTHER_COLOR, createEntityColorMap, entityColor, };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type DeltaDirection } from "./chart-delta";
|
|
3
|
+
/**
|
|
4
|
+
* chart-kpi-stat — the package KPI stat for the console chart kit
|
|
5
|
+
* (issue #166). The portal's KpiTile stays app-owned; this is the package
|
|
6
|
+
* sibling that adds the contracted delta and sparkline behavior.
|
|
7
|
+
*
|
|
8
|
+
* The value renders semibold with proportional figures (no tabular-nums —
|
|
9
|
+
* a single stat is read, not columnar-scanned). The delta reads as a
|
|
10
|
+
* direction glyph plus tone token with a named comparison period
|
|
11
|
+
* ("vs {vsLabel}"), per DESIGN.md "Delta and trend encoding". The sparkline
|
|
12
|
+
* slot is decorative here: the stat carries the accessible name, so the slot
|
|
13
|
+
* wrapper is `aria-hidden`.
|
|
14
|
+
*/
|
|
15
|
+
type KpiStatDelta = {
|
|
16
|
+
/** The formatted change, e.g. "+12%". */
|
|
17
|
+
value: React.ReactNode;
|
|
18
|
+
direction: DeltaDirection;
|
|
19
|
+
/** Whether an upward move is good for this KPI. Defaults to true. */
|
|
20
|
+
upIsGood?: boolean;
|
|
21
|
+
/** The named comparison period, e.g. "previous 30 days". */
|
|
22
|
+
vsLabel: string;
|
|
23
|
+
};
|
|
24
|
+
declare function KpiStat({ label, value, delta, sparkline, className, ...props }: Omit<React.ComponentProps<"div">, "children"> & {
|
|
25
|
+
label: React.ReactNode;
|
|
26
|
+
value: React.ReactNode;
|
|
27
|
+
delta?: KpiStatDelta;
|
|
28
|
+
/** Optional decorative sparkline slot (e.g. a `Sparkline` element). */
|
|
29
|
+
sparkline?: React.ReactNode;
|
|
30
|
+
}): React.JSX.Element;
|
|
31
|
+
export { KpiStat };
|
|
32
|
+
export type { KpiStatDelta };
|
|
33
|
+
//# sourceMappingURL=chart-kpi-stat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-kpi-stat.d.ts","sourceRoot":"","sources":["../../src/components/chart-kpi-stat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAS,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAErE;;;;;;;;;;;GAWG;AAEH,KAAK,YAAY,GAAG;IAClB,yCAAyC;IACzC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,SAAS,EAAE,cAAc,CAAA;IACzB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,iBAAS,OAAO,CAAC,EACf,KAAK,EACL,KAAK,EACL,KAAK,EACL,SAAS,EACT,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAAG;IACjD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,uEAAuE;IACvE,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B,qBAoCA;AAED,OAAO,EAAE,OAAO,EAAE,CAAA;AAClB,YAAY,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { Delta } from "./chart-delta.js";
|
|
4
|
+
function KpiStat({ label, value, delta, sparkline, className, ...props }) {
|
|
5
|
+
return (_jsxs("div", { "data-slot": "kpi-stat", className: cn("flex flex-col gap-1.5", className), ...props, children: [_jsx("div", { "data-slot": "kpi-stat-label", className: "font-mono text-xs text-muted-foreground uppercase", children: label }), _jsxs("div", { className: "flex items-end justify-between gap-3", children: [_jsx("div", { "data-slot": "kpi-stat-value", className: "text-3xl leading-none font-semibold text-foreground", children: value }), sparkline ? (_jsx("div", { "data-slot": "kpi-stat-sparkline", "aria-hidden": "true", children: sparkline })) : null] }), delta ? (_jsx(Delta, { value: delta.value, direction: delta.direction, upIsGood: delta.upIsGood, vsLabel: delta.vsLabel })) : null] }));
|
|
6
|
+
}
|
|
7
|
+
export { KpiStat };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* chart-sparkline — hand-rolled SVG sparkline for the console chart kit
|
|
4
|
+
* (issue #166; ADR 0010 D1 sparkline carve-out; DESIGN.md "Delta and trend
|
|
5
|
+
* encoding", "Mark specs").
|
|
6
|
+
*
|
|
7
|
+
* Sparklines normalize against a meaningful baseline, never the visible
|
|
8
|
+
* min–max span alone: the vertical domain always includes the provided
|
|
9
|
+
* baseline, so a flat-ish series near a distant baseline reads as flat, not
|
|
10
|
+
* as dramatic noise. Marks render in real pixel space (uniform viewBox, no
|
|
11
|
+
* non-uniform scaling), single stroke of at least 2px with round joins/caps.
|
|
12
|
+
*
|
|
13
|
+
* When rendered inside a named stat (KpiStat), the sparkline is decorative
|
|
14
|
+
* and stays `aria-hidden`; when standalone, pass `label` so it carries its
|
|
15
|
+
* own accessible name. A standalone sparkline also renders a visually hidden
|
|
16
|
+
* data alternative next to the `role="img"` svg — the point values in order —
|
|
17
|
+
* so the trend is not vision-only. The decorative (aria-hidden) variant emits
|
|
18
|
+
* no such list.
|
|
19
|
+
*/
|
|
20
|
+
type SparklinePoint = {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Baseline-anchored normalization: the vertical domain is
|
|
26
|
+
* [min(baseline, data), max(baseline, data)] — never the visible data
|
|
27
|
+
* min–max alone. Points are inset by half the stroke width so the stroke
|
|
28
|
+
* never clips at the edges. A zero-span domain centers the line vertically.
|
|
29
|
+
*/
|
|
30
|
+
declare function normalizeSparklinePoints(data: readonly number[], baseline: number, width: number, height: number, strokeWidth?: number): SparklinePoint[];
|
|
31
|
+
declare function Sparkline({ data, baseline, label, width, height, stroke, strokeWidth, className, ...props }: Omit<React.ComponentProps<"svg">, "children" | "height" | "viewBox" | "width"> & {
|
|
32
|
+
data: readonly number[];
|
|
33
|
+
/** The meaningful comparison baseline the vertical domain anchors to. */
|
|
34
|
+
baseline: number;
|
|
35
|
+
/** Accessible name for standalone use; omit inside a named stat. */
|
|
36
|
+
label?: string;
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
/** Stroke token: ink or one chart token, never a raw color. */
|
|
40
|
+
stroke?: string;
|
|
41
|
+
strokeWidth?: number;
|
|
42
|
+
}): React.JSX.Element;
|
|
43
|
+
export { Sparkline, normalizeSparklinePoints };
|
|
44
|
+
export type { SparklinePoint };
|
|
45
|
+
//# sourceMappingURL=chart-sparkline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-sparkline.d.ts","sourceRoot":"","sources":["../../src/components/chart-sparkline.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B;;;;;;;;;;;;;;;;;GAiBG;AAEH,KAAK,cAAc,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C;;;;;GAKG;AACH,iBAAS,wBAAwB,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,SAAI,GACd,cAAc,EAAE,CAqBlB;AAED,iBAAS,SAAS,CAAC,EACjB,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,KAAW,EACX,MAAW,EACX,MAA4B,EAC5B,WAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CACL,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAC3B,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAC5C,GAAG;IACF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,qBAqDA;AAED,OAAO,EAAE,SAAS,EAAE,wBAAwB,EAAE,CAAA;AAC9C,YAAY,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Baseline-anchored normalization: the vertical domain is
|
|
5
|
+
* [min(baseline, data), max(baseline, data)] — never the visible data
|
|
6
|
+
* min–max alone. Points are inset by half the stroke width so the stroke
|
|
7
|
+
* never clips at the edges. A zero-span domain centers the line vertically.
|
|
8
|
+
*/
|
|
9
|
+
function normalizeSparklinePoints(data, baseline, width, height, strokeWidth = 2) {
|
|
10
|
+
if (data.length === 0) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const min = Math.min(baseline, ...data);
|
|
14
|
+
const max = Math.max(baseline, ...data);
|
|
15
|
+
const span = max - min;
|
|
16
|
+
const pad = strokeWidth / 2;
|
|
17
|
+
const usableWidth = Math.max(width - strokeWidth, 0);
|
|
18
|
+
const usableHeight = Math.max(height - strokeWidth, 0);
|
|
19
|
+
return data.map((value, index) => ({
|
|
20
|
+
x: pad +
|
|
21
|
+
(data.length === 1
|
|
22
|
+
? usableWidth / 2
|
|
23
|
+
: (index / (data.length - 1)) * usableWidth),
|
|
24
|
+
y: span === 0 ? height / 2 : pad + (1 - (value - min) / span) * usableHeight,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function Sparkline({ data, baseline, label, width = 120, height = 32, stroke = "var(--nc-chart-1)", strokeWidth = 2, className, ...props }) {
|
|
28
|
+
// The 2px minimum is a DESIGN.md contract floor ("single stroke of at least
|
|
29
|
+
// 2px"); clamp the public prop so a caller passing a thinner stroke still
|
|
30
|
+
// renders at the floor and the normalization inset matches the painted line.
|
|
31
|
+
const effectiveStrokeWidth = Math.max(strokeWidth, 2);
|
|
32
|
+
const points = normalizeSparklinePoints(data, baseline, width, height, effectiveStrokeWidth);
|
|
33
|
+
const svg = (_jsx("svg", { "data-slot": "sparkline", width: width, height: height, viewBox: `0 0 ${width} ${height}`, className: cn("shrink-0", className), ...(label
|
|
34
|
+
? { role: "img", "aria-label": label }
|
|
35
|
+
: { "aria-hidden": true }), ...props, children: points.length >= 2 ? (_jsx("polyline", { fill: "none", stroke: stroke, strokeWidth: effectiveStrokeWidth, strokeLinejoin: "round", strokeLinecap: "round", points: points.map((point) => `${point.x},${point.y}`).join(" ") })) : null }));
|
|
36
|
+
// Decorative inside a named stat: emit the bare svg, no data twin.
|
|
37
|
+
if (!label) {
|
|
38
|
+
return svg;
|
|
39
|
+
}
|
|
40
|
+
// Standalone: pair the graphic with a visually hidden ordered value list so
|
|
41
|
+
// the trend is available to assistive tech, not vision-only.
|
|
42
|
+
return (_jsxs("span", { "data-slot": "sparkline-standalone", className: "inline-flex", children: [svg, _jsx("span", { "data-slot": "sparkline-values", className: "sr-only", children: `${label}: ${data.join(", ")}` })] }));
|
|
43
|
+
}
|
|
44
|
+
export { Sparkline, normalizeSparklinePoints };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type EntityColorMap } from "./chart-entity-colors";
|
|
3
|
+
type TimeSeriesSeries = {
|
|
4
|
+
/** Stable entity id — the data key in each datum and the color-map key. */
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
type TimeSeriesDatum = Record<string, number | string>;
|
|
9
|
+
type TimeSeriesProps = Omit<React.ComponentProps<"figure">, "children"> & {
|
|
10
|
+
data: readonly TimeSeriesDatum[];
|
|
11
|
+
/** The datum key carrying the period (e.g. the day). */
|
|
12
|
+
xKey: string;
|
|
13
|
+
series: readonly TimeSeriesSeries[];
|
|
14
|
+
/** The shared page-context entity-color assignment map. */
|
|
15
|
+
entityColorMap: EntityColorMap;
|
|
16
|
+
/** REQUIRED accessible name for the chart and its table alternative. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Column header for the period column in the table alternative. */
|
|
19
|
+
xLabel?: string;
|
|
20
|
+
valueFormatter?: (value: number) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Y-axis width. Defaults to `"auto"` (recharts 3.8 sizes the gutter to the
|
|
23
|
+
* formatted ticks); pass a number to pin it.
|
|
24
|
+
*/
|
|
25
|
+
yAxisWidth?: number;
|
|
26
|
+
/** Label for the folded neutral "Other" series (entities past the sixth). */
|
|
27
|
+
otherLabel?: string;
|
|
28
|
+
};
|
|
29
|
+
declare function TimeSeriesBars({ data, xKey, series, entityColorMap, label, xLabel, valueFormatter, yAxisWidth, otherLabel, className, ...props }: TimeSeriesProps): React.JSX.Element;
|
|
30
|
+
declare function TimeSeriesArea({ data, xKey, series, entityColorMap, label, xLabel, valueFormatter, yAxisWidth, otherLabel, className, ...props }: TimeSeriesProps): React.JSX.Element;
|
|
31
|
+
export { TimeSeriesArea, TimeSeriesBars };
|
|
32
|
+
export type { TimeSeriesDatum, TimeSeriesProps, TimeSeriesSeries };
|
|
33
|
+
//# sourceMappingURL=chart-time-series.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-time-series.d.ts","sourceRoot":"","sources":["../../src/components/chart-time-series.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqB9B,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kCAAkC,CAAA;AAsDzC,KAAK,gBAAgB,GAAG;IACtB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,KAAK,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;AAEtD,KAAK,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,GAAG;IACxE,IAAI,EAAE,SAAS,eAAe,EAAE,CAAA;IAChC,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACnC,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAA;IAC9B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAC1C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAiKD,iBAAS,cAAc,CAAC,EACtB,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,cAAc,EACd,KAAK,EACL,MAAiB,EACjB,cAA8B,EAC9B,UAAU,EACV,UAAoB,EACpB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,eAAe,qBAuDjB;AAED,iBAAS,cAAc,CAAC,EACtB,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,cAAc,EACd,KAAK,EACL,MAAiB,EACjB,cAA8B,EAC9B,UAAU,EACV,UAAoB,EACpB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,eAAe,qBAgEjB;AAED,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAA;AACzC,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAA"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Area, AreaChart, Bar, BarChart, BarStack, CartesianGrid, XAxis, YAxis, } from "recharts";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from "./chart.js";
|
|
6
|
+
import { CHART_ENTITY_OTHER_COLOR, entityColor, } from "./chart-entity-colors.js";
|
|
7
|
+
/**
|
|
8
|
+
* chart-time-series — daily stacked time-series charts for the console chart
|
|
9
|
+
* kit (issue #166), built on the ADR 0010 Recharts foundation
|
|
10
|
+
* (ChartContainer / ChartTooltip / ChartLegend; chart.tsx untouched).
|
|
11
|
+
*
|
|
12
|
+
* Contract highlights (DESIGN.md "Mark specs", "Legends and labels"):
|
|
13
|
+
* - A y-axis is ALWAYS present — never axisless stacks.
|
|
14
|
+
* - Stacked segments keep a surface gap; the whole stack is wrapped in a
|
|
15
|
+
* Recharts `BarStack` so the 4px rounded data-ends sit on the visible top of
|
|
16
|
+
* the stack (anchored square at the baseline) even when the last series is
|
|
17
|
+
* zero-valued — the radius rounds the stack, not one hard-coded series.
|
|
18
|
+
* - The public `valueFormatter` reaches every value surface — the table twin,
|
|
19
|
+
* the y-axis ticks, and the tooltip value — not only the hidden table cells.
|
|
20
|
+
* - Series colors resolve from the shared page-context entity-color map,
|
|
21
|
+
* never from array index.
|
|
22
|
+
* - Direct labels are not feasible on stacked dailies, so at two or more
|
|
23
|
+
* series a legend identifies every series; a single series is titled by
|
|
24
|
+
* the accessible name, not legended.
|
|
25
|
+
* - Every chart names a built-in table alternative: a visually hidden
|
|
26
|
+
* `<table>` twin renders the same series, and the chart root carries the
|
|
27
|
+
* accessible name. Recharts' `accessibilityLayer` (ARIA + arrow-key data
|
|
28
|
+
* navigation) stays enabled, and the focusable chart svg carries its own
|
|
29
|
+
* `title` so the accessibility tree names the graphic, not just the figure.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The stock `ChartContainer` suppresses the Recharts svg outline
|
|
33
|
+
* (`[&_.recharts-surface]:outline-hidden`) so the default browser focus ring
|
|
34
|
+
* never shows, which would leave the `accessibilityLayer` keyboard path with
|
|
35
|
+
* no visible focus. chart.tsx is untouched, so we restore a tokenized ring
|
|
36
|
+
* through the `className` we pass to our own `ChartContainer` instances: the
|
|
37
|
+
* base rule sets `outline: 2px solid transparent` on `.recharts-surface`, and
|
|
38
|
+
* these `:focus-visible` overrides sit on a distinct arbitrary-variant selector
|
|
39
|
+
* (a different tailwind-merge group AND modifier, so `cn()` keeps both) with
|
|
40
|
+
* higher CSS specificity, flipping the outline color to the `--ring` role only
|
|
41
|
+
* while the svg is keyboard-focused.
|
|
42
|
+
*/
|
|
43
|
+
const CHART_SURFACE_FOCUS_CLASS = "[&_.recharts-surface:focus-visible]:outline-2 " +
|
|
44
|
+
"[&_.recharts-surface:focus-visible]:outline-ring " +
|
|
45
|
+
"[&_.recharts-surface:focus-visible]:outline-offset-2";
|
|
46
|
+
/**
|
|
47
|
+
* Synthetic id/key for the single aggregated neutral series. Entities past the
|
|
48
|
+
* sixth resolve to the neutral register, and rendering each as its own
|
|
49
|
+
* indistinguishable neutral mark is unreadable, so they fold into ONE "Other"
|
|
50
|
+
* series (values summed per bucket) in the marks, the legend, and the table
|
|
51
|
+
* twin. Unknown to any entity-color map, so `entityColor` resolves it to the
|
|
52
|
+
* neutral register.
|
|
53
|
+
*/
|
|
54
|
+
const OTHER_SERIES_ID = "__nc-other-series__";
|
|
55
|
+
function defaultFormat(value) {
|
|
56
|
+
return value.toLocaleString("en-US");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Route the public `valueFormatter` through the visible tooltip, not only the
|
|
60
|
+
* table twin. The stock shadcn tooltip takes a full-row `formatter`, so this
|
|
61
|
+
* re-renders the stock row shape — entity swatch, series label, formatted
|
|
62
|
+
* value — with the formatter applied to the number.
|
|
63
|
+
*/
|
|
64
|
+
function tooltipRowFormatter(series, valueFormatter) {
|
|
65
|
+
const labelById = new Map(series.map((entry) => [entry.id, entry.label]));
|
|
66
|
+
return (value, name, item) => (_jsxs(_Fragment, { children: [_jsx("span", { "data-slot": "time-series-tooltip-swatch", className: "size-2.5 shrink-0 rounded-[2px]", style: { backgroundColor: item.color ?? item.payload?.fill } }), _jsxs("span", { className: "flex flex-1 items-center justify-between gap-2 leading-none", children: [_jsx("span", { className: "text-muted-foreground", children: labelById.get(String(name)) ?? name }), _jsx("span", { className: "font-mono font-medium text-foreground tabular-nums", children: typeof value === "number" ? valueFormatter(value) : String(value) })] })] }));
|
|
67
|
+
}
|
|
68
|
+
function seriesChartConfig(series, entityColorMap) {
|
|
69
|
+
return Object.fromEntries(series.map((entry) => [
|
|
70
|
+
entry.id,
|
|
71
|
+
{ label: entry.label, color: entityColor(entityColorMap, entry.id) },
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fold every series whose entity color resolves to the neutral "other"
|
|
76
|
+
* register into ONE aggregated "Other" series (values summed per bucket).
|
|
77
|
+
* Beyond six entities the color map returns the neutral register, and drawing
|
|
78
|
+
* each tail entity as its own indistinguishable neutral mark is unreadable —
|
|
79
|
+
* so the marks, legend, and table twin all show at most one neutral series.
|
|
80
|
+
* Series resolving to a hue pass through unchanged; with no neutral series the
|
|
81
|
+
* inputs return untouched (so ≤6 series are never rewritten).
|
|
82
|
+
*/
|
|
83
|
+
function foldNeutralSeries(series, entityColorMap, data, otherLabel) {
|
|
84
|
+
const isNeutral = (entry) => entityColor(entityColorMap, entry.id) === CHART_ENTITY_OTHER_COLOR;
|
|
85
|
+
const neutral = series.filter(isNeutral);
|
|
86
|
+
if (neutral.length === 0) {
|
|
87
|
+
return { series: [...series], data: [...data] };
|
|
88
|
+
}
|
|
89
|
+
const hued = series.filter((entry) => !isNeutral(entry));
|
|
90
|
+
const foldedSeries = [
|
|
91
|
+
...hued,
|
|
92
|
+
{ id: OTHER_SERIES_ID, label: otherLabel },
|
|
93
|
+
];
|
|
94
|
+
const foldedData = data.map((datum) => {
|
|
95
|
+
const otherSum = neutral.reduce((sum, entry) => {
|
|
96
|
+
const value = datum[entry.id];
|
|
97
|
+
return sum + (typeof value === "number" ? value : 0);
|
|
98
|
+
}, 0);
|
|
99
|
+
const next = { ...datum };
|
|
100
|
+
for (const entry of neutral) {
|
|
101
|
+
delete next[entry.id];
|
|
102
|
+
}
|
|
103
|
+
next[OTHER_SERIES_ID] = otherSum;
|
|
104
|
+
return next;
|
|
105
|
+
});
|
|
106
|
+
return { series: foldedSeries, data: foldedData };
|
|
107
|
+
}
|
|
108
|
+
function TimeSeriesTableAlternative({ data, xKey, xLabel, series, label, valueFormatter, }) {
|
|
109
|
+
return (_jsxs("table", { className: "sr-only", children: [_jsx("caption", { className: "sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: xLabel }), series.map((entry) => (_jsx("th", { scope: "col", children: entry.label }, entry.id)))] }) }), _jsx("tbody", { children: data.map((datum, index) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: String(datum[xKey] ?? "") }), series.map((entry) => {
|
|
110
|
+
const value = datum[entry.id];
|
|
111
|
+
return (_jsx("td", { children: typeof value === "number"
|
|
112
|
+
? valueFormatter(value)
|
|
113
|
+
: String(value ?? "") }, entry.id));
|
|
114
|
+
})] }, index))) })] }));
|
|
115
|
+
}
|
|
116
|
+
function TimeSeriesFrame({ label, className, children, ...props }) {
|
|
117
|
+
return (_jsx("figure", { "aria-label": label, className: cn("m-0 flex w-full flex-col", className), ...props, children: children }));
|
|
118
|
+
}
|
|
119
|
+
function TimeSeriesBars({ data, xKey, series, entityColorMap, label, xLabel = "Period", valueFormatter = defaultFormat, yAxisWidth, otherLabel = "Other", className, ...props }) {
|
|
120
|
+
const folded = foldNeutralSeries(series, entityColorMap, data, otherLabel);
|
|
121
|
+
const config = seriesChartConfig(folded.series, entityColorMap);
|
|
122
|
+
return (_jsxs(TimeSeriesFrame, { "data-slot": "time-series-bars", label: label, className: className, ...props, children: [_jsx(ChartContainer, { config: config, className: cn("w-full", CHART_SURFACE_FOCUS_CLASS), children: _jsxs(BarChart, { accessibilityLayer: true, title: label, data: folded.data, children: [_jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: xKey, tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(YAxis, { tickLine: false, axisLine: false, width: yAxisWidth ?? "auto", tickFormatter: (value) => valueFormatter(Number(value)) }), _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { formatter: tooltipRowFormatter(folded.series, valueFormatter) }) }), folded.series.length > 1 ? (_jsx(ChartLegend, { content: _jsx(ChartLegendContent, {}) })) : null, _jsx(BarStack, { stackId: "stacked", radius: [4, 4, 0, 0], children: folded.series.map((entry) => (_jsx(Bar, { dataKey: entry.id, maxBarSize: 24, fill: entityColor(entityColorMap, entry.id), stroke: "var(--background)", strokeWidth: 2 }, entry.id))) })] }) }), _jsx(TimeSeriesTableAlternative, { data: folded.data, xKey: xKey, xLabel: xLabel, series: folded.series, label: label, valueFormatter: valueFormatter })] }));
|
|
123
|
+
}
|
|
124
|
+
function TimeSeriesArea({ data, xKey, series, entityColorMap, label, xLabel = "Period", valueFormatter = defaultFormat, yAxisWidth, otherLabel = "Other", className, ...props }) {
|
|
125
|
+
const folded = foldNeutralSeries(series, entityColorMap, data, otherLabel);
|
|
126
|
+
const config = seriesChartConfig(folded.series, entityColorMap);
|
|
127
|
+
return (_jsxs(TimeSeriesFrame, { "data-slot": "time-series-area", label: label, className: className, ...props, children: [_jsx(ChartContainer, { config: config, className: cn("w-full", CHART_SURFACE_FOCUS_CLASS), children: _jsxs(AreaChart, { accessibilityLayer: true, title: label, data: folded.data, children: [_jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: xKey, tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(YAxis, { tickLine: false, axisLine: false, width: yAxisWidth ?? "auto", tickFormatter: (value) => valueFormatter(Number(value)) }), _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { formatter: tooltipRowFormatter(folded.series, valueFormatter) }) }), folded.series.length > 1 ? (_jsx(ChartLegend, { content: _jsx(ChartLegendContent, {}) })) : null, folded.series.map((entry) => {
|
|
128
|
+
const color = entityColor(entityColorMap, entry.id);
|
|
129
|
+
return (_jsx(Area, { type: "monotone", dataKey: entry.id, stackId: "stacked", stroke: color, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", fill: color, fillOpacity: 0.1,
|
|
130
|
+
// Recharts' default activeDot ring is stroke #fff, which the
|
|
131
|
+
// chart foundation remaps to transparent — an explicit surface
|
|
132
|
+
// ring keeps the DESIGN.md 2px marker-ring spec on hover/focus.
|
|
133
|
+
activeDot: { r: 4, stroke: "var(--background)", strokeWidth: 2 } }, entry.id));
|
|
134
|
+
})] }) }), _jsx(TimeSeriesTableAlternative, { data: folded.data, xKey: xKey, xLabel: xLabel, series: folded.series, label: label, valueFormatter: valueFormatter })] }));
|
|
135
|
+
}
|
|
136
|
+
export { TimeSeriesArea, TimeSeriesBars };
|
package/dist/components/chart.js
CHANGED
|
@@ -94,7 +94,7 @@ function ChartTooltipContent({ active, payload, className, indicator = "dot", hi
|
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
96
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
|
97
|
-
return (_jsxs("div", { className: cn("grid min-w-32 items-start gap-1.5 rounded-
|
|
97
|
+
return (_jsxs("div", { className: cn("grid min-w-32 items-start gap-1.5 rounded-md border border-border bg-popover px-nadicode-sm py-nadicode-filter-chip-gap text-xs text-popover-foreground shadow-tooltip", className), children: [!nestLabel ? tooltipLabel : null, _jsx("div", { className: "grid gap-1.5", children: payload
|
|
98
98
|
.filter((item) => item.type !== "none")
|
|
99
99
|
.map((item, index) => {
|
|
100
100
|
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`;
|
|
@@ -4,6 +4,6 @@ import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { CheckIcon } from "lucide-react";
|
|
6
6
|
function Checkbox({ className, ...props }) {
|
|
7
|
-
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer relative flex size-4 shrink-0 items-center justify-center rounded-
|
|
7
|
+
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer relative flex size-4 shrink-0 items-center justify-center rounded-md border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:top-1/2 after:left-1/2 after:size-touch-target after:-translate-x-1/2 after:-translate-y-1/2 focus-visible:border-ring focus-visible:ring-focus focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-focus aria-invalid:ring-destructive aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground [@media(hover:hover)_and_(pointer:fine)]:after:hidden", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) }) }));
|
|
8
8
|
}
|
|
9
9
|
export { Checkbox };
|