@payglocal_ui/flux-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 (72) hide show
  1. package/package.json +72 -0
  2. package/src/accordion.tsx +68 -0
  3. package/src/alert.tsx +107 -0
  4. package/src/avatar-group.tsx +96 -0
  5. package/src/avatar-tag.tsx +136 -0
  6. package/src/avatar.tsx +39 -0
  7. package/src/badge.tsx +98 -0
  8. package/src/blanket.tsx +61 -0
  9. package/src/breadcrumb.tsx +119 -0
  10. package/src/button-group.tsx +218 -0
  11. package/src/button.tsx +83 -0
  12. package/src/calendar.tsx +227 -0
  13. package/src/callout.tsx +68 -0
  14. package/src/card.tsx +103 -0
  15. package/src/chart-templates.tsx +587 -0
  16. package/src/chart.tsx +379 -0
  17. package/src/checkbox-select.tsx +239 -0
  18. package/src/checkbox.tsx +54 -0
  19. package/src/code.tsx +154 -0
  20. package/src/command.tsx +77 -0
  21. package/src/country-select.tsx +242 -0
  22. package/src/currency-amount-input.tsx +72 -0
  23. package/src/data-table.tsx +378 -0
  24. package/src/date-picker.tsx +317 -0
  25. package/src/dialog.tsx +81 -0
  26. package/src/drawer.tsx +91 -0
  27. package/src/dropdown-menu.tsx +174 -0
  28. package/src/empty-state.tsx +32 -0
  29. package/src/field.tsx +243 -0
  30. package/src/flag.tsx +265 -0
  31. package/src/form.tsx +168 -0
  32. package/src/grid-flex.tsx +241 -0
  33. package/src/heading.tsx +202 -0
  34. package/src/icon-button.tsx +93 -0
  35. package/src/index.ts +332 -0
  36. package/src/inline-dialog.tsx +153 -0
  37. package/src/inline-edit.tsx +212 -0
  38. package/src/input-group.tsx +151 -0
  39. package/src/input.tsx +28 -0
  40. package/src/label.tsx +21 -0
  41. package/src/layout.tsx +119 -0
  42. package/src/link.tsx +80 -0
  43. package/src/lozenge.tsx +61 -0
  44. package/src/menu.tsx +146 -0
  45. package/src/otp-input.tsx +117 -0
  46. package/src/page-header.tsx +28 -0
  47. package/src/pagination.tsx +185 -0
  48. package/src/password-input.tsx +34 -0
  49. package/src/popover.tsx +31 -0
  50. package/src/progress-indicator.tsx +94 -0
  51. package/src/progress.tsx +95 -0
  52. package/src/radio-group.tsx +46 -0
  53. package/src/responsive.tsx +276 -0
  54. package/src/scroll-area.tsx +39 -0
  55. package/src/section-message.tsx +119 -0
  56. package/src/select.tsx +144 -0
  57. package/src/separator.tsx +26 -0
  58. package/src/side-nav.tsx +264 -0
  59. package/src/skeleton.tsx +74 -0
  60. package/src/slider.tsx +25 -0
  61. package/src/sonner.tsx +32 -0
  62. package/src/spinner.tsx +54 -0
  63. package/src/spotlight.tsx +141 -0
  64. package/src/status-badge.tsx +86 -0
  65. package/src/switch.tsx +70 -0
  66. package/src/tabs.tsx +57 -0
  67. package/src/tag.tsx +52 -0
  68. package/src/textarea.tsx +25 -0
  69. package/src/time-picker.tsx +443 -0
  70. package/src/tooltip.tsx +29 -0
  71. package/src/utils.ts +6 -0
  72. package/src/visually-hidden.tsx +25 -0
