@alpic-ai/ui 1.155.0 → 1.156.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.
@@ -13,6 +13,8 @@ interface BarChartProps {
13
13
  valueLabels?: boolean;
14
14
  height?: number;
15
15
  yAxisWidth?: number;
16
+ bare?: boolean;
17
+ barCategoryGap?: number | string;
16
18
  palette?: ChartPaletteName;
17
19
  referenceLine?: {
18
20
  y: number;
@@ -27,6 +29,6 @@ interface BarChartProps {
27
29
  labelFormatter?: (label: string | number) => string;
28
30
  className?: string;
29
31
  }
30
- declare function BarChart({ data, index, series, variant, legend, legendAlign, valueLabels, height, yAxisWidth, palette, referenceLine, markers, texture, partialLastBar, loading, valueFormatter, labelFormatter, className }: BarChartProps): React$1.JSX.Element;
32
+ declare function BarChart({ data, index, series, variant, legend, legendAlign, valueLabels, height, yAxisWidth, bare, barCategoryGap, palette, referenceLine, markers, texture, partialLastBar, loading, valueFormatter, labelFormatter, className }: BarChartProps): React$1.JSX.Element;
31
33
  //#endregion
32
34
  export { BarChart, BarChartProps };
@@ -12,7 +12,7 @@ import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, LabelList, Responsive
12
12
  //#region src/components/bar-chart.tsx
13
13
  const BAR_RADIUS = 4;
14
14
  const MAX_BAR_SIZE = 48;
15
- function BarChart({ data, index, series, variant = "stacked", legend = false, legendAlign = "left", valueLabels = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, texture = false, partialLastBar = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
15
+ function BarChart({ data, index, series, variant = "stacked", legend = false, legendAlign = "left", valueLabels = false, height = 200, yAxisWidth = 48, bare = false, barCategoryGap, palette, referenceLine, markers, texture = false, partialLastBar = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
16
16
  const { palette: paletteColors, theme } = useChartContext(palette);
17
17
  const reactId = React$1.useId().replace(/:/g, "");
18
18
  const resolved = resolveSeries(series, paletteColors, theme);
@@ -39,7 +39,12 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
39
39
  rendered,
40
40
  stacked
41
41
  ]);
42
- const margin = {
42
+ const margin = bare ? {
43
+ top: 0,
44
+ right: 0,
45
+ bottom: 0,
46
+ left: 0
47
+ } : {
43
48
  top: markers?.length || valueLabels ? 18 : 8,
44
49
  right: 8,
45
50
  bottom: 2,
@@ -100,7 +105,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
100
105
  data,
101
106
  stackOffset: variant === "expand" ? "expand" : "none",
102
107
  margin,
103
- barCategoryGap: stacked ? "20%" : "16%",
108
+ barCategoryGap: barCategoryGap ?? (stacked ? "20%" : "16%"),
104
109
  children: [
105
110
  /* @__PURE__ */ jsxs("defs", { children: [!stacked && rendered.map((entry, slot) => /* @__PURE__ */ jsxs("linearGradient", {
106
111
  id: `bar-${reactId}-${slot}`,
@@ -138,7 +143,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
138
143
  strokeOpacity: .65
139
144
  })]
140
145
  })] }),
141
- /* @__PURE__ */ jsx(CartesianGrid, {
146
+ !bare && /* @__PURE__ */ jsx(CartesianGrid, {
142
147
  vertical: false,
143
148
  stroke: theme.grid,
144
149
  strokeDasharray: "2 4"
@@ -146,17 +151,20 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
146
151
  /* @__PURE__ */ jsx(XAxis, {
147
152
  dataKey: index,
148
153
  ...axis,
154
+ hide: bare,
155
+ height: bare ? 0 : void 0,
149
156
  tick: makeXAxisTick(theme),
150
157
  interval: "preserveStartEnd",
151
158
  minTickGap: 56
152
159
  }),
153
160
  /* @__PURE__ */ jsx(YAxis, {
154
161
  ...axis,
155
- width: yAxisWidth,
162
+ hide: bare,
163
+ width: bare ? 0 : yAxisWidth,
156
164
  domain: referenceLine && variant !== "expand" ? [0, Math.ceil(Math.max(numericMax, referenceLine.y) * 1.15)] : void 0,
157
165
  tickFormatter: (value) => variant === "expand" ? `${Math.round(value * 100)}%` : valueFormatter(value)
158
166
  }),
159
- /* @__PURE__ */ jsx(Tooltip, {
167
+ !bare && /* @__PURE__ */ jsx(Tooltip, {
160
168
  offset: 12,
161
169
  allowEscapeViewBox: {
162
170
  x: false,
@@ -172,7 +180,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
172
180
  showTotal: withTotal
173
181
  })
174
182
  }),
175
- renderReferenceLine({
183
+ !bare && renderReferenceLine({
176
184
  referenceLine,
177
185
  numericMax,
178
186
  theme
@@ -215,11 +223,11 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
215
223
  content: renderValueLabel(entry.color)
216
224
  })]
217
225
  }, entry.key)),
218
- renderMarkers(markers, theme)
226
+ !bare && renderMarkers(markers, theme)
219
227
  ]
220
228
  })
221
229
  })
222
- }), legend && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
230
+ }), legend && !bare && !isEmpty && /* @__PURE__ */ jsx(ChartLegend, {
223
231
  items: legendItems,
224
232
  align: legendAlign,
225
233
  insetLeft: yAxisWidth
@@ -5,7 +5,7 @@ import * as React$1 from "react";
5
5
  import { Slot } from "@radix-ui/react-slot";
6
6
  import { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
7
7
  //#region src/components/form.d.ts
8
- declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>({ children, watch, getValues, getFieldState, setError, clearErrors, setValue, setValues, trigger, formState, resetField, reset, handleSubmit, unregister, control, register, setFocus, subscribe }: import("react-hook-form").FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React$1.JSX.Element;
8
+ declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>({ children, watch, getValues, getFieldState, setError, clearErrors, setValue, setValues, trigger, formState, resetField, reset, resetDefaultValues, handleSubmit, unregister, control, register, setFocus, subscribe }: import("react-hook-form").FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React$1.JSX.Element;
9
9
  declare const FormField: <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>({ ...props }: ControllerProps<TFieldValues, TName>) => React$1.JSX.Element;
10
10
  declare const useFormField: () => {
11
11
  invalid: boolean;
@@ -8,6 +8,7 @@ interface StatDelta {
8
8
  direction: "up" | "down";
9
9
  invert?: boolean;
10
10
  label?: React$1.ReactNode;
11
+ indicator?: "arrow" | "sign";
11
12
  }
12
13
  interface StatProps extends React$1.ComponentProps<"div"> {
13
14
  value: React$1.ReactNode;
@@ -17,7 +18,11 @@ interface StatProps extends React$1.ComponentProps<"div"> {
17
18
  value: number;
18
19
  }>;
19
20
  semantic?: "error" | "warning" | "success";
21
+ compact?: boolean;
20
22
  }
21
- declare function Stat({ value, unit, delta, sparkline, semantic, className, ...props }: StatProps): React$1.JSX.Element;
23
+ declare function Stat({ value, unit, delta, sparkline, semantic, compact, className, ...props }: StatProps): React$1.JSX.Element;
24
+ declare function StatSparkline({ sparkline, semantic, className }: Pick<StatProps, "sparkline" | "semantic"> & {
25
+ className?: string;
26
+ }): React$1.JSX.Element | null;
22
27
  //#endregion
23
- export { Stat, StatDelta, StatProps, statDeltaVariants };
28
+ export { Stat, StatDelta, StatProps, StatSparkline, statDeltaVariants };
@@ -2,7 +2,7 @@
2
2
  import { cn } from "../lib/cn.mjs";
3
3
  import { useChartContext } from "./chart-container.mjs";
4
4
  import { ArrowDown, ArrowUp } from "lucide-react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { cva } from "class-variance-authority";
7
7
  import * as React$1 from "react";
8
8
  import { Area, AreaChart, ResponsiveContainer } from "recharts";
@@ -26,85 +26,92 @@ const SEMANTIC_KEY = {
26
26
  warning: "warning",
27
27
  success: "success"
28
28
  };
29
- function Stat({ value, unit, delta, sparkline, semantic, className, ...props }) {
30
- const { palette, theme } = useChartContext();
31
- const gradientId = React$1.useId().replace(/:/g, "");
32
- const sparkData = React$1.useMemo(() => toSparkData(sparkline), [sparkline]);
33
- const hasSpark = sparkData.some((point) => point.value > 0);
34
- const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0];
29
+ function Stat({ value, unit, delta, sparkline, semantic, compact = false, className, ...props }) {
35
30
  const sentiment = delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
36
31
  return /* @__PURE__ */ jsxs("div", {
37
32
  "data-slot": "stat",
38
33
  className: cn("flex min-w-0 flex-col gap-2.5", className),
39
34
  ...props,
40
35
  children: [/* @__PURE__ */ jsxs("div", {
41
- className: "flex min-w-0 items-baseline gap-x-2.5",
36
+ className: cn("flex min-w-0 items-baseline gap-x-2.5", compact && "gap-x-1.5"),
42
37
  children: [
43
38
  /* @__PURE__ */ jsx("span", {
44
- className: "type-display-sm min-w-0 truncate font-bold leading-none tracking-tight tabular-nums text-foreground",
39
+ className: cn("min-w-0 font-bold leading-none tracking-tight tabular-nums text-foreground", compact ? "type-display-xs whitespace-nowrap" : "type-display-sm truncate"),
45
40
  children: value
46
41
  }),
47
42
  unit && /* @__PURE__ */ jsx("span", {
48
43
  className: "font-mono text-[11px] leading-none text-quaternary-foreground mb-0.5",
49
44
  children: unit
50
45
  }),
51
- delta && /* @__PURE__ */ jsxs(DeltaPill, {
46
+ delta && /* @__PURE__ */ jsx(DeltaPill, {
52
47
  sentiment,
53
- className: "mb-0.5",
54
- children: [delta.direction === "up" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3.5" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3.5" }), delta.label ?? `${delta.value}%`]
48
+ className: cn("mb-0.5 shrink-0", compact && "px-1.5"),
49
+ children: delta.indicator === "sign" ? /* @__PURE__ */ jsxs(Fragment, { children: [delta.direction === "up" ? "+" : "-", delta.label ?? `${delta.value}%`] }) : /* @__PURE__ */ jsxs(Fragment, { children: [delta.direction === "up" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3.5" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3.5" }), delta.label ?? `${delta.value}%`] })
55
50
  })
56
51
  ]
57
- }), hasSpark && /* @__PURE__ */ jsx("div", {
58
- className: "pointer-events-none h-9 w-full",
59
- children: /* @__PURE__ */ jsx(ResponsiveContainer, {
60
- width: "100%",
61
- height: "100%",
62
- initialDimension: {
63
- width: 0,
64
- height: 36
52
+ }), /* @__PURE__ */ jsx(StatSparkline, {
53
+ sparkline,
54
+ semantic
55
+ })]
56
+ });
57
+ }
58
+ function StatSparkline({ sparkline, semantic, className }) {
59
+ const { palette, theme } = useChartContext();
60
+ const gradientId = React$1.useId().replace(/:/g, "");
61
+ const sparkData = React$1.useMemo(() => toSparkData(sparkline), [sparkline]);
62
+ const hasSpark = sparkData.some((point) => point.value > 0);
63
+ const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0];
64
+ if (!hasSpark) return null;
65
+ return /* @__PURE__ */ jsx("div", {
66
+ className: cn("pointer-events-none h-9 w-full", className),
67
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
68
+ width: "100%",
69
+ height: "100%",
70
+ initialDimension: {
71
+ width: 0,
72
+ height: 36
73
+ },
74
+ children: /* @__PURE__ */ jsxs(AreaChart, {
75
+ data: sparkData,
76
+ margin: {
77
+ top: 4,
78
+ right: 2,
79
+ bottom: 0,
80
+ left: 2
65
81
  },
66
- children: /* @__PURE__ */ jsxs(AreaChart, {
67
- data: sparkData,
68
- margin: {
69
- top: 4,
70
- right: 2,
71
- bottom: 0,
72
- left: 2
73
- },
74
- children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", {
75
- id: gradientId,
76
- x1: "0",
77
- y1: "0",
78
- x2: "0",
79
- y2: "1",
80
- children: [/* @__PURE__ */ jsx("stop", {
81
- offset: "0%",
82
- stopColor: sparkColor,
83
- stopOpacity: .4
84
- }), /* @__PURE__ */ jsx("stop", {
85
- offset: "100%",
86
- stopColor: sparkColor,
87
- stopOpacity: 0
88
- })]
89
- }) }), /* @__PURE__ */ jsx(Area, {
90
- type: "monotone",
91
- dataKey: "value",
92
- stroke: sparkColor,
93
- strokeWidth: 1.6,
94
- fill: `url(#${gradientId})`,
95
- isAnimationActive: false,
96
- dot: ({ cx, cy, index }) => {
97
- return index === sparkData.length - 1 && cx != null && cy != null ? /* @__PURE__ */ jsx("circle", {
98
- cx,
99
- cy,
100
- r: 2.5,
101
- fill: sparkColor
102
- }, index) : /* @__PURE__ */ jsx("g", {}, index);
103
- }
82
+ children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", {
83
+ id: gradientId,
84
+ x1: "0",
85
+ y1: "0",
86
+ x2: "0",
87
+ y2: "1",
88
+ children: [/* @__PURE__ */ jsx("stop", {
89
+ offset: "0%",
90
+ stopColor: sparkColor,
91
+ stopOpacity: .4
92
+ }), /* @__PURE__ */ jsx("stop", {
93
+ offset: "100%",
94
+ stopColor: sparkColor,
95
+ stopOpacity: 0
104
96
  })]
105
- })
97
+ }) }), /* @__PURE__ */ jsx(Area, {
98
+ type: "monotone",
99
+ dataKey: "value",
100
+ stroke: sparkColor,
101
+ strokeWidth: 1.6,
102
+ fill: `url(#${gradientId})`,
103
+ isAnimationActive: false,
104
+ dot: ({ cx, cy, index }) => {
105
+ return index === sparkData.length - 1 && cx != null && cy != null ? /* @__PURE__ */ jsx("circle", {
106
+ cx,
107
+ cy,
108
+ r: 2.5,
109
+ fill: sparkColor
110
+ }, index) : /* @__PURE__ */ jsx("g", {}, index);
111
+ }
112
+ })]
106
113
  })
107
- })]
114
+ })
108
115
  });
