omnira-ui 0.1.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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/cli/omnira-init.mjs +260 -0
  4. package/cli/presets.mjs +386 -0
  5. package/components/ui/AppStoreButton/AppStoreButton.module.css +87 -0
  6. package/components/ui/AppStoreButton/AppStoreButton.tsx +114 -0
  7. package/components/ui/AppStoreButton/index.ts +2 -0
  8. package/components/ui/Avatar/Avatar.module.css +76 -0
  9. package/components/ui/Avatar/Avatar.tsx +102 -0
  10. package/components/ui/Avatar/index.ts +2 -0
  11. package/components/ui/Badge/Badge.module.css +120 -0
  12. package/components/ui/Badge/Badge.tsx +25 -0
  13. package/components/ui/Badge/index.ts +2 -0
  14. package/components/ui/BadgeGroup/BadgeGroup.module.css +214 -0
  15. package/components/ui/BadgeGroup/BadgeGroup.tsx +88 -0
  16. package/components/ui/BadgeGroup/index.ts +2 -0
  17. package/components/ui/Browser/Browser.module.css +105 -0
  18. package/components/ui/Browser/Browser.tsx +54 -0
  19. package/components/ui/Browser/index.ts +2 -0
  20. package/components/ui/Button/Button.module.css +188 -0
  21. package/components/ui/Button/Button.tsx +74 -0
  22. package/components/ui/Button/index.ts +2 -0
  23. package/components/ui/ButtonUtility/ButtonUtility.module.css +154 -0
  24. package/components/ui/ButtonUtility/ButtonUtility.tsx +65 -0
  25. package/components/ui/ButtonUtility/index.ts +2 -0
  26. package/components/ui/Card/Card.module.css +80 -0
  27. package/components/ui/Card/Card.tsx +39 -0
  28. package/components/ui/Card/index.ts +2 -0
  29. package/components/ui/Checkbox/Checkbox.module.css +92 -0
  30. package/components/ui/Checkbox/Checkbox.tsx +95 -0
  31. package/components/ui/Checkbox/index.ts +2 -0
  32. package/components/ui/Collapse/Collapse.module.css +60 -0
  33. package/components/ui/Collapse/Collapse.tsx +100 -0
  34. package/components/ui/Collapse/index.ts +2 -0
  35. package/components/ui/CreditCard/CreditCard.module.css +117 -0
  36. package/components/ui/CreditCard/CreditCard.tsx +86 -0
  37. package/components/ui/CreditCard/index.ts +2 -0
  38. package/components/ui/Dropdown/Dropdown.module.css +269 -0
  39. package/components/ui/Dropdown/Dropdown.tsx +419 -0
  40. package/components/ui/Dropdown/index.ts +31 -0
  41. package/components/ui/GridLines/GridLines.module.css +95 -0
  42. package/components/ui/GridLines/GridLines.tsx +10 -0
  43. package/components/ui/GridLines/index.ts +1 -0
  44. package/components/ui/Illustration/Illustration.module.css +10 -0
  45. package/components/ui/Illustration/Illustration.tsx +96 -0
  46. package/components/ui/Illustration/index.ts +2 -0
  47. package/components/ui/Input/Input.module.css +217 -0
  48. package/components/ui/Input/Input.tsx +93 -0
  49. package/components/ui/Input/index.ts +2 -0
  50. package/components/ui/Phone/Phone.module.css +104 -0
  51. package/components/ui/Phone/Phone.tsx +54 -0
  52. package/components/ui/Phone/index.ts +2 -0
  53. package/components/ui/PinInput/PinInput.module.css +104 -0
  54. package/components/ui/PinInput/PinInput.tsx +210 -0
  55. package/components/ui/PinInput/index.ts +1 -0
  56. package/components/ui/ProgressBar/ProgressBar.module.css +180 -0
  57. package/components/ui/ProgressBar/ProgressBar.tsx +206 -0
  58. package/components/ui/ProgressBar/index.ts +2 -0
  59. package/components/ui/QRCode/QRCode.module.css +13 -0
  60. package/components/ui/QRCode/QRCode.tsx +128 -0
  61. package/components/ui/QRCode/index.ts +2 -0
  62. package/components/ui/RadioButton/RadioButton.module.css +92 -0
  63. package/components/ui/RadioButton/RadioButton.tsx +134 -0
  64. package/components/ui/RadioButton/index.ts +2 -0
  65. package/components/ui/RadioGroup/RadioGroup.module.css +382 -0
  66. package/components/ui/RadioGroup/RadioGroup.tsx +292 -0
  67. package/components/ui/RadioGroup/index.ts +1 -0
  68. package/components/ui/Rating/Rating.module.css +99 -0
  69. package/components/ui/Rating/Rating.tsx +123 -0
  70. package/components/ui/Rating/index.ts +2 -0
  71. package/components/ui/Select/Select.module.css +371 -0
  72. package/components/ui/Select/Select.tsx +507 -0
  73. package/components/ui/Select/index.ts +2 -0
  74. package/components/ui/SidebarNavigation/SidebarDual.tsx +71 -0
  75. package/components/ui/SidebarNavigation/SidebarFeatureCard.module.css +351 -0
  76. package/components/ui/SidebarNavigation/SidebarFeatureCard.tsx +388 -0
  77. package/components/ui/SidebarNavigation/SidebarNavigation.module.css +610 -0
  78. package/components/ui/SidebarNavigation/SidebarParts.tsx +215 -0
  79. package/components/ui/SidebarNavigation/SidebarSectionDividers.tsx +46 -0
  80. package/components/ui/SidebarNavigation/SidebarSectionHeadings.tsx +52 -0
  81. package/components/ui/SidebarNavigation/SidebarSimple.tsx +46 -0
  82. package/components/ui/SidebarNavigation/SidebarSlim.tsx +48 -0
  83. package/components/ui/SidebarNavigation/index.ts +29 -0
  84. package/components/ui/SidebarNavigation/types.ts +27 -0
  85. package/components/ui/Slider/Slider.module.css +93 -0
  86. package/components/ui/Slider/Slider.tsx +118 -0
  87. package/components/ui/Slider/index.ts +2 -0
  88. package/components/ui/SocialButton/SocialButton.module.css +223 -0
  89. package/components/ui/SocialButton/SocialButton.tsx +185 -0
  90. package/components/ui/SocialButton/index.ts +2 -0
  91. package/components/ui/Tag/Tag.module.css +203 -0
  92. package/components/ui/Tag/Tag.tsx +161 -0
  93. package/components/ui/Tag/index.ts +2 -0
  94. package/components/ui/TextEditor/TextEditor.module.css +182 -0
  95. package/components/ui/TextEditor/TextEditor.tsx +323 -0
  96. package/components/ui/TextEditor/index.ts +1 -0
  97. package/components/ui/Textarea/Textarea.module.css +75 -0
  98. package/components/ui/Textarea/Textarea.tsx +59 -0
  99. package/components/ui/Textarea/index.ts +2 -0
  100. package/components/ui/Toggle/Toggle.module.css +118 -0
  101. package/components/ui/Toggle/Toggle.tsx +77 -0
  102. package/components/ui/Toggle/index.ts +2 -0
  103. package/components/ui/Tooltip/Tooltip.module.css +95 -0
  104. package/components/ui/Tooltip/Tooltip.tsx +55 -0
  105. package/components/ui/Tooltip/index.ts +2 -0
  106. package/components/ui/VideoPlayer/VideoPlayer.module.css +120 -0
  107. package/components/ui/VideoPlayer/VideoPlayer.tsx +229 -0
  108. package/components/ui/VideoPlayer/index.ts +2 -0
  109. package/lib/cn.ts +3 -0
  110. package/lib/copy-to-clipboard.ts +21 -0
  111. package/package.json +61 -0
