@jmruthers/pace-core 0.5.66 → 0.5.67

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 (112) hide show
  1. package/dist/{PublicLoadingSpinner-CXJ-W9wZ.d.ts → PublicLoadingSpinner-DdKXTkCZ.d.ts} +94 -1
  2. package/dist/{chunk-PSE2XO4L.js → chunk-ZB6AEA7I.js} +308 -260
  3. package/dist/chunk-ZB6AEA7I.js.map +1 -0
  4. package/dist/components.d.ts +2 -1
  5. package/dist/components.js +3 -1
  6. package/dist/components.js.map +1 -1
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.js +3 -1
  9. package/dist/index.js.map +1 -1
  10. package/docs/api/classes/ColumnFactory.md +1 -1
  11. package/docs/api/classes/ErrorBoundary.md +1 -1
  12. package/docs/api/classes/InvalidScopeError.md +1 -1
  13. package/docs/api/classes/MissingUserContextError.md +1 -1
  14. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  15. package/docs/api/classes/PermissionDeniedError.md +1 -1
  16. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  17. package/docs/api/classes/RBACAuditManager.md +1 -1
  18. package/docs/api/classes/RBACCache.md +1 -1
  19. package/docs/api/classes/RBACEngine.md +1 -1
  20. package/docs/api/classes/RBACError.md +1 -1
  21. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  22. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  23. package/docs/api/classes/StorageUtils.md +1 -1
  24. package/docs/api/interfaces/AggregateConfig.md +1 -1
  25. package/docs/api/interfaces/ButtonProps.md +1 -1
  26. package/docs/api/interfaces/CardProps.md +1 -1
  27. package/docs/api/interfaces/ColorPalette.md +1 -1
  28. package/docs/api/interfaces/ColorShade.md +1 -1
  29. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  30. package/docs/api/interfaces/DataTableAction.md +1 -1
  31. package/docs/api/interfaces/DataTableColumn.md +1 -1
  32. package/docs/api/interfaces/DataTableProps.md +1 -1
  33. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  34. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  35. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  36. package/docs/api/interfaces/EventContextType.md +1 -1
  37. package/docs/api/interfaces/EventLogoProps.md +1 -1
  38. package/docs/api/interfaces/EventProviderProps.md +1 -1
  39. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  40. package/docs/api/interfaces/FileUploadProps.md +1 -1
  41. package/docs/api/interfaces/FooterProps.md +1 -1
  42. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  43. package/docs/api/interfaces/InputProps.md +1 -1
  44. package/docs/api/interfaces/LabelProps.md +1 -1
  45. package/docs/api/interfaces/LoginFormProps.md +1 -1
  46. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  47. package/docs/api/interfaces/NavigationContextType.md +1 -1
  48. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  49. package/docs/api/interfaces/NavigationItem.md +1 -1
  50. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  51. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  52. package/docs/api/interfaces/Organisation.md +1 -1
  53. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  54. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  55. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  56. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  57. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  58. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  59. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  60. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  61. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  62. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  63. package/docs/api/interfaces/PaletteData.md +1 -1
  64. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  65. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  66. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  67. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  68. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  69. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  70. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  71. package/docs/api/interfaces/RBACConfig.md +1 -1
  72. package/docs/api/interfaces/RBACContextType.md +1 -1
  73. package/docs/api/interfaces/RBACLogger.md +1 -1
  74. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  75. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  76. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  77. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  78. package/docs/api/interfaces/RouteConfig.md +1 -1
  79. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  80. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  81. package/docs/api/interfaces/StorageConfig.md +1 -1
  82. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  83. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  84. package/docs/api/interfaces/StorageListOptions.md +1 -1
  85. package/docs/api/interfaces/StorageListResult.md +1 -1
  86. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  87. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  88. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  89. package/docs/api/interfaces/StyleImport.md +1 -1
  90. package/docs/api/interfaces/SwitchProps.md +34 -0
  91. package/docs/api/interfaces/ToastActionElement.md +1 -1
  92. package/docs/api/interfaces/ToastProps.md +1 -1
  93. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  94. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  95. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  96. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  97. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  98. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  99. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  100. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  101. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  102. package/docs/api/interfaces/UserEventAccess.md +1 -1
  103. package/docs/api/interfaces/UserMenuProps.md +1 -1
  104. package/docs/api/interfaces/UserProfile.md +1 -1
  105. package/docs/api/modules.md +36 -2
  106. package/package.json +2 -1
  107. package/src/components/Switch/Switch.test.tsx +438 -0
  108. package/src/components/Switch/Switch.tsx +140 -0
  109. package/src/components/Switch/index.ts +9 -0
  110. package/src/components/index.ts +2 -0
  111. package/src/index.ts +2 -0
  112. package/dist/chunk-PSE2XO4L.js.map +0 -1
