@djangocfg/ui-core 2.1.412 → 2.1.415

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 (51) hide show
  1. package/package.json +4 -4
  2. package/src/components/data/avatar-group/index.tsx +224 -0
  3. package/src/components/data/badge-overflow/index.tsx +259 -0
  4. package/src/components/data/circular-progress/index.tsx +358 -0
  5. package/src/components/data/relative-time-card/index.tsx +191 -0
  6. package/src/components/data/stat/index.tsx +140 -0
  7. package/src/components/data/status/index.tsx +80 -0
  8. package/src/components/effects/GlowBackground.tsx +9 -1
  9. package/src/components/effects/swap/index.tsx +289 -0
  10. package/src/components/feedback/banner/index.tsx +693 -0
  11. package/src/components/forms/checkbox-group/index.tsx +243 -0
  12. package/src/components/forms/editable/index.tsx +420 -0
  13. package/src/components/forms/input-otp/index.tsx +12 -3
  14. package/src/components/forms/mask-input/index.tsx +466 -0
  15. package/src/components/forms/otp/index.tsx +12 -8
  16. package/src/components/forms/segmented-input/index.tsx +319 -0
  17. package/src/components/forms/tags-input/index.tsx +896 -0
  18. package/src/components/forms/time-picker/index.tsx +285 -0
  19. package/src/components/index.ts +51 -0
  20. package/src/components/layout/key-value/index.tsx +884 -0
  21. package/src/components/layout/stack/index.tsx +349 -0
  22. package/src/components/navigation/context-menu/index.tsx +9 -6
  23. package/src/components/navigation/stepper/index.tsx +1307 -0
  24. package/src/components/select/multi-select-pro-async.tsx +11 -2
  25. package/src/components/select/multi-select-pro.tsx +11 -2
  26. package/src/components/specialized/presence/index.tsx +181 -0
  27. package/src/components/specialized/primitive/index.tsx +83 -0
  28. package/src/components/specialized/visually-hidden/index.tsx +19 -0
  29. package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
  30. package/src/hooks/dom/index.ts +4 -0
  31. package/src/hooks/dom/useFormReset.ts +49 -0
  32. package/src/hooks/dom/useLayoutEffect.ts +16 -0
  33. package/src/hooks/dom/useSize.ts +57 -0
  34. package/src/hooks/state/index.ts +4 -0
  35. package/src/hooks/state/useCallbackRef.ts +25 -0
  36. package/src/hooks/state/usePrevious.ts +20 -0
  37. package/src/hooks/state/useStateMachine.ts +29 -0
  38. package/src/lib/compose-event-handlers.ts +22 -0
  39. package/src/lib/compose-refs.ts +65 -0
  40. package/src/lib/create-context.tsx +62 -0
  41. package/src/lib/get-element-ref.ts +33 -0
  42. package/src/lib/index.ts +5 -0
  43. package/src/lib/styles.ts +103 -0
  44. package/src/styles/README.md +43 -0
  45. package/src/styles/palette/utils.ts +15 -5
  46. package/src/styles/utilities/animations.css +135 -0
  47. package/src/styles/utilities/display.css +62 -0
  48. package/src/styles/utilities/glass.css +57 -0
  49. package/src/styles/utilities/marquee.css +69 -0
  50. package/src/styles/utilities/step.css +25 -0
  51. package/src/styles/utilities.css +6 -259
