@jmruthers/pace-core 0.5.64 → 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 (131) hide show
  1. package/dist/{DataTable-7BER7PDS.js → DataTable-MFUXNGPR.js} +2 -2
  2. package/dist/{DataTable-D15XipLZ.d.ts → DataTable-ntgmhO2W.d.ts} +1 -1
  3. package/dist/{PublicLoadingSpinner-CXJ-W9wZ.d.ts → PublicLoadingSpinner-DdKXTkCZ.d.ts} +94 -1
  4. package/dist/{chunk-S66AJVI2.js → chunk-4HQ5BOVZ.js} +97 -27
  5. package/dist/chunk-4HQ5BOVZ.js.map +1 -0
  6. package/dist/{chunk-2LPYEFXI.js → chunk-ZB6AEA7I.js} +309 -261
  7. package/dist/chunk-ZB6AEA7I.js.map +1 -0
  8. package/dist/components.d.ts +4 -3
  9. package/dist/components.js +4 -2
  10. package/dist/components.js.map +1 -1
  11. package/dist/hooks.d.ts +1 -1
  12. package/dist/index.d.ts +6 -5
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/{types-E5WSpEtz.d.ts → types-CGX9Vyf5.d.ts} +8 -0
  16. package/dist/types.js.map +1 -1
  17. package/dist/utils.d.ts +2 -2
  18. package/dist/utils.js +1 -1
  19. package/docs/api/classes/ColumnFactory.md +6 -6
  20. package/docs/api/classes/ErrorBoundary.md +1 -1
  21. package/docs/api/classes/InvalidScopeError.md +1 -1
  22. package/docs/api/classes/MissingUserContextError.md +1 -1
  23. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  24. package/docs/api/classes/PermissionDeniedError.md +1 -1
  25. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  26. package/docs/api/classes/RBACAuditManager.md +1 -1
  27. package/docs/api/classes/RBACCache.md +1 -1
  28. package/docs/api/classes/RBACEngine.md +1 -1
  29. package/docs/api/classes/RBACError.md +1 -1
  30. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  31. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  32. package/docs/api/classes/StorageUtils.md +1 -1
  33. package/docs/api/interfaces/AggregateConfig.md +4 -4
  34. package/docs/api/interfaces/ButtonProps.md +1 -1
  35. package/docs/api/interfaces/CardProps.md +1 -1
  36. package/docs/api/interfaces/ColorPalette.md +1 -1
  37. package/docs/api/interfaces/ColorShade.md +1 -1
  38. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  39. package/docs/api/interfaces/DataTableAction.md +14 -14
  40. package/docs/api/interfaces/DataTableColumn.md +21 -21
  41. package/docs/api/interfaces/DataTableProps.md +1 -1
  42. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  43. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  44. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  45. package/docs/api/interfaces/EventContextType.md +1 -1
  46. package/docs/api/interfaces/EventLogoProps.md +1 -1
  47. package/docs/api/interfaces/EventProviderProps.md +1 -1
  48. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  49. package/docs/api/interfaces/FileUploadProps.md +1 -1
  50. package/docs/api/interfaces/FooterProps.md +1 -1
  51. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  52. package/docs/api/interfaces/InputProps.md +1 -1
  53. package/docs/api/interfaces/LabelProps.md +1 -1
  54. package/docs/api/interfaces/LoginFormProps.md +1 -1
  55. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  56. package/docs/api/interfaces/NavigationContextType.md +1 -1
  57. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  58. package/docs/api/interfaces/NavigationItem.md +1 -1
  59. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  60. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  61. package/docs/api/interfaces/Organisation.md +1 -1
  62. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  63. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  64. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  65. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  66. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  67. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  68. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  69. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  70. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  71. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  72. package/docs/api/interfaces/PaletteData.md +1 -1
  73. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  74. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  75. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  76. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  77. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  78. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  79. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  80. package/docs/api/interfaces/RBACConfig.md +1 -1
  81. package/docs/api/interfaces/RBACContextType.md +1 -1
  82. package/docs/api/interfaces/RBACLogger.md +1 -1
  83. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  84. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  85. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  86. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  87. package/docs/api/interfaces/RouteConfig.md +1 -1
  88. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  89. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  90. package/docs/api/interfaces/StorageConfig.md +1 -1
  91. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  92. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  93. package/docs/api/interfaces/StorageListOptions.md +1 -1
  94. package/docs/api/interfaces/StorageListResult.md +1 -1
  95. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  96. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  97. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  98. package/docs/api/interfaces/StyleImport.md +1 -1
  99. package/docs/api/interfaces/SwitchProps.md +34 -0
  100. package/docs/api/interfaces/ToastActionElement.md +1 -1
  101. package/docs/api/interfaces/ToastProps.md +1 -1
  102. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  103. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  104. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  105. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  106. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  107. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  108. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  109. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  110. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  111. package/docs/api/interfaces/UserEventAccess.md +1 -1
  112. package/docs/api/interfaces/UserMenuProps.md +1 -1
  113. package/docs/api/interfaces/UserProfile.md +1 -1
  114. package/docs/api/modules.md +38 -4
  115. package/package.json +2 -1
  116. package/src/components/DataTable/components/DataTableBody.tsx +27 -11
  117. package/src/components/DataTable/components/DataTableCore.tsx +13 -13
  118. package/src/components/DataTable/components/EditableRow.tsx +46 -28
  119. package/src/components/DataTable/components/UnifiedTableBody.tsx +86 -38
  120. package/src/components/DataTable/components/VirtualizedDataTable.tsx +5 -3
  121. package/src/components/DataTable/core/ColumnFactory.ts +4 -0
  122. package/src/components/DataTable/types.ts +10 -0
  123. package/src/components/Switch/Switch.test.tsx +438 -0
  124. package/src/components/Switch/Switch.tsx +140 -0
  125. package/src/components/Switch/index.ts +9 -0
  126. package/src/components/index.ts +2 -0
  127. package/src/index.ts +2 -0
  128. package/src/types/index.ts +2 -0
  129. package/dist/chunk-2LPYEFXI.js.map +0 -1
  130. package/dist/chunk-S66AJVI2.js.map +0 -1
  131. /package/dist/{DataTable-7BER7PDS.js.map → DataTable-MFUXNGPR.js.map} +0 -0