@@ -0,0 +1,443 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { Clock, ChevronDown, X } from "lucide-react";
6
+ import { cn } from "./utils";
7
+
8
+ /* ─── Types ──────────────────────────────────────────────────────────────── */
9
+
10
+ export interface TimePickerProps {
11
+ /** Controlled value in 24-hour "HH:MM" format. Pass "" for no selection. */
12
+ value: string;
13
+ /** Called with a new "HH:MM" string, or "" when cleared. */
14
+ onValueChange: (value: string) => void;
15
+ /** Show 12-hour (AM/PM) columns instead of 24-hour. Default: false. */
16
+ use24Hour?: boolean;
17
+ placeholder?: string;
18
+ label?: string;
19
+ disabled?: boolean;
20
+ className?: string;
21
+ }
22
+
23
+ /* ─── Helpers ────────────────────────────────────────────────────────────── */
24
+
25
+ function pad(n: number) {
26
+ return String(n).padStart(2, "0");
27
+ }
28
+
29
+ function parseHHMM(v: string): { h: number; m: number } | null {
30
+ if (!v) return null;
31
+ const parts = v.split(":");
32
+ if (parts.length < 2) return null;
33
+ const h = Number(parts[0]);
34
+ const m = Number(parts[1]);
35
+ if (isNaN(h) || isNaN(m)) return null;
36
+ return { h, m };
37
+ }
38
+
39
+ function toHHMM(h: number, m: number) {
40
+ return `${pad(h)}:${pad(m)}`;
41
+ }
42
+
43
+ function displayTime(value: string, use24Hour: boolean) {
44
+ const p = parseHHMM(value);
45
+ if (!p) return "";
46
+ if (use24Hour) return `${pad(p.h)}:${pad(p.m)}`;
47
+ const period = p.h < 12 ? "AM" : "PM";
48
+ const displayH = p.h % 12 === 0 ? 12 : p.h % 12;
49
+ return `${pad(displayH)}:${pad(p.m)} ${period}`;
50
+ }
51
+
52
+ /* ─── ScrollColumn ───────────────────────────────────────────────────────── */
53
+
54
+ const ITEM_H = 36; // px
55
+ const VISIBLE = 5; // items visible at once
56
+ const PAD = 2; // invisible padding rows above/below
57
+
58
+ interface ScrollColumnProps<T> {
59
+ items: T[];
60
+ selected: T;
61
+ onSelect: (item: T) => void;
62
+ renderItem: (item: T) => React.ReactNode;
63
+ getKey: (item: T) => string | number;
64
+ }
65
+
66
+ function ScrollColumn<T>({
67
+ items,
68
+ selected,
69
+ onSelect,
70
+ renderItem,
71
+ getKey,
72
+ }: ScrollColumnProps<T>) {
73
+ const containerRef = React.useRef<HTMLDivElement>(null);
74
+ const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
75
+ const programmaticRef = React.useRef(false);
76
+
77
+ const selectedIndex = items.findIndex((item) => getKey(item) === getKey(selected));
78
+
79
+ /* Scroll to selected item */
80
+ const scrollToIndex = React.useCallback(
81
+ (index: number, behavior: ScrollBehavior = "smooth") => {
82
+ const el = containerRef.current;
83
+ if (!el) return;
84
+ programmaticRef.current = true;
85
+ el.scrollTo({ top: index * ITEM_H, behavior });
86
+ if (behavior === "smooth") {
87
+ // Release flag after animation
88
+ setTimeout(() => {
89
+ programmaticRef.current = false;
90
+ }, 350);
91
+ } else {
92
+ programmaticRef.current = false;
93
+ }
94
+ },
95
+ []
96
+ );
97
+
98
+ /* Initial scroll (instant) */
99
+ React.useEffect(() => {
100
+ if (selectedIndex >= 0) scrollToIndex(selectedIndex, "instant");
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, []);
103
+
104
+ /* Scroll when selected changes from outside */
105
+ React.useEffect(() => {
106
+ if (selectedIndex >= 0) scrollToIndex(selectedIndex, "smooth");
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ }, [selectedIndex]);
109
+
110
+ /* Snap on scroll end */
111
+ function handleScroll() {
112
+ if (programmaticRef.current) return;
113
+ if (debounceRef.current) clearTimeout(debounceRef.current);
114
+ debounceRef.current = setTimeout(() => {
115
+ const el = containerRef.current;
116
+ if (!el) return;
117
+ const index = Math.round(el.scrollTop / ITEM_H);
118
+ const clamped = Math.max(0, Math.min(index, items.length - 1));
119
+ onSelect(items[clamped]);
120
+ scrollToIndex(clamped, "smooth");
121
+ }, 80);
122
+ }
123
+
124
+ return (
125
+ <div className="relative flex flex-col items-center" style={{ width: 64 }}>
126
+ {/* Highlight band */}
127
+ <div
128
+ className="pointer-events-none absolute left-0 right-0 rounded-lg bg-primary/10"
129
+ style={{
130
+ top: PAD * ITEM_H,
131
+ height: ITEM_H,
132
+ zIndex: 1,
133
+ }}
134
+ />
135
+
136
+ {/* Scroll container */}
137
+ <div
138
+ ref={containerRef}
139
+ onScroll={handleScroll}
140
+ className="relative z-10 overflow-y-scroll"
141
+ style={{
142
+ height: VISIBLE * ITEM_H,
143
+ scrollSnapType: "y mandatory",
144
+ scrollbarWidth: "none",
145
+ msOverflowStyle: "none",
146
+ }}
147
+ >
148
+ {/* Top padding rows */}
149
+ {Array.from({ length: PAD }).map((_, i) => (
150
+ <div key={`pad-top-${i}`} style={{ height: ITEM_H }} />
151
+ ))}
152
+
153
+ {items.map((item) => {
154
+ const isSelected = getKey(item) === getKey(selected);
155
+ return (
156
+ <div
157
+ key={getKey(item)}
158
+ onClick={() => {
159
+ onSelect(item);
160
+ scrollToIndex(items.findIndex((it) => getKey(it) === getKey(item)), "smooth");
161
+ }}
162
+ className={cn(
163
+ "flex cursor-pointer items-center justify-center text-[14px] font-medium transition-colors duration-150 select-none",
164
+ isSelected
165
+ ? "text-primary font-semibold"
166
+ : "text-muted-foreground hover:text-foreground"
167
+ )}
168
+ style={{
169
+ height: ITEM_H,
170
+ scrollSnapAlign: "start",
171
+ }}
172
+ >
173
+ {renderItem(item)}
174
+ </div>
175
+ );
176
+ })}
177
+
178
+ {/* Bottom padding rows */}
179
+ {Array.from({ length: PAD }).map((_, i) => (
180
+ <div key={`pad-bot-${i}`} style={{ height: ITEM_H }} />
181
+ ))}
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+
187
+ /* ─── TimePicker ─────────────────────────────────────────────────────────── */
188
+
189
+ const PANEL_W = 224;
190
+ const PANEL_H = 220;
191
+
192
+ export const TimePicker = React.forwardRef<HTMLButtonElement, TimePickerProps>(
193
+ (
194
+ {
195
+ value,
196
+ onValueChange,
197
+ use24Hour = false,
198
+ placeholder = "Select time",
199
+ label,
200
+ disabled = false,
201
+ className,
202
+ },
203
+ ref
204
+ ) => {
205
+ const parsed = parseHHMM(value);
206
+
207
+ /* ── Local column state (derived from value, kept in sync) ── */
208
+ const [hour, setHour] = React.useState<number>(() => {
209
+ if (!parsed) return use24Hour ? 0 : 12;
210
+ if (use24Hour) return parsed.h;
211
+ return parsed.h % 12 === 0 ? 12 : parsed.h % 12;
212
+ });
213
+ const [minute, setMinute] = React.useState<number>(() => parsed?.m ?? 0);
214
+ const [period, setPeriod] = React.useState<"AM" | "PM">(() => {
215
+ if (!parsed) return "AM";
216
+ return parsed.h < 12 ? "AM" : "PM";
217
+ });
218
+
219
+ /* Sync columns when value prop changes */
220
+ React.useEffect(() => {
221
+ const p = parseHHMM(value);
222
+ if (p) {
223
+ setMinute(p.m);
224
+ if (use24Hour) {
225
+ setHour(p.h);
226
+ } else {
227
+ setHour(p.h % 12 === 0 ? 12 : p.h % 12);
228
+ setPeriod(p.h < 12 ? "AM" : "PM");
229
+ }
230
+ }
231
+ // eslint-disable-next-line react-hooks/exhaustive-deps
232
+ }, [value]);
233
+
234
+ /* ── Panel open/position ── */
235
+ const [open, setOpen] = React.useState(false);
236
+ const [panelPos, setPanelPos] = React.useState({ top: 0, left: 0 });
237
+ const [mounted, setMounted] = React.useState(false);
238
+
239
+ const triggerRef = React.useRef<HTMLButtonElement>(null);
240
+ const panelRef = React.useRef<HTMLDivElement>(null);
241
+
242
+ React.useEffect(() => { setMounted(true); }, []);
243
+
244
+ function openPanel() {
245
+ if (disabled) return;
246
+ const el = triggerRef.current;
247
+ if (!el) return;
248
+ el.scrollIntoView({ block: "nearest", behavior: "auto" });
249
+ requestAnimationFrame(() => {
250
+ if (!triggerRef.current) return;
251
+ const rect = triggerRef.current.getBoundingClientRect();
252
+ const vw = window.innerWidth;
253
+ const vh = window.innerHeight;
254
+ let left = rect.left;
255
+ if (left + PANEL_W > vw - 8) left = vw - PANEL_W - 8;
256
+ let top = rect.bottom + 6;
257
+ if (top + PANEL_H > vh - 8) top = rect.top - PANEL_H - 6;
258
+ setPanelPos({ top, left });
259
+ setOpen(true);
260
+ });
261
+ }
262
+
263
+ /* Close on outside click */
264
+ React.useEffect(() => {
265
+ if (!open) return;
266
+ function handler(e: MouseEvent) {
267
+ const inTrigger = triggerRef.current?.contains(e.target as Node);
268
+ const inPanel = panelRef.current?.contains(e.target as Node);
269
+ if (!inTrigger && !inPanel) setOpen(false);
270
+ }
271
+ document.addEventListener("mousedown", handler);
272
+ return () => document.removeEventListener("mousedown", handler);
273
+ }, [open]);
274
+
275
+ /* ── Emit value when columns change ── */
276
+ function emitChange(h: number, m: number, p: "AM" | "PM") {
277
+ let h24 = h;
278
+ if (!use24Hour) {
279
+ if (p === "AM") h24 = h === 12 ? 0 : h;
280
+ else h24 = h === 12 ? 12 : h + 12;
281
+ }
282
+ onValueChange(toHHMM(h24, m));
283
+ }
284
+
285
+ function handleHourChange(h: number) {
286
+ setHour(h);
287
+ emitChange(h, minute, period);
288
+ }
289
+ function handleMinuteChange(m: number) {
290
+ setMinute(m);
291
+ emitChange(hour, m, period);
292
+ }
293
+ function handlePeriodChange(p: "AM" | "PM") {
294
+ setPeriod(p);
295
+ emitChange(hour, minute, p);
296
+ }
297
+
298
+ /* ── Column data ── */
299
+ const hours = use24Hour
300
+ ? Array.from({ length: 24 }, (_, i) => i)
301
+ : Array.from({ length: 12 }, (_, i) => i + 1);
302
+
303
+ const minutes = Array.from({ length: 60 }, (_, i) => i);
304
+
305
+ /* ── Clear ── */
306
+ function handleClear(e: React.MouseEvent) {
307
+ e.stopPropagation();
308
+ onValueChange("");
309
+ }
310
+
311
+ /* ── Panel ── */
312
+ const panel = open ? (
313
+ <div
314
+ ref={panelRef}
315
+ className={cn(
316
+ "isolate rounded-xl border border-border bg-popover text-popover-foreground shadow-lg",
317
+ "flex flex-col gap-0"
318
+ )}
319
+ style={{
320
+ position: "fixed",
321
+ top: panelPos.top,
322
+ left: panelPos.left,
323
+ width: PANEL_W,
324
+ zIndex: 20000,
325
+ }}
326
+ >
327
+ {/* Header */}
328
+ <div className="flex items-center gap-1.5 border-b border-border px-4 py-2.5">
329
+ <Clock className="size-3.5 text-muted-foreground" />
330
+ <span className="text-[13px] font-medium text-muted-foreground">
331
+ {value ? displayTime(value, use24Hour) : "—"}
332
+ </span>
333
+ </div>
334
+
335
+ {/* Column labels */}
336
+ <div
337
+ className="flex items-center justify-around px-2 pt-2 pb-0.5"
338
+ style={{ paddingLeft: 8, paddingRight: use24Hour ? 8 : 8 }}
339
+ >
340
+ <span className="w-16 text-center text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
341
+ Hr
342
+ </span>
343
+ <span className="w-16 text-center text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
344
+ Min
345
+ </span>
346
+ {!use24Hour && (
347
+ <span className="w-16 text-center text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
348
+ AM/PM
349
+ </span>
350
+ )}
351
+ </div>
352
+
353
+ {/* Columns */}
354
+ <div className="flex items-center justify-around px-2 pb-3">
355
+ <ScrollColumn
356
+ items={hours}
357
+ selected={hour}
358
+ onSelect={handleHourChange}
359
+ renderItem={(h) => pad(h)}
360
+ getKey={(h) => h}
361
+ />
362
+ <div className="text-[18px] font-light text-muted-foreground/50 pb-0.5">:</div>
363
+ <ScrollColumn
364
+ items={minutes}
365
+ selected={minute}
366
+ onSelect={handleMinuteChange}
367
+ renderItem={(m) => pad(m)}
368
+ getKey={(m) => m}
369
+ />
370
+ {!use24Hour && (
371
+ <>
372
+ <div className="w-px self-stretch bg-border mx-1" />
373
+ <ScrollColumn
374
+ items={["AM", "PM"] as const}
375
+ selected={period}
376
+ onSelect={handlePeriodChange}
377
+ renderItem={(p) => p}
378
+ getKey={(p) => p}
379
+ />
380
+ </>
381
+ )}
382
+ </div>
383
+ </div>
384
+ ) : null;
385
+
386
+ /* ── Merge refs ── */
387
+ function mergeRef(el: HTMLButtonElement | null) {
388
+ (triggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = el;
389
+ if (typeof ref === "function") ref(el);
390
+ else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = el;
391
+ }
392
+
393
+ return (
394
+ <div className={cn("relative", className)}>
395
+ {label && (
396
+ <p className="mb-1.5 text-sm font-medium text-foreground">{label}</p>
397
+ )}
398
+
399
+ <button
400
+ ref={mergeRef}
401
+ type="button"
402
+ disabled={disabled}
403
+ onClick={() => (open ? setOpen(false) : openPanel())}
404
+ className={cn(
405
+ "flex h-11 min-h-11 w-full items-center gap-3 rounded-lg border border-border bg-card px-4 text-left text-[15px] shadow-sm",
406
+ "transition-colors duration-150",
407
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
408
+ open && "border-ring ring-2 ring-ring/35",
409
+ !open && !disabled && "hover:border-muted-foreground/45",
410
+ disabled && "cursor-not-allowed opacity-50"
411
+ )}
412
+ >
413
+ <Clock className="size-[1.0625rem] shrink-0 text-muted-foreground" />
414
+ <span
415
+ className={cn(
416
+ "flex-1 truncate",
417
+ value ? "text-foreground" : "text-muted-foreground"
418
+ )}
419
+ >
420
+ {value ? displayTime(value, use24Hour) : placeholder}
421
+ </span>
422
+ {value && !disabled ? (
423
+ <X
424
+ className="size-3.5 shrink-0 text-muted-foreground hover:text-foreground transition-colors duration-150"
425
+ onClick={handleClear}
426
+ />
427
+ ) : (
428
+ <ChevronDown
429
+ className={cn(
430
+ "size-[1.0625rem] shrink-0 text-muted-foreground transition-transform duration-150",
431
+ open && "rotate-180"
432
+ )}
433
+ />
434
+ )}
435
+ </button>
436
+
437
+ {mounted && panel && createPortal(panel, document.body)}
438
+ </div>
439
+ );
440
+ }
441
+ );
442
+
443
+ TimePicker.displayName = "TimePicker";
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5
+ import { cn } from "./utils";
6
+
7
+ const TooltipProvider = TooltipPrimitive.Provider;
8
+ const Tooltip = TooltipPrimitive.Root;
9
+ const TooltipTrigger = TooltipPrimitive.Trigger;
10
+
11
+ const TooltipContent = React.forwardRef<
12
+ React.ElementRef<typeof TooltipPrimitive.Content>,
13
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
14
+ >(({ className, sideOffset = 6, ...props }, ref) => (
15
+ <TooltipPrimitive.Portal>
16
+ <TooltipPrimitive.Content
17
+ ref={ref}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-[130] max-w-xs overflow-hidden rounded-lg border border-border bg-popover px-3 py-2 text-sm leading-snug text-popover-foreground shadow-md",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ </TooltipPrimitive.Portal>
26
+ ));
27
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
28
+
29
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
package/src/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import { cn } from "./utils";
3
+
4
+ export interface VisuallyHiddenProps extends React.HTMLAttributes<HTMLElement> {
5
+ as?: React.ElementType;
6
+ focusable?: boolean;
7
+ }
8
+
9
+ const VisuallyHidden = React.forwardRef<HTMLElement, VisuallyHiddenProps>(
10
+ ({ as: Component = "span", focusable = false, className, ...props }, ref) => (
11
+ <Component
12
+ ref={ref}
13
+ className={cn(
14
+ focusable
15
+ ? "sr-only focus:not-sr-only focus:absolute focus:z-50 focus:px-4 focus:py-2 focus:bg-card focus:text-foreground focus:rounded-md focus:shadow-lg focus:ring-2 focus:ring-ring"
16
+ : "sr-only",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ );
23
+ VisuallyHidden.displayName = "VisuallyHidden";
24
+
25
+ export { VisuallyHidden };