@hilum/ui 3.2.3 → 3.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilum/ui",
3
- "version": "3.2.3",
3
+ "version": "3.2.5",
4
4
  "private": false,
5
5
  "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
6
  "license": "MIT",
@@ -3,6 +3,11 @@
3
3
  import * as React from "react";
4
4
  import { AlertDialog } from "radix-ui";
5
5
  import { cn } from "../lib/utils";
6
+ import {
7
+ desktopDialogContentClassName,
8
+ dialogSheetMotionClassName,
9
+ mobileDialogSheetContentClassName,
10
+ } from "../lib/mobile-popper-sheet";
6
11
 
7
12
  const AlertDialogRoot = AlertDialog.Root;
8
13
  const AlertDialogTrigger = AlertDialog.Trigger;
@@ -34,11 +39,10 @@ const AlertDialogContent = React.forwardRef<
34
39
  <AlertDialog.Content
35
40
  ref={ref}
36
41
  className={cn(
37
- "fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
38
- "w-full max-w-md bg-card rounded-2xl shadow-elevated p-6",
39
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
40
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
41
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
42
+ mobileDialogSheetContentClassName,
43
+ desktopDialogContentClassName,
44
+ "sm:max-w-md",
45
+ dialogSheetMotionClassName,
42
46
  className,
43
47
  )}
44
48
  {...props}
@@ -55,7 +59,16 @@ function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDiv
55
59
  AlertDialogHeader.displayName = "AlertDialogHeader";
56
60
 
57
61
  function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
58
- return <div className={cn("flex items-center justify-end gap-2 mt-6", className)} {...props} />;
62
+ return (
63
+ <div
64
+ className={cn(
65
+ "mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end",
66
+ "[&>*]:w-full sm:[&>*]:w-auto",
67
+ className,
68
+ )}
69
+ {...props}
70
+ />
71
+ );
59
72
  }
60
73
  AlertDialogFooter.displayName = "AlertDialogFooter";
61
74
 
