@ahrowe/ui 0.39.1 → 0.40.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.
Files changed (138) hide show
  1. package/README.md +2 -1
  2. package/dist/cjs-types/common/calendar/calendar.d.ts +4 -0
  3. package/dist/cjs-types/common/calendar/calendar.types.d.ts +141 -0
  4. package/dist/cjs-types/common/calendar/components/agendaList/agendaList.d.ts +5 -0
  5. package/dist/cjs-types/common/calendar/components/agendaList/agendaList.types.d.ts +23 -0
  6. package/dist/cjs-types/common/calendar/components/agendaList/index.d.ts +1 -0
  7. package/dist/cjs-types/common/calendar/components/calendarHeader/calendarHeader.d.ts +4 -0
  8. package/dist/cjs-types/common/calendar/components/calendarHeader/calendarHeader.types.d.ts +17 -0
  9. package/dist/cjs-types/common/calendar/components/calendarHeader/index.d.ts +1 -0
  10. package/dist/cjs-types/common/calendar/components/compactMonth/compactMonth.d.ts +9 -0
  11. package/dist/cjs-types/common/calendar/components/compactMonth/compactMonth.types.d.ts +1 -0
  12. package/dist/cjs-types/common/calendar/components/compactMonth/index.d.ts +1 -0
  13. package/dist/cjs-types/common/calendar/components/monthView/index.d.ts +1 -0
  14. package/dist/cjs-types/common/calendar/components/monthView/monthView.d.ts +5 -0
  15. package/dist/cjs-types/common/calendar/components/monthView/monthView.types.d.ts +10 -0
  16. package/dist/cjs-types/common/calendar/components/timeGrid/index.d.ts +1 -0
  17. package/dist/cjs-types/common/calendar/components/timeGrid/timeGrid.d.ts +5 -0
  18. package/dist/cjs-types/common/calendar/components/timeGrid/timeGrid.types.d.ts +40 -0
  19. package/dist/cjs-types/common/calendar/dates.d.ts +50 -0
  20. package/dist/cjs-types/common/calendar/format.d.ts +32 -0
  21. package/dist/cjs-types/common/calendar/gesture.d.ts +32 -0
  22. package/dist/cjs-types/common/calendar/index.d.ts +2 -0
  23. package/dist/cjs-types/common/calendar/layout.d.ts +74 -0
  24. package/dist/cjs-types/common/calendar/useGestures.d.ts +37 -0
  25. package/dist/cjs-types/common/calendar/usePager.d.ts +20 -0
  26. package/dist/cjs-types/common/calendar/view.types.d.ts +26 -0
  27. package/dist/cjs-types/common/configProvider/configProvider.types.d.ts +3 -0
  28. package/dist/cjs-types/common/confirmModal/confirmModal.types.d.ts +1 -1
  29. package/dist/cjs-types/common/datePicker/index.d.ts +1 -1
  30. package/dist/cjs-types/common/modal/modal.types.d.ts +4 -3
  31. package/dist/cjs-types/common/themeProvider/theme.types.d.ts +8 -0
  32. package/dist/cjs-types/common/utils/day.d.ts +8 -0
  33. package/dist/cjs-types/common/virtualList/useColumns.d.ts +3 -1
  34. package/dist/cjs-types/index.d.ts +2 -0
  35. package/dist/esm/common/calendar/calendar.mjs +2 -0
  36. package/dist/esm/common/calendar/calendar.mjs.map +1 -0
  37. package/dist/esm/common/calendar/calendar.module.mjs +2 -0
  38. package/dist/esm/common/calendar/calendar.module.mjs.map +1 -0
  39. package/dist/esm/common/calendar/calendar.types.mjs +2 -0
  40. package/dist/esm/common/calendar/calendar.types.mjs.map +1 -0
  41. package/dist/esm/common/calendar/components/agendaList/agendaList.mjs +2 -0
  42. package/dist/esm/common/calendar/components/agendaList/agendaList.mjs.map +1 -0
  43. package/dist/esm/common/calendar/components/agendaList/agendaList.module.mjs +2 -0
  44. package/dist/esm/common/calendar/components/agendaList/agendaList.module.mjs.map +1 -0
  45. package/dist/esm/common/calendar/components/calendarHeader/calendarHeader.mjs +2 -0
  46. package/dist/esm/common/calendar/components/calendarHeader/calendarHeader.mjs.map +1 -0
  47. package/dist/esm/common/calendar/components/calendarHeader/calendarHeader.module.mjs +2 -0
  48. package/dist/esm/common/calendar/components/calendarHeader/calendarHeader.module.mjs.map +1 -0
  49. package/dist/esm/common/calendar/components/compactMonth/compactMonth.mjs +2 -0
  50. package/dist/esm/common/calendar/components/compactMonth/compactMonth.mjs.map +1 -0
  51. package/dist/esm/common/calendar/components/compactMonth/compactMonth.module.mjs +2 -0
  52. package/dist/esm/common/calendar/components/compactMonth/compactMonth.module.mjs.map +1 -0
  53. package/dist/esm/common/calendar/components/monthView/monthView.mjs +2 -0
  54. package/dist/esm/common/calendar/components/monthView/monthView.mjs.map +1 -0
  55. package/dist/esm/common/calendar/components/monthView/monthView.module.mjs +2 -0
  56. package/dist/esm/common/calendar/components/monthView/monthView.module.mjs.map +1 -0
  57. package/dist/esm/common/calendar/components/timeGrid/timeGrid.mjs +2 -0
  58. package/dist/esm/common/calendar/components/timeGrid/timeGrid.mjs.map +1 -0
  59. package/dist/esm/common/calendar/components/timeGrid/timeGrid.module.mjs +2 -0
  60. package/dist/esm/common/calendar/components/timeGrid/timeGrid.module.mjs.map +1 -0
  61. package/dist/esm/common/calendar/dates.mjs +2 -0
  62. package/dist/esm/common/calendar/dates.mjs.map +1 -0
  63. package/dist/esm/common/calendar/format.mjs +2 -0
  64. package/dist/esm/common/calendar/format.mjs.map +1 -0
  65. package/dist/esm/common/calendar/gesture.mjs +2 -0
  66. package/dist/esm/common/calendar/gesture.mjs.map +1 -0
  67. package/dist/esm/common/calendar/layout.mjs +2 -0
  68. package/dist/esm/common/calendar/layout.mjs.map +1 -0
  69. package/dist/esm/common/calendar/pager.module.mjs +2 -0
  70. package/dist/esm/common/calendar/pager.module.mjs.map +1 -0
  71. package/dist/esm/common/calendar/useGestures.mjs +2 -0
  72. package/dist/esm/common/calendar/useGestures.mjs.map +1 -0
  73. package/dist/esm/common/calendar/usePager.mjs +2 -0
  74. package/dist/esm/common/calendar/usePager.mjs.map +1 -0
  75. package/dist/esm/common/confirmModal/confirmModal.mjs +1 -1
  76. package/dist/esm/common/confirmModal/confirmModal.mjs.map +1 -1
  77. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs +1 -1
  78. package/dist/esm/common/datePicker/dateFunctions.mjs +1 -1
  79. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  80. package/dist/esm/common/input/input.mjs +1 -1
  81. package/dist/esm/common/input/input.mjs.map +1 -1
  82. package/dist/esm/common/modal/modal.mjs +1 -1
  83. package/dist/esm/common/modal/modal.mjs.map +1 -1
  84. package/dist/esm/common/popover/popover.mjs +1 -1
  85. package/dist/esm/common/popover/popover.mjs.map +1 -1
  86. package/dist/esm/common/scrollFade/scrollFade.mjs +1 -1
  87. package/dist/esm/common/tabHeader/tabHeader.module.mjs.map +1 -1
  88. package/dist/esm/common/utils/day.mjs +1 -1
  89. package/dist/esm/common/utils/day.mjs.map +1 -1
  90. package/dist/esm/common/virtualList/useColumns.mjs +1 -1
  91. package/dist/esm/common/virtualList/useColumns.mjs.map +1 -1
  92. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  93. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  94. package/dist/esm/index.mjs +1 -1
  95. package/dist/index.cjs +3 -3
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/style.css +1 -1
  98. package/dist/types/common/calendar/calendar.d.ts +4 -0
  99. package/dist/types/common/calendar/calendar.types.d.ts +141 -0
  100. package/dist/types/common/calendar/components/agendaList/agendaList.d.ts +5 -0
  101. package/dist/types/common/calendar/components/agendaList/agendaList.types.d.ts +23 -0
  102. package/dist/types/common/calendar/components/agendaList/index.d.ts +1 -0
  103. package/dist/types/common/calendar/components/calendarHeader/calendarHeader.d.ts +4 -0
  104. package/dist/types/common/calendar/components/calendarHeader/calendarHeader.types.d.ts +17 -0
  105. package/dist/types/common/calendar/components/calendarHeader/index.d.ts +1 -0
  106. package/dist/types/common/calendar/components/compactMonth/compactMonth.d.ts +9 -0
  107. package/dist/types/common/calendar/components/compactMonth/compactMonth.types.d.ts +1 -0
  108. package/dist/types/common/calendar/components/compactMonth/index.d.ts +1 -0
  109. package/dist/types/common/calendar/components/monthView/index.d.ts +1 -0
  110. package/dist/types/common/calendar/components/monthView/monthView.d.ts +5 -0
  111. package/dist/types/common/calendar/components/monthView/monthView.types.d.ts +10 -0
  112. package/dist/types/common/calendar/components/timeGrid/index.d.ts +1 -0
  113. package/dist/types/common/calendar/components/timeGrid/timeGrid.d.ts +5 -0
  114. package/dist/types/common/calendar/components/timeGrid/timeGrid.types.d.ts +40 -0
  115. package/dist/types/common/calendar/dates.d.ts +50 -0
  116. package/dist/types/common/calendar/format.d.ts +32 -0
  117. package/dist/types/common/calendar/gesture.d.ts +32 -0
  118. package/dist/types/common/calendar/index.d.ts +2 -0
  119. package/dist/types/common/calendar/layout.d.ts +74 -0
  120. package/dist/types/common/calendar/useGestures.d.ts +37 -0
  121. package/dist/types/common/calendar/usePager.d.ts +20 -0
  122. package/dist/types/common/calendar/view.types.d.ts +26 -0
  123. package/dist/types/common/configProvider/configProvider.types.d.ts +3 -0
  124. package/dist/types/common/confirmModal/confirmModal.types.d.ts +1 -1
  125. package/dist/types/common/datePicker/index.d.ts +1 -1
  126. package/dist/types/common/modal/modal.types.d.ts +4 -3
  127. package/dist/types/common/themeProvider/theme.types.d.ts +8 -0
  128. package/dist/types/common/utils/day.d.ts +8 -0
  129. package/dist/types/common/virtualList/useColumns.d.ts +3 -1
  130. package/dist/types/index.d.ts +2 -0
  131. package/docs/CLAUDE.md +7 -5
  132. package/docs/Calendar.md +184 -0
  133. package/docs/ConfigProvider.md +2 -1
  134. package/docs/ConfirmModal.md +1 -1
  135. package/docs/DatePicker.md +9 -2
  136. package/docs/Modal.md +2 -2
  137. package/docs/TimeInput.md +2 -0
  138. package/package.json +1 -1