@@ -32,7 +32,9 @@ const MemoizedCell = memo(({ cell, style }: { cell: any; style?: React.CSSProper
32
32
  )}
33
33
  style={style}
34
34
  >
35
- {flexRender(cell.column?.columnDef?.cell, cell.getContext?.() || {})}
35
+ <div className={cell.column?.columnDef?.meta?.align === 'right' ? 'text-right' : ''}>
36
+ {flexRender(cell.column?.columnDef?.cell, cell.getContext?.() || {})}
37
+ </div>
36
38
  </td>
37
39
  );
38
40
  });
@@ -185,7 +187,7 @@ export function VirtualizedDataTable<TData extends DataRecord>({
185
187
  style={{}}
186
188
  onClick={header.column?.getToggleSortingHandler ? header.column.getToggleSortingHandler() : undefined}
187
189
  >
188
- <div className="flex items-center space-x-1">
190
+ <div className={`flex items-center space-x-1 ${header.column?.columnDef?.meta?.align === 'right' ? 'justify-end' : ''}`}>
189
191
  {header.isPlaceholder
190
192
  ? null
191
193
  : flexRender(header.column?.columnDef?.header, header.getContext?.() || {})}
@@ -413,7 +415,7 @@ export function EnhancedVirtualizedDataTable<TData extends DataRecord>({
413
415
  style={{}}
414
416
  onClick={header.column?.getToggleSortingHandler ? header.column.getToggleSortingHandler() : undefined}
415
417
  >
416
- <div className="flex items-center space-x-1">
418
+ <div className={`flex items-center space-x-1 ${header.column?.columnDef?.meta?.align === 'right' ? 'justify-end' : ''}`}>
417
419
  {header.isPlaceholder
418
420
  ? null
419
421
  : flexRender(header.column?.columnDef?.header, header.getContext?.() || {})}
@@ -120,6 +120,10 @@ export class ColumnFactory<TData extends DataRecord = DataRecord> {
120
120
  size: options.size,
121
121
  minSize: options.minSize,
122
122
  maxSize: options.maxSize,
123
+ meta: {
124
+ align: 'right' as const,
125
+ type: 'number' as const,
126
+ },
123
127
  } as ColumnDef<TData>;
124
128
  }
125
129
 
@@ -17,6 +17,16 @@ import type {
17
17
  } from '@tanstack/react-table';
18
18
  import type { ImportModalConfig } from './components/ImportModal';
19
19
 
20
+ // Extend TanStack Table types to include custom meta properties
21
+ declare module '@tanstack/react-table' {
22
+ interface ColumnMeta<TData, TValue> {
23
+ /** Text alignment for the column content */
24
+ align?: 'left' | 'right' | 'center';
25
+ /** Column type for styling purposes */
26
+ type?: 'text' | 'number' | 'date' | 'boolean' | 'custom';
27
+ }
28
+ }
29
+
20
30
  // ============================================================================
21
31
  // CORE DATA TYPES
22
32
  // ============================================================================
@@ -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
@@ -23,3 +23,5 @@ export * from './guards';
23
23
  export * from './validation';
24
24
  export * from './theme';
25
25
  export * from './security';
26
+
27
+ // Type declarations are handled via module augmentation in individual files