tempest-react-sdk 0.7.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tempest-react-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "SDK público da Tempest com componentes, hooks e integrações para projetos React.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,11 +25,13 @@
25
25
  "dist",
26
26
  "bin",
27
27
  "template",
28
+ "template-pwa",
28
29
  "README.md",
29
30
  "LICENSE"
30
31
  ],
31
32
  "bin": {
32
- "create-tempest-app": "./bin/create-tempest-app.mjs"
33
+ "create-tempest-app": "./bin/create-tempest-app.mjs",
34
+ "tempest": "./bin/tempest.mjs"
33
35
  },
34
36
  "main": "./dist/tempest-react-sdk.cjs",
35
37
  "module": "./dist/tempest-react-sdk.js",
@@ -53,6 +55,11 @@
53
55
  "import": "./dist/vite.js",
54
56
  "require": "./dist/vite.cjs"
55
57
  },
58
+ "./sw": {
59
+ "types": "./dist/sw.d.ts",
60
+ "import": "./dist/sw.js",
61
+ "require": "./dist/sw.cjs"
62
+ },
56
63
  "./styles.css": "./dist/styles.css",
57
64
  "./package.json": "./package.json"
58
65
  },
@@ -68,6 +75,7 @@
68
75
  "test:coverage": "vitest run --coverage",
69
76
  "clean": "rm -rf dist coverage",
70
77
  "size": "size-limit",
78
+ "docs:llms": "node scripts/gen-llms.mjs",
71
79
  "prepare": "husky",
72
80
  "prepublishOnly": "npm run typecheck && npm run lint && npm run test:run && npm run build"
73
81
  },
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "tabWidth": 2,
5
+ "printWidth": 100,
6
+ "trailingComma": "all",
7
+ "arrowParens": "always",
8
+ "endOfLine": "lf"
9
+ }
@@ -2,6 +2,8 @@ import js from "@eslint/js";
2
2
  import globals from "globals";
3
3
  import reactHooks from "eslint-plugin-react-hooks";
4
4
  import reactRefresh from "eslint-plugin-react-refresh";
5
+ import simpleImportSort from "eslint-plugin-simple-import-sort";
6
+ import unusedImports from "eslint-plugin-unused-imports";
5
7
  import tseslint from "typescript-eslint";
6
8
 
7
9
  export default tseslint.config(
@@ -16,12 +18,27 @@ export default tseslint.config(
16
18
  plugins: {
17
19
  "react-hooks": reactHooks,
18
20
  "react-refresh": reactRefresh,
21
+ "simple-import-sort": simpleImportSort,
22
+ "unused-imports": unusedImports,
19
23
  },
20
24
  rules: {
21
25
  ...reactHooks.configs.recommended.rules,
22
26
  "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
23
27
  "@typescript-eslint/consistent-type-imports": "error",
24
- "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
28
+ // Organize imports/exports (tempest fix).
29
+ "simple-import-sort/imports": "error",
30
+ "simple-import-sort/exports": "error",
31
+ // Remove dead imports + flag unused vars (tempest fix).
32
+ "@typescript-eslint/no-unused-vars": "off",
33
+ "unused-imports/no-unused-imports": "error",
34
+ "unused-imports/no-unused-vars": [
35
+ "warn",
36
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
37
+ ],
38
+ // Tidy whitespace (tempest fix).
39
+ "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0, maxBOF: 0 }],
40
+ "no-trailing-spaces": "error",
41
+ "eol-last": ["error", "always"],
25
42
  },
26
43
  },
27
44
  );
@@ -9,7 +9,10 @@
9
9
  "preview": "vite preview",
10
10
  "typecheck": "tsc --noEmit",
11
11
  "lint": "eslint .",
12
- "lint:fix": "eslint . --fix"
12
+ "lint:fix": "eslint . --fix",
13
+ "format": "prettier --write .",
14
+ "fix": "tempest fix",
15
+ "doctor": "tempest doctor"
13
16
  },
