@djangocfg/ui-core 2.1.4 → 2.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -102,7 +102,7 @@
102
102
  "vaul": "1.1.2"
103
103
  },
104
104
  "devDependencies": {
105
- "@djangocfg/typescript-config": "^2.1.4",
105
+ "@djangocfg/typescript-config": "^2.1.5",
106
106
  "@types/node": "^24.7.2",
107
107
  "@types/react": "^19.1.0",
108
108
  "@types/react-dom": "^19.1.0",
@@ -0,0 +1,213 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ChevronDownIcon,
6
+ ChevronLeftIcon,
7
+ ChevronRightIcon,
8
+ } from "lucide-react"
9
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10
+
11
+ import { cn } from "../../lib/utils"
12
+ import { Button, buttonVariants } from "../../components/button"
13
+
14
+ function Calendar({
15
+ className,
16
+ classNames,
17
+ showOutsideDays = true,
18
+ captionLayout = "label",
19
+ buttonVariant = "ghost",
20
+ formatters,
21
+ components,
22
+ ...props
23
+ }: React.ComponentProps<typeof DayPicker> & {
24
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"]
25
+ }) {
26
+ const defaultClassNames = getDefaultClassNames()
27
+
28
+ return (
29
+ <DayPicker
30
+ showOutsideDays={showOutsideDays}
31
+ className={cn(
32
+ "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
33
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
34
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35
+ className
36
+ )}
37
+ captionLayout={captionLayout}
38
+ formatters={{
39
+ formatMonthDropdown: (date) =>
40
+ date.toLocaleString("default", { month: "short" }),
41
+ ...formatters,
42
+ }}
43
+ classNames={{
44
+ root: cn("w-fit", defaultClassNames.root),
45
+ months: cn(
46
+ "relative flex flex-col gap-4 md:flex-row",
47
+ defaultClassNames.months
48
+ ),
49
+ month: cn("flex w-full min-w-[calc(var(--cell-size)*7)] flex-col gap-4", defaultClassNames.month),
50
+ nav: cn(
51
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
52
+ defaultClassNames.nav
53
+ ),
54
+ button_previous: cn(
55
+ buttonVariants({ variant: buttonVariant }),
56
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
57
+ defaultClassNames.button_previous
58
+ ),
59
+ button_next: cn(
60
+ buttonVariants({ variant: buttonVariant }),
61
+ "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
62
+ defaultClassNames.button_next
63
+ ),
64
+ month_caption: cn(
65
+ "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
66
+ defaultClassNames.month_caption
67
+ ),
68
+ dropdowns: cn(
69
+ "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
70
+ defaultClassNames.dropdowns
71
+ ),
72
+ dropdown_root: cn(
73
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
74
+ defaultClassNames.dropdown_root
75
+ ),
76
+ dropdown: cn(
77
+ "bg-popover absolute inset-0 opacity-0",
78
+ defaultClassNames.dropdown
79
+ ),
80
+ caption_label: cn(
81
+ "select-none font-medium",
82
+ captionLayout === "label"
83
+ ? "text-sm"
84
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
85
+ defaultClassNames.caption_label
86
+ ),
87
+ table: "w-full border-collapse",
88
+ weekdays: cn("flex", defaultClassNames.weekdays),
89
+ weekday: cn(
90
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
91
+ defaultClassNames.weekday
92
+ ),
93
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
94
+ week_number_header: cn(
95
+ "w-[--cell-size] select-none",
96
+ defaultClassNames.week_number_header
97
+ ),
98
+ week_number: cn(
99
+ "text-muted-foreground select-none text-[0.8rem]",
100
+ defaultClassNames.week_number
101
+ ),
102
+ day: cn(
103
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
104
+ defaultClassNames.day
105
+ ),
106
+ range_start: cn(
107
+ "bg-accent rounded-l-md",
108
+ defaultClassNames.range_start
109
+ ),
110
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
111
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
112
+ today: cn(
113
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
114
+ defaultClassNames.today
115
+ ),
116
+ outside: cn(
117
+ "text-muted-foreground aria-selected:text-muted-foreground",
118
+ defaultClassNames.outside
119
+ ),
120
+ disabled: cn(
121
+ "text-muted-foreground opacity-50",
122
+ defaultClassNames.disabled
123
+ ),
124
+ hidden: cn("invisible", defaultClassNames.hidden),
125
+ ...classNames,
126
+ }}
127
+ components={{
128
+ Root: ({ className, rootRef, ...props }) => {
129
+ return (
130
+ <div
131
+ data-slot="calendar"
132
+ ref={rootRef}
133
+ className={cn(className)}
134
+ {...props}
135
+ />
136
+ )
137
+ },
138
+ Chevron: ({ className, orientation, ...props }) => {
139
+ if (orientation === "left") {
140
+ return (
141
+ <ChevronLeftIcon className={cn("size-4", className)} {...props} />
142
+ )
143
+ }
144
+
145
+ if (orientation === "right") {
146
+ return (
147
+ <ChevronRightIcon
148
+ className={cn("size-4", className)}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ return (
155
+ <ChevronDownIcon className={cn("size-4", className)} {...props} />
156
+ )
157
+ },
158
+ DayButton: CalendarDayButton,
159
+ WeekNumber: ({ children, ...props }) => {
160
+ return (
161
+ <td {...props}>
162
+ <div className="flex size-[--cell-size] items-center justify-center text-center">
163
+ {children}
164
+ </div>
165
+ </td>
166
+ )
167
+ },
168
+ ...components,
169
+ }}
170
+ {...props}
171
+ />
172
+ )
173
+ }
174
+
175
+ function CalendarDayButton({
176
+ className,
177
+ day,
178
+ modifiers,
179
+ ...props
180
+ }: React.ComponentProps<typeof DayButton>) {
181
+ const defaultClassNames = getDefaultClassNames()
182
+
183
+ const ref = React.useRef<HTMLButtonElement>(null)
184
+ React.useEffect(() => {
185
+ if (modifiers.focused) ref.current?.focus()
186
+ }, [modifiers.focused])
187
+
188
+ return (
189
+ <Button
190
+ ref={ref}
191
+ variant="ghost"
192
+ size="icon"
193
+ data-day={day.date.toLocaleDateString()}
194
+ data-selected-single={
195
+ modifiers.selected &&
196
+ !modifiers.range_start &&
197
+ !modifiers.range_end &&
198
+ !modifiers.range_middle
199
+ }
200
+ data-range-start={modifiers.range_start}
201
+ data-range-end={modifiers.range_end}
202
+ data-range-middle={modifiers.range_middle}
203
+ className={cn(
204
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
205
+ defaultClassNames.day,
206
+ className
207
+ )}
208
+ {...props}
209
+ />
210
+ )
211
+ }
212
+
213
+ export { Calendar, CalendarDayButton }
@@ -0,0 +1,191 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { format } from 'date-fns'
5
+ import { CalendarIcon } from 'lucide-react'
6
+ import type { SelectSingleEventHandler } from 'react-day-picker'
7
+
8
+ import { cn } from '../../lib/utils'
9
+ import { Button } from '../../components/button'
10
+ import { Calendar } from './calendar'
11
+ import { Popover, PopoverContent, PopoverTrigger } from '../../components/popover'
12
+
13
+ // =============================================================================
14
+ // DatePicker - Single date selection with popover
15
+ // =============================================================================
16
+
17
+ export interface DatePickerProps {
18
+ /** Selected date */
19
+ value?: Date
20
+ /** Callback when date changes */
21
+ onChange?: (date: Date | undefined) => void
22
+ /** Placeholder text when no date selected */
23
+ placeholder?: string
24
+ /** Date format string (date-fns format) */
25
+ dateFormat?: string
26
+ /** Disable the picker */
27
+ disabled?: boolean
28
+ /** Minimum selectable date */
29
+ fromDate?: Date
30
+ /** Maximum selectable date */
31
+ toDate?: Date
32
+ /** Additional class names for the trigger button */
33
+ className?: string
34
+ /** Button variant */
35
+ variant?: 'default' | 'outline' | 'ghost'
36
+ /** Align popover */
37
+ align?: 'start' | 'center' | 'end'
38
+ }
39
+
40
+ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
41
+ (
42
+ {
43
+ value,
44
+ onChange,
45
+ placeholder = 'Pick a date',
46
+ dateFormat = 'PPP',
47
+ disabled = false,
48
+ fromDate,
49
+ toDate,
50
+ className,
51
+ variant = 'outline',
52
+ align = 'start',
53
+ },
54
+ ref
55
+ ) => {
56
+ const [open, setOpen] = React.useState(false)
57
+
58
+ const handleSelect: SelectSingleEventHandler = (date) => {
59
+ onChange?.(date)
60
+ setOpen(false)
61
+ }
62
+
63
+ return (
64
+ <Popover open={open} onOpenChange={setOpen}>
65
+ <PopoverTrigger asChild>
66
+ <Button
67
+ ref={ref}
68
+ variant={variant}
69
+ disabled={disabled}
70
+ className={cn(
71
+ 'w-full justify-start text-left font-normal',
72
+ !value && 'text-muted-foreground',
73
+ className
74
+ )}
75
+ >
76
+ <CalendarIcon className="mr-2 h-4 w-4" />
77
+ {value ? format(value, dateFormat) : placeholder}
78
+ </Button>
79
+ </PopoverTrigger>
80
+ <PopoverContent className="w-auto p-0" align={align}>
81
+ <Calendar
82
+ mode="single"
83
+ selected={value}
84
+ onSelect={handleSelect}
85
+ disabled={disabled}
86
+ fromDate={fromDate}
87
+ toDate={toDate}
88
+ initialFocus
89
+ />
90
+ </PopoverContent>
91
+ </Popover>
92
+ )
93
+ }
94
+ )
95
+ DatePicker.displayName = 'DatePicker'
96
+
97
+ // =============================================================================
98
+ // DateRangePicker - Date range selection with popover
99
+ // =============================================================================
100
+
101
+ export interface DateRange {
102
+ from?: Date
103
+ to?: Date
104
+ }
105
+
106
+ export interface DateRangePickerProps {
107
+ /** Selected date range */
108
+ value?: DateRange
109
+ /** Callback when range changes */
110
+ onChange?: (range: DateRange | undefined) => void
111
+ /** Placeholder text when no range selected */
112
+ placeholder?: string
113
+ /** Date format string (date-fns format) */
114
+ dateFormat?: string
115
+ /** Disable the picker */
116
+ disabled?: boolean
117
+ /** Minimum selectable date */
118
+ fromDate?: Date
119
+ /** Maximum selectable date */
120
+ toDate?: Date
121
+ /** Number of months to display */
122
+ numberOfMonths?: number
123
+ /** Additional class names for the trigger button */
124
+ className?: string
125
+ /** Button variant */
126
+ variant?: 'default' | 'outline' | 'ghost'
127
+ /** Align popover */
128
+ align?: 'start' | 'center' | 'end'
129
+ }
130
+
131
+ const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
132
+ (
133
+ {
134
+ value,
135
+ onChange,
136
+ placeholder = 'Pick a date range',
137
+ dateFormat = 'LLL dd, y',
138
+ disabled = false,
139
+ fromDate,
140
+ toDate,
141
+ numberOfMonths = 2,
142
+ className,
143
+ variant = 'outline',
144
+ align = 'start',
145
+ },
146
+ ref
147
+ ) => {
148
+ const [open, setOpen] = React.useState(false)
149
+
150
+ const formatRange = () => {
151
+ if (!value?.from) return placeholder
152
+ if (!value.to) return format(value.from, dateFormat)
153
+ return `${format(value.from, dateFormat)} - ${format(value.to, dateFormat)}`
154
+ }
155
+
156
+ return (
157
+ <Popover open={open} onOpenChange={setOpen}>
158
+ <PopoverTrigger asChild>
159
+ <Button
160
+ ref={ref}
161
+ variant={variant}
162
+ disabled={disabled}
163
+ className={cn(
164
+ 'w-full justify-start text-left font-normal',
165
+ !value?.from && 'text-muted-foreground',
166
+ className
167
+ )}
168
+ >
169
+ <CalendarIcon className="mr-2 h-4 w-4" />
170
+ {formatRange()}
171
+ </Button>
172
+ </PopoverTrigger>
173
+ <PopoverContent className="w-auto p-0" align={align}>
174
+ <Calendar
175
+ mode="range"
176
+ selected={value?.from ? { from: value.from, to: value.to } : undefined}
177
+ onSelect={onChange}
178
+ disabled={disabled}
179
+ fromDate={fromDate}
180
+ toDate={toDate}
181
+ numberOfMonths={numberOfMonths}
182
+ initialFocus
183
+ />
184
+ </PopoverContent>
185
+ </Popover>
186
+ )
187
+ }
188
+ )
189
+ DateRangePicker.displayName = 'DateRangePicker'
190
+
191
+ export { DatePicker, DateRangePicker }
@@ -8,8 +8,8 @@ import {
8
8
  } from "lucide-react"