@@ -0,0 +1,99 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const calloutVariants = cva(
6
+ "flex w-full min-w-0 gap-3 rounded-xl border px-4 py-3.5 shadow-natural",
7
+ {
8
+ variants: {
9
+ tone: {
10
+ default: "border-border bg-card text-foreground",
11
+ info: "border-brand-secondary/50 bg-brand-secondary/15 text-foreground",
12
+ success: "border-success/25 bg-success/10 text-foreground",
13
+ warning: "border-warning/35 bg-warning/15 text-foreground",
14
+ destructive: "border-destructive/25 bg-destructive/10 text-foreground",
15
+ },
16
+ compact: {
17
+ true: "px-3 py-2.5",
18
+ false: "",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ tone: "default",
23
+ compact: false,
24
+ },
25
+ },
26
+ );
27
+
28
+ const calloutIconVariants = cva(
29
+ "mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg [&_svg]:size-4",
30
+ {
31
+ variants: {
32
+ tone: {
33
+ default: "bg-muted text-muted-foreground",
34
+ info: "bg-brand-secondary/35 text-muted-foreground",
35
+ success: "bg-success/15 text-success",
36
+ warning: "bg-warning/20 text-warning",
37
+ destructive: "bg-destructive/15 text-destructive",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ tone: "default",
42
+ },
43
+ },
44
+ );
45
+
46
+ interface CalloutProps
47
+ extends Omit<React.ComponentProps<"div">, "title">,
48
+ VariantProps<typeof calloutVariants> {
49
+ title?: React.ReactNode;
50
+ description?: React.ReactNode;
51
+ icon?: React.ReactNode;
52
+ actions?: React.ReactNode;
53
+ }
54
+
55
+ function Callout({
56
+ title,
57
+ description,
58
+ icon,
59
+ actions,
60
+ tone = "default",
61
+ compact = false,
62
+ className,
63
+ children,
64
+ role,
65
+ ...props
66
+ }: CalloutProps) {
67
+ const resolvedRole = role ?? (tone === "destructive" ? "alert" : "status");
68
+
69
+ return (
70
+ <div
71
+ role={resolvedRole}
72
+ className={cn(calloutVariants({ tone, compact }), className)}
73
+ {...props}
74
+ >
75
+ {icon && <div className={cn(calloutIconVariants({ tone }))}>{icon}</div>}
76
+ <div className="flex min-w-0 flex-1 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
77
+ <div className="min-w-0">
78
+ {title && <p className="body font-semibold text-balance">{title}</p>}
79
+ {description && (
80
+ <p className={cn("body text-pretty text-muted-foreground", title && "mt-1")}>
81
+ {description}
82
+ </p>
83
+ )}
84
+ {children && <div className={cn(title || description ? "mt-2" : "")}>{children}</div>}
85
+ </div>
86
+ {actions && (
87
+ <div className="flex shrink-0 items-center gap-2 max-sm:w-full max-sm:flex-col max-sm:items-stretch">
88
+ {actions}
89
+ </div>
90
+ )}
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ Callout.displayName = "Callout";
97
+
98
+ export { Callout };
99
+ export type { CalloutProps };
@@ -92,6 +92,18 @@ const CardFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>
92
92
  );
93
93
  CardFooter.displayName = "CardFooter";
94
94
 
95
+ const CardAction = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
96
+ ({ className, ...props }, ref) => (
97
+ <div
98
+ ref={ref}
99
+ data-slot="card-action"
100
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
101
+ {...props}
102
+ />
103
+ ),
104
+ );
105
+ CardAction.displayName = "CardAction";
106
+
95
107
  /* ------------------------------------------------------------------ */
96
108
  /* CardMedia — full-bleed image/gradient header */
97
109
  /* ------------------------------------------------------------------ */
@@ -115,6 +127,7 @@ export {
115
127
  CardDescription,
116
128
  CardContent,
117
129
  CardFooter,
130
+ CardAction,
118
131
  CardMedia,
119
132
  cardVariants,
120
133
  };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { ResponsiveContainer, type TooltipContentProps } from "recharts";
4
+ import * as RechartsPrimitive from "recharts";
5
5
  import { cn } from "../lib/utils";
6
6
  import { tokens } from "../tokens/tokens";
7
7
 
@@ -14,49 +14,418 @@ export const CHART_COLORS = {
14
14
  dark: tokens.semantic.light.foreground,
15
15
  };
16
16
 
17
- interface ChartContainerProps {
18
- children: React.ReactNode;
19
- className?: string;
20
- height?: number;
17
+ const THEMES = { light: "", dark: ".dark" } as const;
18
+
19
+ export type ChartConfig = {
20
+ [k in string]: {
21
+ label?: React.ReactNode;
22
+ icon?: React.ComponentType;
23
+ } & (
24
+ | { color?: string; theme?: never }
25
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
26
+ );
27
+ };
28
+
29
+ type ChartContextProps = {
30
+ config: ChartConfig;
31
+ };
32
+
33
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
34
+
35
+ function useChart() {
36
+ const context = React.useContext(ChartContext);
37
+
38
+ if (!context) {
39
+ throw new Error("useChart must be used within a <ChartContainer />");
40
+ }
41
+
42
+ return context;
21
43
  }
22
44
 
23
- function ChartContainer({ children, className, height = 300 }: ChartContainerProps) {
45
+ type ChartContainerProps = React.ComponentProps<"div"> & {
46
+ config?: ChartConfig;
47
+ height?: number;
48
+ children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
49
+ };
50
+
51
+ function ChartContainer({
52
+ id,
53
+ className,
54
+ children,
55
+ config = {},
56
+ height,
57
+ style,
58
+ ...props
59
+ }: ChartContainerProps) {
60
+ const uniqueId = React.useId();
61
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
62
+ const resolvedStyle =
63
+ height === undefined ? style : ({ ...style, height } as React.CSSProperties);
64
+
24
65
  return (
25
- <div className={cn("w-full", className)}>
26
- <ResponsiveContainer width="100%" height={height}>
27
- {children as React.ReactElement}
28
- </ResponsiveContainer>
29
- </div>
66
+ <ChartContext.Provider value={{ config }}>
67
+ <div
68
+ data-slot="chart"
69
+ data-chart={chartId}
70
+ className={cn(
71
+ "flex aspect-video justify-center text-xs",
72
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground",
73
+ "[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50",
74
+ "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border",
75
+ "[&_.recharts-dot[stroke='#fff']]:stroke-transparent",
76
+ "[&_.recharts-layer]:outline-hidden",
77
+ "[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border",
78
+ "[&_.recharts-radial-bar-background-sector]:fill-muted",
79
+ "[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted",
80
+ "[&_.recharts-reference-line_[stroke='#ccc']]:stroke-border",
81
+ "[&_.recharts-sector]:outline-hidden",
82
+ "[&_.recharts-sector[stroke='#fff']]:stroke-transparent",
83
+ "[&_.recharts-surface]:outline-hidden",
84
+ className,
85
+ )}
86
+ style={resolvedStyle}
87
+ {...props}
88
+ >
89
+ <ChartStyle id={chartId} config={config} />
90
+ <RechartsPrimitive.ResponsiveContainer>
91
+ {children}
92
+ </RechartsPrimitive.ResponsiveContainer>
93
+ </div>
94
+ </ChartContext.Provider>
30
95
  );
31
96
  }
32
97
  ChartContainer.displayName = "ChartContainer";
33
98
 
34
- // Custom tooltip component styled to match the design system
35
- interface ChartTooltipProps extends Partial<TooltipContentProps<number, string>> {
36
- labelClassName?: string;
99
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
100
+ const colorConfig = Object.entries(config).filter(
101
+ ([, itemConfig]) => itemConfig.theme || itemConfig.color,
102
+ );
103
+
104
+ if (!colorConfig.length) {
105
+ return null;
106
+ }
107
+
108
+ return (
109
+ <style
110
+ dangerouslySetInnerHTML={{
111
+ __html: Object.entries(THEMES)
112
+ .map(
113
+ ([theme, prefix]) => `
114
+ ${prefix} [data-chart=${id}] {
115
+ ${colorConfig
116
+ .map(([key, itemConfig]) => {
117
+ const color =
118
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
119
+ itemConfig.color;
120
+ return color ? ` --color-${key}: ${color};` : null;
121
+ })
122
+ .join("\n")}
37
123
  }
124
+ `,
125
+ )
126
+ .join("\n"),
127
+ }}
128
+ />
129
+ );
130
+ };
38
131
 
39
- function ChartTooltip({ active, payload, label, labelClassName }: ChartTooltipProps) {
40
- if (!active || !payload?.length) return null;
132
+ type ChartTooltipContentProps = Omit<
133
+ React.HTMLAttributes<HTMLDivElement>,
134
+ "children" | "content"
135
+ > & {
136
+ active?: boolean;
137
+ payload?: Array<Record<string, any>>;
138
+ indicator?: "line" | "dot" | "dashed";
139
+ hideLabel?: boolean;
140
+ hideIndicator?: boolean;
141
+ label?: React.ReactNode;
142
+ labelFormatter?: (
143
+ value: React.ReactNode,
144
+ payload: Array<Record<string, any>>,
145
+ ) => React.ReactNode;
146
+ labelClassName?: string;
147
+ formatter?: (
148
+ value: unknown,
149
+ name: unknown,
150
+ item: Record<string, any>,
151
+ index: number,
152
+ payload: unknown,
153
+ ) => React.ReactNode;
154
+ color?: string;
155
+ nameKey?: string;
156
+ labelKey?: string;
157
+ };
158
+
159
+ type ChartTooltipProps =
160
+ | React.ComponentProps<typeof RechartsPrimitive.Tooltip>
161
+ | ChartTooltipContentProps;
162
+
163
+ function ChartTooltip(props: ChartTooltipProps) {
164
+ const hasDirectTooltipPayload = "active" in props || "payload" in props;
165
+ if (hasDirectTooltipPayload) {
166
+ return <ChartTooltipContent {...(props as ChartTooltipContentProps)} />;
167
+ }
41
168
  return (
42
- <div className="rounded-xl border border-border bg-card px-3 py-2 shadow-elevated">
43
- {label && <p className={cn("mb-1 label text-muted-foreground", labelClassName)}>{label}</p>}
44
- {payload.map((entry, i) => (
45
- <div key={i} className="flex items-center gap-2">
46
- <span
47
- className="inline-block h-2 w-2 rounded-full"
48
- style={{ backgroundColor: entry.color }}
49
- />
50
- <span className="caption text-muted-foreground">{entry.name}:</span>
51
- <span className="caption font-medium text-foreground">{entry.value}</span>
169
+ <RechartsPrimitive.Tooltip
170
+ {...(props as React.ComponentProps<typeof RechartsPrimitive.Tooltip>)}
171
+ />
172
+ );
173
+ }
174
+ ChartTooltip.displayName = "ChartTooltip";
175
+
176
+ function ChartTooltipContent({
177
+ active,
178
+ payload,
179
+ className,
180
+ indicator = "dot",
181
+ hideLabel = false,
182
+ hideIndicator = false,
183
+ label,
184
+ labelFormatter,
185
+ labelClassName,
186
+ formatter,
187
+ color,
188
+ nameKey,
189
+ labelKey,
190
+ }: ChartTooltipContentProps) {
191
+ const chartContext = React.useContext(ChartContext);
192
+ const config = chartContext?.config ?? {};
193
+
194
+ const tooltipLabel = React.useMemo(() => {
195
+ if (hideLabel || !payload?.length) {
196
+ return null;
197
+ }
198
+
199
+ const [item] = payload;
200
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
201
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
202
+ const value =
203
+ !labelKey && typeof label === "string"
204
+ ? config[label as keyof typeof config]?.label || label
205
+ : itemConfig?.label;
206
+
207
+ if (labelFormatter) {
208
+ return (
209
+ <div className={cn("font-medium", labelClassName)}>
210
+ {labelFormatter(value, payload)}
52
211
  </div>
53
- ))}
212
+ );
213
+ }
214
+
215
+ if (!value) {
216
+ return null;
217
+ }
218
+
219
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
220
+ }, [
221
+ label,
222
+ labelFormatter,
223
+ payload,
224
+ hideLabel,
225
+ labelClassName,
226
+ config,
227
+ labelKey,
228
+ ]);
229
+
230
+ if (!active || !payload?.length) {
231
+ return null;
232
+ }
233
+
234
+ const nestLabel = payload.length === 1 && indicator !== "dot";
235
+
236
+ return (
237
+ <div
238
+ className={cn(
239
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
240
+ className,
241
+ )}
242
+ >
243
+ {!nestLabel ? tooltipLabel : null}
244
+ <div className="grid gap-1.5">
245
+ {payload.map((item: Record<string, any>, index: number) => {
246
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
247
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
248
+ const itemPayload = item.payload as Record<string, unknown> | undefined;
249
+ const indicatorColor =
250
+ color ||
251
+ (typeof itemPayload?.fill === "string" ? itemPayload.fill : undefined) ||
252
+ item.color;
253
+
254
+ return (
255
+ <div
256
+ key={`${item.dataKey || item.name || index}`}
257
+ className={cn(
258
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
259
+ indicator === "dot" && "items-center",
260
+ )}
261
+ >
262
+ {formatter && item?.value !== undefined && item.name ? (
263
+ formatter(item.value, item.name, item, index, item.payload)
264
+ ) : (
265
+ <>
266
+ {itemConfig?.icon ? (
267
+ <itemConfig.icon />
268
+ ) : (
269
+ !hideIndicator && (
270
+ <div
271
+ className={cn(
272
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
273
+ {
274
+ "h-2.5 w-2.5": indicator === "dot",
275
+ "w-1": indicator === "line",
276
+ "w-0 border-[1.5px] border-dashed bg-transparent":
277
+ indicator === "dashed",
278
+ "my-0.5": nestLabel && indicator === "dashed",
279
+ },
280
+ )}
281
+ style={
282
+ {
283
+ "--color-bg": indicatorColor,
284
+ "--color-border": indicatorColor,
285
+ } as React.CSSProperties
286
+ }
287
+ />
288
+ )
289
+ )}
290
+ <div
291
+ className={cn(
292
+ "flex flex-1 justify-between leading-none",
293
+ nestLabel ? "items-end" : "items-center",
294
+ )}
295
+ >
296
+ <div className="grid gap-1.5">
297
+ {nestLabel ? tooltipLabel : null}
298
+ <span className="text-muted-foreground">
299
+ {itemConfig?.label || item.name}
300
+ </span>
301
+ </div>
302
+ {item.value !== undefined && item.value !== null && (
303
+ <span className="font-mono font-medium tabular-nums text-foreground">
304
+ {typeof item.value === "number"
305
+ ? item.value.toLocaleString()
306
+ : item.value}
307
+ </span>
308
+ )}
309
+ </div>
310
+ </>
311
+ )}
312
+ </div>
313
+ );
314
+ })}
315
+ </div>
54
316
  </div>
55
317
  );
56
318
  }
57
- ChartTooltip.displayName = "ChartTooltip";
319
+ ChartTooltipContent.displayName = "ChartTooltipContent";
320
+
321
+ const ChartLegend = RechartsPrimitive.Legend;
322
+
323
+ type ChartLegendPayloadItem = {
324
+ color?: string;
325
+ dataKey?: string | number;
326
+ value?: string | number;
327
+ };
328
+
329
+ function ChartLegendContent({
330
+ className,
331
+ hideIcon = false,
332
+ payload,
333
+ verticalAlign = "bottom",
334
+ nameKey,
335
+ }: Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
336
+ hideIcon?: boolean;
337
+ nameKey?: string;
338
+ payload?: ChartLegendPayloadItem[];
339
+ verticalAlign?: "top" | "middle" | "bottom";
340
+ }) {
341
+ const { config } = useChart();
58
342
 
59
- export { ChartContainer, ChartTooltip };
343
+ if (!payload?.length) {
344
+ return null;
345
+ }
346
+
347
+ return (
348
+ <div
349
+ className={cn(
350
+ "flex items-center justify-center gap-4",
351
+ verticalAlign === "top" ? "pb-3" : "pt-3",
352
+ className,
353
+ )}
354
+ >
355
+ {payload.map((item: ChartLegendPayloadItem) => {
356
+ const key = `${nameKey || item.dataKey || "value"}`;
357
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
358
+
359
+ return (
360
+ <div
361
+ key={`${item.value}`}
362
+ className="flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
363
+ >
364
+ {itemConfig?.icon && !hideIcon ? (
365
+ <itemConfig.icon />
366
+ ) : (
367
+ <div
368
+ className="h-2 w-2 shrink-0 rounded-[2px]"
369
+ style={{
370
+ backgroundColor: item.color,
371
+ }}
372
+ />
373
+ )}
374
+ {itemConfig?.label}
375
+ </div>
376
+ );
377
+ })}
378
+ </div>
379
+ );
380
+ }
381
+ ChartLegendContent.displayName = "ChartLegendContent";
382
+
383
+ function getPayloadConfigFromPayload(
384
+ config: ChartConfig,
385
+ payload: unknown,
386
+ key: string,
387
+ ) {
388
+ if (typeof payload !== "object" || payload === null) {
389
+ return undefined;
390
+ }
391
+
392
+ const payloadPayload =
393
+ "payload" in payload &&
394
+ typeof payload.payload === "object" &&
395
+ payload.payload !== null
396
+ ? payload.payload
397
+ : undefined;
398
+
399
+ let configLabelKey: string = key;
400
+
401
+ if (
402
+ key in payload &&
403
+ typeof payload[key as keyof typeof payload] === "string"
404
+ ) {
405
+ configLabelKey = payload[key as keyof typeof payload] as string;
406
+ } else if (
407
+ payloadPayload &&
408
+ key in payloadPayload &&
409
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
410
+ ) {
411
+ configLabelKey = payloadPayload[
412
+ key as keyof typeof payloadPayload
413
+ ] as string;
414
+ }
415
+
416
+ return configLabelKey in config
417
+ ? config[configLabelKey]
418
+ : config[key as keyof typeof config];
419
+ }
420
+
421
+ export {
422
+ ChartContainer,
423
+ ChartTooltip,
424
+ ChartTooltipContent,
425
+ ChartLegend,
426
+ ChartLegendContent,
427
+ ChartStyle,
428
+ };
60
429
 
61
430
  export {
62
431
  BarChart,