@hilum/ui 3.5.1 → 3.5.2

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/llms.txt CHANGED
@@ -5,10 +5,10 @@
5
5
  ## Key Facts
6
6
 
7
7
  - Package name: `@hilum/ui`
8
- - Package version: 3.4.0
9
- - Component modules: 99
8
+ - Package version: 3.5.2
9
+ - Component modules: 100
10
10
  - Atom components: 70
11
- - Molecule components: 29
11
+ - Molecule components: 30
12
12
  - Docs site: https://ui.hilum.dev
13
13
  - GitHub repository: https://github.com/hilum-labs/hilum-ui
14
14
  - Machine-readable docs index: https://ui.hilum.dev/catalog.json
@@ -153,6 +153,7 @@ Primary exports:
153
153
  - Spinner (/atoms/spinner/) — Animated loading indicator for async operations.
154
154
  - Stacked List (/molecules/stacked-list/) — Vertically stacked rows with consistent padding and optional hover behavior. Composes: Badge · Avatar.
155
155
  - Stat Card (/molecules/stat-card/) — Metric display card with label, value, optional trend, and optional icon. Composes: Card · Badge · Icon.
156
+ - Usage Bar (/molecules/usage-bar/) — Compact quota, readiness, and capacity meter with inline and stacked layouts. Composes: Progress.
156
157
  - Status Badge (/atoms/status-badge/) — Semantic badge that maps status values to labels, variants, icons, and dots.
157
158
  - Steps (/atoms/steps/) — Multi-step progress indicator with circles, bullets, and progress variants.
158
159
  - Switch (/atoms/switch/) — Toggle between two states, on and off.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilum/ui",
3
- "version": "3.5.1",
3
+ "version": "3.5.2",
4
4
  "private": false,
5
5
  "description": "Hilum UI — design system primitives (Button, Input, Dialog, Combobox, …) + tokens + fonts + icons",
6
6
  "license": "MIT",
@@ -0,0 +1,105 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ type UsageBarTone = "primary" | "warning" | "destructive" | "success" | "muted";
5
+ type UsageBarLayout = "stacked" | "inline";
6
+
7
+ interface UsageBarProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ label?: React.ReactNode;
9
+ value?: number;
10
+ max?: number;
11
+ percent?: number;
12
+ valueLabel?: React.ReactNode;
13
+ showPercent?: boolean;
14
+ layout?: UsageBarLayout;
15
+ tone?: UsageBarTone;
16
+ barClassName?: string;
17
+ indicatorClassName?: string;
18
+ }
19
+
20
+ const toneClassName: Record<UsageBarTone, string> = {
21
+ primary: "bg-primary",
22
+ warning: "bg-warning",
23
+ destructive: "bg-destructive",
24
+ success: "bg-success",
25
+ muted: "bg-muted-foreground",
26
+ };
27
+
28
+ function clampPercent(value: number) {
29
+ if (!Number.isFinite(value)) return 0;
30
+ return Math.min(Math.max(value, 0), 100);
31
+ }
32
+
33
+ function inferTone(percent: number): UsageBarTone {
34
+ if (percent >= 90) return "destructive";
35
+ if (percent >= 75) return "warning";
36
+ return "primary";
37
+ }
38
+
39
+ function formatUsageValue(value: number | undefined, max: number | undefined) {
40
+ if (typeof value !== "number" || typeof max !== "number") return undefined;
41
+ return `${value.toLocaleString()} / ${max.toLocaleString()}`;
42
+ }
43
+
44
+ function UsageBar({
45
+ label,
46
+ value,
47
+ max,
48
+ percent,
49
+ valueLabel,
50
+ showPercent,
51
+ layout = "stacked",
52
+ tone,
53
+ className,
54
+ barClassName,
55
+ indicatorClassName,
56
+ ...props
57
+ }: UsageBarProps) {
58
+ const resolvedPercent = clampPercent(
59
+ typeof percent === "number" ? percent : max && max > 0 && typeof value === "number" ? (value / max) * 100 : 0,
60
+ );
61
+ const resolvedTone = tone ?? inferTone(resolvedPercent);
62
+ const resolvedValueLabel = valueLabel ?? formatUsageValue(value, max);
63
+ const percentLabel = `${Math.round(resolvedPercent)}%`;
64
+
65
+ const bar = (
66
+ <div
67
+ className={cn("h-2 w-full overflow-hidden rounded-full bg-muted", layout === "inline" && "w-28", barClassName)}
68
+ aria-hidden="true"
69
+ >
70
+ <div
71
+ className={cn("h-full rounded-full transition-all", toneClassName[resolvedTone], indicatorClassName)}
72
+ style={{ width: `${resolvedPercent}%` }}
73
+ />
74
+ </div>
75
+ );
76
+
77
+ if (layout === "inline") {
78
+ return (
79
+ <div className={cn("flex min-w-0 items-center gap-2", className)} {...props}>
80
+ {label ? <span className="caption shrink-0 text-muted-foreground">{label}</span> : null}
81
+ {bar}
82
+ {showPercent !== false ? (
83
+ <span className="w-10 shrink-0 text-xs tabular-nums text-muted-foreground">{percentLabel}</span>
84
+ ) : null}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <div className={cn("space-y-1.5", className)} {...props}>
91
+ {(label || resolvedValueLabel || showPercent) && (
92
+ <div className="flex min-w-0 items-center justify-between gap-3 text-sm">
93
+ {label ? <span className="min-w-0 truncate text-muted-foreground">{label}</span> : <span />}
94
+ <span className="shrink-0 font-medium tabular-nums text-foreground">
95
+ {resolvedValueLabel ?? (showPercent ? percentLabel : null)}
96
+ </span>
97
+ </div>
98
+ )}
99
+ {bar}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ export { UsageBar };
105
+ export type { UsageBarProps, UsageBarLayout, UsageBarTone };
package/src/index.ts CHANGED
@@ -108,3 +108,4 @@ export * from "./components/toggle-group";
108
108
  export * from "./components/toggle";
109
109
  export * from "./components/tooltip";
110
110
  export * from "./components/url-redirect-prompt";
111
+ export * from "./components/usage-bar";