@djangocfg/ui-core 2.1.3 → 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 +2 -2
- package/src/components/@old/calendar.tsx +213 -0
- package/src/components/@old/date-picker.tsx +191 -0
- package/src/components/{calendar.tsx → calendar/calendar.tsx} +4 -4
- package/src/components/calendar/date-picker.tsx +189 -0
- package/src/components/calendar/index.ts +9 -0
- package/src/components/index.ts +4 -1
- package/src/components/kbd.tsx +17 -3
- package/src/components/toast.tsx +2 -2
- package/src/hooks/useToast.ts +41 -6
- package/src/styles/README.md +229 -0
- package/src/styles/theme/tokens.css +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
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 "
|
|
12
|
-
import { Button, buttonVariants } from "
|
|
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-
|
|
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'
|
package/src/components/index.ts
CHANGED
|
@@ -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';
|
package/src/components/kbd.tsx
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { cn } from "../lib/utils"
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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 }
|
package/src/components/toast.tsx
CHANGED
|
@@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
|
|
|
16
16
|
<ToastPrimitives.Viewport
|
|
17
17
|
ref={ref}
|
|
18
18
|
className={cn(
|
|
19
|
-
"fixed
|
|
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-
|
|
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: {
|
package/src/hooks/useToast.ts
CHANGED
|
@@ -9,13 +9,15 @@ import type {
|
|
|
9
9
|
} from "../components/toast"
|
|
10
10
|
|
|
11
11
|
const TOAST_LIMIT = 1
|
|
12
|
-
const
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
//
|
|
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
|
+
```
|