@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.
Files changed (45) hide show
  1. package/.turbo/turbo-test.log +5 -5
  2. package/CHANGELOG.md +26 -0
  3. package/LICENSE +21 -0
  4. package/SECURITY.md +19 -0
  5. package/package.json +10 -2
  6. package/src/accordion/accordion.tsx +1 -1
  7. package/src/alert/alert.tsx +1 -1
  8. package/src/alert-dialog/alert-dialog.tsx +7 -7
  9. package/src/avatar/avatar.tsx +1 -1
  10. package/src/breadcrumb/breadcrumb.tsx +2 -2
  11. package/src/button/button.tsx +1 -1
  12. package/src/calendar/calendar-day.tsx +131 -0
  13. package/src/calendar/calendar-header.tsx +291 -0
  14. package/src/calendar/calendar.tsx +195 -182
  15. package/src/calendar/calendar.types.ts +135 -0
  16. package/src/calendar/calendar.utils.ts +23 -0
  17. package/src/calendar/index.ts +2 -1
  18. package/src/card/card.tsx +1 -1
  19. package/src/carousel/carousel.tsx +1 -1
  20. package/src/chart/chart.tsx +1 -1
  21. package/src/collapsible/collapsible.tsx +1 -1
  22. package/src/combobox/combobox.tsx +1 -0
  23. package/src/command/command.tsx +13 -8
  24. package/src/context-menu/context-menu.tsx +6 -6
  25. package/src/dialog/dialog-responsive.tsx +5 -5
  26. package/src/dialog/dialog.tsx +13 -11
  27. package/src/direction/direction.tsx +2 -1
  28. package/src/drawer/drawer.tsx +5 -5
  29. package/src/dropdown-menu/dropdown-menu.tsx +6 -6
  30. package/src/empty/empty.tsx +1 -1
  31. package/src/field/field.tsx +2 -2
  32. package/src/hover-card/hover-card.tsx +1 -1
  33. package/src/input-group/input-group.tsx +1 -1
  34. package/src/input-otp/input-otp.tsx +1 -1
  35. package/src/item/item.tsx +5 -5
  36. package/src/menubar/menubar.tsx +8 -8
  37. package/src/navigation-menu/navigation-menu.tsx +4 -4
  38. package/src/popover/popover.tsx +1 -1
  39. package/src/resizable/resizable.tsx +1 -1
  40. package/src/select/select.tsx +6 -6
  41. package/src/sheet/sheet.tsx +5 -5
  42. package/src/sonner/sonner.chunks.tsx +0 -1
  43. package/src/table/table.tsx +1 -1
  44. package/src/tabs/tabs.tsx +1 -1
  45. 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 { type Direction, useDirection } from '@gentleduck/primitives/direction'
5
- import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
5
+ import { useDirection } from '@gentleduck/primitives/direction'
6
+ import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
6
7
  import * as React from 'react'
