tempest-react-sdk 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +4 -4
  2. package/dist/components/Command/Command.module.cjs.map +1 -1
  3. package/dist/components/Command/Command.module.js.map +1 -1
  4. package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
  5. package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
  6. package/dist/components/DataTable/DataTable.cjs +1 -1
  7. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  8. package/dist/components/DataTable/DataTable.js +1 -0
  9. package/dist/components/DataTable/DataTable.js.map +1 -1
  10. package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
  11. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
  12. package/dist/components/DataTable/use-dev-warnings.js +4 -3
  13. package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
  14. package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
  15. package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
  16. package/dist/components/InstallBanner/InstallBanner.cjs +1 -1
  17. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  18. package/dist/components/InstallBanner/InstallBanner.js +62 -37
  19. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  20. package/dist/components/InstallBanner/InstallBanner.module.cjs +1 -1
  21. package/dist/components/InstallBanner/InstallBanner.module.cjs.map +1 -1
  22. package/dist/components/InstallBanner/InstallBanner.module.js +4 -3
  23. package/dist/components/InstallBanner/InstallBanner.module.js.map +1 -1
  24. package/dist/components/InstallButton/InstallButton.cjs +1 -1
  25. package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
  26. package/dist/components/InstallButton/InstallButton.js +34 -10
  27. package/dist/components/InstallButton/InstallButton.js.map +1 -1
  28. package/dist/components/InstallButton/InstallButton.module.cjs +3 -0
  29. package/dist/components/InstallButton/InstallButton.module.cjs.map +1 -0
  30. package/dist/components/InstallButton/InstallButton.module.js +10 -0
  31. package/dist/components/InstallButton/InstallButton.module.js.map +1 -0
  32. package/dist/components/InstallButton/install-hints.cjs +2 -0
  33. package/dist/components/InstallButton/install-hints.cjs.map +1 -0
  34. package/dist/components/InstallButton/install-hints.js +32 -0
  35. package/dist/components/InstallButton/install-hints.js.map +1 -0
  36. package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
  37. package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
  38. package/dist/components/Select/Select.cjs +1 -1
  39. package/dist/components/Select/Select.cjs.map +1 -1
  40. package/dist/components/Select/Select.js +43 -40
  41. package/dist/components/Select/Select.js.map +1 -1
  42. package/dist/components/Select/Select.module.cjs +1 -1
  43. package/dist/components/Select/Select.module.cjs.map +1 -1
  44. package/dist/components/Select/Select.module.js +5 -3
  45. package/dist/components/Select/Select.module.js.map +1 -1
  46. package/dist/hooks/use-install-prompt.cjs +1 -1
  47. package/dist/hooks/use-install-prompt.cjs.map +1 -1
  48. package/dist/hooks/use-install-prompt.js +9 -2
  49. package/dist/hooks/use-install-prompt.js.map +1 -1
  50. package/dist/http/api-client.cjs +1 -1
  51. package/dist/http/api-client.cjs.map +1 -1
  52. package/dist/http/api-client.js +80 -74
  53. package/dist/http/api-client.js.map +1 -1
  54. package/dist/http/decode-response.cjs +2 -0
  55. package/dist/http/decode-response.cjs.map +1 -0
  56. package/dist/http/decode-response.js +28 -0
  57. package/dist/http/decode-response.js.map +1 -0
  58. package/dist/styles/InstallButton.css +2 -0
  59. package/dist/styles/actions.css +6 -1
  60. package/dist/styles/advanced.css +2 -2
  61. package/dist/styles/component/Command.css +1 -1
  62. package/dist/styles/component/ContextMenu.css +1 -1
  63. package/dist/styles/component/DropdownMenu.css +1 -1
  64. package/dist/styles/component/InstallBanner.css +2 -0
  65. package/dist/styles/component/InstallButton.css +4 -0
  66. package/dist/styles/component/SegmentedControl.css +1 -1
  67. package/dist/styles/component/Select.css +4 -0
  68. package/dist/styles/core.css +2 -2
  69. package/dist/styles/forms.css +4 -0
  70. package/dist/styles/manifest.json +2 -1
  71. package/dist/styles/navigation.css +1 -1
  72. package/dist/styles/tokens.css +2 -2
  73. package/dist/styles.css +1 -1
  74. package/dist/tempest-react-sdk.cjs +1 -1
  75. package/dist/tempest-react-sdk.d.ts +258 -26
  76. package/dist/tempest-react-sdk.js +319 -318
  77. package/dist/theme/ThemeProvider.cjs +1 -1
  78. package/dist/theme/ThemeProvider.cjs.map +1 -1
  79. package/dist/theme/ThemeProvider.js +40 -45
  80. package/dist/theme/ThemeProvider.js.map +1 -1
  81. package/dist/theme/create-theme.cjs +1 -1
  82. package/dist/theme/create-theme.cjs.map +1 -1
  83. package/dist/theme/create-theme.js +1 -1
  84. package/dist/theme/create-theme.js.map +1 -1
  85. package/dist/theme/initial-theme.cjs +2 -2
  86. package/dist/theme/initial-theme.cjs.map +1 -1
  87. package/dist/theme/initial-theme.js +7 -12
  88. package/dist/theme/initial-theme.js.map +1 -1
  89. package/dist/utils/format.cjs +1 -1
  90. package/dist/utils/format.cjs.map +1 -1
  91. package/dist/utils/format.js +5 -3
  92. package/dist/utils/format.js.map +1 -1
  93. package/dist/utils/storage.cjs +1 -1
  94. package/dist/utils/storage.cjs.map +1 -1
  95. package/dist/utils/storage.js +13 -0
  96. package/dist/utils/storage.js.map +1 -1
  97. package/package.json +1 -1
package/README.md CHANGED
@@ -284,7 +284,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
284
284
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
285
285
  | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`), `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
286
286
  | `charts` _(subpath `tempest-react-sdk/charts`, opt. peer `recharts`)_ | `AreaChart`, `BarChart`, `LineChart`, `PieChart`, `RadarChart`, `DEFAULT_CHART_COLORS` — themed recharts wrappers (recharts externalized; install it only if you use charts) |
287
- | `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2024 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one chunk per initial letter** (25 max) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces. Unknown slug renders `fallback` and never throws; lucide's 257 deprecated aliases keep resolving |
287
+ | `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
288
288
  | `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio`, `createAudioBus` / `useAudioBus` (gain above 100%, post-mix limiter, `setSinkId` routing), `DEFAULT_MAX_GAIN` |
289
289
  | `capture` | `BarcodeScanner`, `useBarcodeScanner`, `useTorch`, `isBarcodeDetectionSupported`, `getSupportedBarcodeFormats`, `createBarcodeDetector`, `normalizeBarcode`, `useVideoRecorder`, `createVideoRecorder`, `pickVideoMimeType`, `isVideoRecordingSupported`, `useScreenCapture`, `isScreenCaptureSupported`, `useSpeechRecognition`, `isSpeechRecognitionSupported`, `createMediaRecorder` — barcode/QR reading over the native `BarcodeDetector` (Chromium-only, so inject a polyfill through `detector` or render the `unsupported` fallback), video + screen recording on the same engine as the audio recorder, and Web Speech dictation (**Chromium streams the audio to a Google server**) |
