@jmruthers/pace-core 0.5.140 → 0.5.141

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 (156) hide show
  1. package/README.md +2 -2
  2. package/dist/{DataTable-JXFCA2BJ.js → DataTable-EGIN2NKK.js} +3 -3
  3. package/dist/{EventLogo-rFL_kRjk.d.ts → EventLogo-B3V3otev.d.ts} +307 -1
  4. package/dist/{chunk-BOOI7GK2.js → chunk-3R472UXR.js} +117 -1
  5. package/dist/chunk-3R472UXR.js.map +1 -0
  6. package/dist/{chunk-5JMOHWDI.js → chunk-ALUN6O3G.js} +492 -324
  7. package/dist/chunk-ALUN6O3G.js.map +1 -0
  8. package/dist/{chunk-6DXZ6V5Q.js → chunk-PZV3XZKJ.js} +2 -2
  9. package/dist/{chunk-TLT2ZR3L.js → chunk-WKTQM2IC.js} +2 -2
  10. package/dist/components.d.ts +3 -1
  11. package/dist/components.js +15 -3
  12. package/dist/components.js.map +1 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +18 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/rbac/index.d.ts +94 -1
  17. package/dist/rbac/index.js +4 -2
  18. package/dist/utils.js +1 -1
  19. package/docs/api/README.md +2 -2
  20. package/docs/api/classes/ColumnFactory.md +1 -1
  21. package/docs/api/classes/ErrorBoundary.md +1 -1
  22. package/docs/api/classes/InvalidScopeError.md +1 -1
  23. package/docs/api/classes/MissingUserContextError.md +1 -1
  24. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  25. package/docs/api/classes/PermissionDeniedError.md +1 -1
  26. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  27. package/docs/api/classes/RBACAuditManager.md +1 -1
  28. package/docs/api/classes/RBACCache.md +1 -1
  29. package/docs/api/classes/RBACEngine.md +1 -1
  30. package/docs/api/classes/RBACError.md +1 -1
  31. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  32. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  33. package/docs/api/classes/StorageUtils.md +1 -1
  34. package/docs/api/enums/FileCategory.md +1 -1
  35. package/docs/api/interfaces/AggregateConfig.md +1 -1
  36. package/docs/api/interfaces/BadgeProps.md +1 -1
  37. package/docs/api/interfaces/ButtonProps.md +1 -1
  38. package/docs/api/interfaces/CalendarProps.md +40 -0
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  51. package/docs/api/interfaces/EventLogoProps.md +1 -1
  52. package/docs/api/interfaces/ExportColumn.md +1 -1
  53. package/docs/api/interfaces/ExportOptions.md +1 -1
  54. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  55. package/docs/api/interfaces/FileMetadata.md +1 -1
  56. package/docs/api/interfaces/FileReference.md +1 -1
  57. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  58. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  59. package/docs/api/interfaces/FileUploadProps.md +1 -1
  60. package/docs/api/interfaces/FooterProps.md +1 -1
  61. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  62. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  63. package/docs/api/interfaces/InputProps.md +1 -1
  64. package/docs/api/interfaces/LabelProps.md +1 -1
  65. package/docs/api/interfaces/LoginFormProps.md +1 -1
  66. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  67. package/docs/api/interfaces/NavigationContextType.md +1 -1
  68. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  69. package/docs/api/interfaces/NavigationItem.md +1 -1
  70. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  71. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  72. package/docs/api/interfaces/Organisation.md +1 -1
  73. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  74. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  75. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  76. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  77. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  78. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  79. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  80. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  81. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  82. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  83. package/docs/api/interfaces/PaletteData.md +1 -1
  84. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  85. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  86. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  87. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  88. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  92. package/docs/api/interfaces/RBACConfig.md +1 -1
  93. package/docs/api/interfaces/RBACLogger.md +1 -1
  94. package/docs/api/interfaces/ResourcePermissions.md +155 -0
  95. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  97. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  98. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  99. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  100. package/docs/api/interfaces/RouteConfig.md +1 -1
  101. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  102. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  103. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  104. package/docs/api/interfaces/StorageConfig.md +1 -1
  105. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  106. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  107. package/docs/api/interfaces/StorageListOptions.md +1 -1
  108. package/docs/api/interfaces/StorageListResult.md +1 -1
  109. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  110. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  111. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  112. package/docs/api/interfaces/StyleImport.md +1 -1
  113. package/docs/api/interfaces/SwitchProps.md +1 -1
  114. package/docs/api/interfaces/TabsContentProps.md +9 -0
  115. package/docs/api/interfaces/TabsListProps.md +9 -0
  116. package/docs/api/interfaces/TabsProps.md +9 -0
  117. package/docs/api/interfaces/TabsTriggerProps.md +9 -0
  118. package/docs/api/interfaces/TextareaProps.md +53 -0
  119. package/docs/api/interfaces/ToastActionElement.md +1 -1
  120. package/docs/api/interfaces/ToastProps.md +1 -1
  121. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  122. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  123. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  125. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  130. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  132. package/docs/api/interfaces/UseResourcePermissionsOptions.md +34 -0
  133. package/docs/api/interfaces/UserEventAccess.md +1 -1
  134. package/docs/api/interfaces/UserMenuProps.md +1 -1
  135. package/docs/api/interfaces/UserProfile.md +1 -1
  136. package/docs/api/modules.md +289 -2
  137. package/package.json +3 -1
  138. package/src/components/Calendar/Calendar.test.tsx +338 -0
  139. package/src/components/Calendar/Calendar.tsx +192 -0
  140. package/src/components/Calendar/index.ts +10 -0
  141. package/src/components/Tabs/Tabs.test.tsx +439 -0
  142. package/src/components/Tabs/Tabs.tsx +202 -0
  143. package/src/components/Tabs/index.ts +10 -0
  144. package/src/components/Textarea/Textarea.test.tsx +269 -0
  145. package/src/components/Textarea/Textarea.tsx +133 -0
  146. package/src/components/Textarea/index.ts +10 -0
  147. package/src/components/index.ts +11 -0
  148. package/src/index.ts +11 -0
  149. package/src/rbac/hooks/index.ts +2 -0
  150. package/src/rbac/hooks/useResourcePermissions.test.ts +633 -0
  151. package/src/rbac/hooks/useResourcePermissions.ts +235 -0
  152. package/dist/chunk-5JMOHWDI.js.map +0 -1
  153. package/dist/chunk-BOOI7GK2.js.map +0 -1
  154. /package/dist/{DataTable-JXFCA2BJ.js.map → DataTable-EGIN2NKK.js.map} +0 -0
  155. /package/dist/{chunk-6DXZ6V5Q.js.map → chunk-PZV3XZKJ.js.map} +0 -0
  156. /package/dist/{chunk-TLT2ZR3L.js.map → chunk-WKTQM2IC.js.map} +0 -0