@@ -205,6 +205,14 @@ export interface ThemeVariables {
205
205
  '--tree-selected-background'?: string;
206
206
  /** Selected row text. Falls back to --text-on-primary. */
207
207
  '--tree-selected-color'?: string;
208
+ /** Height of one hour in the week and day views. Pinching zooms from it. Falls back to 3em. */
209
+ '--calendar-hour-height'?: string;
210
+ /** Today's date marker. Falls back to --primary-color. */
211
+ '--calendar-today-color'?: string;
212
+ /** The current-time line. Falls back to --error-color. */
213
+ '--calendar-now-color'?: string;
214
+ /** Corner radius of an event. Falls back to 6px. */
215
+ '--calendar-event-radius'?: string;
208
216
  /** Page button size. Falls back to 36px. */
209
217
  '--pagination-size'?: string;
210
218
  /** Gap between page buttons. Falls back to 4px. */
@@ -14,3 +14,11 @@ export declare function formatDay({ year, month, day }: DayParts): string;
14
14
  export declare function dateToDay(date: Date): string;
15
15
  /** The first moment of a `'YYYY-MM-DD'` day in the browser's time zone. An Invalid Date for anything else. */
16
16
  export declare function dayToDate(day: string): Date;
17
+ /** The `'HH:MM'` of `date` in the browser's time zone, the value a TimeInput holds; `''` for an Invalid Date. */
18
+ export declare function dateToTime(date: Date): string;
19
+ /**
20
+ * The moment a `'YYYY-MM-DD'` day and an `'HH:MM'` time stand for in the browser's time zone,
21
+ * DatePicker's and TimeInput's values together. A time the clock skips that day resolves forwards
22
+ * (02:30 becomes 03:30), a repeated one to its first occurrence. An Invalid Date for anything else.
23
+ */
24
+ export declare function dayTimeToDate(day: string, time: string): Date;
@@ -19,6 +19,8 @@ interface UseColumnsArgs<T> {
19
19
  containerHeight: number;
20
20
  /** Re-measure `fit` columns when the container width settles (e.g. after a route transition). */
21
21
  containerWidth: number;
22
+ /** Re-measure `fit` columns when rows replace the spinner under the same `items`. */
23
+ isLoading: boolean;
22
24
  }
23
25
  export interface UseColumnsResult<T> {
24
26
  hasColumns: boolean;
@@ -48,5 +50,5 @@ export interface UseColumnsResult<T> {
48
50
  * `fit`-column width measurement that pins one shared px track across the header
49
51
  * and every row grid so they stay aligned.
50
52
  */
51
- export declare function useColumns<T>({ columns, items, multiSelect, visibleColumnKeys, onVisibleColumnsChange, persistColumnsKey, showColumnToggle, userWidths, isFrozen, rootRef, startIndex, endIndex, containerHeight, containerWidth, }: UseColumnsArgs<T>): UseColumnsResult<T>;
53
+ export declare function useColumns<T>({ columns, items, multiSelect, visibleColumnKeys, onVisibleColumnsChange, persistColumnsKey, showColumnToggle, userWidths, isFrozen, rootRef, startIndex, endIndex, containerHeight, containerWidth, isLoading, }: UseColumnsArgs<T>): UseColumnsResult<T>;
52
54
  export {};
@@ -21,6 +21,8 @@ export { default as Button } from './common/button/index.js';
21
21
  export * from './common/button/index.js';
22
22
  export { default as ButtonGroup } from './common/buttonGroup/index.js';
23
23
  export * from './common/buttonGroup/index.js';
24
+ export { default as Calendar } from './common/calendar/index.js';
25
+ export * from './common/calendar/index.js';
24
26
  export { default as Card } from './common/card/index.js';
25
27
  export * from './common/card/index.js';
26
28
  export { default as Carousel } from './common/carousel/index.js';
package/docs/CLAUDE.md CHANGED
@@ -20,10 +20,11 @@ enums and slot names. Do not infer props from a component's name.
20
20
  RadioGroup, DatePicker, TimeInput, OtpInput, IconPicker) take `formValidator`, which then owns
21
21
  the value: `new FormValidator('', [Validators.required()])`. Whole forms use
22
22
  `useFormValidatorGroup`. See FormValidator.md and FormValidatorGroup.md.
23
- - **Days and times are strings**, never a `Date`: DatePicker holds `'YYYY-MM-DD'`, TimeInput
24
- `'HH:MM'`. `dateToDay` and `dayToDate` convert in the browser's time zone; never use
25
- `toISOString().slice(0, 10)`, which gives the UTC day. DatePicker.md shows how to turn a day
26
- into a moment in a given time zone.
23
+ - **Days and times are strings**, never a `Date`: DatePicker and Calendar's `date` hold
24
+ `'YYYY-MM-DD'`, TimeInput `'HH:MM'`. Calendar events are moments, so they stay `Date`s.
25
+ `dateToDay` and `dayToDate` convert a day in the browser's time zone, `dayTimeToDate(day, time)`
26
+ and `dateToTime` a day and a time together. Never use `toISOString().slice(0, 10)`, which gives
27
+ the UTC day. DatePicker.md shows how to turn a day into a moment in a given time zone.
27
28
  - **Styling.** `className` and `style` hit the root element; `classNames.<slot>` and `styles.<slot>`
28
29
  hit named inner elements. Slot names are on each doc's **Slots** line. Every native attribute of
29
30
  the root element passes through (`id`, `aria-*`, `data-*`, `on*`).
@@ -58,6 +59,7 @@ enums and slot names. Do not infer props from a component's name.
58
59
  | Breadcrumb.md | Breadcrumb | A trail showing the current page's position within a hierarchy, with links back to each ancestor, such as a file browser path, a category/subcategory drill-down, or a multi-level settings page |
59
60
  | Button.md | Button, ButtonStyleType, ButtonSize, ButtonType | Primary interactive action trigger |
60
61
  | ButtonGroup.md | ButtonGroup, ButtonGroupOrientation | Joins a row (or column) of buttons into a single seamless control, merging their borders and rounding only the outer corners |
62
+ | Calendar.md | Calendar, CalendarView | Showing and editing your own events over time, in month, week, day and agenda views: bookings, shifts, appointments, a team schedule |
61
63
  | Card.md | Card, CardStyleType, CardHeader, CardMedia, CardContent, CardActions | Surface container for grouping related content — list items, profile cards, dashboard tiles, product listings |
62
64
  | Carousel.md | Carousel | A slide track for stepping through a set of items one at a time, with arrows, dots, and drag/swipe support, plus optional autoplay |
63
65
  | Checkbox.md | Checkbox | Boolean toggle input — settings, multi-select lists, accept terms, feature flags |
@@ -67,7 +69,7 @@ enums and slot names. Do not infer props from a component's name.
67
69
  | ConfigProvider.md | ConfigProvider | Set global **default props** for library components once, near the root of your app, instead of repeating the same prop on every instance |
68
70
  | ConfirmModal.md | ConfirmModal | Confirmation dialog before destructive or irreversible actions — delete, archive, send, overwrite |
69
71
  | CopyButton.md | CopyButton | Icon-only button that copies a value to the clipboard: an invoice number, an ID in a table row, an API key or share link inside an `Input` |
70
- | DatePicker.md | DatePicker, dateToDay, dayToDate | Date selection input — booking forms, birth date fields, date range pickers, deadline selectors |
72
+ | DatePicker.md | DatePicker, dateToDay, dayToDate, dateToTime, dayTimeToDate | Date selection input — booking forms, birth date fields, date range pickers, deadline selectors |
71
73
  | Divider.md | Divider, DividerTextPosition, DividerOrientation | A rule that separates sections of content — horizontal by default, filling the width of its parent |
72
74
  | Drawer.md | Drawer | Inline horizontal panel that collapses and expands by animating its width — side panels, navigation rails, detail/filter panes that slide open beside the main content |
73
75
  | DropZone.md | DropZone, DropZoneReturnType, DropZoneStatus | Drag-and-drop file upload area, optionally click-to-open the native picker |
@@ -0,0 +1,184 @@
1
+ # Calendar
2
+
3
+ **When to use:** Showing and editing your own events over time, in month, week, day and agenda views: bookings, shifts, appointments, a team schedule. To let a user pick one date, use [DatePicker](DatePicker.md); for a sequence of past steps, use [Timeline](Timeline.md).
4
+
5
+ **Keywords:** schedule, scheduler, planner, appointments, booking, meetings, week view, month view, drag and drop, events
6
+
7
+ **Import:** `import { Calendar, CalendarView } from '@ahrowe/ui'`
8
+ **Types:** `import type { CalendarProps, CalendarEvent, CalendarRange, CalendarEventChange, CalendarEventContext, CalendarSlot, CalendarLabels, CalendarWeekday, CalendarWeekStart, CalendarSlots, CalendarDefaults } from '@ahrowe/ui'`
9
+
10
+ ```tsx
11
+ import { useState } from 'react';
12
+ import { Calendar, CalendarView, Popover, PopoverPlacement, Presentation } from '@ahrowe/ui';
13
+ import type { CalendarEvent, CalendarSlot } from '@ahrowe/ui';
14
+
15
+ // Your own event type: the calendar hands the same objects back in every callback.
16
+ interface Booking extends CalendarEvent {
17
+ room: string;
18
+ }
19
+
20
+ const [bookings, setBookings] = useState<Booking[]>(initial);
21
+ // A clicked event, or a new slot that has no id yet.
22
+ const [editing, setEditing] = useState<{ booking: Booking | CalendarSlot; anchor: HTMLElement } | null>(null);
23
+
24
+ <Calendar<Booking>
25
+ events={bookings}
26
+ defaultView={CalendarView.Week}
27
+ // Setting a callback is what switches the interaction on.
28
+ onEventClick={(booking, anchor) => setEditing({ booking, anchor })}
29
+ onSlotCreate={(slot, anchor) => setEditing({ booking: slot, anchor })}
30
+ onEventChange={({ event, start, end }) =>
31
+ setBookings((all) => all.map((b) => (b.id === event.id ? { ...b, start, end } : b)))
32
+ }
33
+ // One line in a short event or a month row; a second line where there is room for it.
34
+ renderEvent={(booking, { singleLine }) =>
35
+ singleLine ? booking.title : <>{booking.title}<small className='room'>{booking.room}</small></>
36
+ }
37
+ />
38
+
39
+ // The editor is yours: next to the event on a desktop, a bottom sheet on a phone.
40
+ <Popover
41
+ anchor={editing?.anchor ?? null}
42
+ isOpen={editing !== null}
43
+ onOpenChange={(isOpen) => !isOpen && setEditing(null)}
44
+ placement={PopoverPlacement.Right}
45
+ presentation={Presentation.Auto}
46
+ content={editing && <BookingEditor booking={editing.booking} onDone={() => setEditing(null)} />}
47
+ />
48
+
49
+ // Read-only month and agenda
50
+ <Calendar events={events} views={[CalendarView.Month, CalendarView.Agenda]} />
51
+
52
+ // A working week that opens at 06:00
53
+ <Calendar events={events} defaultView={CalendarView.Week} hiddenDays={[0, 6]} scrollToHour={6} />
54
+ ```
55
+
56
+ **Enums:** `CalendarView`: `Month`, `Week`, `Day`, `Agenda`
57
+
58
+ **Days and moments:** the selected day (`date`) is a `'YYYY-MM-DD'` string, like DatePicker's, and is the same day wherever the calendar is opened. Events are moments, so their `start` and `end` are `Date`s, shown in the browser's time zone. `dayTimeToDate` and `dateToTime` turn one into the other; see "When a day stands for a moment" in [DatePicker.md](DatePicker.md), also for a zone other than the browser's.
59
+
60
+ **Events** are plain objects: `{ id, title, start, end, allDay?, color?, readOnly? }`.
61
+
62
+ - `end` is **exclusive**, for timed and all-day events alike. A one-day all-day event on the 26th runs from midnight on the 26th to midnight on the 27th. A timed event ending at midnight does not show on the next day.
63
+ - Timed events of 24 hours or more are drawn in the all-day row, not stretched down the time grid.
64
+ - A timed event is drawn exactly as long as it lasts. One under 15 minutes has no room for its title, so it is a bar of its real length with the title on a line beneath it, and the next event in its column leaves that line free. Zoomed in far enough, the bar holds its title like any other event.
65
+ - `color` is any CSS colour, `var(--success-color)` included. The event is tinted from it against the page background, so any colour reads in both light and dark themes.
66
+ - `readOnly` events can still be clicked, but not moved or resized.
67
+
68
+ **Loading events and recurring series.** `onRangeChange` fires on mount and whenever the shown period changes, with that period **and one either side of it**, because swiping reveals the neighbours before it pages. Fetch events for that range, and expand recurring series into it yourself: the calendar has no recurrence rules of its own.
69
+
70
+ ```tsx
71
+ const [events, setEvents] = useState<CalendarEvent[]>([]);
72
+
73
+ <Calendar
74
+ events={events}
75
+ onRangeChange={async ({ start, end }) => setEvents(await api.events(start, end))}
76
+ />
77
+ ```
78
+
79
+ **Editing:**
80
+
81
+ - With a **mouse**, drag on empty time to create, drag an event to move it, and drag its bottom edge to resize it. A plain click on empty time creates an hour (or `slotMinutes`, if longer); in the month view it creates an all-day slot.
82
+ - With **touch**, press and hold for 400ms to pick up, then drag. A tap does what a click does. On a coarse pointer the resize grip is always visible.
83
+ - Moving and resizing snap to `slotMinutes`. A live time label follows the drag, and `Escape` cancels it.
84
+ - Update `events` in the same render as `onEventChange`, or **return a promise**: the event then stays where it was dropped until the promise settles, and is drawn from `events` again after that. So update `events` before it resolves, and reject to put the event back:
85
+
86
+ ```tsx
87
+ onEventChange={async ({ event, start, end }) => {
88
+ await api.save(event.id, { start, end }); // rejects when saving fails: the event goes back
89
+ setBookings((all) => all.map((b) => (b.id === event.id ? { ...b, start, end } : b)));
90
+ }}
91
+ ```
92
+
93
+ **An event editor.** The calendar has none of its own, because what an event holds and how it is saved are yours. The Editing example on the [demo page](https://ahrowe.com/packages/ui/demo/#calendar) is one to copy, built from `Input`, `DatePicker`, `TimeInput`, `Checkbox` and `ActionButtons`; its **Code** tab has all of it. The part that is easy to get wrong is the conversion, because the fields hold days and times and the event holds moments:
94
+
95
+ ```tsx
96
+ import { dateToDay, dateToTime, dayTimeToDate, dayToDate } from '@ahrowe/ui';
97
+ import type { CalendarEvent, CalendarSlot } from '@ahrowe/ui';
98
+
99
+ // What the editor's DatePickers and TimeInputs hold.
100
+ interface Draft { startDay: string; startTime: string; endDay: string; endTime: string; allDay: boolean }
101
+
102
+ function toDraft({ start, end, allDay = false }: Pick<CalendarEvent, 'start' | 'end' | 'allDay'>): Draft {
103
+ // `end` is exclusive, so an all-day event ends at midnight after its last day. Show the last day.
104
+ const lastDay = allDay ? new Date(end.getTime() - 1) : end;
105
+ return { allDay, startDay: dateToDay(start), startTime: dateToTime(start), endDay: dateToDay(lastDay), endTime: dateToTime(end) };
106
+ }
107
+
108
+ function fromDraft({ startDay, startTime, endDay, endTime, allDay }: Draft): CalendarSlot {
109
+ if (!allDay) return { allDay, start: dayTimeToDate(startDay, startTime), end: dayTimeToDate(endDay, endTime) };
110
+ const end = dayToDate(endDay);
111
+ end.setDate(end.getDate() + 1);
112
+ return { allDay, start: dayToDate(startDay), end };
113
+ }
114
+ ```
115
+
116
+ The example also:
117
+
118
+ - **keeps the length when the start changes**, as calendar apps do, counting whole days for an all-day event, since a day the clocks change on is 23 or 25 hours long;
119
+ - **shows a placeholder while a new event is written**: it adds the draft to `events` as a `readOnly` event, and Cancel drops it;
120
+ - **disables Save** until the title is filled in and the end is after the start.
121
+
122
+ **Hiding days:** `hiddenDays` leaves weekdays out of every view, `[0, 6]` being the weekend. The week and month grids close up without them, the day view and its arrows step over them, and a selected day that is hidden shows as the next one after it. An event on hidden days only is not drawn; one that runs into them is drawn on its shown days.
123
+
124
+ **Navigating:** the header's arrows, **Today** and the view switcher. In the month, week and day views a horizontal swipe on touch pages to the next or previous period, with its events already in view while you swipe; the agenda is a list and pages with the arrows only. Pinch the week or day view, or hold Ctrl and use the wheel or trackpad, to zoom the hours; the zoom is kept when switching views. Clicking a day number, or a weekday in the week view, opens that day.
125
+
126
+ **On a narrow calendar** (under 600px of its own width, not the viewport's) the month view becomes a grid with coloured dots above the selected day's events. Tapping a day selects it rather than opening it, and with `onSlotCreate` set, a **New event** button above the list creates an all-day slot on the selected day. Scrolling the list folds the month up into the selected week; swiping then pages by week instead of by month. Under `prefers-reduced-motion: reduce` the month folds in one step, once the list has scrolled the whole way, instead of following the scroll.
127
+
128
+ **Key props:**
129
+
130
+ | Prop | Type | Description |
131
+ |------|------|-------------|
132
+ | `events` | `E[]` | The events to show, `E extends CalendarEvent` |
133
+ | `date` / `defaultDate` / `onDateChange` | `string` | Selected day, `'YYYY-MM-DD'`, controlled or uncontrolled: the same value a [DatePicker](DatePicker.md) holds, so the two can share state |
134
+ | `view` / `defaultView` / `onViewChange` | `CalendarView` | Shown view, controlled or uncontrolled (default `Month`) |
135
+ | `views` | `CalendarView[]` | Views in the switcher, in order; hidden when there is one (default all four) |
136
+ | `locale` | `string` | BCP 47 tag for dates, times and the week start (default the browser's) |
137
+ | `weekStartsOn` | `0` to `6` | First day of the week, `0` being Sunday (default the locale's, else Monday) |
138
+ | `slotMinutes` | `number` | What creating, moving and resizing snap to (default `15`) |
139
+ | `hiddenDays` | `CalendarWeekday[]` | Weekdays left out of every view, `0` being Sunday: `[0, 6]` hides the weekend |
140
+ | `scrollToHour` | `number` | Hour the week and day views open at, such as `6` (default an hour before now while today is shown, otherwise `7`) |
141
+ | `onRangeChange` | `(range: CalendarRange) => void` | The shown period plus one on either side |
142
+ | `onEventClick` | `(event: E, anchor: HTMLElement) => void` | An event was clicked; `anchor` is its element, ready for `Popover`'s `anchor` |
143
+ | `onEventChange` | `(change: CalendarEventChange<E>) => void \| Promise<unknown>` | Moved or resized. Setting it makes events draggable. A returned promise keeps the event in place until it settles |
144
+ | `onSlotCreate` | `(slot: CalendarSlot, anchor: HTMLElement) => void` | Empty time clicked or dragged over. Setting it enables creating. `anchor` is the day's cell in the month view (the **New event** button in a narrow one) and an invisible box over the new slot in the week and day views, ready for `Popover`'s `anchor` |
145
+ | `renderEvent` | `(event: E, context: CalendarEventContext) => ReactNode` | Content of an event in place of its title. No interactive elements. `context.singleLine` is set where it has one line: an event under 45 minutes, an all-day bar, a month row. Keep to the title there |
146
+ | `ref` | `Ref<HTMLDivElement>` | The root element |
147
+
148
+ Give the calendar a height. It is `40em` tall by default and scrolls inside itself; `style={{ height: '100dvh' }}` or a class sets another.
149
+
150
+ **Keyboard:** in the month view the days are a grid with one tab stop on the selected day: `←`/`→` move a day, `↑`/`↓` a week, `Home`/`End` go to the start and end of the week, `PageUp`/`PageDown` a month. `Enter` or `Space` opens a list of that day's events, with a **New event** button when `onSlotCreate` is set. In the week and day views every event is a button, in time order.
151
+
152
+ **Accessibility:**
153
+
154
+ - The period title is a polite live region, so paging is announced.
155
+ - Days carry their full date and event count as their name, and today has `aria-current="date"`.
156
+ - Each day column of the time grid is a group named by its date.
157
+ - **Dragging needs a single-pointer alternative (WCAG 2.5.7).** Creating has one, a click. For moving and resizing it has to be your `onEventClick` dialog, so let it edit the start and end.
158
+
159
+ **Theming:** these are theme variables (typed on `ThemeVariables`). Set them theme-wide via `ThemeProvider`'s `variables`, or per instance via `style`:
160
+
161
+ | Variable | Falls back to |
162
+ |----------|---------------|
163
+ | `--calendar-hour-height` | `3em` (the height of one hour; pinching zooms from it) |
164
+ | `--calendar-today-color` | `var(--primary-color)` |
165
+ | `--calendar-now-color` | `var(--error-color)` |
166
+ | `--calendar-event-radius` | `6px` |
167
+
168
+ **Global defaults:** `ConfigProvider` sets `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays` and `scrollToHour` for every calendar, e.g. `defaultProps={{ Calendar: { locale: 'de-AT', weekStartsOn: 1 } }}`. The other props depend on your event type, so they are set per instance. See [ConfigProvider.md](ConfigProvider.md).
169
+
170
+ **Slots:** `root` `header` `dayHeader` `cell` `event`
171
+
172
+ **Labels:** `previous` `next` `today` `viewSwitcher` `month` `week` `day` `agenda` `allDay` `more` `eventCount` `noEvents` `newEvent`: the text this component renders of its own. `more` and `eventCount` are functions of a count. Dates and times come from `locale`, not from here. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; keys merge individually. Type: `CalendarLabels`.
173
+
174
+ **Limits:**
175
+
176
+ - **One time zone, the browser's.** A `timeZone` prop would need `Temporal`, which iOS Safari does not ship yet.
177
+ - **No recurrence rules.** Expand series for the range `onRangeChange` reports.
178
+ - **Moving stays within the shown period**, and between days or times of the same kind: an event cannot be dragged from the time grid into the all-day row.
179
+ - **No right-to-left layout.**
180
+ - **Safari on macOS** sends a trackpad pinch as a gesture event rather than a Ctrl+wheel, so there it zooms the page as usual; pinching on a touch screen zooms the hours in every browser.
181
+ - **A controlled `date` must update in the same render as `onDateChange`.** After a swipe, the calendar shows the new period's panes straight away; a parent that updates later makes it jump back and forth.
182
+ - **A tap outside your own popover also reaches the calendar.** `Popover` closes on pointer release and lets the click through, so a tap on empty time closes it and creates a slot: an editor opened from `onSlotCreate` moves to the new slot.
183
+
184
+ HTML attributes such as `id`, `aria-label` and `data-*` are passed to the root element.
@@ -95,6 +95,7 @@ Each entry is a `Partial<...Props>`, so any of that component's props can be def
95
95
  - **Display:** `Button` · `CopyButton` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider` · `Timer` · `WeatherIcon`
96
96
  - **Overlays:** `ConfirmModal` · `Modal` · `CommandPalette` · `Popover`
97
97
  - **Editors:** `RoomDrawer`, `RoomViewer`
98
+ - **Scheduling:** `Calendar`, for `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays` and `scrollToHour` only (see below)
98
99
 
99
100
  ```tsx
100
101
  <ConfigProvider
@@ -134,6 +135,6 @@ Every string a component renders of its own is set through its `labels` prop, an
134
135
 
135
136
  **Labels merge per key**, unlike every other default here. An instance that overrides one string keeps the provider's translations for the rest, so `<Alert labels={{ dismiss: 'Weg damit' }} />` under the provider above does not lose anything else Alert might have been given. Anything the component does not produce itself, a `title`, a column `label`, a button's children, is already yours and never appears in `labels`.
136
137
 
137
- `ColorPicker` and `VirtualList` appear in `ComponentDefaults` with their labels only: the first is configured elsewhere, the second has generic props that a per-key default map cannot express. `Toast` is not there at all, because `ToastProvider` is already its app-wide configuration and carries `labels` itself.
138
+ `ColorPicker` and `VirtualList` appear in `ComponentDefaults` with their labels only: the first is configured elsewhere, the second has generic props that a per-key default map cannot express. `Calendar` is generic too, so its entry is the props that do not depend on the event type: `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays` and `scrollToHour`. `Toast` is not there at all, because `ToastProvider` is already its app-wide configuration and carries `labels` itself.
138
139
 
139
140
  Each component's doc lists its own keys under **Labels:**.
@@ -34,7 +34,7 @@ const [isOpen, setIsOpen] = useState(false);
34
34
  | Prop | Type | Description |
35
35
  |------|------|-------------|
36
36
  | `isOpen` | `boolean` | Controls visibility |
37
- | `title` | `string` | Dialog heading |
37
+ | `title` | `ReactNode` | Dialog heading |
38
38
  | `content` | `ReactNode` | Dialog body |
39
39
  | `onConfirm` | `() => void \| Promise<void>` | Called when user confirms |
40
40
  | `onClose` | `(didSubmit: boolean) => void` | Called on both confirm and cancel |
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Keyboard:** the day grid uses a roving tab stop (the selected day, or today, or the 1st — one Tab reaches it, not every day individually); arrow keys move by day (`←`/`→`) or by week (`↑`/`↓`), crossing into the adjacent month automatically when they run past the edge, and Enter/Space selects the focused day.
8
8
 
9
- **Import:** `import { DatePicker, dateToDay, dayToDate } from '@ahrowe/ui'`
9
+ **Import:** `import { DatePicker, dateToDay, dayToDate, dateToTime, dayTimeToDate } from '@ahrowe/ui'`
10
10
  **Types:** `import type { DatePickerProps } from '@ahrowe/ui'`
11
11
 
12
12
  ```tsx
@@ -93,7 +93,14 @@ const closesAt = DateTime.fromISO(day, { zone: 'Europe/Vienna' }).endOf('day').t
93
93
  const shown = DateTime.fromJSDate(closesAt, { zone: 'Europe/Vienna' }).toISODate();
94
94
  ```
95
95
 
96
- With a [TimeInput](TimeInput.md) next to it, which holds `'HH:MM'`, the two make a moment the same way: ``DateTime.fromISO(`${day}T${time}`, { zone })``, or, in the user's own zone, `dayToDate(day)` followed by `setHours(hours, minutes)`.
96
+ With a [TimeInput](TimeInput.md) next to it, which holds `'HH:MM'`, the two make one moment. In the user's own zone:
97
+
98
+ ```ts
99
+ const start = dayTimeToDate(day, time); // an Invalid Date unless both are complete
100
+ const [shownDay, shownTime] = [dateToDay(start), dateToTime(start)]; // and back, for the two inputs
101
+ ```
102
+
103
+ On the day the clocks go forward a time can be missing (02:30 in Vienna on the last Sunday of March); `dayTimeToDate` moves it on by the gap, to 03:30. On the day they go back a time happens twice, and it takes the first. In a given zone the same comes from Luxon: ``DateTime.fromISO(`${day}T${time}`, { zone })``.
97
104
 
98
105
  ## Migration from Date values
99
106
 
package/docs/Modal.md CHANGED
@@ -48,7 +48,7 @@ const [isOpen, setIsOpen] = useState(false);
48
48
  | Prop | Type | Description |
49
49
  |------|------|-------------|
50
50
  | `isOpen` | `boolean` | Controls visibility |
51
- | `title` | `string` | Header title |
51
+ | `title` | `ReactNode` | Header title, also the dialog's accessible name |
52
52
  | `onClose` | `() => void` | Called when the backdrop is clicked, the close button is pressed, or Escape is pressed |
53
53
  | `footer` | `ReactNode` | Pinned below the scrolling body, e.g. action buttons. Nothing renders when omitted |
54
54
  | `hideHeader` | `boolean` | Removes the header bar |
@@ -92,7 +92,7 @@ To switch it off and go back to a hard edge, set `--modal-scroll-fade: 0px`, per
92
92
 
93
93
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Modal: { presentation: Presentation.Auto } }}` — which also covers `DatePicker`, `TimeInput` and `KlipyPicker` when they open in a Modal via their own `isModal`. `presentation={Presentation.Auto}` on the provider itself sets every sheet-capable component at once. See [ConfigProvider.md](ConfigProvider.md).
94
94
 
95
- The panel also takes the full native `div` attribute set (`id`, `role`, `aria-*`, `data-*`, …). Pass `aria-label` when using `hideHeader`, since there is then no title to label the dialog with.
95
+ The panel also takes the full native `div` attribute set (`id`, `role`, `aria-*`, `data-*`, …). The title's text names the dialog for screen readers. Pass `aria-label` when using `hideHeader`, or when the title has no text of its own (an icon alone): it then takes over from the title.
96
96
 
97
97
  **Slots:** `container` `backdrop` `modal` `header` `title` `closeButton` `body` `footer`
98
98
 
package/docs/TimeInput.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  The value is a `"HH:MM"` 24-hour string (e.g. `"09:30"`) everywhere — also when `use24Hour={false}` is set (that flag only changes the dial UI, not the stored value).
10
10
 
11
+ With a [DatePicker](DatePicker.md) beside it, `dayTimeToDate(day, time)` makes the two one `Date`, and `dateToTime(date)` gives a `Date`'s time back for the field. See "When a day stands for a moment" there.
12
+
11
13
  ```tsx
12
14
  import { TimeInput, FormValidator } from '@ahrowe/ui';
13
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "description": "A React UI component library with theming, CSS Modules, and TypeScript support",
5
5
  "keywords": [
6
6
  "agent",