290
290
  | `offline` _(peer: `dexie`)_ | `createOfflineStore`, `createOfflineSync` (outbox + delta-pull + watermark engine, now with `subscribe`/`getState`/`dispose` + cross-tab `crossTab`), `useOfflineSync`/`useSyncStatus` (reactive hooks), `lastWriteWins`/`higherVersionWins` (conflict resolvers), types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions`, `OfflineSync`, `OfflineSyncConfig`, `OutboxEntry`, `PullPage`, `SyncPhase`, `SyncState`, `SyncRunSummary`, `SyncTrigger`, `SyncStatus`, `SyncTone`, `WatermarkStore` |
@@ -316,7 +316,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
316
316
  | `webrtc` | `tuneOpus` (Opus `fmtp` per audio m-line: per-key merge, `stereo` + `sprop-stereo`, inserts a missing `fmtp`), `setTunedLocalDescription` (falls back to the untouched SDP when the browser refuses the edit), `setSenderBitrate`, `createLinkStatsSampler` / `useLinkStats` / `readRoundTripMs` (throughput as a delta, round trip from the **selected** candidate pair) |
317
317
  | `geo` _(opt. peer `leaflet`)_ | `haversineKm`, `pathLengthKm`, `bearingDeg`, `estimateTravel`, `createOSRMBackend`, `boundingBox`, `projectMercator`, `fitProjection`, `createPositionTracker`, `usePositionTracker`, `TrajectoryMap` — tile-free SVG map + trajectory tracking (no external/paid API; Leaflet is an opt-in, lazy-loaded tile layer for self-hosted tiles), types: `Coordinate`, `TrackPoint`, `TravelEstimate`, `TravelMode`, `GeoBounds`, `RoutingBackend` |
318
318
  | `br` _(subpath `tempest-react-sdk/br`)_ | `BrazilMap`, `BrazilStateMap`, `BrazilStateCitySelect`, `listStates`, `getState`, `citiesByUf`, `statesByRegion`, `ufChoices`, `cityChoices`, `isValidUf`, `normalizeUf`, `isValidCity`, `loadBrUfGeoJson`, `loadStateMunicipalities`, types `UF`, `BrRegion`, `BrazilState` — clickable 27-UF SVG map + per-state municipality submaps (bundled simplified IBGE GeoJSON, lazy per UF) + states/cities dataset, no external API; mirrors the FastAPI SDK `utils/locations` |
319
- | `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2024 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one chunk per initial letter** (25 max) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces. Unknown slug renders `fallback` and never throws; lucide's 257 deprecated aliases keep resolving |
319
+ | `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
320
320
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
321
321
  | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`), `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
322
322
  | `editor` _(subpath `tempest-react-sdk/editor`, opt. peers `@tiptap/react`+`@tiptap/starter-kit`)_ | `RichTextEditor` — themed tiptap v3 rich-text editor with toolbar (tiptap externalized; install it only if you use the editor) |
@@ -1938,8 +1938,8 @@ npm run clean # rm -rf dist coverage
1938
1938
  Snapshot of current health:
1939
1939
 
1940
1940
  - 4705 tests / 485 files — 98.7% lines, 97.4% statements, 97.1% functions, 94.6% branches (CI floors: 98/97/96/94).
1941
- - What your app actually pays (brotli, tree-shaken slices measured by `npm run size`): `cn` alone 133 B · one `Button` 798 B · a typical app shell (5 components + router + providers + HTTP + auth + a hook) 6.88 KB · offline/PWA surface 4.41 KB · `AIChat` 9.76 KB · audio capture 5.59 KB · device capture (barcode + video + screen + speech) 5.28 KB · BR payment rails (Pix + boleto + NFe + holidays) 9.00 KB · passkeys 2.15 KB · `styles.css` 27.64 KB · the opt-in `utilities.css` 1.33 KB.
1942
- - Full-barrel ceiling — nobody imports this, it is the no-tree-shaking worst case: 108.04 KB ESM / 129.51 KB CJS.
1941
+ - What your app actually pays (brotli, tree-shaken slices measured by `npm run size` on 0.65.0): `cn` alone 203 B · one `Button` 853 B · a typical app shell (5 components + router + providers + HTTP + auth + a hook) 9.94 KB · offline/PWA surface 4.64 KB · `Chat` 10.53 KB · `AIChat` 10.16 KB · audio capture 5.88 KB · device capture (barcode + video + screen + speech) 5.46 KB · BR payment rails (Pix + boleto + NFe + holidays) 9.21 KB · passkeys 2.27 KB · `styles.css` 29.75 KB · the opt-in `utilities.css` 1.36 KB.
1942
+ - Full-barrel ceiling — nobody imports this, it is the no-tree-shaking worst case: 130.22 KB ESM / 155.07 KB CJS.
1943
1943
  - Husky pre-commit runs `lint-staged` (eslint --fix + prettier --write) on staged files.
1944
1944
 
1945
1945
  The demo gallery lives in `examples/gallery` and consumes the local SDK via `file:../..`:
@@ -1 +1 @@
1
- {"version":3,"file":"Command.module.cjs","names":[],"sources":["../../../src/components/Command/Command.module.css"],"sourcesContent":[".overlay {\n position: fixed;\n inset: 0;\n background-color: rgba(15, 23, 42, 0.55);\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: max(10vh, var(--tempest-safe-area-top)) var(--tempest-space-4) var(--tempest-space-4)\n var(--tempest-space-4);\n z-index: var(--tempest-z-modal);\n animation: tempest-fade-in var(--tempest-duration-base) var(--tempest-ease-out);\n}\n\n.dialog {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border-radius: var(--tempest-radius-xl);\n box-shadow: var(--tempest-shadow-xl);\n border: 1px solid var(--tempest-border);\n width: 100%;\n max-width: 640px;\n max-height: 70vh;\n max-height: 70dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n animation: tempest-modal-in var(--tempest-duration-base) var(--tempest-ease-emphasized);\n}\n\n.inputRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-4) var(--tempest-space-5);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.searchIcon {\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.input {\n flex: 1;\n min-width: 0;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-normal);\n}\n\n.input::placeholder {\n color: var(--tempest-text-muted);\n}\n\n.list {\n list-style: none;\n margin: 0;\n padding: var(--tempest-space-2);\n overflow-y: auto;\n}\n\n.group {\n list-style: none;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-2);\n}\n\n.groupHeading {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--tempest-text-muted);\n}\n\n.groupList {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-md);\n cursor: pointer;\n color: var(--tempest-text);\n transition: var(--tempest-transition-color);\n}\n\n.option.active {\n background-color: var(--tempest-surface);\n}\n\n.icon {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.empty {\n list-style: none;\n padding: var(--tempest-space-6) var(--tempest-space-3);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n@keyframes tempest-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes tempest-modal-in {\n from {\n opacity: 0;\n transform: translateY(-12px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .overlay,\n .dialog {\n animation: none;\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"Command.module.cjs","names":[],"sources":["../../../src/components/Command/Command.module.css"],"sourcesContent":[".overlay {\n position: fixed;\n inset: 0;\n background-color: rgba(15, 23, 42, 0.55);\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: max(10vh, var(--tempest-safe-area-top)) var(--tempest-space-4) var(--tempest-space-4)\n var(--tempest-space-4);\n z-index: var(--tempest-z-modal);\n animation: tempest-fade-in var(--tempest-duration-base) var(--tempest-ease-out);\n}\n\n.dialog {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border-radius: var(--tempest-radius-xl);\n box-shadow: var(--tempest-shadow-xl);\n border: 1px solid var(--tempest-border);\n width: 100%;\n max-width: 640px;\n max-height: 70vh;\n max-height: 70dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n animation: tempest-modal-in var(--tempest-duration-base) var(--tempest-ease-emphasized);\n}\n\n.inputRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-4) var(--tempest-space-5);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.searchIcon {\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.input {\n flex: 1;\n min-width: 0;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-normal);\n}\n\n.input::placeholder {\n color: var(--tempest-text-muted);\n}\n\n.list {\n list-style: none;\n margin: 0;\n padding: var(--tempest-space-2);\n overflow-y: auto;\n}\n\n.group {\n list-style: none;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-2);\n}\n\n.groupHeading {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--tempest-text-muted);\n}\n\n.groupList {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-md);\n cursor: pointer;\n color: var(--tempest-text);\n transition: var(--tempest-transition-color);\n}\n\n.option.active {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n box-shadow: inset 3px 0 0 0 var(--tempest-selected-indicator);\n}\n\n.icon {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.empty {\n list-style: none;\n padding: var(--tempest-space-6) var(--tempest-space-3);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n@keyframes tempest-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes tempest-modal-in {\n from {\n opacity: 0;\n transform: translateY(-12px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .overlay,\n .dialog {\n animation: none;\n }\n}\n"],"mappings":";"}
@@ -1 +1 @@
1
- {"version":3,"file":"Command.module.js","names":[],"sources":["../../../src/components/Command/Command.module.css"],"sourcesContent":[".overlay {\n position: fixed;\n inset: 0;\n background-color: rgba(15, 23, 42, 0.55);\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: max(10vh, var(--tempest-safe-area-top)) var(--tempest-space-4) var(--tempest-space-4)\n var(--tempest-space-4);\n z-index: var(--tempest-z-modal);\n animation: tempest-fade-in var(--tempest-duration-base) var(--tempest-ease-out);\n}\n\n.dialog {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border-radius: var(--tempest-radius-xl);\n box-shadow: var(--tempest-shadow-xl);\n border: 1px solid var(--tempest-border);\n width: 100%;\n max-width: 640px;\n max-height: 70vh;\n max-height: 70dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n animation: tempest-modal-in var(--tempest-duration-base) var(--tempest-ease-emphasized);\n}\n\n.inputRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-4) var(--tempest-space-5);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.searchIcon {\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.input {\n flex: 1;\n min-width: 0;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-normal);\n}\n\n.input::placeholder {\n color: var(--tempest-text-muted);\n}\n\n.list {\n list-style: none;\n margin: 0;\n padding: var(--tempest-space-2);\n overflow-y: auto;\n}\n\n.group {\n list-style: none;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-2);\n}\n\n.groupHeading {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--tempest-text-muted);\n}\n\n.groupList {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-md);\n cursor: pointer;\n color: var(--tempest-text);\n transition: var(--tempest-transition-color);\n}\n\n.option.active {\n background-color: var(--tempest-surface);\n}\n\n.icon {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.empty {\n list-style: none;\n padding: var(--tempest-space-6) var(--tempest-space-3);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n@keyframes tempest-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes tempest-modal-in {\n from {\n opacity: 0;\n transform: translateY(-12px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .overlay,\n .dialog {\n animation: none;\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"Command.module.js","names":[],"sources":["../../../src/components/Command/Command.module.css"],"sourcesContent":[".overlay {\n position: fixed;\n inset: 0;\n background-color: rgba(15, 23, 42, 0.55);\n backdrop-filter: blur(2px);\n -webkit-backdrop-filter: blur(2px);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: max(10vh, var(--tempest-safe-area-top)) var(--tempest-space-4) var(--tempest-space-4)\n var(--tempest-space-4);\n z-index: var(--tempest-z-modal);\n animation: tempest-fade-in var(--tempest-duration-base) var(--tempest-ease-out);\n}\n\n.dialog {\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n border-radius: var(--tempest-radius-xl);\n box-shadow: var(--tempest-shadow-xl);\n border: 1px solid var(--tempest-border);\n width: 100%;\n max-width: 640px;\n max-height: 70vh;\n max-height: 70dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n animation: tempest-modal-in var(--tempest-duration-base) var(--tempest-ease-emphasized);\n}\n\n.inputRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-4) var(--tempest-space-5);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.searchIcon {\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.input {\n flex: 1;\n min-width: 0;\n border: none;\n outline: none;\n background: transparent;\n color: var(--tempest-text);\n font-size: var(--tempest-text-base);\n line-height: var(--tempest-leading-normal);\n}\n\n.input::placeholder {\n color: var(--tempest-text-muted);\n}\n\n.list {\n list-style: none;\n margin: 0;\n padding: var(--tempest-space-2);\n overflow-y: auto;\n}\n\n.group {\n list-style: none;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-2);\n}\n\n.groupHeading {\n padding: var(--tempest-space-2) var(--tempest-space-3);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--tempest-text-muted);\n}\n\n.groupList {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.option {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-radius: var(--tempest-radius-md);\n cursor: pointer;\n color: var(--tempest-text);\n transition: var(--tempest-transition-color);\n}\n\n.option.active {\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n box-shadow: inset 3px 0 0 0 var(--tempest-selected-indicator);\n}\n\n.icon {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--tempest-text-muted);\n flex-shrink: 0;\n}\n\n.label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.empty {\n list-style: none;\n padding: var(--tempest-space-6) var(--tempest-space-3);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n@keyframes tempest-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes tempest-modal-in {\n from {\n opacity: 0;\n transform: translateY(-12px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .overlay,\n .dialog {\n animation: none;\n }\n}\n"],"mappings":";"}
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMenu.module.cjs","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n/*\n * A hold on a coarse pointer is this component's right click, and the platform\n * has its own ideas about what a hold means: iOS raises the callout and both\n * platforms start a text selection. Scoped to `pointer: coarse` so a mouse user\n * keeps ordinary selection over the same content.\n */\n@media (pointer: coarse) {\n .root {\n -webkit-touch-callout: none;\n user-select: none;\n }\n}\n\n/*\n * The menu takes focus when it opens so a screen reader announces it, and that\n * focus gets no ring of its own: measured in Chrome, focusing it from a long\n * press still matches `:focus-visible`, which drew a heavy ring around the whole\n * menu after every touch. The indicator that matters is on the focused item,\n * which is where the arrow keys move next.\n */\n.menu:focus {\n outline: none;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-surface);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"ContextMenu.module.cjs","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n/*\n * A hold on a coarse pointer is this component's right click, and the platform\n * has its own ideas about what a hold means: iOS raises the callout and both\n * platforms start a text selection. Scoped to `pointer: coarse` so a mouse user\n * keeps ordinary selection over the same content.\n */\n@media (pointer: coarse) {\n .root {\n -webkit-touch-callout: none;\n user-select: none;\n }\n}\n\n/*\n * The menu takes focus when it opens so a screen reader announces it, and that\n * focus gets no ring of its own: measured in Chrome, focusing it from a long\n * press still matches `:focus-visible`, which drew a heavy ring around the whole\n * menu after every touch. The indicator that matters is on the focused item,\n * which is where the arrow keys move next.\n */\n.menu:focus {\n outline: none;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n box-shadow: inset 3px 0 0 0 var(--tempest-selected-indicator);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMenu.module.js","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n/*\n * A hold on a coarse pointer is this component's right click, and the platform\n * has its own ideas about what a hold means: iOS raises the callout and both\n * platforms start a text selection. Scoped to `pointer: coarse` so a mouse user\n * keeps ordinary selection over the same content.\n */\n@media (pointer: coarse) {\n .root {\n -webkit-touch-callout: none;\n user-select: none;\n }\n}\n\n/*\n * The menu takes focus when it opens so a screen reader announces it, and that\n * focus gets no ring of its own: measured in Chrome, focusing it from a long\n * press still matches `:focus-visible`, which drew a heavy ring around the whole\n * menu after every touch. The indicator that matters is on the focused item,\n * which is where the arrow keys move next.\n */\n.menu:focus {\n outline: none;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-surface);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
1
+ {"version":3,"file":"ContextMenu.module.js","names":[],"sources":["../../../src/components/ContextMenu/ContextMenu.module.css"],"sourcesContent":[".root {\n display: contents;\n}\n\n/*\n * A hold on a coarse pointer is this component's right click, and the platform\n * has its own ideas about what a hold means: iOS raises the callout and both\n * platforms start a text selection. Scoped to `pointer: coarse` so a mouse user\n * keeps ordinary selection over the same content.\n */\n@media (pointer: coarse) {\n .root {\n -webkit-touch-callout: none;\n user-select: none;\n }\n}\n\n/*\n * The menu takes focus when it opens so a screen reader announces it, and that\n * focus gets no ring of its own: measured in Chrome, focusing it from a long\n * press still matches `:focus-visible`, which drew a heavy ring around the whole\n * menu after every touch. The indicator that matters is on the focused item,\n * which is where the arrow keys move next.\n */\n.menu:focus {\n outline: none;\n}\n\n.menu {\n position: fixed;\n z-index: var(--tempest-z-popover);\n min-width: 180px;\n max-width: 320px;\n background-color: var(--tempest-bg);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n box-shadow: var(--tempest-shadow-lg);\n padding: var(--tempest-space-1);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-text-base);\n list-style: none;\n margin: 0;\n animation: tempest-context-menu-in var(--tempest-duration-fast) var(--tempest-ease-out);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: none;\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: inherit;\n line-height: var(--tempest-leading-snug);\n text-align: left;\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n .item:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.item:focus,\n.item.active {\n outline: none;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n box-shadow: inset 3px 0 0 0 var(--tempest-selected-indicator);\n}\n\n.item:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n\n.item.danger {\n color: var(--tempest-danger);\n}\n\n.separator {\n height: 1px;\n background-color: var(--tempest-border);\n margin: var(--tempest-space-1) 0;\n}\n\n@keyframes tempest-context-menu-in {\n from {\n opacity: 0;\n transform: scale(0.96);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .menu {\n animation: none;\n }\n}\n"],"mappings":";"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-pagination.cjs"),n=require("../../hooks/use-announce.cjs"),r=require("../Pagination/Pagination.cjs"),i=require("../SearchBar/SearchBar.cjs"),a=require("../Table/Table.cjs"),o=require("../../utils/compare-values.cjs"),s=require("./DataTable.module.cjs"),c=require("./EditableCell.cjs"),ee=require("./LoadingRows.cjs"),te=require("./use-dev-warnings.cjs"),ne=require("./edit-labels.cjs");let l=require("react"),u=require("react/jsx-runtime");function re(e,t){return`${String(e)}::${String(t)}`}function d(e){return typeof e.header==`string`?e.header:String(e.key)}function f({data:f,columns:p,pageSize:m=10,searchable:h=!1,searchKeys:g,initialSort:ie,rowKey:_=(e,t)=>t,emptyMessage:ae,onCellChange:v,editLabels:y,totalItems:b,page:x,onPageChange:S,manualSort:oe,onSortChange:se,manualSearch:ce,onSearchChange:le,loading:C=!1,className:ue,...de}){let[w,fe]=(0,l.useState)(``),[T,pe]=(0,l.useState)(ie??null),{page:me,setPage:E}=t.usePagination(1,m),he=n.useAnnounce(),D=b!==void 0,O=oe??D,k=ce??D,A=x??me,j=(0,l.useCallback)(e=>{x===void 0&&E(e),S?.(e)},[x,E,S]);te.useDevWarnings({serverMode:D,controlledPage:x,onPageChange:S,sortIsManual:O,onSortChange:se,searchable:h,onSearchChange:le});let[M,N]=(0,l.useState)(null),[P,F]=(0,l.useState)(null),[I,L]=(0,l.useState)({}),[R,z]=(0,l.useState)({}),[B,V]=(0,l.useState)({}),H=(0,l.useMemo)(()=>({...ne.DEFAULT_EDIT_LABELS,...y}),[y]),U=v!==void 0&&p.some(e=>e.editable),W=(0,l.useMemo)(()=>!h||k?[]:g&&g.length>0?g:p.filter(e=>{let t=f.find(t=>t[e.key]!=null),n=t?t[e.key]:void 0;return typeof n==`string`||typeof n==`number`}).map(e=>e.key),[h,k,g,p,f]),G=(0,l.useMemo)(()=>{let e=w.trim().toLowerCase();return!e||!h||k?f:f.filter(t=>W.some(n=>{let r=t[n];return r!=null&&String(r).toLowerCase().includes(e)}))},[f,w,h,k,W]),K=(0,l.useMemo)(()=>{if(!T||O)return G;let e=T.direction===`asc`?1:-1;return[...G].sort((t,n)=>o.compareValues(t[T.key],n[T.key])*e)},[G,T,O]),ge=b??K.length,q=Math.max(1,Math.ceil(ge/m));(0,l.useEffect)(()=>{!D&&A>q&&j(q)},[D,A,q,j]);let J=D?A:Math.min(A,q),Y=(0,l.useMemo)(()=>{if(D)return K;let e=(J-1)*m;return K.slice(e,e+m)},[D,K,J,m]);function _e(e){let t=T,n=!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null;pe(n),se?.(n)}let X=(0,l.useCallback)(e=>(J-1)*m+e,[J,m]),Z=(0,l.useMemo)(()=>{if(!U)return[];let e=[];return Y.forEach((t,n)=>{let r=_(t,X(n));for(let t of p)t.editable&&e.push(re(r,t.key))}),e},[U,Y,p,X]),Q=(0,l.useCallback)((e,t,n)=>n in I?I[n]:e[t.key],[I]),ve=(0,l.useCallback)(async(e,t,n)=>{V(t=>({...t,[e]:!0}));try{await v?.(n),he(H.saved(d(t)))}catch(n){L(t=>{let n={...t};return delete n[e],n});let r=n instanceof Error&&n.message?n.message:H.saveFailed(d(t));z(t=>({...t,[e]:r}))}finally{V(t=>{let n={...t};return delete n[e],n})}},[v,he,H]),$=(0,l.useCallback)((e,t)=>{if(t===`none`){N(null),F(e);return}let n=Z.indexOf(e),r=Z[n+(t===`next`?1:-1)];if(r===void 0){N(null),F(e);return}N(r),F(null)},[Z]),ye=(0,l.useCallback)((e,t,n,r,i,a)=>{let o=Q(e,t,n);if(i===(t.formatEdit?t.formatEdit(e):String(o??``))){$(n,a);return}let s=t.parse?t.parse(i,e):t.editorType===`number`?Number(i):i.trim(),c=t.validate?.(s,e)??null;if(c){z(e=>({...e,[n]:c}));return}z(e=>{let t={...e};return delete t[n],t}),L(e=>({...e,[n]:s})),$(n,a),ve(n,t,{row:e,key:t.key,value:s,previous:o,rowIndex:X(r)})},[Q,$,ve,X]),be=(0,l.useMemo)(()=>p.map(e=>{let t=T?.key===e.key?T?.direction===`asc`?` ▲`:` ▼`:``,n=e.sortable?(0,u.jsxs)(`button`,{type:`button`,className:s.default.sortButton,onClick:()=>_e(e.key),"aria-label":`Ordenar por ${d(e)}`,children:[e.header,(0,u.jsx)(`span`,{className:s.default.sortIndicator,"aria-hidden":!0,children:t})]}):e.header,r=t=>e.render?e.render(t):t[e.key]??null,i=(t,n)=>{let r={...t,[e.key]:n};return e.render?e.render(r):r[e.key]??null},a=U&&e.editable===!0;return{key:String(e.key),header:n,align:e.align,priority:e.priority,width:e.width,render:a?(t,n)=>{let r=re(_(t,X(n)),e.key),a=Q(t,e,r),o=e.formatEdit?e.formatEdit(t):String(a??``);return(0,u.jsx)(c.EditableCell,{text:o,columnLabel:d(e),rowNumber:n+1,inputType:e.editorType??`text`,editing:M===r,refocus:P===r,saving:B[r]===!0,error:R[r]??null,errorId:`tempest-cell-error-${r.replace(/[^\w-]/g,`_`)}`,labels:H,onOpen:()=>{N(r),F(null)},onCommit:(i,a)=>ye(t,e,r,n,i,a),onCancel:()=>{N(null),F(r),z(e=>{let t={...e};return delete t[r],t})},children:i(t,a)})}:r}}),[p,T,U,M,P,B,R,H,I,X,ye]),xe=C&&Y.length===0;return(0,u.jsxs)(`div`,{className:e.cn(s.default.wrapper,ue),...de,children:[h&&(0,u.jsx)(i.SearchBar,{value:w,onChange:e=>{fe(e),j(1),le?.(e)},wrapperClassName:s.default.search}),(0,u.jsx)(`div`,{className:e.cn(C&&!xe&&s.default.pending),"aria-busy":C||void 0,"data-testid":`tempest-datatable-body`,children:xe?(0,u.jsx)(ee.LoadingRows,{columns:be.length,rows:Math.min(m,8)}):(0,u.jsx)(a.Table,{columns:be,data:Y,rowKey:(e,t)=>_(e,X(t)),emptyMessage:ae})}),q>1&&(0,u.jsx)(r.Pagination,{page:J,totalPages:q,onPageChange:j,totalItems:ge})]})}exports.DataTable=f;
1
+ const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-pagination.cjs"),n=require("../../hooks/use-announce.cjs"),r=require("../Pagination/Pagination.cjs"),i=require("../SearchBar/SearchBar.cjs"),a=require("../Table/Table.cjs"),o=require("../../utils/compare-values.cjs"),s=require("./DataTable.module.cjs"),c=require("./EditableCell.cjs"),ee=require("./LoadingRows.cjs"),te=require("./use-dev-warnings.cjs"),ne=require("./edit-labels.cjs");let l=require("react"),u=require("react/jsx-runtime");function re(e,t){return`${String(e)}::${String(t)}`}function d(e){return typeof e.header==`string`?e.header:String(e.key)}function f({data:f,columns:p,pageSize:m=10,searchable:h=!1,searchKeys:g,initialSort:ie,rowKey:_=(e,t)=>t,emptyMessage:ae,onCellChange:v,editLabels:y,totalItems:b,page:x,onPageChange:S,manualSort:oe,onSortChange:se,manualSearch:ce,onSearchChange:le,loading:C=!1,className:ue,...de}){let[w,fe]=(0,l.useState)(``),[T,pe]=(0,l.useState)(ie??null),{page:me,setPage:E}=t.usePagination(1,m),he=n.useAnnounce(),D=b!==void 0,O=oe??D,k=ce??D,A=x??me,j=(0,l.useCallback)(e=>{x===void 0&&E(e),S?.(e)},[x,E,S]);te.useDevWarnings({serverMode:D,controlledPage:x,onPageChange:S,sortIsManual:O,hasSortableColumn:p.some(e=>e.sortable),onSortChange:se,searchable:h,onSearchChange:le});let[M,N]=(0,l.useState)(null),[P,F]=(0,l.useState)(null),[I,L]=(0,l.useState)({}),[R,z]=(0,l.useState)({}),[B,V]=(0,l.useState)({}),H=(0,l.useMemo)(()=>({...ne.DEFAULT_EDIT_LABELS,...y}),[y]),U=v!==void 0&&p.some(e=>e.editable),W=(0,l.useMemo)(()=>!h||k?[]:g&&g.length>0?g:p.filter(e=>{let t=f.find(t=>t[e.key]!=null),n=t?t[e.key]:void 0;return typeof n==`string`||typeof n==`number`}).map(e=>e.key),[h,k,g,p,f]),G=(0,l.useMemo)(()=>{let e=w.trim().toLowerCase();return!e||!h||k?f:f.filter(t=>W.some(n=>{let r=t[n];return r!=null&&String(r).toLowerCase().includes(e)}))},[f,w,h,k,W]),K=(0,l.useMemo)(()=>{if(!T||O)return G;let e=T.direction===`asc`?1:-1;return[...G].sort((t,n)=>o.compareValues(t[T.key],n[T.key])*e)},[G,T,O]),ge=b??K.length,q=Math.max(1,Math.ceil(ge/m));(0,l.useEffect)(()=>{!D&&A>q&&j(q)},[D,A,q,j]);let J=D?A:Math.min(A,q),Y=(0,l.useMemo)(()=>{if(D)return K;let e=(J-1)*m;return K.slice(e,e+m)},[D,K,J,m]);function _e(e){let t=T,n=!t||t.key!==e?{key:e,direction:`asc`}:t.direction===`asc`?{key:e,direction:`desc`}:null;pe(n),se?.(n)}let X=(0,l.useCallback)(e=>(J-1)*m+e,[J,m]),Z=(0,l.useMemo)(()=>{if(!U)return[];let e=[];return Y.forEach((t,n)=>{let r=_(t,X(n));for(let t of p)t.editable&&e.push(re(r,t.key))}),e},[U,Y,p,X]),Q=(0,l.useCallback)((e,t,n)=>n in I?I[n]:e[t.key],[I]),ve=(0,l.useCallback)(async(e,t,n)=>{V(t=>({...t,[e]:!0}));try{await v?.(n),he(H.saved(d(t)))}catch(n){L(t=>{let n={...t};return delete n[e],n});let r=n instanceof Error&&n.message?n.message:H.saveFailed(d(t));z(t=>({...t,[e]:r}))}finally{V(t=>{let n={...t};return delete n[e],n})}},[v,he,H]),$=(0,l.useCallback)((e,t)=>{if(t===`none`){N(null),F(e);return}let n=Z.indexOf(e),r=Z[n+(t===`next`?1:-1)];if(r===void 0){N(null),F(e);return}N(r),F(null)},[Z]),ye=(0,l.useCallback)((e,t,n,r,i,a)=>{let o=Q(e,t,n);if(i===(t.formatEdit?t.formatEdit(e):String(o??``))){$(n,a);return}let s=t.parse?t.parse(i,e):t.editorType===`number`?Number(i):i.trim(),c=t.validate?.(s,e)??null;if(c){z(e=>({...e,[n]:c}));return}z(e=>{let t={...e};return delete t[n],t}),L(e=>({...e,[n]:s})),$(n,a),ve(n,t,{row:e,key:t.key,value:s,previous:o,rowIndex:X(r)})},[Q,$,ve,X]),be=(0,l.useMemo)(()=>p.map(e=>{let t=T?.key===e.key?T?.direction===`asc`?` ▲`:` ▼`:``,n=e.sortable?(0,u.jsxs)(`button`,{type:`button`,className:s.default.sortButton,onClick:()=>_e(e.key),"aria-label":`Ordenar por ${d(e)}`,children:[e.header,(0,u.jsx)(`span`,{className:s.default.sortIndicator,"aria-hidden":!0,children:t})]}):e.header,r=t=>e.render?e.render(t):t[e.key]??null,i=(t,n)=>{let r={...t,[e.key]:n};return e.render?e.render(r):r[e.key]??null},a=U&&e.editable===!0;return{key:String(e.key),header:n,align:e.align,priority:e.priority,width:e.width,render:a?(t,n)=>{let r=re(_(t,X(n)),e.key),a=Q(t,e,r),o=e.formatEdit?e.formatEdit(t):String(a??``);return(0,u.jsx)(c.EditableCell,{text:o,columnLabel:d(e),rowNumber:n+1,inputType:e.editorType??`text`,editing:M===r,refocus:P===r,saving:B[r]===!0,error:R[r]??null,errorId:`tempest-cell-error-${r.replace(/[^\w-]/g,`_`)}`,labels:H,onOpen:()=>{N(r),F(null)},onCommit:(i,a)=>ye(t,e,r,n,i,a),onCancel:()=>{N(null),F(r),z(e=>{let t={...e};return delete t[r],t})},children:i(t,a)})}:r}}),[p,T,U,M,P,B,R,H,I,X,ye]),xe=C&&Y.length===0;return(0,u.jsxs)(`div`,{className:e.cn(s.default.wrapper,ue),...de,children:[h&&(0,u.jsx)(i.SearchBar,{value:w,onChange:e=>{fe(e),j(1),le?.(e)},wrapperClassName:s.default.search}),(0,u.jsx)(`div`,{className:e.cn(C&&!xe&&s.default.pending),"aria-busy":C||void 0,"data-testid":`tempest-datatable-body`,children:xe?(0,u.jsx)(ee.LoadingRows,{columns:be.length,rows:Math.min(m,8)}):(0,u.jsx)(a.Table,{columns:be,data:Y,rowKey:(e,t)=>_(e,X(t)),emptyMessage:ae})}),q>1&&(0,u.jsx)(r.Pagination,{page:J,totalPages:q,onPageChange:j,totalItems:ge})]})}exports.DataTable=f;
2
2
  //# sourceMappingURL=DataTable.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.cjs","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n *\n * Each of the four also runs in a second mode, where the caller owns the work and\n * the table only reports intent: totalItems/page/onPageChange, onSearchChange,\n * manualSort/onSortChange, loading/loadingRows. That doubles the props without\n * adding a fifth job — every manual prop short-circuits one stage of the same\n * pipeline.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\n/** Everything a table needs regardless of who owns paging, sorting and searching. */\nexport interface DataTableBaseProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/**\n * Paging, as one of the three shapes that actually work.\n *\n * These used to be three optional props, so the compiler accepted\n * `totalItems` with no `page` — a table whose pager moves an internal page while\n * `data` keeps showing page one. Every prop was optional on its own, so the only\n * place left to catch it was a `console.warn` in dev, in the browser, with the\n * component mounted. As a union the same mistake is a build error at the call\n * site, for free, everywhere.\n */\nexport type DataTablePagingProps =\n | {\n /** Not server mode. */\n totalItems?: never;\n /** The table owns the page. */\n page?: never;\n /** Nothing to report to. */\n onPageChange?: never;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: never;\n /** Current page, 1-based. Controlled — required in server mode. */\n page: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange: (page: number) => void;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). `page` and\n * `onPageChange` come with it — the type says so, because a server-mode\n * table without them silently shows page one forever.\n */\n totalItems: number;\n /** Current page, 1-based. Controlled, and required in server mode. */\n page: number;\n /** Called with the next page. */\n onPageChange: (page: number) => void;\n };\n\n/**\n * Sorting: delegated, and therefore reported, or neither.\n *\n * `manualSort` without `onSortChange` renders a header that moves its arrow and\n * changes nothing else — the arrow is a lie the compiler can now catch.\n */\nexport type DataTableSortProps<T> =\n | {\n /** The table sorts the rows it has. */\n manualSort?: false;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n }\n | {\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort: true;\n /** Where the click goes. Required, since nothing else acts on it. */\n onSortChange: (sort: DataTableSort<T> | null) => void;\n };\n\n/**\n * Searching: delegated, and therefore reported, or neither.\n *\n * `manualSearch` without `onSearchChange` renders a search box that filters\n * nothing and tells nobody — the same shape of lie as a header arrow that turns\n * without sorting.\n *\n * Independent of the paging axis on purpose, and that leaves one gap this type\n * does not close: `totalItems` *implies* `manualSearch`, so a server-mode table\n * with `searchable` and no `onSearchChange` falls into the same hole without ever\n * writing `manualSearch`. Closing it means the search axis has to read the paging\n * axis, which crosses two three-member unions into nine and turns every mismatch\n * into a wall of candidate shapes. That case is a dev warning instead — the one\n * spot where runtime really is the cheaper check, and `use-dev-warnings.ts` says\n * so at the call site.\n */\nexport type DataTableSearchProps =\n | {\n /** The table filters the rows it has. */\n manualSearch?: false;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n }\n | {\n /**\n * Searching is the caller's job: typing reports through `onSearchChange`\n * and the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch: true;\n /** Where the typing goes. Required, since nothing else acts on it. */\n onSearchChange: (term: string) => void;\n };\n\n/**\n * The table's props: the shared half, plus one valid paging shape and one valid\n * sorting shape.\n */\nexport type DataTableProps<T> = DataTableBaseProps<T> &\n DataTablePagingProps &\n DataTableSortProps<T> &\n DataTableSearchProps;\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n onSortChange,\n searchable,\n onSearchChange,\n });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (!searchable || searchIsManual) return [];\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchable, searchIsManual, searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable.cjs","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n *\n * Each of the four also runs in a second mode, where the caller owns the work and\n * the table only reports intent: totalItems/page/onPageChange, onSearchChange,\n * manualSort/onSortChange, loading/loadingRows. That doubles the props without\n * adding a fifth job — every manual prop short-circuits one stage of the same\n * pipeline.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\n/** Everything a table needs regardless of who owns paging, sorting and searching. */\nexport interface DataTableBaseProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/**\n * Paging, as one of the three shapes that actually work.\n *\n * These used to be three optional props, so the compiler accepted\n * `totalItems` with no `page` — a table whose pager moves an internal page while\n * `data` keeps showing page one. Every prop was optional on its own, so the only\n * place left to catch it was a `console.warn` in dev, in the browser, with the\n * component mounted. As a union the same mistake is a build error at the call\n * site, for free, everywhere.\n */\nexport type DataTablePagingProps =\n | {\n /** Not server mode. */\n totalItems?: never;\n /** The table owns the page. */\n page?: never;\n /** Nothing to report to. */\n onPageChange?: never;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: never;\n /** Current page, 1-based. Controlled — required in server mode. */\n page: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange: (page: number) => void;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). `page` and\n * `onPageChange` come with it — the type says so, because a server-mode\n * table without them silently shows page one forever.\n */\n totalItems: number;\n /** Current page, 1-based. Controlled, and required in server mode. */\n page: number;\n /** Called with the next page. */\n onPageChange: (page: number) => void;\n };\n\n/**\n * Sorting: delegated, and therefore reported, or neither.\n *\n * `manualSort` without `onSortChange` renders a header that moves its arrow and\n * changes nothing else — the arrow is a lie the compiler can now catch.\n */\nexport type DataTableSortProps<T> =\n | {\n /** The table sorts the rows it has. */\n manualSort?: false;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n }\n | {\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort: true;\n /** Where the click goes. Required, since nothing else acts on it. */\n onSortChange: (sort: DataTableSort<T> | null) => void;\n };\n\n/**\n * Searching: delegated, and therefore reported, or neither.\n *\n * `manualSearch` without `onSearchChange` renders a search box that filters\n * nothing and tells nobody — the same shape of lie as a header arrow that turns\n * without sorting.\n *\n * Independent of the paging axis on purpose, and that leaves one gap this type\n * does not close: `totalItems` *implies* `manualSearch`, so a server-mode table\n * with `searchable` and no `onSearchChange` falls into the same hole without ever\n * writing `manualSearch`. Closing it means the search axis has to read the paging\n * axis, which crosses two three-member unions into nine and turns every mismatch\n * into a wall of candidate shapes. That case is a dev warning instead — the one\n * spot where runtime really is the cheaper check, and `use-dev-warnings.ts` says\n * so at the call site.\n */\nexport type DataTableSearchProps =\n | {\n /** The table filters the rows it has. */\n manualSearch?: false;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n }\n | {\n /**\n * Searching is the caller's job: typing reports through `onSearchChange`\n * and the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch: true;\n /** Where the typing goes. Required, since nothing else acts on it. */\n onSearchChange: (term: string) => void;\n };\n\n/**\n * The table's props: the shared half, plus one valid paging shape and one valid\n * sorting shape.\n */\nexport type DataTableProps<T> = DataTableBaseProps<T> &\n DataTablePagingProps &\n DataTableSortProps<T> &\n DataTableSearchProps;\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n hasSortableColumn: columns.some((column) => column.sortable),\n onSortChange,\n searchable,\n onSearchChange,\n });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (!searchable || searchIsManual) return [];\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchable, searchIsManual, searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"yfAwQA,SAAS,GAAO,EAA8B,EAAgC,CAC1E,MAAO,GAAG,OAAO,CAAW,EAAE,IAAI,OAAO,CAAS,GACtD,CAEA,SAAS,EAAc,EAAoC,CACvD,OAAO,OAAO,EAAO,QAAW,SAAW,EAAO,OAAS,OAAO,EAAO,GAAG,CAChF,CA+BA,SAAgB,EAAa,CACzB,OACA,UACA,WAAW,GACX,aAAa,GACb,aACA,eACA,UAAU,EAAM,IAAU,EAC1B,gBACA,eACA,aACA,aACA,KAAM,EACN,eACA,cACA,gBACA,gBACA,kBACA,UAAU,GACV,aACA,GAAG,IACe,CAClB,GAAM,CAAC,EAAQ,KAAA,EAAa,EAAA,SAAA,CAAiB,EAAE,EACzC,CAAC,EAAM,KAAA,EAAW,EAAA,SAAA,CAAkC,IAAe,IAAI,EACvE,CAAE,KAAM,GAAc,QAAS,GAAoB,EAAA,cAAc,EAAG,CAAQ,EAC5E,GAAW,EAAA,YAAY,EAEvB,EAAa,IAAe,IAAA,GAC5B,EAAe,IAAc,EAC7B,EAAiB,IAAgB,EACjC,EAAO,GAAkB,GAEzB,GAAA,EAAU,EAAA,YAAA,CACX,GAAiB,CACV,IAAmB,IAAA,IAAW,EAAgB,CAAI,EACtD,IAAe,CAAI,CACvB,EACA,CAAC,EAAgB,EAAiB,CAAY,CAClD,EAEA,GAAA,eAAe,CACX,aACA,iBACA,eACA,eACA,kBAAmB,EAAQ,KAAM,GAAW,EAAO,QAAQ,EAC3D,gBACA,aACA,iBACJ,CAAC,EAED,GAAM,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAwB,IAAI,EACpD,CAAC,EAAS,IAAA,EAAc,EAAA,SAAA,CAAwB,IAAI,EACpD,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAkC,CAAC,CAAC,EAChE,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAiC,CAAC,CAAC,EACzD,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAkC,CAAC,CAAC,EAE1D,GAAA,EAAS,EAAA,QAAA,MACJ,CAAE,GAAG,GAAA,oBAAqB,GAAG,CAAW,GAC/C,CAAC,CAAU,CACf,EACM,EAAiB,IAAiB,IAAA,IAAa,EAAQ,KAAM,GAAW,EAAO,QAAQ,EAEvF,GAAA,EAAsB,EAAA,QAAA,KACpB,CAAC,GAAc,EAAuB,CAAC,EACvC,GAAc,EAAW,OAAS,EAAU,EACzC,EACF,OAAQ,GAAW,CAChB,IAAM,EAAS,EAAK,KAAM,GAAQ,EAAI,EAAO,MAAQ,IAAI,EACnD,EAAQ,EAAS,EAAO,EAAO,KAAO,IAAA,GAC5C,OAAO,OAAO,GAAU,UAAY,OAAO,GAAU,QACzD,CAAC,CAAC,CACD,IAAK,GAAW,EAAO,GAAG,EAChC,CAAC,EAAY,EAAgB,EAAY,EAAS,CAAI,CAAC,EAEpD,GAAA,EAAW,EAAA,QAAA,KAAmB,CAChC,IAAM,EAAO,EAAO,KAAK,CAAC,CAAC,YAAY,EAEvC,MADI,CAAC,GAAQ,CAAC,GAAc,EAAuB,EAC5C,EAAK,OAAQ,GAChB,EAAoB,KAAM,GAAQ,CAC9B,IAAM,EAAQ,EAAI,GAClB,OAAO,GAAS,MAAQ,OAAO,CAAK,CAAC,CAAC,YAAY,CAAC,CAAC,SAAS,CAAI,CACrE,CAAC,CACL,CACJ,EAAG,CAAC,EAAM,EAAQ,EAAY,EAAgB,CAAmB,CAAC,EAE5D,GAAA,EAAS,EAAA,QAAA,KAAmB,CAC9B,GAAI,CAAC,GAAQ,EAAc,OAAO,EAClC,IAAM,EAAS,EAAK,YAAc,MAAQ,EAAI,GAC9C,MAAO,CAAC,GAAG,CAAQ,CAAC,CAAC,MAAM,EAAG,IAAM,EAAA,cAAc,EAAE,EAAK,KAAM,EAAE,EAAK,IAAI,EAAI,CAAM,CACxF,EAAG,CAAC,EAAU,EAAM,CAAY,CAAC,EAE3B,GAAW,GAAc,EAAO,OAChC,EAAa,KAAK,IAAI,EAAG,KAAK,KAAK,GAAW,CAAQ,CAAC,GAS7D,EAAA,EAAA,UAAA,KAAgB,CACR,CAAC,GAAc,EAAO,GAAY,EAAQ,CAAU,CAC5D,EAAG,CAAC,EAAY,EAAM,EAAY,CAAO,CAAC,EAE1C,IAAM,EAAW,EAAa,EAAO,KAAK,IAAI,EAAM,CAAU,EACxD,GAAA,EAAW,EAAA,QAAA,KAAmB,CAChC,GAAI,EAAY,OAAO,EACvB,IAAM,GAAS,EAAW,GAAK,EAC/B,OAAO,EAAO,MAAM,EAAO,EAAQ,CAAQ,CAC/C,EAAG,CAAC,EAAY,EAAQ,EAAU,CAAQ,CAAC,EAE3C,SAAS,GAAW,EAAoB,CACpC,IAAM,EAAU,EACV,EACF,CAAC,GAAW,EAAQ,MAAQ,EACtB,CAAE,MAAK,UAAW,KAAM,EACxB,EAAQ,YAAc,MACpB,CAAE,MAAK,UAAW,MAAO,EACzB,KACZ,GAAQ,CAAI,EACZ,KAAe,CAAI,CACvB,CAEA,IAAM,GAAA,EAAgB,EAAA,YAAA,CACjB,IAAuB,EAAW,GAAK,EAAW,EACnD,CAAC,EAAU,CAAQ,CACvB,EASM,GAAA,EAAkB,EAAA,QAAA,KAAwB,CAC5C,GAAI,CAAC,EAAgB,MAAO,CAAC,EAC7B,IAAM,EAAgB,CAAC,EAOvB,OANA,EAAS,SAAS,EAAK,IAAU,CAC7B,IAAM,EAAM,EAAO,EAAK,EAAc,CAAK,CAAC,EAC5C,IAAK,IAAM,KAAU,EACb,EAAO,UAAU,EAAI,KAAK,GAAO,EAAK,EAAO,GAAG,CAAC,CAE7D,CAAC,EACM,CAEX,EAAG,CAAC,EAAgB,EAAU,EAAS,CAAa,CAAC,EAE/C,GAAA,EAAY,EAAA,YAAA,EACb,EAAQ,EAA4B,IACjC,KAAM,EAAY,EAAU,GAAM,EAAI,EAAO,KACjD,CAAC,CAAS,CACd,EAUM,IAAA,EAAU,EAAA,YAAA,CACZ,MAAO,EAAY,EAA4B,IAAmC,CAC9E,EAAW,IAAa,CAAE,GAAG,GAAU,GAAK,EAAK,EAAE,EACnD,GAAI,CACA,MAAM,IAAe,CAAM,EAC3B,GAAS,EAAO,MAAM,EAAW,CAAM,CAAC,CAAC,CAC7C,OAAS,EAAO,CACZ,EAAc,GAAY,CACtB,IAAM,EAAO,CAAE,GAAG,CAAQ,EAE1B,OADA,OAAO,EAAK,GACL,CACX,CAAC,EACD,IAAM,EACF,aAAiB,OAAS,EAAM,QAC1B,EAAM,QACN,EAAO,WAAW,EAAW,CAAM,CAAC,EAC9C,EAAW,IAAa,CAAE,GAAG,GAAU,GAAK,CAAQ,EAAE,CAC1D,QAAU,CACN,EAAW,GAAY,CACnB,IAAM,EAAO,CAAE,GAAG,CAAQ,EAE1B,OADA,OAAO,EAAK,GACL,CACX,CAAC,CACL,CACJ,EACA,CAAC,EAAc,GAAU,CAAM,CACnC,EAEM,GAAA,EAAW,EAAA,YAAA,EACZ,EAAY,IAA+B,CACxC,GAAI,IAAS,OAAQ,CACjB,EAAW,IAAI,EACf,EAAW,CAAE,EACb,MACJ,CACA,IAAM,EAAQ,EAAgB,QAAQ,CAAE,EAClC,EAAS,EAAgB,GAAS,IAAS,OAAS,EAAI,KAC9D,GAAI,IAAW,IAAA,GAAW,CACtB,EAAW,IAAI,EACf,EAAW,CAAE,EACb,MACJ,CACA,EAAW,CAAM,EACjB,EAAW,IAAI,CACnB,EACA,CAAC,CAAe,CACpB,EASM,IAAA,EAAS,EAAA,YAAA,EAEP,EACA,EACA,EACA,EACA,EACA,IACO,CACP,IAAM,EAAW,EAAU,EAAK,EAAQ,CAAE,EAE1C,GAAI,KADgB,EAAO,WAAa,EAAO,WAAW,CAAG,EAAI,OAAO,GAAY,EAAE,GAC7D,CACrB,EAAS,EAAI,CAAI,EACjB,MACJ,CAEA,IAAM,EAAQ,EAAO,MACf,EAAO,MAAM,EAAK,CAAG,EACrB,EAAO,aAAe,SACpB,OAAO,CAAG,EACV,EAAI,KAAK,EAEX,EAAU,EAAO,WAAW,EAAO,CAAG,GAAK,KACjD,GAAI,EAAS,CACT,EAAW,IAAa,CAAE,GAAG,GAAU,GAAK,CAAQ,EAAE,EACtD,MACJ,CAEA,EAAW,GAAY,CACnB,IAAM,EAAO,CAAE,GAAG,CAAQ,EAE1B,OADA,OAAO,EAAK,GACL,CACX,CAAC,EACD,EAAc,IAAa,CAAE,GAAG,GAAU,GAAK,CAAM,EAAE,EACvD,EAAS,EAAI,CAAI,EACjB,GAAa,EAAI,EAAQ,CACrB,MACA,IAAK,EAAO,IACZ,QACA,WACA,SAAU,EAAc,CAAS,CACrC,CAAC,CACL,EACA,CAAC,EAAW,EAAU,GAAS,CAAa,CAChD,EAEM,IAAA,EAAe,EAAA,QAAA,KAEb,EAAQ,IAAK,GAAW,CAEpB,IAAM,EADW,GAAM,MAAQ,EAAO,IACR,GAAM,YAAc,MAAQ,KAAO,KAAQ,GACnE,EAAS,EAAO,UAClB,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,WAClB,YAAe,GAAW,EAAO,GAAG,EACpC,aAAY,eAAe,EAAW,CAAM,IAJhD,SAAA,CAMK,EAAO,QACR,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,cAAe,cAAA,GAClC,SAAA,CACC,CAAA,CACF,CAER,CAAA,EAAA,EAAO,OAGL,EAAa,GACX,EAAO,OAAe,EAAO,OAAO,CAAG,EACnC,EAAI,EAAO,MAAsB,KAWvC,GAAe,EAAQ,IAA8B,CACvD,IAAM,EAAU,CAAE,GAAG,GAAM,EAAO,KAAM,CAAM,EAE9C,OADI,EAAO,OAAe,EAAO,OAAO,CAAO,EACvC,EAAQ,EAAO,MAAsB,IACjD,EAEM,EAAW,GAAkB,EAAO,WAAa,GAEvD,MAAO,CACH,IAAK,OAAO,EAAO,GAAG,EACtB,SACA,MAAO,EAAO,MACd,SAAU,EAAO,SACjB,MAAO,EAAO,MACd,OAAQ,GACD,EAAQ,IAAkB,CACvB,IAAM,EAAK,GAAO,EAAO,EAAK,EAAc,CAAK,CAAC,EAAG,EAAO,GAAG,EACzD,EAAQ,EAAU,EAAK,EAAQ,CAAE,EACjC,EAAO,EAAO,WACd,EAAO,WAAW,CAAG,EACrB,OAAO,GAAS,EAAE,EACxB,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CACU,OACN,YAAa,EAAW,CAAM,EAC9B,UAAW,EAAQ,EACnB,UAAW,EAAO,YAAc,OAChC,QAAS,IAAY,EACrB,QAAS,IAAY,EACrB,OAAQ,EAAO,KAAQ,GACvB,MAAO,EAAO,IAAO,KACrB,QAAS,sBAAsB,EAAG,QAAQ,UAAW,GAAG,IAChD,SACR,WAAc,CACV,EAAW,CAAE,EACb,EAAW,IAAI,CACnB,EACA,UAAW,EAAK,IACZ,GAAO,EAAK,EAAQ,EAAI,EAAO,EAAK,CAAI,EAE5C,aAAgB,CACZ,EAAW,IAAI,EACf,EAAW,CAAE,EACb,EAAW,GAAY,CACnB,IAAM,EAAO,CAAE,GAAG,CAAQ,EAE1B,OADA,OAAO,EAAK,GACL,CACX,CAAC,CACL,EAEC,SAAA,EAAY,EAAK,CAAK,CACb,CAAA,CAEtB,EACA,CACV,CACJ,CAAC,EAEL,CACI,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACJ,CACJ,EAEM,GAAe,GAAW,EAAS,SAAW,EAEpD,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,EAAS,EAAG,GAAI,GAAnD,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CACI,MAAO,EACP,SAAW,GAAU,CACjB,GAAU,CAAK,EACf,EAAQ,CAAC,EACT,KAAiB,CAAK,CAC1B,EACA,iBAAkB,EAAA,QAAO,MAC5B,CAAA,GAEL,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,GAAG,GAAW,CAAC,IAAgB,EAAA,QAAO,OAAO,EACxD,YAAW,GAAW,IAAA,GACtB,cAAY,yBAEX,SAAA,IACG,EAAA,EAAA,IAAA,CAAC,GAAA,YAAD,CAAa,QAAS,GAAa,OAAQ,KAAM,KAAK,IAAI,EAAU,CAAC,CAAI,CAAA,GAEzE,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACI,QAAS,GACT,KAAM,EACN,QAAS,EAAK,IAAU,EAAO,EAAK,EAAc,CAAK,CAAC,EAC1C,eACjB,CAAA,CAEJ,CAAA,EACJ,EAAa,IACV,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACI,KAAM,EACM,aACZ,aAAc,EACd,WAAY,EACf,CAAA,CAEJ,GAEb"}
@@ -32,6 +32,7 @@ function h({ data: h, columns: g, pageSize: _ = 10, searchable: v = !1, searchKe
32
32
  controlledPage: S,
33
33
  onPageChange: C,
34
34
  sortIsManual: k,
35
+ hasSortableColumn: g.some((e) => e.sortable),
35
36
  onSortChange: ue,
36
37
  searchable: v,
37
38
  onSearchChange: w
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n *\n * Each of the four also runs in a second mode, where the caller owns the work and\n * the table only reports intent: totalItems/page/onPageChange, onSearchChange,\n * manualSort/onSortChange, loading/loadingRows. That doubles the props without\n * adding a fifth job — every manual prop short-circuits one stage of the same\n * pipeline.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\n/** Everything a table needs regardless of who owns paging, sorting and searching. */\nexport interface DataTableBaseProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/**\n * Paging, as one of the three shapes that actually work.\n *\n * These used to be three optional props, so the compiler accepted\n * `totalItems` with no `page` — a table whose pager moves an internal page while\n * `data` keeps showing page one. Every prop was optional on its own, so the only\n * place left to catch it was a `console.warn` in dev, in the browser, with the\n * component mounted. As a union the same mistake is a build error at the call\n * site, for free, everywhere.\n */\nexport type DataTablePagingProps =\n | {\n /** Not server mode. */\n totalItems?: never;\n /** The table owns the page. */\n page?: never;\n /** Nothing to report to. */\n onPageChange?: never;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: never;\n /** Current page, 1-based. Controlled — required in server mode. */\n page: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange: (page: number) => void;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). `page` and\n * `onPageChange` come with it — the type says so, because a server-mode\n * table without them silently shows page one forever.\n */\n totalItems: number;\n /** Current page, 1-based. Controlled, and required in server mode. */\n page: number;\n /** Called with the next page. */\n onPageChange: (page: number) => void;\n };\n\n/**\n * Sorting: delegated, and therefore reported, or neither.\n *\n * `manualSort` without `onSortChange` renders a header that moves its arrow and\n * changes nothing else — the arrow is a lie the compiler can now catch.\n */\nexport type DataTableSortProps<T> =\n | {\n /** The table sorts the rows it has. */\n manualSort?: false;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n }\n | {\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort: true;\n /** Where the click goes. Required, since nothing else acts on it. */\n onSortChange: (sort: DataTableSort<T> | null) => void;\n };\n\n/**\n * Searching: delegated, and therefore reported, or neither.\n *\n * `manualSearch` without `onSearchChange` renders a search box that filters\n * nothing and tells nobody — the same shape of lie as a header arrow that turns\n * without sorting.\n *\n * Independent of the paging axis on purpose, and that leaves one gap this type\n * does not close: `totalItems` *implies* `manualSearch`, so a server-mode table\n * with `searchable` and no `onSearchChange` falls into the same hole without ever\n * writing `manualSearch`. Closing it means the search axis has to read the paging\n * axis, which crosses two three-member unions into nine and turns every mismatch\n * into a wall of candidate shapes. That case is a dev warning instead — the one\n * spot where runtime really is the cheaper check, and `use-dev-warnings.ts` says\n * so at the call site.\n */\nexport type DataTableSearchProps =\n | {\n /** The table filters the rows it has. */\n manualSearch?: false;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n }\n | {\n /**\n * Searching is the caller's job: typing reports through `onSearchChange`\n * and the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch: true;\n /** Where the typing goes. Required, since nothing else acts on it. */\n onSearchChange: (term: string) => void;\n };\n\n/**\n * The table's props: the shared half, plus one valid paging shape and one valid\n * sorting shape.\n */\nexport type DataTableProps<T> = DataTableBaseProps<T> &\n DataTablePagingProps &\n DataTableSortProps<T> &\n DataTableSearchProps;\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n onSortChange,\n searchable,\n onSearchChange,\n });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (!searchable || searchIsManual) return [];\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchable, searchIsManual, searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the table does four jobs\n * a caller turns on independently — paging (pageSize), search (searchable,\n * searchKeys), sort (initialSort) and inline edit (onCellChange, editLabels) — over\n * one row model (data, columns, rowKey, emptyMessage). The body is long because\n * those four share the derived-rows pipeline: filter, then sort, then page, then map\n * to cells, in that order and off the same memo.\n *\n * Each of the four also runs in a second mode, where the caller owns the work and\n * the table only reports intent: totalItems/page/onPageChange, onSearchChange,\n * manualSort/onSortChange, loading/loadingRows. That doubles the props without\n * adding a fifth job — every manual prop short-circuits one stage of the same\n * pipeline.\n */\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\nimport { usePagination } from \"@/hooks\";\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { Table, type TableAlign, type TableColumn, type TablePriority } from \"../Table\";\nimport { Pagination } from \"../Pagination\";\nimport { SearchBar } from \"../SearchBar\";\nimport { EditableCell } from \"./EditableCell\";\nimport { LoadingRows } from \"./LoadingRows\";\nimport { useDevWarnings } from \"./use-dev-warnings\";\nimport { DEFAULT_EDIT_LABELS, type CellCommitMove, type DataTableEditLabels } from \"./edit-labels\";\nimport styles from \"./DataTable.module.css\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort<T> {\n key: keyof T;\n direction: SortDirection;\n}\n\n/** Input types an editable column can use. */\nexport type DataTableEditorType = \"text\" | \"number\" | \"date\" | \"email\" | \"tel\" | \"url\";\n\n/** One accepted cell edit, handed to `onCellChange`. */\nexport interface DataTableCellChange<T> {\n /** The row as it was before the edit. */\n row: T;\n /** Which column changed. */\n key: keyof T;\n /** The parsed new value. */\n value: unknown;\n /** The value that was displayed before the edit. */\n previous: unknown;\n /** Index of the row in the full `data` array. */\n rowIndex: number;\n}\n\n/**\n * Column definition for {@link DataTable}. Extends the headless {@link Table}\n * column shape with a typed `key`, opt-in sorting, opt-in inline editing, and the\n * visual options that are forwarded to the underlying Table cell.\n */\nexport interface DataTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `String(row[key])`. */\n render?: (row: T) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment forwarded to the Table cell. */\n align?: TableAlign;\n /** Responsive visibility priority forwarded to the Table cell. */\n priority?: TablePriority;\n /** Fixed column width forwarded to the Table cell. */\n width?: string | number;\n /**\n * Let cells in this column be edited in place. Requires `onCellChange` on the\n * table; without it the column stays read-only.\n */\n editable?: boolean;\n /** Editor input type. Default `\"text\"`. */\n editorType?: DataTableEditorType;\n /** Text the editor opens with. Defaults to `String(value ?? \"\")`. */\n formatEdit?: (row: T) => string;\n /**\n * Turn the typed string into the stored value. Defaults to the trimmed string,\n * or `Number(raw)` when `editorType` is `\"number\"`.\n */\n parse?: (raw: string, row: T) => unknown;\n /** Return a message to reject the edit, or `null` to accept it. */\n validate?: (value: unknown, row: T) => string | null;\n}\n\n/** Everything a table needs regardless of who owns paging, sorting and searching. */\nexport interface DataTableBaseProps<T> extends HTMLAttributes<HTMLDivElement> {\n /**\n * The rows to work with.\n *\n * By default this is the **full** dataset and sorting, searching and paging\n * all happen in memory. Pass `totalItems` and it becomes the current page as\n * the server returned it, with those three delegated to the caller.\n */\n data: T[];\n /** Column definitions. */\n columns: DataTableColumn<T>[];\n /** Rows per page. Default 10. */\n pageSize?: number;\n /** Render a search input above the table. Default false. */\n searchable?: boolean;\n /**\n * Keys to match the search term against. When omitted, every column whose\n * value is a string or number is searched.\n */\n searchKeys?: (keyof T)[];\n /** Initial sort applied before any header interaction. */\n initialSort?: DataTableSort<T>;\n /** Stable key extractor for rows. Defaults to the row index. */\n rowKey?: (row: T, index: number) => string | number;\n /** Content shown when no rows match. */\n emptyMessage?: ReactNode;\n /**\n * Persist an accepted cell edit. Return a promise: while it is pending the cell\n * already shows the new value, and a rejection rolls that back and surfaces the\n * error in the cell. Without this prop no column is editable.\n */\n onCellChange?: (change: DataTableCellChange<T>) => void | Promise<void>;\n /** Override the PT-BR copy of the editing affordances. */\n editLabels?: Partial<DataTableEditLabels>;\n /**\n * A fetch is in flight.\n *\n * With rows already on screen they stay put, dimmed and `aria-busy`, so the\n * page does not jump under the cursor between pages. With no rows yet it\n * renders placeholder lines at full height, which is a different statement\n * from `emptyMessage`: \"loading\" and \"there is nothing\" are not the same\n * screen.\n */\n loading?: boolean;\n}\n\n/**\n * Paging, as one of the three shapes that actually work.\n *\n * These used to be three optional props, so the compiler accepted\n * `totalItems` with no `page` — a table whose pager moves an internal page while\n * `data` keeps showing page one. Every prop was optional on its own, so the only\n * place left to catch it was a `console.warn` in dev, in the browser, with the\n * component mounted. As a union the same mistake is a build error at the call\n * site, for free, everywhere.\n */\nexport type DataTablePagingProps =\n | {\n /** Not server mode. */\n totalItems?: never;\n /** The table owns the page. */\n page?: never;\n /** Nothing to report to. */\n onPageChange?: never;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). Pair it with\n * `page` and `onPageChange`.\n */\n totalItems?: never;\n /** Current page, 1-based. Controlled — required in server mode. */\n page: number;\n /** Called with the next page. Required whenever `page` is controlled. */\n onPageChange: (page: number) => void;\n }\n | {\n /**\n * Total row count across every page — the `total` of a paginated envelope.\n *\n * Passing it switches the table to **server mode**: `data` is read as the\n * current page, the page count comes from this number instead of\n * `data.length`, and sorting and searching are delegated to the caller\n * (see `manualSort` / `manualSearch`, which are implied here). `page` and\n * `onPageChange` come with it — the type says so, because a server-mode\n * table without them silently shows page one forever.\n */\n totalItems: number;\n /** Current page, 1-based. Controlled, and required in server mode. */\n page: number;\n /** Called with the next page. */\n onPageChange: (page: number) => void;\n };\n\n/**\n * Sorting: delegated, and therefore reported, or neither.\n *\n * `manualSort` without `onSortChange` renders a header that moves its arrow and\n * changes nothing else — the arrow is a lie the compiler can now catch.\n */\nexport type DataTableSortProps<T> =\n | {\n /** The table sorts the rows it has. */\n manualSort?: false;\n /** Called with the next sort state — `null` when the header cycles back to unsorted. */\n onSortChange?: (sort: DataTableSort<T> | null) => void;\n }\n | {\n /**\n * Sorting is the caller's job: clicking a header reports through\n * `onSortChange` and the rows are left in the order they arrived.\n *\n * Implied by `totalItems`, because sorting the page in memory would sort\n * *that page only* while the header claims the whole table is ordered.\n */\n manualSort: true;\n /** Where the click goes. Required, since nothing else acts on it. */\n onSortChange: (sort: DataTableSort<T> | null) => void;\n };\n\n/**\n * Searching: delegated, and therefore reported, or neither.\n *\n * `manualSearch` without `onSearchChange` renders a search box that filters\n * nothing and tells nobody — the same shape of lie as a header arrow that turns\n * without sorting.\n *\n * Independent of the paging axis on purpose, and that leaves one gap this type\n * does not close: `totalItems` *implies* `manualSearch`, so a server-mode table\n * with `searchable` and no `onSearchChange` falls into the same hole without ever\n * writing `manualSearch`. Closing it means the search axis has to read the paging\n * axis, which crosses two three-member unions into nine and turns every mismatch\n * into a wall of candidate shapes. That case is a dev warning instead — the one\n * spot where runtime really is the cheaper check, and `use-dev-warnings.ts` says\n * so at the call site.\n */\nexport type DataTableSearchProps =\n | {\n /** The table filters the rows it has. */\n manualSearch?: false;\n /** Called with the current search term (debouncing, if any, is the caller's). */\n onSearchChange?: (term: string) => void;\n }\n | {\n /**\n * Searching is the caller's job: typing reports through `onSearchChange`\n * and the rows are left as they arrived.\n *\n * Implied by `totalItems`. Filtering the current page would hide the rows\n * that do not match *on this page* and show nothing for a term that only\n * matches on page three — an empty table that looks like \"no results\".\n */\n manualSearch: true;\n /** Where the typing goes. Required, since nothing else acts on it. */\n onSearchChange: (term: string) => void;\n };\n\n/**\n * The table's props: the shared half, plus one valid paging shape and one valid\n * sorting shape.\n */\nexport type DataTableProps<T> = DataTableBaseProps<T> &\n DataTablePagingProps &\n DataTableSortProps<T> &\n DataTableSearchProps;\n\n/** Identity of one cell, stable across re-renders and pagination. */\nfunction cellId(rowKeyValue: string | number, columnKey: PropertyKey): string {\n return `${String(rowKeyValue)}::${String(columnKey)}`;\n}\n\nfunction headerText<T>(column: DataTableColumn<T>): string {\n return typeof column.header === \"string\" ? column.header : String(column.key);\n}\n\n/**\n * Stateful, headless data table built on top of {@link Table}. Adds\n * client-side searching, click-to-sort columns, pagination and opt-in inline\n * editing while delegating all table markup to the underlying Table component.\n *\n * - Clicking a sortable header cycles asc → desc → unsorted.\n * - Search matches a case-insensitive substring across `searchKeys`\n * (or every string/number column when not provided).\n * - Pagination is hidden when the result fits on a single page.\n * - A column with `editable` renders a button that opens an inline editor;\n * `Enter` commits, `Escape` discards, `Tab` walks to the next editable cell.\n *\n * Editing is strictly opt-in: with no `editable` column (or no `onCellChange`) the\n * rendered markup is byte-for-byte what it was before the feature existed, which\n * matters because the component is published.\n *\n * ## Optimistic, with a visible rollback\n *\n * An accepted edit is shown immediately and `onCellChange` runs in the background.\n * If it rejects, the cell returns to the old value **and** shows the reason as a\n * `role=\"alert\"` tied to the cell. A silent revert is worse than no optimistic\n * update at all: the user watched their edit appear and has no reason to doubt it.\n *\n * The header memo depends on `columns`, `sort` and the editing state only:\n * `toggleSort` and the commit callbacks are recreated each render but always close\n * over the same setters, so including them would rebuild every header on every\n * render without changing behaviour. That is why `exhaustive-deps` is silenced on\n * that dependency array.\n */\nexport function DataTable<T>({\n data,\n columns,\n pageSize = 10,\n searchable = false,\n searchKeys,\n initialSort,\n rowKey = (_row, index) => index,\n emptyMessage,\n onCellChange,\n editLabels,\n totalItems,\n page: controlledPage,\n onPageChange,\n manualSort,\n onSortChange,\n manualSearch,\n onSearchChange,\n loading = false,\n className,\n ...rest\n}: DataTableProps<T>) {\n const [search, setSearch] = useState<string>(\"\");\n const [sort, setSort] = useState<DataTableSort<T> | null>(initialSort ?? null);\n const { page: internalPage, setPage: setInternalPage } = usePagination(1, pageSize);\n const announce = useAnnounce();\n\n const serverMode = totalItems !== undefined;\n const sortIsManual = manualSort ?? serverMode;\n const searchIsManual = manualSearch ?? serverMode;\n const page = controlledPage ?? internalPage;\n\n const setPage = useCallback(\n (next: number) => {\n if (controlledPage === undefined) setInternalPage(next);\n onPageChange?.(next);\n },\n [controlledPage, setInternalPage, onPageChange],\n );\n\n useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n hasSortableColumn: columns.some((column) => column.sortable),\n onSortChange,\n searchable,\n onSearchChange,\n });\n\n const [editing, setEditing] = useState<string | null>(null);\n const [refocus, setRefocus] = useState<string | null>(null);\n const [overrides, setOverrides] = useState<Record<string, unknown>>({});\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [saving, setSaving] = useState<Record<string, boolean>>({});\n\n const labels = useMemo<DataTableEditLabels>(\n () => ({ ...DEFAULT_EDIT_LABELS, ...editLabels }),\n [editLabels],\n );\n const editingEnabled = onCellChange !== undefined && columns.some((column) => column.editable);\n\n const effectiveSearchKeys = useMemo<(keyof T)[]>(() => {\n if (!searchable || searchIsManual) return [];\n if (searchKeys && searchKeys.length > 0) return searchKeys;\n return columns\n .filter((column) => {\n const sample = data.find((row) => row[column.key] != null);\n const value = sample ? sample[column.key] : undefined;\n return typeof value === \"string\" || typeof value === \"number\";\n })\n .map((column) => column.key);\n }, [searchable, searchIsManual, searchKeys, columns, data]);\n\n const filtered = useMemo<T[]>(() => {\n const term = search.trim().toLowerCase();\n if (!term || !searchable || searchIsManual) return data;\n return data.filter((row) =>\n effectiveSearchKeys.some((key) => {\n const value = row[key];\n return value != null && String(value).toLowerCase().includes(term);\n }),\n );\n }, [data, search, searchable, searchIsManual, effectiveSearchKeys]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort || sortIsManual) return filtered;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...filtered].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [filtered, sort, sortIsManual]);\n\n const rowCount = totalItems ?? sorted.length;\n const totalPages = Math.max(1, Math.ceil(rowCount / pageSize));\n\n /**\n * Clamp the current page when the dataset shrinks (e.g. after filtering).\n *\n * Skipped in server mode: `page` belongs to the caller there, and a clamp\n * fired against a `totalItems` that has not caught up with the new filter\n * yet would send them a page they did not ask for, mid-fetch.\n */\n useEffect(() => {\n if (!serverMode && page > totalPages) setPage(totalPages);\n }, [serverMode, page, totalPages, setPage]);\n\n const safePage = serverMode ? page : Math.min(page, totalPages);\n const pageRows = useMemo<T[]>(() => {\n if (serverMode) return sorted;\n const start = (safePage - 1) * pageSize;\n return sorted.slice(start, start + pageSize);\n }, [serverMode, sorted, safePage, pageSize]);\n\n function toggleSort(key: keyof T): void {\n const current = sort;\n const next: DataTableSort<T> | null =\n !current || current.key !== key\n ? { key, direction: \"asc\" }\n : current.direction === \"asc\"\n ? { key, direction: \"desc\" }\n : null;\n setSort(next);\n onSortChange?.(next);\n }\n\n const absoluteIndex = useCallback(\n (pageIndex: number) => (safePage - 1) * pageSize + pageIndex,\n [safePage, pageSize],\n );\n\n /**\n * Every editable cell on the page, row-major — the order `Tab` walks.\n *\n * Row-major and not column-major because a row is the record a user is\n * correcting; walking down a column would make them re-find their place on\n * every keystroke.\n */\n const editableCellIds = useMemo<string[]>(() => {\n if (!editingEnabled) return [];\n const ids: string[] = [];\n pageRows.forEach((row, index) => {\n const key = rowKey(row, absoluteIndex(index));\n for (const column of columns) {\n if (column.editable) ids.push(cellId(key, column.key));\n }\n });\n return ids;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editingEnabled, pageRows, columns, absoluteIndex]);\n\n const displayed = useCallback(\n (row: T, column: DataTableColumn<T>, id: string): unknown =>\n id in overrides ? overrides[id] : row[column.key],\n [overrides],\n );\n\n /**\n * Run the caller's `onCellChange` behind the optimistic update.\n *\n * Only the *success* is announced. The failure already renders as a\n * `role=\"alert\"` inside the cell, which screen readers read on insertion —\n * announcing it again from the shared region would read it twice and put the\n * same text in the document twice.\n */\n const persist = useCallback(\n async (id: string, column: DataTableColumn<T>, change: DataTableCellChange<T>) => {\n setSaving((current) => ({ ...current, [id]: true }));\n try {\n await onCellChange?.(change);\n announce(labels.saved(headerText(column)));\n } catch (error) {\n setOverrides((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n const message =\n error instanceof Error && error.message\n ? error.message\n : labels.saveFailed(headerText(column));\n setErrors((current) => ({ ...current, [id]: message }));\n } finally {\n setSaving((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }\n },\n [onCellChange, announce, labels],\n );\n\n const moveFrom = useCallback(\n (id: string, move: CellCommitMove): void => {\n if (move === \"none\") {\n setEditing(null);\n setRefocus(id);\n return;\n }\n const index = editableCellIds.indexOf(id);\n const target = editableCellIds[index + (move === \"next\" ? 1 : -1)];\n if (target === undefined) {\n setEditing(null);\n setRefocus(id);\n return;\n }\n setEditing(target);\n setRefocus(null);\n },\n [editableCellIds],\n );\n\n /**\n * Parse, validate, stage optimistically, then save in the background.\n *\n * Validation runs before anything is staged, and a rejection leaves the editor\n * open with the message attached to the input — the user has to be able to fix\n * what they typed without retyping it.\n */\n const commit = useCallback(\n (\n row: T,\n column: DataTableColumn<T>,\n id: string,\n pageIndex: number,\n raw: string,\n move: CellCommitMove,\n ): void => {\n const previous = displayed(row, column, id);\n const currentText = column.formatEdit ? column.formatEdit(row) : String(previous ?? \"\");\n if (raw === currentText) {\n moveFrom(id, move);\n return;\n }\n\n const value = column.parse\n ? column.parse(raw, row)\n : column.editorType === \"number\"\n ? Number(raw)\n : raw.trim();\n\n const invalid = column.validate?.(value, row) ?? null;\n if (invalid) {\n setErrors((current) => ({ ...current, [id]: invalid }));\n return;\n }\n\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n setOverrides((current) => ({ ...current, [id]: value }));\n moveFrom(id, move);\n void persist(id, column, {\n row,\n key: column.key,\n value,\n previous,\n rowIndex: absoluteIndex(pageIndex),\n });\n },\n [displayed, moveFrom, persist, absoluteIndex],\n );\n\n const tableColumns = useMemo<TableColumn<T>[]>(\n () =>\n columns.map((column) => {\n const isSorted = sort?.key === column.key;\n const indicator = isSorted ? (sort?.direction === \"asc\" ? \" ▲\" : \" ▼\") : \"\";\n const header = column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n aria-label={`Ordenar por ${headerText(column)}`}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {indicator}\n </span>\n </button>\n ) : (\n column.header\n );\n\n const plainCell = (row: T): ReactNode => {\n if (column.render) return column.render(row);\n return (row[column.key] as ReactNode) ?? null;\n };\n\n /**\n * Render an editable cell's content against the optimistic value.\n *\n * The row is shallow-patched rather than the value passed alongside it,\n * because a column with a custom `render` (a `<Money>`, a badge) reads\n * the row — handing it the stale row would show the old number under a\n * cell the user just changed.\n */\n const patchedCell = (row: T, value: unknown): ReactNode => {\n const patched = { ...row, [column.key]: value } as T;\n if (column.render) return column.render(patched);\n return (patched[column.key] as ReactNode) ?? null;\n };\n\n const editable = editingEnabled && column.editable === true;\n\n return {\n key: String(column.key),\n header,\n align: column.align,\n priority: column.priority,\n width: column.width,\n render: editable\n ? (row: T, index: number) => {\n const id = cellId(rowKey(row, absoluteIndex(index)), column.key);\n const value = displayed(row, column, id);\n const text = column.formatEdit\n ? column.formatEdit(row)\n : String(value ?? \"\");\n return (\n <EditableCell\n text={text}\n columnLabel={headerText(column)}\n rowNumber={index + 1}\n inputType={column.editorType ?? \"text\"}\n editing={editing === id}\n refocus={refocus === id}\n saving={saving[id] === true}\n error={errors[id] ?? null}\n errorId={`tempest-cell-error-${id.replace(/[^\\w-]/g, \"_\")}`}\n labels={labels}\n onOpen={() => {\n setEditing(id);\n setRefocus(null);\n }}\n onCommit={(raw, move) =>\n commit(row, column, id, index, raw, move)\n }\n onCancel={() => {\n setEditing(null);\n setRefocus(id);\n setErrors((current) => {\n const next = { ...current };\n delete next[id];\n return next;\n });\n }}\n >\n {patchedCell(row, value)}\n </EditableCell>\n );\n }\n : plainCell,\n };\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n columns,\n sort,\n editingEnabled,\n editing,\n refocus,\n saving,\n errors,\n labels,\n overrides,\n absoluteIndex,\n commit,\n ],\n );\n\n const showSkeleton = loading && pageRows.length === 0;\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n {searchable && (\n <SearchBar\n value={search}\n onChange={(value) => {\n setSearch(value);\n setPage(1);\n onSearchChange?.(value);\n }}\n wrapperClassName={styles.search}\n />\n )}\n <div\n className={cn(loading && !showSkeleton && styles.pending)}\n aria-busy={loading || undefined}\n data-testid=\"tempest-datatable-body\"\n >\n {showSkeleton ? (\n <LoadingRows columns={tableColumns.length} rows={Math.min(pageSize, 8)} />\n ) : (\n <Table\n columns={tableColumns}\n data={pageRows}\n rowKey={(row, index) => rowKey(row, absoluteIndex(index))}\n emptyMessage={emptyMessage}\n />\n )}\n </div>\n {totalPages > 1 && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n onPageChange={setPage}\n totalItems={rowCount}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/dev-mode.cjs");let t=require("react");function n({serverMode:n,controlledPage:r,onPageChange:i,sortIsManual:a,onSortChange:o,searchable:s,onSearchChange:c}){(0,t.useEffect)(()=>{e.isDevBuild()&&(n&&r===void 0&&console.warn("[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. Without it the pager moves the internal page while `data` keeps showing page 1."),r!==void 0&&!i&&console.warn("[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything."),n&&s&&!c&&console.warn("[tempest] <DataTable totalItems searchable> delegates searching, so the box needs `onSearchChange`. Without it the user types and nothing filters and nothing is reported."),a&&!o&&console.warn("[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else."))},[n,r,i,a,o,s,c])}exports.useDevWarnings=n;
1
+ const e=require("../../utils/dev-mode.cjs");let t=require("react");function n({serverMode:n,controlledPage:r,onPageChange:i,sortIsManual:a,hasSortableColumn:o,onSortChange:s,searchable:c,onSearchChange:l}){(0,t.useEffect)(()=>{e.isDevBuild()&&(n&&r===void 0&&console.warn("[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. Without it the pager moves the internal page while `data` keeps showing page 1."),r!==void 0&&!i&&console.warn("[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything."),n&&c&&!l&&console.warn("[tempest] <DataTable totalItems searchable> delegates searching, so the box needs `onSearchChange`. Without it the user types and nothing filters and nothing is reported."),a&&o&&!s&&console.warn("[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else."))},[n,r,i,a,o,s,c,l])}exports.useDevWarnings=n;
2
2
  //# sourceMappingURL=use-dev-warnings.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-dev-warnings.cjs","names":[],"sources":["../../../src/components/DataTable/use-dev-warnings.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { isDevBuild } from \"../../utils/dev-mode\";\n\n/** What {@link useDevWarnings} needs to judge the prop combination. */\nexport interface DataTableDevWarningsInput {\n /** Whether `totalItems` was passed. */\n serverMode: boolean;\n /** The controlled `page`, if any. */\n controlledPage: number | undefined;\n /** The page callback, if any. */\n onPageChange: ((page: number) => void) | undefined;\n /** Whether sorting is delegated. */\n sortIsManual: boolean;\n /** The sort callback, if any. */\n onSortChange: ((sort: never) => void) | undefined;\n /** Whether a search box is rendered at all. */\n searchable: boolean;\n /** The search callback, if any. */\n onSearchChange: ((term: string) => void) | undefined;\n}\n\n/**\n * Warn, in development only, about prop combinations that render a table which\n * silently lies.\n *\n * Each of these produces a screen that looks like it works: the header sorts and\n * nothing moves, the pager renders and clicking it does nothing.\n *\n * Three of them **are** type errors now — `DataTablePagingProps`,\n * `DataTableSortProps` and `DataTableSearchProps` reject them at the call site —\n * so those warnings only reach a caller the types cannot: plain JavaScript, or\n * props arriving through an `any`-typed spread.\n *\n * The fourth is the one the types genuinely cannot afford. `totalItems` *implies*\n * `manualSearch`, so `searchable` with no `onSearchChange` is an inert search box\n * in server mode without anybody writing `manualSearch`. Expressing that means\n * the search axis has to read the paging axis, crossing two three-member unions\n * into nine — so it stays here, which is what \"runtime is the cheaper check\"\n * actually looks like when it is true.\n *\n * @param input - The resolved prop combination.\n */\nexport function useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n onSortChange,\n searchable,\n onSearchChange,\n}: DataTableDevWarningsInput): void {\n useEffect(() => {\n if (!isDevBuild()) return;\n\n if (serverMode && controlledPage === undefined) {\n console.warn(\n \"[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. \" +\n \"Without it the pager moves the internal page while `data` keeps showing page 1.\",\n );\n }\n if (controlledPage !== undefined && !onPageChange) {\n console.warn(\n \"[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything.\",\n );\n }\n if (serverMode && searchable && !onSearchChange) {\n console.warn(\n \"[tempest] <DataTable totalItems searchable> delegates searching, so the box needs \" +\n \"`onSearchChange`. Without it the user types and nothing filters and nothing is reported.\",\n );\n }\n if (sortIsManual && !onSortChange) {\n console.warn(\n \"[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else.\",\n );\n }\n }, [\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n onSortChange,\n searchable,\n onSearchChange,\n ]);\n}\n"],"mappings":"mEA2CA,SAAgB,EAAe,CAC3B,aACA,iBACA,eACA,eACA,eACA,aACA,kBACgC,EAChC,EAAA,EAAA,UAAA,KAAgB,CACP,EAAA,WAAW,IAEZ,GAAc,IAAmB,IAAA,IACjC,QAAQ,KACJ,mKAEJ,EAEA,IAAmB,IAAA,IAAa,CAAC,GACjC,QAAQ,KACJ,0GACJ,EAEA,GAAc,GAAc,CAAC,GAC7B,QAAQ,KACJ,4KAEJ,EAEA,GAAgB,CAAC,GACjB,QAAQ,KACJ,yIACJ,EAER,EAAG,CACC,EACA,EACA,EACA,EACA,EACA,EACA,CACJ,CAAC,CACL"}
