tempest-react-sdk 0.5.1 → 0.8.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 (51) hide show
  1. package/README.md +206 -35
  2. package/bin/create-tempest-app.mjs +248 -0
  3. package/bin/tempest.mjs +282 -0
  4. package/dist/styles.css +1 -1
  5. package/dist/sw.cjs +2 -0
  6. package/dist/sw.cjs.map +1 -0
  7. package/dist/sw.d.ts +103 -0
  8. package/dist/sw.js +95 -0
  9. package/dist/sw.js.map +1 -0
  10. package/dist/tempest-react-sdk.cjs +4 -4
  11. package/dist/tempest-react-sdk.cjs.map +1 -1
  12. package/dist/tempest-react-sdk.d.ts +1697 -7
  13. package/dist/tempest-react-sdk.js +4485 -2989
  14. package/dist/tempest-react-sdk.js.map +1 -1
  15. package/dist/vite.cjs +2 -0
  16. package/dist/vite.cjs.map +1 -0
  17. package/dist/vite.d.ts +62 -0
  18. package/dist/vite.js +46 -0
  19. package/dist/vite.js.map +1 -0
  20. package/package.json +34 -2
  21. package/template/README.md +37 -0
  22. package/template/_env.example +2 -0
  23. package/template/_gitignore +9 -0
  24. package/template/_prettierrc.json +9 -0
  25. package/template/eslint.config.js +44 -0
  26. package/template/index.html +12 -0
  27. package/template/package.json +38 -0
  28. package/template/src/App.tsx +15 -0
  29. package/template/src/layouts/RootLayout.tsx +28 -0
  30. package/template/src/lib/api.ts +17 -0
  31. package/template/src/main.tsx +10 -0
  32. package/template/src/pages/Dashboard.tsx +19 -0
  33. package/template/src/pages/Home.tsx +16 -0
  34. package/template/src/pages/Login.tsx +27 -0
  35. package/template/src/routes.tsx +27 -0
  36. package/template/src/stores/auth.ts +15 -0
  37. package/template/src/vite-env.d.ts +1 -0
  38. package/template/tsconfig.json +26 -0
  39. package/template/vite.config.ts +7 -0
  40. package/template-pwa/README.md +64 -0
  41. package/template-pwa/_env.example +7 -0
  42. package/template-pwa/index.html +22 -0
  43. package/template-pwa/package.json +6 -0
  44. package/template-pwa/public/icon-maskable.svg +4 -0
  45. package/template-pwa/public/icon.svg +4 -0
  46. package/template-pwa/public/manifest.webmanifest +35 -0
  47. package/template-pwa/src/main.tsx +36 -0
  48. package/template-pwa/src/pages/Dashboard.tsx +73 -0
  49. package/template-pwa/src/sw.ts +35 -0
  50. package/template-pwa/src/vite-env.d.ts +12 -0
  51. package/template-pwa/vite.sw.config.ts +27 -0
