@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.
- package/package.json +72 -0
- package/src/accordion.tsx +68 -0
- package/src/alert.tsx +107 -0
- package/src/avatar-group.tsx +96 -0
- package/src/avatar-tag.tsx +136 -0
- package/src/avatar.tsx +39 -0
- package/src/badge.tsx +98 -0
- package/src/blanket.tsx +61 -0
- package/src/breadcrumb.tsx +119 -0
- package/src/button-group.tsx +218 -0
- package/src/button.tsx +83 -0
- package/src/calendar.tsx +227 -0
- package/src/callout.tsx +68 -0
- package/src/card.tsx +103 -0
- package/src/chart-templates.tsx +587 -0
- package/src/chart.tsx +379 -0
- package/src/checkbox-select.tsx +239 -0
- package/src/checkbox.tsx +54 -0
- package/src/code.tsx +154 -0
- package/src/command.tsx +77 -0
- package/src/country-select.tsx +242 -0
- package/src/currency-amount-input.tsx +72 -0
- package/src/data-table.tsx +378 -0
- package/src/date-picker.tsx +317 -0
- package/src/dialog.tsx +81 -0
- package/src/drawer.tsx +91 -0
- package/src/dropdown-menu.tsx +174 -0
- package/src/empty-state.tsx +32 -0
- package/src/field.tsx +243 -0
- package/src/flag.tsx +265 -0
- package/src/form.tsx +168 -0
- package/src/grid-flex.tsx +241 -0
- package/src/heading.tsx +202 -0
- package/src/icon-button.tsx +93 -0
- package/src/index.ts +332 -0
- package/src/inline-dialog.tsx +153 -0
- package/src/inline-edit.tsx +212 -0
- package/src/input-group.tsx +151 -0
- package/src/input.tsx +28 -0
- package/src/label.tsx +21 -0
- package/src/layout.tsx +119 -0
- package/src/link.tsx +80 -0
- package/src/lozenge.tsx +61 -0
- package/src/menu.tsx +146 -0
- package/src/otp-input.tsx +117 -0
- package/src/page-header.tsx +28 -0
- package/src/pagination.tsx +185 -0
- package/src/password-input.tsx +34 -0
- package/src/popover.tsx +31 -0
- package/src/progress-indicator.tsx +94 -0
- package/src/progress.tsx +95 -0
- package/src/radio-group.tsx +46 -0
- package/src/responsive.tsx +276 -0
- package/src/scroll-area.tsx +39 -0
- package/src/section-message.tsx +119 -0
- package/src/select.tsx +144 -0
- package/src/separator.tsx +26 -0
- package/src/side-nav.tsx +264 -0
- package/src/skeleton.tsx +74 -0
- package/src/slider.tsx +25 -0
- package/src/sonner.tsx +32 -0
- package/src/spinner.tsx +54 -0
- package/src/spotlight.tsx +141 -0
- package/src/status-badge.tsx +86 -0
- package/src/switch.tsx +70 -0
- package/src/tabs.tsx +57 -0
- package/src/tag.tsx +52 -0
- package/src/textarea.tsx +25 -0
- package/src/time-picker.tsx +443 -0
- package/src/tooltip.tsx +29 -0
- package/src/utils.ts +6 -0
- 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";
|
package/src/tooltip.tsx
ADDED
|
@@ -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,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 };
|