109
116
  }
110
117
  function DeltaPill({ sentiment, className, ...props }) {
@@ -114,4 +121,4 @@ function DeltaPill({ sentiment, className, ...props }) {
114
121
  });
115
122
  }
116
123
  //#endregion
117
- export { Stat, statDeltaVariants };
124
+ export { Stat, StatSparkline, statDeltaVariants };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/ui",
3
- "version": "1.155.0",
3
+ "version": "1.156.0",
4
4
  "description": "Alpic design system — shared UI components",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,12 +23,12 @@
23
23
  "src"
24
24
  ],
25
25
  "peerDependencies": {
26
- "lucide-react": "^1.24.0",
26
+ "lucide-react": "^1.25.0",
27
27
  "react": "^19.2.7",
28
28
  "react-dom": "^19.2.7",
29
- "react-hook-form": "^7.81.0",
29
+ "react-hook-form": "^7.82.0",
30
30
  "sonner": "^2.0.7",
31
- "tailwindcss": "^4.3.2",
31
+ "tailwindcss": "^4.3.3",
32
32
  "tw-animate-css": "^1.4.0"
33
33
  },
34
34
  "dependencies": {
@@ -57,15 +57,15 @@
57
57
  },
58
58
  "devDependencies": {
59
59
  "@ladle/react": "^5.1.1",
60
- "@tailwindcss/postcss": "^4.3.2",
60
+ "@tailwindcss/postcss": "^4.3.3",
61
61
  "@types/react": "19.2.17",
62
62
  "@types/react-dom": "19.2.3",
63
- "lucide-react": "^1.24.0",
64
- "react-hook-form": "^7.81.0",
63
+ "lucide-react": "^1.25.0",
64
+ "react-hook-form": "^7.82.0",
65
65
  "shx": "^0.4.0",
66
66
  "sonner": "^2.0.7",
67
- "tailwindcss": "^4.3.2",
68
- "tsdown": "^0.22.5",
67
+ "tailwindcss": "^4.3.3",
68
+ "tsdown": "^0.22.9",
69
69
  "tw-animate-css": "^1.4.0",
70
70
  "typescript": "^6.0.3"
71
71
  },