7
- import { type DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
8
- import { Button, buttonVariants } from '../button'
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
- function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
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
- classNames,
32
- showOutsideDays = true,
33
- captionLayout = 'label',
19
+ adapter = defaultAdapter,
34
20
  buttonVariant = 'ghost',
35
- formatters,
36
- components,
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
- ...props
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 as Direction)
43
- const defaultClassNames = getDefaultClassNames()
44
- const localeTag = React.useMemo(() => {
45
- const code = props.locale?.code
46
- if (!code) return undefined
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 monthFormatter = React.useMemo(() => {
51
- return new Intl.DateTimeFormat(localeTag, { month: 'short' })
52
- }, [localeTag])
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 captionFormatter = React.useMemo(() => {
55
- return new Intl.DateTimeFormat(localeTag, { month: 'long', year: 'numeric' })
56
- }, [localeTag])
69
+ const { state, getDayProps, getGridProps, getNavProps, getHeaderProps, announcer } = calendar
57
70
 
58
- const numberFormatter = React.useMemo(() => {
59
- return new Intl.NumberFormat(localeTag)
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 formatLocalizedNumber = React.useCallback(
63
- (value: number) => {
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
- <DayPicker
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
- captionLayout={captionLayout}
83
+ onKeyDown={() => {
84
+ if (!keyboardActive) setKeyboardActive(true)
85
+ }}
86
+ onPointerDown={() => {
87
+ if (keyboardActive) setKeyboardActive(false)
88
+ }}
73
89
  className={cn(
74
- 'group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-size:--spacing(8)]',
75
- String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
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
- classNames={{
80
- button_next: cn(
81
- buttonVariants({ variant: buttonVariant }),
82
- 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
83
- defaultClassNames.button_next,
84
- ),
85
- button_previous: cn(
86
- buttonVariants({ variant: buttonVariant }),
87
- 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
88
- defaultClassNames.button_previous,
89
- ),
90
- caption_label: cn(
91
- 'select-none font-medium',
92
- captionLayout === 'label'
93
- ? 'text-sm'
94
- : 'flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
95
- defaultClassNames.caption_label,
96
- ),
97
- day: cn(
98
- 'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-s-md [&:last-child[data-selected=true]_button]:rounded-e-md',
99
- defaultClassNames.day,
100
- ),
101
- disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
102
- dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
103
- dropdown_root: cn(
104
- 'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
105
- defaultClassNames.dropdown_root,
106
- ),
107
- dropdowns: cn(
108
- 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
109
- defaultClassNames.dropdowns,
110
- ),
111
- hidden: cn('invisible', defaultClassNames.hidden),
112
- month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
113
- month_caption: cn(
114
- 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
115
- defaultClassNames.month_caption,
116
- ),
117
- months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
118
- nav: cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', defaultClassNames.nav),
119
- outside: cn('text-muted-foreground aria-selected:text-muted-foreground', defaultClassNames.outside),
120
- range_end: cn('rounded-e-md bg-accent', defaultClassNames.range_end),
121
- range_middle: cn('rounded-none', defaultClassNames.range_middle),
122
- range_start: cn('rounded-s-md bg-accent', defaultClassNames.range_start),
123
- root: cn('w-fit', defaultClassNames.root),
124
- table: 'w-full border-collapse',
125
- today: cn(
126
- 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',
127
- defaultClassNames.today,
128
- ),
129
- week: cn('mt-2 flex w-full', defaultClassNames.week),
130
- week_number: cn('select-none text-[0.8rem] text-muted-foreground', defaultClassNames.week_number),
131
- week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
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
- if (orientation === 'right') {
146
- return <ChevronRightIcon className={cn('size-4', className)} {...props} />
147
- }
148
-
149
- return <ChevronDownIcon className={cn('size-4', className)} {...props} />
150
- },
151
- DayButton: CalendarDayButton,
152
- Root: ({ className, rootRef, ...props }) => {
153
- return <div className={cn(className)} data-slot="calendar" ref={mergeRefs(ref, rootRef)} {...props} />
154
- },
155
- WeekNumber: ({ children, ...props }) => {
156
- return (
157
- <td {...props}>
158
- <div className="flex size-(--cell-size) items-center justify-center text-center">{children}</div>
159
- </td>
160
- )
161
- },
162
- ...components,
163
- }}
164
- formatters={{
165
- formatCaption: (date) => captionFormatter.format(date),
166
- formatDay: (date) => formatLocalizedNumber(date.getDate()),
167
- formatMonthDropdown: (date) => monthFormatter.format(date),
168
- formatWeekNumber: (weekNumber) => formatLocalizedNumber(weekNumber),
169
- formatYearDropdown: (date) => String(date.getFullYear()),
170
- ...formatters,
171
- }}
172
- showOutsideDays={showOutsideDays}
173
- {...props}
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
- function CalendarDayButton({ className, day, modifiers, ...props }: 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
- 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
+ }
@@ -1 +1,2 @@
1
- export * from './calendar'
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, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
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, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }
212
+ export { Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious }
@@ -266,4 +266,4 @@ function ChartLegendContent({
266
266
  }
267
267
  ChartLegendContent.displayName = 'ChartLegendContent'
268
268
 
269
- export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }
269
+ export { ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent }
@@ -149,4 +149,4 @@ const CollapsibleContent = React.forwardRef<
149
149
  })
150
150
  CollapsibleContent.displayName = 'CollapsibleContent'
151
151
 
152
- export { Collapsible, CollapsibleTrigger, CollapsibleContent }
152
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger }
@@ -122,6 +122,7 @@ const ComboxGroup = React.forwardRef<
122
122
  )
123
123
  })
124
124
  ComboxGroup.displayName = 'ComboxGroup'
125
+
125
126
  export { ComboxGroup }
126
127
 
127
128
  type ComboboxItemProps<T extends ComboboxItemType> = Omit<