@gentleduck/registry-ui 0.2.12 → 0.3.1
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/.turbo/turbo-test.log +5 -5
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/SECURITY.md +19 -0
- package/package.json +10 -2
- package/src/accordion/accordion.tsx +1 -1
- package/src/alert/alert.tsx +1 -1
- package/src/alert-dialog/alert-dialog.tsx +7 -7
- package/src/avatar/avatar.tsx +1 -1
- package/src/breadcrumb/breadcrumb.tsx +2 -2
- package/src/button/button.tsx +1 -1
- package/src/calendar/calendar-day.tsx +131 -0
- package/src/calendar/calendar-header.tsx +291 -0
- package/src/calendar/calendar.tsx +195 -182
- package/src/calendar/calendar.types.ts +135 -0
- package/src/calendar/calendar.utils.ts +23 -0
- package/src/calendar/index.ts +2 -1
- package/src/card/card.tsx +1 -1
- package/src/carousel/carousel.tsx +1 -1
- package/src/chart/chart.tsx +1 -1
- package/src/collapsible/collapsible.tsx +1 -1
- package/src/combobox/combobox.tsx +1 -0
- package/src/command/command.tsx +13 -8
- package/src/context-menu/context-menu.tsx +6 -6
- package/src/dialog/dialog-responsive.tsx +5 -5
- package/src/dialog/dialog.tsx +13 -11
- package/src/direction/direction.tsx +2 -1
- package/src/drawer/drawer.tsx +5 -5
- package/src/dropdown-menu/dropdown-menu.tsx +6 -6
- package/src/empty/empty.tsx +1 -1
- package/src/field/field.tsx +2 -2
- package/src/hover-card/hover-card.tsx +1 -1
- package/src/input-group/input-group.tsx +1 -1
- package/src/input-otp/input-otp.tsx +1 -1
- package/src/item/item.tsx +5 -5
- package/src/menubar/menubar.tsx +8 -8
- package/src/navigation-menu/navigation-menu.tsx +4 -4
- package/src/popover/popover.tsx +1 -1
- package/src/resizable/resizable.tsx +1 -1
- package/src/select/select.tsx +6 -6
- package/src/sheet/sheet.tsx +5 -5
- package/src/sonner/sonner.chunks.tsx +0 -1
- package/src/table/table.tsx +1 -1
- package/src/tabs/tabs.tsx +1 -1
- package/src/tooltip/tooltip.tsx +1 -1
|
@@ -1,211 +1,224 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { NativeAdapter, useCalendar } from '@gentleduck/calendar'
|
|
3
4
|
import { cn } from '@gentleduck/libs/cn'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { useDirection } from '@gentleduck/primitives/direction'
|
|
6
|
+
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
|
|
6
7
|
import * as React from 'react'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { buttonVariants } from '../button'
|
|
9
|
+
import type { CalendarProps } from './calendar.types'
|
|
10
|
+
import { CalendarDayCell } from './calendar-day'
|
|
11
|
+
import { CalendarHeader } from './calendar-header'
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
return (node) => {
|
|
12
|
-
for (const ref of refs) {
|
|
13
|
-
if (typeof ref === 'function') {
|
|
14
|
-
ref(node)
|
|
15
|
-
} else if (ref != null) {
|
|
16
|
-
;(ref as React.MutableRefObject<T | null>).current = node
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
13
|
+
const defaultAdapter = new NativeAdapter()
|
|
21
14
|
|
|
22
|
-
const Calendar = React.forwardRef<
|
|
23
|
-
HTMLDivElement,
|
|
24
|
-
React.ComponentProps<typeof DayPicker> & {
|
|
25
|
-
buttonVariant?: React.ComponentProps<typeof Button>['variant']
|
|
26
|
-
}
|
|
27
|
-
>(
|
|
15
|
+
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
|
28
16
|
(
|
|
29
17
|
{
|
|
30
18
|
className,
|
|
31
|
-
|
|
32
|
-
showOutsideDays = true,
|
|
33
|
-
captionLayout = 'label',
|
|
19
|
+
adapter = defaultAdapter,
|
|
34
20
|
buttonVariant = 'ghost',
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
mode = 'single',
|
|
22
|
+
selected,
|
|
23
|
+
onSelect,
|
|
24
|
+
disabled,
|
|
25
|
+
defaultMonth,
|
|
26
|
+
month: controlledMonth,
|
|
27
|
+
onMonthChange,
|
|
28
|
+
showOutsideDays = true,
|
|
29
|
+
fixedWeeks = false,
|
|
30
|
+
numberOfMonths = 1,
|
|
31
|
+
locale,
|
|
37
32
|
dir,
|
|
38
|
-
|
|
33
|
+
fromDate,
|
|
34
|
+
toDate,
|
|
35
|
+
onDismiss,
|
|
36
|
+
showDropdowns = true,
|
|
37
|
+
yearRange,
|
|
38
|
+
renderDay,
|
|
39
|
+
renderHeader,
|
|
40
|
+
renderWeekday,
|
|
41
|
+
renderFooter,
|
|
39
42
|
},
|
|
40
43
|
ref,
|
|
41
44
|
) => {
|
|
42
|
-
const direction = useDirection(dir
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return code.startsWith('ar') ? `${code}-u-nu-arab` : code
|
|
48
|
-
}, [props.locale])
|
|
45
|
+
const direction = useDirection(dir)
|
|
46
|
+
const currentYear = new Date().getFullYear()
|
|
47
|
+
const resolvedYearRange = yearRange ?? { from: currentYear - 100, to: currentYear + 10 }
|
|
48
|
+
// Build full locale tag with numbering system for Arabic
|
|
49
|
+
const formatLocale = locale?.startsWith('ar') ? `${locale}-u-nu-arab` : locale
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const calendar = useCalendar({
|
|
52
|
+
adapter,
|
|
53
|
+
mode,
|
|
54
|
+
locale: locale ? { locale, weekStartDay: 0, direction } : { weekStartDay: 0, direction },
|
|
55
|
+
month: controlledMonth,
|
|
56
|
+
defaultMonth,
|
|
57
|
+
selected,
|
|
58
|
+
onSelect,
|
|
59
|
+
onMonthChange,
|
|
60
|
+
showOutsideDays,
|
|
61
|
+
fixedWeeks,
|
|
62
|
+
numberOfMonths,
|
|
63
|
+
disabled,
|
|
64
|
+
fromDate,
|
|
65
|
+
toDate,
|
|
66
|
+
onDismiss,
|
|
67
|
+
})
|
|
53
68
|
|
|
54
|
-
const
|
|
55
|
-
return new Intl.DateTimeFormat(localeTag, { month: 'long', year: 'numeric' })
|
|
56
|
-
}, [localeTag])
|
|
69
|
+
const { state, getDayProps, getGridProps, getNavProps, getHeaderProps, announcer } = calendar
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}, [localeTag])
|
|
71
|
+
// Only show focus ring during keyboard navigation, not on mouse clicks
|
|
72
|
+
const [keyboardActive, setKeyboardActive] = React.useState(false)
|
|
61
73
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
return numberFormatter.format(value)
|
|
65
|
-
},
|
|
66
|
-
[numberFormatter],
|
|
67
|
-
)
|
|
74
|
+
const prevNavProps = getNavProps('prev')
|
|
75
|
+
const nextNavProps = getNavProps('next')
|
|
68
76
|
|
|
69
77
|
return (
|
|
70
|
-
|
|
78
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: keyboard/pointer tracking for focus ring management
|
|
79
|
+
<div
|
|
80
|
+
ref={ref}
|
|
81
|
+
data-slot="calendar"
|
|
71
82
|
dir={direction}
|
|
72
|
-
|
|
83
|
+
onKeyDown={() => {
|
|
84
|
+
if (!keyboardActive) setKeyboardActive(true)
|
|
85
|
+
}}
|
|
86
|
+
onPointerDown={() => {
|
|
87
|
+
if (keyboardActive) setKeyboardActive(false)
|
|
88
|
+
}}
|
|
73
89
|
className={cn(
|
|
74
|
-
'group/calendar
|
|
75
|
-
|
|
76
|
-
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
90
|
+
'group/calendar w-fit bg-background p-3 [--gentleduck-calendar-cell:--spacing(8)]',
|
|
91
|
+
'rounded-md in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
|
|
77
92
|
className,
|
|
78
|
-
)}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
weekday: cn(
|
|
133
|
-
'flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground',
|
|
134
|
-
defaultClassNames.weekday,
|
|
135
|
-
),
|
|
136
|
-
weekdays: cn('flex', defaultClassNames.weekdays),
|
|
137
|
-
...classNames,
|
|
138
|
-
}}
|
|
139
|
-
components={{
|
|
140
|
-
Chevron: ({ className, orientation, ...props }) => {
|
|
141
|
-
if (orientation === 'left') {
|
|
142
|
-
return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
|
|
143
|
-
}
|
|
93
|
+
)}>
|
|
94
|
+
<div className="relative flex flex-col gap-4">
|
|
95
|
+
{/* Nav header - spans full width above all months */}
|
|
96
|
+
{renderHeader ? (
|
|
97
|
+
renderHeader({
|
|
98
|
+
month: state.month,
|
|
99
|
+
title: adapter.format(state.month, { month: 'long', year: 'numeric' }, formatLocale),
|
|
100
|
+
direction,
|
|
101
|
+
goToPrevMonth: prevNavProps.onClick,
|
|
102
|
+
goToNextMonth: nextNavProps.onClick,
|
|
103
|
+
isPrevDisabled: prevNavProps.disabled,
|
|
104
|
+
isNextDisabled: nextNavProps.disabled,
|
|
105
|
+
})
|
|
106
|
+
) : numberOfMonths <= 1 ? (
|
|
107
|
+
<CalendarHeader
|
|
108
|
+
adapter={adapter}
|
|
109
|
+
month={state.month}
|
|
110
|
+
title={adapter.format(state.month, { month: 'long', year: 'numeric' }, formatLocale)}
|
|
111
|
+
direction={direction}
|
|
112
|
+
locale={locale}
|
|
113
|
+
buttonVariant={buttonVariant}
|
|
114
|
+
showDropdowns={showDropdowns}
|
|
115
|
+
yearRange={resolvedYearRange}
|
|
116
|
+
getNavProps={getNavProps}
|
|
117
|
+
getHeaderProps={getHeaderProps}
|
|
118
|
+
onMonthSelect={calendar.actions.setMonth}
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="relative flex w-full items-center">
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
{...prevNavProps}
|
|
125
|
+
className={cn(
|
|
126
|
+
buttonVariants({ variant: buttonVariant as 'ghost' }),
|
|
127
|
+
'absolute start-0 z-10 size-(--gentleduck-calendar-cell) select-none p-0 aria-disabled:opacity-50',
|
|
128
|
+
)}>
|
|
129
|
+
<ChevronLeftIcon className={cn('size-4', direction === 'rtl' && 'rotate-180')} />
|
|
130
|
+
</button>
|
|
131
|
+
{state.months.map((m) => (
|
|
132
|
+
<span key={m.month.getTime()} className="flex-1 select-none text-center font-medium text-sm">
|
|
133
|
+
{adapter.format(m.month, { month: 'long', year: 'numeric' }, formatLocale)}
|
|
134
|
+
</span>
|
|
135
|
+
))}
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
{...nextNavProps}
|
|
139
|
+
className={cn(
|
|
140
|
+
buttonVariants({ variant: buttonVariant as 'ghost' }),
|
|
141
|
+
'absolute end-0 z-10 size-(--gentleduck-calendar-cell) select-none p-0 aria-disabled:opacity-50',
|
|
142
|
+
)}>
|
|
143
|
+
<ChevronRightIcon className={cn('size-4', direction === 'rtl' && 'rotate-180')} />
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
144
147
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
148
|
+
<div className="flex flex-col gap-4 md:flex-row">
|
|
149
|
+
{state.months.map((monthGrid) => {
|
|
150
|
+
const gridProps = getGridProps()
|
|
151
|
+
return (
|
|
152
|
+
<div key={monthGrid.month.getTime()} className="flex w-full flex-col gap-4">
|
|
153
|
+
<div {...gridProps}>
|
|
154
|
+
{/* biome-ignore lint/a11y/useSemanticElements: role="row" on div per WAI-ARIA grid pattern */}
|
|
155
|
+
{/* biome-ignore lint/a11y/useFocusableInteractive: weekday header row is not interactive */}
|
|
156
|
+
<div role="row" className="flex">
|
|
157
|
+
{state.weekdays.map((day, index) => (
|
|
158
|
+
// biome-ignore lint/a11y/useSemanticElements: columnheader on div per WAI-ARIA grid pattern
|
|
159
|
+
// biome-ignore lint/a11y/useFocusableInteractive: weekday headers are not interactive
|
|
160
|
+
<div
|
|
161
|
+
key={day}
|
|
162
|
+
role="columnheader"
|
|
163
|
+
className="flex-1 select-none rounded-md text-center font-normal text-[0.8rem] text-muted-foreground">
|
|
164
|
+
{renderWeekday
|
|
165
|
+
? renderWeekday(day, index)
|
|
166
|
+
: locale?.startsWith('ar')
|
|
167
|
+
? day.replace(/^ال/, '')
|
|
168
|
+
: locale?.startsWith('fa')
|
|
169
|
+
? day.slice(0, 2)
|
|
170
|
+
: locale?.startsWith('he')
|
|
171
|
+
? day.replace(/^יום\s*/, '')
|
|
172
|
+
: day}
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
{monthGrid.weeks.map((week) => (
|
|
177
|
+
// biome-ignore lint/a11y/useSemanticElements: role="row" on div per WAI-ARIA grid pattern
|
|
178
|
+
// biome-ignore lint/a11y/useFocusableInteractive: grid rows are not interactive
|
|
179
|
+
<div key={week.weekNumber} role="row" className="mt-2 flex w-full">
|
|
180
|
+
{week.days.map((day, dayIdx) => {
|
|
181
|
+
const {
|
|
182
|
+
onMouseEnter: _,
|
|
183
|
+
role: _role,
|
|
184
|
+
'aria-selected': _ariaSel,
|
|
185
|
+
...dayProps
|
|
186
|
+
} = getDayProps(day)
|
|
187
|
+
const isSelectedSingle =
|
|
188
|
+
day.isSelected && !day.isRangeStart && !day.isRangeEnd && !day.isRangeMiddle
|
|
189
|
+
const isFocused = keyboardActive && day.date.getTime() === state.focusedDate.getTime()
|
|
190
|
+
return (
|
|
191
|
+
<CalendarDayCell
|
|
192
|
+
key={day.date.getTime()}
|
|
193
|
+
day={day}
|
|
194
|
+
dayProps={dayProps}
|
|
195
|
+
isFocused={isFocused}
|
|
196
|
+
isSelectedSingle={isSelectedSingle}
|
|
197
|
+
isFirstInRow={dayIdx === 0}
|
|
198
|
+
isLastInRow={dayIdx === 6}
|
|
199
|
+
locale={locale}
|
|
200
|
+
onFocusDate={(date) => {
|
|
201
|
+
setKeyboardActive(false)
|
|
202
|
+
calendar.actions.focusDate(date)
|
|
203
|
+
}}
|
|
204
|
+
renderDay={renderDay}
|
|
205
|
+
/>
|
|
206
|
+
)
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
{renderFooter?.(state.months)}
|
|
216
|
+
</div>
|
|
217
|
+
<announcer.AnnouncerPortal />
|
|
218
|
+
</div>
|
|
175
219
|
)
|
|
176
220
|
},
|
|
177
221
|
)
|
|
178
222
|
Calendar.displayName = 'Calendar'
|
|
179
223
|
|
|
180
|
-
|
|
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
|
-
className={cn(
|
|
191
|
-
'flex aspect-square size-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 data-[range-start=true]:rounded-s-md data-[range-end=true]:rounded-e-md data-[range-end=true]:bg-primary data-[range-middle=true]:bg-accent data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-accent-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 [&>span]:text-xs [&>span]:opacity-70',
|
|
192
|
-
defaultClassNames.day,
|
|
193
|
-
className,
|
|
194
|
-
)}
|
|
195
|
-
data-day={day.date.toLocaleDateString()}
|
|
196
|
-
data-range-end={modifiers.range_end}
|
|
197
|
-
data-range-middle={modifiers.range_middle}
|
|
198
|
-
data-range-start={modifiers.range_start}
|
|
199
|
-
data-selected-single={
|
|
200
|
-
modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle
|
|
201
|
-
}
|
|
202
|
-
ref={ref}
|
|
203
|
-
size="icon"
|
|
204
|
-
variant="ghost"
|
|
205
|
-
{...props}
|
|
206
|
-
/>
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
CalendarDayButton.displayName = 'CalendarDayButton'
|
|
210
|
-
|
|
211
|
-
export { Calendar, CalendarDayButton }
|
|
224
|
+
export { Calendar }
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { CalendarDay, CalendarMonth, DateAdapter, SelectionMode } from '@gentleduck/calendar'
|
|
2
|
+
import type { Direction } from '@gentleduck/primitives/direction'
|
|
3
|
+
import type { Button } from '../button'
|
|
4
|
+
|
|
5
|
+
export interface CalendarHeaderContext {
|
|
6
|
+
/** The current displayed month Date. */
|
|
7
|
+
month: Date
|
|
8
|
+
/** Formatted title string (e.g. "March 2026"). */
|
|
9
|
+
title: string
|
|
10
|
+
/** Resolved text direction. */
|
|
11
|
+
direction: 'ltr' | 'rtl'
|
|
12
|
+
/** Navigate to the previous month. */
|
|
13
|
+
goToPrevMonth: () => void
|
|
14
|
+
/** Navigate to the next month. */
|
|
15
|
+
goToNextMonth: () => void
|
|
16
|
+
/** Whether previous navigation is disabled. */
|
|
17
|
+
isPrevDisabled: boolean
|
|
18
|
+
/** Whether next navigation is disabled. */
|
|
19
|
+
isNextDisabled: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CalendarProps {
|
|
23
|
+
className?: string
|
|
24
|
+
/**
|
|
25
|
+
* Date adapter for alternative calendar systems (Islamic, Persian, etc.).
|
|
26
|
+
* Default uses `NativeAdapter` (Gregorian).
|
|
27
|
+
*/
|
|
28
|
+
adapter?: DateAdapter<Date>
|
|
29
|
+
/** Variant style for navigation buttons. Default `'ghost'`. */
|
|
30
|
+
buttonVariant?: React.ComponentProps<typeof Button>['variant']
|
|
31
|
+
/** Selection mode. Default `'single'`. */
|
|
32
|
+
mode?: SelectionMode
|
|
33
|
+
/** Controlled selection value. Shape depends on `mode`. */
|
|
34
|
+
// biome-ignore lint/suspicious/noExplicitAny: CalendarValue union is narrowed by mode at runtime
|
|
35
|
+
selected?: any
|
|
36
|
+
/** Called when the selection changes. Value shape depends on `mode`. */
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: CalendarValue union is narrowed by mode at runtime
|
|
38
|
+
onSelect?: (value: any) => void
|
|
39
|
+
/** Dates that cannot be selected. */
|
|
40
|
+
disabled?: Date[] | ((date: Date) => boolean)
|
|
41
|
+
/** Default month to display (uncontrolled). */
|
|
42
|
+
defaultMonth?: Date
|
|
43
|
+
/** Controlled month. */
|
|
44
|
+
month?: Date
|
|
45
|
+
/** Called when the displayed month changes. */
|
|
46
|
+
onMonthChange?: (month: Date) => void
|
|
47
|
+
/** Show days from adjacent months. Default `true`. */
|
|
48
|
+
showOutsideDays?: boolean
|
|
49
|
+
/** Always show 6 weeks. Default `false`. */
|
|
50
|
+
fixedWeeks?: boolean
|
|
51
|
+
/** How many months to show side by side. Default `1`. */
|
|
52
|
+
numberOfMonths?: number
|
|
53
|
+
/** BCP 47 locale tag (e.g. `'ar-SA'`). */
|
|
54
|
+
locale?: string
|
|
55
|
+
/** Text direction. */
|
|
56
|
+
dir?: Direction
|
|
57
|
+
/** Earliest selectable date. */
|
|
58
|
+
fromDate?: Date
|
|
59
|
+
/** Latest selectable date. */
|
|
60
|
+
toDate?: Date
|
|
61
|
+
/** Called when the user presses Escape. */
|
|
62
|
+
onDismiss?: () => void
|
|
63
|
+
/**
|
|
64
|
+
* Show month and year dropdowns in the header.
|
|
65
|
+
* Default `true`. Set to `false` for a minimal caption.
|
|
66
|
+
*/
|
|
67
|
+
showDropdowns?: boolean
|
|
68
|
+
/**
|
|
69
|
+
* Range of years to show in the year dropdown.
|
|
70
|
+
* Default `{ from: currentYear - 100, to: currentYear + 10 }`.
|
|
71
|
+
*/
|
|
72
|
+
yearRange?: { from: number; to: number }
|
|
73
|
+
/**
|
|
74
|
+
* Custom render function for day cells.
|
|
75
|
+
* Receives the day object and the default rendered children (the date number).
|
|
76
|
+
* Return a ReactNode to replace or wrap the default content.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* renderDay={(day, children) => (
|
|
81
|
+
* <>
|
|
82
|
+
* {children}
|
|
83
|
+
* {hasEvents(day.date) && <span className="size-1 rounded-full bg-primary" />}
|
|
84
|
+
* </>
|
|
85
|
+
* )}
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
renderDay?: (day: CalendarDay<Date>, children: React.ReactNode) => React.ReactNode
|
|
89
|
+
/**
|
|
90
|
+
* Custom render function for the navigation header.
|
|
91
|
+
* Receives header context with month info and navigation controls.
|
|
92
|
+
* Return a ReactNode to replace the default header entirely.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* renderHeader={({ title, goToPrevMonth, goToNextMonth }) => (
|
|
97
|
+
* <div className="flex items-center justify-between">
|
|
98
|
+
* <button onClick={goToPrevMonth}><-</button>
|
|
99
|
+
* <span>{title}</span>
|
|
100
|
+
* <button onClick={goToNextMonth}>-></button>
|
|
101
|
+
* </div>
|
|
102
|
+
* )}
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
renderHeader?: (context: CalendarHeaderContext) => React.ReactNode
|
|
106
|
+
/**
|
|
107
|
+
* Custom render function for weekday column headers.
|
|
108
|
+
* Receives the weekday abbreviation (e.g. "Sun") and its index (0-6).
|
|
109
|
+
* Return a ReactNode to replace the default weekday label.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```tsx
|
|
113
|
+
* renderWeekday={(day, index) => (
|
|
114
|
+
* <span className={index === 0 || index === 6 ? 'text-red-500' : ''}>
|
|
115
|
+
* {day}
|
|
116
|
+
* </span>
|
|
117
|
+
* )}
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
renderWeekday?: (day: string, index: number) => React.ReactNode
|
|
121
|
+
/**
|
|
122
|
+
* Render content below the calendar grid.
|
|
123
|
+
* Receives the current months array for context.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```tsx
|
|
127
|
+
* renderFooter={(months) => (
|
|
128
|
+
* <div className="mt-2 text-xs text-muted-foreground">
|
|
129
|
+
* Selected: {selected?.toLocaleDateString()}
|
|
130
|
+
* </div>
|
|
131
|
+
* )}
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
renderFooter?: (months: CalendarMonth<Date>[]) => React.ReactNode
|
|
135
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const MAX_CACHE_SIZE = 20
|
|
2
|
+
|
|
3
|
+
/** Cache Intl.NumberFormat instances to avoid recreating formatters on every render. */
|
|
4
|
+
const NUMBER_FORMAT_CACHE = new Map<string, Intl.NumberFormat>()
|
|
5
|
+
|
|
6
|
+
export function getCachedNumberFormat(locale: string, options?: Intl.NumberFormatOptions): Intl.NumberFormat {
|
|
7
|
+
const key = options ? `${locale}|${JSON.stringify(options)}` : locale
|
|
8
|
+
let fmt = NUMBER_FORMAT_CACHE.get(key)
|
|
9
|
+
if (fmt) {
|
|
10
|
+
// LRU: move to end
|
|
11
|
+
NUMBER_FORMAT_CACHE.delete(key)
|
|
12
|
+
NUMBER_FORMAT_CACHE.set(key, fmt)
|
|
13
|
+
return fmt
|
|
14
|
+
}
|
|
15
|
+
// Evict oldest if at capacity
|
|
16
|
+
if (NUMBER_FORMAT_CACHE.size >= MAX_CACHE_SIZE) {
|
|
17
|
+
const oldest = NUMBER_FORMAT_CACHE.keys().next().value
|
|
18
|
+
if (oldest !== undefined) NUMBER_FORMAT_CACHE.delete(oldest)
|
|
19
|
+
}
|
|
20
|
+
fmt = new Intl.NumberFormat(locale, options)
|
|
21
|
+
NUMBER_FORMAT_CACHE.set(key, fmt)
|
|
22
|
+
return fmt
|
|
23
|
+
}
|
package/src/calendar/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { Calendar } from './calendar'
|
|
2
|
+
export type { CalendarHeaderContext, CalendarProps } from './calendar.types'
|
package/src/card/card.tsx
CHANGED
|
@@ -78,4 +78,4 @@ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDiv
|
|
|
78
78
|
)
|
|
79
79
|
CardFooter.displayName = 'CardFooter'
|
|
80
80
|
|
|
81
|
-
export { Card,
|
|
81
|
+
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|
@@ -209,4 +209,4 @@ const CarouselNext = React.forwardRef<
|
|
|
209
209
|
})
|
|
210
210
|
CarouselNext.displayName = 'CarouselNext'
|
|
211
211
|
|
|
212
|
-
export { type CarouselApi,
|
|
212
|
+
export { Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious }
|
package/src/chart/chart.tsx
CHANGED
|
@@ -266,4 +266,4 @@ function ChartLegendContent({
|
|
|
266
266
|
}
|
|
267
267
|
ChartLegendContent.displayName = 'ChartLegendContent'
|
|
268
268
|
|
|
269
|
-
export { ChartContainer,
|
|
269
|
+
export { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent }
|