@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.
Files changed (44) hide show
  1. package/dist/components/accordion-card.d.mts +1 -5
  2. package/dist/components/accordion-card.mjs +1 -5
  3. package/dist/components/accordion.d.mts +1 -5
  4. package/dist/components/accordion.mjs +1 -5
  5. package/dist/components/attachment-tile.d.mts +0 -1
  6. package/dist/components/chart-legend.d.mts +4 -1
  7. package/dist/components/chart-legend.mjs +21 -7
  8. package/dist/components/chart-primitives.d.mts +3 -8
  9. package/dist/components/chart-primitives.mjs +3 -8
  10. package/dist/components/combobox.d.mts +0 -1
  11. package/dist/components/line-chart.d.mts +3 -1
  12. package/dist/components/line-chart.mjs +7 -4
  13. package/dist/components/select-trigger-variants.d.mts +1 -5
  14. package/dist/components/select-trigger-variants.mjs +1 -5
  15. package/dist/components/table.d.mts +1 -5
  16. package/dist/components/task-progress.d.mts +1 -4
  17. package/dist/lib/chart-palette.d.mts +1 -20
  18. package/dist/lib/chart-palette.mjs +1 -20
  19. package/dist/lib/chart.d.mts +1 -4
  20. package/dist/lib/chart.mjs +2 -11
  21. package/package.json +5 -5
  22. package/src/components/accordion-card.tsx +1 -5
  23. package/src/components/accordion.tsx +1 -5
  24. package/src/components/attachment-tile.tsx +0 -1
  25. package/src/components/bar-chart.tsx +1 -2
  26. package/src/components/bar-list.tsx +1 -2
  27. package/src/components/chart-legend.tsx +47 -10
  28. package/src/components/chart-primitives.tsx +3 -9
  29. package/src/components/combobox.tsx +1 -5
  30. package/src/components/dropdown-menu.tsx +0 -2
  31. package/src/components/heatmap-chart.tsx +4 -10
  32. package/src/components/line-chart.tsx +16 -3
  33. package/src/components/select-trigger-variants.ts +1 -5
  34. package/src/components/table.tsx +1 -5
  35. package/src/components/tabs.tsx +1 -12
  36. package/src/components/task-progress.tsx +2 -14
  37. package/src/components/wizard.tsx +0 -6
  38. package/src/lib/chart-palette.ts +3 -22
  39. package/src/lib/chart.ts +3 -15
  40. package/src/stories/button.stories.tsx +1 -2
  41. package/src/stories/heatmap-chart.stories.tsx +0 -1
  42. package/src/stories/line-chart.stories.tsx +120 -80
  43. package/src/stories/sidebar.stories.tsx +1 -4
  44. 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",