@@ -0,0 +1,438 @@
1
+ /**
2
+ * @file Switch Component Tests
3
+ * @description Comprehensive tests for Switch 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 { Switch } from './Switch';
12
+ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
13
+
14
+ describe('Switch Component', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ describe('Rendering', () => {
20
+ it('renders with default props', () => {
21
+ renderWithProviders(<Switch />);
22
+ const switchElement = screen.getByRole('switch');
23
+ expect(switchElement).toBeInTheDocument();
24
+ expect(switchElement).not.toBeChecked();
25
+ });
26
+
27
+ it('renders with custom className', () => {
28
+ renderWithProviders(<Switch className="custom-switch" />);
29
+ const switchElement = screen.getByRole('switch');
30
+ expect(switchElement).toHaveClass('custom-switch');
31
+ });
32
+
33
+ it('renders with custom id', () => {
34
+ renderWithProviders(<Switch id="test-switch" />);
35
+ const switchElement = screen.getByRole('switch');
36
+ expect(switchElement).toHaveAttribute('id', 'test-switch');
37
+ });
38
+
39
+ it('forwards ref correctly', () => {
40
+ const ref = React.createRef<HTMLButtonElement>();
41
+ renderWithProviders(<Switch ref={ref} />);
42
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
43
+ });
44
+
45
+ it('applies all HTML attributes', () => {
46
+ renderWithProviders(
47
+ <Switch
48
+ data-testid="test-switch"
49
+ aria-label="Test switch"
50
+ title="Test title"
51
+ />
52
+ );
53
+ const switchElement = screen.getByRole('switch');
54
+ expect(switchElement).toHaveAttribute('data-testid', 'test-switch');
55
+ expect(switchElement).toHaveAttribute('aria-label', 'Test switch');
56
+ expect(switchElement).toHaveAttribute('title', 'Test title');
57
+ });
58
+ });
59
+
60
+ describe('State Management', () => {
61
+ it('renders as unchecked by default', () => {
62
+ renderWithProviders(<Switch />);
63
+ const switchElement = screen.getByRole('switch');
64
+ expect(switchElement).not.toBeChecked();
65
+ });
66
+
67
+ it('renders as checked when checked prop is true', () => {
68
+ renderWithProviders(<Switch checked={true} />);
69
+ const switchElement = screen.getByRole('switch');
70
+ expect(switchElement).toBeChecked();
71
+ });
72
+
73
+ it('renders as unchecked when checked prop is false', () => {
74
+ renderWithProviders(<Switch checked={false} />);
75
+ const switchElement = screen.getByRole('switch');
76
+ expect(switchElement).not.toBeChecked();
77
+ });
78
+
79
+ it('handles controlled state changes', () => {
80
+ const handleCheckedChange = vi.fn();
81
+ renderWithProviders(
82
+ <Switch
83
+ checked={false}
84
+ onCheckedChange={handleCheckedChange}
85
+ />
86
+ );
87
+ const switchElement = screen.getByRole('switch');
88
+ expect(switchElement).not.toBeChecked();
89
+ });
90
+ });
91
+
92
+ describe('Disabled State', () => {
93
+ it('renders as disabled when disabled prop is true', () => {
94
+ renderWithProviders(<Switch disabled={true} />);
95
+ const switchElement = screen.getByRole('switch');
96
+ expect(switchElement).toBeDisabled();
97
+ });
98
+
99
+ it('renders as enabled when disabled prop is false', () => {
100
+ renderWithProviders(<Switch disabled={false} />);
101
+ const switchElement = screen.getByRole('switch');
102
+ expect(switchElement).not.toBeDisabled();
103
+ });
104
+
105
+ it('applies disabled styling', () => {
106
+ renderWithProviders(<Switch disabled={true} />);
107
+ const switchElement = screen.getByRole('switch');
108
+ expect(switchElement).toHaveClass('disabled:cursor-not-allowed', 'disabled:opacity-50');
109
+ });
110
+ });
111
+
112
+ describe('Event Handling', () => {
113
+ it('handles click events', async () => {
114
+ const handleCheckedChange = vi.fn();
115
+ const user = userEvent.setup();
116
+
117
+ renderWithProviders(
118
+ <Switch onCheckedChange={handleCheckedChange} />
119
+ );
120
+
121
+ const switchElement = screen.getByRole('switch');
122
+ await user.click(switchElement);
123
+
124
+ expect(handleCheckedChange).toHaveBeenCalledTimes(1);
125
+ expect(handleCheckedChange).toHaveBeenCalledWith(true);
126
+ });
127
+
128
+ it('handles keyboard events (Space)', async () => {
129
+ const handleCheckedChange = vi.fn();
130
+ const user = userEvent.setup();
131
+
132
+ renderWithProviders(
133
+ <Switch onCheckedChange={handleCheckedChange} />
134
+ );
135
+
136
+ const switchElement = screen.getByRole('switch');
137
+ switchElement.focus();
138
+ await user.keyboard(' ');
139
+
140
+ expect(handleCheckedChange).toHaveBeenCalledTimes(1);
141
+ expect(handleCheckedChange).toHaveBeenCalledWith(true);
142
+ });
143
+
144
+ it('handles keyboard events (Enter)', async () => {
145
+ const handleCheckedChange = vi.fn();
146
+ const user = userEvent.setup();
147
+
148
+ renderWithProviders(
149
+ <Switch onCheckedChange={handleCheckedChange} />
150
+ );
151
+
152
+ const switchElement = screen.getByRole('switch');
153
+ switchElement.focus();
154
+ await user.keyboard('{Enter}');
155
+
156
+ expect(handleCheckedChange).toHaveBeenCalledTimes(1);
157
+ expect(handleCheckedChange).toHaveBeenCalledWith(true);
158
+ });
159
+
160
+ it('toggles state on click', async () => {
161
+ const handleCheckedChange = vi.fn();
162
+ const user = userEvent.setup();
163
+
164
+ const { rerender } = renderWithProviders(
165
+ <Switch checked={false} onCheckedChange={handleCheckedChange} />
166
+ );
167
+
168
+ const switchElement = screen.getByRole('switch');
169
+ await user.click(switchElement);
170
+
171
+ expect(handleCheckedChange).toHaveBeenCalledWith(true);
172
+
173
+ // Simulate state change
174
+ rerender(<Switch checked={true} onCheckedChange={handleCheckedChange} />);
175
+ expect(switchElement).toBeChecked();
176
+
177
+ await user.click(switchElement);
178
+ expect(handleCheckedChange).toHaveBeenCalledWith(false);
179
+ });
180
+
181
+ it('does not trigger events when disabled', async () => {
182
+ const handleCheckedChange = vi.fn();
183
+ const user = userEvent.setup();
184
+
185
+ renderWithProviders(
186
+ <Switch disabled={true} onCheckedChange={handleCheckedChange} />
187
+ );
188
+
189
+ const switchElement = screen.getByRole('switch');
190
+ await user.click(switchElement);
191
+
192
+ expect(handleCheckedChange).not.toHaveBeenCalled();
193
+ });
194
+ });
195
+
196
+ describe('Accessibility', () => {
197
+ it('has proper ARIA attributes', () => {
198
+ renderWithProviders(<Switch />);
199
+ const switchElement = screen.getByRole('switch');
200
+ expect(switchElement).toHaveAttribute('type', 'button');
201
+ });
202
+
203
+ it('supports aria-label', () => {
204
+ renderWithProviders(<Switch aria-label="Toggle notifications" />);
205
+ const switchElement = screen.getByRole('switch', { name: 'Toggle notifications' });
206
+ expect(switchElement).toBeInTheDocument();
207
+ });
208
+
209
+ it('supports aria-labelledby', () => {
210
+ renderWithProviders(
211
+ <div>
212
+ <label id="notifications-label">Enable notifications</label>
213
+ <Switch aria-labelledby="notifications-label" />
214
+ </div>
215
+ );
216
+ const switchElement = screen.getByRole('switch', { name: 'Enable notifications' });
217
+ expect(switchElement).toBeInTheDocument();
218
+ });
219
+
220
+ it('supports aria-describedby', () => {
221
+ renderWithProviders(
222
+ <div>
223
+ <Switch aria-describedby="notifications-help" />
224
+ <div id="notifications-help">You will receive email notifications</div>
225
+ </div>
226
+ );
227
+ const switchElement = screen.getByRole('switch');
228
+ expect(switchElement).toHaveAttribute('aria-describedby', 'notifications-help');
229
+ });
230
+
231
+ it('announces state changes to screen readers', () => {
232
+ const { rerender } = renderWithProviders(<Switch checked={false} />);
233
+ let switchElement = screen.getByRole('switch');
234
+ expect(switchElement).toHaveAttribute('aria-checked', 'false');
235
+
236
+ rerender(<Switch checked={true} />);
237
+ switchElement = screen.getByRole('switch');
238
+ expect(switchElement).toHaveAttribute('aria-checked', 'true');
239
+ });
240
+
241
+ it('is keyboard accessible', () => {
242
+ renderWithProviders(<Switch />);
243
+ const switchElement = screen.getByRole('switch');
244
+ expect(switchElement).not.toHaveAttribute('tabindex', '-1');
245
+ });
246
+
247
+ it('has focus visible styles', () => {
248
+ renderWithProviders(<Switch />);
249
+ const switchElement = screen.getByRole('switch');
250
+ expect(switchElement).toHaveClass('focus-visible:outline-none', 'focus-visible:ring-2');
251
+ });
252
+ });
253
+
254
+ describe('Visual States', () => {
255
+ it('applies checked state styling', () => {
256
+ renderWithProviders(<Switch checked={true} />);
257
+ const switchElement = screen.getByRole('switch');
258
+ expect(switchElement).toHaveClass('data-[state=checked]:bg-main-600');
259
+ });
260
+
261
+ it('applies unchecked state styling', () => {
262
+ renderWithProviders(<Switch checked={false} />);
263
+ const switchElement = screen.getByRole('switch');
264
+ expect(switchElement).toHaveClass('data-[state=unchecked]:bg-sec-200');
265
+ });
266
+
267
+ it('has proper sizing', () => {
268
+ renderWithProviders(<Switch />);
269
+ const switchElement = screen.getByRole('switch');
270
+ expect(switchElement).toHaveClass('h-6', 'w-11');
271
+ });
272
+
273
+ it('has proper border and background styling', () => {
274
+ renderWithProviders(<Switch />);
275
+ const switchElement = screen.getByRole('switch');
276
+ expect(switchElement).toHaveClass('rounded-full', 'border-2', 'border-transparent');
277
+ });
278
+
279
+ it('has proper thumb styling', () => {
280
+ renderWithProviders(<Switch />);
281
+ const switchElement = screen.getByRole('switch');
282
+ const thumb = switchElement.querySelector('[data-state]');
283
+ expect(thumb).toHaveClass('h-5', 'w-5', 'rounded-full', 'bg-background');
284
+ });
285
+ });
286
+
287
+ describe('Form Integration', () => {
288
+ it('works with form labels', () => {
289
+ renderWithProviders(
290
+ <div>
291
+ <label htmlFor="notifications-switch">Enable notifications</label>
292
+ <Switch id="notifications-switch" />
293
+ </div>
294
+ );
295
+
296
+ const switchElement = screen.getByRole('switch');
297
+ const label = screen.getByText('Enable notifications');
298
+
299
+ expect(switchElement).toHaveAttribute('id', 'notifications-switch');
300
+ expect(label).toHaveAttribute('for', 'notifications-switch');
301
+ });
302
+
303
+ it('works with form submission', async () => {
304
+ const handleSubmit = vi.fn((e) => e.preventDefault());
305
+ const user = userEvent.setup();
306
+
307
+ renderWithProviders(
308
+ <form onSubmit={handleSubmit}>
309
+ <Switch name="notifications" />
310
+ <button type="submit">Submit</button>
311
+ </form>
312
+ );
313
+
314
+ await user.click(screen.getByRole('button', { name: 'Submit' }));
315
+ expect(handleSubmit).toHaveBeenCalledTimes(1);
316
+ });
317
+
318
+ it('works with form validation', () => {
319
+ renderWithProviders(
320
+ <Switch
321
+ aria-invalid={true}
322
+ aria-describedby="error-message"
323
+ />
324
+ );
325
+
326
+ const switchElement = screen.getByRole('switch');
327
+ expect(switchElement).toHaveAttribute('aria-invalid', 'true');
328
+ expect(switchElement).toHaveAttribute('aria-describedby', 'error-message');
329
+ });
330
+ });
331
+
332
+ describe('Error Handling', () => {
333
+ it('handles missing onCheckedChange gracefully', () => {
334
+ renderWithProviders(<Switch checked={true} />);
335
+ const switchElement = screen.getByRole('switch');
336
+ expect(switchElement).toBeChecked();
337
+ });
338
+
339
+ it('handles invalid props gracefully', () => {
340
+ // @ts-expect-error Testing invalid prop
341
+ renderWithProviders(<Switch invalidProp="test" />);
342
+ const switchElement = screen.getByRole('switch');
343
+ expect(switchElement).toBeInTheDocument();
344
+ });
345
+ });
346
+
347
+ describe('Integration', () => {
348
+ it('works with multiple switches', () => {
349
+ renderWithProviders(
350
+ <div>
351
+ <Switch id="switch1" />
352
+ <Switch id="switch2" />
353
+ <Switch id="switch3" />
354
+ </div>
355
+ );
356
+
357
+ expect(screen.getAllByRole('switch')).toHaveLength(3);
358
+ });
359
+
360
+ it('works with switch groups', () => {
361
+ renderWithProviders(
362
+ <fieldset>
363
+ <legend>Notification preferences</legend>
364
+ <Switch id="email" />
365
+ <label htmlFor="email">Email notifications</label>
366
+ <Switch id="sms" />
367
+ <label htmlFor="sms">SMS notifications</label>
368
+ </fieldset>
369
+ );
370
+
371
+ const switches = screen.getAllByRole('switch');
372
+ expect(switches).toHaveLength(2);
373
+ expect(switches[0]).toHaveAttribute('id', 'email');
374
+ expect(switches[1]).toHaveAttribute('id', 'sms');
375
+ });
376
+
377
+ it('works with controlled state management', async () => {
378
+ const TestComponent = () => {
379
+ const [checked, setChecked] = React.useState(false);
380
+ return (
381
+ <div>
382
+ <Switch
383
+ checked={checked}
384
+ onCheckedChange={setChecked}
385
+ />
386
+ <span data-testid="status">{checked ? 'on' : 'off'}</span>
387
+ </div>
388
+ );
389
+ };
390
+
391
+ const user = userEvent.setup();
392
+ renderWithProviders(<TestComponent />);
393
+
394
+ const switchElement = screen.getByRole('switch');
395
+ const status = screen.getByTestId('status');
396
+
397
+ expect(status).toHaveTextContent('off');
398
+
399
+ await user.click(switchElement);
400
+ expect(status).toHaveTextContent('on');
401
+ });
402
+
403
+ it('works with external state management', async () => {
404
+ const externalState = { checked: false };
405
+ const handleChange = vi.fn((checked) => {
406
+ externalState.checked = checked;
407
+ });
408
+
409
+ const user = userEvent.setup();
410
+ renderWithProviders(
411
+ <Switch
412
+ checked={externalState.checked}
413
+ onCheckedChange={handleChange}
414
+ />
415
+ );
416
+
417
+ const switchElement = screen.getByRole('switch');
418
+ await user.click(switchElement);
419
+
420
+ expect(handleChange).toHaveBeenCalledWith(true);
421
+ });
422
+ });
423
+
424
+ describe('Performance', () => {
425
+ it('renders efficiently with many switches', () => {
426
+ const switches = Array.from({ length: 100 }, (_, i) => (
427
+ <Switch key={i} id={`switch-${i}`} />
428
+ ));
429
+
430
+ const startTime = performance.now();
431
+ renderWithProviders(<div>{switches}</div>);
432
+ const endTime = performance.now();
433
+
434
+ expect(screen.getAllByRole('switch')).toHaveLength(100);
435
+ expect(endTime - startTime).toBeLessThan(1000); // Should render in under 1 second
436
+ });
437
+ });
438
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @file Switch Component
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/Switch
5
+ * @since 0.5.67
6
+ *
7
+ * A toggle switch component built on Radix UI primitives.
8
+ * Provides an accessible, keyboard-navigable switch for boolean states.
9
+ *
10
+ * Features:
11
+ * - WCAG 2.1 AA compliant
12
+ * - Keyboard navigation (Space/Enter)
13
+ * - Focus visible indicators
14
+ * - Disabled state support
15
+ * - Smooth animations
16
+ * - Tailwind v4 compatible
17
+ * - Theme-aware colors
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { Switch } from '@jmruthers/pace-core';
22
+ *
23
+ * function Example() {
24
+ * const [checked, setChecked] = React.useState(false);
25
+ *
26
+ * return (
27
+ * <div className="flex items-center gap-2">
28
+ * <Switch
29
+ * checked={checked}
30
+ * onCheckedChange={setChecked}
31
+ * id="notifications"
32
+ * />
33
+ * <label htmlFor="notifications">
34
+ * Enable notifications
35
+ * </label>
36
+ * </div>
37
+ * );
38
+ * }
39
+ * ```
40
+ *
41
+ * @example With Form
42
+ * ```tsx
43
+ * import { Switch, Label } from '@jmruthers/pace-core';
44
+ *
45
+ * function FormExample() {
46
+ * return (
47
+ * <div className="flex items-center gap-2">
48
+ * <Switch id="terms" />
49
+ * <Label htmlFor="terms">
50
+ * I agree to the terms and conditions
51
+ * </Label>
52
+ * </div>
53
+ * );
54
+ * }
55
+ * ```
56
+ *
57
+ * @example Disabled State
58
+ * ```tsx
59
+ * <Switch disabled checked />
60
+ * ```
61
+ *
62
+ * @accessibility
63
+ * - Uses Radix UI's accessible switch primitive
64
+ * - Proper keyboard navigation (Space to toggle)
65
+ * - Screen reader announcements for state changes
66
+ * - Focus visible styles for keyboard users
67
+ * - ARIA attributes handled automatically
68
+ */
69
+
70
+ import * as React from "react";
71
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
72
+ import { cn } from "../../utils/cn";
73
+
74
+ /**
75
+ * Switch component props
76
+ * Extends all props from Radix UI Switch.Root
77
+ */
78
+ export interface SwitchProps
79
+ extends React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root> {
80
+ /**
81
+ * Additional CSS classes to apply to the switch
82
+ */
83
+ className?: string;
84
+ }
85
+
86
+ /**
87
+ * Switch component
88
+ *
89
+ * A toggle switch for boolean states. Built on Radix UI for accessibility.
90
+ *
91
+ * @component
92
+ * @example
93
+ * ```tsx
94
+ * <Switch checked={isEnabled} onCheckedChange={setIsEnabled} />
95
+ * ```
96
+ */
97
+ const Switch = React.forwardRef<
98
+ React.ElementRef<typeof SwitchPrimitive.Root>,
99
+ SwitchProps
100
+ >(({ className, ...props }, ref) => (
101
+ <SwitchPrimitive.Root
102
+ className={cn(
103
+ // Base styles
104
+ "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full",
105
+ // Border
106
+ "border-2 border-transparent",
107
+ // Transitions
108
+ "transition-colors",
109
+ // Focus styles
110
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main-600",
111
+ "focus-visible:ring-offset-2 focus-visible:ring-offset-background",
112
+ // Disabled state
113
+ "disabled:cursor-not-allowed disabled:opacity-50",
114
+ // State-based colors (using theme variables)
115
+ "data-[state=checked]:bg-main-600",
116
+ "data-[state=unchecked]:bg-sec-200",
117
+ className
118
+ )}
119
+ {...props}
120
+ ref={ref}
121
+ >
122
+ <SwitchPrimitive.Thumb
123
+ className={cn(
124
+ // Base styles
125
+ "pointer-events-none block h-5 w-5 rounded-full",
126
+ // Background and shadow
127
+ "bg-background shadow-lg ring-0",
128
+ // Transition
129
+ "transition-transform",
130
+ // State-based position
131
+ "data-[state=checked]:translate-x-5",
132
+ "data-[state=unchecked]:translate-x-0"
133
+ )}
134
+ />
135
+ </SwitchPrimitive.Root>
136
+ ));
137
+
138
+ Switch.displayName = SwitchPrimitive.Root.displayName;
139
+
140
+ export { Switch };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file Switch Component Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/Switch
5
+ * @since 0.5.67
6
+ */
7
+
8
+ export { Switch } from './Switch';
9
+ export type { SwitchProps } from './Switch';
@@ -50,6 +50,8 @@ export { Alert, AlertTitle, AlertDescription } from './Alert';
50
50
  export { Avatar, AvatarImage, AvatarFallback } from './Avatar';
51
51
 
52
52
  export { Checkbox } from './Checkbox';
53
+ export { Switch } from './Switch';
54
+ export type { SwitchProps } from './Switch';
53
55
  export { Progress } from './Progress';
54
56
 
55
57
  // Table components
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export { Alert, AlertTitle, AlertDescription } from './components/Alert/Alert';
62
62
  export { Avatar, AvatarImage, AvatarFallback } from './components/Avatar/Avatar';
63
63
 
64
64
  export { Checkbox } from './components/Checkbox/Checkbox';
65
+ export { Switch } from './components/Switch/Switch';
66
+ export type { SwitchProps } from './components/Switch/Switch';
65
67
  export { Progress } from './components/Progress/Progress';
66
68
 
67
69
  // ADVANCED UI COMPONENTS