9
9
  import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10
10
 
11
- import { cn } from "../lib/utils"
12
- import { Button, buttonVariants } from "./button"
11
+ import { cn } from "../../lib/utils"
12
+ import { Button, buttonVariants } from "../button"
13
13
 
14
14
  function Calendar({
15
15
  className,
@@ -41,12 +41,12 @@ function Calendar({
41
41
  ...formatters,
42
42
  }}
43
43
  classNames={{
44
- root: cn("w-full", defaultClassNames.root),
44
+ root: cn("w-fit", defaultClassNames.root),
45
45
  months: cn(
46
46
  "relative flex flex-col gap-4 md:flex-row",
47
47
  defaultClassNames.months
48
48
  ),
49
- month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
49
+ month: cn("flex w-full min-w-[calc(var(--cell-size)*7)] flex-col gap-4", defaultClassNames.month),
50
50
  nav: cn(
51
51
  "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
52
52
  defaultClassNames.nav
@@ -0,0 +1,189 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { format } from 'date-fns'
5
+ import { CalendarIcon } from 'lucide-react'
6
+ import type { SelectSingleEventHandler, DateRange as RDPDateRange } from 'react-day-picker'
7
+
8
+ import { cn } from '../../lib/utils'
9
+ import { Button } from '../button'
10
+ import { Calendar } from './calendar'
11
+ import { Popover, PopoverContent, PopoverTrigger } from '../popover'
12
+
13
+ // =============================================================================
14
+ // DatePicker - Single date selection with popover
15
+ // =============================================================================
16
+
17
+ export interface DatePickerProps {
18
+ /** Selected date */
19
+ value?: Date
20
+ /** Callback when date changes */
21
+ onChange?: (date: Date | undefined) => void
22
+ /** Placeholder text when no date selected */
23
+ placeholder?: string
24
+ /** Date format string (date-fns format) */
25
+ dateFormat?: string
26
+ /** Disable the picker */
27
+ disabled?: boolean
28
+ /** Minimum selectable date */
29
+ fromDate?: Date
30
+ /** Maximum selectable date */
31
+ toDate?: Date
32
+ /** Additional class names for the trigger button */
33
+ className?: string
34
+ /** Button variant */
35
+ variant?: 'default' | 'outline' | 'ghost'
36
+ /** Align popover */
37
+ align?: 'start' | 'center' | 'end'
38
+ }
39
+
40
+ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
41
+ (
42
+ {
43
+ value,
44
+ onChange,
45
+ placeholder = 'Pick a date',
46
+ dateFormat = 'PPP',
47
+ disabled = false,
48
+ fromDate,
49
+ toDate,
50
+ className,
51
+ variant = 'outline',
52
+ align = 'start',
53
+ },
54
+ ref
55
+ ) => {
56
+ const [open, setOpen] = React.useState(false)
57
+
58
+ const handleSelect: SelectSingleEventHandler = (date) => {
59
+ onChange?.(date)
60
+ setOpen(false)
61
+ }
62
+
63
+ return (
64
+ <Popover open={open} onOpenChange={setOpen}>
65
+ <PopoverTrigger asChild>
66
+ <Button
67
+ ref={ref}
68
+ variant={variant}
69
+ disabled={disabled}
70
+ className={cn(
71
+ 'w-full justify-start text-left font-normal',
72
+ !value && 'text-muted-foreground',
73
+ className
74
+ )}
75
+ >
76
+ <CalendarIcon className="mr-2 h-4 w-4" />
77
+ {value ? format(value, dateFormat) : placeholder}
78
+ </Button>
79
+ </PopoverTrigger>
80
+ <PopoverContent className="w-auto p-0" align={align}>
81
+ <Calendar
82
+ mode="single"
83
+ selected={value}
84
+ onSelect={handleSelect}
85
+ disabled={disabled}
86
+ fromDate={fromDate}
87
+ toDate={toDate}
88
+ initialFocus
89
+ />
90
+ </PopoverContent>
91
+ </Popover>
92
+ )
93
+ }
94
+ )
95
+ DatePicker.displayName = 'DatePicker'
96
+
97
+ // =============================================================================
98
+ // DateRangePicker - Date range selection with popover
99
+ // =============================================================================
100
+
101
+ /** Date range type - re-exported from react-day-picker for convenience */
102
+ export type DateRange = RDPDateRange
103
+
104
+ export interface DateRangePickerProps {
105
+ /** Selected date range */
106
+ value?: DateRange
107
+ /** Callback when range changes */
108
+ onChange?: (range: DateRange | undefined) => void
109
+ /** Placeholder text when no range selected */
110
+ placeholder?: string
111
+ /** Date format string (date-fns format) */
112
+ dateFormat?: string
113
+ /** Disable the picker */
114
+ disabled?: boolean
115
+ /** Minimum selectable date */
116
+ fromDate?: Date
117
+ /** Maximum selectable date */
118
+ toDate?: Date
119
+ /** Number of months to display */
120
+ numberOfMonths?: number
121
+ /** Additional class names for the trigger button */
122
+ className?: string
123
+ /** Button variant */
124
+ variant?: 'default' | 'outline' | 'ghost'
125
+ /** Align popover */
126
+ align?: 'start' | 'center' | 'end'
127
+ }
128
+
129
+ const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
130
+ (
131
+ {
132
+ value,
133
+ onChange,
134
+ placeholder = 'Pick a date range',
135
+ dateFormat = 'LLL dd, y',
136
+ disabled = false,
137
+ fromDate,
138
+ toDate,
139
+ numberOfMonths = 2,
140
+ className,
141
+ variant = 'outline',
142
+ align = 'start',
143
+ },
144
+ ref
145
+ ) => {
146
+ const [open, setOpen] = React.useState(false)
147
+
148
+ const formatRange = () => {
149
+ if (!value?.from) return placeholder
150
+ if (!value.to) return format(value.from, dateFormat)
151
+ return `${format(value.from, dateFormat)} - ${format(value.to, dateFormat)}`
152
+ }
153
+
154
+ return (
155
+ <Popover open={open} onOpenChange={setOpen}>
156
+ <PopoverTrigger asChild>
157
+ <Button
158
+ ref={ref}
159
+ variant={variant}
160
+ disabled={disabled}
161
+ className={cn(
162
+ 'w-full justify-start text-left font-normal',
163
+ !value?.from && 'text-muted-foreground',
164
+ className
165
+ )}
166
+ >
167
+ <CalendarIcon className="mr-2 h-4 w-4" />
168
+ {formatRange()}
169
+ </Button>
170
+ </PopoverTrigger>
171
+ <PopoverContent className="w-auto p-0" align={align}>
172
+ <Calendar
173
+ mode="range"
174
+ selected={value}
175
+ onSelect={onChange}
176
+ disabled={disabled}
177
+ fromDate={fromDate}
178
+ toDate={toDate}
179
+ numberOfMonths={numberOfMonths}
180
+ initialFocus
181
+ />
182
+ </PopoverContent>
183
+ </Popover>
184
+ )
185
+ }
186
+ )
187
+ DateRangePicker.displayName = 'DateRangePicker'
188
+
189
+ export { DatePicker, DateRangePicker }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Calendar Components
3
+ *
4
+ * @module calendar
5
+ */
6
+
7
+ export { Calendar, CalendarDayButton } from './calendar'
8
+ export { DatePicker, DateRangePicker } from './date-picker'
9
+ export type { DatePickerProps, DateRangePickerProps, DateRange } from './date-picker'
@@ -49,7 +49,9 @@ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './
49
49
  export { Collapsible, CollapsibleContent, CollapsibleTrigger } from './collapsible';
50
50
  export { Progress } from './progress';
51
51
  export { Avatar, AvatarFallback, AvatarImage } from './avatar';
52
- export { Calendar } from './calendar';
52
+ export { Calendar, CalendarDayButton } from './calendar';
53
+ export { DatePicker, DateRangePicker } from './calendar';
54
+ export type { DatePickerProps, DateRangePickerProps, DateRange } from './calendar';
53
55
  export { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from './carousel';
54
56
  export { TokenIcon, getAllTokenSymbols, searchTokens, getTokensByCategory } from './token-icon';
55
57
  export type { TokenIconProps, TokenSymbol, TokenCategory } from './token-icon';
@@ -76,6 +78,7 @@ export { Spinner } from './spinner';
76
78
  export { Preloader, PreloaderSkeleton } from './preloader';
77
79
  export type { PreloaderProps, PreloaderSkeletonProps } from './preloader';
78
80
  export { Kbd, KbdGroup } from './kbd';
81
+ export type { KbdProps, KbdSize } from './kbd';
79
82
  export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea } from './input-group';
80
83
  export { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter } from './item';
81
84
  export { Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldLegend, FieldSeparator, FieldSet, FieldContent, FieldTitle } from './field';
@@ -1,13 +1,26 @@
1
1
  import { cn } from "../lib/utils"
2
2
 
3
- function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
3
+ type KbdSize = "xs" | "sm" | "default" | "lg";
4
+
5
+ interface KbdProps extends React.ComponentProps<"kbd"> {
6
+ size?: KbdSize;
7
+ }
8
+
9
+ const kbdSizeStyles: Record<KbdSize, string> = {
10
+ xs: "h-4 min-w-4 px-0.5 text-[10px] [&_svg:not([class*='size-'])]:size-2.5",
11
+ sm: "h-5 min-w-5 px-1 text-xs [&_svg:not([class*='size-'])]:size-3",
12
+ default: "h-6 min-w-6 px-1.5 text-xs [&_svg:not([class*='size-'])]:size-3.5",
13
+ lg: "h-7 min-w-7 px-2 text-sm [&_svg:not([class*='size-'])]:size-4",
14
+ };
15
+
16
+ function Kbd({ className, size = "default", ...props }: KbdProps) {
4
17
  return (
5
18
  <kbd
6
19
  data-slot="kbd"
7
20
  className={cn(
8
- "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium",
9
- "[&_svg:not([class*='size-'])]:size-3",
21
+ "bg-muted text-muted-foreground pointer-events-none inline-flex w-fit select-none items-center justify-center gap-1 rounded-sm font-mono font-medium border border-border/50",
10
22
  "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
23
+ kbdSizeStyles[size],
11
24
  className
12
25
  )}
13
26
  {...props}
@@ -26,3 +39,4 @@ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
26
39
  }
27
40
 
28
41
  export { Kbd, KbdGroup }
42
+ export type { KbdProps, KbdSize }
@@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
16
16
  <ToastPrimitives.Viewport
17
17
  ref={ref}
18
18
  className={cn(
19
- "fixed top-0 z-9999 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-md",
19
+ "fixed bottom-0 right-0 z-9999 flex max-h-screen w-full flex-col p-4 md:max-w-md",
20
20
  className
21
21
  )}
22
22
  {...props}
@@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
25
25
  ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26
26
 
27
27
  const toastVariants = cva(
28
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
28
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-bottom-full",
29
29
  {
30
30
  variants: {
31
31
  variant: {
@@ -9,13 +9,15 @@ import type {
9
9
  } from "../components/toast"
10
10
 
11
11
  const TOAST_LIMIT = 1
12
- const TOAST_REMOVE_DELAY = 1000000
12
+ const TOAST_DEFAULT_DURATION = 5000 // 5 seconds default auto-dismiss
13
+ const TOAST_DISMISS_DELAY = 300 // Animation delay before removing from DOM
13
14
 
14
15
  type ToasterToast = ToastProps & {
15
16
  id: string
16
17
  title?: React.ReactNode
17
18
  description?: React.ReactNode
18
19
  action?: ToastActionElement
20
+ duration?: number // Auto-dismiss duration in ms
19
21
  }
20
22
 
21
23
  const actionTypes = {
@@ -57,8 +59,10 @@ interface State {
57
59
  }
58
60
 
59
61
  const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
62
+ const autoDismissTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
60
63
 
61
- const addToRemoveQueue = (toastId: string) => {
64
+ // Queue toast for removal after animation
65
+ const addToRemoveQueue = (toastId: string, delay: number = TOAST_DISMISS_DELAY) => {
62
66
  if (toastTimeouts.has(toastId)) {
63
67
  return
64
68
  }
@@ -69,11 +73,36 @@ const addToRemoveQueue = (toastId: string) => {
69
73
  type: "REMOVE_TOAST",
70
74
  toastId: toastId,
71
75
  })
72
- }, TOAST_REMOVE_DELAY)
76
+ }, delay)
73
77
 
74
78
  toastTimeouts.set(toastId, timeout)
75
79
  }
76
80
 
81
+ // Schedule auto-dismiss after duration
82
+ const scheduleAutoDismiss = (toastId: string, duration: number) => {
83
+ // Clear existing auto-dismiss if any
84
+ const existing = autoDismissTimeouts.get(toastId)
85
+ if (existing) {
86
+ clearTimeout(existing)
87
+ }
88
+
89
+ const timeout = setTimeout(() => {
90
+ autoDismissTimeouts.delete(toastId)
91
+ dispatch({ type: "DISMISS_TOAST", toastId })
92
+ }, duration)
93
+
94
+ autoDismissTimeouts.set(toastId, timeout)
95
+ }
96
+
97
+ // Clear auto-dismiss timeout (e.g., on manual dismiss)
98
+ const clearAutoDismiss = (toastId: string) => {
99
+ const timeout = autoDismissTimeouts.get(toastId)
100
+ if (timeout) {
101
+ clearTimeout(timeout)
102
+ autoDismissTimeouts.delete(toastId)
103
+ }
104
+ }
105
+
77
106
  export const reducer = (state: State, action: Action): State => {
78
107
  switch (action.type) {
79
108
  case "ADD_TOAST":
@@ -93,12 +122,13 @@ export const reducer = (state: State, action: Action): State => {
93
122
  case "DISMISS_TOAST": {
94
123
  const { toastId } = action
95
124
 
96
- // ! Side effects ! - This could be extracted into a dismissToast() action,
97
- // but I'll keep it here for simplicity
125
+ // Clear auto-dismiss timer and queue for removal
98
126
  if (toastId) {
127
+ clearAutoDismiss(toastId)
99
128
  addToRemoveQueue(toastId)
100
129
  } else {
101
130
  state.toasts.forEach((toast) => {
131
+ clearAutoDismiss(toast.id)
102
132
  addToRemoveQueue(toast.id)
103
133
  })
104
134
  }
@@ -142,7 +172,7 @@ function dispatch(action: Action) {
142
172
 
143
173
  type Toast = Omit<ToasterToast, "id">
144
174
 
145
- function toast({ ...props }: Toast) {
175
+ function toast({ duration = TOAST_DEFAULT_DURATION, ...props }: Toast) {
146
176
  const id = genId()
147
177
 
148
178
  const update = (props: ToasterToast) =>
@@ -164,6 +194,11 @@ function toast({ ...props }: Toast) {
164
194
  },
165
195
  })
166
196
 
197
+ // Schedule auto-dismiss after duration (0 means no auto-dismiss)
198
+ if (duration > 0) {
199
+ scheduleAutoDismiss(id, duration)
200
+ }
201
+
167
202
  return {
168
203
  id: id,
169
204
  dismiss,
@@ -0,0 +1,229 @@
1
+ # Tailwind CSS v4 Styles Guide
2
+
3
+ This directory contains the CSS architecture for `@djangocfg/ui-core` using **Tailwind CSS v4**.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ styles/
9
+ ├── index.css # Main entry point (import order is critical!)
10
+ ├── theme.css # Theme imports
11
+ ├── base.css # Base element styles
12
+ ├── utilities.css # Custom utility classes
13
+ ├── sources.css # Source detection for Tailwind v4 monorepo
14
+ └── theme/
15
+ ├── tokens.css # @theme block with CSS custom properties
16
+ ├── light.css # Light theme variables
17
+ ├── dark.css # Dark theme variables
18
+ └── animations.css # Animation keyframes
19
+ ```
20
+
21
+ ## Key Changes in Tailwind v4
22
+
23
+ 1. **CSS-First Configuration**: Theme is now defined using CSS custom properties in an `@theme` block instead of JavaScript
24
+ 2. **New Import Syntax**: Use `@import "tailwindcss"` instead of `@tailwind` directives
25
+ 3. **Simplified PostCSS Setup**: Use `@tailwindcss/postcss` plugin
26
+ 4. **Performance Improvements**: 10x faster build times, significantly smaller CSS bundles
27
+ 5. **Modern Browser Support**: Optimized for Safari 16.4+, Chrome 111+, Firefox 128+
28
+ 6. **Source Detection Required**: Tailwind v4 requires explicit `@source` directives to scan files in monorepo packages
29
+
30
+ ## App Setup (globals.css) - CRITICAL!
31
+
32
+ When creating a new app that uses UI packages, your `globals.css` MUST follow this pattern:
33
+
34
+ ```css
35
+ /**
36
+ * CRITICAL: Import order matters for Tailwind CSS v4!
37
+ * 1. Theme variables MUST come BEFORE @import "tailwindcss"
38
+ * 2. @source directives MUST be added for each package with Tailwind classes
39
+ */
40
+
41
+ /* 1. Import UI package styles FIRST (contains @theme variables) */
42
+ @import "@djangocfg/ui-nextjs/styles";
43
+
44
+ /* 2. Import other package styles (layouts, etc.) */
45
+ @import "@djangocfg/layouts/styles";
46
+
47
+ /* 3. Import Tailwind CSS v4 (AFTER theme variables!) */
48
+ @import "tailwindcss";
49
+
50
+ /* 4. Load plugins */
51
+ @plugin "tailwindcss-animate";
52
+
53
+ /* 5. Source detection for packages NOT included in ui-nextjs/styles */
54
+ /* This tells Tailwind where to scan for utility classes */
55
+ @source "../../../packages/your-package/src/**/*.{ts,tsx}";
56
+ ```
57
+
58
+ ### Why @source is Required
59
+
60
+ In Tailwind v4 monorepo, automatic class detection doesn't work across packages. Each package with Tailwind classes needs:
61
+
62
+ 1. **Either** a `@source` directive in the consuming app's `globals.css`
63
+ 2. **Or** a `sources.css` file in the package that's imported via the styles entry point
64
+
65
+ **Example**: If you have `playground-ui` package with components using `pb-20`, `grid-cols-5`, etc., and these classes don't work:
66
+
67
+ ```css
68
+ /* Add this to your app's globals.css */
69
+ @source "../../../packages/playground-ui/src/**/*.{ts,tsx}";
70
+ ```
71
+
72
+ ### Common Symptoms of Missing @source
73
+
74
+ - Tailwind classes don't apply (but inline styles work)
75
+ - Grid classes like `grid-cols-3`, `grid-cols-5` don't work
76
+ - Spacing classes like `pb-20`, `mt-10` have no effect
77
+ - Classes work in some packages but not others
78
+
79
+ ## Import Order (Critical!)
80
+
81
+ ```css
82
+ /* 1. Theme variables MUST come first */
83
+ @import "./theme.css";
84
+
85
+ /* 2. Source detection for Tailwind v4 monorepo */
86
+ @import "./sources.css";
87
+
88
+ /* 3. Base styles */
89
+ @import "./base.css";
90
+
91
+ /* 4. Custom utilities */
92
+ @import "./utilities.css";
93
+ ```
94
+
95
+ ## Best Practices
96
+
97
+ ### Spacing & Sizing
98
+
99
+ - Use standard Tailwind classes: `py-16 sm:py-20 md:py-24 lg:py-32`
100
+ - Responsive patterns: `px-4 sm:px-6 lg:px-8`
101
+ - Container pattern: `container max-w-7xl mx-auto`
102
+
103
+ ### Arbitrary Values
104
+
105
+ **IMPORTANT**: Arbitrary values like `h-[80px]`, `z-[100]` may NOT work in v4!
106
+
107
+ ```tsx
108
+ // ❌ May not work in Tailwind v4
109
+ <div className="h-[80px] z-[100]">
110
+
111
+ // ✅ Define tokens in @theme instead
112
+ @theme {
113
+ --spacing-80: 20rem;
114
+ --z-index-100: 100;
115
+ }
116
+
117
+ // ✅ Or use inline styles (always reliable)
118
+ <div style={{ height: '80px', zIndex: 100 }}>
119
+ ```
120
+
121
+ ### Opacity with HSL Colors
122
+
123
+ **CRITICAL**: `bg-background/80` does NOT work with HSL colors in v4!
124
+
125
+ ```tsx
126
+ // ❌ BROKEN - does not work in Tailwind v4
127
+ <nav className="bg-background/80 border-border/30">
128
+
129
+ // ✅ WORKING - use inline styles for opacity
130
+ <nav
131
+ className="sticky top-0 z-100 backdrop-blur-xl"
132
+ style={{
133
+ backgroundColor: 'hsl(var(--background) / 0.8)',
134
+ borderColor: 'hsl(var(--border) / 0.3)'
135
+ }}
136
+ >
137
+ ```
138
+
139
+ ### Fixed Sizes
140
+
141
+ ```tsx
142
+ // ✅ Inline styles for fixed sizes (always reliable)
143
+ <Avatar
144
+ className="aspect-square rounded-full overflow-hidden"
145
+ style={{ width: '80px', height: '80px' }}
146
+ >
147
+ <AvatarImage src={avatar} alt="User" />
148
+ </Avatar>
149
+ ```
150
+
151
+ ### Spacing Requirements
152
+
153
+ Spacing utilities (`h-20`, `p-4`, etc.) require `--spacing-*` variables defined in `@theme` block:
154
+
155
+ ```css
156
+ @theme {
157
+ --spacing: 0.25rem; /* Base unit: 4px */
158
+ --spacing-0: 0;
159
+ --spacing-1: 0.25rem; /* 4px */
160
+ --spacing-2: 0.5rem; /* 8px */
161
+ --spacing-4: 1rem; /* 16px */
162
+ --spacing-8: 2rem; /* 32px */
163
+ --spacing-13: 3.25rem; /* 52px - for min-h-13 */
164
+ /* ... */
165
+ }
166
+ ```
167
+
168
+ ### Z-Index Requirements
169
+
170
+ Z-index utilities (`z-50`, `z-100`) require `--z-index-*` variables:
171
+
172
+ ```css
173
+ @theme {
174
+ --z-index-50: 50;
175
+ --z-index-100: 100;
176
+ --z-index-9999: 9999;
177
+ }
178
+ ```
179
+
180
+ ## Common Patterns
181
+
182
+ ### Responsive Design (Mobile-First)
183
+
184
+ ```tsx
185
+ <div className="px-4 sm:px-6 lg:px-8">
186
+ <div className="py-16 sm:py-20 md:py-24 lg:py-32">
187
+ Content
188
+ </div>
189
+ </div>
190
+ ```
191
+
192
+ ### Breakpoints
193
+
194
+ - `sm:` - 640px (40rem)
195
+ - `md:` - 768px (48rem)
196
+ - `lg:` - 1024px (64rem)
197
+ - `xl:` - 1280px (80rem)
198
+ - `2xl:` - 1536px (96rem)
199
+
200
+ ### Circles
201
+
202
+ ```tsx
203
+ // ✅ Perfect circle
204
+ <div className="aspect-square rounded-full overflow-hidden w-10 h-10">
205
+ ```
206
+
207
+ ## What to Avoid
208
+
209
+ - Custom utilities like: `section-padding`, `animate-*`, `shadow-brand`
210
+ - Arbitrary values without checking if they work: `h-[53px]`, `min-h-[100px]`
211
+ - Opacity modifiers with HSL colors: `bg-background/80`
212
+ - Assuming spacing values exist without defining them in `@theme`
213
+
214
+ ## CSS Variables
215
+
216
+ Use CSS variables for dynamic values:
217
+
218
+ ```tsx
219
+ <div style={{ color: 'var(--color-primary)' }}>
220
+ ```
221
+
222
+ Or in CSS:
223
+
224
+ ```css
225
+ .my-class {
226
+ background-color: hsl(var(--background));
227
+ color: hsl(var(--foreground));
228
+ }
229
+ ```
@@ -67,6 +67,7 @@
67
67
  --spacing-10: 2.5rem; /* 40px */
68
68
  --spacing-11: 2.75rem; /* 44px */
69
69
  --spacing-12: 3rem; /* 48px */
70
+ --spacing-13: 3.25rem; /* 52px */
70
71
  --spacing-14: 3.5rem; /* 56px */
71
72
  --spacing-16: 4rem; /* 64px */
72
73
  --spacing-20: 5rem; /* 80px */