@particle-academy/react-fancy 5.54.2 → 5.56.0

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/CHANGELOG.md CHANGED
@@ -11,6 +11,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [5.56.0] - 2026-10-21
15
+
16
+ ### Added
17
+
18
+ - **`Calendar` per-day hooks and a controlled visible month.** All additive:
19
+ **nothing to do** for an existing Calendar, which renders and behaves as
20
+ before, plus new data attributes.
21
+ - Every day cell carries a stable id, `data-react-fancy-calendar-day`,
22
+ `YYYY-MM-DD` in local time by default (`dateId` replaces it). Leading and
23
+ trailing days carry `data-outside-month`; the heading carries
24
+ `data-react-fancy-calendar-month`.
25
+ - `renderDay(ctx)` draws a day's content, `dayClassName(ctx)` adds classes
26
+ to one day, `dayAriaLabel(ctx)` sets its accessible name. `ctx` is
27
+ `{ date, id, selected, disabled, inRange, outsideMonth, today }`, exported
28
+ as `CalendarDayContext`. A day shaded with `dayClassName` should get a
29
+ `dayAriaLabel` too: colour alone does not reach a screen reader.
30
+ - `visibleMonth` / `onVisibleMonthChange`: the shown month is controlled when
31
+ `visibleMonth` is given (the chevrons propose, the host decides);
32
+ uncontrolled it starts at today as before, and still reports each move.
33
+ The callback gets the 1st of the month.
34
+ - Opening a day on click is the existing `onChange` (single mode).
35
+ - Verified in Chrome in light, `.dark` and `[data-theme="dark"]`.
36
+
37
+ The requester's wording was `month` / `onMonthChange`. The names here follow
38
+ the spec (`visibleMonth` / `onVisibleMonthChange`), because `value` is already
39
+ a date and a bare `month` reads as the selected one.
40
+
41
+ ## [5.55.0] - 2026-10-21
42
+
43
+ ### Added
44
+
45
+ - **`TimeGrid` typed cells and range selection.** Pass `cells` (with `states`)
46
+ and the grid renders the host's own vocabulary instead of on/off. **Nothing
47
+ to do for an existing grid:** without `cells` it is the same boolean grid,
48
+ with the same DOM (byte-identical in Chrome against 5.54.2) and the same
49
+ timings. `value` / `onChange` are now optional in the type because a typed
50
+ grid does not use them.
51
+ - `cells: TimeGridCell[][]`: each cell is `{ state, disabled?, reasons?,
52
+ marks?, meta? }`. `meta` is handed back untouched in every callback.
53
+ - `states: Record<id, { tone?, label?, className? }>`: the exclusive
54
+ dimension (open / closed / booked …). `marks: Record<id, { tone?, icon?,
55
+ label? }>`: non-exclusive overlays. The kit declares the shape; the host
56
+ declares the vocabulary.
57
+ - `selectionShape: "rectangle" | "linear"`. A rectangle is the same window
58
+ across rows (09:00-17:00 Mon-Fri); a linear run goes in reading order and
59
+ crosses row ends (23:54 Mon to 00:06 Tue). `selection` is a discriminated
60
+ union, controlled or uncontrolled; `onSelectionChange(next, cells)` hands
61
+ the cells in selection order; `onSelectionCommit` fires when a drag ends
62
+ or Enter is pressed.
63
+ - Selection is derived from anchor + current cell, so a fast drag no longer
64
+ leaves gaps, and pointer capture keeps a drag tracking (clamped to the
65
+ edge) after it leaves the grid.
66
+ - `disabledRange: "clamp" | "refuse" | "span"` (default `clamp`) decides what
67
+ a range crossing a disabled cell does, identically in both shapes.
68
+ `onBlockedSelect(blocked)` reports clipped or refused cells with their
69
+ `reasons`.
70
+ - A disabled cell's `reasons` reach the `title`, the cell's
71
+ `aria-description`, and a tooltip shown while the cell has keyboard focus.
72
+ - Keyboard: the grid takes focus (`aria-activedescendant`), not every cell.
73
+ Arrows move, Shift+arrows extend from the anchor, Space selects the
74
+ focused cell, Enter commits, Escape clears. Header clicks select a whole
75
+ row or column.
76
+ - `onCellHover(cell, { r, c })` for a host-owned detail panel; `null, null`
77
+ on leave. `aria-label` names the grid.
78
+ - `cells` without `states` throws in development: an unknown vocabulary
79
+ would otherwise render as one colour. An unknown state id warns once and
80
+ renders neutral.
81
+ - Colours: state and mark tones from the existing `TimeGridTone` set; the
82
+ neutral fill, selection wash, focus outline, headers and tooltip read the
83
+ `--fancy-*` tokens. Dark mode follows `.dark` and `[data-theme="dark"]`.
84
+ - A mark's `icon` is a `ReactNode` (e.g. `<Icon name="lock" />`), not an
85
+ icon name: resolving names would import every Lucide glyph into any bundle
86
+ that uses TimeGrid (measured: 16k to 56k lines).
87
+ - **Measured (Chrome, 4x CPU throttle, 240 columns, median of 7):** typed grid
88
+ mount 157 ms at 7 days, 260 ms at 14 days, 552 ms at 31 days; update
89
+ 44-75 ms. The 14-day mount is still over the 200 ms budget. Column windowing
90
+ is the next release.
91
+
14
92
  ## [5.54.2] - 2026-10-21
