@alpic-ai/ui 1.165.1 → 1.167.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.
@@ -5,6 +5,9 @@ interface BarListProps {
5
5
  data: ReadonlyArray<Record<string, string | number | null | undefined>>;
6
6
  index: string;
7
7
  dataKey?: string;
8
+ secondaryKey?: string;
9
+ secondaryVariantKey?: string;
10
+ titleKey?: string;
8
11
  maxItems?: number;
9
12
  palette?: ChartPaletteName;
10
13
  /** Renders bars in a single semantic hue (e.g. red for errors) rather than the palette ramp. */
@@ -14,6 +17,6 @@ interface BarListProps {
14
17
  labelFormatter?: (label: string | number) => string;
15
18
  className?: string;
16
19
  }
17
- declare function BarList({ data, index, dataKey, maxItems, palette, semantic, loading, valueFormatter, labelFormatter, className }: BarListProps): React$1.JSX.Element;
20
+ declare function BarList({ data, index, dataKey, secondaryKey, secondaryVariantKey, titleKey, maxItems, palette, semantic, loading, valueFormatter, labelFormatter, className }: BarListProps): React$1.JSX.Element;
18
21
  //#endregion
19
22
  export { BarList, BarListProps };
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.mjs";
3
3
  import { rampColor } from "../lib/chart-palette.mjs";
4
- import { formatShare } from "../lib/chart.mjs";
5
4
  import { useChartContext } from "./chart-container.mjs";
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.mjs";
6
+ import { Badge } from "./badge.mjs";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  import * as React$1 from "react";
8
9
  //#region src/components/bar-list.tsx
@@ -11,19 +12,28 @@ const SEMANTIC_KEY = {
11
12
  warning: "warning",
12
13
  success: "success"
13
14
  };
15
+ const SECONDARY_BADGE_VARIANT = {
16
+ error: "error",
17
+ warning: "warning",
18
+ success: "success"
19
+ };
14
20
  const PLACEHOLDER_HEIGHT = 168;
15
21
  const RAMP_CEILING = .8;
16
22
  const SEMANTIC_FLOOR = .62;
17
23
  const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
18
- function BarList({ data, index, dataKey = "value", maxItems, palette, semantic, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
24
+ const ROW_INTERACTIVE_CLASS = "rounded-md text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card";
25
+ function BarList({ data, index, dataKey = "value", secondaryKey, secondaryVariantKey, titleKey, maxItems, palette, semantic, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
19
26
  const { paletteName, theme } = useChartContext(palette);
20
27
  const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
21
28
  const [active, setActive] = React$1.useState(null);
22
- const total = React$1.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
23
29
  const rows = React$1.useMemo(() => {
30
+ const optionalText = (row, key) => key != null && row[key] != null ? String(row[key]) : void 0;
24
31
  const mapped = data.map((row) => ({
25
32
  name: String(row[index] ?? ""),
26
- value: Number(row[dataKey]) || 0
33
+ value: Number(row[dataKey]) || 0,
34
+ secondary: optionalText(row, secondaryKey),
35
+ secondaryVariant: SECONDARY_BADGE_VARIANT[optionalText(row, secondaryVariantKey) ?? ""],
36
+ title: optionalText(row, titleKey)
27
37
  }));
28
38
  mapped.sort((lower, upper) => upper.value - lower.value);
29
39
  const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
@@ -39,11 +49,15 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
39
49
  data,
40
50
  index,
41
51
  dataKey,
52
+ secondaryKey,
53
+ secondaryVariantKey,
54
+ titleKey,
42
55
  maxItems,
43
56
  paletteName,
44
57
  accent
45
58
  ]);
46
59
  const maxValue = rows.reduce((max, row) => row.value > max ? row.value : max, 0);
60
+ const hasSecondary = rows.some((row) => row.secondary != null);
47
61
  const isEmpty = rows.length === 0;
48
62
  const formatName = (name) => labelFormatter ? labelFormatter(name) : name;
49
63
  if (loading) return /* @__PURE__ */ jsxs("div", {
@@ -64,10 +78,12 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
64
78
  const fillWidth = `${fraction * 100}%`;
65
79
  const dimmed = active !== null && active !== slot;
66
80
  const isActive = active === slot;
67
- return /* @__PURE__ */ jsxs("div", {
81
+ const hasTooltip = row.title != null;
82
+ const bar = /* @__PURE__ */ jsxs(hasTooltip ? "button" : "div", {
83
+ type: hasTooltip ? "button" : void 0,
68
84
  onMouseEnter: () => setActive(slot),
69
85
  onMouseLeave: () => setActive(null),
70
- className: "flex items-center gap-3 py-1",
86
+ className: cn("flex items-center gap-3 py-1", hasTooltip && ROW_INTERACTIVE_CLASS),
71
87
  children: [/* @__PURE__ */ jsxs("div", {
72
88
  className: "relative h-[30px] flex-1 overflow-hidden rounded-md bg-muted",
73
89
  children: [/* @__PURE__ */ jsx("span", {
@@ -91,12 +107,30 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
91
107
  children: formatName(row.name)
92
108
  })
93
109
  })]
94
- }), /* @__PURE__ */ jsx("span", {
95
- className: "min-w-[4rem] shrink-0 whitespace-nowrap text-right font-mono text-text-xs tabular-nums motion-safe:transition-colors",
96
- style: { color: isActive ? row.color : void 0 },
97
- children: isActive ? formatShare(total > 0 ? row.value / total : 0) : valueFormatter(row.value)
110
+ }), /* @__PURE__ */ jsxs("div", {
111
+ className: "flex shrink-0 items-center justify-end gap-2.5 whitespace-nowrap font-mono text-text-xs tabular-nums",
112
+ children: [/* @__PURE__ */ jsx("span", {
113
+ className: "min-w-[4rem] text-right motion-safe:transition-colors",
114
+ style: { color: isActive ? row.color : void 0 },
115
+ children: valueFormatter(row.value)
116
+ }), hasSecondary && /* @__PURE__ */ jsx("span", {
117
+ className: "flex min-w-[3.5rem] justify-end",
118
+ children: row.secondary != null && /* @__PURE__ */ jsx(Badge, {
119
+ variant: row.secondaryVariant ?? "secondary",
120
+ size: "sm",
121
+ children: row.secondary
122
+ })
123
+ })]
98
124
  })]
99
125
  }, row.name);
126
+ if (row.title == null) return bar;
127
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
128
+ asChild: true,
129
+ children: bar
130
+ }), /* @__PURE__ */ jsx(TooltipContent, {
131
+ className: "max-w-md text-pretty text-left",
132
+ children: row.title
133
+ })] }, row.name);
100
134
  })