@@ -0,0 +1,285 @@
1
+ "use client"
2
+
3
+ import { Clock } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../../lib/utils";
7
+ import { Button } from "../../forms/button";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "../../overlay/popover";
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export interface TimePickerProps {
15
+ /** Selected time value as HH:mm string */
16
+ value?: string;
17
+ /** Default uncontrolled value */
18
+ defaultValue?: string;
19
+ /** Callback when time changes (HH:mm) */
20
+ onChange?: (value: string) => void;
21
+ /** Placeholder text */
22
+ placeholder?: string;
23
+ /** Disable the picker */
24
+ disabled?: boolean;
25
+ /** Use 12-hour format with AM/PM */
26
+ use12Hour?: boolean;
27
+ /** Minimum time (HH:mm) */
28
+ min?: string;
29
+ /** Maximum time (HH:mm) */
30
+ max?: string;
31
+ /** Step in minutes for minute selector @default 1 */
32
+ minuteStep?: number;
33
+ /** Additional class for trigger button */
34
+ className?: string;
35
+ /** Button variant */
36
+ variant?: "default" | "outline" | "ghost";
37
+ /** Align popover */
38
+ align?: "start" | "center" | "end";
39
+ /** Form field name */
40
+ name?: string;
41
+ }
42
+
43
+ // =============================================================================
44
+ // Utilities
45
+ // =============================================================================
46
+
47
+ function parseTime(timeStr: string): { hours: number; minutes: number } | null {
48
+ const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
49
+ if (!match) return null;
50
+ const hours = parseInt(match[1], 10);
51
+ const minutes = parseInt(match[2], 10);
52
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
53
+ return { hours, minutes };
54
+ }
55
+
56
+ function formatTime(hours: number, minutes: number): string {
57
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
58
+ }
59
+
60
+ function format12Hour(hours: number, minutes: number): string {
61
+ const period = hours >= 12 ? "PM" : "AM";
62
+ const h = hours % 12 || 12;
63
+ return `${String(h).padStart(2, "0")}:${String(minutes).padStart(2, "0")} ${period}`;
64
+ }
65
+
66
+ function isTimeInRange(time: string, min?: string, max?: string): boolean {
67
+ if (min && time < min) return false;
68
+ if (max && time > max) return false;
69
+ return true;
70
+ }
71
+
72
+ // =============================================================================
73
+ // Component
74
+ // =============================================================================
75
+
76
+ const TimePicker = React.forwardRef<HTMLButtonElement, TimePickerProps>(
77
+ (
78
+ {
79
+ value: controlledValue,
80
+ defaultValue,
81
+ onChange,
82
+ placeholder = "Select time",
83
+ disabled = false,
84
+ use12Hour = false,
85
+ min,
86
+ max,
87
+ minuteStep = 1,
88
+ className,
89
+ variant = "outline",
90
+ align = "start",
91
+ name,
92
+ },
93
+ ref
94
+ ) => {
95
+ const isControlled = controlledValue !== undefined;
96
+ const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
97
+ const resolvedValue = isControlled ? controlledValue : internalValue;
98
+
99
+ const [open, setOpen] = React.useState(false);
100
+ const [tempHours, setTempHours] = React.useState(0);
101
+ const [tempMinutes, setTempMinutes] = React.useState(0);
102
+ const [tempPeriod, setTempPeriod] = React.useState<"AM" | "PM">("AM");
103
+
104
+ const parsed = React.useMemo(() => parseTime(resolvedValue), [resolvedValue]);
105
+
106
+ // Sync temp values when opening popover
107
+ React.useEffect(() => {
108
+ if (open && parsed) {
109
+ setTempHours(parsed.hours);
110
+ setTempMinutes(parsed.minutes);
111
+ setTempPeriod(parsed.hours >= 12 ? "PM" : "AM");
112
+ } else if (open) {
113
+ setTempHours(12);
114
+ setTempMinutes(0);
115
+ setTempPeriod("AM");
116
+ }
117
+ }, [open, parsed]);
118
+
119
+ const updateValue = React.useCallback(
120
+ (hours: number, minutes: number) => {
121
+ const formatted = formatTime(hours, minutes);
122
+ if (!isControlled) {
123
+ setInternalValue(formatted);
124
+ }
125
+ onChange?.(formatted);
126
+ },
127
+ [isControlled, onChange]
128
+ );
129
+
130
+ const handleApply = React.useCallback(() => {
131
+ let hours = tempHours;
132
+ if (use12Hour) {
133
+ if (tempPeriod === "PM" && hours !== 12) hours += 12;
134
+ if (tempPeriod === "AM" && hours === 12) hours = 0;
135
+ }
136
+ const formatted = formatTime(hours, tempMinutes);
137
+ if (isTimeInRange(formatted, min, max)) {
138
+ updateValue(hours, tempMinutes);
139
+ setOpen(false);
140
+ }
141
+ }, [tempHours, tempMinutes, tempPeriod, use12Hour, min, max, updateValue]);
142
+
143
+ const displayValue = React.useMemo(() => {
144
+ if (!parsed) return placeholder;
145
+ if (use12Hour) {
146
+ return format12Hour(parsed.hours, parsed.minutes);
147
+ }
148
+ return formatTime(parsed.hours, parsed.minutes);
149
+ }, [parsed, use12Hour, placeholder]);
150
+
151
+ // Generate hour options
152
+ const hourOptions = React.useMemo(() => {
153
+ if (use12Hour) {
154
+ return Array.from({ length: 12 }, (_, i) => i + 1);
155
+ }
156
+ return Array.from({ length: 24 }, (_, i) => i);
157
+ }, [use12Hour]);
158
+
159
+ // Generate minute options
160
+ const minuteOptions = React.useMemo(() => {
161
+ const steps = Math.floor(60 / minuteStep);
162
+ return Array.from({ length: steps }, (_, i) => i * minuteStep);
163
+ }, [minuteStep]);
164
+
165
+ const isFormControl = React.useRef(false);
166
+ const rootRef = React.useRef<HTMLDivElement>(null);
167
+ React.useImperativeHandle(ref, () => rootRef.current as unknown as HTMLButtonElement);
168
+
169
+ React.useEffect(() => {
170
+ if (rootRef.current) {
171
+ isFormControl.current = !!rootRef.current.closest("form");
172
+ }
173
+ }, []);
174
+
175
+ return (
176
+ <>
177
+ <Popover open={open} onOpenChange={setOpen}>
178
+ <PopoverTrigger asChild>
179
+ <Button
180
+ ref={rootRef as unknown as React.Ref<HTMLButtonElement>}
181
+ variant={variant}
182
+ disabled={disabled}
183
+ className={cn(
184
+ "w-full justify-start text-left font-normal",
185
+ !resolvedValue && "text-muted-foreground",
186
+ className
187
+ )}
188
+ >
189
+ <Clock className="mr-2 h-4 w-4" />
190
+ {displayValue}
191
+ </Button>
192
+ </PopoverTrigger>
193
+ <PopoverContent className="w-auto p-3" align={align}>
194
+ <div className="flex flex-col gap-3">
195
+ <div className="flex items-center gap-2">
196
+ {/* Hours */}
197
+ <div className="flex flex-col gap-1">
198
+ <span className="text-xs text-muted-foreground text-center">Hour</span>
199
+ <div className="flex flex-col max-h-48 overflow-y-auto border rounded-[var(--radius)]">
200
+ {hourOptions.map((h) => {
201
+ const selected = use12Hour
202
+ ? (tempHours % 12 || 12) === h
203
+ : tempHours === h;
204
+ return (
205
+ <button
206
+ key={h}
207
+ type="button"
208
+ onClick={() => setTempHours(use12Hour ? h : h)}
209
+ className={cn(
210
+ "px-3 py-1.5 text-sm text-left hover:bg-accent transition-colors",
211
+ selected && "bg-primary text-primary-foreground hover:bg-primary/90"
212
+ )}
213
+ >
214
+ {String(h).padStart(2, "0")}
215
+ </button>
216
+ );
217
+ })}
218
+ </div>
219
+ </div>
220
+
221
+ <span className="text-lg font-medium text-muted-foreground">:</span>
222
+
223
+ {/* Minutes */}
224
+ <div className="flex flex-col gap-1">
225
+ <span className="text-xs text-muted-foreground text-center">Min</span>
226
+ <div className="flex flex-col max-h-48 overflow-y-auto border rounded-[var(--radius)]">
227
+ {minuteOptions.map((m) => {
228
+ const selected = tempMinutes === m;
229
+ return (
230
+ <button
231
+ key={m}
232
+ type="button"
233
+ onClick={() => setTempMinutes(m)}
234
+ className={cn(
235
+ "px-3 py-1.5 text-sm text-left hover:bg-accent transition-colors",
236
+ selected && "bg-primary text-primary-foreground hover:bg-primary/90"
237
+ )}
238
+ >
239
+ {String(m).padStart(2, "0")}
240
+ </button>
241
+ );
242
+ })}
243
+ </div>
244
+ </div>
245
+
246
+ {/* AM/PM */}
247
+ {use12Hour && (
248
+ <div className="flex flex-col gap-1">
249
+ <span className="text-xs text-muted-foreground text-center">&nbsp;</span>
250
+ <div className="flex flex-col border rounded-[var(--radius)]">
251
+ {(["AM", "PM"] as const).map((p) => (
252
+ <button
253
+ key={p}
254
+ type="button"
255
+ onClick={() => setTempPeriod(p)}
256
+ className={cn(
257
+ "px-3 py-1.5 text-sm text-left hover:bg-accent transition-colors",
258
+ tempPeriod === p && "bg-primary text-primary-foreground hover:bg-primary/90"
259
+ )}
260
+ >
261
+ {p}
262
+ </button>
263
+ ))}
264
+ </div>
265
+ </div>
266
+ )}
267
+ </div>
268
+
269
+ <Button size="sm" onClick={handleApply} className="w-full">
270
+ Apply
271
+ </Button>
272
+ </div>
273
+ </PopoverContent>
274
+ </Popover>
275
+ {isFormControl.current && name && (
276
+ <input type="hidden" name={name} value={resolvedValue} disabled={disabled} />
277
+ )}
278
+ </>
279
+ );
280
+ }
281
+ );
282
+
283
+ TimePicker.displayName = "TimePicker";
284
+
285
+ export { TimePicker };
@@ -9,6 +9,8 @@ export { Input } from './forms/input';
9
9
  export { Textarea } from './forms/textarea';