@@ -0,0 +1,104 @@
1
+ /* ── Root ── */
2
+ .root {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 6px;
6
+ width: fit-content;
7
+ }
8
+
9
+ /* ── Label ── */
10
+ .label {
11
+ font-size: 12px;
12
+ font-weight: 600;
13
+ text-transform: uppercase;
14
+ letter-spacing: 0.06em;
15
+ color: var(--color-text-tertiary);
16
+ }
17
+
18
+ /* ── Description ── */
19
+ .description {
20
+ font-size: 12px;
21
+ color: var(--color-text-tertiary);
22
+ }
23
+
24
+ /* ── Group ── */
25
+ .group {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 8px;
29
+ }
30
+
31
+ /* ── Separator ── */
32
+ .separator {
33
+ color: var(--color-text-tertiary);
34
+ user-select: none;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ flex-shrink: 0;
39
+ }
40
+
41
+ .separator_sm {
42
+ font-size: 14px;
43
+ width: 12px;
44
+ }
45
+
46
+ .separator_md {
47
+ font-size: 16px;
48
+ width: 14px;
49
+ }
50
+
51
+ .separator_lg {
52
+ font-size: 18px;
53
+ width: 16px;
54
+ }
55
+
56
+ /* ── Slot ── */
57
+ .slot {
58
+ text-align: center;
59
+ border-radius: var(--radius-md);
60
+ background: var(--color-bg-input);
61
+ border: 1px solid var(--color-border-standard);
62
+ font-family: inherit;
63
+ font-weight: 600;
64
+ color: var(--color-text-primary);
65
+ transition: all 0.2s;
66
+ outline: none;
67
+ caret-color: var(--color-lime);
68
+ }
69
+
70
+ .slot:focus {
71
+ border-color: var(--color-border-lime-strong);
72
+ background: var(--color-bg-elevated);
73
+ box-shadow: 0 0 0 3px var(--color-bg-lime-subtle);
74
+ }
75
+
76
+ .slot::placeholder {
77
+ color: var(--color-text-tertiary);
78
+ opacity: 0.4;
79
+ }
80
+
81
+ /* ── Slot sizes ── */
82
+ .slot_sm {
83
+ width: 36px;
84
+ height: 36px;
85
+ font-size: 14px;
86
+ }
87
+
88
+ .slot_md {
89
+ width: 48px;
90
+ height: 48px;
91
+ font-size: 18px;
92
+ }
93
+
94
+ .slot_lg {
95
+ width: 60px;
96
+ height: 60px;
97
+ font-size: 22px;
98
+ }
99
+
100
+ /* ── Disabled ── */
101
+ .slotDisabled {
102
+ opacity: 0.5;
103
+ cursor: not-allowed;
104
+ }
@@ -0,0 +1,210 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useRef,
7
+ useState,
8
+ useCallback,
9
+ forwardRef,
10
+ } from "react";
11
+ import { cn } from "@/lib/cn";
12
+ import styles from "./PinInput.module.css";
13
+
14
+ /* ── Context ── */
15
+ interface PinCtx {
16
+ size: "sm" | "md" | "lg";
17
+ disabled: boolean;
18
+ values: string[];
19
+ setValue: (index: number, value: string) => void;
20
+ focusSlot: (index: number) => void;
21
+ registerSlot: (index: number, el: HTMLInputElement | null) => void;
22
+ maxLength: number;
23
+ }
24
+
25
+ const Ctx = createContext<PinCtx>({
26
+ size: "md",
27
+ disabled: false,
28
+ values: [],
29
+ setValue: () => {},
30
+ focusSlot: () => {},
31
+ registerSlot: () => {},
32
+ maxLength: 6,
33
+ });
34
+
35
+ /* ── Root ── */
36
+ interface PinInputRootProps {
37
+ size?: "sm" | "md" | "lg";
38
+ disabled?: boolean;
39
+ children: React.ReactNode;
40
+ className?: string;
41
+ onComplete?: (code: string) => void;
42
+ }
43
+
44
+ function PinInputRoot({
45
+ size = "md",
46
+ disabled = false,
47
+ children,
48
+ className,
49
+ onComplete,
50
+ }: PinInputRootProps) {
51
+ const [values, setValues] = useState<string[]>([]);
52
+ const slotsRef = useRef<Map<number, HTMLInputElement>>(new Map());
53
+ const maxLengthRef = useRef(6);
54
+
55
+ const registerSlot = useCallback((index: number, el: HTMLInputElement | null) => {
56
+ if (el) slotsRef.current.set(index, el);
57
+ else slotsRef.current.delete(index);
58
+ }, []);
59
+
60
+ const focusSlot = useCallback((index: number) => {
61
+ const el = slotsRef.current.get(index);
62
+ if (el) {
63
+ el.focus();
64
+ el.select();
65
+ }
66
+ }, []);
67
+
68
+ const setValue = useCallback(
69
+ (index: number, value: string) => {
70
+ setValues((prev) => {
71
+ const next = [...prev];
72
+ next[index] = value;
73
+ const code = next.join("");
74
+ if (code.length === maxLengthRef.current && onComplete) {
75
+ setTimeout(() => onComplete(code), 0);
76
+ }
77
+ return next;
78
+ });
79
+ },
80
+ [onComplete],
81
+ );
82
+
83
+ return (
84
+ <Ctx.Provider
85
+ value={{
86
+ size,
87
+ disabled,
88
+ values,
89
+ setValue,
90
+ focusSlot,
91
+ registerSlot,
92
+ maxLength: maxLengthRef.current,
93
+ }}
94
+ >
95
+ <div className={cn(styles.root, className)}>{children}</div>
96
+ </Ctx.Provider>
97
+ );
98
+ }
99
+
100
+ /* ── Label ── */
101
+ function Label({ children, className }: { children: React.ReactNode; className?: string }) {
102
+ return <label className={cn(styles.label, className)}>{children}</label>;
103
+ }
104
+
105
+ /* ── Description ── */
106
+ function Description({ children, className }: { children: React.ReactNode; className?: string }) {
107
+ return <span className={cn(styles.description, className)}>{children}</span>;
108
+ }
109
+
110
+ /* ── Group ── */
111
+ interface GroupProps {
112
+ children: React.ReactNode;
113
+ maxLength?: number;
114
+ className?: string;
115
+ }
116
+
117
+ function Group({ children, maxLength = 6, className }: GroupProps) {
118
+ const ctx = useContext(Ctx);
119
+ (ctx as { maxLength: number }).maxLength = maxLength;
120
+ return <div className={cn(styles.group, className)}>{children}</div>;
121
+ }
122
+
123
+ /* ── Separator ── */
124
+ function Separator({ className }: { className?: string }) {
125
+ const { size } = useContext(Ctx);
126
+ return (
127
+ <span
128
+ className={cn(styles.separator, styles[`separator_${size}`], className)}
129
+ aria-hidden
130
+ >
131
+
132
+ </span>
133
+ );
134
+ }
135
+
136
+ /* ── Slot ── */
137
+ interface SlotProps {
138
+ index: number;
139
+ className?: string;
140
+ }
141
+
142
+ const Slot = forwardRef<HTMLInputElement, SlotProps>(({ index, className }, _ref) => {
143
+ const { size, disabled, values, setValue, focusSlot, registerSlot } = useContext(Ctx);
144
+
145
+ const handleRef = useCallback(
146
+ (el: HTMLInputElement | null) => {
147
+ registerSlot(index, el);
148
+ },
149
+ [index, registerSlot],
150
+ );
151
+
152
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
153
+ const raw = e.target.value.replace(/\D/g, "");
154
+ const char = raw.slice(-1);
155
+ setValue(index, char);
156
+ if (char) focusSlot(index + 1);
157
+ };
158
+
159
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
160
+ if (e.key === "Backspace") {
161
+ if (!values[index]) {
162
+ focusSlot(index - 1);
163
+ } else {
164
+ setValue(index, "");
165
+ }
166
+ } else if (e.key === "ArrowLeft") {
167
+ focusSlot(index - 1);
168
+ } else if (e.key === "ArrowRight") {
169
+ focusSlot(index + 1);
170
+ }
171
+ };
172
+
173
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
174
+ e.preventDefault();
175
+ const pasted = e.clipboardData.getData("text").replace(/\D/g, "");
176
+ for (let i = 0; i < pasted.length; i++) {
177
+ setValue(index + i, pasted[i]);
178
+ }
179
+ focusSlot(index + pasted.length - 1);
180
+ };
181
+
182
+ return (
183
+ <input
184
+ ref={handleRef}
185
+ type="text"
186
+ inputMode="numeric"
187
+ autoComplete="one-time-code"
188
+ maxLength={1}
189
+ value={values[index] || ""}
190
+ onChange={handleChange}
191
+ onKeyDown={handleKeyDown}
192
+ onPaste={handlePaste}
193
+ onFocus={(e) => e.target.select()}
194
+ disabled={disabled}
195
+ className={cn(styles.slot, styles[`slot_${size}`], disabled && styles.slotDisabled, className)}
196
+ aria-label={`Digit ${index + 1}`}
197
+ />
198
+ );
199
+ });
200
+
201
+ Slot.displayName = "PinInput.Slot";
202
+
203
+ /* ── Compound export ── */
204
+ export const PinInput = Object.assign(PinInputRoot, {
205
+ Label,
206
+ Description,
207
+ Group,
208
+ Separator,
209
+ Slot,
210
+ });
@@ -0,0 +1 @@
1
+ export { PinInput } from "./PinInput";
@@ -0,0 +1,180 @@
1
+ /* ══════════════════════════════════════
2
+ Linear Progress Bar
3
+ ══════════════════════════════════════ */
4
+
5
+ .wrapper {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 6px;
9
+ width: 100%;
10
+ }
11
+
12
+ .wrapper_right {
13
+ flex-direction: column;
14
+ }
15
+
16
+ .label {
17
+ font-size: 13px;
18
+ font-weight: 500;
19
+ color: var(--color-text-secondary);
20
+ margin-bottom: 2px;
21
+ }
22
+
23
+ /* ── Bar row ── */
24
+ .barRow {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 10px;
28
+ width: 100%;
29
+ }
30
+
31
+ /* ── Track ── */
32
+ .track {
33
+ flex: 1;
34
+ min-width: 0;
35
+ border-radius: 999px;
36
+ background: var(--color-border-medium);
37
+ overflow: hidden;
38
+ position: relative;
39
+ }
40
+
41
+ .track_sm { height: 6px; }
42
+ .track_md { height: 10px; }
43
+ .track_lg { height: 14px; }
44
+
45
+ /* ── Fill ── */
46
+ .fill {
47
+ height: 100%;
48
+ border-radius: 999px;
49
+ transition: width 0.4s ease;
50
+ }
51
+
52
+ .fill_lime { background: var(--color-lime); stroke: var(--color-lime); }
53
+ .fill_blue { background: #3b82f6; stroke: #3b82f6; }
54
+ .fill_red { background: #ef4444; stroke: #ef4444; }
55
+ .fill_orange { background: #f59e0b; stroke: #f59e0b; }
56
+
57
+ /* ── Text positions ── */
58
+ .textRight {
59
+ font-size: 13px;
60
+ font-weight: 600;
61
+ color: var(--color-text-primary);
62
+ white-space: nowrap;
63
+ min-width: 36px;
64
+ text-align: right;
65
+ }
66
+
67
+ .textBottom {
68
+ font-size: 12px;
69
+ font-weight: 500;
70
+ color: var(--color-text-secondary);
71
+ text-align: right;
72
+ }
73
+
74
+ /* ── Floating bubble ── */
75
+ .floatingWrap {
76
+ position: relative;
77
+ width: 100%;
78
+ height: 28px;
79
+ }
80
+
81
+ .floatingBubble {
82
+ position: absolute;
83
+ transform: translateX(-50%);
84
+ background: var(--color-bg-card);
85
+ border: 1px solid var(--color-border-standard);
86
+ border-radius: var(--radius-sm);
87
+ padding: 2px 8px;
88
+ font-size: 11px;
89
+ font-weight: 600;
90
+ color: var(--color-text-primary);
91
+ white-space: nowrap;
92
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
93
+ top: 0;
94
+ }
95
+
96
+ .floatingBubble::after {
97
+ content: "";
98
+ position: absolute;
99
+ left: 50%;
100
+ transform: translateX(-50%);
101
+ bottom: -5px;
102
+ border-left: 5px solid transparent;
103
+ border-right: 5px solid transparent;
104
+ border-top: 5px solid var(--color-border-standard);
105
+ }
106
+
107
+ .floatingBubbleBottom {
108
+ top: auto;
109
+ bottom: 0;
110
+ }
111
+
112
+ .floatingBubbleBottom::after {
113
+ bottom: auto;
114
+ top: -5px;
115
+ border-top: none;
116
+ border-bottom: 5px solid var(--color-border-standard);
117
+ }
118
+
119
+ /* ══════════════════════════════════════
120
+ Circle Progress Bar
121
+ ══════════════════════════════════════ */
122
+
123
+ .circleWrap {
124
+ position: relative;
125
+ display: inline-flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ }
129
+
130
+ .circleTrack {
131
+ stroke: var(--color-border-medium);
132
+ }
133
+
134
+ .circleFill {
135
+ transition: stroke-dashoffset 0.5s ease;
136
+ }
137
+
138
+ .circleText {
139
+ position: absolute;
140
+ inset: 0;
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: 1px;
146
+ }
147
+
148
+ .circlePct {
149
+ font-weight: 700;
150
+ color: var(--color-text-primary);
151
+ font-family: var(--font-display);
152
+ line-height: 1;
153
+ }
154
+
155
+ .circleLabel {
156
+ color: var(--color-text-tertiary);
157
+ font-weight: 500;
158
+ line-height: 1.2;
159
+ }
160
+
161
+ /* ══════════════════════════════════════
162
+ Half Circle Progress Bar
163
+ ══════════════════════════════════════ */
164
+
165
+ .halfWrap {
166
+ position: relative;
167
+ display: inline-flex;
168
+ align-items: flex-end;
169
+ justify-content: center;
170
+ }
171
+
172
+ .halfText {
173
+ position: absolute;
174
+ left: 0;
175
+ right: 0;
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ gap: 1px;
180
+ }
@@ -0,0 +1,206 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/cn";
4
+ import styles from "./ProgressBar.module.css";
5
+
6
+ /* ── Linear Progress Bar ── */
7
+ export interface ProgressBarProps {
8
+ min?: number;
9
+ max?: number;
10
+ value: number;
11
+ label?: string;
12
+ /** Where to display the percentage text */
13
+ textPosition?: "right" | "bottom" | "top-floating" | "bottom-floating" | "none";
14
+ size?: "sm" | "md" | "lg";
15
+ color?: "lime" | "blue" | "red" | "orange";
16
+ className?: string;
17
+ }
18
+
19
+ export function ProgressBar({
20
+ min = 0,
21
+ max = 100,
22
+ value,
23
+ label,
24
+ textPosition = "right",
25
+ size = "md",
26
+ color = "lime",
27
+ className,
28
+ }: ProgressBarProps) {
29
+ const pct = Math.round(((value - min) / (max - min)) * 100);
30
+ const clampedPct = Math.max(0, Math.min(100, pct));
31
+
32
+ return (
33
+ <div className={cn(styles.wrapper, styles[`wrapper_${textPosition}`], className)}>
34
+ {label && <span className={styles.label}>{label}</span>}
35
+
36
+ {textPosition === "top-floating" && (
37
+ <div className={styles.floatingWrap}>
38
+ <div className={styles.floatingBubble} style={{ left: `${clampedPct}%` }}>
39
+ {clampedPct}%
40
+ </div>
41
+ </div>
42
+ )}
43
+
44
+ <div className={styles.barRow}>
45
+ <div
46
+ className={cn(styles.track, styles[`track_${size}`])}
47
+ role="progressbar"
48
+ aria-valuenow={value}
49
+ aria-valuemin={min}
50
+ aria-valuemax={max}
51
+ >
52
+ <div
53
+ className={cn(styles.fill, styles[`fill_${color}`])}
54
+ style={{ width: `${clampedPct}%` }}
55
+ />
56
+ </div>
57
+ {textPosition === "right" && (
58
+ <span className={styles.textRight}>{clampedPct}%</span>
59
+ )}
60
+ </div>
61
+
62
+ {textPosition === "bottom" && (
63
+ <span className={styles.textBottom}>{clampedPct}%</span>
64
+ )}
65
+
66
+ {textPosition === "bottom-floating" && (
67
+ <div className={styles.floatingWrap}>
68
+ <div className={cn(styles.floatingBubble, styles.floatingBubbleBottom)} style={{ left: `${clampedPct}%` }}>
69
+ {clampedPct}%
70
+ </div>
71
+ </div>
72
+ )}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ /* ── Circle Progress Bar ── */
78
+ export interface ProgressBarCircleProps {
79
+ min?: number;
80
+ max?: number;
81
+ value: number;
82
+ label?: string;
83
+ size?: "xxs" | "xs" | "sm" | "md" | "lg";
84
+ color?: "lime" | "blue" | "red" | "orange";
85
+ className?: string;
86
+ }
87
+
88
+ const circleSizes: Record<string, { dim: number; stroke: number; fontSize: number }> = {
89
+ xxs: { dim: 48, stroke: 4, fontSize: 11 },
90
+ xs: { dim: 64, stroke: 5, fontSize: 13 },
91
+ sm: { dim: 96, stroke: 6, fontSize: 16 },
92
+ md: { dim: 128, stroke: 8, fontSize: 20 },
93
+ lg: { dim: 160, stroke: 10, fontSize: 24 },
94
+ };
95
+
96
+ export function ProgressBarCircle({
97
+ min = 0,
98
+ max = 100,
99
+ value,
100
+ label,
101
+ size = "sm",
102
+ color = "lime",
103
+ className,
104
+ }: ProgressBarCircleProps) {
105
+ const pct = Math.round(((value - min) / (max - min)) * 100);
106
+ const clampedPct = Math.max(0, Math.min(100, pct));
107
+ const cfg = circleSizes[size];
108
+ const r = (cfg.dim - cfg.stroke) / 2;
109
+ const circ = 2 * Math.PI * r;
110
+ const offset = circ - (clampedPct / 100) * circ;
111
+
112
+ return (
113
+ <div className={cn(styles.circleWrap, className)} style={{ width: cfg.dim, height: cfg.dim }}>
114
+ <svg width={cfg.dim} height={cfg.dim} viewBox={`0 0 ${cfg.dim} ${cfg.dim}`}>
115
+ <circle
116
+ cx={cfg.dim / 2}
117
+ cy={cfg.dim / 2}
118
+ r={r}
119
+ fill="none"
120
+ className={styles.circleTrack}
121
+ strokeWidth={cfg.stroke}
122
+ strokeLinecap="round"
123
+ />
124
+ <circle
125
+ cx={cfg.dim / 2}
126
+ cy={cfg.dim / 2}
127
+ r={r}
128
+ fill="none"
129
+ className={cn(styles.circleFill, styles[`fill_${color}`])}
130
+ strokeWidth={cfg.stroke}
131
+ strokeDasharray={circ}
132
+ strokeDashoffset={offset}
133
+ strokeLinecap="round"
134
+ transform={`rotate(-90 ${cfg.dim / 2} ${cfg.dim / 2})`}
135
+ />
136
+ </svg>
137
+ <div className={styles.circleText} style={{ fontSize: cfg.fontSize }}>
138
+ <span className={styles.circlePct}>{clampedPct}%</span>
139
+ {label && <span className={styles.circleLabel} style={{ fontSize: Math.max(9, cfg.fontSize - 4) }}>{label}</span>}
140
+ </div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ /* ── Half Circle Progress Bar ── */
146
+ export interface ProgressBarHalfCircleProps {
147
+ min?: number;
148
+ max?: number;
149
+ value: number;
150
+ label?: string;
151
+ size?: "xxs" | "xs" | "sm" | "md" | "lg";
152
+ color?: "lime" | "blue" | "red" | "orange";
153
+ className?: string;
154
+ }
155
+
156
+ const halfSizes: Record<string, { w: number; h: number; stroke: number; fontSize: number }> = {
157
+ xxs: { w: 64, h: 36, stroke: 4, fontSize: 11 },
158
+ xs: { w: 88, h: 48, stroke: 5, fontSize: 13 },
159
+ sm: { w: 128, h: 68, stroke: 6, fontSize: 16 },
160
+ md: { w: 168, h: 88, stroke: 8, fontSize: 20 },
161
+ lg: { w: 208, h: 108, stroke: 10, fontSize: 24 },
162
+ };
163
+
164
+ export function ProgressBarHalfCircle({
165
+ min = 0,
166
+ max = 100,
167
+ value,
168
+ label,
169
+ size = "sm",
170
+ color = "lime",
171
+ className,
172
+ }: ProgressBarHalfCircleProps) {
173
+ const pct = Math.round(((value - min) / (max - min)) * 100);
174
+ const clampedPct = Math.max(0, Math.min(100, pct));
175
+ const cfg = halfSizes[size];
176
+ const r = (cfg.w - cfg.stroke) / 2;
177
+ const halfCirc = Math.PI * r;
178
+ const offset = halfCirc - (clampedPct / 100) * halfCirc;
179
+
180
+ return (
181
+ <div className={cn(styles.halfWrap, className)} style={{ width: cfg.w, height: cfg.h }}>
182
+ <svg width={cfg.w} height={cfg.h} viewBox={`0 0 ${cfg.w} ${cfg.h}`} overflow="visible">
183
+ <path
184
+ d={`M ${cfg.stroke / 2} ${cfg.h} A ${r} ${r} 0 0 1 ${cfg.w - cfg.stroke / 2} ${cfg.h}`}
185
+ fill="none"
186
+ className={styles.circleTrack}
187
+ strokeWidth={cfg.stroke}
188
+ strokeLinecap="round"
189
+ />
190
+ <path
191
+ d={`M ${cfg.stroke / 2} ${cfg.h} A ${r} ${r} 0 0 1 ${cfg.w - cfg.stroke / 2} ${cfg.h}`}
192
+ fill="none"
193
+ className={cn(styles.circleFill, styles[`fill_${color}`])}
194
+ strokeWidth={cfg.stroke}
195
+ strokeDasharray={halfCirc}
196
+ strokeDashoffset={offset}
197
+ strokeLinecap="round"
198
+ />
199
+ </svg>
200
+ <div className={styles.halfText} style={{ bottom: 0 }}>
201
+ <span className={styles.circlePct} style={{ fontSize: cfg.fontSize }}>{clampedPct}%</span>
202
+ {label && <span className={styles.circleLabel} style={{ fontSize: Math.max(9, cfg.fontSize - 4) }}>{label}</span>}
203
+ </div>
204
+ </div>
205
+ );
206
+ }
@@ -0,0 +1,2 @@
1
+ export { ProgressBar, ProgressBarCircle, ProgressBarHalfCircle } from "./ProgressBar";
2
+ export type { ProgressBarProps, ProgressBarCircleProps, ProgressBarHalfCircleProps } from "./ProgressBar";