@jmruthers/pace-core 0.6.4 → 0.6.5

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 (101) hide show
  1. package/dist/{DataTable-E7YQZD7D.js → DataTable-AOVNCPTX.js} +8 -8
  2. package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-QTFVrL-Z.d.ts} +65 -83
  3. package/dist/{UnifiedAuthProvider-QPXO24B4.js → UnifiedAuthProvider-4SBX4LU5.js} +4 -4
  4. package/dist/{api-6LVZTHDS.js → api-O6HTBX5Y.js} +3 -3
  5. package/dist/{chunk-I6DAQMWX.js → chunk-6COVEUS7.js} +130 -106
  6. package/dist/chunk-6COVEUS7.js.map +1 -0
  7. package/dist/{chunk-36LVWXB2.js → chunk-AFVQODI2.js} +37 -1
  8. package/dist/{chunk-36LVWXB2.js.map → chunk-AFVQODI2.js.map} +1 -1
  9. package/dist/{chunk-3LPHPB62.js → chunk-EFN2EIMK.js} +2 -2
  10. package/dist/{chunk-ATKZM7RX.js → chunk-G7QEZTYQ.js} +31 -31
  11. package/dist/{chunk-ATKZM7RX.js.map → chunk-G7QEZTYQ.js.map} +1 -1
  12. package/dist/{chunk-NN6WWZ5U.js → chunk-HU2C6SSC.js} +29 -18
  13. package/dist/chunk-HU2C6SSC.js.map +1 -0
  14. package/dist/{chunk-AVMLPIM7.js → chunk-IHB5DR3H.js} +102 -51
  15. package/dist/chunk-IHB5DR3H.js.map +1 -0
  16. package/dist/{chunk-7JPAB3T5.js → chunk-IVOFDYWT.js} +364 -208
  17. package/dist/chunk-IVOFDYWT.js.map +1 -0
  18. package/dist/{chunk-6SOIHG6Z.js → chunk-JGRYX5UX.js} +120 -20
  19. package/dist/chunk-JGRYX5UX.js.map +1 -0
  20. package/dist/{chunk-OEWDTMG7.js → chunk-NTM7ZSB6.js} +4 -4
  21. package/dist/chunk-NTM7ZSB6.js.map +1 -0
  22. package/dist/{chunk-5EC5MEWX.js → chunk-RGAWHO7N.js} +4 -4
  23. package/dist/chunk-RGAWHO7N.js.map +1 -0
  24. package/dist/{chunk-YKRAFF5K.js → chunk-UPPMRMYG.js} +3 -3
  25. package/dist/{chunk-YKRAFF5K.js.map → chunk-UPPMRMYG.js.map} +1 -1
  26. package/dist/components.d.ts +2 -3
  27. package/dist/components.js +24 -28
  28. package/dist/components.js.map +1 -1
  29. package/dist/{contextValidator-OOPCLPZW.js → contextValidator-5OGXSPKS.js} +2 -2
  30. package/dist/hooks.d.ts +3 -3
  31. package/dist/hooks.js +41 -139
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +27 -18
  34. package/dist/index.js +41 -50
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.js +3 -3
  37. package/dist/rbac/index.d.ts +16 -9
  38. package/dist/rbac/index.js +6 -6
  39. package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-ClnV4tnv.d.ts} +8 -8
  40. package/dist/utils.js +1 -1
  41. package/docs/api/modules.md +210 -100
  42. package/package.json +1 -2
  43. package/scripts/validate-master.js +1 -1
  44. package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
  45. package/src/components/DataTable/components/ImportModal.tsx +4 -6
  46. package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
  47. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
  48. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
  49. package/src/components/DataTable/core/DataTableContext.tsx +1 -1
  50. package/src/components/DateTimeField/DateTimeField.tsx +17 -19
  51. package/src/components/DateTimeField/README.md +5 -2
  52. package/src/components/Dialog/Dialog.test.tsx +248 -228
  53. package/src/components/Dialog/Dialog.tsx +455 -325
  54. package/src/components/Dialog/index.ts +3 -3
  55. package/src/components/FileDisplay/FileDisplay.test.tsx +41 -0
  56. package/src/components/FileDisplay/FileDisplay.tsx +5 -5
  57. package/src/components/Form/Form.test.tsx +3 -2
  58. package/src/components/Form/Form.tsx +4 -5
  59. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
  60. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
  61. package/src/components/LoginForm/LoginForm.tsx +2 -2
  62. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
  63. package/src/components/PaceAppLayout/PaceAppLayout.tsx +32 -39
  64. package/src/components/PaceAppLayout/README.md +10 -9
  65. package/src/components/PaceAppLayout/test-setup.tsx +40 -31
  66. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
  67. package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
  68. package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
  69. package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
  70. package/src/components/UserMenu/UserMenu.test.tsx +38 -6
  71. package/src/components/UserMenu/UserMenu.tsx +36 -34
  72. package/src/components/index.ts +3 -4
  73. package/src/hooks/useEventTheme.ts +4 -4
  74. package/src/hooks/useEvents.ts +11 -7
  75. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  76. package/src/hooks/useOrganisationPermissions.ts +4 -4
  77. package/src/hooks/useOrganisations.ts +13 -7
  78. package/src/index.ts +11 -1
  79. package/src/rbac/README.md +20 -20
  80. package/src/rbac/hooks/useRBAC.test.ts +21 -3
  81. package/src/rbac/hooks/useRBAC.ts +4 -3
  82. package/src/rbac/hooks/useResourcePermissions.test.ts +125 -30
  83. package/src/rbac/hooks/useResourcePermissions.ts +57 -29
  84. package/src/rbac/permissions.ts +17 -17
  85. package/src/rbac/utils/contextValidator.ts +36 -0
  86. package/src/services/AuthService.ts +2 -5
  87. package/src/services/InactivityService.ts +139 -58
  88. package/src/styles/core.css +4 -0
  89. package/src/utils/formatting/formatTime.test.ts +3 -2
  90. package/dist/chunk-5EC5MEWX.js.map +0 -1
  91. package/dist/chunk-6SOIHG6Z.js.map +0 -1
  92. package/dist/chunk-7JPAB3T5.js.map +0 -1
  93. package/dist/chunk-AVMLPIM7.js.map +0 -1
  94. package/dist/chunk-I6DAQMWX.js.map +0 -1
  95. package/dist/chunk-NN6WWZ5U.js.map +0 -1
  96. package/dist/chunk-OEWDTMG7.js.map +0 -1
  97. /package/dist/{DataTable-E7YQZD7D.js.map → DataTable-AOVNCPTX.js.map} +0 -0
  98. /package/dist/{UnifiedAuthProvider-QPXO24B4.js.map → UnifiedAuthProvider-4SBX4LU5.js.map} +0 -0
  99. /package/dist/{api-6LVZTHDS.js.map → api-O6HTBX5Y.js.map} +0 -0
  100. /package/dist/{chunk-3LPHPB62.js.map → chunk-EFN2EIMK.js.map} +0 -0
  101. /package/dist/{contextValidator-OOPCLPZW.js.map → contextValidator-5OGXSPKS.js.map} +0 -0
