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.
Files changed (68) hide show
  1. package/dist/index.d.ts +2004 -4
  2. package/dist/index.js +5949 -2
  3. package/dist/index.js.map +1 -1
  4. package/package.json +41 -5
  5. package/src/components/accordion.tsx +86 -0
  6. package/src/components/alert.tsx +81 -0
  7. package/src/components/avatar.tsx +53 -0
  8. package/src/components/badge.tsx +51 -0
  9. package/src/components/calendar.tsx +193 -0
  10. package/src/components/card.tsx +85 -0
  11. package/src/components/chart/chart.tsx +193 -0
  12. package/src/components/chart/container.tsx +324 -0
  13. package/src/components/chart/data.ts +101 -0
  14. package/src/components/chart/index.tsx +84 -0
  15. package/src/components/chart/nivo-charts.tsx +227 -0
  16. package/src/components/chart/nivo-theme.ts +136 -0
  17. package/src/components/chart/recharts-charts.tsx +232 -0
  18. package/src/components/chart/tokens.ts +210 -0
  19. package/src/components/chart/types.ts +142 -0
  20. package/src/components/checkbox.tsx +49 -0
  21. package/src/components/chip.tsx +49 -0
  22. package/src/components/combobox.tsx +405 -0
  23. package/src/components/command.tsx +225 -0
  24. package/src/components/data-table/data-table.tsx +731 -0
  25. package/src/components/data-table/export.ts +121 -0
  26. package/src/components/data-table/features.ts +123 -0
  27. package/src/components/data-table/index.tsx +45 -0
  28. package/src/components/data-table/parts.tsx +568 -0
  29. package/src/components/data-table/system-columns.tsx +91 -0
  30. package/src/components/data-table/types.ts +182 -0
  31. package/src/components/data-table/use-data-table.ts +224 -0
  32. package/src/components/data-table/use-server-source.ts +232 -0
  33. package/src/components/date-field.tsx +327 -0
  34. package/src/components/date-picker.tsx +230 -0
  35. package/src/components/dialog.tsx +146 -0
  36. package/src/components/dropdown-menu.tsx +314 -0
  37. package/src/components/dropzone.tsx +260 -0
  38. package/src/components/empty-state.tsx +91 -0
  39. package/src/components/error-banner.tsx +65 -0
  40. package/src/components/field.tsx +250 -0
  41. package/src/components/form.tsx +212 -0
  42. package/src/components/input.tsx +32 -0
  43. package/src/components/kbd.tsx +26 -0
  44. package/src/components/label.tsx +26 -0
  45. package/src/components/loading-skeleton.tsx +153 -0
  46. package/src/components/markdown.tsx +377 -0
  47. package/src/components/page-header.tsx +99 -0
  48. package/src/components/popover.tsx +133 -0
  49. package/src/components/progress.tsx +83 -0
  50. package/src/components/radio-group.tsx +48 -0
  51. package/src/components/scroll-area.tsx +74 -0
  52. package/src/components/section-card.tsx +75 -0
  53. package/src/components/segmented.tsx +81 -0
  54. package/src/components/select.tsx +222 -0
  55. package/src/components/separator.tsx +41 -0
  56. package/src/components/sheet.tsx +161 -0
  57. package/src/components/skeleton.tsx +22 -0
  58. package/src/components/sortable-list.tsx +275 -0
  59. package/src/components/spinner.tsx +32 -0
  60. package/src/components/status-dot.tsx +78 -0
  61. package/src/components/switch.tsx +48 -0
  62. package/src/components/tabs.tsx +109 -0
  63. package/src/components/textarea.tsx +75 -0
  64. package/src/components/toast.tsx +150 -0
  65. package/src/components/toolbar.tsx +63 -0
  66. package/src/components/tooltip.tsx +103 -0
  67. package/src/hooks/use-cortena-theme.ts +268 -0
  68. 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";