@@ -2,7 +2,6 @@
2
2
  interface AttachmentTileProps {
3
3
  src?: string;
4
4
  isImage?: boolean;
5
- /** Accessible label describing the attachment */
6
5
  label?: string;
7
6
  onRemove?: () => void;
8
7
  onClick?: () => void;
@@ -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) => /* @__PURE__ */ jsxs("span", {
26
- className: "inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]",
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
- }, item.name))
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 numericMax = React$1.useMemo(() => computeNumericMax(data, series, false), [data, series]);
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
- resolved.map((entry) => /* @__PURE__ */ jsx(Line, {
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;
@@ -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;
@@ -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.161.0",
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.17",
66
- "@types/react-dom": "19.2.3",
67
- "lucide-react": "^1.27.0",
68
- "react-hook-form": "^7.83.0",
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
@@ -9,7 +9,6 @@ import { TooltipIconButton } from "./tooltip-icon-button";
9
9
  interface AttachmentTileProps {
10
10
  src?: string;
11
11
  isImage?: boolean;
12
- /** Accessible label describing the attachment */
13
12
  label?: string;
14
13
  onRemove?: () => void;
15
14
  onClick?: () => void;
@@ -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 read cleanest as flat solids — a per-segment gradient
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
- // Semantic bars keep a single hue: the lightest bar still holds this much colour
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({ items, align = "left", insetLeft, className, style, ...props }: ChartLegendProps) {
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
- <span
38
- key={item.name}
39
- className="inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]"
40
- >
41
- <Swatch color={item.color} dashed={item.dashed} />
42
- {item.name}
43
- </span>
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
- // Stay open in multi mode
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" spreads clustered values across the whole ramp by colouring each cell
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 follows the cursor; rendered through a portal so a transformed/
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 every cell — and the gaps
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, series, false), [data, series]);
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
- {resolved.map((entry) => (
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 && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
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",
@@ -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
 
@@ -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
 
@@ -1,11 +1,5 @@
1
1
  "use client";
2
2
 
3
- /*
4
- * Wizard family — primitives for multi-step flows.
5
- *
6
- * - WizardProgress — step counter + progress bar
7
- */
8
-
9
3
  import type * as React from "react";
10
4
 
11
5
  import { cn } from "../lib/cn";
@@ -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
- // Empty heat cell — the only theme-dependent charting value, so it travels as a
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
- // Simulated states via className matches Figma page layout:
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
- return { t: hourLabel(index, length), p50: Math.round(p50), p95: Math.round(p95 * (index === 19 ? 1.7 : 1)) };
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
- export const AllVariants: Story = () => (
68
- <div className="mx-auto max-w-[1600px] p-8">
69
- <div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
70
- <ChartCard
71
- palette="cyan"
72
- accent="left"
73
- kicker="Last 24h"
74
- title="Sessions"
75
- description="Full-width · left accent"
76
- className="md:col-span-2 xl:col-span-3"
77
- >
78
- <LineChart
79
- data={compare}
80
- index="t"
81
- series={[
82
- { key: "claude", name: "Claude" },
83
- { key: "chatgpt", name: "ChatGPT" },
84
- ]}
85
- lastValueLabel
86
- legend
87
- valueFormatter={nf}
88
- />
89
- </ChartCard>
90
-
91
- <ChartCard palette="magenta" kicker="Last 24h" title="Output tokens" description="Single series · value flags">
92
- <Stat value="1.4M" unit="tokens" delta={{ value: 8.1, direction: "up" }} sparkline={tokensSpark} />
93
- <LineChart data={tokens} index="t" series={[{ key: "v", name: "tokens" }]} valueFlags valueFormatter={fmtK} />
94
- </ChartCard>
95
-
96
- <ChartCard palette="magenta" kicker="Last 24h" title="Request latency" description="p50 vs p95 · SLA band + peak">
97
- <Stat value="240ms" unit="p50 · current" delta={{ value: 4.2, direction: "down", invert: true }} />
98
- <LineChart
99
- data={latency}
100
- index="t"
101
- series={[
102
- { key: "p50", name: "p50" },
103
- { key: "p95", name: "p95", dashed: true, color: "#9b5de5" },
104
- ]}
105
- referenceLine={{ y: 1000, label: "SLA 1s", band: true }}
106
- markers={[{ x: latencyPeak.t, y: latencyPeak.p95, label: "peak", color: "#9b5de5" }]}
107
- legend
108
- valueFormatter={ms}
109
- />
110
- </ChartCard>
111
-
112
- <ChartCard palette="cyan" kicker="Last 24h" title="Sessions" description="Claude vs ChatGPT · last values">
113
- <LineChart
114
- data={compare}
115
- index="t"
116
- series={[
117
- { key: "claude", name: "Claude" },
118
- { key: "chatgpt", name: "ChatGPT" },
119
- ]}
120
- lastValueLabel
121
- legend
122
- valueFormatter={nf}
123
- />
124
- </ChartCard>
125
-
126
- <ChartCard palette="cyan" kicker="Last 24h" title="Throughput" description="Dots · stepped curve">
127
- <LineChart
128
- data={tokens}
129
- index="t"
130
- series={[{ key: "v", name: "tokens" }]}
131
- curve="step"
132
- dots
133
- valueFormatter={fmtK}
134
- />
135
- </ChartCard>
136
-
137
- <ChartCard palette="magenta" kicker="State" title="Loading" description="Mono spinner placeholder">
138
- <LineChart data={[]} index="t" series={[{ key: "v", name: "tokens" }]} loading />
139
- </ChartCard>
140
-
141
- <ChartCard palette="magenta" kicker="State" title="Empty" description="No data in range">
142
- <LineChart data={[]} index="t" series={[{ key: "v", name: "tokens" }]} />
143
- </ChartCard>
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
- </div>
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">