@nadicodeai/ui 6.0.1 → 6.1.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/dist/components/action-menu.d.ts +8 -0
- package/dist/components/action-menu.d.ts.map +1 -0
- package/dist/components/action-menu.js +8 -0
- package/dist/components/card.d.ts +1 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +5 -3
- package/dist/components/chart-breakdown-table.d.ts +3 -2
- package/dist/components/chart-breakdown-table.d.ts.map +1 -1
- package/dist/components/chart-breakdown-table.js +13 -9
- package/dist/components/chart-bullet-bar.d.ts +13 -12
- package/dist/components/chart-bullet-bar.d.ts.map +1 -1
- package/dist/components/chart-bullet-bar.js +16 -36
- package/dist/components/chart-kpi-stat.d.ts +26 -15
- package/dist/components/chart-kpi-stat.d.ts.map +1 -1
- package/dist/components/chart-kpi-stat.js +28 -4
- package/dist/components/chart-time-series-layout.d.ts +6 -0
- package/dist/components/chart-time-series-layout.d.ts.map +1 -0
- package/dist/components/chart-time-series-layout.js +6 -0
- package/dist/components/chart-time-series.d.ts +6 -2
- package/dist/components/chart-time-series.d.ts.map +1 -1
- package/dist/components/chart-time-series.js +27 -9
- package/dist/components/chart.js +1 -1
- package/dist/components/collection-filters.d.ts +28 -0
- package/dist/components/collection-filters.d.ts.map +1 -0
- package/dist/components/collection-filters.js +54 -0
- package/dist/components/collection-scope.d.ts +15 -0
- package/dist/components/collection-scope.d.ts.map +1 -0
- package/dist/components/collection-scope.js +7 -0
- package/dist/components/collection-toolbar.d.ts +10 -0
- package/dist/components/collection-toolbar.d.ts.map +1 -0
- package/dist/components/collection-toolbar.js +4 -0
- package/dist/components/settings-facts.d.ts +28 -0
- package/dist/components/settings-facts.d.ts.map +1 -0
- package/dist/components/settings-facts.js +7 -0
- package/dist/components/settings-section.d.ts +5 -5
- package/dist/components/settings-section.d.ts.map +1 -1
- package/dist/components/settings-section.js +6 -11
- package/dist/components/sidebar.d.ts +4 -1
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +3 -3
- package/dist/components/table-layouts.d.ts +22 -0
- package/dist/components/table-layouts.d.ts.map +1 -0
- package/dist/components/table-layouts.js +20 -0
- package/dist/components/table.d.ts +15 -2
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +8 -4
- package/dist/eslint/rules/jsx-class-utility.js +44 -12
- package/dist/eslint/rules/no-resting-settings-control.js +15 -13
- package/dist/eslint/rules/no-shadcn-appearance-override.js +189 -36
- package/dist/eslint/rules/shadcn-composition.js +48 -3
- package/dist/hooks/use-confirm-action.d.ts +0 -6
- package/dist/hooks/use-confirm-action.d.ts.map +1 -1
- package/dist/hooks/use-confirm-action.js +5 -12
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/lib/use-copy-to-clipboard.d.ts +0 -41
- package/dist/lib/use-copy-to-clipboard.d.ts.map +1 -1
- package/dist/lib/use-copy-to-clipboard.js +6 -15
- package/docs/component-classification.md +3 -2
- package/docs/contract.md +79 -8
- package/docs/product-compositions.md +14 -11
- package/package.json +3 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ActionMenuProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
triggerId?: string;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
export declare function ActionMenu({ label, triggerId, children }: ActionMenuProps): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=action-menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-menu.d.ts","sourceRoot":"","sources":["../../src/components/action-menu.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,eAAe,+BAazE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { EllipsisIcon } from "lucide-react";
|
|
4
|
+
import { Button } from "./button.js";
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "./dropdown-menu.js";
|
|
6
|
+
export function ActionMenu({ label, triggerId, children }) {
|
|
7
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { id: triggerId, "aria-label": label, render: _jsx(Button, { variant: "ghost", size: "icon-sm" }), children: _jsx(EllipsisIcon, { "aria-hidden": "true" }) }), _jsx(DropdownMenuContent, { align: "end", children: children })] }));
|
|
8
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
-
type CardSurface = "plain" | "resting" | "raised";
|
|
3
|
+
type CardSurface = "plain" | "resting" | "raised" | "outlined";
|
|
4
4
|
type CardVariant = "default" | "interactive" | "link";
|
|
5
5
|
declare function Card({ className, render, size, surface, variant, ...props }: useRender.ComponentProps<"div"> & {
|
|
6
6
|
size?: "default" | "sm";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../src/components/card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAItD,KAAK,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../src/components/card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAItD,KAAK,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;AAC/D,KAAK,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAC;AAEtD,iBAAS,IAAI,CAAC,EACZ,SAAS,EACT,MAAM,EACN,IAAgB,EAChB,OAAmB,EACnB,OAAmB,EACnB,GAAG,KAAK,EACT,EAAE,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG;IACnC,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,0EAsBA;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,SAAS,CAAC,EACjB,SAAS,EACT,WAAmB,EACnB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,qBAazD;AAED,iBAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQ5E;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQvE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAIxE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQvE;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,CAAC"}
|
package/dist/components/card.js
CHANGED
|
@@ -3,11 +3,13 @@ import { mergeProps } from "@base-ui/react/merge-props";
|
|
|
3
3
|
import { useRender } from "@base-ui/react/use-render";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
function Card({ className, render, size = "default", surface = "resting", variant = "default", ...props }) {
|
|
6
|
+
const structure = cn("group/card flex flex-col gap-(--card-spacing) overflow-hidden bg-card py-(--card-spacing) text-sm text-card-foreground [--card-spacing:var(--spacing-nadicode-lg)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:var(--spacing-nadicode-md)] data-[size=sm]:has-data-[slot=card-footer]:pb-0", variant === "interactive" && "cursor-pointer", variant === "link" && "nc-transition-paint");
|
|
7
|
+
const surfaceClassName = surface === "outlined"
|
|
8
|
+
? cn(structure, "rounded-lg border border-border", className)
|
|
9
|
+
: cn(structure, "rounded-3xl border-0 *:[img:first-child]:rounded-t-3xl *:[img:last-child]:rounded-b-3xl", surface === "resting" && "nc-elevation-resting", surface === "raised" && "nc-elevation-raised", className);
|
|
6
10
|
return useRender({
|
|
7
11
|
defaultTagName: "div",
|
|
8
|
-
props: mergeProps({
|
|
9
|
-
className: cn("group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-3xl border-0 bg-card py-(--card-spacing) text-sm text-card-foreground [--card-spacing:var(--spacing-nadicode-lg)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:var(--spacing-nadicode-md)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-3xl *:[img:last-child]:rounded-b-3xl", surface === "resting" && "nc-elevation-resting", surface === "raised" && "nc-elevation-raised", variant === "interactive" && "cursor-pointer", variant === "link" && "nc-transition-paint", className),
|
|
10
|
-
}, props),
|
|
12
|
+
props: mergeProps({ className: surfaceClassName }, props),
|
|
11
13
|
render,
|
|
12
14
|
state: { size, slot: "card", surface, variant },
|
|
13
15
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Table } from "./table.js";
|
|
3
2
|
import { type EntityColorMap } from "./chart-entity-colors.js";
|
|
4
3
|
/**
|
|
5
4
|
* chart-breakdown-table — entity breakdown with proportion bars for the
|
|
@@ -20,7 +19,7 @@ type BreakdownEntity = {
|
|
|
20
19
|
label: React.ReactNode;
|
|
21
20
|
value: number;
|
|
22
21
|
};
|
|
23
|
-
declare function BreakdownTable({ entities, entityColorMap, label, topN, otherValue, valueFormatter, columnLabels, otherLabel,
|
|
22
|
+
declare function BreakdownTable({ entities, entityColorMap, label, topN, otherValue, valueFormatter, columnLabels, otherLabel, totalLabel, }: {
|
|
24
23
|
entities: readonly BreakdownEntity[];
|
|
25
24
|
/** The shared page-context entity-color assignment map. */
|
|
26
25
|
entityColorMap: EntityColorMap;
|
|
@@ -42,6 +41,8 @@ declare function BreakdownTable({ entities, entityColorMap, label, topN, otherVa
|
|
|
42
41
|
};
|
|
43
42
|
/** Label for the neutral rollup row. */
|
|
44
43
|
otherLabel?: React.ReactNode;
|
|
44
|
+
/** When provided, adds the sum of all entities and the remainder below the rows. */
|
|
45
|
+
totalLabel?: React.ReactNode;
|
|
45
46
|
}): React.JSX.Element;
|
|
46
47
|
export { BreakdownTable };
|
|
47
48
|
export type { BreakdownEntity };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-breakdown-table.d.ts","sourceRoot":"","sources":["../../src/components/chart-breakdown-table.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"chart-breakdown-table.d.ts","sourceRoot":"","sources":["../../src/components/chart-breakdown-table.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kCAAkC,CAAC;AAE1C;;;;;;;;;;;;GAYG;AAEH,KAAK,eAAe,GAAG;IACrB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AA+EF,iBAAS,cAAc,CAAC,EACtB,QAAQ,EACR,cAAc,EACd,KAAK,EACL,IAAI,EACJ,UAAU,EACV,cAA8B,EAC9B,YAAY,EACZ,UAA8B,EAC9B,UAAU,GACX,EAAE;IACD,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IACrC,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAC;IAC/B,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C,YAAY,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,wCAAwC;IACxC,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B,qBAmEA;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAC1B,YAAY,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
-
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "./table.js";
|
|
3
|
+
import { Table, TableBody, TableCaption, TableCell, TableDetailRow, TableFooter, TableHead, TableHeader, TableRow, } from "./table.js";
|
|
4
|
+
import { tableCompactLayouts } from "./table-layouts.js";
|
|
4
5
|
import { CHART_ENTITY_OTHER_COLOR, entityColor, } from "./chart-entity-colors.js";
|
|
5
6
|
function percentOf(value, total) {
|
|
6
7
|
return total > 0 ? (value / total) * 100 : 0;
|
|
@@ -11,13 +12,15 @@ function formatShare(share) {
|
|
|
11
12
|
function defaultFormat(value) {
|
|
12
13
|
return value.toLocaleString("en-US");
|
|
13
14
|
}
|
|
14
|
-
function
|
|
15
|
-
return (_jsxs(
|
|
16
|
-
width: `${Number(share.toFixed(3))}%`,
|
|
17
|
-
backgroundColor: color,
|
|
18
|
-
} }) }), _jsx("span", { className: "w-12 text-right font-mono text-xs text-muted-foreground tabular-nums", children: formatShare(share) })] }) })] }));
|
|
15
|
+
function ShareBar({ share, color }) {
|
|
16
|
+
return (_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { "data-slot": "breakdown-table-bar", "aria-hidden": "true", className: "h-1.5 min-w-0 flex-1 overflow-hidden rounded-xs bg-muted", children: _jsx("span", { className: "block h-full rounded-r-xs", style: { width: `${Number(share.toFixed(3))}%`, backgroundColor: color } }) }), _jsx("span", { className: "shrink-0 text-right font-mono text-xs text-muted-foreground tabular-nums", children: formatShare(share) })] }));
|
|
19
17
|
}
|
|
20
|
-
function
|
|
18
|
+
function BreakdownRow({ label, value, share, color, shareLabel, valueFormatter, ...props }) {
|
|
19
|
+
return (_jsxs(_Fragment, { children: [_jsxs(TableRow, { className: tableCompactLayouts.standard.row, ...props, children: [_jsx(TableCell, { className: "wrap-anywhere font-medium whitespace-normal", children: _jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { "data-slot": "breakdown-table-dot", "aria-hidden": "true", className: "size-2 shrink-0 rounded-xs", style: { backgroundColor: color } }), _jsx("span", { className: "min-w-0", children: label })] }) }), _jsx(TableCell, { className: "text-right font-mono tabular-nums", children: valueFormatter(value) }), _jsx(TableCell, { className: tableCompactLayouts.standard.column, children: _jsx(ShareBar, { share: share, color: color }) })] }), _jsx(TableDetailRow, { colSpan: 2, facts: [
|
|
20
|
+
{ id: "share", label: shareLabel, value: _jsx(ShareBar, { share: share, color: color }) },
|
|
21
|
+
] })] }));
|
|
22
|
+
}
|
|
23
|
+
function BreakdownTable({ entities, entityColorMap, label, topN, otherValue, valueFormatter = defaultFormat, columnLabels, otherLabel = "Everything else", totalLabel, }) {
|
|
21
24
|
const providedOther = otherValue !== undefined;
|
|
22
25
|
const sorted = [...entities].sort((a, b) => b.value - a.value);
|
|
23
26
|
const visible = !providedOther && topN !== undefined && topN < sorted.length ? sorted.slice(0, topN) : sorted;
|
|
@@ -26,6 +29,7 @@ function BreakdownTable({ entities, entityColorMap, label, topN, otherValue, val
|
|
|
26
29
|
: sorted.slice(visible.length).reduce((sum, entity) => sum + entity.value, 0);
|
|
27
30
|
const total = sorted.reduce((sum, entity) => sum + entity.value, 0) + (providedOther ? otherValue : 0);
|
|
28
31
|
const showOther = providedOther ? otherValue > 0 : visible.length < sorted.length;
|
|
29
|
-
|
|
32
|
+
const shareLabel = columnLabels?.share ?? "Share of total";
|
|
33
|
+
return (_jsxs(Table, { "data-slot": "breakdown-table", className: "table-fixed text-sm", compactDetails: "standard", children: [_jsx(TableCaption, { className: "sr-only", children: label }), _jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-1/2 whitespace-normal", children: columnLabels?.entity ?? "Entity" }), _jsx(TableHead, { className: "w-1/4 text-right whitespace-normal", children: columnLabels?.value ?? "Value" }), _jsx(TableHead, { className: cn("w-1/4 whitespace-normal", tableCompactLayouts.standard.column), children: shareLabel })] }) }), _jsxs(TableBody, { children: [visible.map((entity) => (_jsx(BreakdownRow, { label: entity.label, value: entity.value, share: percentOf(entity.value, total), color: entityColor(entityColorMap, entity.id), shareLabel: shareLabel, valueFormatter: valueFormatter }, entity.id))), showOther ? (_jsx(BreakdownRow, { "data-slot": "breakdown-table-other", label: otherLabel, value: foldedValue, share: percentOf(foldedValue, total), color: CHART_ENTITY_OTHER_COLOR, shareLabel: shareLabel, valueFormatter: valueFormatter })) : null] }), totalLabel ? (_jsx(TableFooter, { children: _jsx(BreakdownRow, { label: totalLabel, value: total, share: percentOf(total, total), color: CHART_ENTITY_OTHER_COLOR, shareLabel: shareLabel, valueFormatter: valueFormatter }) })) : null] }));
|
|
30
34
|
}
|
|
31
35
|
export { BreakdownTable };
|
|
@@ -7,16 +7,15 @@ import * as React from "react";
|
|
|
7
7
|
* warning fraction (`thresholdFraction` is a REQUIRED consumer input, never a
|
|
8
8
|
* constant of this component — 0.8 is the common product default) and a cap
|
|
9
9
|
* tick at 100%. Overflow renders as a distinct segment continuing past the
|
|
10
|
-
* cap tick, never clamped away.
|
|
11
|
-
*
|
|
10
|
+
* cap tick, never clamped away. The required product-owned tone communicates
|
|
11
|
+
* consequence independently of position: a full included allowance can have
|
|
12
|
+
* funded extra available. The current value reads at the bar tip.
|
|
12
13
|
*
|
|
13
14
|
* Two scales, one meter owner. The default `panel` scale is the full grammar
|
|
14
|
-
* above. The `row` scale
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* encodes register instead; it keeps the overflow segment, because an over-run
|
|
19
|
-
* is never clamped away at either scale.
|
|
15
|
+
* above. The `row` scale carries a percent and a thin bar without ticks or a
|
|
16
|
+
* second value line, so measurements align in compact tables and membership
|
|
17
|
+
* rows. Its tone still comes from the product; compact does not mean warning.
|
|
18
|
+
* Overflow remains visible at either scale.
|
|
20
19
|
*
|
|
21
20
|
* Accessibility: `role="meter"` with a REQUIRED accessible name (`label`);
|
|
22
21
|
* `aria-valuenow` clamps to the meter maximum while `aria-valuetext` conveys
|
|
@@ -24,9 +23,9 @@ import * as React from "react";
|
|
|
24
23
|
* place the words live — the row prints no caption — so the caller passes a
|
|
25
24
|
* label that reads as a sentence, not a bare noun.
|
|
26
25
|
*/
|
|
27
|
-
type
|
|
26
|
+
type BulletBarTone = "neutral" | "warning" | "danger";
|
|
28
27
|
type BulletBarSize = "panel" | "row";
|
|
29
|
-
declare function BulletBar({ value, max, thresholdFraction, label, size, valueFormatter, valueText, className, ...props }: Omit<React.ComponentProps<"div">, "children"> & {
|
|
28
|
+
declare function BulletBar({ value, max, thresholdFraction, tone, label, size, valueFormatter, valueText, className, ...props }: Omit<React.ComponentProps<"div">, "children"> & {
|
|
30
29
|
value: number;
|
|
31
30
|
/** The cap (included usage / limit) the meter measures against. */
|
|
32
31
|
max: number;
|
|
@@ -36,11 +35,13 @@ declare function BulletBar({ value, max, thresholdFraction, label, size, valueFo
|
|
|
36
35
|
* fraction is a consumer input, never a constant of this contract.
|
|
37
36
|
*/
|
|
38
37
|
thresholdFraction: number;
|
|
38
|
+
/** REQUIRED product consequence; not derived from the measurement. */
|
|
39
|
+
tone: BulletBarTone;
|
|
39
40
|
/** REQUIRED accessible name for the meter. */
|
|
40
41
|
label: string;
|
|
41
42
|
/**
|
|
42
43
|
* `panel` (default) is the full meter grammar. `row` is the single-line
|
|
43
|
-
*
|
|
44
|
+
* compact comparison scale: percent plus a thin bar, no ticks, no value line.
|
|
44
45
|
*/
|
|
45
46
|
size?: BulletBarSize;
|
|
46
47
|
valueFormatter?: (value: number) => string;
|
|
@@ -48,5 +49,5 @@ declare function BulletBar({ value, max, thresholdFraction, label, size, valueFo
|
|
|
48
49
|
valueText?: string;
|
|
49
50
|
}): React.JSX.Element;
|
|
50
51
|
export { BulletBar };
|
|
51
|
-
export type {
|
|
52
|
+
export type { BulletBarTone };
|
|
52
53
|
//# sourceMappingURL=chart-bullet-bar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-bullet-bar.d.ts","sourceRoot":"","sources":["../../src/components/chart-bullet-bar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B
|
|
1
|
+
{"version":3,"file":"chart-bullet-bar.d.ts","sourceRoot":"","sources":["../../src/components/chart-bullet-bar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AACtD,KAAK,aAAa,GAAG,OAAO,GAAG,KAAK,CAAC;AAmCrC,iBAAS,SAAS,CAAC,EACjB,KAAK,EACL,GAAG,EACH,iBAAiB,EACjB,IAAI,EACJ,KAAK,EACL,IAAc,EACd,cAA8B,EAC9B,SAAS,EACT,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAAG;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,IAAI,EAAE,aAAa,CAAC;IACpB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,qBAgIA;AAED,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,YAAY,EAAE,aAAa,EAAE,CAAC"}
|
|
@@ -5,27 +5,22 @@ import { cn } from "../lib/utils.js";
|
|
|
5
5
|
* rail tone the segments wear: `--nc-state-review` / `--nc-state-blocked` are
|
|
6
6
|
* fill tones (0.85 amber reads at 1.6:1 on the light canvas), while
|
|
7
7
|
* `--nc-warning-deep` and `--nc-error-deep` are authored light AND dark so the
|
|
8
|
-
* percentage stays readable in both modes
|
|
9
|
-
* crosses a threshold.
|
|
8
|
+
* percentage stays readable in both modes when the product signals attention.
|
|
10
9
|
*/
|
|
11
10
|
const BULLET_BAR_VALUE_CLASS = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
neutral: "text-foreground",
|
|
12
|
+
warning: "text-(--nc-warning-deep)",
|
|
13
|
+
danger: "text-(--nc-error-deep)",
|
|
15
14
|
};
|
|
16
|
-
/**
|
|
17
|
-
* The consumed length carries the severity too, not only the tip figure: a
|
|
18
|
-
* meter at its cap and a meter at 40% drew the same calm ramp fill, so the one
|
|
19
|
-
* state where the reader is actually blocked was legible only from a 10px
|
|
20
|
-
* percentage. Position stays the primary signal (the threshold and cap ticks
|
|
21
|
-
* do not move); the tone reinforces it, per DESIGN.md's meter grammar. These
|
|
22
|
-
* are the rail tones of the feedback pairs, the same register the overflow
|
|
23
|
-
* segment already wears.
|
|
24
|
-
*/
|
|
25
15
|
const BULLET_BAR_FILL_CLASS = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
neutral: "bg-(--nc-chart-seq-3)",
|
|
17
|
+
warning: "bg-(--nc-state-review)",
|
|
18
|
+
danger: "bg-(--nc-state-blocked)",
|
|
19
|
+
};
|
|
20
|
+
const BULLET_BAR_OVERFLOW_CLASS = {
|
|
21
|
+
neutral: "bg-(--nc-chart-seq-4)",
|
|
22
|
+
warning: "bg-(--nc-warning-deep)",
|
|
23
|
+
danger: "bg-(--nc-error-deep)",
|
|
29
24
|
};
|
|
30
25
|
function percent(fraction) {
|
|
31
26
|
return `${Number((fraction * 100).toFixed(3))}%`;
|
|
@@ -33,7 +28,7 @@ function percent(fraction) {
|
|
|
33
28
|
function defaultFormat(value) {
|
|
34
29
|
return value.toLocaleString("en-US");
|
|
35
30
|
}
|
|
36
|
-
function BulletBar({ value, max, thresholdFraction, label, size = "panel", valueFormatter = defaultFormat, valueText, className, ...props }) {
|
|
31
|
+
function BulletBar({ value, max, thresholdFraction, tone, label, size = "panel", valueFormatter = defaultFormat, valueText, className, ...props }) {
|
|
37
32
|
const overflow = Math.max(value - max, 0);
|
|
38
33
|
const total = Math.max(value, max);
|
|
39
34
|
const fillFraction = total > 0 ? Math.min(value, max) / total : 0;
|
|
@@ -41,29 +36,14 @@ function BulletBar({ value, max, thresholdFraction, label, size = "panel", value
|
|
|
41
36
|
const capFraction = total > 0 ? max / total : 1;
|
|
42
37
|
const thresholdTickFraction = capFraction * thresholdFraction;
|
|
43
38
|
const tipFraction = fillFraction + overflowFraction;
|
|
44
|
-
// Reaching the cap IS the blocked state, not merely the last warning step:
|
|
45
|
-
// an allowance consumed to exactly 100% stops the work the same way 104%
|
|
46
|
-
// does, so both read as `over`. A zero cap with no value stays `ok` — an
|
|
47
|
-
// unconfigured meter is not an exhausted one.
|
|
48
|
-
const severity = overflow > 0 || (max > 0 && value >= max)
|
|
49
|
-
? "over"
|
|
50
|
-
: max > 0 && value >= thresholdFraction * max
|
|
51
|
-
? "warn"
|
|
52
|
-
: "ok";
|
|
53
39
|
const meterValueText = valueText ??
|
|
54
40
|
`${valueFormatter(value)} of ${valueFormatter(max)}${overflow > 0 ? ` (${valueFormatter(overflow)} over the cap)` : ""}`;
|
|
55
|
-
// The row scale prints this instead of the tip value: at row scale the
|
|
56
|
-
// reader is asking "how close to the wall", not "how many".
|
|
57
41
|
const consumedPercent = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
58
42
|
const segments = (_jsxs(_Fragment, { children: [_jsx("div", { "data-slot": "bullet-bar-track",
|
|
59
43
|
// One step darker than the surface it sits on, in both themes: at
|
|
60
44
|
// `bg-muted` an empty meter on the light canvas read as a hairline
|
|
61
45
|
// rather than as a track with nothing in it.
|
|
62
|
-
className: "absolute inset-y-0 rounded-r-sm bg-muted-foreground/20", style: { left: 0, width: percent(capFraction) } }), _jsx("div", { "data-slot": "bullet-bar-fill", className: cn("absolute inset-y-0", BULLET_BAR_FILL_CLASS[
|
|
63
|
-
// A darker step of the same error family: now that the consumed
|
|
64
|
-
// length is itself red at the cap, the over-run has to stay a
|
|
65
|
-
// DISTINCT segment (DESIGN.md), which one shared tone would erase.
|
|
66
|
-
className: "absolute inset-y-0 rounded-r-sm bg-(--nc-error-deep)", style: {
|
|
46
|
+
className: "absolute inset-y-0 rounded-r-sm bg-muted-foreground/20", style: { left: 0, width: percent(capFraction) } }), _jsx("div", { "data-slot": "bullet-bar-fill", className: cn("absolute inset-y-0", BULLET_BAR_FILL_CLASS[tone], overflow === 0 && "rounded-r-sm"), style: { left: 0, width: percent(fillFraction) } }), overflow > 0 ? (_jsx("div", { "data-slot": "bullet-bar-overflow", className: cn("absolute inset-y-0 rounded-r-sm", BULLET_BAR_OVERFLOW_CLASS[tone]), style: {
|
|
67
47
|
left: percent(fillFraction),
|
|
68
48
|
width: percent(overflowFraction),
|
|
69
49
|
} })) : null] }));
|
|
@@ -76,8 +56,8 @@ function BulletBar({ value, max, thresholdFraction, label, size = "panel", value
|
|
|
76
56
|
"aria-valuetext": meterValueText,
|
|
77
57
|
};
|
|
78
58
|
if (size === "row") {
|
|
79
|
-
return (_jsxs("div", { "data-slot": "bullet-bar", "data-
|
|
59
|
+
return (_jsxs("div", { "data-slot": "bullet-bar", "data-tone": tone, "data-size": "row", className: cn("flex items-center gap-2", className), ...props, children: [_jsxs("span", { "data-slot": "bullet-bar-percent", "aria-hidden": "true", className: cn("font-mono text-xs tabular-nums", BULLET_BAR_VALUE_CLASS[tone]), children: [consumedPercent, "%"] }), _jsx("div", { ...meterAria, className: "relative h-1 min-w-0 flex-1", children: segments })] }));
|
|
80
60
|
}
|
|
81
|
-
return (_jsxs("div", { "data-slot": "bullet-bar", "data-
|
|
61
|
+
return (_jsxs("div", { "data-slot": "bullet-bar", "data-tone": tone, "data-size": "panel", className: cn("flex flex-col gap-1", className), ...props, children: [_jsxs("div", { ...meterAria, className: "relative h-3 w-full", children: [segments, _jsx("div", { "data-slot": "bullet-bar-threshold-tick", "aria-hidden": "true", className: "absolute -inset-y-0.5 w-px bg-foreground", style: { left: percent(thresholdTickFraction) } }), _jsx("div", { "data-slot": "bullet-bar-cap-tick", "aria-hidden": "true", className: "absolute -inset-y-0.5 w-px bg-foreground", style: { left: percent(capFraction) } })] }), _jsx("div", { className: "relative h-4 w-full text-xs", "aria-hidden": "true", children: _jsx("span", { "data-slot": "bullet-bar-value", className: cn("absolute font-mono whitespace-nowrap tabular-nums", tipFraction > 0.6 && "-translate-x-full", BULLET_BAR_VALUE_CLASS[tone]), style: { left: percent(tipFraction) }, children: valueFormatter(value) }) })] }));
|
|
82
62
|
}
|
|
83
63
|
export { BulletBar };
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
2
3
|
import { type DeltaDirection } from "./chart-delta.js";
|
|
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
4
|
type KpiStatDelta = {
|
|
16
5
|
/** The formatted change, e.g. "+12%". */
|
|
17
6
|
value: React.ReactNode;
|
|
@@ -20,14 +9,36 @@ type KpiStatDelta = {
|
|
|
20
9
|
upIsGood?: boolean;
|
|
21
10
|
/** The named comparison period, e.g. "previous 30 days". */
|
|
22
11
|
vsLabel: string;
|
|
12
|
+
/** A volume change has direction without implying success or failure. */
|
|
13
|
+
neutral?: boolean;
|
|
23
14
|
};
|
|
24
|
-
|
|
15
|
+
type KpiStatTone = "default" | "warning" | "critical";
|
|
16
|
+
declare function KpiStat({ label, value, delta, sparkline, note, tone, className, ...props }: Omit<React.ComponentProps<"dl">, "children"> & {
|
|
25
17
|
label: React.ReactNode;
|
|
26
18
|
value: React.ReactNode;
|
|
27
19
|
delta?: KpiStatDelta;
|
|
28
20
|
/** Optional decorative sparkline slot (e.g. a `Sparkline` element). */
|
|
29
21
|
sparkline?: React.ReactNode;
|
|
22
|
+
note?: React.ReactNode;
|
|
23
|
+
tone?: KpiStatTone;
|
|
30
24
|
}): React.JSX.Element;
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
type KpiStatGroupItem = Pick<React.ComponentProps<typeof KpiStat>, "label" | "value" | "delta" | "note" | "tone"> & {
|
|
26
|
+
id: string;
|
|
27
|
+
sparkline?: {
|
|
28
|
+
data: readonly number[];
|
|
29
|
+
baseline: number;
|
|
30
|
+
};
|
|
31
|
+
/** Bind an app link or data attributes without replacing the tile's presentation. */
|
|
32
|
+
render?: useRender.ComponentProps<"div">["render"];
|
|
33
|
+
interactive?: boolean;
|
|
34
|
+
};
|
|
35
|
+
declare function KpiStatGroup({ items, heading, }: {
|
|
36
|
+
items: readonly KpiStatGroupItem[];
|
|
37
|
+
heading?: {
|
|
38
|
+
title: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
};
|
|
41
|
+
}): React.JSX.Element | null;
|
|
42
|
+
export { KpiStat, KpiStatGroup };
|
|
43
|
+
export type { KpiStatDelta, KpiStatTone, KpiStatGroupItem };
|
|
33
44
|
//# sourceMappingURL=chart-kpi-stat.d.ts.map
|
|
@@ -1 +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,CAAC;
|
|
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,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAKtD,OAAO,EAAS,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAGtE,KAAK,YAAY,GAAG;IAClB,yCAAyC;IACzC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,SAAS,EAAE,cAAc,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,KAAK,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAQtD,iBAAS,OAAO,CAAC,EACf,KAAK,EACL,KAAK,EACL,KAAK,EACL,SAAS,EACT,IAAI,EACJ,IAAgB,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,GAAG;IAChD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,uEAAuE;IACvE,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB,qBA+CA;AAED,KAAK,gBAAgB,GAAG,IAAI,CAC1B,KAAK,CAAC,cAAc,CAAC,OAAO,OAAO,CAAC,EACpC,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAC9C,GAAG;IACF,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,qFAAqF;IACrF,MAAM,CAAC,EAAE,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC;IACnD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAqBF,iBAAS,YAAY,CAAC,EACpB,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD,4BA2BA;AAED,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AACjC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
2
3
|
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { Card, CardDescription, CardHeader, CardTitle } from "./card.js";
|
|
5
|
+
import { Separator } from "./separator.js";
|
|
3
6
|
import { Delta } from "./chart-delta.js";
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
import { Sparkline } from "./chart-sparkline.js";
|
|
8
|
+
const valueTones = {
|
|
9
|
+
default: "text-foreground",
|
|
10
|
+
warning: "text-(--nc-warning-deep)",
|
|
11
|
+
critical: "text-(--nc-error-deep)",
|
|
12
|
+
};
|
|
13
|
+
function KpiStat({ label, value, delta, sparkline, note, tone = "default", className, ...props }) {
|
|
14
|
+
return (_jsxs("dl", { "data-slot": "kpi-stat", className: cn("@container/kpi-stat flex min-w-0 flex-col gap-1.5", className), ...props, children: [_jsx("dt", { "data-slot": "kpi-stat-label", className: "min-h-8 text-xs font-medium text-muted-foreground", children: label }), _jsxs("dd", { className: "flex min-w-0 flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-end justify-between gap-3", children: [_jsx("div", { "data-slot": "kpi-stat-value", className: cn("min-w-0 shrink-0 text-[clamp(1.125rem,12cqw,1.875rem)] leading-none font-semibold", valueTones[tone]), children: value }), sparkline ? (_jsx("div", { "data-slot": "kpi-stat-sparkline", "aria-hidden": "true", className: "hidden min-w-0 max-w-24 @[15rem]/kpi-stat:block [&>svg]:h-auto [&>svg]:max-w-full", children: sparkline })) : null] }), delta?.neutral ? (_jsxs("p", { className: "text-sm tabular-nums text-muted-foreground", children: [delta.value, " \u00B7 ", delta.vsLabel] })) : delta ? (_jsx(Delta, { value: delta.value, direction: delta.direction, upIsGood: delta.upIsGood, vsLabel: delta.vsLabel })) : null, note ? _jsx("p", { className: "text-sm font-medium text-muted-foreground", children: note }) : null] })] }));
|
|
6
15
|
}
|
|
7
|
-
|
|
16
|
+
function KpiStatTile({ render, interactive, sparkline, ...stat }) {
|
|
17
|
+
return useRender({
|
|
18
|
+
defaultTagName: "div",
|
|
19
|
+
render,
|
|
20
|
+
props: {
|
|
21
|
+
className: cn("block h-full min-w-0 p-4", interactive && "nc-transition-paint rounded-md hover:bg-accent"),
|
|
22
|
+
children: (_jsx(KpiStat, { ...stat, ...(sparkline ? { sparkline: _jsx(Sparkline, { ...sparkline, width: 96, height: 28 }) } : {}) })),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function KpiStatGroup({ items, heading, }) {
|
|
27
|
+
if (items.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
return (_jsxs(Card, { className: "gap-0 py-0", children: [heading ? (_jsxs(_Fragment, { children: [_jsxs(CardHeader, { className: "py-4", children: [_jsx(CardTitle, { children: _jsx("h2", { children: heading.title }) }), heading.description ? _jsx(CardDescription, { children: heading.description }) : null] }), _jsx(Separator, {})] })) : null, _jsx("div", { "data-slot": "kpi-stat-group", className: "grid min-w-0 grid-cols-1 divide-y divide-border sm:grid-cols-[repeat(auto-fit,minmax(0,1fr))] sm:divide-x sm:divide-y-0", children: items.map(({ id, ...item }) => (_jsx("div", { className: "min-w-0", children: _jsx(KpiStatTile, { ...item }) }, id))) })] }));
|
|
30
|
+
}
|
|
31
|
+
export { KpiStat, KpiStatGroup };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-time-series-layout.d.ts","sourceRoot":"","sources":["../../src/components/chart-time-series-layout.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAE9D,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAKtF,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { type TimeSeriesSize } from "./chart-time-series-layout.js";
|
|
2
3
|
import { type EntityColorMap } from "./chart-entity-colors.js";
|
|
3
4
|
type TimeSeriesSeries = {
|
|
4
5
|
/** Stable entity id — the data key in each datum and the color-map key. */
|
|
@@ -7,10 +8,13 @@ type TimeSeriesSeries = {
|
|
|
7
8
|
};
|
|
8
9
|
type TimeSeriesDatum = Record<string, number | string>;
|
|
9
10
|
type TimeSeriesProps = Omit<React.ComponentProps<"figure">, "children"> & {
|
|
11
|
+
size?: TimeSeriesSize;
|
|
10
12
|
data: readonly TimeSeriesDatum[];
|
|
11
13
|
/** The datum key carrying the period (e.g. the day). */
|
|
12
14
|
xKey: string;
|
|
13
15
|
series: readonly TimeSeriesSeries[];
|
|
16
|
+
/** Stable entity-id datum key for horizontal categorical bars; omitted for time series. */
|
|
17
|
+
barCategoryKey?: string;
|
|
14
18
|
/** The shared page-context entity-color assignment map. */
|
|
15
19
|
entityColorMap: EntityColorMap;
|
|
16
20
|
/** REQUIRED accessible name for the chart and its table alternative. */
|
|
@@ -26,8 +30,8 @@ type TimeSeriesProps = Omit<React.ComponentProps<"figure">, "children"> & {
|
|
|
26
30
|
/** Label for the folded neutral "Other" series (entities past the sixth). */
|
|
27
31
|
otherLabel?: string;
|
|
28
32
|
};
|
|
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;
|
|
33
|
+
declare function TimeSeriesBars({ data, xKey, series, entityColorMap, label, xLabel, valueFormatter, yAxisWidth, otherLabel, barCategoryKey, size, className, ...props }: TimeSeriesProps): React.JSX.Element;
|
|
34
|
+
declare function TimeSeriesArea({ data, xKey, series, entityColorMap, label, xLabel, valueFormatter, yAxisWidth, otherLabel, size, className, ...props }: TimeSeriesProps): React.JSX.Element;
|
|
31
35
|
export { TimeSeriesArea, TimeSeriesBars };
|
|
32
36
|
export type { TimeSeriesDatum, TimeSeriesProps, TimeSeriesSeries };
|
|
33
37
|
//# sourceMappingURL=chart-time-series.d.ts.map
|
|
@@ -1 +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,CAAC;
|
|
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,CAAC;AAgB/B,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAS/F,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kCAAkC,CAAC;AAkH1C,KAAK,gBAAgB,GAAG;IACtB,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEvD,KAAK,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,GAAG;IACxE,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,IAAI,EAAE,SAAS,eAAe,EAAE,CAAC;IACjC,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpC,2FAA2F;IAC3F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAC;IAC/B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAsJF,iBAAS,cAAc,CAAC,EACtB,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,cAAc,EACd,KAAK,EACL,MAAiB,EACjB,cAA8B,EAC9B,UAAU,EACV,UAAoB,EACpB,cAAc,EACd,IAAgB,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,eAAe,qBAqKjB;AAED,iBAAS,cAAc,CAAC,EACtB,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,cAAc,EACd,KAAK,EACL,MAAiB,EACjB,cAA8B,EAC9B,UAAU,EACV,UAAoB,EACpB,IAAgB,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,eAAe,qBA6FjB;AAED,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
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";
|
|
3
|
+
import { Area, AreaChart, Bar, BarChart, BarStack, CartesianGrid, Rectangle, Text, XAxis, YAxis, } from "recharts";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { timeSeriesLayouts } from "./chart-time-series-layout.js";
|
|
5
6
|
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from "./chart.js";
|
|
6
7
|
import { CHART_ENTITY_OTHER_COLOR, entityColor, } from "./chart-entity-colors.js";
|
|
7
8
|
/**
|
|
8
|
-
* chart-time-series —
|
|
9
|
+
* chart-time-series — stacked time-series and categorical bars for the console chart
|
|
9
10
|
* kit (issue #166), built on the Recharts foundation
|
|
10
11
|
* (ChartContainer / ChartTooltip / ChartLegend; chart.tsx untouched).
|
|
11
12
|
*
|
|
@@ -17,8 +18,9 @@ import { CHART_ENTITY_OTHER_COLOR, entityColor, } from "./chart-entity-colors.js
|
|
|
17
18
|
* zero-valued — the radius rounds the stack, not one hard-coded series.
|
|
18
19
|
* - The public `valueFormatter` reaches every value surface — the table twin,
|
|
19
20
|
* the y-axis ticks, and the tooltip value — not only the hidden table cells.
|
|
20
|
-
* - Series colors resolve from the shared page-context
|
|
21
|
-
* never from array index.
|
|
21
|
+
* - Series and categorical bar colors resolve from the shared page-context
|
|
22
|
+
* entity-color map, never from array index. Categorical bars run horizontally
|
|
23
|
+
* with every entity label visible; time-series axes keep their even stride.
|
|
22
24
|
* - Direct labels are not feasible on stacked dailies, so at two or more
|
|
23
25
|
* series a legend identifies every series; a single series is titled by
|
|
24
26
|
* the accessible name, not legended.
|
|
@@ -166,17 +168,29 @@ function TimeSeriesTableAlternative({ data, xKey, xLabel, series, label, valueFo
|
|
|
166
168
|
function TimeSeriesFrame({ label, className, children, ...props }) {
|
|
167
169
|
return (_jsx("figure", { "aria-label": label, className: cn("m-0 flex w-full flex-col", className), ...props, children: children }));
|
|
168
170
|
}
|
|
169
|
-
function TimeSeriesBars({ data, xKey, series, entityColorMap, label, xLabel = "Period", valueFormatter = defaultFormat, yAxisWidth, otherLabel = "Other", className, ...props }) {
|
|
171
|
+
function TimeSeriesBars({ data, xKey, series, entityColorMap, label, xLabel = "Period", valueFormatter = defaultFormat, yAxisWidth, otherLabel = "Other", barCategoryKey, size = "default", className, ...props }) {
|
|
172
|
+
if (barCategoryKey) {
|
|
173
|
+
const config = seriesChartConfig(series, entityColorMap);
|
|
174
|
+
const coloredData = data.map((datum) => ({
|
|
175
|
+
...datum,
|
|
176
|
+
fill: entityColor(entityColorMap, String(datum[barCategoryKey] ?? "")),
|
|
177
|
+
}));
|
|
178
|
+
const domainMax = niceYDomainMax(data, series);
|
|
179
|
+
return (_jsxs(TimeSeriesFrame, { "data-slot": "categorical-bars", label: label, className: className, ...props, children: [_jsx(ChartContainer, { config: config, className: cn("w-full aspect-auto", CHART_SURFACE_FOCUS_CLASS), style: { height: Math.max(256, data.length * 64 + 40) }, children: _jsxs(BarChart, { accessibilityLayer: true, title: label, data: coloredData, layout: "vertical", children: [_jsx(CartesianGrid, { horizontal: false }), _jsx(XAxis, { type: "number", tickLine: false, axisLine: false, tickCount: 3, ...(domainMax === undefined ? {} : { domain: [0, domainMax] }), tickFormatter: (value) => valueFormatter(Number(value)) }), _jsx(YAxis, { type: "category", dataKey: xKey, width: 120, interval: 0, tickLine: false, axisLine: false, tick: ({ x, y, payload }) => (_jsx(Text, { x: x, y: y, width: 108, breakAll: true, maxLines: 2, "aria-label": String(payload.value), textAnchor: "end", verticalAnchor: "middle", className: "fill-muted-foreground", children: String(payload.value) })) }), _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { formatter: (value, name, item) => tooltipRowFormatter(series, valueFormatter)(value, name, {
|
|
180
|
+
...item,
|
|
181
|
+
color: item.payload?.fill,
|
|
182
|
+
}, 0, []) }) }), series.map((entry) => (_jsx(Bar, { dataKey: entry.id, isAnimationActive: false, maxBarSize: 24, shape: ({ x, y, width, height, originalDataIndex }) => (_jsx(Rectangle, { x: x, y: y, width: width, height: height, radius: [0, 4, 4, 0], fill: coloredData[originalDataIndex]?.fill })) }, entry.id)))] }) }), _jsx(TimeSeriesTableAlternative, { data: data, xKey: xKey, xLabel: xLabel, series: series, label: label, valueFormatter: valueFormatter })] }));
|
|
183
|
+
}
|
|
170
184
|
const folded = foldNeutralSeries(series, entityColorMap, data, otherLabel);
|
|
171
185
|
const config = seriesChartConfig(folded.series, entityColorMap);
|
|
172
186
|
const yDomainMax = niceYDomainMax(folded.data, folded.series);
|
|
173
|
-
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, interval: "equidistantPreserveStart", minTickGap: X_AXIS_MIN_TICK_GAP }), _jsx(YAxis, { tickLine: false, axisLine: false, width: yAxisWidth ?? "auto", tickCount: Y_AXIS_TICK_COUNT, ...(yDomainMax === undefined ? {} : { domain: [0, yDomainMax] }), 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, {
|
|
187
|
+
return (_jsxs(TimeSeriesFrame, { "data-slot": "time-series-bars", label: label, className: cn(timeSeriesLayouts[size].frame, className), ...props, children: [_jsx(ChartContainer, { config: config, className: cn("w-full aspect-auto", timeSeriesLayouts[size].chart, 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, interval: "equidistantPreserveStart", minTickGap: X_AXIS_MIN_TICK_GAP }), _jsx(YAxis, { tickLine: false, axisLine: false, width: yAxisWidth ?? "auto", tickCount: Y_AXIS_TICK_COUNT, ...(yDomainMax === undefined ? {} : { domain: [0, yDomainMax] }), 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, { radius: [4, 4, 0, 0], children: folded.series.map((entry) => (_jsx(Bar, { dataKey: entry.id, isAnimationActive: false, 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 })] }));
|
|
174
188
|
}
|
|
175
|
-
function TimeSeriesArea({ data, xKey, series, entityColorMap, label, xLabel = "Period", valueFormatter = defaultFormat, yAxisWidth, otherLabel = "Other", className, ...props }) {
|
|
189
|
+
function TimeSeriesArea({ data, xKey, series, entityColorMap, label, xLabel = "Period", valueFormatter = defaultFormat, yAxisWidth, otherLabel = "Other", size = "default", className, ...props }) {
|
|
176
190
|
const folded = foldNeutralSeries(series, entityColorMap, data, otherLabel);
|
|
177
191
|
const config = seriesChartConfig(folded.series, entityColorMap);
|
|
178
192
|
const yDomainMax = niceYDomainMax(folded.data, folded.series);
|
|
179
|
-
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, margin: { top: 5, right: 20, bottom: 5, left: 20 }, children: [_jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: xKey, tickLine: false, axisLine: false, tickMargin: 8, interval: "equidistantPreserveStart", minTickGap: X_AXIS_MIN_TICK_GAP }), _jsx(YAxis, { tickLine: false, axisLine: false, width: yAxisWidth ?? "auto", tickCount: Y_AXIS_TICK_COUNT, ...(yDomainMax === undefined ? {} : { domain: [0, yDomainMax] }), 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) => {
|
|
193
|
+
return (_jsxs(TimeSeriesFrame, { "data-slot": "time-series-area", label: label, className: cn(timeSeriesLayouts[size].frame, className), ...props, children: [_jsx(ChartContainer, { config: config, className: cn("w-full aspect-auto", timeSeriesLayouts[size].chart, CHART_SURFACE_FOCUS_CLASS), children: _jsxs(AreaChart, { accessibilityLayer: true, title: label, data: folded.data, margin: { top: 5, right: 20, bottom: 5, left: 20 }, children: [_jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: xKey, tickLine: false, axisLine: false, tickMargin: 8, interval: "equidistantPreserveStart", minTickGap: X_AXIS_MIN_TICK_GAP }), _jsx(YAxis, { tickLine: false, axisLine: false, width: yAxisWidth ?? "auto", tickCount: Y_AXIS_TICK_COUNT, ...(yDomainMax === undefined ? {} : { domain: [0, yDomainMax] }), 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) => {
|
|
180
194
|
const color = entityColor(entityColorMap, entry.id);
|
|
181
195
|
return (_jsx(Area, { type: "monotone", dataKey: entry.id, isAnimationActive: false, stackId: "stacked", stroke: color, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", fill: color,
|
|
182
196
|
// DESIGN.md mark specs: an area fill reads as a light wash
|
|
@@ -185,7 +199,11 @@ function TimeSeriesArea({ data, xKey, series, entityColorMap, label, xLabel = "P
|
|
|
185
199
|
// Recharts' default activeDot ring is stroke #fff, which the
|
|
186
200
|
// chart foundation remaps to transparent — an explicit surface
|
|
187
201
|
// ring keeps the DESIGN.md 2px marker-ring spec on hover/focus.
|
|
188
|
-
activeDot: {
|
|
202
|
+
activeDot: {
|
|
203
|
+
r: 4,
|
|
204
|
+
stroke: "var(--background)",
|
|
205
|
+
strokeWidth: 2,
|
|
206
|
+
} }, entry.id));
|
|
189
207
|
})] }) }), _jsx(TimeSeriesTableAlternative, { data: folded.data, xKey: xKey, xLabel: xLabel, series: folded.series, label: label, valueFormatter: valueFormatter })] }));
|
|
190
208
|
}
|
|
191
209
|
export { TimeSeriesArea, TimeSeriesBars };
|
package/dist/components/chart.js
CHANGED
|
@@ -39,7 +39,7 @@ function ChartContainer({ id, className, children, config, initialDimension = IN
|
|
|
39
39
|
const chartId = sanitizeChartToken(`chart-${id ?? uniqueId.replace(/:/g, "")}`);
|
|
40
40
|
return (_jsx(ChartContext.Provider, { value: { config }, children: _jsxs("div", { "data-slot": "chart", "data-chart": chartId, className: cn(
|
|
41
41
|
// eslint-disable-next-line no-restricted-syntax -- the hexes here are MATCH KEYS for Recharts' hardcoded default strokes (#ccc grid and reference lines, #fff dot and sector outlines), remapped onto theme tokens. None of them is a colour this component emits.
|
|
42
|
-
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-
|
|
42
|
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick-label_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", className), ...props, children: [_jsx(ChartStyle, { id: chartId, config: config }), _jsx(RechartsPrimitive.ResponsiveContainer, { initialDimension: initialDimension, children: children })] }) }));
|
|
43
43
|
}
|
|
44
44
|
const ChartStyle = ({ id, config }) => {
|
|
45
45
|
const chartId = sanitizeChartToken(id);
|