15
93
 
16
94
  ### Fixed
package/dist/calendar.cjs CHANGED
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var chunkUET6D7B3_cjs = require('./chunk-UET6D7B3.cjs');
3
+ var chunkCQ7QXZLR_cjs = require('./chunk-CQ7QXZLR.cjs');
4
4
  require('./chunk-ZU5Q77OT.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "Calendar", {
9
9
  enumerable: true,
10
- get: function () { return chunkUET6D7B3_cjs.Calendar; }
10
+ get: function () { return chunkCQ7QXZLR_cjs.Calendar; }
11
11
  });
12
12
  //# sourceMappingURL=calendar.cjs.map
13
13
  //# sourceMappingURL=calendar.cjs.map
@@ -1,10 +1,24 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode } from 'react';
2
3
 
3
4
  type CalendarMode = "single" | "range" | "multiple";
4
5
  interface DateRange {
5
6
  start: Date | null;
6
7
  end: Date | null;
7
8
  }
9
+ /** Everything a per-day hook needs to know about one day cell. */
10
+ interface CalendarDayContext {
11
+ date: Date;
12
+ /** Stable id: `dateId(date)`, default `YYYY-MM-DD` in local time. Also on the cell as `data-react-fancy-calendar-day`. */
13
+ id: string;
14
+ selected: boolean;
15
+ disabled: boolean;
16
+ /** Strictly between a range's ends (range mode). */
17
+ inRange: boolean;
18
+ /** A leading/trailing day from the neighbouring month. */
19
+ outsideMonth: boolean;
20
+ today: boolean;
21
+ }
8
22
  interface CalendarProps {
9
23
  mode?: CalendarMode;
10
24
  value?: Date | Date[] | DateRange | null;
@@ -13,8 +27,23 @@ interface CalendarProps {
13
27
  maxDate?: Date;
14
28
  disabledDates?: Date[];
15
29
  className?: string;
30
+ /** Stable id per day. Default `YYYY-MM-DD` in local time. */
31
+ dateId?: (date: Date) => string;
32
+ /** The day cell's content. Default: the day number. The cell (button, click, state classes) stays the Calendar's. */
33
+ renderDay?: (ctx: CalendarDayContext) => ReactNode;
34
+ /** Extra classes for one day cell, merged after the built-in ones. */
35
+ dayClassName?: (ctx: CalendarDayContext) => string | undefined;
36
+ /**
37
+ * The day cell's accessible name. Pair it with `dayClassName`: a shaded day
38
+ * must say what the shade means. It REPLACES the name, so include the date.
39
+ */
40
+ dayAriaLabel?: (ctx: CalendarDayContext) => string | undefined;
41
+ /** Controlled visible month (any day in it). Uncontrolled when omitted; starts at today. */
42
+ visibleMonth?: Date;
43
+ /** Called with the 1st of the month navigated to, controlled or not. */
44
+ onVisibleMonthChange?: (month: Date) => void;
16
45
  }
17
46
 
18
47
  declare const Calendar: react.ForwardRefExoticComponent<CalendarProps & react.RefAttributes<HTMLDivElement>>;
19
48
 
20
- export { Calendar, type CalendarMode, type CalendarProps, type DateRange };
49
+ export { Calendar, type CalendarDayContext, type CalendarMode, type CalendarProps, type DateRange };
@@ -1,10 +1,24 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode } from 'react';
2
3
 