@@ -0,0 +1,338 @@
1
+ /**
2
+ * @file Calendar Component Tests
3
+ * @description Comprehensive tests for Calendar component
4
+ * @package @jmruthers/pace-core
5
+ */
6
+
7
+ import React from 'react';
8
+ import { screen } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ import { Calendar } from './Calendar';
12
+ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
13
+
14
+ // Mock react-day-picker to avoid complex date rendering issues in tests
15
+ vi.mock('react-day-picker', () => {
16
+ const React = require('react');
17
+ return {
18
+ DayPicker: ({ selected, onSelect, disabled, mode, className, classNames, ...props }: any) => {
19
+ const handleDateClick = (date: Date) => {
20
+ if (disabled && typeof disabled === 'function' && disabled(date)) {
21
+ return;
22
+ }
23
+ if (onSelect) {
24
+ if (mode === 'multiple') {
25
+ const current = Array.isArray(selected) ? selected : [];
26
+ const dateStr = date.toISOString();
27
+ if (current.some((d: Date) => d.toISOString() === dateStr)) {
28
+ onSelect(current.filter((d: Date) => d.toISOString() !== dateStr));
29
+ } else {
30
+ onSelect([...current, date]);
31
+ }
32
+ } else if (mode === 'range') {
33
+ // Simplified range logic for testing
34
+ onSelect({ from: date, to: date });
35
+ } else {
36
+ onSelect(date);
37
+ }
38
+ }
39
+ };
40
+
41
+ const today = new Date();
42
+ const dates = Array.from({ length: 7 }, (_, i) => {
43
+ const date = new Date(today);
44
+ date.setDate(today.getDate() + i - 3);
45
+ return date;
46
+ });
47
+
48
+ return (
49
+ <div data-testid="calendar" className={className} {...props}>
50
+ <div className="calendar-header">Calendar</div>
51
+ <div className="calendar-grid">
52
+ {dates.map((date, idx) => {
53
+ const isDisabled = disabled && typeof disabled === 'function' && disabled(date);
54
+ const isSelected =
55
+ (mode === 'single' && selected && date.toDateString() === selected.toDateString()) ||
56
+ (mode === 'multiple' && Array.isArray(selected) && selected.some((d: Date) => d.toDateString() === date.toDateString())) ||
57
+ (mode === 'range' && selected?.from && date.toDateString() === selected.from.toDateString());
58
+
59
+ return (
60
+ <button
61
+ key={idx}
62
+ data-testid={`date-${date.toISOString().split('T')[0]}`}
63
+ onClick={() => !isDisabled && handleDateClick(date)}
64
+ disabled={isDisabled}
65
+ className={isSelected ? 'selected' : ''}
66
+ aria-selected={isSelected}
67
+ >
68
+ {date.getDate()}
69
+ </button>
70
+ );
71
+ })}
72
+ </div>
73
+ </div>
74
+ );
75
+ },
76
+ };
77
+ });
78
+
79
+ describe('Calendar Component', () => {
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ });
83
+
84
+ // Basic rendering tests
85
+ describe('Rendering', () => {
86
+ it('renders calendar with default props', () => {
87
+ renderWithProviders(<Calendar />);
88
+ const calendar = screen.getByTestId('calendar');
89
+ expect(calendar).toBeInTheDocument();
90
+ });
91
+
92
+ it('renders with custom className', () => {
93
+ renderWithProviders(<Calendar className="custom-calendar" />);
94
+ const calendar = screen.getByTestId('calendar');
95
+ // className is applied to the wrapper div, check parent element
96
+ const wrapper = calendar.parentElement;
97
+ expect(wrapper).toHaveClass('custom-calendar');
98
+ });
99
+
100
+ it('forwards ref correctly', () => {
101
+ const ref = React.createRef<HTMLDivElement>();
102
+ renderWithProviders(<Calendar ref={ref} />);
103
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
104
+ });
105
+ });
106
+
107
+ // Single date selection tests
108
+ describe('Single Date Selection', () => {
109
+ it('handles single date selection', async () => {
110
+ const handleSelect = vi.fn();
111
+ const user = userEvent.setup();
112
+
113
+ renderWithProviders(
114
+ <Calendar mode="single" selected={undefined} onSelect={handleSelect} />
115
+ );
116
+
117
+ const today = new Date();
118
+ const dateStr = today.toISOString().split('T')[0];
119
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
120
+
121
+ await user.click(dateButton);
122
+
123
+ expect(handleSelect).toHaveBeenCalled();
124
+ expect(handleSelect.mock.calls[0][0]).toBeInstanceOf(Date);
125
+ });
126
+
127
+ it('displays selected date', () => {
128
+ const selectedDate = new Date();
129
+ renderWithProviders(
130
+ <Calendar mode="single" selected={selectedDate} />
131
+ );
132
+
133
+ const dateStr = selectedDate.toISOString().split('T')[0];
134
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
135
+ expect(dateButton).toHaveAttribute('aria-selected', 'true');
136
+ });
137
+ });
138
+
139
+ // Multiple date selection tests
140
+ describe('Multiple Date Selection', () => {
141
+ it('handles multiple date selection', async () => {
142
+ const handleSelect = vi.fn();
143
+ const user = userEvent.setup();
144
+
145
+ renderWithProviders(
146
+ <Calendar mode="multiple" selected={[]} onSelect={handleSelect} />
147
+ );
148
+
149
+ const today = new Date();
150
+ const dateStr = today.toISOString().split('T')[0];
151
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
152
+
153
+ await user.click(dateButton);
154
+
155
+ expect(handleSelect).toHaveBeenCalled();
156
+ expect(Array.isArray(handleSelect.mock.calls[0][0])).toBe(true);
157
+ });
158
+
159
+ it('toggles date selection in multiple mode', async () => {
160
+ const TestComponent = () => {
161
+ const today = new Date();
162
+ const [selected, setSelected] = React.useState<Date[]>([today]);
163
+ const handleSelect = vi.fn((dates) => {
164
+ setSelected(dates);
165
+ return dates;
166
+ });
167
+
168
+ return (
169
+ <Calendar mode="multiple" selected={selected} onSelect={handleSelect} />
170
+ );
171
+ };
172
+
173
+ const user = userEvent.setup();
174
+ renderWithProviders(<TestComponent />);
175
+
176
+ const today = new Date();
177
+ const dateStr = today.toISOString().split('T')[0];
178
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
179
+
180
+ expect(dateButton).toHaveAttribute('aria-selected', 'true');
181
+
182
+ await user.click(dateButton);
183
+
184
+ // After clicking, the date should be deselected (toggled off)
185
+ // Note: The mock handles the toggle logic
186
+ expect(dateButton).toBeInTheDocument();
187
+ });
188
+ });
189
+
190
+ // Range date selection tests
191
+ describe('Range Date Selection', () => {
192
+ it('handles range date selection', async () => {
193
+ const handleSelect = vi.fn();
194
+ const user = userEvent.setup();
195
+
196
+ renderWithProviders(
197
+ <Calendar mode="range" selected={undefined} onSelect={handleSelect} />
198
+ );
199
+
200
+ const today = new Date();
201
+ const dateStr = today.toISOString().split('T')[0];
202
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
203
+
204
+ await user.click(dateButton);
205
+
206
+ expect(handleSelect).toHaveBeenCalled();
207
+ const selected = handleSelect.mock.calls[0][0];
208
+ expect(selected).toHaveProperty('from');
209
+ });
210
+ });
211
+
212
+ // Disabled dates tests
213
+ describe('Disabled Dates', () => {
214
+ it('disables dates based on function', () => {
215
+ const disabledDate = new Date();
216
+ disabledDate.setDate(disabledDate.getDate() + 1);
217
+
218
+ renderWithProviders(
219
+ <Calendar
220
+ mode="single"
221
+ disabled={(date) => date > new Date()}
222
+ />
223
+ );
224
+
225
+ const dateStr = disabledDate.toISOString().split('T')[0];
226
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
227
+ expect(dateButton).toBeDisabled();
228
+ });
229
+
230
+ it('prevents selection of disabled dates', async () => {
231
+ const handleSelect = vi.fn();
232
+ const user = userEvent.setup();
233
+
234
+ const tomorrow = new Date();
235
+ tomorrow.setDate(tomorrow.getDate() + 1);
236
+
237
+ renderWithProviders(
238
+ <Calendar
239
+ mode="single"
240
+ selected={undefined}
241
+ onSelect={handleSelect}
242
+ disabled={(date) => date > new Date()}
243
+ />
244
+ );
245
+
246
+ const dateStr = tomorrow.toISOString().split('T')[0];
247
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
248
+
249
+ await user.click(dateButton);
250
+
251
+ expect(handleSelect).not.toHaveBeenCalled();
252
+ });
253
+
254
+ it('disables past dates', () => {
255
+ const yesterday = new Date();
256
+ yesterday.setDate(yesterday.getDate() - 1);
257
+
258
+ renderWithProviders(
259
+ <Calendar
260
+ mode="single"
261
+ disabled={(date) => date < new Date()}
262
+ />
263
+ );
264
+
265
+ const dateStr = yesterday.toISOString().split('T')[0];
266
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
267
+ expect(dateButton).toBeDisabled();
268
+ });
269
+ });
270
+
271
+ // Accessibility tests
272
+ describe('Accessibility', () => {
273
+ it('has proper ARIA attributes', () => {
274
+ renderWithProviders(<Calendar mode="single" />);
275
+ const calendar = screen.getByTestId('calendar');
276
+ expect(calendar).toBeInTheDocument();
277
+ });
278
+
279
+ it('marks selected dates with aria-selected', () => {
280
+ const selectedDate = new Date();
281
+ renderWithProviders(
282
+ <Calendar mode="single" selected={selectedDate} />
283
+ );
284
+
285
+ const dateStr = selectedDate.toISOString().split('T')[0];
286
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
287
+ expect(dateButton).toHaveAttribute('aria-selected', 'true');
288
+ });
289
+
290
+ it('marks unselected dates with aria-selected="false"', () => {
291
+ const selectedDate = new Date();
292
+ const unselectedDate = new Date();
293
+ unselectedDate.setDate(selectedDate.getDate() + 1);
294
+
295
+ renderWithProviders(
296
+ <Calendar mode="single" selected={selectedDate} />
297
+ );
298
+
299
+ const dateStr = unselectedDate.toISOString().split('T')[0];
300
+ const dateButton = screen.getByTestId(`date-${dateStr}`);
301
+ expect(dateButton).toHaveAttribute('aria-selected', 'false');
302
+ });
303
+ });
304
+
305
+ // Integration tests
306
+ describe('Integration', () => {
307
+ it('works with controlled state', () => {
308
+ const TestComponent = () => {
309
+ const [date, setDate] = React.useState<Date | undefined>(undefined);
310
+ return (
311
+ <div>
312
+ <Calendar mode="single" selected={date} onSelect={setDate} />
313
+ <div data-testid="selected-date">
314
+ {date ? date.toISOString().split('T')[0] : 'No date selected'}
315
+ </div>
316
+ </div>
317
+ );
318
+ };
319
+
320
+ renderWithProviders(<TestComponent />);
321
+
322
+ expect(screen.getByTestId('selected-date')).toHaveTextContent('No date selected');
323
+ });
324
+
325
+ it('handles undefined selected prop gracefully', () => {
326
+ renderWithProviders(<Calendar mode="single" selected={undefined} />);
327
+ const calendar = screen.getByTestId('calendar');
328
+ expect(calendar).toBeInTheDocument();
329
+ });
330
+
331
+ it('handles null selected prop gracefully', () => {
332
+ renderWithProviders(<Calendar mode="single" selected={null as any} />);
333
+ const calendar = screen.getByTestId('calendar');
334
+ expect(calendar).toBeInTheDocument();
335
+ });
336
+ });
337
+ });
338
+
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @file Calendar Component
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/Calendar
5
+ * @since 0.5.141
6
+ *
7
+ * A date picker calendar component built on react-day-picker.
8
+ * Provides accessible date selection with keyboard navigation and ARIA support.
9
+ *
10
+ * Features:
11
+ * - Single, range, and multiple date selection modes
12
+ * - Date disabling (past dates, weekends, etc.)
13
+ * - Localization support
14
+ * - Keyboard navigation
15
+ * - Accessible date selection
16
+ * - Customizable styling
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * // Single date selection
21
+ * <Calendar
22
+ * mode="single"
23
+ * selected={date}
24
+ * onSelect={setDate}
25
+ * />
26
+ *
27
+ * // Date range selection
28
+ * <Calendar
29
+ * mode="range"
30
+ * selected={dateRange}
31
+ * onSelect={setDateRange}
32
+ * />
33
+ *
34
+ * // Multiple date selection
35
+ * <Calendar
36
+ * mode="multiple"
37
+ * selected={dates}
38
+ * onSelect={setDates}
39
+ * />
40
+ *
41
+ * // With disabled dates
42
+ * <Calendar
43
+ * mode="single"
44
+ * selected={date}
45
+ * onSelect={setDate}
46
+ * disabled={(date) => date < new Date()}
47
+ * />
48
+ * ```
49
+ *
50
+ * @accessibility
51
+ * - WCAG 2.1 AA compliant
52
+ * - Keyboard navigation (Arrow keys, Page Up/Down, Home, End)
53
+ * - Screen reader support with proper ARIA attributes
54
+ * - Focus management
55
+ * - Date announcements
56
+ */
57
+
58
+ import * as React from 'react';
59
+ import { DayPicker, type DayPickerProps } from 'react-day-picker';
60
+ import { cn } from '../../utils/core/cn';
61
+
62
+ // ============================================================================
63
+ // CALENDAR COMPONENT
64
+ // ============================================================================
65
+
66
+ export interface CalendarProps extends Omit<DayPickerProps, 'className' | 'classNames' | 'styles'> {
67
+ /**
68
+ * Additional CSS classes to apply to the calendar wrapper
69
+ */
70
+ className?: string;
71
+ /**
72
+ * Custom classNames for DayPicker sub-components
73
+ */
74
+ classNames?: DayPickerProps['classNames'];
75
+ }
76
+
77
+ /**
78
+ * Calendar component
79
+ * A flexible, accessible calendar component for date selection.
80
+ * Built on react-day-picker with pace-core styling.
81
+ *
82
+ * @param props - Calendar configuration and styling
83
+ * @param ref - Forwarded ref (not used directly, but maintained for API consistency)
84
+ * @returns JSX.Element - The rendered calendar element
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * // Single date selection
89
+ * <Calendar
90
+ * mode="single"
91
+ * selected={date}
92
+ * onSelect={setDate}
93
+ * />
94
+ *
95
+ * // With disabled dates
96
+ * <Calendar
97
+ * mode="single"
98
+ * selected={date}
99
+ * onSelect={setDate}
100
+ * disabled={(date) => date < new Date()}
101
+ * />
102
+ * ```
103
+ */
104
+ const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
105
+ ({ className, classNames, mode, ...props }, ref) => {
106
+ return (
107
+ <div ref={ref} className={cn('p-3', className)}>
108
+ <DayPicker
109
+ mode={mode}
110
+ className="rounded-md border border-sec-200 bg-background"
111
+ classNames={{
112
+ months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
113
+ month: 'space-y-4',
114
+ caption: 'flex justify-center pt-1 relative items-center',
115
+ caption_label: 'text-sm font-medium',
116
+ nav: 'space-x-1 flex items-center',
117
+ nav_button: cn(
118
+ 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
119
+ 'border border-input hover:bg-acc-100',
120
+ 'inline-flex items-center justify-center rounded-md',
121
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2'
122
+ ),
123
+ nav_button_previous: 'absolute left-1',
124
+ nav_button_next: 'absolute right-1',
125
+ table: 'w-full border-collapse space-y-1',
126
+ head_row: 'flex',
127
+ head_cell: 'text-sec-600 rounded-md w-9 font-normal text-[0.8rem]',
128
+ row: 'flex w-full mt-2',
129
+ cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-acc-50/50 [&:has([aria-selected])]:bg-acc-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
130
+ day: cn(
131
+ 'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
132
+ 'hover:bg-acc-100 hover:text-main-600',
133
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600 focus-visible:ring-offset-2',
134
+ 'inline-flex items-center justify-center rounded-md'
135
+ ),
136
+ day_range_end: 'day-range-end',
137
+ day_selected: 'bg-main-600 text-main-50 hover:bg-main-600 hover:text-main-50 focus:bg-main-600 focus:text-main-50',
138
+ day_today: 'bg-sec-100 text-main-600 font-semibold',
139
+ day_outside: 'day-outside text-sec-400 opacity-50 aria-selected:bg-acc-50/50 aria-selected:text-sec-400 aria-selected:opacity-30',
140
+ day_disabled: 'text-sec-400 opacity-50 cursor-not-allowed',
141
+ day_range_middle: 'aria-selected:bg-acc-100 aria-selected:text-main-600',
142
+ day_hidden: 'invisible',
143
+ ...classNames,
144
+ }}
145
+ components={{
146
+ IconLeft: ({ ...props }) => (
147
+ <svg
148
+ xmlns="http://www.w3.org/2000/svg"
149
+ viewBox="0 0 24 24"
150
+ fill="none"
151
+ stroke="currentColor"
152
+ strokeWidth="2"
153
+ strokeLinecap="round"
154
+ strokeLinejoin="round"
155
+ className="h-4 w-4"
156
+ {...props}
157
+ >
158
+ <path d="m15 18-6-6 6-6" />
159
+ </svg>
160
+ ),
161
+ IconRight: ({ ...props }) => (
162
+ <svg
163
+ xmlns="http://www.w3.org/2000/svg"
164
+ viewBox="0 0 24 24"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ strokeWidth="2"
168
+ strokeLinecap="round"
169
+ strokeLinejoin="round"
170
+ className="h-4 w-4"
171
+ {...props}
172
+ >
173
+ <path d="m9 18 6-6-6-6" />
174
+ </svg>
175
+ ),
176
+ ...props.components,
177
+ }}
178
+ {...(props as any)}
179
+ />
180
+ </div>
181
+ );
182
+ }
183
+ );
184
+
185
+ Calendar.displayName = 'Calendar';
186
+
187
+ // ============================================================================
188
+ // EXPORTS
189
+ // ============================================================================
190
+
191
+ export { Calendar };
192
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file Calendar component exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Calendar
5
+ * @since 0.5.141
6
+ */
7
+
8
+ export { Calendar } from './Calendar';
9
+ export type { CalendarProps } from './Calendar';
10
+