package/README.md CHANGED
@@ -7,6 +7,16 @@
7
7
  [![TypeScript](https://img.shields.io/badge/types-TypeScript-3178c6.svg?logo=typescript)](https://www.typescriptlang.org/)
8
8
  [![Bundle size](https://img.shields.io/bundlephobia/minzip/tempest-react-sdk?label=gzip)](https://bundlephobia.com/package/tempest-react-sdk)
9
9
 
10
+ [**📖 Documentação completa (PT-BR) →**](https://mauriciobenjamin700.github.io/tempest-react-sdk/) · [**📖 Full documentation (EN-US) →**](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)
11
+
12
+ > O site MkDocs é bilíngue (**PT-BR** padrão · **EN-US**) com seletor de idioma 🇧🇷/🇺🇸 no cabeçalho. — The MkDocs site is bilingual (**PT-BR** default · **EN-US**) with a 🇧🇷/🇺🇸 language switcher in the header. The site is the navigable, per-module source of truth; this README stays the npm/GitHub landing page.
13
+
14
+ - **PT-BR:** [Scaffold](https://mauriciobenjamin700.github.io/tempest-react-sdk/scaffold/) · [Estrutura de app](https://mauriciobenjamin700.github.io/tempest-react-sdk/architecture/) · [Componentes](https://mauriciobenjamin700.github.io/tempest-react-sdk/components/) · [Hooks](https://mauriciobenjamin700.github.io/tempest-react-sdk/hooks/) · [Utilitários](https://mauriciobenjamin700.github.io/tempest-react-sdk/utilities/)
15
+ - **EN-US:** [Scaffold](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/scaffold/) · [App foundation](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/architecture/) · [Components](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/components/) · [Hooks](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/hooks/) · [Utilities](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/utilities/)
16
+ - **🤖 For LLMs:** [llms.txt](https://mauriciobenjamin700.github.io/tempest-react-sdk/llms.txt) (curated index) · [llms-full.txt](https://mauriciobenjamin700.github.io/tempest-react-sdk/llms-full.txt) (full docs in one block) — [llmstxt.org](https://llmstxt.org) convention.
17
+
18
+ > 💡 `pip install -r docs/requirements.txt && mkdocs serve` é só para preview local — em produção use as URLs do GitHub Pages acima. / For local preview only — in production use the GitHub Pages URLs above.
19
+
10
20
  Shared React/TypeScript building blocks used across Tempest frontends: UI components, hooks, HTTP client, auth store, query keys, forms (zod), real-time transports (SSE / WebSocket / Web Push / Service Worker), theme, i18n, telemetry, feature flags, offline storage, error boundary, and a curated set of utilities (`cn`, `formatCurrency`, `formatCPF`, etc.).
11
21
 
12
22
  The goal is to start every new React frontend with the same opinionated foundation already in place — no copy-pasting `Button`/`Input` styles, no rewriting the same auth Zustand store, no re-inventing the SSE reconnect loop. The patterns here are a distillation of what was consolidated in **alofans-frontend** and **transport-admin-system** — apps that consume the SDK gain consistency without paying for boilerplate.
@@ -20,6 +30,8 @@ The goal is to start every new React frontend with the same opinionated foundati
20
30
  - [Peer & bundled dependencies](#peer--bundled-dependencies)
21
31
  - [CSS import](#css-import)
22
32
  - [What's inside](#whats-inside)
33
+ - [Scaffold a new app](#scaffold-a-new-app)
34
+ - [App foundation (routing, state, providers, Vite)](#app-foundation)
23
35
  - [Architecture overview](#architecture-overview)
24
36
  - [Quickstart — wiring the app providers](#quickstart--wiring-the-app-providers)
25
37
  - [Recipes](#recipes)
@@ -75,7 +87,31 @@ The goal is to start every new React frontend with the same opinionated foundati
75
87
  - Fast HMR — provider files (`ThemeProvider`, `I18nProvider`, etc.) opt into React Refresh.
76
88
  - First-class compatibility with the Vite plugin ecosystem (`vite-plugin-pwa` for service workers, `vite-plugin-dts`, `vite-plugin-svgr`, etc.).
77
89
 
78
- To bootstrap a new app:
90
+ **Fastest path — scaffold a fully wired app** with the `create-tempest-app` CLI that ships **inside the SDK** (Vite `@` alias, declarative routing, Zustand store, TanStack Query, providers — all pre-fiados):
91
+
92
+ ```bash
93
+ # brand-new project (no install needed)
94
+ npx -p tempest-react-sdk create-tempest-app my-app
95
+ cd my-app
96
+ npm install
97
+ npm run dev
98
+
99
+ # want it installable + web-push ready? add --pwa
100
+ npx -p tempest-react-sdk create-tempest-app my-app --pwa
101
+ ```
102
+
103
+ The `--pwa` flag overlays a manifest, a service worker built from `tempest-react-sdk/sw`, and push wiring (`usePushSubscription` + `useBeforeInstallPrompt`) on top of the base app — no `vite-plugin-pwa`. See [Scaffold › PWA mode](https://mauriciobenjamin700.github.io/tempest-react-sdk/scaffold/#modo-pwa-pwa).
104
+
105
+ Already have a project? Install the SDK, then scaffold `src/` + configs into it:
106
+
107
+ ```bash
108
+ npm install tempest-react-sdk
109
+ npx create-tempest-app . # merges into the current dir, skips existing files
110
+ ```
111
+
112
+ See [Scaffold a new app](#scaffold-a-new-app) for the generated layout.
113
+
114
+ Or start from a bare Vite template and add the SDK manually:
79
115
 
80
116
  ```bash
81
117
  npm create vite@latest my-app -- --template react-ts
@@ -113,17 +149,19 @@ Requires React `>=18` and Node `>=20.19` to build.
113
149
 
114
150
  Only **react** and **react-dom** are peer dependencies — those must come from the host app so a single React copy lives in the tree.
115
151
 
116
- Everything else (`zod`, `zustand`, `dexie`, `react-hook-form`, `@tanstack/react-query`, `lucide-react`) is a **direct dependency** of the SDK, installed automatically by `npm install tempest-react-sdk`. You never need to install them manually.
152
+ Everything else (`zod`, `zustand`, `dexie`, `react-hook-form`, `@tanstack/react-query`, `react-router-dom`, `lucide-react`) is a **direct dependency** of the SDK, installed automatically by `npm install tempest-react-sdk`. You never need to install them manually.
117
153
 
118
- | Package | Status | Used by |
119
- | ------------------------------------- | ------------------- | ------------------------------------------------------------------- |
120
- | `react`, `react-dom` (`^18 \|\| ^19`) | **Peer (required)** | Everything |
121
- | `@tanstack/react-query` (`^5`) | Direct dep (auto) | `QueryProvider`, `createQueryKeys` |
122
- | `zod` (`^3.23 \|\| ^4`) | Direct dep (auto) | `parseResponse`, `validateForm`, `zodResolver`, `useZodForm` |
123
- | `zustand` (`^4 \|\| ^5`) | Direct dep (auto) | `createAuthStore` |
124
- | `dexie` (`^4.4`) | Direct dep (auto) | `createOfflineStore` |
125
- | `react-hook-form` (`^7.76`) | Direct dep (auto) | `zodResolver`, `useZodForm`, masked inputs |
126
- | `lucide-react` (`>=0.400`) | Direct dep (auto) | Component icons (`leftIcon`/`rightIcon` on `Input`, `Button`, etc.) |
154
+ | Package | Status | Used by |
155
+ | ------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
156
+ | `react`, `react-dom` (`^18 \|\| ^19`) | **Peer (required)** | Everything |
157
+ | `@tanstack/react-query` (`^5`) | Direct dep (auto) | `QueryProvider`, `createQueryKeys`, `AppProviders` |
158
+ | `zod` (`^3.23 \|\| ^4`) | Direct dep (auto) | `parseResponse`, `validateForm`, `zodResolver`, `useZodForm` |
159
+ | `zustand` (`^4 \|\| ^5`) | Direct dep (auto) | `createAuthStore`, `createStore`, `createSelectors` |
160
+ | `react-router-dom` (`^7`) | Direct dep (auto) | `AppRouter`, `defineRoutes`, `RouteGuard`, routing re-exports |
161
+ | `dexie` (`^4.4`) | Direct dep (auto) | `createOfflineStore` |
162
+ | `react-hook-form` (`^7.76`) | Direct dep (auto) | `zodResolver`, `useZodForm`, masked inputs |
163
+ | `lucide-react` (`>=0.400`) | Direct dep (auto) | Component icons (`leftIcon`/`rightIcon` on `Input`, `Button`, etc.) |
164
+ | `vite`, `@vitejs/plugin-react` | **Optional peer** | `createViteConfig` (`tempest-react-sdk/vite`) — already in any Vite app |
127
165
 
128
166
  The minimum install is just:
129
167
 
@@ -155,35 +193,168 @@ The styles ship hashed under the `tempest_` namespace — they do **not** collid
155
193
 
156
194
  Every module is re-exported from the package root — `import { Button, useDebounce, createApiClient } from "tempest-react-sdk"` always works.
157
195
 
158
- | Module | Exports |
159
- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
- | `components` | `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
161
- | `hooks` | `useDebounce`, `usePagination`, `useClientFilter`, `useMediaQuery`, `useOnline`, `useDocumentVisibility`, `useIntersectionObserver`, `useResizeObserver`, `useClipboard`, `useKeyboardShortcut`, `useBeforeInstallPrompt`, `useIdle`, `useGeolocation`, `useScrollLock`, `useFocusTrap`, `useStableCallback`, `useDeepMemo` |
162
- | `http` | `createApiClient`, `parseResponse`, `uploadWithProgress`, `retry`, `generateIdempotencyKey`, `usePoll`, types: `ApiClient`, `ApiClientConfig`, `ApiError`, `RequestOptions`, `RetryOptions`, `UploadProgressEvent`, `UploadWithProgressOptions`, `UsePollOptions`, `UsePollResult` |
163
- | `auth` _(peer: `zustand`)_ | `createAuthStore`, `AuthGuard`, `decodeJWT`, `isJWTExpired`, `lazyWithRetry`, `createRefreshQueue`, types: `AuthState`, `CreateAuthStoreOptions`, `AuthGuardProps`, `DecodedJWT`, `LazyWithRetryOptions` |
164
- | `query` _(peer: `@tanstack/react-query`)_ | `QueryProvider`, `createQueryKeys`, `STALE_TIME`, `CACHE_TIME`, `REFETCH_TIME` |
165
- | `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
166
- | `sse` | `createEventStream`, `useEventStream` |
167
- | `ws` | `createWebSocket`, `useWebSocket` |
168
- | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
169
- | `sw` | `registerServiceWorker`, `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener` |
170
- | `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio` |
171
- | `offline` _(peer: `dexie`)_ | `createOfflineStore`, types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions` |
172
- | `error-boundary` | `ErrorBoundary`, `useErrorHandler`, types: `ErrorBoundaryProps`, `ErrorBoundaryRenderProps` |
173
- | `theme` | `ThemeProvider`, `useTheme`, `getInitialTheme`, `themeInitScript`, types: `ThemeMode`, `ResolvedTheme` |
174
- | `i18n` | `createI18n`, `I18nProvider`, `useI18n`, `useTranslate`, types: `Catalog`, `Messages`, `I18n`, `InterpolationValues` |
175
- | `logger` | `createLogger`, `consoleSink`, types: `Logger`, `LogEntry`, `LogLevel`, `LoggerSink` |
176
- | `telemetry` | `TelemetryProvider`, `useTelemetry`, `consoleTelemetryAdapter`, `createSentryTelemetryAdapter`, `createPostHogTelemetryAdapter`, types: `TelemetryAdapter`, `TelemetryEvent`, `TelemetryUser`, `CreateSentryTelemetryAdapterOptions`, `SentryLike`, `CreatePostHogTelemetryAdapterOptions`, `PostHogLike` |
177
- | `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
178
- | `share` | `share`, `isShareSupported`, types: `SharePayload`, `ShareResult` |
179
- | `utils` | `cn`, `formatCurrency`, `formatDate`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`, `storage` |
180
-
181
- Full per-module docs in [`docs/`](./docs) (one markdown per module + draw.io diagrams in [`docs/diagrams/`](./docs/diagrams)).
196
+ | Module | Exports |
197
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
198
+ | `components` | `Avatar`, `Badge`, `Breadcrumbs`, `Button`, `Card`, `Checkbox`, `ChipInput`, `ConfirmDialog`, `Container`, `DatePicker`, `Drawer`, `EmptyState`, `ErrorState`, `FileUpload`, `Form` (`FormSection`, `FormRow`, `FormActions`), `Grid`, `Input`, `Modal`, `Pagination`, `Progress`, `Radio`, `RadioGroup`, `SearchBar`, `Select`, `Skeleton`, `Spinner`, `Stack`, `Stepper`, `Switch`, `Table`, `Tabs`, `Textarea`, `Toast` (`ToastProvider`, `useToast`), `Tooltip`, `VirtualList` |
199
+ | `hooks` | `useDebounce`, `usePagination`, `useClientFilter`, `useMediaQuery`, `useOnline`, `useDocumentVisibility`, `useIntersectionObserver`, `useResizeObserver`, `useClipboard`, `useKeyboardShortcut`, `useBeforeInstallPrompt`, `useIdle`, `useGeolocation`, `useScrollLock`, `useFocusTrap`, `useStableCallback`, `useDeepMemo` |
200
+ | `http` | `createApiClient`, `parseResponse`, `uploadWithProgress`, `retry`, `generateIdempotencyKey`, `usePoll`, types: `ApiClient`, `ApiClientConfig`, `ApiError`, `RequestOptions`, `RetryOptions`, `UploadProgressEvent`, `UploadWithProgressOptions`, `UsePollOptions`, `UsePollResult` |
201
+ | `auth` _(peer: `zustand`)_ | `createAuthStore`, `AuthGuard`, `decodeJWT`, `isJWTExpired`, `lazyWithRetry`, `createRefreshQueue`, types: `AuthState`, `CreateAuthStoreOptions`, `AuthGuardProps`, `DecodedJWT`, `LazyWithRetryOptions` |
202
+ | `query` _(peer: `@tanstack/react-query`)_ | `QueryProvider`, `createQueryKeys`, `STALE_TIME`, `CACHE_TIME`, `REFETCH_TIME` |
203
+ | `router` _(dep: `react-router-dom`)_ | `defineRoutes`, `AppRouter`, `RouteGuard`, + re-exports (`Link`, `NavLink`, `Outlet`, `Navigate`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`, `useMatch`, `useRouteError`, `redirect`, `BrowserRouter`/`HashRouter`/`MemoryRouter`/`Routes`/`Route`), types: `TempestRouteObject`, `RouterKind`, `AppRouterProps`, `RouteGuardProps` |
204
+ | `store` _(dep: `zustand`)_ | `createStore`, `createSelectors`, types: `CreateStoreOptions`, `CreateStorePersistOptions`, `WithSelectors` |
205
+ | `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
206
+ | `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig` |
207
+ | `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
208
+ | `sse` | `createEventStream`, `useEventStream` |
209
+ | `ws` | `createWebSocket`, `useWebSocket` |
210
+ | `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
211
+ | `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker`, `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener` — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
212
+ | `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio` |
213
+ | `offline` _(peer: `dexie`)_ | `createOfflineStore`, types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions` |
214
+ | `error-boundary` | `ErrorBoundary`, `useErrorHandler`, types: `ErrorBoundaryProps`, `ErrorBoundaryRenderProps` |
215
+ | `theme` | `ThemeProvider`, `useTheme`, `getInitialTheme`, `themeInitScript`, types: `ThemeMode`, `ResolvedTheme` |
216
+ | `i18n` | `createI18n`, `I18nProvider`, `useI18n`, `useTranslate`, types: `Catalog`, `Messages`, `I18n`, `InterpolationValues` |
217
+ | `logger` | `createLogger`, `consoleSink`, types: `Logger`, `LogEntry`, `LogLevel`, `LoggerSink` |
218
+ | `telemetry` | `TelemetryProvider`, `useTelemetry`, `consoleTelemetryAdapter`, `createSentryTelemetryAdapter`, `createPostHogTelemetryAdapter`, types: `TelemetryAdapter`, `TelemetryEvent`, `TelemetryUser`, `CreateSentryTelemetryAdapterOptions`, `SentryLike`, `CreatePostHogTelemetryAdapterOptions`, `PostHogLike` |
219
+ | `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
220
+ | `share` | `share`, `isShareSupported`, types: `SharePayload`, `ShareResult` |
221
+ | `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime` |
222
+ | generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
223
+
224
+ Full per-module docs are published as a bilingual MkDocs site on GitHub Pages — **[Português (BR)](https://mauriciobenjamin700.github.io/tempest-react-sdk/)** / **[English (US)](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)** (one page per module + draw.io diagrams in [`docs/diagrams/`](./docs/diagrams)). The source markdown lives in [`docs/`](./docs) (PT-BR base files + `.en.md` translations).
225
+
226
+ > **Local preview:** `pip install -r docs/requirements.txt && mkdocs serve` (the published site is built and deployed automatically by `.github/workflows/docs.yml`).
182
227
 
183
228
  A demo app exercising every module lives in [`examples/gallery`](./examples/gallery) — `cd examples/gallery && npm install && npm run dev`.
184
229
 
185
230
  ---
186
231
 
232
+ ## Scaffold a new app
233
+
234
+ The **`create-tempest-app`** CLI **ships inside the `tempest-react-sdk` package** (it is the package's `bin`) and generates a ready-to-run Vite + React 19 + TypeScript project already wired with the SDK — no manual provider/router/store setup:
235
+
236
+ ```bash
237
+ # brand-new project folder (npx pulls the SDK and runs its bin)
238
+ npx -p tempest-react-sdk create-tempest-app my-app
239
+ cd my-app
240
+ npm install
241
+ cp .env.example .env
242
+ npm run dev # http://127.0.0.1:5173
243
+ ```
244
+
245
+ Or, in a project that already depends on the SDK, scaffold into the current directory (existing files are left untouched; an existing `package.json` gets the Tempest scripts/deps merged in):
246
+
247
+ ```bash
248
+ npm install tempest-react-sdk
249
+ npx create-tempest-app .
250
+ ```
251
+
252
+ Generated layout:
253
+
254
+ ```text
255
+ my-app/
256
+ ├── index.html
257
+ ├── package.json # react, react-dom, tempest-react-sdk (+ vite/ts devDeps)
258
+ ├── tsconfig.json # "@/*" -> "./src/*"
259
+ ├── vite.config.ts # export default createViteConfig()
260
+ ├── .env.example # VITE_API_URL
261
+ └── src/
262
+ ├── main.tsx # createRoot + "tempest-react-sdk/styles.css" + <App/>
263
+ ├── App.tsx # <AppProviders> → <AppRouter routes fallback/>
264
+ ├── routes.tsx # defineRoutes([...]) — index, login, lazy + guarded dashboard
265
+ ├── layouts/RootLayout.tsx # nav (Link) + <Outlet/>
266
+ ├── pages/ # Home, Login, Dashboard (lazy + protected)
267
+ ├── stores/auth.ts # createSelectors(createAuthStore<User>())
268
+ └── lib/api.ts # createApiClient(...) + createQueryKeys
269
+ ```
270
+
271
+ Each generated file demonstrates one SDK capability. Full walkthrough: **[scaffold docs (PT)](https://mauriciobenjamin700.github.io/tempest-react-sdk/scaffold/)** · **[EN](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/scaffold/)**.
272
+
273
+ ---
274
+
275
+ ## App foundation
276
+
277
+ Beyond UI blocks, the SDK ships an opinionated **application foundation** so every Tempest frontend wires Vite, routing, state and cache the same way. These are also what the scaffold above generates.
278
+
279
+ **Vite config** — one call wires `@vitejs/plugin-react`, the `@` → `src` alias and dev-server defaults (import from the Node subpath):
280
+
281
+ ```ts
282
+ // vite.config.ts
283
+ import { createViteConfig } from "tempest-react-sdk/vite";
284
+
285
+ export default createViteConfig({
286
+ proxy: { "/api": "http://127.0.0.1:8000" },
287
+ });
288
+ ```
289
+
290
+ > Declare the same alias in `tsconfig.json` so the type-checker resolves it: `"paths": { "@/*": ["./src/*"] }`.
291
+
292
+ **Declarative routing** (React Router v7) — describe the tree as data, with `lazy` code-splitting and per-route `guard` redirects:
293
+
294
+ ```tsx
295
+ import { defineRoutes, AppRouter } from "tempest-react-sdk";
296
+ import { useAuth } from "@/stores/auth";
297
+
298
+ export const routes = defineRoutes([
299
+ {
300
+ path: "/",
301
+ element: <RootLayout />,
302
+ children: [
303
+ { index: true, element: <Home /> },
304
+ { path: "login", element: <Login /> },
305
+ {
306
+ path: "dashboard",
307
+ lazy: () => import("@/pages/Dashboard"),
308
+ guard: () => useAuth.getState().isAuthenticated,
309
+ redirectTo: "/login",
310
+ },
311
+ ],
312
+ },
313
+ ]);
314
+
315
+ // <AppRouter routes={routes} fallback={<p>Loading…</p>} />
316
+ ```
317
+
318
+ `AppRouter` also re-exports `Link`, `NavLink`, `Outlet`, `Navigate`, `useNavigate`, `useParams`, … so apps import their whole routing surface from the SDK.
319
+
320
+ **State (Zustand)** — `createStore` for any domain slice, `createSelectors` for per-field subscription hooks:
321
+
322
+ ```ts
323
+ import { createStore, createSelectors } from "tempest-react-sdk";
324
+
325
+ interface CartState {
326
+ items: string[];
327
+ add: (id: string) => void;
328
+ }
329
+
330
+ export const useCart = createSelectors(
331
+ createStore<CartState>(
332
+ (set) => ({ items: [], add: (id) => set((s) => ({ items: [...s.items, id] })) }),
333
+ { persist: { name: "cart", partialize: (s) => ({ items: s.items }) } },
334
+ ),
335
+ );
336
+ // const items = useCart.use.items(); // subscribes only to `items`
337
+ ```
338
+
339
+ **Provider composition** — `AppProviders` nests ErrorBoundary → Query → Theme → i18n in one block (Query + Theme on by default; i18n + ErrorBoundary opt-in):
340
+
341
+ ```tsx
342
+ import { AppProviders, AppRouter } from "tempest-react-sdk";
343
+ import { routes } from "@/routes";
344
+
345
+ export function App() {
346
+ return (
347
+ <AppProviders errorBoundary={{ fallback: <p>Something went wrong.</p> }}>
348
+ <AppRouter routes={routes} fallback={<p>Loading…</p>} />
349
+ </AppProviders>
350
+ );
351
+ }
352
+ ```
353
+
354
+ Per-topic guides (bilingual): **[Routing](https://mauriciobenjamin700.github.io/tempest-react-sdk/routing/)** · **[State](https://mauriciobenjamin700.github.io/tempest-react-sdk/state/)** · **[Providers](https://mauriciobenjamin700.github.io/tempest-react-sdk/app-providers/)** · **[Vite & alias](https://mauriciobenjamin700.github.io/tempest-react-sdk/vite-config/)**.
355
+
356
+ ---
357
+
187
358
  ## Architecture overview
188
359
 
189
360
  The SDK is a layered set of building blocks. Apps wire the layers together; the SDK never owns the app shell.
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ // create-tempest-app — ships inside tempest-react-sdk.
3
+ //
4
+ // npx create-tempest-app my-app → scaffold a brand-new project folder
5
+ // npx create-tempest-app . → scaffold into the current directory
6
+ // npx create-tempest-app → same as "." (merge into the current dir)
7
+ // npx create-tempest-app my-app --pwa → also wire installability + web-push
8
+ //
9
+ // In merge mode, files the user already has are left untouched and an existing
10
+ // package.json has the Tempest scripts/deps merged in (your name/version are
11
+ // preserved). The `--pwa` flag overlays the PWA template (manifest, service
12
+ // worker, push-subscribe wiring) on top of the base.
13
+ import { cp, mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
14
+ import { existsSync } from "node:fs";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const PKG_ROOT = resolve(__dirname, "..");
20
+ const TEMPLATE_DIR = join(PKG_ROOT, "template");
21
+ const TEMPLATE_PWA_DIR = join(PKG_ROOT, "template-pwa");
22
+
23
+ /** Files renamed on copy so they ship inside the npm tarball. */
24
+ const RENAME_ON_COPY = {
25
+ _gitignore: ".gitignore",
26
+ "_env.example": ".env.example",
27
+ "_prettierrc.json": ".prettierrc.json",
28
+ };
29
+
30
+ const c = {
31
+ reset: "\x1b[0m",
32
+ bold: "\x1b[1m",
33
+ dim: "\x1b[2m",
34
+ green: "\x1b[32m",
35
+ cyan: "\x1b[36m",
36
+ yellow: "\x1b[33m",
37
+ red: "\x1b[31m",
38
+ };
39
+
40
+ function isValidName(name) {
41
+ return /^[a-z0-9._-]+$/i.test(name) && !name.startsWith(".");
42
+ }
43
+
44
+ /** Final on-disk name for a template entry (dotfiles are unprefixed on copy). */
45
+ function finalName(entryName) {
46
+ return RENAME_ON_COPY[entryName] ?? entryName;
47
+ }
48
+
49
+ /** Recursively collect existing file paths under `dir`, relative to it. */
50
+ async function listFiles(dir, relBase = "", out = new Set()) {
51
+ if (!existsSync(dir)) return out;
52
+ const entries = await readdir(dir, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ const rel = join(relBase, entry.name);
55
+ if (entry.isDirectory()) {
56
+ await listFiles(join(dir, entry.name), rel, out);
57
+ } else {
58
+ out.add(rel);
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ async function isEmptyDir(dir) {
65
+ if (!existsSync(dir)) return true;
66
+ const entries = await readdir(dir);
67
+ return entries.filter((e) => e !== ".git").length === 0;
68
+ }
69
+
70
+ /** Read the SDK's own version so the generated app pins a matching range. */
71
+ async function readSdkVersion() {
72
+ try {
73
+ const pkg = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8"));
74
+ return pkg.version ?? "latest";
75
+ } catch {
76
+ return "latest";
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Copy a template tree into `destDir`. `package.json` is handled separately;
82
+ * any path already present in `protect` (the user's own files) is skipped and
83
+ * recorded. Everything else is overwritten, so a later overlay wins over the
84
+ * base it sits on.
85
+ */
86
+ async function copyTree(srcDir, destDir, protect, skipped, relBase = "") {
87
+ const entries = await readdir(srcDir, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ const rel = join(relBase, finalName(entry.name));
90
+ const from = join(srcDir, entry.name);
91
+ const to = join(destDir, finalName(entry.name));
92
+ if (entry.isDirectory()) {
93
+ await mkdir(to, { recursive: true });
94
+ await copyTree(from, to, protect, skipped, rel);
95
+ } else if (entry.name === "package.json") {
96
+ // handled separately by writeFreshPackageJson / mergePackageJson
97
+ } else if (protect.has(rel)) {
98
+ skipped.add(rel);
99
+ } else {
100
+ await cp(from, to);
101
+ }
102
+ }
103
+ }
104
+
105
+ /** Recursively rename underscore-prefixed dotfiles after a directory copy. */
106
+ async function fixDotfiles(dir) {
107
+ const entries = await readdir(dir, { withFileTypes: true });
108
+ for (const entry of entries) {
109
+ const full = join(dir, entry.name);
110
+ if (entry.isDirectory()) {
111
+ await fixDotfiles(full);
112
+ } else if (RENAME_ON_COPY[entry.name]) {
113
+ await rename(full, join(dir, RENAME_ON_COPY[entry.name]));
114
+ }
115
+ }
116
+ }
117
+
118
+ /** Stamp the SDK version into a fresh package.json. */
119
+ async function writeFreshPackageJson(destDir, name, sdkVersion) {
120
+ const pkg = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
121
+ pkg.name = name;
122
+ pkg.dependencies["tempest-react-sdk"] = `^${sdkVersion}`;
123
+ await writeFile(join(destDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
124
+ }
125
+
126
+ /** Merge Tempest scripts + deps into an existing package.json (non-destructive). */
127
+ async function mergePackageJson(destDir, sdkVersion) {
128
+ const tpl = JSON.parse(await readFile(join(TEMPLATE_DIR, "package.json"), "utf8"));
129
+ const target = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
130
+
131
+ target.type ??= "module";
132
+ target.scripts = { ...tpl.scripts, ...(target.scripts ?? {}) };
133
+ target.dependencies = { ...(target.dependencies ?? {}), ...tpl.dependencies };
134
+ target.dependencies["tempest-react-sdk"] = `^${sdkVersion}`;
135
+ target.devDependencies = { ...(target.devDependencies ?? {}), ...tpl.devDependencies };
136
+
137
+ await writeFile(join(destDir, "package.json"), JSON.stringify(target, null, 2) + "\n");
138
+ }
139
+
140
+ /**
141
+ * Fold the PWA package.json patch into the just-written one: PWA scripts win
142
+ * (e.g. `build` also bundles the service worker), deps/devDeps are added.
143
+ */
144
+ async function applyPwaPackageJson(destDir) {
145
+ const patch = JSON.parse(await readFile(join(TEMPLATE_PWA_DIR, "package.json"), "utf8"));
146
+ const target = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
147
+
148
+ target.scripts = { ...(target.scripts ?? {}), ...(patch.scripts ?? {}) };
149
+ target.dependencies = { ...(target.dependencies ?? {}), ...(patch.dependencies ?? {}) };
150
+ target.devDependencies = {
151
+ ...(target.devDependencies ?? {}),
152
+ ...(patch.devDependencies ?? {}),
153
+ };
154
+
155
+ await writeFile(join(destDir, "package.json"), JSON.stringify(target, null, 2) + "\n");
156
+ }
157
+
158
+ function parseArgs(argv) {
159
+ const rest = argv.slice(2);
160
+ const flags = new Set(rest.filter((a) => a.startsWith("--")));
161
+ const positionals = rest.filter((a) => !a.startsWith("--"));
162
+ return { name: positionals[0], pwa: flags.has("--pwa") };
163
+ }
164
+
165
+ async function main() {
166
+ console.log(`\n${c.bold}${c.cyan}create-tempest-app${c.reset}\n`);
167
+
168
+ if (!existsSync(TEMPLATE_DIR)) {
169
+ console.error(`${c.red}✗ Template not found at ${TEMPLATE_DIR}${c.reset}`);
170
+ process.exit(1);
171
+ }
172
+
173
+ const { name: arg, pwa } = parseArgs(process.argv);
174
+
175
+ if (pwa && !existsSync(TEMPLATE_PWA_DIR)) {
176
+ console.error(`${c.red}✗ PWA template not found at ${TEMPLATE_PWA_DIR}${c.reset}`);
177
+ process.exit(1);
178
+ }
179
+
180
+ const sdkVersion = await readSdkVersion();
181
+ const mergeMode = arg === "." || arg === undefined;
182
+
183
+ // Resolve destination + validate.
184
+ let destDir;
185
+ if (mergeMode) {
186
+ destDir = process.cwd();
187
+ console.log(`${c.dim}Scaffolding into the current directory…${c.reset}`);
188
+ } else {
189
+ if (!isValidName(arg)) {
190
+ console.error(`${c.red}✗ Invalid project name: "${arg}"${c.reset}`);
191
+ process.exit(1);
192
+ }
193
+ destDir = resolve(process.cwd(), arg);
194
+ if (!(await isEmptyDir(destDir))) {
195
+ console.error(`${c.red}✗ Directory "${arg}" exists and is not empty.${c.reset}`);
196
+ console.error(
197
+ `${c.dim} Use "create-tempest-app ." to merge into the current directory.${c.reset}`,
198
+ );
199
+ process.exit(1);
200
+ }
201
+ await mkdir(destDir, { recursive: true });
202
+ console.log(`${c.dim}Scaffolding into ${destDir}…${c.reset}`);
203
+ }
204
+ if (pwa) console.log(`${c.dim}PWA mode: installability + web-push wiring.${c.reset}`);
205
+
206
+ // Snapshot the user's own files so neither copy pass clobbers them.
207
+ const protect = await listFiles(destDir);
208
+ const hadPkg = protect.has("package.json");
209
+ const skipped = new Set();
210
+
211
+ await copyTree(TEMPLATE_DIR, destDir, protect, skipped);
212
+ if (pwa) await copyTree(TEMPLATE_PWA_DIR, destDir, protect, skipped);
213
+ await fixDotfiles(destDir);
214
+
215
+ // package.json: merge into the user's if present, otherwise write fresh.
216
+ if (hadPkg) {
217
+ await mergePackageJson(destDir, sdkVersion);
218
+ console.log(`${c.dim}Merged scripts + deps into existing package.json.${c.reset}`);
219
+ } else {
220
+ const name = mergeMode ? (destDir.split("/").pop() || "tempest-app").toLowerCase() : arg;
221
+ await writeFreshPackageJson(destDir, name, sdkVersion);
222
+ }
223
+ if (pwa) await applyPwaPackageJson(destDir);
224
+
225
+ if (skipped.size) {
226
+ console.log(`\n${c.yellow}Skipped ${skipped.size} existing file(s):${c.reset}`);
227
+ for (const f of skipped) console.log(` ${c.dim}· ${f}${c.reset}`);
228
+ }
229
+
230
+ console.log(`\n${c.green}✓ Done!${c.reset} Next steps:\n`);
231
+ if (!mergeMode) console.log(` ${c.bold}cd ${arg}${c.reset}`);
232
+ console.log(` ${c.bold}npm install${c.reset}`);
233
+ console.log(` ${c.bold}npm run dev${c.reset}`);
234
+ if (pwa) {
235
+ console.log(
236
+ `\n${c.dim}PWA: set VITE_VAPID_PUBLIC_KEY in .env, then test installability${c.reset}`,
237
+ );
238
+ console.log(
239
+ `${c.dim}and push with a production build: ${c.reset}${c.bold}npm run build && npm run preview${c.reset}`,
240
+ );
241
+ }
242
+ console.log();
243
+ }
244
+
245
+ main().catch((err) => {
246
+ console.error(`${c.red}✗ ${err instanceof Error ? err.message : String(err)}${c.reset}`);
247
+ process.exit(1);
248
+ });