3
4
  type CalendarMode = "single" | "range" | "multiple";
4
5
  interface DateRange {
5
6
  start: Date | null;
6
7
  end: Date | null;
7
8
  }
9
+ /** Everything a per-day hook needs to know about one day cell. */
10
+ interface CalendarDayContext {
11
+ date: Date;
12
+ /** Stable id: `dateId(date)`, default `YYYY-MM-DD` in local time. Also on the cell as `data-react-fancy-calendar-day`. */
13
+ id: string;
14
+ selected: boolean;
15
+ disabled: boolean;
16
+ /** Strictly between a range's ends (range mode). */
17
+ inRange: boolean;
18
+ /** A leading/trailing day from the neighbouring month. */
19
+ outsideMonth: boolean;
20
+ today: boolean;
21
+ }
8
22
  interface CalendarProps {
9
23
  mode?: CalendarMode;
10
24
  value?: Date | Date[] | DateRange | null;
@@ -13,8 +27,23 @@ interface CalendarProps {
13
27
  maxDate?: Date;
14
28
  disabledDates?: Date[];
15
29
  className?: string;
30
+ /** Stable id per day. Default `YYYY-MM-DD` in local time. */
31
+ dateId?: (date: Date) => string;
32
+ /** The day cell's content. Default: the day number. The cell (button, click, state classes) stays the Calendar's. */
33
+ renderDay?: (ctx: CalendarDayContext) => ReactNode;
34
+ /** Extra classes for one day cell, merged after the built-in ones. */
35
+ dayClassName?: (ctx: CalendarDayContext) => string | undefined;
36
+ /**
37
+ * The day cell's accessible name. Pair it with `dayClassName`: a shaded day
38
+ * must say what the shade means. It REPLACES the name, so include the date.
39
+ */
40
+ dayAriaLabel?: (ctx: CalendarDayContext) => string | undefined;
41
+ /** Controlled visible month (any day in it). Uncontrolled when omitted; starts at today. */
42
+ visibleMonth?: Date;
43
+ /** Called with the 1st of the month navigated to, controlled or not. */
44
+ onVisibleMonthChange?: (month: Date) => void;
16
45
  }
17
46
 
18
47
  declare const Calendar: react.ForwardRefExoticComponent<CalendarProps & react.RefAttributes<HTMLDivElement>>;
19
48
 
20
- export { Calendar, type CalendarMode, type CalendarProps, type DateRange };
49
+ export { Calendar, type CalendarDayContext, type CalendarMode, type CalendarProps, type DateRange };
package/dist/calendar.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Calendar } from './chunk-HVND2IIB.js';
1
+ export { Calendar } from './chunk-X2A3NAJW.js';
2
2
  import './chunk-MMW2JGKZ.js';
3
3
  //# sourceMappingURL=calendar.js.map
4
4
  //# sourceMappingURL=calendar.js.map