14
17
  "dependencies": {
15
18
  "react": "^19.0.0",
@@ -24,7 +27,10 @@
24
27
  "eslint": "^9.36.0",
25
28
  "eslint-plugin-react-hooks": "^5.2.0",
26
29
  "eslint-plugin-react-refresh": "^0.4.22",
30
+ "eslint-plugin-simple-import-sort": "^12.1.0",
31
+ "eslint-plugin-unused-imports": "^4.1.4",
27
32
  "globals": "^16.0.0",
33
+ "prettier": "^3.8.0",
28
34
  "typescript": "~5.9.0",
29
35
  "typescript-eslint": "^8.45.0",
30
36
  "vite": "^7.0.0"
@@ -0,0 +1,64 @@
1
+ # Tempest App (PWA)
2
+
3
+ Scaffolded with [`create-tempest-app --pwa`](https://www.npmjs.com/package/tempest-react-sdk) and powered by [`tempest-react-sdk`](https://www.npmjs.com/package/tempest-react-sdk).
4
+
5
+ ## Stack
6
+
7
+ - **Vite** with the `@` → `src` alias (`tempest-react-sdk/vite` → `createViteConfig`)
8
+ - **React Router v7** declarative routing (`defineRoutes` + `<AppRouter>`)
9
+ - **Zustand** state (`createAuthStore` + `createSelectors`)
10
+ - **TanStack Query** cache (mounted by `<AppProviders>`)
11
+ - **PWA**: installable manifest + a service worker built from `tempest-react-sdk/sw`, with web-push wiring
12
+
13
+ ## Getting started
14
+
15
+ ```bash
16
+ npm install
17
+ cp .env.example .env # adjust VITE_API_URL + VITE_VAPID_PUBLIC_KEY
18
+ npm run dev # http://127.0.0.1:5173
19
+ ```
20
+
21
+ ## PWA layout
22
+
23
+ ```text
24
+ public/manifest.webmanifest # install metadata (name, icons, theme color)
25
+ public/icon.svg # app icon (replace with your brand; PNG 192/512 recommended)
26
+ src/sw.ts # service worker — push + notificationclick + skip-waiting
27
+ vite.sw.config.ts # bundles src/sw.ts → dist/sw.js (classic worker)
28
+ index.html # manifest link + theme-color + apple meta tags
29
+ src/main.tsx # registers /sw.js in production, cleans up SW in dev
30
+ src/pages/Dashboard.tsx # Install button + push notifications toggle
31
+ ```
32
+
33
+ ## How it works
34
+
35
+ - **Install**: `index.html` links the manifest; `useBeforeInstallPrompt` (in
36
+ `Dashboard.tsx`) surfaces a custom Install button when the browser offers one.
37
+ - **Service worker**: `src/sw.ts` imports `installPushHandler`,
38
+ `installNotificationClickHandler` and `installSkipWaitingListener` from
39
+ `tempest-react-sdk/sw`. `npm run build` bundles it to `dist/sw.js` via
40
+ `vite.sw.config.ts`, and `src/main.tsx` registers it in production.
41
+ - **Web push**: `usePushSubscription` reads `VITE_VAPID_PUBLIC_KEY`, subscribes
42
+ through the active service worker, and hands the subscription to your
43
+ `onSubscribe` callback to POST to your backend.
44
+
45
+ > ⚠️ The service worker is bundled at **build time**, so push and offline behave
46
+ > only in a production build. Test them with:
47
+ >
48
+ > ```bash
49
+ > npm run build && npm run preview
50
+ > ```
51
+ >
52
+ > In `npm run dev` the worker is intentionally unregistered to avoid stale caches.
53
+
54
+ ## Replace the icons
55
+
56
+ `public/icon.svg` is a placeholder. For guaranteed installability across all
57
+ browsers, add PNG icons (192×192 and 512×512) to `public/` and point the
58
+ `manifest.webmanifest` `icons` entries at them.
59
+
60
+ ## Next steps
61
+
62
+ - Add a route: drop a page in `src/pages/` and an entry in `src/routes.tsx`.
63
+ - Fetch data: `useQuery({ queryKey: queryKeys.me(), queryFn: () => api.get("/me") })`.
64
+ - Generate VAPID keys on your backend and set `VITE_VAPID_PUBLIC_KEY` in `.env`.
@@ -0,0 +1,7 @@
1
+ # Base URL of your backend API (consumed by src/lib/api.ts).
2
+ VITE_API_URL=http://127.0.0.1:8000
3
+
4
+ # VAPID public key for web push (consumed by usePushSubscription in
5
+ # src/pages/Dashboard.tsx). Generate a key pair on your backend; the private
6
+ # half stays on the server, this public half ships to the browser.
7
+ VITE_VAPID_PUBLIC_KEY=
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/icon.svg" />
6
+ <link rel="apple-touch-icon" href="/icon.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
8
+ <meta name="theme-color" content="#0b1a36" />
9
+ <meta name="description" content="App React fiado com tempest-react-sdk." />
10
+ <meta name="application-name" content="Tempest App" />
11
+ <meta name="apple-mobile-web-app-capable" content="yes" />
12
+ <meta name="apple-mobile-web-app-title" content="Tempest App" />
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
14
+ <meta name="mobile-web-app-capable" content="yes" />
15
+ <link rel="manifest" href="/manifest.webmanifest" />
16
+ <title>Tempest App</title>
17
+ </head>
18
+ <body>
19
+ <div id="root"></div>
20
+ <script type="module" src="/src/main.tsx"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "build": "tsc --noEmit && vite build && npm run build:sw",
4
+ "build:sw": "vite build --config vite.sw.config.ts"
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Tempest">
2
+ <rect width="512" height="512" fill="#0b1a36" />
3
+ <path d="M176 200h160l-30 50h-54v140l-46 24V250h-54z" fill="#ffffff" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Tempest">
2
+ <rect width="512" height="512" rx="96" fill="#0b1a36" />
3
+ <path d="M152 168h208l-40 64h-72v176l-64 32V232h-72z" fill="#ffffff" />
4
+ </svg>
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "Tempest App",
3
+ "short_name": "Tempest",
4
+ "description": "App React fiado com tempest-react-sdk.",
5
+ "id": "/",
6
+ "start_url": "/?source=pwa",
7
+ "scope": "/",
8
+ "display": "standalone",
9
+ "orientation": "portrait",
10
+ "lang": "pt-BR",
11
+ "dir": "ltr",
12
+ "background_color": "#ffffff",
13
+ "theme_color": "#0b1a36",
14
+ "categories": ["productivity", "utilities"],
15
+ "icons": [
16
+ {
17
+ "src": "/icon.svg",
18
+ "sizes": "192x192",
19
+ "type": "image/svg+xml",
20
+ "purpose": "any"
21
+ },
22
+ {
23
+ "src": "/icon.svg",
24
+ "sizes": "512x512",
25
+ "type": "image/svg+xml",
26
+ "purpose": "any"
27
+ },
28
+ {
29
+ "src": "/icon-maskable.svg",
30
+ "sizes": "512x512",
31
+ "type": "image/svg+xml",
32
+ "purpose": "maskable"
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,36 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { registerServiceWorker, skipWaiting } from "tempest-react-sdk";
4
+ import "tempest-react-sdk/styles.css";
5
+ import { App } from "@/App";
6
+
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ );
12
+
13
+ // The service worker is bundled to `/sw.js` at build time, so it only exists in
14
+ // production. In dev we proactively unregister any leftover worker to avoid a
15
+ // stale cache shadowing your `npm run dev` output.
16
+ if (import.meta.env.DEV && "serviceWorker" in navigator) {
17
+ void navigator.serviceWorker
18
+ .getRegistrations()
19
+ .then((regs) => Promise.all(regs.map((r) => r.unregister())))
20
+ .catch((err) => console.warn("[sw] dev cleanup failed", err));
21
+ }
22
+
23
+ if (import.meta.env.PROD) {
24
+ void registerServiceWorker({
25
+ url: "/sw.js",
26
+ onUpdate: (waiting) => {
27
+ // A new worker is ready. Prompt your users however you like; here we
28
+ // just activate it and reload so the next visit is up to date.
29
+ if (confirm("Nova versão disponível. Atualizar agora?")) {
30
+ skipWaiting(waiting);
31
+ window.location.reload();
32
+ }
33
+ },
34
+ onError: (err) => console.warn("[sw] registration failed", err),
35
+ });
36
+ }
@@ -0,0 +1,73 @@
1
+ import { Button, useBeforeInstallPrompt, usePushSubscription } from "tempest-react-sdk";
2
+ import { useAuth } from "@/stores/auth";
3
+
4
+ /**
5
+ * Protected page (see the `guard` in src/routes.tsx). Lazy-loaded, so it is
6
+ * code-split into its own chunk. Default export because `lazy` expects one.
7
+ *
8
+ * Adds two PWA controls:
9
+ * - Install button (`useBeforeInstallPrompt`) — appears only when the browser
10
+ * offers an install prompt and the app is not yet installed.
11
+ * - Push toggle (`usePushSubscription`) — subscribes/unsubscribes to web push.
12
+ * The service worker must be active, so this works in a production build
13
+ * (`npm run build && npm run preview`), not in `npm run dev`.
14
+ */
15
+ export default function Dashboard() {
16
+ const user = useAuth.use.user();
17
+ const install = useBeforeInstallPrompt();
18
+
19
+ const push = usePushSubscription({
20
+ vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY ?? "",
21
+ onSubscribe: async (subscription) => {
22
+ // Send the subscription to your backend so it can deliver pushes.
23
+ // await api.post("/webpush/subscribe", { body: subscription });
24
+ console.log("push subscription", subscription);
25
+ },
26
+ onUnsubscribe: async () => {
27
+ // await api.delete("/webpush/my");
28
+ console.log("push unsubscribed");
29
+ },
30
+ });
31
+
32
+ return (
33
+ <section>
34
+ <h1>Dashboard</h1>
35
+ <p>
36
+ Signed in as <strong>{user?.name}</strong> ({user?.email}).
37
+ </p>
38
+ <p>This route is only reachable while authenticated.</p>
39
+
40
+ <h2>PWA</h2>
41
+
42
+ {install.installed ? (
43
+ <p>✅ App installed.</p>
44
+ ) : install.installable ? (
45
+ <Button onClick={() => void install.prompt()}>Install app</Button>
46
+ ) : (
47
+ <p>Install prompt not available in this browser/context.</p>
48
+ )}
49
+
50
+ <h2>Notifications</h2>
51
+
52
+ {!push.supported ? (
53
+ <p>Web push is not supported here (needs HTTPS + an active service worker).</p>
54
+ ) : (
55
+ <>
56
+ <Button
57
+ onClick={() =>
58
+ void (push.subscribed ? push.unsubscribe() : push.subscribe())
59
+ }
60
+ disabled={push.loading}
61
+ >
62
+ {push.subscribed ? "Disable notifications" : "Enable notifications"}
63
+ </Button>
64
+ <p>
65
+ Permission: <strong>{push.permission}</strong> · Subscribed:{" "}
66
+ <strong>{String(push.subscribed)}</strong>
67
+ </p>
68
+ {push.error && <p style={{ color: "crimson" }}>{push.error.message}</p>}
69
+ </>
70
+ )}
71
+ </section>
72
+ );
73
+ }
@@ -0,0 +1,35 @@
1
+ /// <reference lib="webworker" />
2
+ import {
3
+ installNotificationClickHandler,
4
+ installPushHandler,
5
+ installSkipWaitingListener,
6
+ } from "tempest-react-sdk/sw";
7
+
8
+ /**
9
+ * Service worker. Bundled to `dist/sw.js` by `vite.sw.config.ts` (see the
10
+ * `build:sw` script) and registered from `src/main.tsx`.
11
+ *
12
+ * The three SDK helpers wire the boilerplate:
13
+ * - `installPushHandler` parses the `push` payload (JSON, plain-text fallback)
14
+ * and shows a notification.
15
+ * - `installNotificationClickHandler` focuses an open tab on click, or opens a
16
+ * new one at the payload's `url`.
17
+ * - `installSkipWaitingListener` activates a waiting worker when the page posts
18
+ * `{ type: "SKIP_WAITING" }` (paired with `registerServiceWorker`'s
19
+ * `onUpdate` in `main.tsx`).
20
+ */
21
+ declare const self: ServiceWorkerGlobalScope;
22
+
23
+ installPushHandler({
24
+ defaultTitle: "Notificação",
25
+ defaultIcon: "/icon.svg",
26
+ defaultBadge: "/icon.svg",
27
+ });
28
+
29
+ installNotificationClickHandler();
30
+ installSkipWaitingListener();
31
+
32
+ // Take control of open pages as soon as this worker activates.
33
+ self.addEventListener("activate", (event) => {
34
+ event.waitUntil(self.clients.claim());
35
+ });
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ /** Base URL of the backend API (src/lib/api.ts). */
5
+ readonly VITE_API_URL?: string;
6
+ /** VAPID public key for web push (src/pages/Dashboard.tsx). */
7
+ readonly VITE_VAPID_PUBLIC_KEY?: string;
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv;
12
+ }
@@ -0,0 +1,27 @@
1
+ import { resolve } from "node:path";
2
+ import { defineConfig } from "vite";
3
+
4
+ /**
5
+ * Standalone build that bundles `src/sw.ts` (and the `tempest-react-sdk/sw`
6
+ * helpers it imports) into a single classic service worker at `dist/sw.js`.
7
+ *
8
+ * Run after the app build via the `build:sw` script — `emptyOutDir: false`
9
+ * keeps the app's `dist/` intact. The IIFE format produces a worker with no
10
+ * `import`/`export` tokens, so it registers as a classic worker (no
11
+ * `type: "module"` needed).
12
+ */
13
+ export default defineConfig({
14
+ build: {
15
+ emptyOutDir: false,
16
+ sourcemap: true,
17
+ lib: {
18
+ entry: resolve(__dirname, "src/sw.ts"),
19
+ formats: ["iife"],
20
+ name: "sw",
21
+ fileName: () => "sw.js",
22
+ },
23
+ rollupOptions: {
24
+ output: { entryFileNames: "sw.js", inlineDynamicImports: true },
25
+ },
26
+ },
27
+ });