@@ -9,31 +9,37 @@
9
9
  */
10
10
 
11
11
  import React from 'react';
12
- import { render, screen, within } from '@testing-library/react';
12
+ import { render, screen, within, waitFor } from '@testing-library/react';
13
13
  import userEvent from '@testing-library/user-event';
14
14
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
15
  import { ViewRowModal } from '../ViewRowModal';
16
16
 
17
- // Mock Dialog components - Use the same pattern as other tests
18
- // Note: Path must match the import in ViewRowModal.tsx exactly
19
- vi.mock('../../Dialog/Dialog', async () => {
20
- const actual = await vi.importActual('../../Dialog/Dialog');
21
- return {
22
- ...actual,
23
- Dialog: ({ children, open, onOpenChange }: any) => (
24
- open ? <div role="dialog" data-testid="dialog">{children}</div> : null
25
- ),
26
- DialogContent: ({ children, className }: any) => (
27
- <div data-testid="dialog-content" className={className}>{children}</div>
28
- ),
29
- DialogHeader: ({ children }: any) => (
30
- <div data-testid="dialog-header">{children}</div>
31
- ),
32
- DialogTitle: ({ children, className }: any) => (
33
- <h2 data-testid="dialog-title" className={className} role="heading" aria-level={2}>{children}</h2>
34
- ),
35
- };
36
- });
17
+ // Helper function to wait for dialog to be accessible
18
+ // Native dialog elements are only accessible after showModal() completes
19
+ // In test environments, we use querySelector as fallback since getByRole may not work
20
+ // Note: In test environments (jsdom), dialog.open may not be set even when dialog is rendered
21
+ const waitForDialog = async (): Promise<HTMLElement> => {
22
+ return await waitFor(
23
+ () => {
24
+ // Try getByRole first (works in browsers with full dialog support)
25
+ try {
26
+ const dialog = screen.getByRole('dialog');
27
+ expect(dialog).toBeInTheDocument();
28
+ return dialog;
29
+ } catch (e) {
30
+ // Fallback: use querySelector for test environments that don't fully support dialog accessibility
31
+ const dialog = document.querySelector('dialog[role="dialog"]') as HTMLDialogElement;
32
+ if (!dialog) {
33
+ throw new Error('Dialog not found in DOM');
34
+ }
35
+ // In test environments, dialog.open may not be set even when dialog is rendered
36
+ // Just check that dialog exists in DOM - that's sufficient for testing
37
+ return dialog;
38
+ }
39
+ },
40
+ { timeout: 3000 }
41
+ );
42
+ };
37
43
 
