basuicn 0.3.10 → 0.3.11
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/dist/ui-cli.cjs +1 -1
- package/package.json +1 -1
- package/registry.json +2 -2
- package/scripts/ui-cli.ts +1 -1
package/dist/ui-cli.cjs
CHANGED
|
@@ -27,7 +27,7 @@ var import_fs = __toESM(require("fs"), 1);
|
|
|
27
27
|
var import_path = __toESM(require("path"), 1);
|
|
28
28
|
var import_child_process = require("child_process");
|
|
29
29
|
var import_readline = __toESM(require("readline"), 1);
|
|
30
|
-
var VERSION = "0.3.
|
|
30
|
+
var VERSION = "0.3.11";
|
|
31
31
|
var REGISTRY_LOCAL = "./registry.json";
|
|
32
32
|
var REGISTRY_REMOTE = "https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json";
|
|
33
33
|
var c = {
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
"files": [
|
|
306
306
|
{
|
|
307
307
|
"path": "src/components/ui/datepicker/DatePicker.tsx",
|
|
308
|
-
"content": "'use client';\r\nimport * as React from 'react';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { DayPicker, type DateRange } from 'react-day-picker';\r\nimport { format } from 'date-fns';\r\nimport { Calendar as CalendarIcon, ChevronDown, Clock } from 'lucide-react';\r\nimport { tv } from 'tailwind-variants';\r\nimport * as locales from 'react-day-picker/locale';\r\n\r\nimport 'react-day-picker/dist/style.css';\r\nimport { Button } from '../button/Button';\r\n\r\n// ---------- types ----------\r\n\r\nexport type TimeFormat = 'HH' | 'HH:mm' | 'HH:mm:ss';\r\nexport type DatePickerMode = 'single' | 'range' | 'time-only';\r\nexport type TimePickerStyle = 'input' | 'select';\r\n\r\ninterface TimeParts {\r\n h: string;\r\n m: string;\r\n s: string;\r\n}\r\n\r\n/** Props for the DatePicker component */\r\nexport interface DatePickerProps {\r\n /** Picker mode: single date, date range, or time-only */\r\n mode?: DatePickerMode;\r\n /** Selected date (Date for single, DateRange for range) */\r\n date?: Date | DateRange;\r\n /** Callback fired when the date changes */\r\n onDateChange?: (date: Date | DateRange | undefined) => void;\r\n /** Alternative callback (alias for onDateChange) */\r\n onChange?: (date: Date | DateRange | undefined) => void;\r\n /** Current time string, only used when mode is 'time-only' */\r\n timeValue?: string;\r\n /** Callback fired when the time value changes (time-only mode) */\r\n onTimeChange?: (time: string) => void;\r\n /** Label text displayed above the picker */\r\n label?: string;\r\n /** Placeholder text when no date is selected */\r\n placeholder?: string;\r\n /** Disable all dates before today */\r\n disablePastDates?: boolean;\r\n /** Show time picker alongside the calendar */\r\n showTime?: boolean;\r\n /** Time format: hours only, hours:minutes, or hours:minutes:seconds */\r\n timeFormat?: TimeFormat;\r\n /** Time picker UI style: native input or dropdown selects */\r\n timePickerStyle?: TimePickerStyle;\r\n /** Disable the entire picker */\r\n disabled?: boolean;\r\n className?: string;\r\n /** Helper text displayed below the picker */\r\n description?: string;\r\n /** Error message displayed below the picker (replaces description) */\r\n error?: string;\r\n required?: boolean;\r\n}\r\n\r\n// ---------- helpers ----------\r\n\r\nconst DEFAULT_TIME: TimeParts = { h: '00', m: '00', s: '00' };\r\n\r\nfunction parseTimeParts(timeStr: string): TimeParts {\r\n const [h = '00', m = '00', s = '00'] = timeStr.split(':');\r\n return {\r\n h: h.padStart(2, '0'),\r\n m: m.padStart(2, '0'),\r\n s: s.padStart(2, '0'),\r\n };\r\n}\r\n\r\nfunction buildTimeString(parts: TimeParts, fmt: TimeFormat): string {\r\n if (fmt === 'HH') return parts.h;\r\n if (fmt === 'HH:mm') return `${parts.h}:${parts.m}`;\r\n return `${parts.h}:${parts.m}:${parts.s}`;\r\n}\r\n\r\nfunction applyTimeToDate(base: Date, parts: TimeParts): Date {\r\n const d = new Date(base);\r\n d.setHours(Number(parts.h), Number(parts.m), Number(parts.s), 0);\r\n return d;\r\n}\r\n\r\nfunction dateToTimeParts(d: Date): TimeParts {\r\n return {\r\n h: d.getHours().toString().padStart(2, '0'),\r\n m: d.getMinutes().toString().padStart(2, '0'),\r\n s: d.getSeconds().toString().padStart(2, '0'),\r\n };\r\n}\r\n\r\nfunction formatDateDisplay(d: Date, showTime: boolean, fmt: TimeFormat): string {\r\n const datePart = format(d, 'dd/MM/yyyy');\r\n if (!showTime) return datePart;\r\n if (fmt === 'HH') return `${datePart} ${format(d, 'HH')}h`;\r\n if (fmt === 'HH:mm') return `${datePart} ${format(d, 'HH:mm')}`;\r\n return `${datePart} ${format(d, 'HH:mm:ss')}`;\r\n}\r\n\r\nfunction padOptions(count: number) {\r\n return Array.from({ length: count }, (_, i) => ({\r\n label: i.toString().padStart(2, '0'),\r\n value: i.toString().padStart(2, '0'),\r\n }));\r\n}\r\n\r\nconst hoursOptions = padOptions(24);\r\nconst minutesOptions = padOptions(60);\r\nconst secondsOptions = padOptions(60);\r\n\r\n// ---------- styles ----------\r\n\r\nconst popoverContent = tv({\r\n base: 'z-50 rounded-xl border border-border bg-background text-foreground shadow-xl outline-none data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n});\r\n\r\n// ---------- sub-components ----------\r\n\r\ninterface NativeSelectProps {\r\n value: string;\r\n options: { label: string; value: string }[];\r\n onChange: (val: string) => void;\r\n 'aria-label'?: string;\r\n}\r\n\r\nconst NativeScrollSelect: React.FC<NativeSelectProps> = ({ value, options, onChange, 'aria-label': ariaLabel }) => (\r\n <select\r\n aria-label={ariaLabel}\r\n value={value}\r\n onChange={(e) => onChange(e.target.value)}\r\n className=\"h-9 w-full rounded-md border border-border bg-background px-2 text-sm text-foreground focus:border-primary focus:outline-none\"\r\n >\r\n {options.map((o) => (\r\n <option key={o.value} value={o.value}>{o.label}</option>\r\n ))}\r\n </select>\r\n);\r\n\r\ninterface TimePickerProps {\r\n parts: TimeParts;\r\n onChange: (parts: TimeParts) => void;\r\n timeFormat: TimeFormat;\r\n timePickerStyle: TimePickerStyle;\r\n}\r\n\r\nconst TimePicker: React.FC<TimePickerProps> = ({ parts, onChange, timeFormat, timePickerStyle }) => {\r\n const showMinutes = timeFormat === 'HH:mm' || timeFormat === 'HH:mm:ss';\r\n const showSeconds = timeFormat === 'HH:mm:ss';\r\n\r\n if (timePickerStyle === 'input') {\r\n const step = showSeconds ? 1 : 60;\r\n const rawValue = showSeconds\r\n ? `${parts.h}:${parts.m}:${parts.s}`\r\n : `${parts.h}:${parts.m}`;\r\n\r\n return (\r\n <input\r\n type=\"time\"\r\n value={rawValue}\r\n step={step}\r\n onChange={(e) => {\r\n const [h = '00', m = '00', s = '00'] = e.target.value.split(':');\r\n onChange({ h: h.padStart(2, '0'), m: m.padStart(2, '0'), s: s.padStart(2, '0') });\r\n }}\r\n className=\"h-9 w-full rounded-md border border-border bg-background px-3 text-sm text-foreground focus:border-primary focus:outline-none\"\r\n />\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"flex items-center gap-1.5\">\r\n <div className=\"flex-1\">\r\n <NativeScrollSelect\r\n aria-label=\"Hours\"\r\n value={parts.h}\r\n options={hoursOptions}\r\n onChange={(val) => onChange({ ...parts, h: val })}\r\n />\r\n </div>\r\n {showMinutes && (\r\n <>\r\n <span className=\"text-sm font-bold text-muted-foreground\">:</span>\r\n <div className=\"flex-1\">\r\n <NativeScrollSelect\r\n aria-label=\"Minutes\"\r\n value={parts.m}\r\n options={minutesOptions}\r\n onChange={(val) => onChange({ ...parts, m: val })}\r\n />\r\n </div>\r\n </>\r\n )}\r\n {showSeconds && (\r\n <>\r\n <span className=\"text-sm font-bold text-muted-foreground\">:</span>\r\n <div className=\"flex-1\">\r\n <NativeScrollSelect\r\n aria-label=\"Seconds\"\r\n value={parts.s}\r\n options={secondsOptions}\r\n onChange={(val) => onChange({ ...parts, s: val })}\r\n />\r\n </div>\r\n </>\r\n )}\r\n </div>\r\n );\r\n};\r\n\r\n// ---------- main component ----------\r\n\r\nexport const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(({\r\n mode = 'single',\r\n date,\r\n onDateChange,\r\n onChange,\r\n timeValue,\r\n onTimeChange,\r\n label,\r\n placeholder = 'Select date...',\r\n disablePastDates = false,\r\n showTime = false,\r\n timeFormat = 'HH:mm:ss',\r\n timePickerStyle = 'select',\r\n disabled = false,\r\n className,\r\n description,\r\n error,\r\n required,\r\n}, ref) => {\r\n const [open, setOpen] = React.useState(false);\r\n const triggerRef = React.useRef<HTMLButtonElement>(null);\r\n\r\n const timeParts = React.useMemo<TimeParts>(() => {\r\n if (mode === 'time-only' && timeValue) return parseTimeParts(timeValue);\r\n if (date instanceof Date) return dateToTimeParts(date);\r\n return DEFAULT_TIME;\r\n }, [date, timeValue, mode]);\r\n\r\n const rangeTimeParts = React.useMemo<{ from: TimeParts; to: TimeParts }>(() => {\r\n if (mode !== 'range') return { from: DEFAULT_TIME, to: DEFAULT_TIME };\r\n const range = date as DateRange | undefined;\r\n return {\r\n from: range?.from ? dateToTimeParts(range.from) : DEFAULT_TIME,\r\n to: range?.to ? dateToTimeParts(range.to) : DEFAULT_TIME,\r\n };\r\n }, [date, mode]);\r\n\r\n const emitRange = (newRange: DateRange | undefined) => {\r\n onDateChange?.(newRange);\r\n onChange?.(newRange);\r\n };\r\n\r\n const handlePartsChange = (newParts: TimeParts) => {\r\n if (mode === 'time-only') {\r\n onTimeChange?.(buildTimeString(newParts, timeFormat));\r\n return;\r\n }\r\n if (date instanceof Date) {\r\n const newDate = applyTimeToDate(date, newParts);\r\n onDateChange?.(newDate);\r\n onChange?.(newDate);\r\n }\r\n };\r\n\r\n const handleRangePartsChange = (newParts: TimeParts, which: 'from' | 'to') => {\r\n const range = date as DateRange | undefined;\r\n const target = range?.[which];\r\n if (!range || !target) return;\r\n emitRange({ ...range, [which]: applyTimeToDate(target, newParts) });\r\n };\r\n\r\n const handleDateSelect = (selectedDate: Date | DateRange | Date[] | undefined) => {\r\n if (!selectedDate) {\r\n onDateChange?.(undefined);\r\n onChange?.(undefined);\r\n return;\r\n }\r\n if (mode === 'single' && showTime && selectedDate instanceof Date) {\r\n const newDate = applyTimeToDate(selectedDate, timeParts);\r\n onDateChange?.(newDate);\r\n onChange?.(newDate);\r\n return;\r\n }\r\n if (mode === 'range' && showTime) {\r\n const newRange = selectedDate as DateRange;\r\n const preserved: DateRange = {\r\n from: newRange.from ? applyTimeToDate(newRange.from, rangeTimeParts.from) : undefined,\r\n to: newRange.to ? applyTimeToDate(newRange.to, rangeTimeParts.to) : undefined,\r\n };\r\n emitRange(preserved);\r\n return;\r\n }\r\n // Because of our mode checking, we can be confident here\r\n onDateChange?.(selectedDate as DateRange);\r\n onChange?.(selectedDate as DateRange);\r\n };\r\n\r\n // ---------- render trigger label ----------\r\n const triggerLabel = React.useMemo(() => {\r\n if (mode === 'time-only') {\r\n const val = timeValue ?? buildTimeString(timeParts, timeFormat);\r\n if (!val || val === '00' || val === '00:00' || val === '00:00:00')\r\n return <span className=\"text-muted-foreground\">{placeholder || 'Select time...'}</span>;\r\n return <span>{val}</span>;\r\n }\r\n\r\n if (!date) return <span className=\"text-muted-foreground\">{placeholder}</span>;\r\n\r\n if (mode === 'single' && date instanceof Date) {\r\n return <span>{formatDateDisplay(date, showTime, timeFormat)}</span>;\r\n }\r\n\r\n if (mode === 'range') {\r\n const range = date as DateRange;\r\n const fmtOne = (d: Date) => (showTime ? formatDateDisplay(d, true, timeFormat) : format(d, 'dd/MM/yyyy'));\r\n if (range.from && range.to) {\r\n return <span>{fmtOne(range.from)} – {fmtOne(range.to)}</span>;\r\n }\r\n if (range.from) return <span>{fmtOne(range.from)} –</span>;\r\n }\r\n\r\n return <span className=\"text-muted-foreground\">{placeholder}</span>;\r\n }, [date, mode, showTime, timeFormat, timeValue, timeParts, placeholder]);\r\n\r\n const isTimeMode = mode === 'time-only';\r\n const isRangeWithTime = mode === 'range' && showTime;\r\n const needsTimePicker = isTimeMode || (mode === 'single' && showTime);\r\n const timeLegend = timeFormat === 'HH' ? 'Select hour' : timeFormat === 'HH:mm' ? 'Hour : Minute' : 'Hour : Minute : Second';\r\n\r\n return (\r\n <div ref={ref} className={`flex flex-col gap-1.5 w-full ${className || ''}`}>\r\n {label && (\r\n <label className=\"text-sm font-medium text-foreground\">\r\n {label}\r\n {required && <span className=\"ml-0.5 text-destructive\">*</span>}\r\n </label>\r\n )}\r\n\r\n <BasePopover.Root open={open} onOpenChange={disabled ? undefined : setOpen}>\r\n <BasePopover.Trigger\r\n render={\r\n <button\r\n ref={triggerRef}\r\n type=\"button\"\r\n disabled={disabled}\r\n className={[\r\n 'flex h-10 w-full items-center gap-2 rounded-lg border bg-background px-3 py-2 text-sm',\r\n 'ring-offset-background transition-shadow',\r\n 'hover:border-primary focus:border-primary focus:outline-none',\r\n 'disabled:cursor-not-allowed disabled:opacity-50',\r\n error ? 'border-danger focus:border-danger' : 'border-border',\r\n 'group',\r\n ].join(' ')}\r\n >\r\n {isTimeMode ? (\r\n <Clock className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\r\n ) : (\r\n <CalendarIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\r\n )}\r\n <div className=\"flex-1 truncate text-left\">{triggerLabel}</div>\r\n <ChevronDown className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-open:rotate-180\" />\r\n </button>\r\n }\r\n />\r\n\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner anchor={triggerRef} sideOffset={6} className=\"z-50\">\r\n <BasePopover.Popup className={popoverContent()}>\r\n {!isTimeMode && mode === 'single' && (\r\n <div className=\"p-2 flex justify-center\">\r\n <DayPicker\r\n mode=\"single\"\r\n locale={locales.vi}\r\n selected={date as Date | undefined}\r\n onSelect={(d) => handleDateSelect(d)}\r\n disabled={disablePastDates ? [{ before: new Date() }] : undefined}\r\n className=\"rdp-custom\"\r\n />\r\n </div>\r\n )}\r\n {!isTimeMode && mode === 'range' && (\r\n <div className=\"p-2 flex justify-center\">\r\n <DayPicker\r\n mode=\"range\"\r\n locale={locales.vi}\r\n selected={date as DateRange | undefined}\r\n onSelect={(d) => handleDateSelect(d)}\r\n disabled={disablePastDates ? [{ before: new Date() }] : undefined}\r\n className=\"rdp-custom\"\r\n />\r\n </div>\r\n )}\r\n\r\n {/* Time picker — single / time-only */}\r\n {needsTimePicker && (\r\n <div className={`border-t border-border p-3 flex flex-col gap-2 ${isTimeMode ? 'border-t-0' : ''}`}>\r\n <div className=\"flex items-center gap-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide\">\r\n <Clock className=\"w-3.5 h-3.5\" />\r\n <span>{timeLegend}</span>\r\n </div>\r\n <TimePicker\r\n parts={timeParts}\r\n onChange={handlePartsChange}\r\n timeFormat={timeFormat}\r\n timePickerStyle={timePickerStyle}\r\n />\r\n </div>\r\n )}\r\n\r\n {/* Time picker — range (2 bộ: Từ / Đến) */}\r\n {isRangeWithTime && (() => {\r\n const range = date as DateRange | undefined;\r\n const hasFrom = !!range?.from;\r\n const hasTo = !!range?.to;\r\n return (\r\n <div className=\"border-t border-border p-3 flex flex-col gap-3\">\r\n <div className=\"flex items-center gap-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide\">\r\n <Clock className=\"w-3.5 h-3.5\" />\r\n <span>{timeLegend}</span>\r\n </div>\r\n <div className=\"grid grid-cols-2 gap-3\">\r\n <div className=\"flex flex-col gap-1.5\">\r\n <span className=\"text-[11px] font-medium text-muted-foreground\">Từ</span>\r\n <div className={!hasFrom ? 'pointer-events-none opacity-50' : ''}>\r\n <TimePicker\r\n parts={rangeTimeParts.from}\r\n onChange={(p) => handleRangePartsChange(p, 'from')}\r\n timeFormat={timeFormat}\r\n timePickerStyle={timePickerStyle}\r\n />\r\n </div>\r\n </div>\r\n <div className=\"flex flex-col gap-1.5\">\r\n <span className=\"text-[11px] font-medium text-muted-foreground\">Đến</span>\r\n <div className={!hasTo ? 'pointer-events-none opacity-50' : ''}>\r\n <TimePicker\r\n parts={rangeTimeParts.to}\r\n onChange={(p) => handleRangePartsChange(p, 'to')}\r\n timeFormat={timeFormat}\r\n timePickerStyle={timePickerStyle}\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n })()}\r\n\r\n {/* Footer actions */}\r\n <div className=\"flex items-center justify-between gap-2 p-3 border-t border-border\">\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n if (mode === 'time-only') {\r\n onTimeChange?.('');\r\n } else {\r\n onDateChange?.(undefined);\r\n }\r\n }}\r\n className=\"text-xs text-muted-foreground hover:text-foreground transition-colors underline-offset-2 hover:underline\"\r\n >\r\n Clear\r\n </button>\r\n <Button size=\"sm\" onClick={() => setOpen(false)}>\r\n Confirm\r\n </Button>\r\n </div>\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n </BasePopover.Root>\r\n {description && !error && (\r\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nDatePicker.displayName = \"DatePicker\";\r\n"
|
|
308
|
+
"content": "import * as React from 'react';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { DayPicker, type DateRange } from 'react-day-picker';\r\nimport { format } from 'date-fns';\r\nimport { Calendar as CalendarIcon, ChevronDown, Clock } from 'lucide-react';\r\nimport { tv } from 'tailwind-variants';\r\nimport * as locales from 'react-day-picker/locale';\r\n\r\nimport 'react-day-picker/dist/style.css';\r\nimport { Button } from '../button/Button';\r\n\r\n// ---------- types ----------\r\n\r\nexport type TimeFormat = 'HH' | 'HH:mm' | 'HH:mm:ss';\r\nexport type DatePickerMode = 'single' | 'range' | 'time-only';\r\nexport type TimePickerStyle = 'input' | 'select';\r\n\r\ninterface TimeParts {\r\n h: string;\r\n m: string;\r\n s: string;\r\n}\r\n\r\n/** Props for the DatePicker component */\r\nexport interface DatePickerProps {\r\n /** Picker mode: single date, date range, or time-only */\r\n mode?: DatePickerMode;\r\n /** Selected date (Date for single, DateRange for range) */\r\n date?: Date | DateRange;\r\n /** Callback fired when the date changes */\r\n onDateChange?: (date: Date | DateRange | undefined) => void;\r\n /** Alternative callback (alias for onDateChange) */\r\n onChange?: (date: Date | DateRange | undefined) => void;\r\n /** Current time string, only used when mode is 'time-only' */\r\n timeValue?: string;\r\n /** Callback fired when the time value changes (time-only mode) */\r\n onTimeChange?: (time: string) => void;\r\n /** Label text displayed above the picker */\r\n label?: string;\r\n /** Placeholder text when no date is selected */\r\n placeholder?: string;\r\n /** Disable all dates before today */\r\n disablePastDates?: boolean;\r\n /** Show time picker alongside the calendar */\r\n showTime?: boolean;\r\n /** Time format: hours only, hours:minutes, or hours:minutes:seconds */\r\n timeFormat?: TimeFormat;\r\n /** Time picker UI style: native input or dropdown selects */\r\n timePickerStyle?: TimePickerStyle;\r\n /** Disable the entire picker */\r\n disabled?: boolean;\r\n className?: string;\r\n /** Helper text displayed below the picker */\r\n description?: string;\r\n /** Error message displayed below the picker (replaces description) */\r\n error?: string;\r\n required?: boolean;\r\n captionLayout?: \"label\" | \"dropdown\" | \"dropdown-months\" | \"dropdown-years\" | undefined;\r\n}\r\n\r\n// ---------- helpers ----------\r\n\r\nconst DEFAULT_TIME: TimeParts = { h: '00', m: '00', s: '00' };\r\n\r\nfunction parseTimeParts(timeStr: string): TimeParts {\r\n const [h = '00', m = '00', s = '00'] = timeStr.split(':');\r\n return {\r\n h: h.padStart(2, '0'),\r\n m: m.padStart(2, '0'),\r\n s: s.padStart(2, '0'),\r\n };\r\n}\r\n\r\nfunction buildTimeString(parts: TimeParts, fmt: TimeFormat): string {\r\n if (fmt === 'HH') return parts.h;\r\n if (fmt === 'HH:mm') return `${parts.h}:${parts.m}`;\r\n return `${parts.h}:${parts.m}:${parts.s}`;\r\n}\r\n\r\nfunction applyTimeToDate(base: Date, parts: TimeParts): Date {\r\n const d = new Date(base);\r\n d.setHours(Number(parts.h), Number(parts.m), Number(parts.s), 0);\r\n return d;\r\n}\r\n\r\nfunction dateToTimeParts(d: Date): TimeParts {\r\n return {\r\n h: d.getHours().toString().padStart(2, '0'),\r\n m: d.getMinutes().toString().padStart(2, '0'),\r\n s: d.getSeconds().toString().padStart(2, '0'),\r\n };\r\n}\r\n\r\nfunction formatDateDisplay(d: Date, showTime: boolean, fmt: TimeFormat): string {\r\n const datePart = format(d, 'dd/MM/yyyy');\r\n if (!showTime) return datePart;\r\n if (fmt === 'HH') return `${datePart} ${format(d, 'HH')}h`;\r\n if (fmt === 'HH:mm') return `${datePart} ${format(d, 'HH:mm')}`;\r\n return `${datePart} ${format(d, 'HH:mm:ss')}`;\r\n}\r\n\r\nfunction padOptions(count: number) {\r\n return Array.from({ length: count }, (_, i) => ({\r\n label: i.toString().padStart(2, '0'),\r\n value: i.toString().padStart(2, '0'),\r\n }));\r\n}\r\n\r\nconst hoursOptions = padOptions(24);\r\nconst minutesOptions = padOptions(60);\r\nconst secondsOptions = padOptions(60);\r\n\r\n// ---------- styles ----------\r\n\r\nconst popoverContent = tv({\r\n base: 'z-50 rounded-xl border border-border bg-background text-foreground shadow-xl outline-none data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n});\r\n\r\n// ---------- sub-components ----------\r\n\r\ninterface NativeSelectProps {\r\n value: string;\r\n options: { label: string; value: string }[];\r\n onChange: (val: string) => void;\r\n 'aria-label'?: string;\r\n}\r\n\r\nconst NativeScrollSelect: React.FC<NativeSelectProps> = ({ value, options, onChange, 'aria-label': ariaLabel }) => (\r\n <select\r\n aria-label={ariaLabel}\r\n value={value}\r\n onChange={(e) => onChange(e.target.value)}\r\n className=\"h-9 w-full rounded-md border border-border bg-background px-2 text-sm text-foreground focus:border-primary focus:outline-none\"\r\n >\r\n {options.map((o) => (\r\n <option key={o.value} value={o.value}>{o.label}</option>\r\n ))}\r\n </select>\r\n);\r\n\r\ninterface TimePickerProps {\r\n parts: TimeParts;\r\n onChange: (parts: TimeParts) => void;\r\n timeFormat: TimeFormat;\r\n timePickerStyle: TimePickerStyle;\r\n}\r\n\r\nconst TimePicker: React.FC<TimePickerProps> = ({ parts, onChange, timeFormat, timePickerStyle }) => {\r\n const showMinutes = timeFormat === 'HH:mm' || timeFormat === 'HH:mm:ss';\r\n const showSeconds = timeFormat === 'HH:mm:ss';\r\n\r\n if (timePickerStyle === 'input') {\r\n const step = showSeconds ? 1 : 60;\r\n const rawValue = showSeconds\r\n ? `${parts.h}:${parts.m}:${parts.s}`\r\n : `${parts.h}:${parts.m}`;\r\n\r\n return (\r\n <input\r\n type=\"time\"\r\n value={rawValue}\r\n step={step}\r\n onChange={(e) => {\r\n const [h = '00', m = '00', s = '00'] = e.target.value.split(':');\r\n onChange({ h: h.padStart(2, '0'), m: m.padStart(2, '0'), s: s.padStart(2, '0') });\r\n }}\r\n className=\"h-9 w-full rounded-md border border-border bg-background px-3 text-sm text-foreground focus:border-primary focus:outline-none\"\r\n />\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"flex items-center gap-1.5\">\r\n <div className=\"flex-1\">\r\n <NativeScrollSelect\r\n aria-label=\"Hours\"\r\n value={parts.h}\r\n options={hoursOptions}\r\n onChange={(val) => onChange({ ...parts, h: val })}\r\n />\r\n </div>\r\n {showMinutes && (\r\n <>\r\n <span className=\"text-sm font-bold text-muted-foreground\">:</span>\r\n <div className=\"flex-1\">\r\n <NativeScrollSelect\r\n aria-label=\"Minutes\"\r\n value={parts.m}\r\n options={minutesOptions}\r\n onChange={(val) => onChange({ ...parts, m: val })}\r\n />\r\n </div>\r\n </>\r\n )}\r\n {showSeconds && (\r\n <>\r\n <span className=\"text-sm font-bold text-muted-foreground\">:</span>\r\n <div className=\"flex-1\">\r\n <NativeScrollSelect\r\n aria-label=\"Seconds\"\r\n value={parts.s}\r\n options={secondsOptions}\r\n onChange={(val) => onChange({ ...parts, s: val })}\r\n />\r\n </div>\r\n </>\r\n )}\r\n </div>\r\n );\r\n};\r\n\r\n// ---------- main component ----------\r\n\r\nexport const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(({\r\n mode = 'single',\r\n date: dateProp,\r\n onDateChange,\r\n onChange,\r\n timeValue,\r\n onTimeChange,\r\n label,\r\n placeholder = 'Select date...',\r\n disablePastDates = false,\r\n showTime = false,\r\n timeFormat = 'HH:mm:ss',\r\n timePickerStyle = 'select',\r\n disabled = false,\r\n className,\r\n description,\r\n error,\r\n required,\r\n captionLayout = undefined,\r\n}, ref) => {\r\n const [open, setOpen] = React.useState(false);\r\n const triggerRef = React.useRef<HTMLButtonElement>(null);\r\n\r\n // Internal state — hoạt động cả controlled lẫn uncontrolled\r\n const isControlled = dateProp !== undefined;\r\n const [internalDate, setInternalDate] = React.useState<Date | DateRange | undefined>(undefined);\r\n const date = isControlled ? dateProp : internalDate;\r\n\r\n // Calendar luôn navigate đến tháng của ngày đã chọn khi mở\r\n const [calendarMonth, setCalendarMonth] = React.useState<Date>(new Date());\r\n const handleOpenChange = (newOpen: boolean) => {\r\n if (newOpen) {\r\n const selectedDate = date instanceof Date ? date : (date as DateRange)?.from;\r\n setCalendarMonth(selectedDate ?? new Date());\r\n }\r\n setOpen(newOpen);\r\n };\r\n\r\n const timeParts = React.useMemo<TimeParts>(() => {\r\n if (mode === 'time-only' && timeValue) return parseTimeParts(timeValue);\r\n if (date instanceof Date) return dateToTimeParts(date);\r\n return DEFAULT_TIME;\r\n }, [date, timeValue, mode]);\r\n\r\n const handlePartsChange = (newParts: TimeParts) => {\r\n if (mode === 'time-only') {\r\n onTimeChange?.(buildTimeString(newParts, timeFormat));\r\n return;\r\n }\r\n if (date instanceof Date) {\r\n const newDate = applyTimeToDate(date, newParts);\r\n if (!isControlled) setInternalDate(newDate);\r\n onDateChange?.(newDate);\r\n onChange?.(newDate);\r\n }\r\n };\r\n\r\n const handleDateSelect = (selectedDate: Date | DateRange | Date[] | undefined) => {\r\n if (!selectedDate) {\r\n if (!isControlled) setInternalDate(undefined);\r\n onDateChange?.(undefined);\r\n onChange?.(undefined);\r\n return;\r\n }\r\n if (mode === 'single' && showTime && selectedDate instanceof Date) {\r\n const newDate = applyTimeToDate(selectedDate, timeParts);\r\n if (!isControlled) setInternalDate(newDate);\r\n onDateChange?.(newDate);\r\n onChange?.(newDate);\r\n } else {\r\n if (!isControlled) setInternalDate(selectedDate as Date | DateRange);\r\n onDateChange?.(selectedDate as DateRange);\r\n onChange?.(selectedDate as DateRange);\r\n // Auto-close sau khi chọn date (single mode không có time)\r\n if (mode === 'single' && !showTime) setOpen(false);\r\n }\r\n };\r\n\r\n // ---------- render trigger label ----------\r\n const triggerLabel = React.useMemo(() => {\r\n if (mode === 'time-only') {\r\n const val = timeValue ?? buildTimeString(timeParts, timeFormat);\r\n if (!val || val === '00' || val === '00:00' || val === '00:00:00')\r\n return <span className=\"text-muted-foreground\">{placeholder || 'Select time...'}</span>;\r\n return <span>{val}</span>;\r\n }\r\n\r\n if (!date) return <span className=\"text-muted-foreground\">{placeholder}</span>;\r\n\r\n if (mode === 'single' && date instanceof Date) {\r\n return <span>{formatDateDisplay(date, showTime, timeFormat)}</span>;\r\n }\r\n\r\n if (mode === 'range') {\r\n const range = date as DateRange;\r\n if (range.from && range.to) {\r\n return (\r\n <span>\r\n {format(range.from, 'dd/MM/yyyy')} – {format(range.to, 'dd/MM/yyyy')}\r\n </span>\r\n );\r\n }\r\n if (range.from) return <span>{format(range.from, 'dd/MM/yyyy')} –</span>;\r\n }\r\n\r\n return <span className=\"text-muted-foreground\">{placeholder}</span>;\r\n }, [date, mode, showTime, timeFormat, timeValue, timeParts, placeholder]);\r\n\r\n const isTimeMode = mode === 'time-only';\r\n const needsTimePicker = isTimeMode || (mode === 'single' && showTime);\r\n\r\n return (\r\n <div ref={ref} className={`flex flex-col gap-1.5 w-full ${className || ''}`}>\r\n {label && (\r\n <label className=\"text-sm font-medium text-foreground\">\r\n {label}\r\n {required && <span className=\"ml-0.5 text-destructive\">*</span>}\r\n </label>\r\n )}\r\n\r\n <BasePopover.Root open={open} onOpenChange={disabled ? undefined : handleOpenChange}>\r\n <BasePopover.Trigger\r\n render={\r\n <button\r\n ref={triggerRef}\r\n type=\"button\"\r\n disabled={disabled}\r\n className={[\r\n 'flex h-10 w-full items-center gap-2 rounded-lg border bg-background px-3 py-2 text-sm',\r\n 'ring-offset-background transition-shadow',\r\n 'hover:border-primary focus:border-primary focus:outline-none',\r\n 'disabled:cursor-not-allowed disabled:opacity-50',\r\n error ? 'border-danger focus:border-danger' : 'border-border',\r\n 'group',\r\n ].join(' ')}\r\n >\r\n {isTimeMode ? (\r\n <Clock className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\r\n ) : (\r\n <CalendarIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\r\n )}\r\n <div className=\"flex-1 truncate text-left\">{triggerLabel}</div>\r\n <ChevronDown className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-open:rotate-180\" />\r\n </button>\r\n }\r\n />\r\n\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner anchor={triggerRef} sideOffset={6} className=\"z-50\">\r\n <BasePopover.Popup className={popoverContent()}>\r\n {!isTimeMode && mode === 'single' && (\r\n <div className=\"p-2 flex justify-center\">\r\n <DayPicker\r\n mode=\"single\"\r\n locale={locales.vi}\r\n selected={date as Date | undefined}\r\n month={calendarMonth}\r\n onMonthChange={setCalendarMonth}\r\n onSelect={(d) => handleDateSelect(d)}\r\n captionLayout={captionLayout}\r\n startMonth={new Date(1900, 0)}\r\n endMonth={new Date(2100, 11)}\r\n disabled={disablePastDates ? [{ before: new Date() }] : undefined}\r\n className=\"rdp-custom\"\r\n />\r\n </div>\r\n )}\r\n {!isTimeMode && mode === 'range' && (\r\n <div className=\"p-2 flex justify-center\">\r\n <DayPicker\r\n mode=\"range\"\r\n locale={locales.vi}\r\n selected={date as DateRange | undefined}\r\n month={calendarMonth}\r\n onMonthChange={setCalendarMonth}\r\n onSelect={(d) => handleDateSelect(d)}\r\n captionLayout={captionLayout}\r\n startMonth={new Date(1900, 0)}\r\n endMonth={new Date(2100, 11)}\r\n disabled={disablePastDates ? [{ before: new Date() }] : undefined}\r\n className=\"rdp-custom\"\r\n />\r\n </div>\r\n )}\r\n\r\n {/* Time picker */}\r\n {needsTimePicker && (\r\n <div className={`border-t border-border p-3 flex flex-col gap-2 ${isTimeMode ? 'border-t-0' : ''}`}>\r\n <div className=\"flex items-center gap-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide\">\r\n <Clock className=\"w-3.5 h-3.5\" />\r\n <span>\r\n {timeFormat === 'HH' ? 'Select hour' : timeFormat === 'HH:mm' ? 'Hour : Minute' : 'Hour : Minute : Second'}\r\n </span>\r\n </div>\r\n <TimePicker\r\n parts={timeParts}\r\n onChange={handlePartsChange}\r\n timeFormat={timeFormat}\r\n timePickerStyle={timePickerStyle}\r\n />\r\n </div>\r\n )}\r\n\r\n {/* Footer actions */}\r\n <div className=\"flex items-center justify-between gap-2 p-3 border-t border-border\">\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n if (mode === 'time-only') {\r\n onTimeChange?.('');\r\n } else {\r\n if (!isControlled) setInternalDate(undefined);\r\n onDateChange?.(undefined);\r\n onChange?.(undefined);\r\n }\r\n }}\r\n className=\"text-xs text-muted-foreground hover:text-foreground transition-colors underline-offset-2 hover:underline\"\r\n >\r\n Clear\r\n </button>\r\n <Button size=\"sm\" onClick={() => setOpen(false)}>\r\n Confirm\r\n </Button>\r\n </div>\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n </BasePopover.Root>\r\n {description && !error && (\r\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nDatePicker.displayName = \"DatePicker\";\r\n"
|
|
309
309
|
},
|
|
310
310
|
{
|
|
311
311
|
"path": "src/components/ui/datepicker/index.ts",
|
|
@@ -339,7 +339,7 @@
|
|
|
339
339
|
"files": [
|
|
340
340
|
{
|
|
341
341
|
"path": "src/components/ui/drawer/Drawer.tsx",
|
|
342
|
-
"content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\n\r\nconst drawerVariants = tv({\r\n slots: {\r\n overlay: [\r\n 'fixed inset-0 z-50 bg-black/40',\r\n 'transition-opacity duration-200 ease-out',\r\n 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',\r\n ],\r\n panel: [\r\n 'fixed z-50 bg-background shadow-2xl flex flex-col',\r\n 'outline-none overflow-hidden m-0 p-0 max-w-full max-h-full border-none',\r\n 'transition duration-300 ease-out',\r\n 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',\r\n ],\r\n header:\r\n 'flex items-center justify-between px-6 py-4 border-b border-border/50 shrink-0',\r\n title: 'text-base font-semibold text-foreground',\r\n description: 'text-sm text-muted-foreground mt-0.5',\r\n body: 'flex-1 overflow-y-auto px-6 py-4',\r\n footer: 'px-6 py-4 border-t border-border/50 shrink-0',\r\n close:\r\n 'rounded-sm opacity-70 hover:opacity-100 transition-opacity ring-offset-background focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',\r\n },\r\n variants: {\r\n direction: {\r\n left: {\r\n panel: [\r\n 'inset-y-0 left-0 h-full',\r\n 'data-[starting-style]:-translate-x-full data-[ending-style]:-translate-x-full',\r\n ],\r\n },\r\n right: {\r\n panel: [\r\n 'inset-y-0 right-0 h-full',\r\n 'data-[starting-style]:translate-x-full data-[ending-style]:translate-x-full',\r\n ],\r\n },\r\n top: {\r\n panel: [\r\n 'inset-x-0 top-0 w-full',\r\n 'data-[starting-style]:-translate-y-full data-[ending-style]:-translate-y-full',\r\n ],\r\n },\r\n bottom: {\r\n panel: [\r\n 'inset-x-0 bottom-0 w-full',\r\n 'data-[starting-style]:translate-y-full data-[ending-style]:translate-y-full',\r\n ],\r\n },\r\n },\r\n size: {\r\n sm: {},\r\n md: {},\r\n lg: {},\r\n full: {},\r\n },\r\n backdropBlur: {\r\n true: { overlay: 'backdrop-blur-sm' },\r\n false: { overlay: '' },\r\n },\r\n },\r\n compoundVariants: [\r\n { direction: 'left', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'left', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'left', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'left', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'right', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'right', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'right', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'right', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'top', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'top', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'top', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'top', size: 'full', class: { panel: 'h-full' } },\r\n { direction: 'bottom', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'bottom', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'bottom', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'bottom', size: 'full', class: { panel: 'h-full' } },\r\n ],\r\n defaultVariants: {\r\n direction: '
|
|
342
|
+
"content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\n\r\nconst drawerVariants = tv({\r\n slots: {\r\n overlay: [\r\n 'fixed inset-0 z-50 bg-black/40',\r\n 'transition-opacity duration-200 ease-out',\r\n 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',\r\n ],\r\n panel: [\r\n 'fixed z-50 bg-background shadow-2xl flex flex-col',\r\n 'outline-none overflow-hidden m-0 p-0 max-w-full max-h-full border-none',\r\n 'transition duration-300 ease-out',\r\n 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',\r\n ],\r\n header:\r\n 'flex items-center justify-between px-6 py-4 border-b border-border/50 shrink-0',\r\n title: 'text-base font-semibold text-foreground',\r\n description: 'text-sm text-muted-foreground mt-0.5',\r\n body: 'flex-1 overflow-y-auto px-6 py-4',\r\n footer: 'px-6 py-4 border-t border-border/50 shrink-0',\r\n close:\r\n 'rounded-sm opacity-70 hover:opacity-100 transition-opacity ring-offset-background focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',\r\n },\r\n variants: {\r\n direction: {\r\n left: {\r\n panel: [\r\n 'inset-y-0 left-0 h-full',\r\n 'data-[starting-style]:-translate-x-full data-[ending-style]:-translate-x-full',\r\n ],\r\n },\r\n right: {\r\n panel: [\r\n 'inset-y-0 right-0 h-full',\r\n 'data-[starting-style]:translate-x-full data-[ending-style]:translate-x-full',\r\n ],\r\n },\r\n top: {\r\n panel: [\r\n 'inset-x-0 top-0 w-full',\r\n 'data-[starting-style]:-translate-y-full data-[ending-style]:-translate-y-full',\r\n ],\r\n },\r\n bottom: {\r\n panel: [\r\n 'inset-x-0 bottom-0 w-full',\r\n 'data-[starting-style]:translate-y-full data-[ending-style]:translate-y-full',\r\n ],\r\n },\r\n },\r\n size: {\r\n sm: {},\r\n md: {},\r\n lg: {},\r\n full: {},\r\n },\r\n backdropBlur: {\r\n true: { overlay: 'backdrop-blur-sm' },\r\n false: { overlay: '' },\r\n },\r\n },\r\n compoundVariants: [\r\n { direction: 'left', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'left', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'left', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'left', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'right', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'right', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'right', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'right', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'top', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'top', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'top', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'top', size: 'full', class: { panel: 'h-full' } },\r\n { direction: 'bottom', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'bottom', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'bottom', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'bottom', size: 'full', class: { panel: 'h-full' } },\r\n ],\r\n defaultVariants: {\r\n direction: 'top',\r\n size: 'md',\r\n backdropBlur: true,\r\n },\r\n});\r\n\r\n/* ─── Root ─── */\r\nconst Drawer = BaseDialog.Root;\r\n\r\n/* ─── Trigger ─── */\r\nconst DrawerTrigger = BaseDialog.Trigger;\r\n\r\n/* ─── Close (re-export for custom close buttons) ─── */\r\nconst DrawerClose = BaseDialog.Close;\r\n\r\n/* ─── Content (Portal + Backdrop + Popup) ─── */\r\ninterface DrawerContentProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>, 'className'>,\r\n VariantProps<typeof drawerVariants> {\r\n className?: string;\r\n keepMounted?: boolean;\r\n}\r\n\r\nconst DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(\r\n ({ className, children, direction, size, backdropBlur, keepMounted, ...props }, ref) => {\r\n const slots = drawerVariants({ direction, size, backdropBlur });\r\n return (\r\n <BaseDialog.Portal keepMounted={keepMounted}>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup ref={ref} className={slots.panel({ className })} {...props}>\r\n {children}\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n );\r\n },\r\n);\r\nDrawerContent.displayName = 'DrawerContent';\r\n\r\n/* ─── Header (includes close button by default) ─── */\r\ninterface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\r\n hideClose?: boolean;\r\n}\r\n\r\nconst DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\r\n ({ className, children, hideClose, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return (\r\n <div ref={ref} className={slots.header({ className })} {...props}>\r\n <div>{children}</div>\r\n {!hideClose && (\r\n <BaseDialog.Close className={slots.close()} aria-label=\"Close\">\r\n <X className=\"h-4 w-4\" />\r\n </BaseDialog.Close>\r\n )}\r\n </div>\r\n );\r\n },\r\n);\r\nDrawerHeader.displayName = 'DrawerHeader';\r\n\r\n/* ─── Title ─── */\r\nconst DrawerTitle = React.forwardRef<\r\n HTMLHeadingElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>, 'className'> & {\r\n className?: string;\r\n }\r\n>(({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <BaseDialog.Title ref={ref} className={slots.title({ className })} {...props} />;\r\n});\r\nDrawerTitle.displayName = 'DrawerTitle';\r\n\r\n/* ─── Description ─── */\r\nconst DrawerDescription = React.forwardRef<\r\n HTMLParagraphElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>, 'className'> & {\r\n className?: string;\r\n }\r\n>(({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return (\r\n <BaseDialog.Description ref={ref} className={slots.description({ className })} {...props} />\r\n );\r\n});\r\nDrawerDescription.displayName = 'DrawerDescription';\r\n\r\n/* ─── Body (scrollable content area) ─── */\r\nconst DrawerBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <div ref={ref} className={slots.body({ className })} {...props} />;\r\n },\r\n);\r\nDrawerBody.displayName = 'DrawerBody';\r\n\r\n/* ─── Footer ─── */\r\nconst DrawerFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <div ref={ref} className={slots.footer({ className })} {...props} />;\r\n },\r\n);\r\nDrawerFooter.displayName = 'DrawerFooter';\r\n\r\nexport {\r\n Drawer,\r\n DrawerTrigger,\r\n DrawerContent,\r\n DrawerHeader,\r\n DrawerTitle,\r\n DrawerDescription,\r\n DrawerBody,\r\n DrawerFooter,\r\n DrawerClose,\r\n};\r\nexport type { DrawerContentProps, DrawerHeaderProps };\r\n"
|
|
343
343
|
}
|
|
344
344
|
]
|
|
345
345
|
},
|
package/scripts/ui-cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import readline from 'readline';
|
|
|
6
6
|
|
|
7
7
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
8
8
|
|
|
9
|
-
const VERSION = '0.3.
|
|
9
|
+
const VERSION = '0.3.11';
|
|
10
10
|
const REGISTRY_LOCAL = './registry.json';
|
|
11
11
|
const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json';
|
|
12
12
|
|