cortena-ui 0.1.0 → 1.0.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/index.d.ts +2004 -4
- package/dist/index.js +5949 -2
- package/dist/index.js.map +1 -1
- package/package.json +41 -5
- package/src/components/accordion.tsx +86 -0
- package/src/components/alert.tsx +81 -0
- package/src/components/avatar.tsx +53 -0
- package/src/components/badge.tsx +51 -0
- package/src/components/calendar.tsx +193 -0
- package/src/components/card.tsx +85 -0
- package/src/components/chart/chart.tsx +193 -0
- package/src/components/chart/container.tsx +324 -0
- package/src/components/chart/data.ts +101 -0
- package/src/components/chart/index.tsx +84 -0
- package/src/components/chart/nivo-charts.tsx +227 -0
- package/src/components/chart/nivo-theme.ts +136 -0
- package/src/components/chart/recharts-charts.tsx +232 -0
- package/src/components/chart/tokens.ts +210 -0
- package/src/components/chart/types.ts +142 -0
- package/src/components/checkbox.tsx +49 -0
- package/src/components/chip.tsx +49 -0
- package/src/components/combobox.tsx +405 -0
- package/src/components/command.tsx +225 -0
- package/src/components/data-table/data-table.tsx +731 -0
- package/src/components/data-table/export.ts +121 -0
- package/src/components/data-table/features.ts +123 -0
- package/src/components/data-table/index.tsx +45 -0
- package/src/components/data-table/parts.tsx +568 -0
- package/src/components/data-table/system-columns.tsx +91 -0
- package/src/components/data-table/types.ts +182 -0
- package/src/components/data-table/use-data-table.ts +224 -0
- package/src/components/data-table/use-server-source.ts +232 -0
- package/src/components/date-field.tsx +327 -0
- package/src/components/date-picker.tsx +230 -0
- package/src/components/dialog.tsx +146 -0
- package/src/components/dropdown-menu.tsx +314 -0
- package/src/components/dropzone.tsx +260 -0
- package/src/components/empty-state.tsx +91 -0
- package/src/components/error-banner.tsx +65 -0
- package/src/components/field.tsx +250 -0
- package/src/components/form.tsx +212 -0
- package/src/components/input.tsx +32 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +26 -0
- package/src/components/loading-skeleton.tsx +153 -0
- package/src/components/markdown.tsx +377 -0
- package/src/components/page-header.tsx +99 -0
- package/src/components/popover.tsx +133 -0
- package/src/components/progress.tsx +83 -0
- package/src/components/radio-group.tsx +48 -0
- package/src/components/scroll-area.tsx +74 -0
- package/src/components/section-card.tsx +75 -0
- package/src/components/segmented.tsx +81 -0
- package/src/components/select.tsx +222 -0
- package/src/components/separator.tsx +41 -0
- package/src/components/sheet.tsx +161 -0
- package/src/components/skeleton.tsx +22 -0
- package/src/components/sortable-list.tsx +275 -0
- package/src/components/spinner.tsx +32 -0
- package/src/components/status-dot.tsx +78 -0
- package/src/components/switch.tsx +48 -0
- package/src/components/tabs.tsx +109 -0
- package/src/components/textarea.tsx +75 -0
- package/src/components/toast.tsx +150 -0
- package/src/components/toolbar.tsx +63 -0
- package/src/components/tooltip.tsx +103 -0
- package/src/hooks/use-cortena-theme.ts +268 -0
- package/src/index.ts +91 -3
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
import { ChartContainer, ChartEmpty, ChartError, ChartLoading, type ChartContainerProps } from "./container";
|
|
6
|
+
import { configFor, isEmptyData, isSeriesArray, toRows, toSlices } from "./data";
|
|
7
|
+
import { NivoChart, type NivoType } from "./nivo-charts";
|
|
8
|
+
import { RechartsChart, type RechartsType } from "./recharts-charts";
|
|
9
|
+
import {
|
|
10
|
+
chartEngine,
|
|
11
|
+
type ChartConfig,
|
|
12
|
+
type ChartDataFor,
|
|
13
|
+
type ChartRenderer,
|
|
14
|
+
type ChartRow,
|
|
15
|
+
type ChartScale,
|
|
16
|
+
type ChartSeries,
|
|
17
|
+
type ChartSlice,
|
|
18
|
+
type ChartTree,
|
|
19
|
+
type ChartType,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
export interface ChartProps<T extends ChartType = ChartType>
|
|
23
|
+
extends Omit<ChartContainerProps, "children" | "config" | "responsive"> {
|
|
24
|
+
/** Which chart to draw; the package picks the engine. See {@link ChartType} for the data shape per type. */
|
|
25
|
+
type: T;
|
|
26
|
+
/** The data, in the shape documented on {@link ChartType}. */
|
|
27
|
+
data: ChartDataFor<T>;
|
|
28
|
+
/** Series presentation keyed by series id; colours default to `--ds-viz-1..n` in order. */
|
|
29
|
+
config?: ChartConfig;
|
|
30
|
+
/** Nivo renderer; `canvas` for large series. Ignored by the Recharts types and by sunburst. */
|
|
31
|
+
renderer?: ChartRenderer;
|
|
32
|
+
/** Category key for row data (bar, line, area, radar); inferred as the first string field. */
|
|
33
|
+
indexBy?: string;
|
|
34
|
+
/** Series keys for row data; inferred as every numeric field. */
|
|
35
|
+
keys?: string[];
|
|
36
|
+
/** Stack bars and areas instead of grouping them. */
|
|
37
|
+
stacked?: boolean;
|
|
38
|
+
/** Continuous scale for heatmap and calendar. Default sequential. */
|
|
39
|
+
scale?: ChartScale;
|
|
40
|
+
/** Show a legend. Default: when there is more than one series. */
|
|
41
|
+
legend?: boolean;
|
|
42
|
+
/** Draw grid lines (cartesian and radar types). Default true. */
|
|
43
|
+
grid?: boolean;
|
|
44
|
+
/** Show a tooltip on hover. Default true. */
|
|
45
|
+
tooltip?: boolean;
|
|
46
|
+
/** Format values in axes, tooltips and labels. */
|
|
47
|
+
valueFormatter?: (value: number) => string;
|
|
48
|
+
/** Calendar range (ISO dates); defaults to the data's extent. */
|
|
49
|
+
from?: string;
|
|
50
|
+
to?: string;
|
|
51
|
+
/** Render the loading skeleton instead of the data. */
|
|
52
|
+
loading?: boolean;
|
|
53
|
+
/** Render the error state with this message instead of the data. */
|
|
54
|
+
error?: React.ReactNode | Error | null;
|
|
55
|
+
/** Content of the empty state. Default "No data". */
|
|
56
|
+
emptyMessage?: React.ReactNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function seriesKeys(type: ChartType, data: unknown, indexBy?: string, keys?: string[]): string[] {
|
|
60
|
+
switch (type) {
|
|
61
|
+
case "bar":
|
|
62
|
+
case "line":
|
|
63
|
+
case "area":
|
|
64
|
+
case "radar":
|
|
65
|
+
return toRows(data as ChartRow[] | ChartSeries[], indexBy, keys).keys;
|
|
66
|
+
case "pie":
|
|
67
|
+
case "donut":
|
|
68
|
+
case "funnel":
|
|
69
|
+
return toSlices(data as ChartSlice[]).map((s) => s.id);
|
|
70
|
+
case "scatter":
|
|
71
|
+
case "scatterplot":
|
|
72
|
+
case "heatmap":
|
|
73
|
+
return isSeriesArray(data) ? data.map((s) => String(s.id)) : [];
|
|
74
|
+
case "treemap":
|
|
75
|
+
case "sunburst":
|
|
76
|
+
return ((data as ChartTree).children ?? []).map((c) => String(c.id ?? c.name ?? ""));
|
|
77
|
+
case "calendar":
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class ChunkBoundary extends React.Component<
|
|
83
|
+
{ children: React.ReactNode; fallback: React.ReactNode },
|
|
84
|
+
{ failed: boolean }
|
|
85
|
+
> {
|
|
86
|
+
state = { failed: false };
|
|
87
|
+
static getDerivedStateFromError() {
|
|
88
|
+
return { failed: true };
|
|
89
|
+
}
|
|
90
|
+
render() {
|
|
91
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Chart — one component for every chart type the fleet draws.
|
|
97
|
+
*
|
|
98
|
+
* `type` picks the engine: Recharts (bundled) for bar, line, area, pie, donut,
|
|
99
|
+
* scatter, radar and funnel; Nivo, lazy-loaded per package, for heatmap,
|
|
100
|
+
* calendar, treemap and sunburst, with `renderer="canvas"` for large series.
|
|
101
|
+
* Colours, fonts, axes, grid, tooltip, legend and the empty, loading and
|
|
102
|
+
* error states all come from cortena-design tokens; a consumer passes data
|
|
103
|
+
* and, optionally, a `config` of labels.
|
|
104
|
+
*/
|
|
105
|
+
function Chart<T extends ChartType>({
|
|
106
|
+
type,
|
|
107
|
+
data,
|
|
108
|
+
config,
|
|
109
|
+
renderer = "svg",
|
|
110
|
+
indexBy,
|
|
111
|
+
keys,
|
|
112
|
+
stacked,
|
|
113
|
+
scale = "sequential",
|
|
114
|
+
legend,
|
|
115
|
+
grid = true,
|
|
116
|
+
tooltip = true,
|
|
117
|
+
valueFormatter,
|
|
118
|
+
from,
|
|
119
|
+
to,
|
|
120
|
+
loading,
|
|
121
|
+
error,
|
|
122
|
+
emptyMessage,
|
|
123
|
+
className,
|
|
124
|
+
height,
|
|
125
|
+
...props
|
|
126
|
+
}: ChartProps<T>) {
|
|
127
|
+
const engine = chartEngine(type);
|
|
128
|
+
const empty = isEmptyData(data);
|
|
129
|
+
const fullConfig = React.useMemo(
|
|
130
|
+
() => configFor(empty ? [] : seriesKeys(type, data, indexBy, keys), config),
|
|
131
|
+
[type, data, indexBy, keys, config, empty],
|
|
132
|
+
);
|
|
133
|
+
const showLegend = legend ?? Object.keys(fullConfig).length > 1;
|
|
134
|
+
const errorText = error instanceof Error ? error.message : error;
|
|
135
|
+
|
|
136
|
+
let body: React.ReactNode;
|
|
137
|
+
let responsive = false;
|
|
138
|
+
if (loading) body = <ChartLoading />;
|
|
139
|
+
else if (errorText) body = <ChartError>{errorText}</ChartError>;
|
|
140
|
+
else if (empty) body = <ChartEmpty>{emptyMessage}</ChartEmpty>;
|
|
141
|
+
else if (engine === "nivo") {
|
|
142
|
+
body = (
|
|
143
|
+
<ChunkBoundary fallback={<ChartError>Chart could not be loaded</ChartError>}>
|
|
144
|
+
<React.Suspense fallback={<ChartLoading />}>
|
|
145
|
+
<NivoChart
|
|
146
|
+
type={type as NivoType}
|
|
147
|
+
data={data}
|
|
148
|
+
config={fullConfig}
|
|
149
|
+
renderer={renderer}
|
|
150
|
+
scale={scale}
|
|
151
|
+
legend={showLegend}
|
|
152
|
+
valueFormatter={valueFormatter}
|
|
153
|
+
from={from}
|
|
154
|
+
to={to}
|
|
155
|
+
/>
|
|
156
|
+
</React.Suspense>
|
|
157
|
+
</ChunkBoundary>
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
responsive = true;
|
|
161
|
+
body = (
|
|
162
|
+
<RechartsChart
|
|
163
|
+
type={type as RechartsType}
|
|
164
|
+
data={data}
|
|
165
|
+
config={fullConfig}
|
|
166
|
+
indexBy={indexBy}
|
|
167
|
+
keys={keys}
|
|
168
|
+
stacked={stacked}
|
|
169
|
+
legend={showLegend}
|
|
170
|
+
grid={grid}
|
|
171
|
+
tooltip={tooltip}
|
|
172
|
+
valueFormatter={valueFormatter}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<ChartContainer
|
|
179
|
+
data-type={type}
|
|
180
|
+
data-engine={engine}
|
|
181
|
+
data-renderer={engine === "nivo" ? renderer : "svg"}
|
|
182
|
+
config={fullConfig}
|
|
183
|
+
height={height}
|
|
184
|
+
responsive={responsive}
|
|
185
|
+
className={cn(className)}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
{body}
|
|
189
|
+
</ChartContainer>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { Chart };
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Legend as RechartsLegend,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip as RechartsTooltip,
|
|
8
|
+
type LegendPayload,
|
|
9
|
+
type LegendProps,
|
|
10
|
+
type TooltipContentProps,
|
|
11
|
+
type TooltipProps,
|
|
12
|
+
} from "recharts";
|
|
13
|
+
import { cn } from "@/lib/cn";
|
|
14
|
+
import { Skeleton } from "@/components/skeleton";
|
|
15
|
+
import { vizVar } from "./tokens";
|
|
16
|
+
import type { ChartConfig } from "./types";
|
|
17
|
+
|
|
18
|
+
/* ── context ─────────────────────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
interface ChartContextValue {
|
|
21
|
+
config: ChartConfig;
|
|
22
|
+
/** The container element, so token resolution can honour a nested theme scope. */
|
|
23
|
+
element: HTMLDivElement | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ChartContext = React.createContext<ChartContextValue | null>(null);
|
|
27
|
+
|
|
28
|
+
/** The nearest ChartContainer's config and element; null outside one. */
|
|
29
|
+
export function useChart(): ChartContextValue | null {
|
|
30
|
+
return React.useContext(ChartContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the colour a series key paints with: `config[key].color` when set,
|
|
35
|
+
* otherwise `var(--ds-viz-N)` by the key's position in the config (or the
|
|
36
|
+
* `index` given). Recharts consumers reference it as `var(--color-<key>)`,
|
|
37
|
+
* which ChartContainer defines on its element.
|
|
38
|
+
*/
|
|
39
|
+
export function seriesColor(config: ChartConfig | undefined, key: string, index?: number): string {
|
|
40
|
+
const entry = config?.[key];
|
|
41
|
+
if (entry?.color) return entry.color;
|
|
42
|
+
const keys = config ? Object.keys(config) : [];
|
|
43
|
+
const position = keys.indexOf(key);
|
|
44
|
+
return vizVar(position >= 0 ? position : (index ?? 0));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ── container ───────────────────────────────────────────────────────────── */
|
|
48
|
+
|
|
49
|
+
export interface ChartContainerProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
50
|
+
/** Series presentation; colours default to `--ds-viz-1..n` in key order. */
|
|
51
|
+
config?: ChartConfig;
|
|
52
|
+
/** Height of the drawing area; width is always 100% of the parent. Default 300. */
|
|
53
|
+
height?: number | string;
|
|
54
|
+
/**
|
|
55
|
+
* Wrap children in Recharts' ResponsiveContainer (default). Nivo's
|
|
56
|
+
* `Responsive*` components size themselves, so the Nivo renderers pass
|
|
57
|
+
* `responsive={false}`.
|
|
58
|
+
*/
|
|
59
|
+
responsive?: boolean;
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* ChartContainer — the sized, token-styled surface every chart draws in.
|
|
65
|
+
*
|
|
66
|
+
* It defines `--color-<key>` for each config key (so Recharts parts can say
|
|
67
|
+
* `fill="var(--color-revenue)"`) and styles Recharts' axis, grid, tick and
|
|
68
|
+
* cursor classes from tokens with a caption-sm floor for text, so a custom
|
|
69
|
+
* chart composed inside it needs no local colours or fonts.
|
|
70
|
+
*/
|
|
71
|
+
function ChartContainer({
|
|
72
|
+
config = {},
|
|
73
|
+
height = 300,
|
|
74
|
+
responsive = true,
|
|
75
|
+
className,
|
|
76
|
+
style,
|
|
77
|
+
children,
|
|
78
|
+
...props
|
|
79
|
+
}: ChartContainerProps) {
|
|
80
|
+
const [element, setElement] = React.useState<HTMLDivElement | null>(null);
|
|
81
|
+
const vars = React.useMemo(() => {
|
|
82
|
+
const out: Record<string, string> = {};
|
|
83
|
+
for (const key of Object.keys(config)) out[`--color-${key}`] = seriesColor(config, key);
|
|
84
|
+
return out;
|
|
85
|
+
}, [config]);
|
|
86
|
+
const ctx = React.useMemo(() => ({ config, element }), [config, element]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ChartContext.Provider value={ctx}>
|
|
90
|
+
<div
|
|
91
|
+
ref={setElement}
|
|
92
|
+
data-slot="chart"
|
|
93
|
+
className={cn(
|
|
94
|
+
"relative w-full min-w-0 overflow-hidden text-[length:var(--ds-text-caption-sm)] text-[color:var(--ds-foreground)]",
|
|
95
|
+
responsive ? "flex justify-center" : "block [&>div]:h-full [&>div]:w-full",
|
|
96
|
+
"[&_.recharts-cartesian-axis-tick-value]:fill-[var(--ds-muted-foreground)]",
|
|
97
|
+
"[&_.recharts-polar-angle-axis-tick-value]:fill-[var(--ds-muted-foreground)]",
|
|
98
|
+
"[&_.recharts-polar-radius-axis-tick-value]:fill-[var(--ds-muted-foreground)]",
|
|
99
|
+
"[&_.recharts-cartesian-grid-horizontal_line]:stroke-[var(--ds-border-subtle)]",
|
|
100
|
+
"[&_.recharts-cartesian-grid-vertical_line]:stroke-[var(--ds-border-subtle)]",
|
|
101
|
+
"[&_.recharts-polar-grid-angle_line]:stroke-[var(--ds-border-subtle)]",
|
|
102
|
+
"[&_.recharts-polar-grid-concentric-polygon]:stroke-[var(--ds-border-subtle)]",
|
|
103
|
+
"[&_.recharts-polar-grid-concentric-circle]:stroke-[var(--ds-border-subtle)]",
|
|
104
|
+
"[&_.recharts-cartesian-axis-line]:stroke-[var(--ds-border)]",
|
|
105
|
+
"[&_.recharts-cartesian-axis-tick-line]:stroke-[var(--ds-border)]",
|
|
106
|
+
"[&_.recharts-radial-bar-background-sector]:fill-[var(--ds-muted)]",
|
|
107
|
+
"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--ds-hover)]",
|
|
108
|
+
"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--ds-border-strong)]",
|
|
109
|
+
"[&_.recharts-reference-line_line]:stroke-[var(--ds-border-strong)]",
|
|
110
|
+
"[&_.recharts-pie-sector_.recharts-sector]:stroke-[var(--ds-card)]",
|
|
111
|
+
"[&_.recharts-funnel-trapezoid_.recharts-trapezoid]:stroke-[var(--ds-card)]",
|
|
112
|
+
"[&_.recharts-layer]:outline-hidden [&_.recharts-surface]:outline-hidden [&_.recharts-sector]:outline-hidden",
|
|
113
|
+
"[&_text]:font-[family-name:var(--ds-font-body)]",
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
style={{ height, ...vars, ...style } as React.CSSProperties}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{responsive ? (
|
|
120
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
121
|
+
{children as React.ReactElement}
|
|
122
|
+
</ResponsiveContainer>
|
|
123
|
+
) : (
|
|
124
|
+
children
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</ChartContext.Provider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── states ──────────────────────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
function ChartState({ className, ...props }: React.ComponentProps<"div">) {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className={cn(
|
|
137
|
+
"flex h-full w-full items-center justify-center rounded-[var(--ds-radius-md)] border border-dashed border-[var(--ds-border-subtle)]",
|
|
138
|
+
"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]",
|
|
139
|
+
className,
|
|
140
|
+
)}
|
|
141
|
+
{...props}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** "No data" placeholder; rendered by Chart when the data is empty. */
|
|
147
|
+
function ChartEmpty({ children = "No data", ...props }: React.ComponentProps<"div">) {
|
|
148
|
+
return (
|
|
149
|
+
<ChartState data-slot="chart-empty" role="status" {...props}>
|
|
150
|
+
{children}
|
|
151
|
+
</ChartState>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Error placeholder; rendered by Chart for an `error` prop or a failed Nivo chunk. */
|
|
156
|
+
function ChartError({ className, children = "Chart could not be rendered", ...props }: React.ComponentProps<"div">) {
|
|
157
|
+
return (
|
|
158
|
+
<ChartState
|
|
159
|
+
data-slot="chart-error"
|
|
160
|
+
role="alert"
|
|
161
|
+
className={cn("border-[var(--ds-destructive)] text-[color:var(--ds-destructive)]", className)}
|
|
162
|
+
{...props}
|
|
163
|
+
>
|
|
164
|
+
{children}
|
|
165
|
+
</ChartState>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Loading placeholder: a skeleton the size of the drawing area, shown while a Nivo chunk loads. */
|
|
170
|
+
function ChartLoading({ className, ...props }: React.ComponentProps<"div">) {
|
|
171
|
+
return (
|
|
172
|
+
<div data-slot="chart-loading" className={cn("h-full w-full", className)} {...props}>
|
|
173
|
+
<Skeleton className="h-full w-full rounded-[var(--ds-radius-md)]" />
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── tooltip ─────────────────────────────────────────────────────────────── */
|
|
179
|
+
|
|
180
|
+
export interface ChartTooltipContentProps
|
|
181
|
+
extends Partial<Pick<TooltipContentProps, "active" | "payload" | "label">> {
|
|
182
|
+
className?: string;
|
|
183
|
+
/** Hide the series label column. */
|
|
184
|
+
hideLabel?: boolean;
|
|
185
|
+
/** Hide the colour swatch. */
|
|
186
|
+
hideIndicator?: boolean;
|
|
187
|
+
/** Format a value; defaults to `toLocaleString`. */
|
|
188
|
+
formatter?: (value: number | string, name: string) => React.ReactNode;
|
|
189
|
+
/** Format the header (the category). */
|
|
190
|
+
labelFormatter?: (label: React.ReactNode) => React.ReactNode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatValue(value: unknown, formatter?: ChartTooltipContentProps["formatter"], name = ""): React.ReactNode {
|
|
194
|
+
if (formatter && (typeof value === "number" || typeof value === "string")) return formatter(value, name);
|
|
195
|
+
if (typeof value === "number") return value.toLocaleString();
|
|
196
|
+
if (value == null) return "";
|
|
197
|
+
return String(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* ChartTooltipContent — the popover body for Recharts' Tooltip, styled like
|
|
202
|
+
* the package Tooltip on the popover tokens. Pass as `content` to
|
|
203
|
+
* ChartTooltip; reads series labels and colours from the ChartContainer config.
|
|
204
|
+
*/
|
|
205
|
+
function ChartTooltipContent({
|
|
206
|
+
active,
|
|
207
|
+
payload,
|
|
208
|
+
label,
|
|
209
|
+
className,
|
|
210
|
+
hideLabel,
|
|
211
|
+
hideIndicator,
|
|
212
|
+
formatter,
|
|
213
|
+
labelFormatter,
|
|
214
|
+
}: ChartTooltipContentProps) {
|
|
215
|
+
const chart = useChart();
|
|
216
|
+
if (!active || !payload?.length) return null;
|
|
217
|
+
const heading = labelFormatter ? labelFormatter(label) : label;
|
|
218
|
+
return (
|
|
219
|
+
<div
|
|
220
|
+
data-slot="chart-tooltip"
|
|
221
|
+
className={cn(
|
|
222
|
+
"grid min-w-32 gap-1.5 rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)] bg-[var(--ds-popover)] px-2.5 py-1.5",
|
|
223
|
+
"text-[length:var(--ds-text-caption)] text-[color:var(--ds-popover-foreground)] shadow-[var(--ds-shadow-md)]",
|
|
224
|
+
className,
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{!hideLabel && heading != null && heading !== "" ? (
|
|
228
|
+
<div className="font-medium">{heading}</div>
|
|
229
|
+
) : null}
|
|
230
|
+
<div className="grid gap-1">
|
|
231
|
+
{payload.map((item, i) => {
|
|
232
|
+
const key = String(item.dataKey ?? item.name ?? i);
|
|
233
|
+
const entry = chart?.config[key];
|
|
234
|
+
const colour = item.color ?? item.fill ?? item.stroke ?? `var(--color-${key})`;
|
|
235
|
+
const name = entry?.label ?? item.name ?? key;
|
|
236
|
+
return (
|
|
237
|
+
<div key={key} className="flex items-center gap-2">
|
|
238
|
+
{hideIndicator ? null : (
|
|
239
|
+
<span
|
|
240
|
+
aria-hidden
|
|
241
|
+
data-slot="chart-tooltip-indicator"
|
|
242
|
+
className="size-2 shrink-0 rounded-[var(--ds-radius-sm)]"
|
|
243
|
+
style={{ background: colour }}
|
|
244
|
+
/>
|
|
245
|
+
)}
|
|
246
|
+
<span className="flex-1 text-[color:var(--ds-muted-foreground)]">{name}</span>
|
|
247
|
+
<span className="font-[family-name:var(--ds-font-mono)] font-medium tabular-nums">
|
|
248
|
+
{formatValue(item.value, formatter, String(name))}
|
|
249
|
+
</span>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Recharts' Tooltip with the token-styled content and a subtle cursor. */
|
|
259
|
+
function ChartTooltip(props: TooltipProps) {
|
|
260
|
+
return <RechartsTooltip content={<ChartTooltipContent />} {...props} />;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ── legend ──────────────────────────────────────────────────────────────── */
|
|
264
|
+
|
|
265
|
+
export interface ChartLegendContentProps {
|
|
266
|
+
payload?: readonly LegendPayload[];
|
|
267
|
+
verticalAlign?: "top" | "bottom" | "middle";
|
|
268
|
+
className?: string;
|
|
269
|
+
hideIcon?: boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** ChartLegendContent — Recharts' legend on tokens; reads labels from the config. */
|
|
273
|
+
function ChartLegendContent({ payload, verticalAlign = "bottom", className, hideIcon }: ChartLegendContentProps) {
|
|
274
|
+
const chart = useChart();
|
|
275
|
+
if (!payload?.length) return null;
|
|
276
|
+
return (
|
|
277
|
+
<div
|
|
278
|
+
data-slot="chart-legend"
|
|
279
|
+
className={cn(
|
|
280
|
+
"flex flex-wrap items-center justify-center gap-x-4 gap-y-1",
|
|
281
|
+
"text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]",
|
|
282
|
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
283
|
+
className,
|
|
284
|
+
)}
|
|
285
|
+
>
|
|
286
|
+
{payload.map((item, i) => {
|
|
287
|
+
const key = String(item.dataKey ?? item.value ?? i);
|
|
288
|
+
const entry = chart?.config[key];
|
|
289
|
+
const Icon = entry?.icon;
|
|
290
|
+
return (
|
|
291
|
+
<div key={key} className="flex items-center gap-1.5">
|
|
292
|
+
{hideIcon ? null : Icon ? (
|
|
293
|
+
<Icon />
|
|
294
|
+
) : (
|
|
295
|
+
<span
|
|
296
|
+
aria-hidden
|
|
297
|
+
className="size-2 shrink-0 rounded-[var(--ds-radius-sm)]"
|
|
298
|
+
style={{ background: item.color ?? `var(--color-${key})` }}
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
{entry?.label ?? item.value}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
})}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Recharts' Legend with the token-styled content. */
|
|
310
|
+
function ChartLegend(props: LegendProps) {
|
|
311
|
+
return <RechartsLegend content={<ChartLegendContent />} {...props} />;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export {
|
|
315
|
+
ChartContainer,
|
|
316
|
+
ChartContext,
|
|
317
|
+
ChartEmpty,
|
|
318
|
+
ChartError,
|
|
319
|
+
ChartLoading,
|
|
320
|
+
ChartTooltip,
|
|
321
|
+
ChartTooltipContent,
|
|
322
|
+
ChartLegend,
|
|
323
|
+
ChartLegendContent,
|
|
324
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ChartConfig, ChartRow, ChartSeries, ChartSlice, ChartTree } from "./types";
|
|
2
|
+
|
|
3
|
+
/** True for the `{ id, data: [{ x, y }] }[]` series shape. */
|
|
4
|
+
export function isSeriesArray(data: unknown): data is ChartSeries[] {
|
|
5
|
+
return (
|
|
6
|
+
Array.isArray(data) &&
|
|
7
|
+
data.length > 0 &&
|
|
8
|
+
data.every((d) => d && typeof d === "object" && Array.isArray((d as ChartSeries).data))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Infer the category key and the series keys of a row array the way the
|
|
14
|
+
* webchat renderer did: the first string field is the index, every numeric
|
|
15
|
+
* field is a series. Falls back to the first key when nothing is a string.
|
|
16
|
+
*/
|
|
17
|
+
export function inferRowKeys(rows: ChartRow[]): { indexBy: string | undefined; keys: string[] } {
|
|
18
|
+
const sample = rows[0];
|
|
19
|
+
if (!sample) return { indexBy: undefined, keys: [] };
|
|
20
|
+
let indexBy: string | undefined;
|
|
21
|
+
const keys: string[] = [];
|
|
22
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
23
|
+
if (typeof value === "number") keys.push(key);
|
|
24
|
+
else if (!indexBy && typeof value === "string") indexBy = key;
|
|
25
|
+
}
|
|
26
|
+
if (!indexBy) indexBy = Object.keys(sample)[0];
|
|
27
|
+
return { indexBy, keys };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RowData {
|
|
31
|
+
rows: ChartRow[];
|
|
32
|
+
indexBy: string;
|
|
33
|
+
keys: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalise bar/line/area/radar data to rows. Series input is pivoted so each
|
|
38
|
+
* distinct x becomes a row and each series id a column; row input is used as
|
|
39
|
+
* given with inferred keys unless `indexBy`/`keys` are passed.
|
|
40
|
+
*/
|
|
41
|
+
export function toRows(data: ChartRow[] | ChartSeries[], indexBy?: string, keys?: string[]): RowData {
|
|
42
|
+
if (isSeriesArray(data)) {
|
|
43
|
+
const index = indexBy ?? "x";
|
|
44
|
+
const byX = new Map<string | number, ChartRow>();
|
|
45
|
+
const ids: string[] = [];
|
|
46
|
+
for (const series of data) {
|
|
47
|
+
const id = String(series.id);
|
|
48
|
+
ids.push(id);
|
|
49
|
+
for (const point of series.data) {
|
|
50
|
+
let row = byX.get(point.x);
|
|
51
|
+
if (!row) {
|
|
52
|
+
row = { [index]: point.x };
|
|
53
|
+
byX.set(point.x, row);
|
|
54
|
+
}
|
|
55
|
+
row[id] = point.y;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { rows: [...byX.values()], indexBy: index, keys: keys ?? ids };
|
|
59
|
+
}
|
|
60
|
+
const rows = data as ChartRow[];
|
|
61
|
+
const inferred = inferRowKeys(rows);
|
|
62
|
+
return {
|
|
63
|
+
rows,
|
|
64
|
+
indexBy: indexBy ?? inferred.indexBy ?? "x",
|
|
65
|
+
keys: keys ?? inferred.keys,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Pie/donut/funnel slices as `{ name, value }` rows with the id preserved. */
|
|
70
|
+
export function toSlices(data: ChartSlice[]): Array<{ id: string; name: string; value: number }> {
|
|
71
|
+
return data.map((d) => ({ id: String(d.id), name: d.label ?? String(d.id), value: d.value }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Build a ChartConfig for the given keys, keeping any entries the caller set. */
|
|
75
|
+
export function configFor(keys: string[], config?: ChartConfig): ChartConfig {
|
|
76
|
+
const out: ChartConfig = {};
|
|
77
|
+
for (const key of keys) out[key] = { label: key, ...config?.[key] };
|
|
78
|
+
// Keys the caller configured but the data lacks still get their colour slot.
|
|
79
|
+
for (const key of Object.keys(config ?? {})) if (!(key in out)) out[key] = config![key]!;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether a Nivo tree uses `name` rather than `id` as its identity. */
|
|
84
|
+
export function treeIdentity(tree: ChartTree): "id" | "name" {
|
|
85
|
+
return tree.id === undefined && tree.name !== undefined ? "name" : "id";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True when there is nothing to draw. */
|
|
89
|
+
export function isEmptyData(data: unknown): boolean {
|
|
90
|
+
if (data == null) return true;
|
|
91
|
+
if (Array.isArray(data)) {
|
|
92
|
+
if (data.length === 0) return true;
|
|
93
|
+
if (isSeriesArray(data)) return data.every((s) => s.data.length === 0);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (typeof data === "object") {
|
|
97
|
+
const tree = data as ChartTree;
|
|
98
|
+
return !tree.children?.length && tree.value === undefined;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cortena-ui Chart composite (DESIGN-8).
|
|
5
|
+
*
|
|
6
|
+
* One `Chart` with a `type` prop; Recharts for the everyday types, Nivo
|
|
7
|
+
* lazy-loaded for the rest. Consumers import everything chart-related from
|
|
8
|
+
* here, including the themed Recharts primitives for custom charts, and never
|
|
9
|
+
* from `recharts` or `@nivo/*` directly.
|
|
10
|
+
*/
|
|
11
|
+
export { Chart } from "./chart";
|
|
12
|
+
export type { ChartProps } from "./chart";
|
|
13
|
+
export {
|
|
14
|
+
ChartContainer,
|
|
15
|
+
ChartEmpty,
|
|
16
|
+
ChartError,
|
|
17
|
+
ChartLegend,
|
|
18
|
+
ChartLegendContent,
|
|
19
|
+
ChartLoading,
|
|
20
|
+
ChartTooltip,
|
|
21
|
+
ChartTooltipContent,
|
|
22
|
+
seriesColor,
|
|
23
|
+
useChart,
|
|
24
|
+
} from "./container";
|
|
25
|
+
export type { ChartContainerProps, ChartLegendContentProps, ChartTooltipContentProps } from "./container";
|
|
26
|
+
export { nivoTheme, useNivoTheme } from "./nivo-theme";
|
|
27
|
+
export type { NivoTheme } from "./nivo-theme";
|
|
28
|
+
export {
|
|
29
|
+
chartScales,
|
|
30
|
+
resolveColor,
|
|
31
|
+
resolveVizSeries,
|
|
32
|
+
toHex,
|
|
33
|
+
vizSeriesVars,
|
|
34
|
+
vizVar,
|
|
35
|
+
VIZ_SERIES_COUNT,
|
|
36
|
+
} from "./tokens";
|
|
37
|
+
export type { ChartScales } from "./tokens";
|
|
38
|
+
export { chartEngine, NIVO_TYPES, RECHARTS_TYPES } from "./types";
|
|
39
|
+
export type {
|
|
40
|
+
ChartConfig,
|
|
41
|
+
ChartDataFor,
|
|
42
|
+
ChartDay,
|
|
43
|
+
ChartEngine,
|
|
44
|
+
ChartPoint,
|
|
45
|
+
ChartRenderer,
|
|
46
|
+
ChartRow,
|
|
47
|
+
ChartScale,
|
|
48
|
+
ChartSeries,
|
|
49
|
+
ChartSlice,
|
|
50
|
+
ChartTree,
|
|
51
|
+
ChartType,
|
|
52
|
+
} from "./types";
|
|
53
|
+
|
|
54
|
+
// Themed Recharts primitives, for composing a one-off chart inside
|
|
55
|
+
// ChartContainer. Re-exported so no app imports recharts directly.
|
|
56
|
+
export {
|
|
57
|
+
Area,
|
|
58
|
+
AreaChart,
|
|
59
|
+
Bar,
|
|
60
|
+
BarChart,
|
|
61
|
+
CartesianGrid,
|
|
62
|
+
Cell,
|
|
63
|
+
ComposedChart,
|
|
64
|
+
Funnel,
|
|
65
|
+
FunnelChart,
|
|
66
|
+
LabelList,
|
|
67
|
+
Line,
|
|
68
|
+
LineChart,
|
|
69
|
+
Pie,
|
|
70
|
+
PieChart,
|
|
71
|
+
PolarAngleAxis,
|
|
72
|
+
PolarGrid,
|
|
73
|
+
PolarRadiusAxis,
|
|
74
|
+
Radar,
|
|
75
|
+
RadarChart,
|
|
76
|
+
ReferenceLine,
|
|
77
|
+
ResponsiveContainer,
|
|
78
|
+
Scatter,
|
|
79
|
+
ScatterChart,
|
|
80
|
+
XAxis,
|
|
81
|
+
YAxis,
|
|
82
|
+
Legend as RechartsLegend,
|
|
83
|
+
Tooltip as RechartsTooltip,
|
|
84
|
+
} from "recharts";
|