38
44
  // Mock Button component
39
45
  vi.mock('../../Button/Button', () => ({
@@ -105,33 +111,40 @@ describe('[component] ViewRowModal', () => {
105
111
  expect(container.firstChild).toBeNull();
106
112
  });
107
113
 
108
- it('renders modal when open with data', () => {
114
+ it('renders modal when open with data', async () => {
109
115
  render(<ViewRowModal {...baseProps} />);
110
116
 
111
- // Dialog renders with role="dialog" from Radix UI
112
- expect(screen.getByRole('dialog')).toBeInTheDocument();
117
+ // Wait for dialog to be accessible (showModal() is async)
118
+ await waitForDialog();
113
119
  // DialogContent and DialogHeader are rendered but may not have testids in actual implementation
114
120
  // Check for content instead
115
121
  expect(screen.getByText('Row Details')).toBeInTheDocument();
116
122
  });
117
123
 
118
- it('renders default title when title prop is not provided', () => {
124
+ it('renders default title when title prop is not provided', async () => {
119
125
  render(<ViewRowModal {...baseProps} />);
120
126
 
121
- // DialogTitle renders as h2, check for text content
127
+ // Wait for dialog to be accessible
128
+ await waitForDialog();
129
+ // Title is rendered as h2 in DialogHeader, check for text content
122
130
  expect(screen.getByText('Row Details')).toBeInTheDocument();
123
131
  });
124
132
 
125
- it('renders custom title when title prop is provided', () => {
133
+ it('renders custom title when title prop is provided', async () => {
126
134
  render(<ViewRowModal {...baseProps} title="Custom Title" />);
127
135
 
128
- // DialogTitle renders as h2, check for text content
136
+ // Wait for dialog to be accessible
137
+ await waitForDialog();
138
+ // Title is rendered as h2 in DialogHeader, check for text content
129
139
  expect(screen.getByText('Custom Title')).toBeInTheDocument();
130
140
  });
131
141
 
132
- it('renders all data fields', () => {
142
+ it('renders all data fields', async () => {
133
143
  render(<ViewRowModal {...baseProps} />);
134
144
 
145
+ // Wait for dialog to be accessible
146
+ await waitForDialog();
147
+
135
148
  expect(screen.getByText(/id/i)).toBeInTheDocument();
136
149
  expect(screen.getByText(/name/i)).toBeInTheDocument();
137
150
  expect(screen.getByText(/email/i)).toBeInTheDocument();
@@ -139,9 +152,12 @@ describe('[component] ViewRowModal', () => {
139
152
  expect(screen.getByText(/active/i)).toBeInTheDocument();
140
153
  });
141
154
 
142
- it('renders field values correctly', () => {
155
+ it('renders field values correctly', async () => {
143
156
  render(<ViewRowModal {...baseProps} />);
144
157
 
158
+ // Wait for dialog to be accessible
159
+ await waitForDialog();
160
+
145
161
  expect(screen.getByText('1')).toBeInTheDocument();
146
162
  expect(screen.getByText('John Doe')).toBeInTheDocument();
147
163
  expect(screen.getByText('john@example.com')).toBeInTheDocument();
@@ -149,9 +165,12 @@ describe('[component] ViewRowModal', () => {
149
165
  expect(screen.getByText('true')).toBeInTheDocument();
150
166
  });
151
167
 
152
- it('formats Date values as locale date strings', () => {
168
+ it('formats Date values as locale date strings', async () => {
153
169
  render(<ViewRowModal {...baseProps} />);
154
170
 
171
+ // Wait for dialog to be accessible
172
+ await waitForDialog();
173
+
155
174
  const dateValue = screen.getByText(new Date('2024-01-01').toLocaleDateString());
156
175
  expect(dateValue).toBeInTheDocument();
157
176
  });
@@ -203,9 +222,18 @@ describe('[component] ViewRowModal', () => {
203
222
 
204
223
  render(<ViewRowModal {...baseProps} onClose={handleClose} />);
205
224
 
206
- // Get the main "Close" button (not the X icon button)
207
- const closeButtons = screen.getAllByRole('button', { name: /close/i });
208
- const mainCloseButton = closeButtons.find(btn => btn.textContent === 'Close');
225
+ // Wait for dialog title to ensure dialog is rendered
226
+ await waitFor(() => {
227
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
228
+ }, { timeout: 5000 });
229
+
230
+ // Find the main "Close" button (not the X icon button) - there are multiple Close buttons
231
+ const closeButtons = screen.getAllByText('Close');
232
+ // The main Close button is the one that's not inside a span with sr-only class
233
+ const mainCloseButton = closeButtons.find(btn => {
234
+ const button = btn.closest('button');
235
+ return button && !button.querySelector('.sr-only');
236
+ })?.closest('button');
209
237
  expect(mainCloseButton).toBeInTheDocument();
210
238
  if (mainCloseButton) {
211
239
  await user.click(mainCloseButton);
@@ -219,6 +247,12 @@ describe('[component] ViewRowModal', () => {
219
247
 
220
248
  render(<ViewRowModal {...baseProps} onClose={handleClose} />);
221
249
 
250
+ // Wait for dialog content to be rendered
251
+ await waitFor(() => {
252
+ const xButtons = screen.getAllByTestId('x-icon');
253
+ expect(xButtons.length).toBeGreaterThan(0);
254
+ }, { timeout: 5000 });
255
+
222
256
  // Get the X icon button specifically (first button with X icon)
223
257
  const xButtons = screen.getAllByTestId('x-icon');
224
258
  const xButton = xButtons[0].closest('button');
@@ -240,9 +274,18 @@ describe('[component] ViewRowModal', () => {
240
274
  />
241
275
  );
242
276
 
243
- // Get the main Close button (not the X icon button)
244
- const closeButtons = screen.getAllByRole('button', { name: /close/i });
245
- const mainCloseButton = closeButtons.find(btn => btn.textContent === 'Close');
277
+ // Wait for dialog title to ensure dialog is rendered
278
+ await waitFor(() => {
279
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
280
+ }, { timeout: 5000 });
281
+
282
+ // Find the main "Close" button (not the X icon button) - there are multiple Close buttons
283
+ const closeButtons = screen.getAllByText('Close');
284
+ // The main Close button is the one that's not inside a span with sr-only class
285
+ const mainCloseButton = closeButtons.find(btn => {
286
+ const button = btn.closest('button');
287
+ return button && !button.querySelector('.sr-only');
288
+ })?.closest('button');
246
289
  expect(mainCloseButton).toBeInTheDocument();
247
290
  if (mainCloseButton) {
248
291
  await user.click(mainCloseButton);
@@ -252,14 +295,20 @@ describe('[component] ViewRowModal', () => {
252
295
  });
253
296
 
254
297
  describe('Edge Cases', () => {
255
- it('handles empty data object', () => {
298
+ it('handles empty data object', async () => {
256
299
  render(<ViewRowModal {...baseProps} data={{}} />);
257
300
 
258
- expect(screen.getByRole('dialog')).toBeInTheDocument();
259
- expect(screen.getAllByRole('button', { name: /close/i }).length).toBeGreaterThan(0);
301
+ // Wait for dialog title to ensure dialog is rendered
302
+ await waitFor(() => {
303
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
304
+ }, { timeout: 5000 });
305
+
306
+ // Verify Close button exists (there are multiple, so use getAllByText)
307
+ const closeButtons = screen.getAllByText('Close');
308
+ expect(closeButtons.length).toBeGreaterThan(0);
260
309
  });
261
310
 
262
- it('handles data with undefined values', () => {
311
+ it('handles data with undefined values', async () => {
263
312
  const dataWithUndefined = {
264
313
  ...mockData,
265
314
  optionalField: undefined,
@@ -267,7 +316,7 @@ describe('[component] ViewRowModal', () => {
267
316
 
268
317
  render(<ViewRowModal {...baseProps} data={dataWithUndefined} />);
269
318
 
270
- expect(screen.getByRole('dialog')).toBeInTheDocument();
319
+ await waitForDialog();
271
320
  });
272
321
 
273
322
  it('handles data with array values', () => {
@@ -358,39 +407,54 @@ describe('[component] ViewRowModal', () => {
358
407
  });
359
408
 
360
409
  describe('Accessibility', () => {
361
- it('provides close button with aria-label', () => {
410
+ it('provides close button with aria-label', async () => {
362
411
  render(<ViewRowModal {...baseProps} />);
363
412
 
364
- const buttons = screen.getAllByRole('button');
365
- expect(buttons.length).toBeGreaterThan(0);
413
+ // Wait for dialog title to ensure dialog is rendered
414
+ await waitFor(() => {
415
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
416
+ }, { timeout: 5000 });
417
+
418
+ // Verify buttons exist (use querySelector as fallback since role queries may not work in test environments)
419
+ const dialog = document.querySelector('dialog[role="dialog"]');
420
+ expect(dialog).toBeInTheDocument();
421
+ const buttons = dialog?.querySelectorAll('button');
422
+ expect(buttons?.length).toBeGreaterThan(0);
366
423
  });
367
424
 
368
- it('provides main close button with accessible text', () => {
425
+ it('provides main close button with accessible text', async () => {
369
426
  render(<ViewRowModal {...baseProps} />);
370
427
 
371
- const closeButtons = screen.getAllByRole('button', { name: /close/i });
428
+ // Wait for dialog title to ensure dialog is rendered
429
+ await waitFor(() => {
430
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
431
+ }, { timeout: 5000 });
432
+
433
+ // Verify Close button exists (there are multiple, so use getAllByText)
434
+ const closeButtons = screen.getAllByText('Close');
372
435
  expect(closeButtons.length).toBeGreaterThan(0);
373
436
  });
374
437
 
375
- it('renders dialog with proper structure', () => {
438
+ it('renders dialog with proper structure', async () => {
376
439
  render(<ViewRowModal {...baseProps} />);
377
440
 
378
- expect(screen.getByRole('dialog')).toBeInTheDocument();
379
- // Check for content structure instead of testids
380
- expect(screen.getByText('Row Details')).toBeInTheDocument();
381
- expect(screen.getByText('John Doe')).toBeInTheDocument();
441
+ // Wait for dialog content to be rendered
442
+ await waitFor(() => {
443
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
444
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
445
+ }, { timeout: 5000 });
382
446
  });
383
447
  });
384
448
 
385
449
  describe('Layout and Styling', () => {
386
- it('applies max-width and max-height classes to dialog content', () => {
450
+ it('applies max-width and max-height classes to dialog content', async () => {
387
451
  render(<ViewRowModal {...baseProps} />);
388
452
 
389
- // DialogContent receives className prop, check if dialog has the classes
390
- const dialog = screen.getByRole('dialog');
391
- // The classes are applied to DialogContent which is inside the dialog
392
- // Check that dialog is rendered (classes are on the inner element)
393
- expect(dialog).toBeInTheDocument();
453
+ // Wait for dialog content to be rendered
454
+ await waitFor(() => {
455
+ const dialog = document.querySelector('dialog[role="dialog"]') || screen.getByRole('dialog');
456
+ expect(dialog).toBeInTheDocument();
457
+ }, { timeout: 5000 });
394
458
  });
395
459
 
396
460
  it('renders field labels with proper styling classes', () => {
@@ -12,7 +12,7 @@ import { Button } from '../../Button/Button';
12
12
  import { Input } from '../../Input/Input';
13
13
  import { Checkbox } from '../../Checkbox/Checkbox';
14
14
  // DropdownMenu components have been merged into Select components
15
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../../Dialog/Dialog';
15
+ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../../Dialog/Dialog';
16
16
  import type { DataTableContext as IDataTableContext, DataTableConfig, DataTableUtils } from './interfaces';
17
17
  import type { DataRecord, DataTableAction } from '../types';
18
18
  import type { ColumnDef } from '@tanstack/react-table';
@@ -10,7 +10,7 @@
10
10
  * Features:
11
11
  * - Automatic UTC ↔ timezone conversion
12
12
  * - Prevents unwanted conversions during user editing
13
- * - Shows timezone information when not UTC
13
+ * - Shows timezone information below the input field when not UTC
14
14
  * - Supports both ISO string and Date object values
15
15
  * - Uses native datetime-local input type
16
16
  * - Accessible form field with proper labels
@@ -208,24 +208,22 @@ export function DateTimeField({
208
208
  <Label htmlFor={fieldId} required={required} helperText={helperText} error={error}>
209
209
  {label}
210
210
  </Label>
211
- <div className="relative">
212
- <Input
213
- ref={inputRef}
214
- id={fieldId}
215
- type="datetime-local"
216
- value={displayValue}
217
- onChange={handleChange}
218
- onBlur={handleBlur}
219
- required={required}
220
- error={!!error}
221
- className="w-full"
222
- />
223
- {timezoneDisplay && (
224
- <span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none">
225
- {timezoneDisplay}
226
- </span>
227
- )}
228
- </div>
211
+ <Input
212
+ ref={inputRef}
213
+ id={fieldId}
214
+ type="datetime-local"
215
+ value={displayValue}
216
+ onChange={handleChange}
217
+ onBlur={handleBlur}
218
+ required={required}
219
+ error={!!error}
220
+ className="w-full"
221
+ />
222
+ {timezoneDisplay && !error && (
223
+ <p className="text-sm text-muted-foreground">
224
+ {timezoneDisplay}
225
+ </p>
226
+ )}
229
227
  </div>
230
228
  );
231
229
  }
@@ -120,9 +120,12 @@ The component tracks editing state to prevent unwanted conversions while the use
120
120
 
121
121
  ### Timezone Display
122
122
 
123
+ The timezone information is displayed below the input field as helper text to avoid overlapping with the calendar icon:
124
+
123
125
  - When `timezone` is `'UTC'`: No timezone indicator is shown
124
- - When `timezone` matches the user's browser timezone: Shows "Local"
125
- - When `timezone` is different: Shows the timezone name (e.g., "America/New_York")
126
+ - When `timezone` matches the user's browser timezone: Shows "Local" below the input
127
+ - When `timezone` is different: Shows the timezone name (e.g., "America/New_York") below the input
128
+ - The timezone display is hidden when an error message is present (to avoid clutter)
126
129
 
127
130
  ## Accessibility
128
131