10
10
  export { Label } from './forms/label';
11
11
  export { Checkbox } from './forms/checkbox';
12
+ export { CheckboxGroup, CheckboxGroupDescription, CheckboxGroupItem, CheckboxGroupLabel, CheckboxGroupList, CheckboxGroupMessage } from './forms/checkbox-group';
13
+ export type { CheckboxGroupProps, CheckboxGroupItemProps, CheckboxGroupListProps } from './forms/checkbox-group';
12
14
  export { RadioGroup, RadioGroupItem } from './forms/radio-group';
13
15
  export { Switch } from './forms/switch';
14
16
  export { Slider } from './forms/slider';
@@ -24,6 +26,26 @@ export { ButtonGroup as ButtonGroupComponent } from './forms/button-group';
24
26
  export { DownloadButton } from './forms/button-download';
25
27
  export type { DownloadButtonProps } from './forms/button-download';
26
28
 
29
+ // Mask Input
30
+ export { MaskInput } from './forms/mask-input';
31
+ export type { MaskInputProps, MaskDefinition } from './forms/mask-input';
32
+
33
+ // Segmented Input
34
+ export { SegmentedInput } from './forms/segmented-input';
35
+ export type { SegmentedInputProps } from './forms/segmented-input';
36
+
37
+ // Tags Input
38
+ export { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemText, TagsInputItemDelete } from './forms/tags-input';
39
+ export type { TagsInputRootProps, TagsInputInputProps, TagsInputItemProps, TagsInputItemTextProps, TagsInputItemDeleteProps } from './forms/tags-input';
40
+
41
+ // Time Picker
42
+ export { TimePicker } from './forms/time-picker';
43
+ export type { TimePickerProps } from './forms/time-picker';
44
+
45
+ // Editable
46
+ export { Editable, EditablePreview, EditableInput, EditableTextarea } from './forms/editable';
47
+ export type { EditableRootProps, EditablePreviewProps, EditableInputProps, EditableTextareaProps } from './forms/editable';
48
+
27
49
  // ─────────────────────────────────────────────────────────────────────────────