101
135
  });
102
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/ui",
3
- "version": "1.165.1",
3
+ "version": "1.167.0",
4
4
  "description": "Alpic design system — shared UI components",
5
5
  "type": "module",
6
6
  "exports": {
@@ -79,6 +79,6 @@
79
79
  "test:type": "tsgo --noEmit",
80
80
  "test:format": "biome check --error-on-warnings .",
81
81
  "dev:ui": "ladle serve",
82
- "publish:npm": "pnpm publish --tag \"${NPM_TAG}\" --access public --no-git-checks"
82
+ "publish:npm": "../../scripts/publish-if-new.sh"
83
83
  }
84
84
  }
@@ -2,16 +2,20 @@
2
2
 
3
3
  import * as React from "react";
4
4
 
5
- import { formatShare } from "../lib/chart";
6
5
  import type { ChartPaletteName } from "../lib/chart-palette";
7
6
  import { rampColor } from "../lib/chart-palette";
8
7
  import { cn } from "../lib/cn";
8
+ import { Badge, type BadgeVariant } from "./badge";
9
9
  import { useChartContext } from "./chart-container";
10
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
10
11
 
11
12
  export interface BarListProps {
12
13
  data: ReadonlyArray<Record<string, string | number | null | undefined>>;
13
14
  index: string;
14
15
  dataKey?: string;
16
+ secondaryKey?: string;
17
+ secondaryVariantKey?: string;
18
+ titleKey?: string;
15
19
  maxItems?: number;
16
20
  palette?: ChartPaletteName;
17
21
  /** Renders bars in a single semantic hue (e.g. red for errors) rather than the palette ramp. */
@@ -23,6 +27,11 @@ export interface BarListProps {
23
27
  }
24
28
 
25
29
  const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
30
+ const SECONDARY_BADGE_VARIANT: Record<string, BadgeVariant> = {
31
+ error: "error",
32
+ warning: "warning",
33
+ success: "success",
34
+ };
26
35
 
27
36
  const PLACEHOLDER_HEIGHT = 168;
28
37
  // Cap the ramp off its palest end so low-rank bars stay legible on a light card.
@@ -30,11 +39,16 @@ const RAMP_CEILING = 0.8;
30
39
  // The lightest semantic bar keeps this much hue (mixed toward white) so a long list never fades to near-invisible.
31
40
  const SEMANTIC_FLOOR = 0.62;
32
41
  const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
42
+ const ROW_INTERACTIVE_CLASS =
43
+ "rounded-md text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card";
33
44
 
34
45
  function BarList({
35
46
  data,
36
47
  index,
37
48
  dataKey = "value",
49
+ secondaryKey,
50
+ secondaryVariantKey,
51
+ titleKey,
38
52
  maxItems,
39
53
  palette,
40
54
  semantic,
@@ -47,12 +61,15 @@ function BarList({
47
61
  const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
48
62
  const [active, setActive] = React.useState<number | null>(null);
49
63
 
50
- const total = React.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
51
-
52
64
  const rows = React.useMemo(() => {
65
+ const optionalText = (row: (typeof data)[number], key: string | undefined) =>
66
+ key != null && row[key] != null ? String(row[key]) : undefined;
53
67
  const mapped = data.map((row) => ({
54
68
  name: String(row[index] ?? ""),
55
69
  value: Number(row[dataKey]) || 0,
70
+ secondary: optionalText(row, secondaryKey),
71
+ secondaryVariant: SECONDARY_BADGE_VARIANT[optionalText(row, secondaryVariantKey) ?? ""],
72
+ title: optionalText(row, titleKey),
56
73
  }));
57
74
  mapped.sort((lower, upper) => upper.value - lower.value);
58
75
  const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
@@ -66,9 +83,10 @@ function BarList({
66
83
  : rampColor(paletteName, rankFraction * RAMP_CEILING),
67
84
  };
68
85
  });
69
- }, [data, index, dataKey, maxItems, paletteName, accent]);
86
+ }, [data, index, dataKey, secondaryKey, secondaryVariantKey, titleKey, maxItems, paletteName, accent]);
70
87
 
71
88
  const maxValue = rows.reduce((max, row) => (row.value > max ? row.value : max), 0);
89
+ const hasSecondary = rows.some((row) => row.secondary != null);
72
90
  const isEmpty = rows.length === 0;
73
91
  const formatName = (name: string) => (labelFormatter ? labelFormatter(name) : name);
74
92
 
@@ -108,13 +126,16 @@ function BarList({
108
126
  const fillWidth = `${fraction * 100}%`;
109
127
  const dimmed = active !== null && active !== slot;
110
128
  const isActive = active === slot;
111
- return (
112
- // biome-ignore lint/a11y/noStaticElementInteractions: decorative hover-sync; all values are visible without it
113
- <div
129
+ const hasTooltip = row.title != null;
130
+ // Only tooltip rows take focus, so a list without one keeps the tab order it had.
131
+ const Row = hasTooltip ? "button" : "div";
132
+ const bar = (
133
+ <Row
114
134
  key={row.name}
135
+ type={hasTooltip ? "button" : undefined}
115
136
  onMouseEnter={() => setActive(slot)}
116
137
  onMouseLeave={() => setActive(null)}
117
- className="flex items-center gap-3 py-1"
138
+ className={cn("flex items-center gap-3 py-1", hasTooltip && ROW_INTERACTIVE_CLASS)}
118
139
  >
119
140
  <div className="relative h-[30px] flex-1 overflow-hidden rounded-md bg-muted">
120
141
  <span className="pointer-events-none absolute inset-x-3 top-1/2 z-0 -translate-y-1/2 truncate type-text-xs font-medium text-foreground">
@@ -141,13 +162,33 @@ function BarList({
141
162
  </span>
142
163
  </div>
143
164
  </div>
144
- <span
145
- className="min-w-[4rem] shrink-0 whitespace-nowrap text-right font-mono text-text-xs tabular-nums motion-safe:transition-colors"
146
- style={{ color: isActive ? row.color : undefined }}
147
- >
148
- {isActive ? formatShare(total > 0 ? row.value / total : 0) : valueFormatter(row.value)}
149
- </span>
150
- </div>
165
+ <div className="flex shrink-0 items-center justify-end gap-2.5 whitespace-nowrap font-mono text-text-xs tabular-nums">
166
+ <span
167
+ className="min-w-[4rem] text-right motion-safe:transition-colors"
168
+ style={{ color: isActive ? row.color : undefined }}
169
+ >
170
+ {valueFormatter(row.value)}
171
+ </span>
172
+ {hasSecondary && (
173
+ <span className="flex min-w-[3.5rem] justify-end">
174
+ {row.secondary != null && (
175
+ <Badge variant={row.secondaryVariant ?? "secondary"} size="sm">
176
+ {row.secondary}
177
+ </Badge>
178
+ )}
179
+ </span>
180
+ )}
181
+ </div>
182
+ </Row>
183
+ );
184
+ if (row.title == null) {
185
+ return bar;
186
+ }
187
+ return (
188
+ <Tooltip key={row.name}>
189
+ <TooltipTrigger asChild>{bar}</TooltipTrigger>
190
+ <TooltipContent className="max-w-md text-pretty text-left">{row.title}</TooltipContent>
191
+ </Tooltip>
151
192
  );
152
193
  })}
153
194
  </div>