@alpic-ai/ui 1.161.0 → 1.162.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/accordion-card.d.mts +1 -5
- package/dist/components/accordion-card.mjs +1 -5
- package/dist/components/accordion.d.mts +1 -5
- package/dist/components/accordion.mjs +1 -5
- package/dist/components/attachment-tile.d.mts +0 -1
- package/dist/components/chart-legend.d.mts +4 -1
- package/dist/components/chart-legend.mjs +21 -7
- package/dist/components/chart-primitives.d.mts +3 -8
- package/dist/components/chart-primitives.mjs +3 -8
- package/dist/components/combobox.d.mts +0 -1
- package/dist/components/line-chart.d.mts +3 -1
- package/dist/components/line-chart.mjs +7 -4
- package/dist/components/select-trigger-variants.d.mts +1 -5
- package/dist/components/select-trigger-variants.mjs +1 -5
- package/dist/components/table.d.mts +1 -5
- package/dist/components/task-progress.d.mts +1 -4
- package/dist/lib/chart-palette.d.mts +1 -20
- package/dist/lib/chart-palette.mjs +1 -20
- package/dist/lib/chart.d.mts +1 -4
- package/dist/lib/chart.mjs +2 -11
- package/package.json +5 -5
- package/src/components/accordion-card.tsx +1 -5
- package/src/components/accordion.tsx +1 -5
- package/src/components/attachment-tile.tsx +0 -1
- package/src/components/bar-chart.tsx +1 -2
- package/src/components/bar-list.tsx +1 -2
- package/src/components/chart-legend.tsx +47 -10
- package/src/components/chart-primitives.tsx +3 -9
- package/src/components/combobox.tsx +1 -5
- package/src/components/dropdown-menu.tsx +0 -2
- package/src/components/heatmap-chart.tsx +4 -10
- package/src/components/line-chart.tsx +16 -3
- package/src/components/select-trigger-variants.ts +1 -5
- package/src/components/table.tsx +1 -5
- package/src/components/tabs.tsx +1 -12
- package/src/components/task-progress.tsx +2 -14
- package/src/components/wizard.tsx +0 -6
- package/src/lib/chart-palette.ts +3 -22
- package/src/lib/chart.ts +3 -15
- package/src/stories/button.stories.tsx +1 -2
- package/src/stories/heatmap-chart.stories.tsx +0 -1
- package/src/stories/line-chart.stories.tsx +120 -80
- package/src/stories/sidebar.stories.tsx +1 -4
- package/src/stories/wizard.stories.tsx +0 -1
|
@@ -9,11 +9,7 @@ declare function AccordionCard({ defaultOpen, open, onOpenChange, disabled, clas
|
|
|
9
9
|
} & Omit<React$1.ComponentProps<typeof AccordionPrimitive.Root>, "type" | "collapsible" | "defaultValue" | "value" | "onValueChange" | "disabled">): React$1.JSX.Element;
|
|
10
10
|
declare function AccordionCardHeader({ className, children, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Trigger>): React$1.JSX.Element;
|
|
11
11
|
declare function AccordionCardTitle({ className, children, ...props }: React$1.HTMLAttributes<HTMLHeadingElement>): React$1.JSX.Element;
|
|
12
|
-
/**
|
|
13
|
-
* `className` is forwarded to the inner padding wrapper, not the Radix Content root.
|
|
14
|
-
* The root always applies overflow-hidden and the accordion animations.
|
|
15
|
-
* To override overflow or animation behavior, wrap this component in an additional element.
|
|
16
|
-
*/
|
|
12
|
+
/** `className` styles the inner padding wrapper, not the Radix Content root, which keeps overflow-hidden and the accordion animations. */
|
|
17
13
|
declare function AccordionCardContent({ className, children, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Content>): React$1.JSX.Element;
|
|
18
14
|
//#endregion
|
|
19
15
|
export { AccordionCard, AccordionCardContent, AccordionCardHeader, AccordionCardTitle };
|
|
@@ -41,11 +41,7 @@ function AccordionCardTitle({ className, children, ...props }) {
|
|
|
41
41
|
children
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
45
|
-
* `className` is forwarded to the inner padding wrapper, not the Radix Content root.
|
|
46
|
-
* The root always applies overflow-hidden and the accordion animations.
|
|
47
|
-
* To override overflow or animation behavior, wrap this component in an additional element.
|
|
48
|
-
*/
|
|
44
|
+
/** `className` styles the inner padding wrapper, not the Radix Content root, which keeps overflow-hidden and the accordion animations. */
|
|
49
45
|
function AccordionCardContent({ className, children, ...props }) {
|
|
50
46
|
return /* @__PURE__ */ jsx(AccordionPrimitive.Content, {
|
|
51
47
|
"data-slot": "accordion-card-content",
|
|
@@ -4,11 +4,7 @@ import * as React$1 from "react";
|
|
|
4
4
|
declare function Accordion({ ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Root>): React$1.JSX.Element;
|
|
5
5
|
declare function AccordionItem({ className, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Item>): React$1.JSX.Element;
|
|
6
6
|
declare function AccordionTrigger({ className, children, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Trigger>): React$1.JSX.Element;
|
|
7
|
-
/**
|
|
8
|
-
* `className` is forwarded to the inner padding wrapper, not the Radix Content root.
|
|
9
|
-
* The root always applies overflow-hidden and the accordion animations.
|
|
10
|
-
* To override overflow or animation behavior, wrap this component in an additional element.
|
|
11
|
-
*/
|
|
7
|
+
/** `className` styles the inner padding wrapper, not the Radix Content root, which keeps overflow-hidden and the accordion animations. */
|
|
12
8
|
declare function AccordionContent({ className, children, ...props }: React$1.ComponentProps<typeof AccordionPrimitive.Content>): React$1.JSX.Element;
|
|
13
9
|
//#endregion
|
|
14
10
|
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
|
@@ -28,11 +28,7 @@ function AccordionTrigger({ className, children, ...props }) {
|
|
|
28
28
|
})
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
* `className` is forwarded to the inner padding wrapper, not the Radix Content root.
|
|
33
|
-
* The root always applies overflow-hidden and the accordion animations.
|
|
34
|
-
* To override overflow or animation behavior, wrap this component in an additional element.
|
|
35
|
-
*/
|
|
31
|
+
/** `className` styles the inner padding wrapper, not the Radix Content root, which keeps overflow-hidden and the accordion animations. */
|
|
36
32
|
function AccordionContent({ className, children, ...props }) {
|
|
37
33
|
return /* @__PURE__ */ jsx(AccordionPrimitive.Content, {
|
|
38
34
|
"data-slot": "accordion-content",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/components/chart-legend.d.ts
|
|
2
2
|
interface ChartLegendItem {
|
|
3
|
+
key: string;
|
|
3
4
|
name: string;
|
|
4
5
|
color: string;
|
|
5
6
|
dashed?: boolean;
|
|
@@ -8,7 +9,9 @@ interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
8
9
|
items: ChartLegendItem[];
|
|
9
10
|
align?: "left" | "center" | "right";
|
|
10
11
|
insetLeft?: number;
|
|
12
|
+
hiddenKeys?: ReadonlySet<string>;
|
|
13
|
+
onItemClick?: (key: string) => void;
|
|
11
14
|
}
|
|
12
|
-
declare function ChartLegend({ items, align, insetLeft, className, style, ...props }: ChartLegendProps): import("react").JSX.Element;
|
|
15
|
+
declare function ChartLegend({ items, align, insetLeft, hiddenKeys, onItemClick, className, style, ...props }: ChartLegendProps): import("react").JSX.Element;
|
|
13
16
|
//#endregion
|
|
14
17
|
export { ChartLegend, ChartLegendItem, ChartLegendProps };
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/components/chart-legend.tsx
|
|
5
5
|
const ALIGN_CLASS = {
|
|
6
6
|
left: "justify-start",
|
|
7
7
|
center: "justify-center",
|
|
8
8
|
right: "justify-end"
|
|
9
9
|
};
|
|
10
|
+
const ITEM_CLASS = "inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]";
|
|
11
|
+
const INTERACTIVE_CLASS = "rounded-sm outline-none transition-opacity focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card [@media(hover:hover)]:hover:text-foreground";
|
|
10
12
|
function Swatch({ color, dashed }) {
|
|
11
13
|
return /* @__PURE__ */ jsx("span", {
|
|
12
14
|
"aria-hidden": true,
|
|
@@ -14,7 +16,7 @@ function Swatch({ color, dashed }) {
|
|
|
14
16
|
style: dashed ? { border: `1.5px solid ${color}` } : { background: color }
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
|
-
function ChartLegend({ items, align = "left", insetLeft, className, style, ...props }) {
|
|
19
|
+
function ChartLegend({ items, align = "left", insetLeft, hiddenKeys, onItemClick, className, style, ...props }) {
|
|
18
20
|
return /* @__PURE__ */ jsx("div", {
|
|
19
21
|
className: cn("flex flex-wrap gap-x-4 gap-y-1.5", ALIGN_CLASS[align], className),
|
|
20
22
|
style: {
|
|
@@ -22,13 +24,25 @@ function ChartLegend({ items, align = "left", insetLeft, className, style, ...pr
|
|
|
22
24
|
...style
|
|
23
25
|
},
|
|
24
26
|
...props,
|
|
25
|
-
children: items.map((item) =>
|
|
26
|
-
|
|
27
|
-
children: [/* @__PURE__ */ jsx(Swatch, {
|
|
27
|
+
children: items.map((item) => {
|
|
28
|
+
const swatchAndName = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Swatch, {
|
|
28
29
|
color: item.color,
|
|
29
30
|
dashed: item.dashed
|
|
30
|
-
}), item.name]
|
|
31
|
-
|
|
31
|
+
}), item.name] });
|
|
32
|
+
if (!onItemClick) return /* @__PURE__ */ jsx("span", {
|
|
33
|
+
className: ITEM_CLASS,
|
|
34
|
+
children: swatchAndName
|
|
35
|
+
}, item.key);
|
|
36
|
+
const hidden = hiddenKeys?.has(item.key) ?? false;
|
|
37
|
+
return /* @__PURE__ */ jsx("button", {
|
|
38
|
+
type: "button",
|
|
39
|
+
"aria-label": `${item.name} series`,
|
|
40
|
+
"aria-pressed": !hidden,
|
|
41
|
+
onClick: () => onItemClick(item.key),
|
|
42
|
+
className: cn(ITEM_CLASS, INTERACTIVE_CLASS, hidden && "opacity-40"),
|
|
43
|
+
children: swatchAndName
|
|
44
|
+
}, item.key);
|
|
45
|
+
})
|
|
32
46
|
});
|
|
33
47
|
}
|
|
34
48
|
//#endregion
|
|
@@ -22,14 +22,12 @@ declare function makeChartAxis(theme: ChartTheme): {
|
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
24
|
declare function makeLegendItems(resolved: ResolvedSeries[]): {
|
|
25
|
+
key: string;
|
|
25
26
|
name: string;
|
|
26
27
|
color: string;
|
|
27
28
|
dashed: boolean | undefined;
|
|
28
29
|
}[];
|
|
29
|
-
/**
|
|
30
|
-
* Stacked series must reach the stack height (sum per x-point), not the tallest
|
|
31
|
-
* single series, or reference bands and explicit Y domains clip a tall stack short.
|
|
32
|
-
*/
|
|
30
|
+
/** Stacked series must reach the stack height (sum per x-point), or reference bands and explicit Y domains clip a tall stack short. */
|
|
33
31
|
declare function computeNumericMax(data: ChartData, series: ChartSeries[], stacked: boolean): number;
|
|
34
32
|
declare function makeActiveDot(entry: ResolvedSeries, { valueFlags, theme, valueFormatter }: {
|
|
35
33
|
valueFlags: boolean;
|
|
@@ -55,10 +53,7 @@ declare function makeLastValueLabel(color: string, { dataLength, theme, valueFor
|
|
|
55
53
|
value?: string | number | boolean | Array<string | number | boolean> | null;
|
|
56
54
|
index?: number;
|
|
57
55
|
}) => import("react").JSX.Element | null;
|
|
58
|
-
/**
|
|
59
|
-
* Returns recharts elements (not a wrapper component) so recharts still detects
|
|
60
|
-
* them as direct children when spread into a chart via `{renderReferenceLine(...)}`.
|
|
61
|
-
*/
|
|
56
|
+
/** Returns recharts elements (not a wrapper component) so recharts still detects them as direct chart children. */
|
|
62
57
|
declare function renderReferenceLine({ referenceLine, numericMax, theme }: {
|
|
63
58
|
referenceLine: ReferenceLineConfig | undefined;
|
|
64
59
|
numericMax: number;
|
|
@@ -19,15 +19,13 @@ function makeChartAxis(theme) {
|
|
|
19
19
|
}
|
|
20
20
|
function makeLegendItems(resolved) {
|
|
21
21
|
return resolved.map((entry) => ({
|
|
22
|
+
key: entry.key,
|
|
22
23
|
name: entry.name,
|
|
23
24
|
color: entry.color,
|
|
24
25
|
dashed: entry.dashed
|
|
25
26
|
}));
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Stacked series must reach the stack height (sum per x-point), not the tallest
|
|
29
|
-
* single series, or reference bands and explicit Y domains clip a tall stack short.
|
|
30
|
-
*/
|
|
28
|
+
/** Stacked series must reach the stack height (sum per x-point), or reference bands and explicit Y domains clip a tall stack short. */
|
|
31
29
|
function computeNumericMax(data, series, stacked) {
|
|
32
30
|
let max = 0;
|
|
33
31
|
for (const row of data) {
|
|
@@ -87,10 +85,7 @@ function makeLastValueLabel(color, { dataLength, theme, valueFormatter }) {
|
|
|
87
85
|
});
|
|
88
86
|
};
|
|
89
87
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Returns recharts elements (not a wrapper component) so recharts still detects
|
|
92
|
-
* them as direct children when spread into a chart via `{renderReferenceLine(...)}`.
|
|
93
|
-
*/
|
|
88
|
+
/** Returns recharts elements (not a wrapper component) so recharts still detects them as direct chart children. */
|
|
94
89
|
function renderReferenceLine({ referenceLine, numericMax, theme }) {
|
|
95
90
|
if (!referenceLine) return null;
|
|
96
91
|
return [referenceLine.band ? /* @__PURE__ */ jsx(ReferenceArea, {
|
|
@@ -42,7 +42,6 @@ declare function ComboboxEmpty({ className, ...props }: React.ComponentProps<typ
|
|
|
42
42
|
declare function ComboboxGroup({ className, ...props }: React.ComponentProps<typeof Command.Group>): import("react").JSX.Element;
|
|
43
43
|
declare function ComboboxSeparator({ className, ...props }: React.ComponentProps<typeof Command.Separator>): import("react").JSX.Element;
|
|
44
44
|
interface ComboboxItemProps extends Omit<React.ComponentProps<typeof Command.Item>, "onSelect"> {
|
|
45
|
-
/** The value stored when this item is selected */
|
|
46
45
|
itemValue: string;
|
|
47
46
|
}
|
|
48
47
|
declare function ComboboxItem({ className, children, itemValue, ...props }: ComboboxItemProps): import("react").JSX.Element;
|
|
@@ -28,10 +28,12 @@ interface LineChartProps {
|
|
|
28
28
|
markers?: ChartMarker[];
|
|
29
29
|
lastValueLabel?: boolean;
|
|
30
30
|
loading?: boolean;
|
|
31
|
+
hiddenSeries?: ReadonlySet<string>;
|
|
32
|
+
onLegendItemClick?: (key: string) => void;
|
|
31
33
|
valueFormatter?: (value: number) => string;
|
|
32
34
|
labelFormatter?: (label: string | number) => string;
|
|
33
35
|
className?: string;
|
|
34
36
|
}
|
|
35
|
-
declare function LineChart({ data, index, series, curve, legend, legendAlign, valueFlags, dots, height, yAxisWidth, palette, referenceLine, markers, lastValueLabel, loading, valueFormatter, labelFormatter, className }: LineChartProps): React$1.JSX.Element;
|
|
37
|
+
declare function LineChart({ data, index, series, curve, legend, legendAlign, valueFlags, dots, height, yAxisWidth, palette, referenceLine, markers, lastValueLabel, loading, hiddenSeries, onLegendItemClick, valueFormatter, labelFormatter, className }: LineChartProps): React$1.JSX.Element;
|
|
36
38
|
//#endregion
|
|
37
39
|
export { LineChart, LineChartProps };
|
|
@@ -15,10 +15,11 @@ const CURVE_TYPE = {
|
|
|
15
15
|
linear: "linear",
|
|
16
16
|
step: "stepAfter"
|
|
17
17
|
};
|
|
18
|
-
function LineChart({ data, index, series, curve = "monotone", legend = false, legendAlign = "left", valueFlags = false, dots = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
18
|
+
function LineChart({ data, index, series, curve = "monotone", legend = false, legendAlign = "left", valueFlags = false, dots = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, loading = false, hiddenSeries, onLegendItemClick, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
19
19
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
20
20
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
21
|
-
const
|
|
21
|
+
const visible = resolved.filter((entry) => !hiddenSeries?.has(entry.key));
|
|
22
|
+
const numericMax = React$1.useMemo(() => computeNumericMax(data, visible, false), [data, visible]);
|
|
22
23
|
const curveType = CURVE_TYPE[curve];
|
|
23
24
|
const margin = {
|
|
24
25
|
top: markers?.length ? 18 : 8,
|
|
@@ -88,7 +89,7 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
88
89
|
numericMax,
|
|
89
90
|
theme
|
|
90
91
|
}),
|
|
91
|
-
|
|
92
|
+
visible.map((entry) => /* @__PURE__ */ jsx(Line, {
|
|
92
93
|
type: curveType,
|
|
93
94
|
dataKey: entry.key,
|
|
94
95
|
name: entry.name,
|
|
@@ -124,7 +125,9 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
124
125
|
}), legend && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
|
|
125
126
|
items: legendItems,
|
|
126
127
|
align: legendAlign,
|
|
127
|
-
insetLeft: yAxisWidth
|
|
128
|
+
insetLeft: yAxisWidth,
|
|
129
|
+
hiddenKeys: hiddenSeries,
|
|
130
|
+
onItemClick: onLegendItemClick
|
|
128
131
|
})]
|
|
129
132
|
});
|
|
130
133
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
//#region src/components/select-trigger-variants.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Shared trigger styles for Select and Combobox.
|
|
4
|
-
* Both components use the same visual treatment for their trigger buttons.
|
|
5
|
-
* Edit here to keep them in sync.
|
|
6
|
-
*/
|
|
2
|
+
/** Shared by Select and Combobox so their trigger buttons stay visually in sync. */
|
|
7
3
|
declare const selectTriggerVariants: (props?: ({
|
|
8
4
|
size?: "sm" | "md" | null | undefined;
|
|
9
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { cva } from "class-variance-authority";
|
|
2
2
|
//#region src/components/select-trigger-variants.ts
|
|
3
|
-
/**
|
|
4
|
-
* Shared trigger styles for Select and Combobox.
|
|
5
|
-
* Both components use the same visual treatment for their trigger buttons.
|
|
6
|
-
* Edit here to keep them in sync.
|
|
7
|
-
*/
|
|
3
|
+
/** Shared by Select and Combobox so their trigger buttons stay visually in sync. */
|
|
8
4
|
const selectTriggerVariants = cva([
|
|
9
5
|
"flex w-full items-center gap-2 rounded-md border bg-background text-foreground shadow-xs outline-hidden",
|
|
10
6
|
"transition-colors",
|
|
@@ -10,11 +10,7 @@ declare function TableFooter({ className, ...props }: React$1.ComponentProps<"tf
|
|
|
10
10
|
declare function TableRow({ className, ...props }: React$1.ComponentProps<"tr">): React$1.JSX.Element;
|
|
11
11
|
declare function TableHead({ className, ...props }: React$1.ComponentProps<"th">): React$1.JSX.Element;
|
|
12
12
|
interface TableCellProps extends React$1.ComponentProps<"td"> {
|
|
13
|
-
/**
|
|
14
|
-
* When true, the cell renders edge-to-edge so the child can act as the
|
|
15
|
-
* interactive surface (e.g. a button or popover trigger filling the cell).
|
|
16
|
-
* Defaults to false (standard padded cell).
|
|
17
|
-
*/
|
|
13
|
+
/** Renders the cell edge-to-edge so the child (e.g. a button or popover trigger) can fill it as the interactive surface. */
|
|
18
14
|
interactive?: boolean;
|
|
19
15
|
}
|
|
20
16
|
declare function TableCell({ className, interactive, ...props }: TableCellProps): React$1.JSX.Element;
|
|
@@ -8,10 +8,7 @@ interface TaskProgressStep {
|
|
|
8
8
|
interface TaskProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
9
9
|
steps: readonly TaskProgressStep[];
|
|
10
10
|
trailingLabel?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Optional mount-time stagger: each row fades + slides in with `animation-delay = index × stepRevealDelayMs`.
|
|
13
|
-
* Pure CSS, runs once on mount. Omit for no animation.
|
|
14
|
-
*/
|
|
11
|
+
/** Mount-time stagger: each row fades in after `index × stepRevealDelayMs`; omit for no animation. */
|
|
15
12
|
stepRevealDelayMs?: number;
|
|
16
13
|
}
|
|
17
14
|
declare function TaskProgress({ steps, trailingLabel, stepRevealDelayMs, className, ...props }: TaskProgressProps): import("react").JSX.Element;
|
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
//#region src/lib/chart-palette.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Chart palettes — the locked "paired system" from the analytics-v2 design.
|
|
4
|
-
*
|
|
5
|
-
* These are charting-API color values (consumed by Recharts at draw time, which
|
|
6
|
-
* cannot read CSS custom properties), so per the design-system rules they live
|
|
7
|
-
* as documented hex constants rather than `--color-*` tokens. They are
|
|
8
|
-
* theme-independent: the same series colors read on light and dark canvases.
|
|
9
|
-
* Only the chrome (grid, axis text, tooltip surface, semantic colors) is
|
|
10
|
-
* theme-aware — see `useChartTheme`.
|
|
11
|
-
*
|
|
12
|
-
* Rules baked in:
|
|
13
|
-
* - Pink leads (index 0) — never buried, fixing the V1 "pink last" bug.
|
|
14
|
-
* - Adjacent dashboard cards alternate lead palette (magenta / cyan) — the
|
|
15
|
-
* page decides which to pass; the component just receives one.
|
|
16
|
-
*/
|
|
17
2
|
declare const MAGENTA_PALETTE: readonly ["#da1b6a", "#ec82b0", "#9a67d7", "#668fdf", "#47bde0", "#75ddd8", "#cd8c4d", "#3eb0a1"];
|
|
18
3
|
declare const CYAN_PALETTE: readonly ["#2eb2c5", "#50d0c0", "#4f90e0", "#8d74e0", "#bc73e0", "#ec82b0", "#da1b6a", "#c08dd3"];
|
|
19
4
|
type ChartPaletteName = "magenta" | "cyan";
|
|
@@ -28,11 +13,7 @@ declare const heatRamp: (palette: ChartPaletteName, empty: string, isDark?: bool
|
|
|
28
13
|
declare const luminance: (hex: string) => number;
|
|
29
14
|
declare const paletteColor: (palette: readonly string[], index: number) => string;
|
|
30
15
|
declare const rampColor: (palette: ChartPaletteName, fraction: number) => string;
|
|
31
|
-
/**
|
|
32
|
-
* Interpolate continuously across a multi-stop heat ramp (e.g. `heatRampMagenta`),
|
|
33
|
-
* so a normalized 0..1 value reads as a smooth single-hue gradient rather than
|
|
34
|
-
* the five discrete bands.
|
|
35
|
-
*/
|
|
16
|
+
/** Interpolates continuously across the ramp so a 0..1 value reads as a smooth gradient rather than five discrete bands. */
|
|
36
17
|
declare const heatColor: (stops: readonly string[], fraction: number) => string;
|
|
37
18
|
//#endregion
|
|
38
19
|
export { CHART_PALETTES, CYAN_PALETTE, ChartPaletteName, HEAT_EMPTY, MAGENTA_PALETTE, heatColor, heatRamp, heatRampMagenta, heatRampMint, luminance, paletteColor, rampColor };
|
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
//#region src/lib/chart-palette.ts
|
|
2
|
-
/**
|
|
3
|
-
* Chart palettes — the locked "paired system" from the analytics-v2 design.
|
|
4
|
-
*
|
|
5
|
-
* These are charting-API color values (consumed by Recharts at draw time, which
|
|
6
|
-
* cannot read CSS custom properties), so per the design-system rules they live
|
|
7
|
-
* as documented hex constants rather than `--color-*` tokens. They are
|
|
8
|
-
* theme-independent: the same series colors read on light and dark canvases.
|
|
9
|
-
* Only the chrome (grid, axis text, tooltip surface, semantic colors) is
|
|
10
|
-
* theme-aware — see `useChartTheme`.
|
|
11
|
-
*
|
|
12
|
-
* Rules baked in:
|
|
13
|
-
* - Pink leads (index 0) — never buried, fixing the V1 "pink last" bug.
|
|
14
|
-
* - Adjacent dashboard cards alternate lead palette (magenta / cyan) — the
|
|
15
|
-
* page decides which to pass; the component just receives one.
|
|
16
|
-
*/
|
|
17
2
|
const MAGENTA_PALETTE = [
|
|
18
3
|
"#da1b6a",
|
|
19
4
|
"#ec82b0",
|
|
@@ -101,11 +86,7 @@ const rampColor = (palette, fraction) => {
|
|
|
101
86
|
const [from, to] = RAMP_ENDS[palette];
|
|
102
87
|
return mixHex(from, to, fraction);
|
|
103
88
|
};
|
|
104
|
-
/**
|
|
105
|
-
* Interpolate continuously across a multi-stop heat ramp (e.g. `heatRampMagenta`),
|
|
106
|
-
* so a normalized 0..1 value reads as a smooth single-hue gradient rather than
|
|
107
|
-
* the five discrete bands.
|
|
108
|
-
*/
|
|
89
|
+
/** Interpolates continuously across the ramp so a 0..1 value reads as a smooth gradient rather than five discrete bands. */
|
|
109
90
|
const heatColor = (stops, fraction) => {
|
|
110
91
|
const clamped = Math.min(1, Math.max(0, fraction));
|
|
111
92
|
const segment = (stops.length - 1) * clamped;
|
package/dist/lib/chart.d.mts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import "react";
|
|
2
2
|
//#region src/lib/chart.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Declarative description of one plotted series. The same shape drives area,
|
|
5
|
-
* line and bar charts — what differs is how each chart renders it.
|
|
6
|
-
*/
|
|
3
|
+
/** One plotted series; the same shape drives area, line and bar charts. */
|
|
7
4
|
interface ChartSeries {
|
|
8
5
|
key: string;
|
|
9
6
|
name?: string;
|
package/dist/lib/chart.mjs
CHANGED
|
@@ -7,22 +7,13 @@ const semanticColor = (theme, semantic) => {
|
|
|
7
7
|
if (semantic === "warning") return theme.warning;
|
|
8
8
|
return theme.success;
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
* Resolve each series to a concrete color and name. Color precedence:
|
|
12
|
-
* explicit `color` → `semantic` token → palette slot by declared index (so the
|
|
13
|
-
* lead series keeps the lead color even after the bands are reordered).
|
|
14
|
-
*/
|
|
10
|
+
/** Palette fallback uses the declared index so the lead series keeps the lead color even after reordering. */
|
|
15
11
|
const resolveSeries = (series, palette, theme) => series.map((entry, index) => ({
|
|
16
12
|
...entry,
|
|
17
13
|
name: entry.name ?? entry.key,
|
|
18
14
|
color: entry.color ?? (entry.semantic ? semanticColor(theme, entry.semantic) : paletteColor(palette, index))
|
|
19
15
|
}));
|
|
20
|
-
/**
|
|
21
|
-
* Stacked bands read cleanest as a vertical gradient: darkest at the baseline,
|
|
22
|
-
* lightest at the top edge. Sorting by luminance enforces that for any palette
|
|
23
|
-
* (this is what fixed the "muddy cyan" stack in the lab). Render order maps to
|
|
24
|
-
* stack order in Recharts — first entry sits at the bottom.
|
|
25
|
-
*/
|
|
16
|
+
/** Stacked bands read cleanest darkest-at-baseline to lightest-on-top; sorting by luminance enforces that for any palette. */
|
|
26
17
|
const orderByLuminance = (series) => [...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
|
|
27
18
|
const makeXAxisTick = (theme) => ({ x, y, payload, index, visibleTicksCount }) => {
|
|
28
19
|
const isFirst = index === 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.162.0",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@ladle/react": "^5.1.1",
|
|
64
64
|
"@tailwindcss/postcss": "^4.3.3",
|
|
65
|
-
"@types/react": "19.2.
|
|
66
|
-
"@types/react-dom": "19.2.
|
|
67
|
-
"lucide-react": "^1.
|
|
68
|
-
"react-hook-form": "^7.
|
|
65
|
+
"@types/react": "19.2.18",
|
|
66
|
+
"@types/react-dom": "19.2.4",
|
|
67
|
+
"lucide-react": "^1.28.0",
|
|
68
|
+
"react-hook-form": "^7.84.0",
|
|
69
69
|
"shx": "^0.4.0",
|
|
70
70
|
"sonner": "^2.0.7",
|
|
71
71
|
"tailwindcss": "^4.3.3",
|
|
@@ -79,11 +79,7 @@ function AccordionCardTitle({ className, children, ...props }: React.HTMLAttribu
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
* `className` is forwarded to the inner padding wrapper, not the Radix Content root.
|
|
84
|
-
* The root always applies overflow-hidden and the accordion animations.
|
|
85
|
-
* To override overflow or animation behavior, wrap this component in an additional element.
|
|
86
|
-
*/
|
|
82
|
+
/** `className` styles the inner padding wrapper, not the Radix Content root, which keeps overflow-hidden and the accordion animations. */
|
|
87
83
|
function AccordionCardContent({
|
|
88
84
|
className,
|
|
89
85
|
children,
|
|
@@ -43,11 +43,7 @@ function AccordionTrigger({ className, children, ...props }: React.ComponentProp
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* `className` is forwarded to the inner padding wrapper, not the Radix Content root.
|
|
48
|
-
* The root always applies overflow-hidden and the accordion animations.
|
|
49
|
-
* To override overflow or animation behavior, wrap this component in an additional element.
|
|
50
|
-
*/
|
|
46
|
+
/** `className` styles the inner padding wrapper, not the Radix Content root, which keeps overflow-hidden and the accordion animations. */
|
|
51
47
|
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
52
48
|
return (
|
|
53
49
|
<AccordionPrimitive.Content
|
|
@@ -122,8 +122,7 @@ function BarChart({
|
|
|
122
122
|
if (texture && lead && entry.key === lead.key) {
|
|
123
123
|
return `url(#bar-hatch-${reactId})`;
|
|
124
124
|
}
|
|
125
|
-
// Stacked segments
|
|
126
|
-
// banding looks like a drop shadow between bands. Grouped bars keep the fade.
|
|
125
|
+
// Stacked segments stay flat solids — per-segment gradients read as drop shadows between bands; grouped bars keep the fade.
|
|
127
126
|
if (stacked) {
|
|
128
127
|
return entry.color;
|
|
129
128
|
}
|
|
@@ -27,8 +27,7 @@ const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "succe
|
|
|
27
27
|
const PLACEHOLDER_HEIGHT = 168;
|
|
28
28
|
// Cap the ramp off its palest end so low-rank bars stay legible on a light card.
|
|
29
29
|
const RAMP_CEILING = 0.8;
|
|
30
|
-
//
|
|
31
|
-
// (mixed toward white) so a long error list never fades to near-invisible.
|
|
30
|
+
// The lightest semantic bar keeps this much hue (mixed toward white) so a long list never fades to near-invisible.
|
|
32
31
|
const SEMANTIC_FLOOR = 0.62;
|
|
33
32
|
const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
|
|
34
33
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
|
|
5
5
|
export interface ChartLegendItem {
|
|
6
|
+
key: string;
|
|
6
7
|
name: string;
|
|
7
8
|
color: string;
|
|
8
9
|
dashed?: boolean;
|
|
@@ -12,10 +13,18 @@ export interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
12
13
|
items: ChartLegendItem[];
|
|
13
14
|
align?: "left" | "center" | "right";
|
|
14
15
|
insetLeft?: number;
|
|
16
|
+
hiddenKeys?: ReadonlySet<string>;
|
|
17
|
+
onItemClick?: (key: string) => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const ALIGN_CLASS = { left: "justify-start", center: "justify-center", right: "justify-end" } as const;
|
|
18
21
|
|
|
22
|
+
const ITEM_CLASS =
|
|
23
|
+
"inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]";
|
|
24
|
+
|
|
25
|
+
const INTERACTIVE_CLASS =
|
|
26
|
+
"rounded-sm outline-none transition-opacity focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card [@media(hover:hover)]:hover:text-foreground";
|
|
27
|
+
|
|
19
28
|
function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
|
|
20
29
|
return (
|
|
21
30
|
<span
|
|
@@ -26,22 +35,50 @@ function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
|
|
|
26
35
|
);
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
function ChartLegend({
|
|
38
|
+
function ChartLegend({
|
|
39
|
+
items,
|
|
40
|
+
align = "left",
|
|
41
|
+
insetLeft,
|
|
42
|
+
hiddenKeys,
|
|
43
|
+
onItemClick,
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
...props
|
|
47
|
+
}: ChartLegendProps) {
|
|
30
48
|
return (
|
|
31
49
|
<div
|
|
32
50
|
className={cn("flex flex-wrap gap-x-4 gap-y-1.5", ALIGN_CLASS[align], className)}
|
|
33
51
|
style={{ paddingLeft: align === "left" ? insetLeft : undefined, ...style }}
|
|
34
52
|
{...props}
|
|
35
53
|
>
|
|
36
|
-
{items.map((item) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
{items.map((item) => {
|
|
55
|
+
const swatchAndName = (
|
|
56
|
+
<>
|
|
57
|
+
<Swatch color={item.color} dashed={item.dashed} />
|
|
58
|
+
{item.name}
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
if (!onItemClick) {
|
|
62
|
+
return (
|
|
63
|
+
<span key={item.key} className={ITEM_CLASS}>
|
|
64
|
+
{swatchAndName}
|
|
65
|
+
</span>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const hidden = hiddenKeys?.has(item.key) ?? false;
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
key={item.key}
|
|
72
|
+
type="button"
|
|
73
|
+
aria-label={`${item.name} series`}
|
|
74
|
+
aria-pressed={!hidden}
|
|
75
|
+
onClick={() => onItemClick(item.key)}
|
|
76
|
+
className={cn(ITEM_CLASS, INTERACTIVE_CLASS, hidden && "opacity-40")}
|
|
77
|
+
>
|
|
78
|
+
{swatchAndName}
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
45
82
|
</div>
|
|
46
83
|
);
|
|
47
84
|
}
|
|
@@ -19,13 +19,10 @@ export function makeChartAxis(theme: ChartTheme) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function makeLegendItems(resolved: ResolvedSeries[]) {
|
|
22
|
-
return resolved.map((entry) => ({ name: entry.name, color: entry.color, dashed: entry.dashed }));
|
|
22
|
+
return resolved.map((entry) => ({ key: entry.key, name: entry.name, color: entry.color, dashed: entry.dashed }));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* Stacked series must reach the stack height (sum per x-point), not the tallest
|
|
27
|
-
* single series, or reference bands and explicit Y domains clip a tall stack short.
|
|
28
|
-
*/
|
|
25
|
+
/** Stacked series must reach the stack height (sum per x-point), or reference bands and explicit Y domains clip a tall stack short. */
|
|
29
26
|
export function computeNumericMax(data: ChartData, series: ChartSeries[], stacked: boolean) {
|
|
30
27
|
let max = 0;
|
|
31
28
|
for (const row of data) {
|
|
@@ -114,10 +111,7 @@ export function makeLastValueLabel(
|
|
|
114
111
|
};
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
/**
|
|
118
|
-
* Returns recharts elements (not a wrapper component) so recharts still detects
|
|
119
|
-
* them as direct children when spread into a chart via `{renderReferenceLine(...)}`.
|
|
120
|
-
*/
|
|
114
|
+
/** Returns recharts elements (not a wrapper component) so recharts still detects them as direct chart children. */
|
|
121
115
|
export function renderReferenceLine({
|
|
122
116
|
referenceLine,
|
|
123
117
|
numericMax,
|
|
@@ -68,12 +68,10 @@ function Combobox(props: ComboboxProps) {
|
|
|
68
68
|
getOptionLabel,
|
|
69
69
|
} = props;
|
|
70
70
|
|
|
71
|
-
// Single mode state
|
|
72
71
|
const [uncontrolledSingleValue, setUncontrolledSingleValue] = useState<string | null>(
|
|
73
72
|
!multiple ? ((props as ComboboxSingleProps).defaultValue ?? null) : null,
|
|
74
73
|
);
|
|
75
74
|
|
|
76
|
-
// Multi mode state
|
|
77
75
|
const [uncontrolledMultiValue, setUncontrolledMultiValue] = useState<string[]>(
|
|
78
76
|
multiple ? ((props as ComboboxMultipleProps).defaultValue ?? []) : [],
|
|
79
77
|
);
|
|
@@ -93,7 +91,6 @@ function Combobox(props: ComboboxProps) {
|
|
|
93
91
|
[isOpenControlled, controlledOnOpenChange],
|
|
94
92
|
);
|
|
95
93
|
|
|
96
|
-
// Resolve current values
|
|
97
94
|
const singleValue = multiple
|
|
98
95
|
? null
|
|
99
96
|
: (((props as ComboboxSingleProps).value !== undefined
|
|
@@ -132,7 +129,7 @@ function Combobox(props: ComboboxProps) {
|
|
|
132
129
|
setUncontrolledMultiValue(next);
|
|
133
130
|
}
|
|
134
131
|
onValueChangeMulti?.(next);
|
|
135
|
-
//
|
|
132
|
+
// Intentionally stays open in multi mode so several items can be picked.
|
|
136
133
|
} else {
|
|
137
134
|
if (!isControlledSingle) {
|
|
138
135
|
setUncontrolledSingleValue(itemValue);
|
|
@@ -332,7 +329,6 @@ function ComboboxSeparator({ className, ...props }: React.ComponentProps<typeof
|
|
|
332
329
|
}
|
|
333
330
|
|
|
334
331
|
interface ComboboxItemProps extends Omit<React.ComponentProps<typeof CommandPrimitive.Item>, "onSelect"> {
|
|
335
|
-
/** The value stored when this item is selected */
|
|
336
332
|
itemValue: string;
|
|
337
333
|
}
|
|
338
334
|
|
|
@@ -117,8 +117,6 @@ function DropdownMenuLabel({
|
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/* ── Header (avatar group) ────────────────────────────────────────────────── */
|
|
121
|
-
|
|
122
120
|
function DropdownMenuHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
123
121
|
return (
|
|
124
122
|
<div
|
|
@@ -129,16 +129,13 @@ function HeatmapChart({
|
|
|
129
129
|
const { columns, rows, cells, maxValue, peakKey } = layout;
|
|
130
130
|
const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
|
|
131
131
|
|
|
132
|
-
// "rank"
|
|
133
|
-
// by its position among the distinct non-zero values rather than its raw ratio
|
|
134
|
-
// to the max — the right read for skewed activity counts (à la GitHub).
|
|
132
|
+
// "rank" colours each cell by its position among distinct non-zero values, spreading skewed activity counts across the whole ramp (à la GitHub).
|
|
135
133
|
const colorFractionOf = React.useMemo(() => {
|
|
136
134
|
if (colorScale === "rank") {
|
|
137
135
|
const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort(
|
|
138
136
|
(lower, upper) => lower - upper,
|
|
139
137
|
);
|
|
140
|
-
// Floor non-zero ranks above the empty colour so the quietest active day
|
|
141
|
-
// still reads as coloured, never mistaken for no data.
|
|
138
|
+
// Floor non-zero ranks above the empty colour so the quietest active day is never mistaken for no data.
|
|
142
139
|
const RANK_FLOOR = 0.18;
|
|
143
140
|
const fractionByValue = new Map(
|
|
144
141
|
sorted.map((value, index) => [
|
|
@@ -189,9 +186,7 @@ function HeatmapChart({
|
|
|
189
186
|
const formatX = (label: string) => (xTickFormatter ? xTickFormatter(label) : label);
|
|
190
187
|
const formatY = (label: string) => (yTickFormatter ? yTickFormatter(label) : label);
|
|
191
188
|
|
|
192
|
-
// The tooltip
|
|
193
|
-
// clipped ancestor (e.g. the card's mount animation + overflow-hidden) can't
|
|
194
|
-
// shift or trim it. Flip near the viewport edges.
|
|
189
|
+
// The tooltip renders through a portal so a transformed/clipped ancestor (e.g. the card's mount animation + overflow-hidden) can't shift or trim it.
|
|
195
190
|
const tooltipLeft =
|
|
196
191
|
hovered && hovered.x + TOOLTIP_OFFSET + TOOLTIP_WIDTH > window.innerWidth
|
|
197
192
|
? hovered.x - TOOLTIP_OFFSET - TOOLTIP_WIDTH
|
|
@@ -268,8 +263,7 @@ function HeatmapChart({
|
|
|
268
263
|
/>
|
|
269
264
|
);
|
|
270
265
|
})}
|
|
271
|
-
{/* Transparent hit layer over the full pitch so
|
|
272
|
-
between dots — registers hover uniformly. */}
|
|
266
|
+
{/* Transparent hit layer over the full pitch so cells and the gaps between dots register hover uniformly. */}
|
|
273
267
|
{cells.map((cell) => (
|
|
274
268
|
// biome-ignore lint/a11y/noStaticElementInteractions: decorative hover tooltip; all values are visible on the cells
|
|
275
269
|
<rect
|
|
@@ -48,6 +48,8 @@ export interface LineChartProps {
|
|
|
48
48
|
markers?: ChartMarker[];
|
|
49
49
|
lastValueLabel?: boolean;
|
|
50
50
|
loading?: boolean;
|
|
51
|
+
hiddenSeries?: ReadonlySet<string>;
|
|
52
|
+
onLegendItemClick?: (key: string) => void;
|
|
51
53
|
valueFormatter?: (value: number) => string;
|
|
52
54
|
labelFormatter?: (label: string | number) => string;
|
|
53
55
|
className?: string;
|
|
@@ -69,6 +71,8 @@ function LineChart({
|
|
|
69
71
|
markers,
|
|
70
72
|
lastValueLabel = false,
|
|
71
73
|
loading = false,
|
|
74
|
+
hiddenSeries,
|
|
75
|
+
onLegendItemClick,
|
|
72
76
|
valueFormatter = (value) => value.toLocaleString("en-US"),
|
|
73
77
|
labelFormatter,
|
|
74
78
|
className,
|
|
@@ -76,8 +80,9 @@ function LineChart({
|
|
|
76
80
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
77
81
|
|
|
78
82
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
83
|
+
const visible = resolved.filter((entry) => !hiddenSeries?.has(entry.key));
|
|
79
84
|
|
|
80
|
-
const numericMax = React.useMemo(() => computeNumericMax(data,
|
|
85
|
+
const numericMax = React.useMemo(() => computeNumericMax(data, visible, false), [data, visible]);
|
|
81
86
|
|
|
82
87
|
const curveType = CURVE_TYPE[curve];
|
|
83
88
|
const margin = { top: markers?.length ? 18 : 8, right: lastValueLabel ? 56 : 8, bottom: 2, left: 0 };
|
|
@@ -129,7 +134,7 @@ function LineChart({
|
|
|
129
134
|
/>
|
|
130
135
|
{renderReferenceLine({ referenceLine, numericMax, theme })}
|
|
131
136
|
|
|
132
|
-
{
|
|
137
|
+
{visible.map((entry) => (
|
|
133
138
|
<Line
|
|
134
139
|
key={entry.key}
|
|
135
140
|
type={curveType}
|
|
@@ -159,7 +164,15 @@ function LineChart({
|
|
|
159
164
|
)}
|
|
160
165
|
</div>
|
|
161
166
|
|
|
162
|
-
{legend && !isEmpty &&
|
|
167
|
+
{legend && !isEmpty && (
|
|
168
|
+
<ChartLegend
|
|
169
|
+
items={legendItems}
|
|
170
|
+
align={legendAlign}
|
|
171
|
+
insetLeft={yAxisWidth}
|
|
172
|
+
hiddenKeys={hiddenSeries}
|
|
173
|
+
onItemClick={onLegendItemClick}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
163
176
|
</div>
|
|
164
177
|
);
|
|
165
178
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { cva } from "class-variance-authority";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Shared trigger styles for Select and Combobox.
|
|
5
|
-
* Both components use the same visual treatment for their trigger buttons.
|
|
6
|
-
* Edit here to keep them in sync.
|
|
7
|
-
*/
|
|
3
|
+
/** Shared by Select and Combobox so their trigger buttons stay visually in sync. */
|
|
8
4
|
export const selectTriggerVariants = cva(
|
|
9
5
|
[
|
|
10
6
|
"flex w-full items-center gap-2 rounded-md border bg-background text-foreground shadow-xs outline-hidden",
|
package/src/components/table.tsx
CHANGED
|
@@ -75,11 +75,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
interface TableCellProps extends React.ComponentProps<"td"> {
|
|
78
|
-
/**
|
|
79
|
-
* When true, the cell renders edge-to-edge so the child can act as the
|
|
80
|
-
* interactive surface (e.g. a button or popover trigger filling the cell).
|
|
81
|
-
* Defaults to false (standard padded cell).
|
|
82
|
-
*/
|
|
78
|
+
/** Renders the cell edge-to-edge so the child (e.g. a button or popover trigger) can fill it as the interactive surface. */
|
|
83
79
|
interactive?: boolean;
|
|
84
80
|
}
|
|
85
81
|
|
package/src/components/tabs.tsx
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
* Two tab families, one visual system:
|
|
5
|
-
*
|
|
6
|
-
* - Tabs / TabsList / TabsTrigger / TabsContent — state-managed.
|
|
7
|
-
* Use when tab content lives in the same page (e.g. "Code" vs "Preview").
|
|
8
|
-
*
|
|
9
|
-
* - TabsNav / TabsNavList / TabsNavTrigger — semantic <nav>/<ul>/<a>, URL-based.
|
|
10
|
-
* Use when each tab is a route (e.g. settings pages). Pair with <Link> via asChild.
|
|
11
|
-
*
|
|
12
|
-
* Both share `tabsTriggerVariants` for visual styles (colors, spacing, active state).
|
|
13
|
-
* Layout concerns (flex-1, orientation group selectors) stay component-specific.
|
|
14
|
-
*/
|
|
3
|
+
/* Two families sharing `tabsTriggerVariants`: Tabs for in-page state-switched content, TabsNav (semantic <nav>/<ul>/<a>) for URL-routed tabs. */
|
|
15
4
|
|
|
16
5
|
import { Slot } from "@radix-ui/react-slot";
|
|
17
6
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
* TaskProgress — multi-step async progress primitive.
|
|
5
|
-
*
|
|
6
|
-
* Dumb component: the consumer owns orchestration (timer, websocket, polling) and
|
|
7
|
-
* passes `steps` with each step's status. Renders a percentage bar at the top
|
|
8
|
-
* and one row per step with done/running/pending state badges.
|
|
9
|
-
*
|
|
10
|
-
* `trailingLabel` is for "still working on something not in the step list" —
|
|
11
|
-
* shows after the last step as a running row that isn't counted in the percent.
|
|
12
|
-
*/
|
|
3
|
+
/* Dumb component: the consumer owns orchestration (timer, websocket, polling) and passes each step's status. */
|
|
13
4
|
|
|
14
5
|
import { CheckIcon } from "lucide-react";
|
|
15
6
|
|
|
@@ -27,10 +18,7 @@ interface TaskProgressStep {
|
|
|
27
18
|
interface TaskProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
28
19
|
steps: readonly TaskProgressStep[];
|
|
29
20
|
trailingLabel?: string;
|
|
30
|
-
/**
|
|
31
|
-
* Optional mount-time stagger: each row fades + slides in with `animation-delay = index × stepRevealDelayMs`.
|
|
32
|
-
* Pure CSS, runs once on mount. Omit for no animation.
|
|
33
|
-
*/
|
|
21
|
+
/** Mount-time stagger: each row fades in after `index × stepRevealDelayMs`; omit for no animation. */
|
|
34
22
|
stepRevealDelayMs?: number;
|
|
35
23
|
}
|
|
36
24
|
|
package/src/lib/chart-palette.ts
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Chart palettes — the locked "paired system" from the analytics-v2 design.
|
|
3
|
-
*
|
|
4
|
-
* These are charting-API color values (consumed by Recharts at draw time, which
|
|
5
|
-
* cannot read CSS custom properties), so per the design-system rules they live
|
|
6
|
-
* as documented hex constants rather than `--color-*` tokens. They are
|
|
7
|
-
* theme-independent: the same series colors read on light and dark canvases.
|
|
8
|
-
* Only the chrome (grid, axis text, tooltip surface, semantic colors) is
|
|
9
|
-
* theme-aware — see `useChartTheme`.
|
|
10
|
-
*
|
|
11
|
-
* Rules baked in:
|
|
12
|
-
* - Pink leads (index 0) — never buried, fixing the V1 "pink last" bug.
|
|
13
|
-
* - Adjacent dashboard cards alternate lead palette (magenta / cyan) — the
|
|
14
|
-
* page decides which to pass; the component just receives one.
|
|
15
|
-
*/
|
|
1
|
+
// Theme-independent hex constants (Recharts can't read CSS custom properties); pink leads at index 0 and adjacent cards alternate magenta/cyan lead palettes.
|
|
16
2
|
|
|
17
3
|
export const MAGENTA_PALETTE = [
|
|
18
4
|
"#da1b6a",
|
|
@@ -46,8 +32,7 @@ export const CHART_PALETTES: Record<ChartPaletteName, readonly string[]> = {
|
|
|
46
32
|
export const heatRampMagenta = (empty: string) => [empty, "#5b0a31", "#a3034a", "#e90060", "#ff7eb6"];
|
|
47
33
|
export const heatRampMint = (empty: string) => [empty, "#0c4a52", "#17b8cf", "#41ddc9", "#6eece7"];
|
|
48
34
|
|
|
49
|
-
//
|
|
50
|
-
// light/dark pair rather than a `--color-*` token (heatmaps draw to SVG fills).
|
|
35
|
+
// The only theme-dependent charting value; a light/dark pair rather than a `--color-*` token because heatmaps draw to SVG fills.
|
|
51
36
|
export const HEAT_EMPTY = { light: "#eef3f3", dark: "#102222" } as const;
|
|
52
37
|
|
|
53
38
|
const HEAT_RAMPS: Record<ChartPaletteName, (empty: string) => string[]> = {
|
|
@@ -99,11 +84,7 @@ export const rampColor = (palette: ChartPaletteName, fraction: number) => {
|
|
|
99
84
|
return mixHex(from, to, fraction);
|
|
100
85
|
};
|
|
101
86
|
|
|
102
|
-
/**
|
|
103
|
-
* Interpolate continuously across a multi-stop heat ramp (e.g. `heatRampMagenta`),
|
|
104
|
-
* so a normalized 0..1 value reads as a smooth single-hue gradient rather than
|
|
105
|
-
* the five discrete bands.
|
|
106
|
-
*/
|
|
87
|
+
/** Interpolates continuously across the ramp so a 0..1 value reads as a smooth gradient rather than five discrete bands. */
|
|
107
88
|
export const heatColor = (stops: readonly string[], fraction: number) => {
|
|
108
89
|
const clamped = Math.min(1, Math.max(0, fraction));
|
|
109
90
|
const segment = (stops.length - 1) * clamped;
|
package/src/lib/chart.ts
CHANGED
|
@@ -2,10 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import type { ChartTheme } from "../hooks/use-chart-theme";
|
|
3
3
|
import { luminance, paletteColor } from "./chart-palette";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Declarative description of one plotted series. The same shape drives area,
|
|
7
|
-
* line and bar charts — what differs is how each chart renders it.
|
|
8
|
-
*/
|
|
5
|
+
/** One plotted series; the same shape drives area, line and bar charts. */
|
|
9
6
|
export interface ChartSeries {
|
|
10
7
|
key: string;
|
|
11
8
|
name?: string;
|
|
@@ -31,11 +28,7 @@ const semanticColor = (theme: ChartTheme, semantic: NonNullable<ChartSeries["sem
|
|
|
31
28
|
return theme.success;
|
|
32
29
|
};
|
|
33
30
|
|
|
34
|
-
/**
|
|
35
|
-
* Resolve each series to a concrete color and name. Color precedence:
|
|
36
|
-
* explicit `color` → `semantic` token → palette slot by declared index (so the
|
|
37
|
-
* lead series keeps the lead color even after the bands are reordered).
|
|
38
|
-
*/
|
|
31
|
+
/** Palette fallback uses the declared index so the lead series keeps the lead color even after reordering. */
|
|
39
32
|
export const resolveSeries = (
|
|
40
33
|
series: readonly ChartSeries[],
|
|
41
34
|
palette: readonly string[],
|
|
@@ -47,12 +40,7 @@ export const resolveSeries = (
|
|
|
47
40
|
color: entry.color ?? (entry.semantic ? semanticColor(theme, entry.semantic) : paletteColor(palette, index)),
|
|
48
41
|
}));
|
|
49
42
|
|
|
50
|
-
/**
|
|
51
|
-
* Stacked bands read cleanest as a vertical gradient: darkest at the baseline,
|
|
52
|
-
* lightest at the top edge. Sorting by luminance enforces that for any palette
|
|
53
|
-
* (this is what fixed the "muddy cyan" stack in the lab). Render order maps to
|
|
54
|
-
* stack order in Recharts — first entry sits at the bottom.
|
|
55
|
-
*/
|
|
43
|
+
/** Stacked bands read cleanest darkest-at-baseline to lightest-on-top; sorting by luminance enforces that for any palette. */
|
|
56
44
|
export const orderByLuminance = (series: ResolvedSeries[]) =>
|
|
57
45
|
[...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
|
|
58
46
|
|
|
@@ -2,8 +2,7 @@ import { ArrowRight, Plus, Sparkles } from "lucide-react";
|
|
|
2
2
|
|
|
3
3
|
import { Button } from "../components/button";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
// variants stacked top-to-bottom, states left-to-right.
|
|
5
|
+
// Hover states are simulated via className; layout matches the Figma page (variants top-to-bottom, states left-to-right).
|
|
7
6
|
|
|
8
7
|
const VARIANT_LABEL = "type-text-xs text-muted-foreground w-20 shrink-0";
|
|
9
8
|
const STATE_HEADER = "type-text-xs text-muted-foreground font-medium w-32 text-center";
|
|
@@ -18,7 +18,6 @@ function mulberry32(seed: number) {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// Long-format activity rows: one record per (day, hour) with a working-hours hump.
|
|
22
21
|
const ACTIVITY = (() => {
|
|
23
22
|
const random = mulberry32(99);
|
|
24
23
|
const rows: Array<{ day: string; hour: string; sessions: number }> = [];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Story } from "@ladle/react";
|
|
2
|
+
import { useState } from "react";
|
|
2
3
|
|
|
3
4
|
import { ChartCard } from "../components/chart-card";
|
|
4
5
|
import { LineChart } from "../components/line-chart";
|
|
@@ -37,7 +38,8 @@ const genLatency = (seed: number) => {
|
|
|
37
38
|
return Array.from({ length }, (_, index) => {
|
|
38
39
|
p50 = Math.max(120, p50 + (rnd() - 0.48) * 40);
|
|
39
40
|
p95 = Math.max(p50 + 200, p95 + (rnd() - 0.46) * 120);
|
|
40
|
-
|
|
41
|
+
const spiked = Math.round(p95 * (index === 19 ? 1.7 : 1));
|
|
42
|
+
return { t: hourLabel(index, length), p50: Math.round(p50), p95: spiked, p99: Math.round(spiked * 1.6) };
|
|
41
43
|
});
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -64,83 +66,121 @@ const ms = (value: number) => `${value}ms`;
|
|
|
64
66
|
const tokensSpark = tokens.map((row) => row.v);
|
|
65
67
|
const latencyPeak = latency.reduce((best, row) => (row.p95 > best.p95 ? row : best));
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
69
|
+
const percentileSeries = [
|
|
70
|
+
{ key: "p95", name: "p95" },
|
|
71
|
+
{ key: "p50", name: "p50" },
|
|
72
|
+
{ key: "p99", name: "p99", dashed: true },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const AllVariants: Story = () => {
|
|
76
|
+
const [isolated, setIsolated] = useState<string | null>(null);
|
|
77
|
+
const hiddenPercentiles = new Set(
|
|
78
|
+
isolated == null ? [] : percentileSeries.map((entry) => entry.key).filter((key) => key !== isolated),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
83
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
84
|
+
<ChartCard
|
|
85
|
+
palette="cyan"
|
|
86
|
+
accent="left"
|
|
87
|
+
kicker="Last 24h"
|
|
88
|
+
title="Sessions"
|
|
89
|
+
description="Full-width · left accent"
|
|
90
|
+
className="md:col-span-2 xl:col-span-3"
|
|
91
|
+
>
|
|
92
|
+
<LineChart
|
|
93
|
+
data={compare}
|
|
94
|
+
index="t"
|
|
95
|
+
series={[
|
|
96
|
+
{ key: "claude", name: "Claude" },
|
|
97
|
+
{ key: "chatgpt", name: "ChatGPT" },
|
|
98
|
+
]}
|
|
99
|
+
lastValueLabel
|
|
100
|
+
legend
|
|
101
|
+
valueFormatter={nf}
|
|
102
|
+
/>
|
|
103
|
+
</ChartCard>
|
|
104
|
+
|
|
105
|
+
<ChartCard palette="magenta" kicker="Last 24h" title="Output tokens" description="Single series · value flags">
|
|
106
|
+
<Stat value="1.4M" unit="tokens" delta={{ value: 8.1, direction: "up" }} sparkline={tokensSpark} />
|
|
107
|
+
<LineChart data={tokens} index="t" series={[{ key: "v", name: "tokens" }]} valueFlags valueFormatter={fmtK} />
|
|
108
|
+
</ChartCard>
|
|
109
|
+
|
|
110
|
+
<ChartCard
|
|
111
|
+
palette="magenta"
|
|
112
|
+
kicker="Last 24h"
|
|
113
|
+
title="Request latency"
|
|
114
|
+
description="p50 vs p95 · SLA band + peak"
|
|
115
|
+
>
|
|
116
|
+
<Stat value="240ms" unit="p50 · current" delta={{ value: 4.2, direction: "down", invert: true }} />
|
|
117
|
+
<LineChart
|
|
118
|
+
data={latency}
|
|
119
|
+
index="t"
|
|
120
|
+
series={[
|
|
121
|
+
{ key: "p50", name: "p50" },
|
|
122
|
+
{ key: "p95", name: "p95", dashed: true, color: "#9b5de5" },
|
|
123
|
+
]}
|
|
124
|
+
referenceLine={{ y: 1000, label: "SLA 1s", band: true }}
|
|
125
|
+
markers={[{ x: latencyPeak.t, y: latencyPeak.p95, label: "peak", color: "#9b5de5" }]}
|
|
126
|
+
legend
|
|
127
|
+
valueFormatter={ms}
|
|
128
|
+
/>
|
|
129
|
+
</ChartCard>
|
|
130
|
+
|
|
131
|
+
<ChartCard
|
|
132
|
+
palette="magenta"
|
|
133
|
+
kicker="Last 24h"
|
|
134
|
+
title="Percentiles"
|
|
135
|
+
description="Interactive legend · click to isolate"
|
|
136
|
+
>
|
|
137
|
+
<LineChart
|
|
138
|
+
data={latency}
|
|
139
|
+
index="t"
|
|
140
|
+
series={percentileSeries}
|
|
141
|
+
dots
|
|
142
|
+
legend
|
|
143
|
+
legendAlign="center"
|
|
144
|
+
lastValueLabel
|
|
145
|
+
hiddenSeries={hiddenPercentiles}
|
|
146
|
+
onLegendItemClick={(key) => setIsolated((current) => (current === key ? null : key))}
|
|
147
|
+
valueFormatter={ms}
|
|
148
|
+
/>
|
|
149
|
+
</ChartCard>
|
|
150
|
+
|
|
151
|
+
<ChartCard palette="cyan" kicker="Last 24h" title="Sessions" description="Claude vs ChatGPT · last values">
|
|
152
|
+
<LineChart
|
|
153
|
+
data={compare}
|
|
154
|
+
index="t"
|
|
155
|
+
series={[
|
|
156
|
+
{ key: "claude", name: "Claude" },
|
|
157
|
+
{ key: "chatgpt", name: "ChatGPT" },
|
|
158
|
+
]}
|
|
159
|
+
lastValueLabel
|
|
160
|
+
legend
|
|
161
|
+
valueFormatter={nf}
|
|
162
|
+
/>
|
|
163
|
+
</ChartCard>
|
|
164
|
+
|
|
165
|
+
<ChartCard palette="cyan" kicker="Last 24h" title="Throughput" description="Dots · stepped curve">
|
|
166
|
+
<LineChart
|
|
167
|
+
data={tokens}
|
|
168
|
+
index="t"
|
|
169
|
+
series={[{ key: "v", name: "tokens" }]}
|
|
170
|
+
curve="step"
|
|
171
|
+
dots
|
|
172
|
+
valueFormatter={fmtK}
|
|
173
|
+
/>
|
|
174
|
+
</ChartCard>
|
|
175
|
+
|
|
176
|
+
<ChartCard palette="magenta" kicker="State" title="Loading" description="Mono spinner placeholder">
|
|
177
|
+
<LineChart data={[]} index="t" series={[{ key: "v", name: "tokens" }]} loading />
|
|
178
|
+
</ChartCard>
|
|
179
|
+
|
|
180
|
+
<ChartCard palette="magenta" kicker="State" title="Empty" description="No data in range">
|
|
181
|
+
<LineChart data={[]} index="t" series={[{ key: "v", name: "tokens" }]} />
|
|
182
|
+
</ChartCard>
|
|
183
|
+
</div>
|
|
144
184
|
</div>
|
|
145
|
-
|
|
146
|
-
|
|
185
|
+
);
|
|
186
|
+
};
|
|
@@ -23,10 +23,7 @@ import {
|
|
|
23
23
|
SidebarTrigger,
|
|
24
24
|
} from "../components/sidebar";
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* Wraps stories so `fixed` inside SidebarProvider becomes `absolute`
|
|
28
|
-
* relative to this container (via `transform: scale(1)` trick).
|
|
29
|
-
*/
|
|
26
|
+
/** `transform: scale(1)` makes the SidebarProvider's `fixed` elements position relative to this frame. */
|
|
30
27
|
function StoryFrame({ children, height = 600 }: { children: ReactNode; height?: number }) {
|
|
31
28
|
return (
|
|
32
29
|
<div
|
|
@@ -14,7 +14,6 @@ const steps = [
|
|
|
14
14
|
{ id: "review", label: "Review & export" },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
/** The step rail is a vertical `TabsNav` composed with the consumer's selection state. */
|
|
18
17
|
function StepRail({ activeIdx, onSelect }: { activeIdx: number; onSelect: (idx: number) => void }) {
|
|
19
18
|
return (
|
|
20
19
|
<TabsNav orientation="vertical" aria-label="Submission steps">
|