@@ -42,6 +42,8 @@ export interface BarChartProps {
42
42
  valueLabels?: boolean;
43
43
  height?: number;
44
44
  yAxisWidth?: number;
45
+ bare?: boolean;
46
+ barCategoryGap?: number | string;
45
47
  palette?: ChartPaletteName;
46
48
  referenceLine?: { y: number; label?: string; band?: boolean };
47
49
  markers?: ChartMarker[];
@@ -63,6 +65,8 @@ function BarChart({
63
65
  valueLabels = false,
64
66
  height = 200,
65
67
  yAxisWidth = 48,
68
+ bare = false,
69
+ barCategoryGap,
66
70
  palette,
67
71
  referenceLine,
68
72
  markers,
@@ -99,7 +103,9 @@ function BarChart({
99
103
  });
100
104
  }, [data, rendered, stacked]);
101
105
 
102
- const margin = { top: markers?.length || valueLabels ? 18 : 8, right: 8, bottom: 2, left: 0 };
106
+ const margin = bare
107
+ ? { top: 0, right: 0, bottom: 0, left: 0 }
108
+ : { top: markers?.length || valueLabels ? 18 : 8, right: 8, bottom: 2, left: 0 };
103
109
 
104
110
  const axis = makeChartAxis(theme);
105
111
 
@@ -174,7 +180,7 @@ function BarChart({
174
180
  data={data as Record<string, string | number>[]}
175
181
  stackOffset={variant === "expand" ? "expand" : "none"}
176
182
  margin={margin}
177
- barCategoryGap={stacked ? "20%" : "16%"}
183
+ barCategoryGap={barCategoryGap ?? (stacked ? "20%" : "16%")}
178
184
  >
179
185
  <defs>
180
186
  {!stacked &&
@@ -198,17 +204,20 @@ function BarChart({
198
204
  )}
199
205
  </defs>
200
206
 
201
- <CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
207
+ {!bare && <CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />}
202
208
  <XAxis
203
209
  dataKey={index}
204
210
  {...axis}
211
+ hide={bare}
212
+ height={bare ? 0 : undefined}
205
213
  tick={makeXAxisTick(theme)}
206
214
  interval="preserveStartEnd"
207
215
  minTickGap={56}
208
216
  />
209
217
  <YAxis
210
218
  {...axis}
211
- width={yAxisWidth}
219
+ hide={bare}
220
+ width={bare ? 0 : yAxisWidth}
212
221
  domain={
213
222
  referenceLine && variant !== "expand"
214
223
  ? [0, Math.ceil(Math.max(numericMax, referenceLine.y) * 1.15)]
@@ -218,19 +227,21 @@ function BarChart({
218
227
  variant === "expand" ? `${Math.round(value * 100)}%` : valueFormatter(value)
219
228
  }
220
229
  />
221
- <Tooltip
222
- offset={12}
223
- allowEscapeViewBox={{ x: false, y: false }}
224
- cursor={{ fill: lead?.color ?? theme.mutedForeground, fillOpacity: theme.isDark ? 0.1 : 0.06 }}
225
- content={
226
- <ChartTooltipContent
227
- valueFormatter={valueFormatter}
228
- labelFormatter={labelFormatter}
229
- showTotal={withTotal}
230
- />
231
- }
232
- />
233
- {renderReferenceLine({ referenceLine, numericMax, theme })}
230
+ {!bare && (
231
+ <Tooltip
232
+ offset={12}
233
+ allowEscapeViewBox={{ x: false, y: false }}
234
+ cursor={{ fill: lead?.color ?? theme.mutedForeground, fillOpacity: theme.isDark ? 0.1 : 0.06 }}
235
+ content={
236
+ <ChartTooltipContent
237
+ valueFormatter={valueFormatter}
238
+ labelFormatter={labelFormatter}
239
+ showTotal={withTotal}
240
+ />
241
+ }
242
+ />
243
+ )}
244
+ {!bare && renderReferenceLine({ referenceLine, numericMax, theme })}
234
245
 
235
246
  {rendered.map((entry, slot) => (
236
247
  <Bar
@@ -272,13 +283,13 @@ function BarChart({
272
283
  </Bar>
273
284
  ))}
274
285
 
275
- {renderMarkers(markers, theme)}
286
+ {!bare && renderMarkers(markers, theme)}
276
287
  </RechartsBarChart>
277
288
  </ResponsiveContainer>
278
289
  )}
279
290
  </div>
280
291
 
281
- {legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
292
+ {legend && !bare && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
282
293
  </div>
283
294
  );
284
295
  }
@@ -26,6 +26,7 @@ export interface StatDelta {
26
26
  direction: "up" | "down";
27
27
  invert?: boolean;
28
28
  label?: React.ReactNode;
29
+ indicator?: "arrow" | "sign";
29
30
  }
30
31
 
31
32
  export interface StatProps extends React.ComponentProps<"div"> {
@@ -34,6 +35,7 @@ export interface StatProps extends React.ComponentProps<"div"> {
34
35
  delta?: StatDelta | null;
35
36
  sparkline?: number[] | Array<{ value: number }>;
36
37
  semantic?: "error" | "warning" | "success";
38
+ compact?: boolean;
37
39
  }
38
40
 
39
41
  const toSparkData = (sparkline: StatProps["sparkline"]) =>
@@ -43,61 +45,87 @@ const toSparkData = (sparkline: StatProps["sparkline"]) =>
43
45
 
44
46
  const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
45
47
 
46
- function Stat({ value, unit, delta, sparkline, semantic, className, ...props }: StatProps) {
47
- const { palette, theme } = useChartContext();
48
- const gradientId = React.useId().replace(/:/g, "");
49
- const sparkData = React.useMemo(() => toSparkData(sparkline), [sparkline]);
50
- const hasSpark = sparkData.some((point) => point.value > 0);
51
- // biome-ignore lint/style/noNonNullAssertion: palettes are never empty
52
- const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0]!;
53
-
48
+ function Stat({ value, unit, delta, sparkline, semantic, compact = false, className, ...props }: StatProps) {
54
49
  const sentiment =
55
50
  delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
56
51
 
57
52
  return (
58
53
  <div data-slot="stat" className={cn("flex min-w-0 flex-col gap-2.5", className)} {...props}>
59
- <div className="flex min-w-0 items-baseline gap-x-2.5">
60
- <span className="type-display-sm min-w-0 truncate font-bold leading-none tracking-tight tabular-nums text-foreground">
54
+ <div className={cn("flex min-w-0 items-baseline gap-x-2.5", compact && "gap-x-1.5")}>
55
+ <span
56
+ className={cn(
57
+ "min-w-0 font-bold leading-none tracking-tight tabular-nums text-foreground",
58
+ compact ? "type-display-xs whitespace-nowrap" : "type-display-sm truncate",
59
+ )}
60
+ >
61
61
  {value}
62
62
  </span>
63
63
  {unit && <span className="font-mono text-[11px] leading-none text-quaternary-foreground mb-0.5">{unit}</span>}
64
64
  {delta && (
65
- <DeltaPill sentiment={sentiment} className="mb-0.5">
66
- {delta.direction === "up" ? <ArrowUp className="size-3.5" /> : <ArrowDown className="size-3.5" />}
67
- {delta.label ?? `${delta.value}%`}
65
+ <DeltaPill sentiment={sentiment} className={cn("mb-0.5 shrink-0", compact && "px-1.5")}>
66
+ {delta.indicator === "sign" ? (
67
+ <>
68
+ {delta.direction === "up" ? "+" : "-"}
69
+ {delta.label ?? `${delta.value}%`}
70
+ </>
71
+ ) : (
72
+ <>
73
+ {delta.direction === "up" ? <ArrowUp className="size-3.5" /> : <ArrowDown className="size-3.5" />}
74
+ {delta.label ?? `${delta.value}%`}
75
+ </>
76
+ )}
68
77
  </DeltaPill>
69
78
  )}
70
79
  </div>
71
- {hasSpark && (
72
- <div className="pointer-events-none h-9 w-full">
73
- <ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
74
- <AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
75
- <defs>
76
- <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
77
- <stop offset="0%" stopColor={sparkColor} stopOpacity={0.4} />
78
- <stop offset="100%" stopColor={sparkColor} stopOpacity={0} />
79
- </linearGradient>
80
- </defs>
81
- <Area
82
- type="monotone"
83
- dataKey="value"
84
- stroke={sparkColor}
85
- strokeWidth={1.6}
86
- fill={`url(#${gradientId})`}
87
- isAnimationActive={false}
88
- dot={({ cx, cy, index }) => {
89
- const isLast = index === sparkData.length - 1;
90
- return isLast && cx != null && cy != null ? (
91
- <circle key={index} cx={cx} cy={cy} r={2.5} fill={sparkColor} />
92
- ) : (
93
- <g key={index} />
94
- );
95
- }}
96
- />
97
- </AreaChart>
98
- </ResponsiveContainer>
99
- </div>
100
- )}
80
+ <StatSparkline sparkline={sparkline} semantic={semantic} />
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function StatSparkline({
86
+ sparkline,
87
+ semantic,
88
+ className,
89
+ }: Pick<StatProps, "sparkline" | "semantic"> & { className?: string }) {
90
+ const { palette, theme } = useChartContext();
91
+ const gradientId = React.useId().replace(/:/g, "");
92
+ const sparkData = React.useMemo(() => toSparkData(sparkline), [sparkline]);
93
+ const hasSpark = sparkData.some((point) => point.value > 0);
94
+ // biome-ignore lint/style/noNonNullAssertion: palettes are never empty
95
+ const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0]!;
96
+
97
+ if (!hasSpark) {
98
+ return null;
99
+ }
100
+
101
+ return (
102
+ <div className={cn("pointer-events-none h-9 w-full", className)}>
103
+ <ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
104
+ <AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
105
+ <defs>
106
+ <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
107
+ <stop offset="0%" stopColor={sparkColor} stopOpacity={0.4} />
108
+ <stop offset="100%" stopColor={sparkColor} stopOpacity={0} />
109
+ </linearGradient>
110
+ </defs>
111
+ <Area
112
+ type="monotone"
113
+ dataKey="value"
114
+ stroke={sparkColor}
115
+ strokeWidth={1.6}
116
+ fill={`url(#${gradientId})`}
117
+ isAnimationActive={false}
118
+ dot={({ cx, cy, index }) => {
119
+ const isLast = index === sparkData.length - 1;
120
+ return isLast && cx != null && cy != null ? (
121
+ <circle key={index} cx={cx} cy={cy} r={2.5} fill={sparkColor} />
122
+ ) : (
123
+ <g key={index} />
124
+ );
125
+ }}
126
+ />
127
+ </AreaChart>
128
+ </ResponsiveContainer>
101
129
  </div>
102
130
  );
103
131
  }
@@ -110,4 +138,4 @@ function DeltaPill({
110
138
  return <span className={cn(statDeltaVariants({ sentiment }), className)} {...props} />;
111
139
  }
112
140
 
113
- export { Stat, statDeltaVariants };
141
+ export { Stat, StatSparkline, statDeltaVariants };