28
50
  // Field (advanced form layout)
29
51
  // ─────────────────────────────────────────────────────────────────────────────
@@ -119,6 +141,8 @@ export {
119
141
  SidebarTrigger,
120
142
  useSidebar,
121
143
  } from './navigation/sidebar';
144
+ export { Stepper, StepperContent, StepperDescription, StepperIndicator, StepperItem, StepperList, StepperNext, StepperPrev, StepperSeparator, StepperTitle, StepperTrigger, useStepper } from './navigation/stepper';
145
+ export type { StepperProps } from './navigation/stepper';
122
146
 
123
147
  // ─────────────────────────────────────────────────────────────────────────────
124
148
  // Layout
@@ -133,6 +157,10 @@ export { ResizableHandle, ResizablePanel, ResizablePanelGroup, useResizableDragg
133
157
  export type { ResizableHandleProps, ImperativePanelHandle } from './layout/resizable';
134
158
  export { Sticky } from './layout/sticky';
135
159
  export { Section, SectionHeader } from './layout/section';
160
+ export { Stack, StackItem } from './layout/stack';
161
+ export type { StackProps } from './layout/stack';
162
+ export { KeyValue, KeyValueAdd, KeyValueError, KeyValueItem, KeyValueKeyInput, KeyValueList, KeyValueRemove, KeyValueValueInput, useKeyValueStore } from './layout/key-value';
163
+ export type { KeyValueProps, KeyValueItemData } from './layout/key-value';
136
164
 
137
165
  // ─────────────────────────────────────────────────────────────────────────────
138
166
  // Data Display
@@ -148,6 +176,18 @@ export { DatePicker, DateRangePicker } from './data/calendar';
148
176
  export type { DatePickerProps, DateRangePickerProps, DateRange } from './data/calendar';
149
177
  export { Toggle, toggleVariants } from './data/toggle';
150
178
  export { ToggleGroup, ToggleGroupItem } from './data/toggle-group';
179
+ export { AvatarGroup } from './data/avatar-group';
180
+ export type { AvatarGroupProps } from './data/avatar-group';
181
+ export { BadgeOverflow } from './data/badge-overflow';
182
+ export type { BadgeOverflowProps } from './data/badge-overflow';
183
+ export { Stat, StatDescription, StatIndicator, StatLabel, StatSeparator, StatTrend, StatValue, statIndicatorVariants } from './data/stat';
184
+ export type { StatIndicatorProps } from './data/stat';
185
+ export { Status, StatusIndicator, StatusLabel, statusVariants } from './data/status';
186
+ export type { StatusProps } from './data/status';
187
+ export { CircularProgress, CircularProgressCombined, CircularProgressIndicator, CircularProgressRange, CircularProgressTrack, CircularProgressValueText } from './data/circular-progress';
188
+ export type { CircularProgressProps } from './data/circular-progress';
189
+ export { RelativeTimeCard, relativeTimeCardVariants } from './data/relative-time-card';
190
+ export type { RelativeTimeCardProps } from './data/relative-time-card';
151
191
 
152
192
  // ─────────────────────────────────────────────────────────────────────────────
153
193
  // Chart Components
@@ -163,6 +203,8 @@ export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMe
163
203
  export { Preloader, PreloaderSkeleton } from './feedback/preloader';
164
204
  export type { PreloaderProps, PreloaderSkeletonProps } from './feedback/preloader';
165
205
  export { Toaster } from './feedback/sonner';
206
+ export { Banner, BannerActions, BannerClose, BannerContent, BannerDescription, BannerIcon, Banners, BannerTitle, useBanner, useBanners } from './feedback/banner';
207
+ export type { BannerProps, BannersProps, BannerVariant } from './feedback/banner';
166
208
 
167
209
  // ─────────────────────────────────────────────────────────────────────────────
168
210
  // Boundary
@@ -189,6 +231,13 @@ export type { TokenIconProps, TokenSymbol, TokenCategory } from './specialized/t
189
231
  export { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter } from './specialized/item';
190
232
  export { Portal } from './specialized/portal';
191
233
  export type { PortalProps } from './specialized/portal';
234
+ export { Presence } from './specialized/presence';
235
+ export type { PresenceProps } from './specialized/presence';
236
+ export { VisuallyHidden } from './specialized/visually-hidden';
237
+ export { VisuallyHiddenInput } from './specialized/visually-hidden-input';
238
+ export type { VisuallyHiddenInputProps, InputValue as VisuallyHiddenInputValue } from './specialized/visually-hidden-input';
239
+ export { Primitive, dispatchDiscreteCustomEvent } from './specialized/primitive';
240
+ export type { PrimitivePropsWithRef } from './specialized/primitive';
192
241
  export { ImageWithFallback } from './specialized/image-with-fallback';
193
242
  export type { ImageWithFallbackProps } from './specialized/image-with-fallback';
194
243
  export { Flag, LanguageFlag, LANGUAGE_TO_COUNTRY, getLanguageCountryCode, FLAG_COMPONENTS } from './specialized/flag';
@@ -199,3 +248,5 @@ export type { FlagProps, LanguageFlagProps, FlagSvgComponent } from './specializ
199
248
  // ─────────────────────────────────────────────────────────────────────────────
200
249
  export { GlowBackground } from './effects';
201
250
  export type { GlowBackgroundProps } from './effects';
251
+ export { Swap, SwapOff, SwapOn, useSwap } from './effects/swap';
252
+ export type { SwapProps } from './effects/swap';