1
+ {"version":3,"file":"use-dev-warnings.cjs","names":[],"sources":["../../../src/components/DataTable/use-dev-warnings.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\nimport { isDevBuild } from \"../../utils/dev-mode\";\n\n/** What {@link useDevWarnings} needs to judge the prop combination. */\nexport interface DataTableDevWarningsInput {\n /** Whether `totalItems` was passed. */\n serverMode: boolean;\n /** The controlled `page`, if any. */\n controlledPage: number | undefined;\n /** The page callback, if any. */\n onPageChange: ((page: number) => void) | undefined;\n /** Whether sorting is delegated. */\n sortIsManual: boolean;\n /** Whether any column renders a header the user can click to sort. */\n hasSortableColumn: boolean;\n /** The sort callback, if any. */\n onSortChange: ((sort: never) => void) | undefined;\n /** Whether a search box is rendered at all. */\n searchable: boolean;\n /** The search callback, if any. */\n onSearchChange: ((term: string) => void) | undefined;\n}\n\n/**\n * Warn, in development only, about prop combinations that render a table which\n * silently lies.\n *\n * Each of these produces a screen that looks like it works: the header sorts and\n * nothing moves, the pager renders and clicking it does nothing.\n *\n * Three of them **are** type errors now — `DataTablePagingProps`,\n * `DataTableSortProps` and `DataTableSearchProps` reject them at the call site —\n * so those warnings only reach a caller the types cannot: plain JavaScript, or\n * props arriving through an `any`-typed spread.\n *\n * Each one also has to be reachable. `totalItems` implies `manualSort`, so the\n * sort warning fired on every server-mode table — including one whose columns are\n * all plain, where no header is clickable and the sentence it prints (\"clicking a\n * sortable header changes the arrow and nothing else\") describes a state the user\n * cannot reach. A warning nobody can act on is not a warning: it teaches the\n * reader to skim past the two next to it, which *are* real, and the way out it\n * suggests is `onSortChange={() => {}}` — a callback that does nothing, that the\n * type-checker accepts, and that would hide the warning on the day a `sortable`\n * column does appear. So the sort warning asks for a sortable column first, the\n * way the search one already asks for `searchable`.\n *\n * The fourth is the one the types genuinely cannot afford. `totalItems` *implies*\n * `manualSearch`, so `searchable` with no `onSearchChange` is an inert search box\n * in server mode without anybody writing `manualSearch`. Expressing that means\n * the search axis has to read the paging axis, crossing two three-member unions\n * into nine — so it stays here, which is what \"runtime is the cheaper check\"\n * actually looks like when it is true.\n *\n * @param input - The resolved prop combination.\n */\nexport function useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n hasSortableColumn,\n onSortChange,\n searchable,\n onSearchChange,\n}: DataTableDevWarningsInput): void {\n useEffect(() => {\n if (!isDevBuild()) return;\n\n if (serverMode && controlledPage === undefined) {\n console.warn(\n \"[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. \" +\n \"Without it the pager moves the internal page while `data` keeps showing page 1.\",\n );\n }\n if (controlledPage !== undefined && !onPageChange) {\n console.warn(\n \"[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything.\",\n );\n }\n if (serverMode && searchable && !onSearchChange) {\n console.warn(\n \"[tempest] <DataTable totalItems searchable> delegates searching, so the box needs \" +\n \"`onSearchChange`. Without it the user types and nothing filters and nothing is reported.\",\n );\n }\n if (sortIsManual && hasSortableColumn && !onSortChange) {\n console.warn(\n \"[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else.\",\n );\n }\n }, [\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n hasSortableColumn,\n onSortChange,\n searchable,\n onSearchChange,\n ]);\n}\n"],"mappings":"mEAwDA,SAAgB,EAAe,CAC3B,aACA,iBACA,eACA,eACA,oBACA,eACA,aACA,kBACgC,EAChC,EAAA,EAAA,UAAA,KAAgB,CACP,EAAA,WAAW,IAEZ,GAAc,IAAmB,IAAA,IACjC,QAAQ,KACJ,mKAEJ,EAEA,IAAmB,IAAA,IAAa,CAAC,GACjC,QAAQ,KACJ,0GACJ,EAEA,GAAc,GAAc,CAAC,GAC7B,QAAQ,KACJ,4KAEJ,EAEA,GAAgB,GAAqB,CAAC,GACtC,QAAQ,KACJ,yIACJ,EAER,EAAG,CACC,EACA,EACA,EACA,EACA,EACA,EACA,EACA,CACJ,CAAC,CACL"}