tempest-react-sdk 0.79.0 → 0.81.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 (131) hide show
  1. package/README.md +35 -4
  2. package/bin/lib/component/generate.test.mjs +13 -1
  3. package/bin/lib/openapi/generate.hostile.test.mjs +58 -5
  4. package/bin/lib/openapi/generate.mjs +145 -34
  5. package/bin/lib/openapi/generate.test.mjs +216 -5
  6. package/bin/lib/openapi/schema-to-zod.mjs +4 -3
  7. package/dist/auth/create-tempest-auth.cjs +1 -1
  8. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  9. package/dist/auth/create-tempest-auth.js +172 -103
  10. package/dist/auth/create-tempest-auth.js.map +1 -1
  11. package/dist/br/boleto.cjs +1 -1
  12. package/dist/br/boleto.cjs.map +1 -1
  13. package/dist/br/boleto.js +2 -1
  14. package/dist/br/boleto.js.map +1 -1
  15. package/dist/br.d.ts +22 -3
  16. package/dist/components/AuthForm/AuthFieldControl.cjs +2 -0
  17. package/dist/components/AuthForm/AuthFieldControl.cjs.map +1 -0
  18. package/dist/components/AuthForm/AuthFieldControl.js +85 -0
  19. package/dist/components/AuthForm/AuthFieldControl.js.map +1 -0
  20. package/dist/components/AuthForm/AuthForm.cjs +2 -0
  21. package/dist/components/AuthForm/AuthForm.cjs.map +1 -0
  22. package/dist/components/AuthForm/AuthForm.js +83 -0
  23. package/dist/components/AuthForm/AuthForm.js.map +1 -0
  24. package/dist/components/AuthForm/AuthForm.module.cjs +3 -0
  25. package/dist/components/AuthForm/AuthForm.module.cjs.map +1 -0
  26. package/dist/components/AuthForm/AuthForm.module.js +14 -0
  27. package/dist/components/AuthForm/AuthForm.module.js.map +1 -0
  28. package/dist/components/AuthForm/AuthLinks.cjs +2 -0
  29. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -0
  30. package/dist/components/AuthForm/AuthLinks.js +29 -0
  31. package/dist/components/AuthForm/AuthLinks.js.map +1 -0
  32. package/dist/components/AuthForm/ForgotPasswordForm.cjs +2 -0
  33. package/dist/components/AuthForm/ForgotPasswordForm.cjs.map +1 -0
  34. package/dist/components/AuthForm/ForgotPasswordForm.js +32 -0
  35. package/dist/components/AuthForm/ForgotPasswordForm.js.map +1 -0
  36. package/dist/components/AuthForm/LoginForm.cjs +2 -0
  37. package/dist/components/AuthForm/LoginForm.cjs.map +1 -0
  38. package/dist/components/AuthForm/LoginForm.js +39 -0
  39. package/dist/components/AuthForm/LoginForm.js.map +1 -0
  40. package/dist/components/AuthForm/ResetPasswordForm.cjs +2 -0
  41. package/dist/components/AuthForm/ResetPasswordForm.cjs.map +1 -0
  42. package/dist/components/AuthForm/ResetPasswordForm.js +39 -0
  43. package/dist/components/AuthForm/ResetPasswordForm.js.map +1 -0
  44. package/dist/components/AuthForm/SignUpForm.cjs +2 -0
  45. package/dist/components/AuthForm/SignUpForm.cjs.map +1 -0
  46. package/dist/components/AuthForm/SignUpForm.js +32 -0
  47. package/dist/components/AuthForm/SignUpForm.js.map +1 -0
  48. package/dist/components/AuthForm/auth-defaults.cjs +2 -0
  49. package/dist/components/AuthForm/auth-defaults.cjs.map +1 -0
  50. package/dist/components/AuthForm/auth-defaults.js +54 -0
  51. package/dist/components/AuthForm/auth-defaults.js.map +1 -0
  52. package/dist/components/AuthForm/auth-schemas.cjs +2 -0
  53. package/dist/components/AuthForm/auth-schemas.cjs.map +1 -0
  54. package/dist/components/AuthForm/auth-schemas.js +26 -0
  55. package/dist/components/AuthForm/auth-schemas.js.map +1 -0
  56. package/dist/components/AuthPage/AuthPage.cjs +2 -0
  57. package/dist/components/AuthPage/AuthPage.cjs.map +1 -0
  58. package/dist/components/AuthPage/AuthPage.js +91 -0
  59. package/dist/components/AuthPage/AuthPage.js.map +1 -0
  60. package/dist/components/AuthPage/AuthPage.module.cjs +3 -0
  61. package/dist/components/AuthPage/AuthPage.module.cjs.map +1 -0
  62. package/dist/components/AuthPage/AuthPage.module.js +24 -0
  63. package/dist/components/AuthPage/AuthPage.module.js.map +1 -0
  64. package/dist/components/FilterBar/filter-apply.cjs +1 -1
  65. package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
  66. package/dist/components/FilterBar/filter-apply.js +7 -7
  67. package/dist/forms/index.js +7 -7
  68. package/dist/forms/masked-inputs.cjs +1 -1
  69. package/dist/forms/masked-inputs.cjs.map +1 -1
  70. package/dist/forms/masked-inputs.js +15 -15
  71. package/dist/http/api-client.cjs +1 -1
  72. package/dist/http/api-client.cjs.map +1 -1
  73. package/dist/http/api-client.js +104 -97
  74. package/dist/http/api-client.js.map +1 -1
  75. package/dist/http/build-url.cjs +1 -1
  76. package/dist/http/build-url.cjs.map +1 -1
  77. package/dist/http/build-url.js +3 -1
  78. package/dist/http/build-url.js.map +1 -1
  79. package/dist/http/presigned-post.cjs +2 -0
  80. package/dist/http/presigned-post.cjs.map +1 -0
  81. package/dist/http/presigned-post.js +73 -0
  82. package/dist/http/presigned-post.js.map +1 -0
  83. package/dist/http/resumable-upload.cjs +1 -1
  84. package/dist/http/resumable-upload.cjs.map +1 -1
  85. package/dist/http/resumable-upload.js +97 -97
  86. package/dist/http/resumable-upload.js.map +1 -1
  87. package/dist/http/upload-with-progress.cjs +1 -1
  88. package/dist/http/upload-with-progress.cjs.map +1 -1
  89. package/dist/http/upload-with-progress.js +12 -12
  90. package/dist/http/use-describe-api-error.cjs +1 -1
  91. package/dist/http/use-describe-api-error.cjs.map +1 -1
  92. package/dist/http/use-describe-api-error.js +9 -9
  93. package/dist/offline/create-offline-sync.cjs +1 -1
  94. package/dist/offline/create-offline-sync.cjs.map +1 -1
  95. package/dist/offline/create-offline-sync.js +78 -56
  96. package/dist/offline/create-offline-sync.js.map +1 -1
  97. package/dist/sse/create-event-stream.cjs +1 -1
  98. package/dist/sse/create-event-stream.cjs.map +1 -1
  99. package/dist/sse/create-event-stream.js +44 -30
  100. package/dist/sse/create-event-stream.js.map +1 -1
  101. package/dist/sse/ticket-event-stream.cjs +2 -0
  102. package/dist/sse/ticket-event-stream.cjs.map +1 -0
  103. package/dist/sse/ticket-event-stream.js +41 -0
  104. package/dist/sse/ticket-event-stream.js.map +1 -0
  105. package/dist/sse/use-event-stream.cjs +1 -1
  106. package/dist/sse/use-event-stream.cjs.map +1 -1
  107. package/dist/sse/use-event-stream.js +7 -4
  108. package/dist/sse/use-event-stream.js.map +1 -1
  109. package/dist/styles/AuthForm.css +2 -0
  110. package/dist/styles/AuthPage.css +2 -0
  111. package/dist/styles/component/AuthForm.css +9 -0
  112. package/dist/styles/component/AuthPage.css +29 -0
  113. package/dist/styles/forms.css +36 -0
  114. package/dist/styles/manifest.json +63 -0
  115. package/dist/styles.css +1 -1
  116. package/dist/sw/background-sync.cjs +1 -1
  117. package/dist/sw/background-sync.cjs.map +1 -1
  118. package/dist/sw/background-sync.js +124 -106
  119. package/dist/sw/background-sync.js.map +1 -1
  120. package/dist/sw.d.ts +49 -15
  121. package/dist/tempest-react-sdk.cjs +1 -1
  122. package/dist/tempest-react-sdk.d.ts +809 -22
  123. package/dist/tempest-react-sdk.js +373 -363
  124. package/dist/utils/format.cjs +1 -1
  125. package/dist/utils/format.cjs.map +1 -1
  126. package/dist/utils/format.js +25 -25
  127. package/dist/utils/numbers.cjs +1 -1
  128. package/dist/utils/numbers.cjs.map +1 -1
  129. package/dist/utils/numbers.js +7 -7
  130. package/package.json +1 -1
  131. package/template-pwa/src/sw.ts +10 -6
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # tempest-react-sdk
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/tempest-react-sdk.svg)](https://www.npmjs.com/package/tempest-react-sdk)
4
- [![CI](https://github.com/mauriciobenjamin700/tempest-react-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mauriciobenjamin700/tempest-react-sdk/actions/workflows/ci.yml)
5
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
6
5
  [![React 18 / 19](https://img.shields.io/badge/react-18%20%7C%2019-61dafb.svg?logo=react)](https://react.dev)
7
6
  [![TypeScript](https://img.shields.io/badge/types-TypeScript-3178c6.svg?logo=typescript)](https://www.typescriptlang.org/)
@@ -43,6 +42,7 @@ The goal is to start every new React frontend with the same opinionated foundati
43
42
  - [Polling](#polling-recipe)
44
43
  - [Retry & idempotency](#retry--idempotency-recipe)
45
44
  - [Auth store (Zustand)](#auth-store-recipe)
45
+ - [Auth screens (login, sign-up, password reset)](#auth-screens-recipe)
46
46
  - [Route guard](#route-guard-recipe)
47
47
  - [JWT helpers & refresh queue](#jwt-helpers--refresh-queue-recipe)
48
48
  - [Code-splitting with retry](#code-splitting-with-retry-recipe)
@@ -267,7 +267,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
267
267
 
268
268
  | Module | Exports |
269
269
  | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
270
- | `components` | `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `InstallBanner`, `InstallButton`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
270
+ | `components` | `AuthPage` + `AuthForm` with the presets `LoginForm`, `SignUpForm`, `ForgotPasswordForm`, `ResetPasswordForm` (auth screens from a list of fields), `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `InstallBanner`, `InstallButton`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
271
271
  | `hooks` | `useDebounce`, `usePagination`, `useClientFilter`, `useDraftFilters` (draft vs applied server-side filter), `useMediaQuery`, `useOnline`, `useDocumentVisibility`, `useIntersectionObserver`, `useResizeObserver`, `useClipboard`, `useKeyboardShortcut`, `useBeforeInstallPrompt`, `useIdle`, `useGeolocation`, `useScrollLock`, `useFocusTrap`, `useStableCallback`, `useAnnounce` (+ pure `announce`/`clearAnnouncer` — one shared live-region pair), `useDeepMemo`, `useObjectUrl`, `useLongPressHandlers` (spreadable long-press handlers + `wasLongPress()` guard), `useInstallPrompt` (install-method resolver with decline cooldown), `useServiceWorkerUpdate` (consent-based SW update flow), `useStorageEstimate` + `estimateStorage`/`requestPersistentStorage` (Storage API quota & persistence), PWA env helpers `isIOS`/`isAndroid`/`isAndroidWithoutPromptApi`/`isStandalone`/`buildOpenInChromeIntent`, type `BeforeInstallPromptEvent` |
272
272
  | `http` | `createApiClient`, `buildApiUrl`, `csrfHeaders`, `parseResponse`, `uploadWithProgress`, `createResumableUpload` (tus 1.0.0: chunked + resumable, `pause`/`resume`/`abort`), `createLocalUploadStorage`, `uploadFingerprint`, `retry`, `generateIdempotencyKey`, `usePoll`, types: `ApiClient`, `ApiClientConfig`, `ApiError`, `BuildApiUrlOptions`, `CsrfOptions`, `RequestOptions`, `RetryOptions`, `UploadProgressEvent`, `UploadWithProgressOptions`, `UsePollOptions`, `UsePollResult` |
273
273
  | `auth` _(peer: `zustand`)_ | `createAuthStore`, `AuthGuard`, `decodeJWT`, `isJWTExpired`, `lazyWithRetry`, `createRefreshQueue`, **passkeys/WebAuthn**: `createPasskeyClient`, `usePasskeyRegistration`, `usePasskeySignIn`, `usePasskeyCapabilities`, `isPasskeySupported`, `isPlatformAuthenticatorAvailable`, `isConditionalMediationAvailable`, `classifyPasskeyError`, `PasskeyError`, `base64UrlToBytes`, `bytesToBase64Url`, types: `AuthState`, `CreateAuthStoreOptions`, `AuthGuardProps`, `DecodedJWT`, `LazyWithRetryOptions` |
@@ -284,7 +284,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
284
284
  | `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` |
285
285
  | `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` |
286
286
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
287
- | `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`, credentials never stored), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
287
+ | `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`, credentials never stored and re-applied only to the worker's origin or `trustedOrigins`), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
288
288
  | `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) |
289
289
  | `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 |
290
290
  | `neural` _(subpath `tempest-react-sdk/neural`, opt. peer `@sapphi-red/web-noise-suppressor`)_ | `isNeuralSuppressionSupported`, `prepareNeuralSuppressor`, `createNeuralSuppressor`, types `NeuralSuppressorAssets`, `NeuralSuppressor` — RNNoise noise suppression: a recurrent model in an `AudioWorklet` separates speech from non-speech on a 48 kHz context and hands back a denoised `MediaStreamTrack` to publish in place of the capture (peer externalized; install it only if you use neural) |
@@ -321,7 +321,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
321
321
  | `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` |
322
322
  | `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 |
323
323
  | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
324
- | `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`, credentials never stored), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
324
+ | `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`, credentials never stored and re-applied only to the worker's origin or `trustedOrigins`), `provideBackgroundSyncCredentials`, `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability), `clearSessionData`/`clearBackgroundSyncQueue` (logout cleanup) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
325
325
  | `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) |
326
326
  | `vision` _(subpath `tempest-react-sdk/vision`, opt. peer `onnxruntime-web`)_ | `Classifier`, `Detector`, `Segmenter`, `DetectClassify` (fused detect→classify pipeline, optionally with a segmentation stage) + `warmup()`, FP16 models, `cache` (Cache Storage), video/`VideoFrame` inputs, `raiseOnEmpty`/`NoDetectionsError`, `LetterboxPipeline`, `readFusionSpec` and results/labels/preprocess — on-device ONNX vision inference, vendored from `@mauriciobenjamin700/ort-vision-sdk-web` (onnxruntime-web externalized; install it only if you use vision). Plus browser capture primitives: `useCameraStream` (rear-camera `getUserMedia` + classified errors + `retry()`), `computeImageLuminance`/`isLuminanceAcceptable`/`LowLuminanceError`/`LUMINANCE_SAMPLE_MAX_EDGE` (BT.709 brightness), `useLiveLuminance` (live rAF sampling) |
327
327
  | `tabular` _(subpath `tempest-react-sdk/tabular`, opt. peer `onnxruntime-web`)_ | `TabularPredictor`, `useTabularPredictor` — scikit-learn models exported by `tempest-fastapi-sdk`'s `export_sklearn_to_onnx`, running in the browser with no inference server. Offline by design: `fetchModelBytes`/`isModelCached`/`cacheModelBytes`/`clearModelCache` (Cache Storage, cache-first) plus `configureOrtAssets`/`ortAssetUrls`/`ORT_WASM_ASSETS` for the runtime's WebAssembly binaries, which ONNX Runtime Web fetches at session creation. Import the default `onnxruntime-web` entry — the WebGPU build has no `ai.onnx.ml` kernels |
@@ -778,6 +778,37 @@ useAuthStore.getState().logout();
778
778
 
779
779
  The store exposes `user`, `token`, `isAuthenticated`, `setSession`, `setUser`, `setToken`, and `logout`. `isAuthenticated` is derived from `token` and rehydrates correctly after page reload.
780
780
 
781
+ ### Auth screens recipe
782
+
783
+ The sign-in screen without hand-building the layout or the form: `AuthPage` is the shell (colored top + rounded sheet below 768 px, 50/50 brand panel and form above it, or a centered `card`), and `LoginForm` / `SignUpForm` / `ForgotPasswordForm` / `ResetPasswordForm` are generated from a list of fields and a zod schema. A 422 with `fields` lands on the matching inputs; any other error shows an `Alert`.
784
+
785
+ ```tsx
786
+ import { AuthPage, LoginForm, createApiClient, useNavigate } from "tempest-react-sdk";
787
+
788
+ const api = createApiClient({ baseURL: "/api" });
789
+
790
+ export function LoginScreen() {
791
+ const navigate = useNavigate();
792
+ return (
793
+ <AuthPage
794
+ brand={<img src="/logo.svg" alt="Acme" width={96} height={96} />}
795
+ back="/"
796
+ title="Bem-vindo!"
797
+ >
798
+ <LoginForm
799
+ onSubmit={async (values) => {
800
+ await api.post("/auth/login", { body: values });
801
+ navigate("/", { replace: true });
802
+ }}
803
+ links={{ forgotPassword: "/recuperar-senha", signUp: "/cadastro" }}
804
+ />
805
+ </AuthPage>
806
+ );
807
+ }
808
+ ```
809
+
810
+ Full guide: [Auth screens](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/components/auth-screens/) · [Telas de autenticação](https://mauriciobenjamin700.github.io/tempest-react-sdk/components/auth-screens/).
811
+
781
812
  ### Route guard recipe
782
813
 
783
814
  ```tsx
@@ -15,10 +15,22 @@ import { fileURLToPath } from "node:url";
15
15
 
16
16
  import { ESLint } from "eslint";
17
17
  import ts from "typescript";
18
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
19
 
20
20
  import { componentFiles, normalizeComponentName } from "./generate.mjs";
21
21
 
22
+ /**
23
+ * Gives the tests in this file 60 s instead of Vitest's 5 s default.
24
+ *
25
+ * Several of them build a TypeScript program (and, some, an ESLint instance)
26
+ * in-process over the generated files. That takes 3-5 s on an idle machine,
27
+ * right at the default, so a busy one — another suite, a model training — failed
28
+ * them by timeout while the code was correct: 5.1-5.7 s measured in three
29
+ * `make check` runs on 2026-11-01, load average between 21 and 48.
30
+ * `test/docs-guard.test.ts` gives its compile 120 s for the same reason.
31
+ */
32
+ vi.setConfig({ testTimeout: 60_000 });
33
+
22
34
  const HERE = dirname(fileURLToPath(import.meta.url));
23
35
  const REPO = resolve(HERE, "..", "..", "..");
24
36
  const CLI = join(REPO, "bin", "tempest.mjs");
@@ -4,11 +4,23 @@ import { fileURLToPath } from "node:url";
4
4
  import vm from "node:vm";
5
5
 
6
6
  import ts from "typescript";
7
- import { describe, expect, it } from "vitest";
7
+ import { describe, expect, it, vi } from "vitest";
8
8
 
9
9
  import { encodeIdSegment } from "../../../src/data";
10
10
  import { generate } from "./generate.mjs";
11
11
 
12
+ /**
13
+ * Gives the tests in this file 60 s instead of Vitest's 5 s default.
14
+ *
15
+ * Several of them build a TypeScript program (and, some, an ESLint instance)
16
+ * in-process over the generated files. That takes 3-5 s on an idle machine,
17
+ * right at the default, so a busy one — another suite, a model training — failed
18
+ * them by timeout while the code was correct: 5.1-5.7 s measured in three
19
+ * `make check` runs on 2026-11-01, load average between 21 and 48.
20
+ * `test/docs-guard.test.ts` gives its compile 120 s for the same reason.
21
+ */
22
+ vi.setConfig({ testTimeout: 60_000 });
23
+
12
24
  const require = createRequire(import.meta.url);
13
25
  /**
14
26
  * Transpile one generated TypeScript file and run it in a fresh VM context.
@@ -84,6 +96,8 @@ function spec(paths, schemas = {}) {
84
96
  }
85
97
 
86
98
  const OK = { 200: { description: "ok" } };
99
+ const schemaRef = (name) => ({ $ref: `#/components/schemas/${name}` });
100
+ const json = (schema) => ({ content: { "application/json": { schema } } });
87
101
 
88
102
  describe("generate — hostile spec text never becomes code (#434)", () => {
89
103
  it("a tag that closes the class doc comment stays inside it", async () => {
@@ -233,12 +247,45 @@ describe("generate — hostile spec text never becomes code (#434)", () => {
233
247
  await runGroup(files, "t");
234
248
  });
235
249
 
250
+ /**
251
+ * Rewritten for #516: this used to pass with `spec({}, …)`, two schemas no
252
+ * route reaches. The check now runs over the schemas a group emits, so the
253
+ * pair has to be referenced to collide — and still is refused, naming both.
254
+ */
236
255
  it("two schema names that map to the same identifier are refused, naming both", () => {
256
+ const both = {
257
+ 200: json({ anyOf: [schemaRef("User-Out"), schemaRef("user.Out")] }),
258
+ };
237
259
  expect(() =>
238
- generate(spec({}, { "User-Out": { type: "object" }, "user.Out": { type: "object" } })),
260
+ generate(
261
+ spec(
262
+ { "/a": { get: { tags: ["t"], responses: both } } },
263
+ { "User-Out": { type: "object" }, "user.Out": { type: "object" } },
264
+ ),
265
+ ),
239
266
  ).toThrow(/"User-Out" and "user\.Out" both map to the TypeScript identifier "UserOut"/);
240
267
  });
241
268
 
269
+ it("schema names no route reaches are neither emitted nor checked, so an orphan cannot abort the run (#516)", () => {
270
+ const { files } = generate(
271
+ spec(
272
+ {
273
+ "/a": {
274
+ get: { tags: ["t"], responses: { 200: json(schemaRef("MediaType")) } },
275
+ },
276
+ },
277
+ {
278
+ MediaType: { type: "string", enum: ["image/png"] },
279
+ "Media-Type": { type: "string", enum: ["image/png", "image/webp"] },
280
+ "***": { type: "object" },
281
+ },
282
+ ),
283
+ );
284
+ expect(files["t/types.ts"]).toContain("export type MediaType = ");
285
+ expect(files["t/schemas.ts"]).not.toContain("image/webp");
286
+ expect(files["t/types.ts"].match(/export type/g)).toHaveLength(1);
287
+ });
288
+
242
289
  it("a path parameter that maps onto another method argument is refused", () => {
243
290
  expect(() =>
244
291
  generate(
@@ -258,10 +305,16 @@ describe("generate — hostile spec text never becomes code (#434)", () => {
258
305
  ).toThrow(/"user-id" and "userId" both map to the TypeScript identifier "userId"/);
259
306
  });
260
307
 
308
+ /** Rewritten for #516 like the collision above: the schema is now referenced. */
261
309
  it("a name with nothing to build an identifier from is refused", () => {
262
- expect(() => generate(spec({}, { "***": { type: "object" } }))).toThrow(
263
- /schema name "\*\*\*" has no letter or digit/,
264
- );
310
+ expect(() =>
311
+ generate(
312
+ spec(
313
+ { "/a": { get: { tags: ["t"], responses: { 200: json(schemaRef("***")) } } } },
314
+ { "***": { type: "object" } },
315
+ ),
316
+ ),
317
+ ).toThrow(/schema name "\*\*\*" has no letter or digit/);
265
318
  });
266
319
 
267
320
  it("a regex pattern with an escaped slash or a line break compiles to the same regex", () => {
@@ -50,12 +50,88 @@ function methodName(op, method, path) {
50
50
  const CLASS_MEMBERS = ["constructor", "api"];
51
51
 
52
52
  /**
53
- * Names a generated method body uses besides its path parameters: the `body` and
54
- * `params` arguments, the `S` schemas import and the `encodeIdSegment` import. A
55
- * path parameter that maps to one of them becomes `<name>Param` (`{body}` →
56
- * `bodyParam`), so it neither duplicates an argument nor shadows the import.
53
+ * Names a generated method body uses besides its path parameters: the `body`,
54
+ * `params` and `headers` arguments, the `S` schemas import, the `encodeIdSegment`
55
+ * import and the module-level `headerRecord` helper. A path parameter that maps to
56
+ * one of them becomes `<name>Param` (`{body}` → `bodyParam`), so it neither
57
+ * duplicates an argument nor shadows the import.
57
58
  */
58
- const METHOD_LOCALS = new Set(["body", "params", "S", "encodeIdSegment"]);
59
+ const METHOD_LOCALS = new Set([
60
+ "body",
61
+ "params",
62
+ "headers",
63
+ "S",
64
+ "encodeIdSegment",
65
+ "headerRecord",
66
+ ]);
67
+
68
+ /**
69
+ * Header parameter names OpenAPI 3 says a generator SHALL ignore: the client sets
70
+ * `Accept` and `Content-Type` from the request it builds and `Authorization` from
71
+ * its `getToken`, so a spec that also declares them must not open a second way to
72
+ * send them. Compared case-insensitively, as HTTP header names are.
73
+ */
74
+ const IGNORED_HEADERS = new Set(["accept", "content-type", "authorization"]);
75
+
76
+ /**
77
+ * Module-level helper a `service.ts` declares when one of its methods sends header
78
+ * parameters. `fetch` turns a header value of `undefined` into the literal text
79
+ * `"undefined"`, and `RequestOptions.headers` only takes strings, so an optional
80
+ * header left out is dropped and every other value goes through `String()`.
81
+ */
82
+ const HEADER_RECORD_HELPER = [
83
+ "/** Header parameters as strings, without the ones left out (`fetch` would send `undefined` as text). */",
84
+ "function headerRecord(",
85
+ " values: Record<string, string | number | boolean | null | undefined>,",
86
+ "): Record<string, string> {",
87
+ " const out: Record<string, string> = {};",
88
+ " for (const [name, value] of Object.entries(values)) {",
89
+ " if (value !== undefined && value !== null) out[name] = String(value);",
90
+ " }",
91
+ " return out;",
92
+ "}",
93
+ ].join("\n");
94
+
95
+ /**
96
+ * Follow a `#/components/parameters/...` `$ref` chain to the parameter object.
97
+ *
98
+ * @param {object | undefined} param - A parameter object or a reference to one.
99
+ * @param {Record<string, object>} components - `components.parameters`.
100
+ * @returns {object | undefined} The parameter object, or `undefined` when the
101
+ * chain does not resolve.
102
+ */
103
+ function resolveParameter(param, components) {
104
+ let node = param;
105
+ let guard = 0;
106
+ while (node && node.$ref && guard++ < 16) node = components[refName(node.$ref)];
107
+ return node && !node.$ref ? node : undefined;
108
+ }
109
+
110
+ /**
111
+ * The parameters that apply to one operation: the path item's `parameters`
112
+ * merged with the operation's, the operation winning on the same `(name, in)` —
113
+ * the rule OpenAPI 3 sets for the two levels.
114
+ *
115
+ * Before #509 only `operation.parameters` was read, so a `{id}` declared on the
116
+ * path item reached the URL literally (`"/items/{id}"`, measured), and a path item
117
+ * declaring one placeholder while the operation declared the other refused to
118
+ * generate. `$ref`s into `components.parameters` are resolved here too: they were
119
+ * dropped the same way, because a reference object has no `in`.
120
+ *
121
+ * @param {object} item - The path item.
122
+ * @param {object} op - The operation.
123
+ * @param {Record<string, object>} components - `components.parameters`.
124
+ * @returns {object[]} The effective parameter objects, path-item order first.
125
+ */
126
+ function operationParameters(item, op, components) {
127
+ const merged = new Map();
128
+ for (const raw of [...(item.parameters ?? []), ...(op.parameters ?? [])]) {
129
+ const param = resolveParameter(raw, components);
130
+ if (!param || typeof param.name !== "string" || typeof param.in !== "string") continue;
131
+ merged.set(`${param.in}\u0000${param.name}`, param);
132
+ }
133
+ return [...merged.values()];
134
+ }
59
135
 
60
136
  /**
61
137
  * Whether a path parameter's schema only ever produces a `string` or a `number` —
@@ -208,16 +284,17 @@ function isQueryScalar(schema, schemas, depth = 0) {
208
284
  }
209
285
 
210
286
  /**
211
- * TS type for a query-param value. Query params serialize to strings, so the
212
- * client only accepts `string | number | boolean | null`. Anything richer
213
- * (objects, arrays) is narrowed to `string` to stay assignable.
287
+ * TS type for a query- or header-param value. Both serialize to strings, so the
288
+ * client's `params` and the generated `headerRecord` only accept
289
+ * `string | number | boolean | null`. Anything richer (objects, arrays) is
290
+ * narrowed to `string` to stay assignable.
214
291
  *
215
292
  * @param {object | undefined} schema - The parameter's schema.
216
293
  * @param {Record<string, object>} schemas - `components.schemas`.
217
294
  * @param {Set<string>} refs - Collects the schemas the type names.
218
295
  * @returns {string} The TypeScript type expression.
219
296
  */
220
- function queryParamType(schema, schemas, refs) {
297
+ function scalarParamType(schema, schemas, refs) {
221
298
  const node = schema ?? { type: "string" };
222
299
  return isQueryScalar(node, schemas) ? tsType(node, refs) : "string";
223
300
  }
@@ -343,16 +420,25 @@ function compareNames(a, b) {
343
420
  * template's ESLint enforces, so the output passes its lint and `tsc` with
344
421
  * `noUnusedLocals` as generated (#504, #505).
345
422
  *
423
+ * A component schema no route reaches is never emitted — FastAPI publishes an
424
+ * unreferenced enum for every `Enum`-annotated field (`ImageMediaType`, #516),
425
+ * and it lands in no group. Its name is not checked either: the identifier
426
+ * collision check runs per group over the schemas that group emits, so an orphan
427
+ * named `Media-Type` next to a referenced `MediaType` no longer aborts the whole
428
+ * run for a type nobody would see.
429
+ *
430
+ * Parameters are read from the path item and the operation, merged by
431
+ * `(name, in)` (see {@link operationParameters}); `in: "cookie"` is skipped,
432
+ * because a browser `fetch` cannot set `Cookie` — the browser sends its own
433
+ * cookies with `credentials`, which is how a cookie parameter reaches the server.
434
+ *
346
435
  * @param {object} doc - The parsed OpenAPI 3.x spec.
347
436
  * @returns {{ files: Record<string, string>, tags: string[] }}
348
437
  */
349
438
  export function generate(doc) {
350
439
  const schemas = doc.components?.schemas ?? {};
351
- assertNoCollision(
352
- Object.keys(schemas).map((n) => [n, typeName(n)]),
353
- "schema names",
354
- );
355
- const groups = new Map(); // slug → { tag, ops: [...] }
440
+ const parameterComponents = doc.components?.parameters ?? {};
441
+ const groups = new Map();
356
442
 
357
443
  for (const [path, item] of Object.entries(doc.paths ?? {})) {
358
444
  for (const method of HTTP_METHODS) {
@@ -361,7 +447,8 @@ export function generate(doc) {
361
447
  const tag = op.tags?.[0] ?? "default";
362
448
  const slug = tagSlug(tag);
363
449
  if (!groups.has(slug)) groups.set(slug, { tag, slug, ops: [] });
364
- groups.get(slug).ops.push({ method, path, op });
450
+ const params = operationParameters(item, op, parameterComponents);
451
+ groups.get(slug).ops.push({ method, path, op, params });
365
452
  }
366
453
  }
367
454
 
@@ -378,9 +465,9 @@ export function generate(doc) {
378
465
  const Class = `${slugIdent(pascal(slug))}Service`;
379
466
 
380
467
  const usedNames = new Set(CLASS_MEMBERS);
381
- const signature = { refs: new Set(), validates: false };
382
- const methods = ops.map(({ method, path, op }) =>
383
- emitMethod(method, path, op, schemas, usedNames, signature),
468
+ const signature = { refs: new Set(), validates: false, sendsHeaders: false };
469
+ const methods = ops.map(({ method, path, op, params }) =>
470
+ emitMethod(method, path, op, params, schemas, usedNames, signature),
384
471
  );
385
472
 
386
473
  const used = new Set();
@@ -392,6 +479,10 @@ export function generate(doc) {
392
479
  }
393
480
  for (const name of signature.refs) collectRefs({ $ref: name }, schemas, used);
394
481
  const { sorted, cyclic } = topoSort(used, schemas);
482
+ assertNoCollision(
483
+ sorted.map((n) => [n, typeName(n)]),
484
+ "schema names",
485
+ );
395
486
  for (const name of sorted) assertPatterns(schemas[name], name);
396
487
 
397
488
  const schemaLines = sorted.map((name) => {
@@ -433,6 +524,7 @@ export function generate(doc) {
433
524
  .filter((group) => group.length)
434
525
  .map((group) => group.join("\n"))
435
526
  .join("\n\n") +
527
+ (signature.sendsHeaders ? `\n\n${HEADER_RECORD_HELPER}` : "") +
436
528
  `\n\n/** Generated service for the "${commentText(tag)}" routes. Inject an ApiClient (createApiClient). */\n` +
437
529
  `export class ${Class} {\n` +
438
530
  ` constructor(private readonly api: ApiClient) {}\n\n` +
@@ -461,16 +553,27 @@ export function generate(doc) {
461
553
  * pagination envelope maps to `OffsetPage<T>` / `CursorPage<T>`, and a body typed
462
554
  * by a `$ref` is validated with its Zod schema before the call.
463
555
  *
556
+ * Arguments come in a fixed order: path parameters, `body`, `params` (query),
557
+ * `headers`. Header parameters are typed like query parameters and sent through
558
+ * the request's `headers`, via `headerRecord`; the `headers` argument defaults to
559
+ * `{}` when none of them is required, so a spec gaining an optional header does
560
+ * not break the call sites. `Accept`, `Content-Type` and `Authorization` are left
561
+ * out ({@link IGNORED_HEADERS}), and so is every `in: "cookie"` parameter: a
562
+ * browser `fetch` cannot set `Cookie` (forbidden header name), the cookie jar
563
+ * sends it with `credentials`.
564
+ *
464
565
  * @param {string} method - The HTTP method, lower case.
465
566
  * @param {string} path - The OpenAPI path template.
466
567
  * @param {object} op - The operation object.
568
+ * @param {object[]} parameters - The effective parameters, from {@link operationParameters}.
467
569
  * @param {Record<string, object>} schemas - `components.schemas`.
468
570
  * @param {Set<string>} usedNames - Member names already taken in the class.
469
- * @param {{ refs: Set<string>, validates: boolean }} signature - Collects the
470
- * component schemas the emitted types name, and whether `S` was used.
571
+ * @param {{ refs: Set<string>, validates: boolean, sendsHeaders: boolean }} signature -
572
+ * Collects the component schemas the emitted types name, whether `S` was used
573
+ * and whether `headerRecord` was.
471
574
  * @returns {string} The method source.
472
575
  */
473
- function emitMethod(method, path, op, schemas, usedNames, signature) {
576
+ function emitMethod(method, path, op, parameters, schemas, usedNames, signature) {
474
577
  let name = methodName(op, method, path);
475
578
  if (usedNames.has(name)) {
476
579
  let n = 2;
@@ -478,7 +581,11 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
478
581
  name = `${name}${n}`;
479
582
  }
480
583
  usedNames.add(name);
481
- const pathParams = (op.parameters ?? []).filter((p) => p.in === "path");
584
+ const pathParams = parameters.filter((p) => p.in === "path");
585
+ const queryParams = parameters.filter((p) => p.in === "query");
586
+ const headerParams = parameters.filter(
587
+ (p) => p.in === "header" && !IGNORED_HEADERS.has(p.name.toLowerCase()),
588
+ );
482
589
  const where = `${method.toUpperCase()} ${path}`;
483
590
  const paramId = (name) => {
484
591
  const id = toIdentifier(name, "camel", `path parameter in ${where}`);
@@ -496,13 +603,11 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
496
603
  [
497
604
  ...[...paramIds],
498
605
  ...(bodySchema(op) ? [["the request body argument", "body"]] : []),
499
- ...((op.parameters ?? []).some((p) => p.in === "query")
500
- ? [["the query argument", "params"]]
501
- : []),
606
+ ...(queryParams.length ? [["the query argument", "params"]] : []),
607
+ ...(headerParams.length ? [["the headers argument", "headers"]] : []),
502
608
  ],
503
609
  `method arguments of ${where}`,
504
610
  );
505
- const queryParams = (op.parameters ?? []).filter((p) => p.in === "query");
506
611
  const body = bodySchema(op);
507
612
  const resp = successSchema(op);
508
613
  const page = resp ? detectPage(resp, schemas, signature.refs) : null;
@@ -512,6 +617,14 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
512
617
  ? tsType(resp, signature.refs)
513
618
  : "void";
514
619
 
620
+ const fields = (list) =>
621
+ list
622
+ .map(
623
+ (p) =>
624
+ `${JSON.stringify(p.name)}${p.required ? "" : "?"}: ${scalarParamType(p.schema, schemas, signature.refs)}`,
625
+ )
626
+ .join("; ");
627
+
515
628
  const args = [];
516
629
  for (const p of pathParams) {
517
630
  args.push(
@@ -519,14 +632,11 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
519
632
  );
520
633
  }
521
634
  if (body) args.push(`body: ${tsType(body, signature.refs)}`);
522
- if (queryParams.length) {
523
- const q = queryParams
524
- .map(
525
- (p) =>
526
- `${JSON.stringify(p.name)}${p.required ? "" : "?"}: ${queryParamType(p.schema, schemas, signature.refs)}`,
527
- )
528
- .join("; ");
529
- args.push(`params: { ${q} }`);
635
+ if (queryParams.length) args.push(`params: { ${fields(queryParams)} }`);
636
+ if (headerParams.length) {
637
+ const optional = headerParams.every((p) => !p.required);
638
+ args.push(`headers: { ${fields(headerParams)} }${optional ? " = {}" : ""}`);
639
+ signature.sendsHeaders = true;
530
640
  }
531
641
 
532
642
  const url = urlTemplate(path, segmentArgs);
@@ -534,6 +644,7 @@ function emitMethod(method, path, op, schemas, usedNames, signature) {
534
644
  const callOpts = [];
535
645
  if (body) callOpts.push("body");
536
646
  if (queryParams.length) callOpts.push("params");
647
+ if (headerParams.length) callOpts.push("headers: headerRecord(headers)");
537
648
  const optsArg = callOpts.length ? `, { ${callOpts.join(", ")} }` : "";
538
649
 
539
650
  let validation = "";