@@ -23,6 +23,10 @@ var MONTHS = [
23
23
  function isSameDay(a, b) {
24
24
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
25
25
  }
26
+ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
27
+ function defaultDateId(date) {
28
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
29
+ }
26
30
  function getDaysInMonth(year, month) {
27
31
  const days = [];
28
32
  const firstDay = new Date(year, month, 1);
@@ -47,34 +51,36 @@ var Calendar = react.forwardRef(
47
51
  minDate,
48
52
  maxDate,
49
53
  disabledDates = [],
50
- className
54
+ className,
55
+ dateId = defaultDateId,
56
+ renderDay,
57
+ dayClassName,
58
+ dayAriaLabel,
59
+ visibleMonth,
60
+ onVisibleMonthChange
51
61
  }, ref) {
52
62
  const today = react.useMemo(() => /* @__PURE__ */ new Date(), []);
53
- const [viewMonth, setViewMonth] = react.useState(today.getMonth());
54
- const [viewYear, setViewYear] = react.useState(today.getFullYear());
63
+ const [innerMonth, setInnerMonth] = react.useState(
64
+ () => new Date(today.getFullYear(), today.getMonth(), 1)
65
+ );
55
66
  const [rangeHover, setRangeHover] = react.useState(null);
67
+ const shown = visibleMonth ?? innerMonth;
68
+ const viewMonth = shown.getMonth();
69
+ const viewYear = shown.getFullYear();
56
70
  const days = react.useMemo(
57
71
  () => getDaysInMonth(viewYear, viewMonth),
58
72
  [viewYear, viewMonth]
59
73
  );
60
- const prevMonth = react.useCallback(() => {
61
- setViewMonth((m) => {
62
- if (m === 0) {
63
- setViewYear((y) => y - 1);
64
- return 11;
65
- }
66
- return m - 1;
67
- });
68
- }, []);
69
- const nextMonth = react.useCallback(() => {
70
- setViewMonth((m) => {
71
- if (m === 11) {
72
- setViewYear((y) => y + 1);
73
- return 0;
74
- }
75
- return m + 1;
76
- });
77
- }, []);
74
+ const goToMonth = react.useCallback(
75
+ (delta) => {
76
+ const next = new Date(viewYear, viewMonth + delta, 1);
77
+ if (visibleMonth === void 0) setInnerMonth(next);
78
+ onVisibleMonthChange?.(next);
79
+ },
80
+ [viewYear, viewMonth, visibleMonth, onVisibleMonthChange]
81
+ );
82
+ const prevMonth = react.useCallback(() => goToMonth(-1), [goToMonth]);
83
+ const nextMonth = react.useCallback(() => goToMonth(1), [goToMonth]);
78
84
  const isDisabled = react.useCallback(
79
85
  (date) => {
80
86
  if (minDate && date < minDate) return true;
@@ -161,7 +167,7 @@ var Calendar = react.forwardRef(
161
167
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { size: 16 })
162
168
  }
163
169
  ),
164
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-semibold", children: [
170
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-semibold", "data-react-fancy-calendar-month": "", children: [
165
171
  MONTHS[viewMonth],
166
172
  " ",
167
173
  viewYear
@@ -191,11 +197,23 @@ var Calendar = react.forwardRef(
191
197
  const inRange = isInRange(date);
192
198
  const disabled = isDisabled(date);
193
199
  const isToday = isSameDay(date, today);
200
+ const ctx = {
201
+ date,
202
+ id: dateId(date),
203
+ selected,
204
+ disabled,
205
+ inRange,
206
+ outsideMonth: !isCurrentMonth,
207
+ today: isToday
208
+ };
194
209
  return /* @__PURE__ */ jsxRuntime.jsx(
195
210
  "button",
196
211
  {
197
212
  type: "button",
198
213
  disabled,
214
+ "data-react-fancy-calendar-day": ctx.id,
215
+ "data-outside-month": isCurrentMonth ? void 0 : "",
216
+ "aria-label": dayAriaLabel?.(ctx) || void 0,
199
217
  onClick: () => handleSelect(date),
200
218
  onMouseEnter: () => mode === "range" && setRangeHover(date),
201
219
  className: chunkZU5Q77OT_cjs.cn(
@@ -205,9 +223,10 @@ var Calendar = react.forwardRef(
205
223
  inRange && "bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
206
224
  selected && "bg-zinc-900 text-white dark:bg-white dark:text-zinc-900",
207
225
  isToday && !selected && "font-bold",
208
- disabled && "cursor-not-allowed opacity-30"
226
+ disabled && "cursor-not-allowed opacity-30",
227
+ dayClassName?.(ctx)
209
228
  ),
210
- children: date.getDate()
229
+ children: renderDay ? renderDay(ctx) : date.getDate()
211
230
  },
212
231
  i
213
232
  );
@@ -220,5 +239,5 @@ var Calendar = react.forwardRef(
220
239
  Calendar.displayName = "Calendar";
221
240
 
222
241
  exports.Calendar = Calendar;
223
- //# sourceMappingURL=chunk-UET6D7B3.cjs.map
224
- //# sourceMappingURL=chunk-UET6D7B3.cjs.map
242
+ //# sourceMappingURL=chunk-CQ7QXZLR.cjs.map
243
+ //# sourceMappingURL=chunk-CQ7QXZLR.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Calendar/Calendar.tsx"],"names":["forwardRef","Calendar","useMemo","useState","useCallback","jsxs","cn","jsx","ChevronLeft","ChevronRight"],"mappings":"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","file":"chunk-CQ7QXZLR.cjs","sourcesContent":["import { forwardRef, useCallback, useMemo, useState } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"../../utils/cn\";\nimport type { CalendarDayContext, CalendarProps, DateRange } from \"./Calendar.types\";\n\nconst DAYS = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\nconst MONTHS = [\n \"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\n \"July\", \"August\", \"September\", \"October\", \"November\", \"December\",\n];\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nconst pad2 = (n: number) => (n < 10 ? `0${n}` : String(n));\n\n/** `YYYY-MM-DD` in LOCAL time — never `toISOString()`, which shifts the day east of UTC. */\nfunction defaultDateId(date: Date): string {\n return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;\n}\n\nfunction getDaysInMonth(year: number, month: number): Date[] {\n const days: Date[] = [];\n const firstDay = new Date(year, month, 1);\n const startDayOfWeek = firstDay.getDay();\n\n // Fill leading days from previous month\n for (let i = startDayOfWeek - 1; i >= 0; i--) {\n days.push(new Date(year, month, -i));\n }\n\n // Fill current month\n const lastDate = new Date(year, month + 1, 0).getDate();\n for (let d = 1; d <= lastDate; d++) {\n days.push(new Date(year, month, d));\n }\n\n // Fill trailing days to complete 6 rows\n while (days.length < 42) {\n days.push(new Date(year, month + 1, days.length - startDayOfWeek - lastDate + 1));\n }\n\n return days;\n}\n\nexport const Calendar = forwardRef<HTMLDivElement, CalendarProps>(\n function Calendar(\n {\n mode = \"single\",\n value,\n onChange,\n minDate,\n maxDate,\n disabledDates = [],\n className,\n dateId = defaultDateId,\n renderDay,\n dayClassName,\n dayAriaLabel,\n visibleMonth,\n onVisibleMonthChange,\n },\n ref,\n ) {\n const today = useMemo(() => new Date(), []);\n const [innerMonth, setInnerMonth] = useState(\n () => new Date(today.getFullYear(), today.getMonth(), 1),\n );\n const [rangeHover, setRangeHover] = useState<Date | null>(null);\n\n const shown = visibleMonth ?? innerMonth;\n const viewMonth = shown.getMonth();\n const viewYear = shown.getFullYear();\n\n const days = useMemo(\n () => getDaysInMonth(viewYear, viewMonth),\n [viewYear, viewMonth],\n );\n\n const goToMonth = useCallback(\n (delta: number) => {\n const next = new Date(viewYear, viewMonth + delta, 1);\n if (visibleMonth === undefined) setInnerMonth(next);\n onVisibleMonthChange?.(next);\n },\n [viewYear, viewMonth, visibleMonth, onVisibleMonthChange],\n );\n const prevMonth = useCallback(() => goToMonth(-1), [goToMonth]);\n const nextMonth = useCallback(() => goToMonth(1), [goToMonth]);\n\n const isDisabled = useCallback(\n (date: Date): boolean => {\n if (minDate && date < minDate) return true;\n if (maxDate && date > maxDate) return true;\n return disabledDates.some((d) => isSameDay(d, date));\n },\n [minDate, maxDate, disabledDates],\n );\n\n const isSelected = useCallback(\n (date: Date): boolean => {\n if (!value) return false;\n if (mode === \"single\" && value instanceof Date) {\n return isSameDay(date, value);\n }\n if (mode === \"multiple\" && Array.isArray(value)) {\n return value.some((v) => isSameDay(date, v));\n }\n if (mode === \"range\" && value && typeof value === \"object\" && \"start\" in value) {\n const range = value as DateRange;\n if (range.start && isSameDay(date, range.start)) return true;\n if (range.end && isSameDay(date, range.end)) return true;\n }\n return false;\n },\n [value, mode],\n );\n\n const isInRange = useCallback(\n (date: Date): boolean => {\n if (mode !== \"range\" || !value || typeof value !== \"object\" || !(\"start\" in value))\n return false;\n const range = value as DateRange;\n if (!range.start) return false;\n const end = range.end ?? rangeHover;\n if (!end) return false;\n const [s, e] = range.start <= end ? [range.start, end] : [end, range.start];\n return date > s && date < e;\n },\n [value, mode, rangeHover],\n );\n\n const handleSelect = useCallback(\n (date: Date) => {\n if (isDisabled(date)) return;\n\n if (mode === \"single\") {\n onChange?.(date);\n } else if (mode === \"multiple\") {\n const arr = (Array.isArray(value) ? value : []) as Date[];\n const exists = arr.findIndex((d) => isSameDay(d, date));\n if (exists >= 0) {\n onChange?.(arr.filter((_, i) => i !== exists));\n } else {\n onChange?.([...arr, date]);\n }\n } else if (mode === \"range\") {\n const range = (value && typeof value === \"object\" && \"start\" in value\n ? value\n : { start: null, end: null }) as DateRange;\n\n if (!range.start || range.end) {\n onChange?.({ start: date, end: null });\n } else {\n const [s, e] =\n date >= range.start ? [range.start, date] : [date, range.start];\n onChange?.({ start: s, end: e });\n }\n }\n },\n [mode, value, onChange, isDisabled],\n );\n\n const btnBase =\n \"flex h-8 w-8 items-center justify-center rounded-lg text-sm transition-colors\";\n\n return (\n <div\n data-react-fancy-calendar=\"\"\n ref={ref}\n className={cn(\n \"inline-block w-72 rounded-xl border border-[color:var(--fancy-border,var(--color-zinc-200))] bg-[color:var(--fancy-surface,var(--color-white))] p-4 dark:border-[color:var(--fancy-border-dark,var(--color-zinc-700))] dark:bg-[color:var(--fancy-surface-dark,var(--color-zinc-900))]\",\n className,\n )}\n >\n {/* Header */}\n <div className=\"mb-3 flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={prevMonth}\n className=\"rounded-lg p-1 text-zinc-400 hover:bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:hover:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]\"\n aria-label=\"Previous month\"\n >\n <ChevronLeft size={16} />\n </button>\n <span className=\"text-sm font-semibold\" data-react-fancy-calendar-month=\"\">\n {MONTHS[viewMonth]} {viewYear}\n </span>\n <button\n type=\"button\"\n onClick={nextMonth}\n className=\"rounded-lg p-1 text-zinc-400 hover:bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:hover:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]\"\n aria-label=\"Next month\"\n >\n <ChevronRight size={16} />\n </button>\n </div>\n\n {/* Day headers */}\n <div className=\"mb-1 grid grid-cols-7 gap-1\">\n {DAYS.map((d) => (\n <div\n key={d}\n className=\"flex h-8 items-center justify-center text-xs font-medium text-zinc-400\"\n >\n {d}\n </div>\n ))}\n </div>\n\n {/* Day grid */}\n <div className=\"grid grid-cols-7 gap-1\">\n {days.map((date, i) => {\n const isCurrentMonth = date.getMonth() === viewMonth;\n const selected = isSelected(date);\n const inRange = isInRange(date);\n const disabled = isDisabled(date);\n const isToday = isSameDay(date, today);\n const ctx: CalendarDayContext = {\n date,\n id: dateId(date),\n selected,\n disabled,\n inRange,\n outsideMonth: !isCurrentMonth,\n today: isToday,\n };\n\n return (\n <button\n key={i}\n type=\"button\"\n disabled={disabled}\n data-react-fancy-calendar-day={ctx.id}\n data-outside-month={isCurrentMonth ? undefined : \"\"}\n aria-label={dayAriaLabel?.(ctx) || undefined}\n onClick={() => handleSelect(date)}\n onMouseEnter={() =>\n mode === \"range\" && setRangeHover(date)\n }\n className={cn(\n btnBase,\n !isCurrentMonth && \"text-zinc-300 dark:text-zinc-600\",\n isCurrentMonth &&\n !selected &&\n !inRange &&\n \"text-[color:var(--fancy-text-soft,var(--color-zinc-700))] hover:bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:text-[color:var(--fancy-text-soft-dark,var(--color-zinc-300))] dark:hover:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]\",\n inRange &&\n \"bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300\",\n selected &&\n \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\",\n isToday && !selected && \"font-bold\",\n disabled && \"cursor-not-allowed opacity-30\",\n dayClassName?.(ctx),\n )}\n >\n {renderDay ? renderDay(ctx) : date.getDate()}\n </button>\n );\n })}\n </div>\n </div>\n );\n },\n);\n\nCalendar.displayName = \"Calendar\";\n"]}