@alpic-ai/ui 0.0.0-staging.g4a0bec2 → 0.0.0-staging.g4d42ca0

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.
@@ -22,6 +22,7 @@ interface BarChartProps {
22
22
  };
23
23
  markers?: ChartMarker[];
24
24
  texture?: boolean;
25
+ partialLastBar?: boolean;
25
26
  loading?: boolean;
26
27
  valueFormatter?: (value: number) => string;
27
28
  labelFormatter?: (label: string | number) => string;
@@ -41,6 +42,7 @@ declare function BarChart({
41
42
  referenceLine,
42
43
  markers,
43
44
  texture,
45
+ partialLastBar,
44
46
  loading,
45
47
  valueFormatter,
46
48
  labelFormatter,
@@ -7,11 +7,11 @@ import { ChartTooltipContent } from "./chart-tooltip.mjs";
7
7
  import { Skeleton } from "./skeleton.mjs";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import * as React$1 from "react";
10
- import { Bar, BarChart as BarChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
10
+ import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
11
11
  //#region src/components/bar-chart.tsx
12
12
  const BAR_RADIUS = 4;
13
13
  const MAX_BAR_SIZE = 48;
14
- function BarChart({ data, index, series, variant = "stacked", legend = false, legendAlign = "left", valueLabels = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
14
+ 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
15
  const { palette: paletteColors, theme } = useChartContext(palette);
16
16
  const reactId = React$1.useId().replace(/:/g, "");
17
17
  const resolved = resolveSeries(series, paletteColors, theme);
@@ -38,6 +38,20 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
38
38
  series,
39
39
  stacked
40
40
  ]);
41
+ const topSlotByRow = React$1.useMemo(() => {
42
+ if (!stacked) return [];
43
+ return data.map((row) => {
44
+ for (let slot = rendered.length - 1; slot >= 0; slot -= 1) {
45
+ const value = Number(row[rendered[slot]?.key ?? ""]);
46
+ if (Number.isFinite(value) && value > 0) return slot;
47
+ }
48
+ return -1;
49
+ });
50
+ }, [
51
+ data,
52
+ rendered,
53
+ stacked
54
+ ]);
41
55
  const margin = {
42
56
  top: markers?.length || valueLabels ? 18 : 8,
43
57
  right: 8,
@@ -207,7 +221,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
207
221
  position: "insideBottomRight"
208
222
  } : void 0
209
223
  }),
210
- rendered.map((entry, slot) => /* @__PURE__ */ jsx(Bar, {
224
+ rendered.map((entry, slot) => /* @__PURE__ */ jsxs(Bar, {
211
225
  dataKey: entry.key,
212
226
  name: entry.name,
213
227
  stackId: stacked ? "stack" : void 0,
@@ -220,10 +234,30 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
220
234
  isAnimationActive: false,
221
235
  animationDuration: 650,
222
236
  animationEasing: "ease-out",
223
- children: valueLabels && (!stacked || rendered.length === 1) && /* @__PURE__ */ jsx(LabelList, {
237
+ children: [(stacked || partialLastBar) && data.map((row, rowIndex) => {
238
+ const roundedTop = stacked ? topSlotByRow[rowIndex] === slot : true;
239
+ const isPartial = partialLastBar && rowIndex === data.length - 1;
240
+ return /* @__PURE__ */ jsx(Cell, {
241
+ radius: roundedTop ? [
242
+ BAR_RADIUS,
243
+ BAR_RADIUS,
244
+ 0,
245
+ 0
246
+ ] : [
247
+ 0,
248
+ 0,
249
+ 0,
250
+ 0
251
+ ],
252
+ fillOpacity: isPartial ? .15 : 1,
253
+ stroke: isPartial ? entry.color : void 0,
254
+ strokeWidth: isPartial ? 1.25 : 0,
255
+ strokeDasharray: isPartial ? "3 2" : void 0
256
+ }, `${entry.key}-${row[index] ?? rowIndex}`);
257
+ }), valueLabels && (!stacked || rendered.length === 1) && /* @__PURE__ */ jsx(LabelList, {
224
258
  dataKey: entry.key,
225
259
  content: renderValueLabel(entry.color)
226
- })
260
+ })]
227
261
  }, entry.key)),
228
262
  markers?.map((marker) => /* @__PURE__ */ jsx(ReferenceDot, {
229
263
  x: marker.x,
@@ -22,6 +22,9 @@ interface HeatmapChartProps {
22
22
  label: string;
23
23
  format?: (value: number) => string;
24
24
  }>;
25
+ tooltipTitleKey?: string;
26
+ colorScale?: "linear" | "rank";
27
+ showLegend?: boolean;
25
28
  ariaLabel?: string;
26
29
  className?: string;
27
30
  }
@@ -41,6 +44,9 @@ declare function HeatmapChart({
41
44
  xTickFormatter,
42
45
  yTickFormatter,
43
46
  tooltipMetrics,
47
+ tooltipTitleKey,
48
+ colorScale,
49
+ showLegend,
44
50
  ariaLabel,
45
51
  className
46
52
  }: HeatmapChartProps): React$1.JSX.Element;
@@ -31,7 +31,7 @@ const uniqueInOrder = (values) => {
31
31
  }
32
32
  return ordered;
33
33
  };
34
- function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square", palette, xLabels, yLabels, showAllXLabels = false, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, tooltipMetrics, ariaLabel = "Activity heatmap", className }) {
34
+ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square", palette, xLabels, yLabels, showAllXLabels = false, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, tooltipMetrics, tooltipTitleKey, colorScale = "linear", showLegend = false, ariaLabel = "Activity heatmap", className }) {
35
35
  const { paletteName, theme } = useChartContext(palette);
36
36
  const [hovered, setHovered] = React$1.useState(null);
37
37
  const [mounted, setMounted] = React$1.useState(false);
@@ -81,6 +81,19 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
81
81
  yLabels
82
82
  ]);
83
83
  const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
84
+ const colorFractionOf = React$1.useMemo(() => {
85
+ if (colorScale === "rank") {
86
+ const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort((lower, upper) => lower - upper);
87
+ const RANK_FLOOR = .18;
88
+ const fractionByValue = new Map(sorted.map((value, index) => [value, sorted.length > 1 ? RANK_FLOOR + (1 - RANK_FLOOR) * (index / (sorted.length - 1)) : 1]));
89
+ return (value) => value > 0 ? fractionByValue.get(value) ?? 0 : 0;
90
+ }
91
+ return (value) => maxValue > 0 ? value / maxValue : 0;
92
+ }, [
93
+ cells,
94
+ colorScale,
95
+ maxValue
96
+ ]);
84
97
  if (loading) return /* @__PURE__ */ jsxs("div", {
85
98
  className: cn("flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs", className),
86
99
  style: { minHeight: PLACEHOLDER_HEIGHT },
@@ -91,7 +104,7 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
91
104
  style: { minHeight: PLACEHOLDER_HEIGHT },
92
105
  children: "no data in range"
93
106
  });
94
- const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
107
+ const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light, theme.isDark);
95
108
  const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
96
109
  const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
97
110
  const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
@@ -104,125 +117,142 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
104
117
  return /* @__PURE__ */ jsxs("div", {
105
118
  "data-slot": "heatmap-chart",
106
119
  className: cn("w-full", className),
107
- children: [/* @__PURE__ */ jsx("div", {
108
- className: "overflow-x-auto",
109
- children: /* @__PURE__ */ jsxs("svg", {
110
- viewBox: `0 0 ${totalWidth} ${totalHeight}`,
111
- className: "block min-w-[480px]",
112
- style: {
113
- width: "100%",
114
- height: "auto"
115
- },
116
- role: "img",
117
- "aria-label": ariaLabel,
118
- onMouseLeave: () => setHovered(null),
119
- children: [
120
- columns.map((label, col) => col % xStride === 0 ? /* @__PURE__ */ jsx("text", {
121
- x: cellX(col) + CELL / 2,
122
- y: PAD.top - 7,
123
- textAnchor: "middle",
124
- className: "fill-quaternary-foreground font-mono text-[8.5px]",
125
- children: formatX(label)
126
- }, `x-${label}`) : null),
127
- rows.map((label, row) => /* @__PURE__ */ jsx("text", {
128
- x: PAD.left - 8,
129
- y: cellY(row) + CELL / 2 + 3,
130
- textAnchor: "end",
131
- className: "fill-quaternary-foreground font-mono text-[8.5px]",
132
- children: formatY(label)
133
- }, `y-${label}`)),
134
- cells.map((cell) => {
135
- const fraction = cell.value / maxValue;
136
- const fill = heatColor(ramp, fraction);
137
- const key = pairKey(cell.rowLabel, cell.colLabel);
138
- const isPeak = highlightPeak && key === peakKey;
139
- if (variant === "dot") return /* @__PURE__ */ jsx("circle", {
140
- cx: cellX(cell.colIndex) + CELL / 2,
141
- cy: cellY(cell.rowIndex) + CELL / 2,
142
- r: CELL / 2 * (DOT_MIN + DOT_RANGE * fraction),
143
- fill,
144
- stroke: isPeak ? theme.foreground : void 0,
145
- strokeWidth: isPeak ? 1.4 : void 0
146
- }, key);
147
- return /* @__PURE__ */ jsx("rect", {
120
+ children: [
121
+ /* @__PURE__ */ jsx("div", {
122
+ className: "overflow-x-auto",
123
+ children: /* @__PURE__ */ jsxs("svg", {
124
+ viewBox: `0 0 ${totalWidth} ${totalHeight}`,
125
+ className: "block min-w-[480px]",
126
+ style: {
127
+ width: "100%",
128
+ height: "auto"
129
+ },
130
+ role: "img",
131
+ "aria-label": ariaLabel,
132
+ onMouseLeave: () => setHovered(null),
133
+ children: [
134
+ columns.map((label, col) => col % xStride === 0 ? /* @__PURE__ */ jsx("text", {
135
+ x: cellX(col) + CELL / 2,
136
+ y: PAD.top - 7,
137
+ textAnchor: "middle",
138
+ className: "fill-quaternary-foreground font-mono text-[8.5px]",
139
+ children: formatX(label)
140
+ }, `x-${label}`) : null),
141
+ rows.map((label, row) => /* @__PURE__ */ jsx("text", {
142
+ x: PAD.left - 8,
143
+ y: cellY(row) + CELL / 2 + 3,
144
+ textAnchor: "end",
145
+ className: "fill-quaternary-foreground font-mono text-[8.5px]",
146
+ children: formatY(label)
147
+ }, `y-${label}`)),
148
+ cells.map((cell) => {
149
+ const fraction = colorFractionOf(cell.value);
150
+ const fill = heatColor(ramp, fraction);
151
+ const key = pairKey(cell.rowLabel, cell.colLabel);
152
+ const isPeak = highlightPeak && key === peakKey;
153
+ if (variant === "dot") return /* @__PURE__ */ jsx("circle", {
154
+ cx: cellX(cell.colIndex) + CELL / 2,
155
+ cy: cellY(cell.rowIndex) + CELL / 2,
156
+ r: CELL / 2 * (DOT_MIN + DOT_RANGE * fraction),
157
+ fill,
158
+ stroke: isPeak ? theme.foreground : void 0,
159
+ strokeWidth: isPeak ? 1.4 : void 0
160
+ }, key);
161
+ return /* @__PURE__ */ jsx("rect", {
162
+ x: cellX(cell.colIndex),
163
+ y: cellY(cell.rowIndex),
164
+ width: CELL,
165
+ height: CELL,
166
+ rx: 2.5,
167
+ fill,
168
+ stroke: isPeak ? theme.foreground : void 0,
169
+ strokeWidth: isPeak ? 1.4 : void 0
170
+ }, key);
171
+ }),
172
+ cells.map((cell) => /* @__PURE__ */ jsx("rect", {
148
173
  x: cellX(cell.colIndex),
149
174
  y: cellY(cell.rowIndex),
150
- width: CELL,
151
- height: CELL,
152
- rx: 2.5,
153
- fill,
154
- stroke: isPeak ? theme.foreground : void 0,
155
- strokeWidth: isPeak ? 1.4 : void 0
156
- }, key);
157
- }),
158
- cells.map((cell) => /* @__PURE__ */ jsx("rect", {
159
- x: cellX(cell.colIndex),
160
- y: cellY(cell.rowIndex),
161
- width: 25,
162
- height: 25,
163
- fill: "transparent",
164
- onMouseMove: (event) => setHovered({
165
- x: event.clientX,
166
- y: event.clientY,
167
- rowLabel: cell.rowLabel,
168
- colLabel: cell.colLabel,
169
- value: cell.value,
170
- record: cell.record
171
- })
172
- }, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
173
- ]
174
- })
175
- }), mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
176
- className: "pointer-events-none fixed z-50 min-w-[130px] rounded-lg border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-lg",
177
- style: {
178
- left: tooltipLeft,
179
- top: tooltipTop
180
- },
181
- children: [/* @__PURE__ */ jsxs("p", {
182
- className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
183
- children: [
184
- formatY(hovered.rowLabel),
185
- " · ",
186
- formatX(hovered.colLabel)
187
- ]
188
- }), tooltipMetrics && tooltipMetrics.length > 0 ? /* @__PURE__ */ jsx("div", {
189
- className: "flex flex-col gap-1",
190
- children: tooltipMetrics.map((metric) => {
191
- const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
192
- const isActive = metric.key === dataKey;
193
- return /* @__PURE__ */ jsxs("div", {
194
- className: "flex items-center justify-between gap-4 text-text-xs",
195
- children: [/* @__PURE__ */ jsxs("span", {
196
- className: "inline-flex items-center gap-2 text-muted-foreground",
197
- children: [/* @__PURE__ */ jsx("span", {
198
- "aria-hidden": true,
199
- className: "size-2 shrink-0 rounded-[2px]",
200
- style: {
201
- background: isActive ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
202
- opacity: isActive ? 1 : .35
203
- }
204
- }), metric.label]
205
- }), /* @__PURE__ */ jsx("span", {
206
- className: cn("font-mono tabular-nums", isActive ? "font-semibold text-foreground" : "text-muted-foreground"),
207
- children: (metric.format ?? valueFormatter)(raw)
208
- })]
209
- }, metric.key);
175
+ width: 25,
176
+ height: 25,
177
+ fill: "transparent",
178
+ onMouseMove: (event) => setHovered({
179
+ x: event.clientX,
180
+ y: event.clientY,
181
+ rowLabel: cell.rowLabel,
182
+ colLabel: cell.colLabel,
183
+ value: cell.value,
184
+ record: cell.record
185
+ })
186
+ }, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
187
+ ]
210
188
  })
211
- }) : /* @__PURE__ */ jsxs("div", {
212
- className: "flex items-center justify-between gap-4 text-text-xs",
213
- children: [/* @__PURE__ */ jsxs("span", {
214
- className: "inline-flex items-center gap-2 text-muted-foreground",
215
- children: [/* @__PURE__ */ jsx("span", {
189
+ }),
190
+ showLegend && /* @__PURE__ */ jsxs("div", {
191
+ className: "mt-3 flex items-center justify-end gap-2",
192
+ children: [
193
+ /* @__PURE__ */ jsx("span", {
194
+ className: "font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]",
195
+ children: "Less"
196
+ }),
197
+ /* @__PURE__ */ jsx("span", {
216
198
  "aria-hidden": true,
217
- className: "size-2 shrink-0 rounded-[2px]",
218
- style: { background: heatColor(ramp, hovered.value / maxValue) }
219
- }), "activity"]
220
- }), /* @__PURE__ */ jsx("span", {
221
- className: "font-mono font-semibold tabular-nums text-foreground",
222
- children: valueFormatter(hovered.value)
199
+ className: "h-2 w-24 rounded-full border border-border",
200
+ style: { background: `linear-gradient(90deg, ${ramp.join(", ")})` }
201
+ }),
202
+ /* @__PURE__ */ jsx("span", {
203
+ className: "font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]",
204
+ children: "More"
205
+ })
206
+ ]
207
+ }),
208
+ mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
209
+ className: "pointer-events-none fixed z-50 min-w-[130px] rounded-lg border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-lg",
210
+ style: {
211
+ left: tooltipLeft,
212
+ top: tooltipTop
213
+ },
214
+ children: [/* @__PURE__ */ jsx("p", {
215
+ className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
216
+ children: tooltipTitleKey && typeof hovered.record?.[tooltipTitleKey] === "string" ? hovered.record[tooltipTitleKey] : `${formatY(hovered.rowLabel)} · ${formatX(hovered.colLabel)}`
217
+ }), tooltipMetrics && tooltipMetrics.length > 0 ? /* @__PURE__ */ jsx("div", {
218
+ className: "flex flex-col gap-1",
219
+ children: tooltipMetrics.map((metric) => {
220
+ const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
221
+ const isActive = metric.key === dataKey;
222
+ return /* @__PURE__ */ jsxs("div", {
223
+ className: "flex items-center justify-between gap-4 text-text-xs",
224
+ children: [/* @__PURE__ */ jsxs("span", {
225
+ className: "inline-flex items-center gap-2 text-muted-foreground",
226
+ children: [/* @__PURE__ */ jsx("span", {
227
+ "aria-hidden": true,
228
+ className: "size-2 shrink-0 rounded-[2px]",
229
+ style: {
230
+ background: isActive ? heatColor(ramp, colorFractionOf(hovered.value)) : theme.mutedForeground,
231
+ opacity: isActive ? 1 : .35
232
+ }
233
+ }), metric.label]
234
+ }), /* @__PURE__ */ jsx("span", {
235
+ className: cn("font-mono tabular-nums", isActive ? "font-semibold text-foreground" : "text-muted-foreground"),
236
+ children: (metric.format ?? valueFormatter)(raw)
237
+ })]
238
+ }, metric.key);
239
+ })
240
+ }) : /* @__PURE__ */ jsxs("div", {
241
+ className: "flex items-center justify-between gap-4 text-text-xs",
242
+ children: [/* @__PURE__ */ jsxs("span", {
243
+ className: "inline-flex items-center gap-2 text-muted-foreground",
244
+ children: [/* @__PURE__ */ jsx("span", {
245
+ "aria-hidden": true,
246
+ className: "size-2 shrink-0 rounded-[2px]",
247
+ style: { background: heatColor(ramp, colorFractionOf(hovered.value)) }
248
+ }), "activity"]
249
+ }), /* @__PURE__ */ jsx("span", {
250
+ className: "font-mono font-semibold tabular-nums text-foreground",
251
+ children: valueFormatter(hovered.value)
252
+ })]
223
253
  })]
224
- })]
225
- }), document.body)]
254
+ }), document.body)
255
+ ]
226
256
  });
227
257
  }
228
258
  //#endregion
@@ -7,7 +7,7 @@ import { cva } from "class-variance-authority";
7
7
  import * as React$1 from "react";
8
8
  import { Area, AreaChart, ResponsiveContainer } from "recharts";
9
9
  //#region src/components/stat.tsx
10
- const statDeltaVariants = cva("inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 font-mono text-[11px] font-medium leading-none", {
10
+ const statDeltaVariants = cva("inline-flex items-center gap-0.5 rounded-md px-2 py-1 font-mono text-xs font-medium leading-none", {
11
11
  variants: { sentiment: {
12
12
  positive: "text-success bg-success/12",
13
13
  negative: "text-destructive bg-destructive/12"
@@ -51,7 +51,7 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props })
51
51
  delta && /* @__PURE__ */ jsxs(DeltaPill, {
52
52
  sentiment,
53
53
  className: "mb-0.5",
54
- children: [delta.direction === "up" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3" }), delta.label ?? `${delta.value}%`]
54
+ children: [delta.direction === "up" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3.5" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3.5" }), delta.label ?? `${delta.value}%`]
55
55
  })
56
56
  ]
57
57
  }), hasSpark && /* @__PURE__ */ jsx("div", {
@@ -1,10 +1,14 @@
1
1
  import * as React$1 from "react";
2
2
 
3
3
  //#region src/components/table.d.ts
4
+ interface TableProps extends React$1.ComponentProps<"table"> {
5
+ containerClassName?: string;
6
+ }
4
7
  declare function Table({
5
8
  className,
9
+ containerClassName,
6
10
  ...props
7
- }: React$1.ComponentProps<"table">): React$1.JSX.Element;
11
+ }: TableProps): React$1.JSX.Element;
8
12
  declare function TableHeader({
9
13
  className,
10
14
  ...props
@@ -2,9 +2,9 @@
2
2
  import { cn } from "../lib/cn.mjs";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region src/components/table.tsx
5
- function Table({ className, ...props }) {
5
+ function Table({ className, containerClassName, ...props }) {
6
6
  return /* @__PURE__ */ jsx("div", {
7
- className: "relative w-full overflow-auto rounded-xl border border-border-secondary bg-background",
7
+ className: cn("relative w-full overflow-auto rounded-xl border border-border-secondary bg-background", containerClassName),
8
8
  children: /* @__PURE__ */ jsx("table", {
9
9
  "data-slot": "table",
10
10
  className: cn("w-full caption-bottom type-text-sm", className),
@@ -42,7 +42,10 @@ const HEAT_RAMPS = {
42
42
  magenta: heatRampMagenta,
43
43
  cyan: heatRampMint
44
44
  };
45
- const heatRamp = (palette, empty) => HEAT_RAMPS[palette](empty);
45
+ const heatRamp = (palette, empty, isDark = true) => {
46
+ const [, ...colored] = HEAT_RAMPS[palette](empty);
47
+ return [empty, ...isDark ? colored : colored.reverse()];
48
+ };
46
49
  const hexToRgb = (hex) => {
47
50
  const value = Number.parseInt(hex.slice(1), 16);
48
51
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/ui",
3
- "version": "0.0.0-staging.g4a0bec2",
3
+ "version": "0.0.0-staging.g4d42ca0",
4
4
  "description": "Alpic design system — shared UI components",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,48 +23,48 @@
23
23
  "src"
24
24
  ],
25
25
  "peerDependencies": {
26
- "lucide-react": "^1.21.0",
26
+ "lucide-react": "^1.23.0",
27
27
  "react": "^19.2.7",
28
28
  "react-dom": "^19.2.7",
29
- "react-hook-form": "^7.80.0",
29
+ "react-hook-form": "^7.81.0",
30
30
  "sonner": "^2.0.7",
31
- "tailwindcss": "^4.3.1",
31
+ "tailwindcss": "^4.3.2",
32
32
  "tw-animate-css": "^1.4.0"
33
33
  },
34
34
  "dependencies": {
35
- "@radix-ui/react-accordion": "^1.2.14",
36
- "@radix-ui/react-avatar": "^1.2.0",
37
- "@radix-ui/react-checkbox": "^1.3.5",
38
- "@radix-ui/react-collapsible": "^1.1.14",
39
- "@radix-ui/react-dialog": "^1.1.17",
40
- "@radix-ui/react-dropdown-menu": "^2.1.18",
41
- "@radix-ui/react-label": "^2.1.10",
42
- "@radix-ui/react-popover": "^1.1.17",
43
- "@radix-ui/react-radio-group": "^1.4.1",
44
- "@radix-ui/react-scroll-area": "^1.2.12",
45
- "@radix-ui/react-select": "^2.3.1",
46
- "@radix-ui/react-separator": "^1.1.10",
35
+ "@radix-ui/react-accordion": "^1.2.15",
36
+ "@radix-ui/react-avatar": "^1.2.1",
37
+ "@radix-ui/react-checkbox": "^1.3.6",
38
+ "@radix-ui/react-collapsible": "^1.1.15",
39
+ "@radix-ui/react-dialog": "^1.1.18",
40
+ "@radix-ui/react-dropdown-menu": "^2.1.19",
41
+ "@radix-ui/react-label": "^2.1.11",
42
+ "@radix-ui/react-popover": "^1.1.18",
43
+ "@radix-ui/react-radio-group": "^1.4.2",
44
+ "@radix-ui/react-scroll-area": "^1.2.13",
45
+ "@radix-ui/react-select": "^2.3.2",
46
+ "@radix-ui/react-separator": "^1.1.11",
47
47
  "@radix-ui/react-slot": "^1.3.0",
48
- "@radix-ui/react-switch": "^1.3.1",
49
- "@radix-ui/react-tabs": "^1.1.15",
50
- "@radix-ui/react-toggle-group": "^1.1.13",
51
- "@radix-ui/react-tooltip": "^1.2.10",
48
+ "@radix-ui/react-switch": "^1.3.2",
49
+ "@radix-ui/react-tabs": "^1.1.16",
50
+ "@radix-ui/react-toggle-group": "^1.1.14",
51
+ "@radix-ui/react-tooltip": "^1.2.11",
52
52
  "class-variance-authority": "^0.7.1",
53
53
  "clsx": "^2.1.1",
54
54
  "cmdk": "^1.1.1",
55
- "recharts": "^3.9.0",
55
+ "recharts": "^3.9.2",
56
56
  "tailwind-merge": "^3.6.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@ladle/react": "^5.1.1",
60
- "@tailwindcss/postcss": "^4.3.1",
60
+ "@tailwindcss/postcss": "^4.3.2",
61
61
  "@types/react": "19.2.17",
62
62
  "@types/react-dom": "19.2.3",
63
- "lucide-react": "^1.21.0",
64
- "react-hook-form": "^7.80.0",
63
+ "lucide-react": "^1.23.0",
64
+ "react-hook-form": "^7.81.0",
65
65
  "shx": "^0.4.0",
66
66
  "sonner": "^2.0.7",
67
- "tailwindcss": "^4.3.1",
67
+ "tailwindcss": "^4.3.2",
68
68
  "tsdown": "^0.22.3",
69
69
  "tw-animate-css": "^1.4.0",
70
70
  "typescript": "^6.0.3"
@@ -72,7 +72,7 @@
72
72
  "scripts": {
73
73
  "build": "shx rm -rf dist && tsdown",
74
74
  "format": "biome check --write --error-on-warnings .",
75
- "test:type": "tsc --noEmit",
75
+ "test:type": "tsgo --noEmit",
76
76
  "test:format": "biome check --error-on-warnings .",
77
77
  "dev:ui": "ladle serve",
78
78
  "publish:npm": "pnpm publish --tag \"${NPM_TAG}\" --access public --no-git-checks"
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import {
5
5
  Bar,
6
6
  CartesianGrid,
7
+ Cell,
7
8
  LabelList,
8
9
  BarChart as RechartsBarChart,
9
10
  ReferenceArea,
@@ -41,6 +42,7 @@ export interface BarChartProps {
41
42
  referenceLine?: { y: number; label?: string; band?: boolean };
42
43
  markers?: ChartMarker[];
43
44
  texture?: boolean;
45
+ partialLastBar?: boolean;
44
46
  loading?: boolean;
45
47
  valueFormatter?: (value: number) => string;
46
48
  labelFormatter?: (label: string | number) => string;
@@ -61,6 +63,7 @@ function BarChart({
61
63
  referenceLine,
62
64
  markers,
63
65
  texture = false,
66
+ partialLastBar = false,
64
67
  loading = false,
65
68
  valueFormatter = (value) => value.toLocaleString("en-US"),
66
69
  labelFormatter,
@@ -97,6 +100,21 @@ function BarChart({
97
100
  return max;
98
101
  }, [data, series, stacked]);
99
102
 
103
+ const topSlotByRow = React.useMemo(() => {
104
+ if (!stacked) {
105
+ return [];
106
+ }
107
+ return data.map((row) => {
108
+ for (let slot = rendered.length - 1; slot >= 0; slot -= 1) {
109
+ const value = Number(row[rendered[slot]?.key ?? ""]);
110
+ if (Number.isFinite(value) && value > 0) {
111
+ return slot;
112
+ }
113
+ }
114
+ return -1;
115
+ });
116
+ }, [data, rendered, stacked]);
117
+
100
118
  const margin = { top: markers?.length || valueLabels ? 18 : 8, right: 8, bottom: 2, left: 0 };
101
119
 
102
120
  const axis = {
@@ -278,6 +296,24 @@ function BarChart({
278
296
  animationDuration={650}
279
297
  animationEasing="ease-out"
280
298
  >
299
+ {(stacked || partialLastBar) &&
300
+ data.map((row, rowIndex) => {
301
+ const roundedTop = stacked ? topSlotByRow[rowIndex] === slot : true;
302
+ const isPartial = partialLastBar && rowIndex === data.length - 1;
303
+ const cellRadius = (roundedTop
304
+ ? [BAR_RADIUS, BAR_RADIUS, 0, 0]
305
+ : [0, 0, 0, 0]) as unknown as number;
306
+ return (
307
+ <Cell
308
+ key={`${entry.key}-${row[index] ?? rowIndex}`}
309
+ radius={cellRadius}
310
+ fillOpacity={isPartial ? 0.15 : 1}
311
+ stroke={isPartial ? entry.color : undefined}
312
+ strokeWidth={isPartial ? 1.25 : 0}
313
+ strokeDasharray={isPartial ? "3 2" : undefined}
314
+ />
315
+ );
316
+ })}
281
317
  {valueLabels && (!stacked || rendered.length === 1) && (
282
318
  <LabelList dataKey={entry.key} content={renderValueLabel(entry.color)} />
283
319
  )}
@@ -24,6 +24,9 @@ export interface HeatmapChartProps {
24
24
  xTickFormatter?: (label: string) => string;
25
25
  yTickFormatter?: (label: string) => string;
26
26
  tooltipMetrics?: ReadonlyArray<{ key: string; label: string; format?: (value: number) => string }>;
27
+ tooltipTitleKey?: string;
28
+ colorScale?: "linear" | "rank";
29
+ showLegend?: boolean;
27
30
  ariaLabel?: string;
28
31
  className?: string;
29
32
  }
@@ -83,6 +86,9 @@ function HeatmapChart({
83
86
  xTickFormatter,
84
87
  yTickFormatter,
85
88
  tooltipMetrics,
89
+ tooltipTitleKey,
90
+ colorScale = "linear",
91
+ showLegend = false,
86
92
  ariaLabel = "Activity heatmap",
87
93
  className,
88
94
  }: HeatmapChartProps) {
@@ -123,6 +129,28 @@ function HeatmapChart({
123
129
  const { columns, rows, cells, maxValue, peakKey } = layout;
124
130
  const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
125
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).
135
+ const colorFractionOf = React.useMemo(() => {
136
+ if (colorScale === "rank") {
137
+ const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort(
138
+ (lower, upper) => lower - upper,
139
+ );
140
+ // Floor non-zero ranks above the empty colour so the quietest active day
141
+ // still reads as coloured, never mistaken for no data.
142
+ const RANK_FLOOR = 0.18;
143
+ const fractionByValue = new Map(
144
+ sorted.map((value, index) => [
145
+ value,
146
+ sorted.length > 1 ? RANK_FLOOR + (1 - RANK_FLOOR) * (index / (sorted.length - 1)) : 1,
147
+ ]),
148
+ );
149
+ return (value: number) => (value > 0 ? (fractionByValue.get(value) ?? 0) : 0);
150
+ }
151
+ return (value: number) => (maxValue > 0 ? value / maxValue : 0);
152
+ }, [cells, colorScale, maxValue]);
153
+
126
154
  if (loading) {
127
155
  return (
128
156
  <div
@@ -152,7 +180,7 @@ function HeatmapChart({
152
180
  );
153
181
  }
154
182
 
155
- const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
183
+ const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light, theme.isDark);
156
184
  const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
157
185
  const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
158
186
  const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
@@ -209,7 +237,7 @@ function HeatmapChart({
209
237
  </text>
210
238
  ))}
211
239
  {cells.map((cell) => {
212
- const fraction = cell.value / maxValue;
240
+ const fraction = colorFractionOf(cell.value);
213
241
  const fill = heatColor(ramp, fraction);
214
242
  const key = pairKey(cell.rowLabel, cell.colLabel);
215
243
  const isPeak = highlightPeak && key === peakKey;
@@ -266,6 +294,18 @@ function HeatmapChart({
266
294
  </svg>
267
295
  </div>
268
296
 
297
+ {showLegend && (
298
+ <div className="mt-3 flex items-center justify-end gap-2">
299
+ <span className="font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]">Less</span>
300
+ <span
301
+ aria-hidden
302
+ className="h-2 w-24 rounded-full border border-border"
303
+ style={{ background: `linear-gradient(90deg, ${ramp.join(", ")})` }}
304
+ />
305
+ <span className="font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]">More</span>
306
+ </div>
307
+ )}
308
+
269
309
  {mounted &&
270
310
  hovered &&
271
311
  createPortal(
@@ -274,7 +314,9 @@ function HeatmapChart({
274
314
  style={{ left: tooltipLeft, top: tooltipTop }}
275
315
  >
276
316
  <p className="mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider">
277
- {formatY(hovered.rowLabel)} · {formatX(hovered.colLabel)}
317
+ {tooltipTitleKey && typeof hovered.record?.[tooltipTitleKey] === "string"
318
+ ? hovered.record[tooltipTitleKey]
319
+ : `${formatY(hovered.rowLabel)} · ${formatX(hovered.colLabel)}`}
278
320
  </p>
279
321
  {tooltipMetrics && tooltipMetrics.length > 0 ? (
280
322
  <div className="flex flex-col gap-1">
@@ -288,7 +330,9 @@ function HeatmapChart({
288
330
  aria-hidden
289
331
  className="size-2 shrink-0 rounded-[2px]"
290
332
  style={{
291
- background: isActive ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
333
+ background: isActive
334
+ ? heatColor(ramp, colorFractionOf(hovered.value))
335
+ : theme.mutedForeground,
292
336
  opacity: isActive ? 1 : 0.35,
293
337
  }}
294
338
  />
@@ -312,7 +356,7 @@ function HeatmapChart({
312
356
  <span
313
357
  aria-hidden
314
358
  className="size-2 shrink-0 rounded-[2px]"
315
- style={{ background: heatColor(ramp, hovered.value / maxValue) }}
359
+ style={{ background: heatColor(ramp, colorFractionOf(hovered.value)) }}
316
360
  />
317
361
  activity
318
362
  </span>
@@ -9,7 +9,7 @@ import { cn } from "../lib/cn";
9
9
  import { useChartContext } from "./chart-container";
10
10
 
11
11
  const statDeltaVariants = cva(
12
- "inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 font-mono text-[11px] font-medium leading-none",
12
+ "inline-flex items-center gap-0.5 rounded-md px-2 py-1 font-mono text-xs font-medium leading-none",
13
13
  {
14
14
  variants: {
15
15
  sentiment: {
@@ -63,7 +63,7 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props }:
63
63
  {unit && <span className="font-mono text-[11px] leading-none text-quaternary-foreground mb-0.5">{unit}</span>}
64
64
  {delta && (
65
65
  <DeltaPill sentiment={sentiment} className="mb-0.5">
66
- {delta.direction === "up" ? <ArrowUp className="size-3" /> : <ArrowDown className="size-3" />}
66
+ {delta.direction === "up" ? <ArrowUp className="size-3.5" /> : <ArrowDown className="size-3.5" />}
67
67
  {delta.label ?? `${delta.value}%`}
68
68
  </DeltaPill>
69
69
  )}
@@ -4,9 +4,18 @@ import type * as React from "react";
4
4
 
5
5
  import { cn } from "../lib/cn";
6
6
 
7
- function Table({ className, ...props }: React.ComponentProps<"table">) {
7
+ interface TableProps extends React.ComponentProps<"table"> {
8
+ containerClassName?: string;
9
+ }
10
+
11
+ function Table({ className, containerClassName, ...props }: TableProps) {
8
12
  return (
9
- <div className="relative w-full overflow-auto rounded-xl border border-border-secondary bg-background">
13
+ <div
14
+ className={cn(
15
+ "relative w-full overflow-auto rounded-xl border border-border-secondary bg-background",
16
+ containerClassName,
17
+ )}
18
+ >
10
19
  <table data-slot="table" className={cn("w-full caption-bottom type-text-sm", className)} {...props} />
11
20
  </div>
12
21
  );
@@ -55,7 +55,10 @@ const HEAT_RAMPS: Record<ChartPaletteName, (empty: string) => string[]> = {
55
55
  cyan: heatRampMint,
56
56
  };
57
57
 
58
- export const heatRamp = (palette: ChartPaletteName, empty: string) => HEAT_RAMPS[palette](empty);
58
+ export const heatRamp = (palette: ChartPaletteName, empty: string, isDark = true) => {
59
+ const [, ...colored] = HEAT_RAMPS[palette](empty);
60
+ return [empty, ...(isDark ? colored : colored.reverse())];
61
+ };
59
62
 
60
63
  const hexToRgb = (hex: string) => {
61
64
  const value = Number.parseInt(hex.slice(1), 16);