npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tempest-react-sdk

v0.5.1

Published

SDK público da Tempest com componentes, hooks e integrações para projetos React.

Readme

tempest-react-sdk

npm version CI License: MIT React 18 / 19 TypeScript Bundle size

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.).

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.


Table of contents


Recommended stack

Vite + React + TypeScript is the supported consumer stack. The SDK is built and tested against Vite 7 in library mode and assumes a Vite-style host app:

  • ESM-first module resolution (the package's exports field declares import / require conditions).
  • import.meta.env for env vars (the recipes use import.meta.env.VITE_API_URL, import.meta.env.VITE_VAPID_PUBLIC_KEY, etc.).
  • Native CSS Modules (the package's hashed tempest_* class names are emitted as CSS Modules under the hood and consumed via the global tempest-react-sdk/styles.css import).
  • Fast HMR — provider files (ThemeProvider, I18nProvider, etc.) opt into React Refresh.
  • First-class compatibility with the Vite plugin ecosystem (vite-plugin-pwa for service workers, vite-plugin-dts, vite-plugin-svgr, etc.).

To bootstrap a new app:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install tempest-react-sdk

The demo gallery in examples/gallery is itself a Vite app — use it as the reference project layout.

Other bundlers (Next.js app router, Webpack, Rspack, Parcel) may work — the package ships standard ESM + CJS + rolled-up .d.ts — but they are not exercised in CI, and Vite-specific features used in the recipes (import.meta.env, vite-plugin-pwa) will need their own equivalents. When in doubt, start with Vite.

Vite reference: https://vite.dev/guide/. React + TypeScript template: https://vite.dev/guide/#scaffolding-your-first-vite-project.


Install

npm install tempest-react-sdk

Via package.json:

{
  "dependencies": {
    "tempest-react-sdk": "^0.1.0"
  }
}

Requires React >=18 and Node >=20.19 to build.

Peer & bundled dependencies

Only react and react-dom are peer dependencies — those must come from the host app so a single React copy lives in the tree.

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.

| Package | Status | Used by | | ------------------------------------- | ------------------- | ------------------------------------------------------------------- | | react, react-dom (^18 \|\| ^19) | Peer (required) | Everything | | @tanstack/react-query (^5) | Direct dep (auto) | QueryProvider, createQueryKeys | | zod (^3.23 \|\| ^4) | Direct dep (auto) | parseResponse, validateForm, zodResolver, useZodForm | | zustand (^4 \|\| ^5) | Direct dep (auto) | createAuthStore | | dexie (^4.4) | Direct dep (auto) | createOfflineStore | | react-hook-form (^7.76) | Direct dep (auto) | zodResolver, useZodForm, masked inputs | | lucide-react (>=0.400) | Direct dep (auto) | Component icons (leftIcon/rightIcon on Input, Button, etc.) |

The minimum install is just:

npm install tempest-react-sdk react react-dom

Bundle impact: every bundled dep is externalised in the SDK's Rollup config, so the SDK's published bundle stays at ~104 KB ESM. Your app's bundler (Vite / webpack / Rspack) resolves these from node_modules and tree-shakes — if you never call createOfflineStore, Dexie never enters your final bundle.

Version conflicts: if your app already pins (say) [email protected], npm dedupes when the range is compatible. If ranges diverge you get two copies — pin a single version in your own package.json to force one, or open an issue if the SDK's range is too tight.

Adapters for external SDKs (@sentry/browser, posthog-js, @growthbook/growthbook, launchdarkly-js-client-sdk) are not bundled — install those only when you opt into the adapter. The caller passes the SDK instance to the factory.

CSS import

Import the base stylesheet once at the entry of your app (e.g. main.tsx / src/index.tsx):

import "tempest-react-sdk/styles.css";

This injects the design tokens (--tempest-primary, --tempest-radius-md, ...), a minimal CSS reset, and the per-component CSS Modules. Tokens live on :root and on [data-tempest-theme="dark"], so the app can override them globally or per subtree (see Theming).

The styles ship hashed under the tempest_ namespace — they do not collide with Tailwind, Stitches, Linaria, or app-level CSS Modules.


What's inside

Every module is re-exported from the package root — import { Button, useDebounce, createApiClient } from "tempest-react-sdk" always works.

| Module | Exports | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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 | | hooks | useDebounce, usePagination, useClientFilter, useMediaQuery, useOnline, useDocumentVisibility, useIntersectionObserver, useResizeObserver, useClipboard, useKeyboardShortcut, useBeforeInstallPrompt, useIdle, useGeolocation, useScrollLock, useFocusTrap, useStableCallback, useDeepMemo | | http | createApiClient, parseResponse, uploadWithProgress, retry, generateIdempotencyKey, usePoll, types: ApiClient, ApiClientConfig, ApiError, RequestOptions, RetryOptions, UploadProgressEvent, UploadWithProgressOptions, UsePollOptions, UsePollResult | | auth (peer: zustand) | createAuthStore, AuthGuard, decodeJWT, isJWTExpired, lazyWithRetry, createRefreshQueue, types: AuthState, CreateAuthStoreOptions, AuthGuardProps, DecodedJWT, LazyWithRetryOptions | | query (peer: @tanstack/react-query) | QueryProvider, createQueryKeys, STALE_TIME, CACHE_TIME, REFETCH_TIME | | forms (peer: zod, react-hook-form) | validateForm, zodResolver, useZodForm, validateCPF, validateCNPJ, formatCEP, formatCNPJ, unmask, CPFInput, CNPJInput, PhoneInput, CEPInput, MoneyInput, useViaCEP | | sse | createEventStream, useEventStream | | ws | createWebSocket, useWebSocket | | push | WebPushClient, WebPushUnsupportedError, WebPushPermissionDeniedError, usePushSubscription, urlBase64ToUint8Array, isPushSupported | | sw | registerServiceWorker, skipWaiting, unregisterAllServiceWorkers, installPushHandler, installNotificationClickHandler, installSkipWaitingListener | | audio | createAudioPlayer, playAudio, stopAudio, useAudio | | offline (peer: dexie) | createOfflineStore, types: OfflineStore, OfflineStoreConfig, ListOptions | | error-boundary | ErrorBoundary, useErrorHandler, types: ErrorBoundaryProps, ErrorBoundaryRenderProps | | theme | ThemeProvider, useTheme, getInitialTheme, themeInitScript, types: ThemeMode, ResolvedTheme | | i18n | createI18n, I18nProvider, useI18n, useTranslate, types: Catalog, Messages, I18n, InterpolationValues | | logger | createLogger, consoleSink, types: Logger, LogEntry, LogLevel, LoggerSink | | telemetry | TelemetryProvider, useTelemetry, consoleTelemetryAdapter, createSentryTelemetryAdapter, createPostHogTelemetryAdapter, types: TelemetryAdapter, TelemetryEvent, TelemetryUser, CreateSentryTelemetryAdapterOptions, SentryLike, CreatePostHogTelemetryAdapterOptions, PostHogLike | | feature-flags | FeatureFlagsProvider, useFeatureFlag, useFlagValue, createInMemoryFlags, createGrowthBookFeatureFlagsAdapter, createLaunchDarklyFeatureFlagsAdapter, types: FeatureFlagsAdapter, FlagValue, GrowthBookLike, LDClientLike | | share | share, isShareSupported, types: SharePayload, ShareResult | | utils | cn, formatCurrency, formatDate, formatDateTime, formatPhone, formatCPF, formatPercent, storage |

Full per-module docs in docs/ (one markdown per module + draw.io diagrams in docs/diagrams/).

A demo app exercising every module lives in examples/gallerycd examples/gallery && npm install && npm run dev.


Architecture overview

The SDK is a layered set of building blocks. Apps wire the layers together; the SDK never owns the app shell.

┌──────────────────────────────────────────────────────────────┐
│  App entry (main.tsx)                                        │
│  ├── import "tempest-react-sdk/styles.css"                   │
│  └── <ThemeProvider>                                         │
│        <I18nProvider>                                        │
│          <FeatureFlagsProvider>                              │
│            <TelemetryProvider>                               │
│              <QueryProvider>                                 │
│                <ToastProvider>                               │
│                  <ErrorBoundary>                             │
│                    <RouterProvider … />                      │
└─────────────────────────────┬────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
       ┌────────────┐  ┌────────────┐  ┌────────────┐
       │   Pages    │  │  Features  │  │  Layouts   │
       └─────┬──────┘  └─────┬──────┘  └─────┬──────┘
             │               │               │
             └───────┬───────┴───────┬───────┘
                     ▼               ▼
            ┌────────────────┐ ┌────────────────┐
            │ Components +   │ │  Hooks +       │
            │ Forms          │ │  Stores        │
            └───────┬────────┘ └───────┬────────┘
                    │                  │
                    └────────┬─────────┘
                             ▼
                ┌────────────────────────┐
                │  HTTP client + zod +   │
                │  SSE / WS / WebPush /  │
                │  Offline / Audio       │
                └────────────────────────┘
  • Presentation layer (components): purely visual; uncontrolled-or-controlled, accessible by default, themable via CSS tokens.
  • Behaviour layer (hooks, forms, auth, error-boundary): React-aware helpers that orchestrate state.
  • Transport layer (http, sse, ws, push, sw): everything that touches the network or the service worker.
  • Persistence layer (offline, auth persist, utils/storage, i18n persist): client-side storage abstractions.
  • Observability layer (telemetry, logger, error-boundary onError): everywhere the app reports on itself.

The SDK ships no App.tsx, no router, no global store. Each layer is opt-in.


Quickstart — wiring the app providers

A minimal main.tsx for an app using HTTP + Query + Auth + Toast + Theme + i18n:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import {
  ThemeProvider,
  I18nProvider,
  QueryProvider,
  ToastProvider,
  ErrorBoundary,
  ErrorState,
  createI18n,
  themeInitScript,
} from "tempest-react-sdk";

import "tempest-react-sdk/styles.css";
import { App } from "./App";

const i18n = createI18n({
  locale: "pt-BR",
  fallback: "en",
  messages: {
    "pt-BR": { hello: "Olá, {name}" },
    en: { hello: "Hello, {name}" },
  },
});

// No-flash dark mode script. Inject in <head> via index.html, OR call here:
document.head.insertAdjacentHTML("afterbegin", `<script>${themeInitScript()}</script>`);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ThemeProvider>
      <I18nProvider i18n={i18n}>
        <QueryProvider>
          <ToastProvider>
            <ErrorBoundary
              fallback={({ error, reset }) => (
                <ErrorState description={error.message} onRetry={reset} />
              )}
            >
              <BrowserRouter>
                <App />
              </BrowserRouter>
            </ErrorBoundary>
          </ToastProvider>
        </QueryProvider>
      </I18nProvider>
    </ThemeProvider>
  </StrictMode>,
);

You only wrap with what you use — every provider is independent. The example above is the maximal case.


Recipes

Each recipe is self-contained. Pick the ones you need.

HTTP client recipe

createApiClient returns a typed ApiClient instance with .get / .post / .put / .patch / .delete methods, a typed ApiError, automatic JSON serialization, AbortSignal support, and pluggable getToken / onUnauthorized hooks.

import { createApiClient } from "tempest-react-sdk";
import { useAuthStore } from "@/store/auth";

export const api = createApiClient({
    baseURL: import.meta.env.VITE_API_URL,
    getToken: () => useAuthStore.getState().token,
    onUnauthorized: () => useAuthStore.getState().logout(),
    withCredentials: true,
    defaultHeaders: { "X-App-Version": __APP_VERSION__ },
});

const user = await api.get<UserResponse>("/users/me");
await api.post("/orders", { body: { total: 100, items: [...] } });

Every method throws ApiError (status, body, url, code) on non-2xx responses. onUnauthorized is called automatically on 401, before the error is thrown — so you can refresh-and-retry or sign out as you choose.

Response parsing with zod recipe

The HTTP client returns untyped JSON (unknown). parseResponse validates against a zod schema and gives you a ZodError-aware failure message tied to the request.

import { createApiClient, parseResponse } from "tempest-react-sdk";
import { z } from "zod";

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

export async function getUser(id: string) {
  const raw = await api.get<unknown>(`/users/${id}`);
  return parseResponse(userSchema, raw, `GET /users/${id}`);
}

On validation failure parseResponse throws an Error whose message includes the request label and the zod issues — exactly the diagnostic you want during a wire-protocol drift.

Upload with progress recipe

fetch cannot report upload progress in browsers. uploadWithProgress falls back to XMLHttpRequest internally while keeping the same ApiError contract:

import { uploadWithProgress } from "tempest-react-sdk";

const formData = new FormData();
formData.append("file", file);
formData.append("alo_id", aloId);

const controller = new AbortController();

await uploadWithProgress<{ url: string }>({
  url: `${API}/uploads`,
  method: "POST",
  body: formData,
  withCredentials: true,
  getToken: () => useAuthStore.getState().token,
  signal: controller.signal,
  onProgress: ({ fraction, loaded, total }) => {
    if (fraction !== null) setProgress(Math.round(fraction * 100));
  },
});

// Cancel:
controller.abort();

fraction is null when total is unknown (chunked / unsized uploads).

Polling recipe

usePoll runs a callback on an interval, pauses while the tab is hidden, and exposes start/stop controls. Use it for "kind-of-realtime" data when you don't need a socket.

import { usePoll } from "tempest-react-sdk";

function ServerStatus() {
  const poll = usePoll({
    interval: 5_000,
    callback: async () => {
      const data = await api.get<Status>("/status");
      setStatus(data);
    },
    immediate: true,
    pauseWhenHidden: true,
  });

  return (
    <Button onClick={poll.running ? poll.stop : poll.start}>
      {poll.running ? "Pause" : "Resume"}
    </Button>
  );
}

Retry & idempotency recipe

import { retry, generateIdempotencyKey } from "tempest-react-sdk";

const idempotencyKey = generateIdempotencyKey();

const result = await retry(
  () =>
    api.post("/payments", {
      body: payload,
      headers: { "Idempotency-Key": idempotencyKey },
    }),
  {
    retries: 3,
    baseDelay: 400,
    shouldRetry: (error) => error instanceof Error && /status (5\d\d|429)/.test(error.message),
  },
);

generateIdempotencyKey returns a v4 UUID using crypto.randomUUID() when available, with a Math.random fallback for older runtimes.

Auth store recipe

createAuthStore<TUser>() returns a typed Zustand store with the persist middleware already wired. The app owns the user shape — the SDK only owns the state shape.

import { createAuthStore } from "tempest-react-sdk";

type SessionUser = { id: string; name: string; is_admin: boolean };

export const useAuthStore = createAuthStore<SessionUser>({
  name: "tempest-app-auth",
  storage: "local",
});

// Anywhere:
useAuthStore.getState().setSession({ user, token });
const isAuthed = useAuthStore((s) => s.isAuthenticated);
useAuthStore.getState().logout();

The store exposes user, token, isAuthenticated, setSession, setUser, setToken, and logout. isAuthenticated is derived from token and rehydrates correctly after page reload.

Route guard recipe

import { Navigate, Outlet } from "react-router-dom";
import { AuthGuard } from "tempest-react-sdk";
import { useAuthStore } from "@/store/auth";

export function ProtectedLayout() {
  const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
  return (
    <AuthGuard isAuthenticated={isAuthenticated} fallback={<Navigate to="/login" replace />}>
      <Outlet />
    </AuthGuard>
  );
}

AuthGuard is a pure render gate — no router coupling. Use any redirect mechanism (react-router, next/navigation, wouter, ...).

JWT helpers & refresh queue recipe

import { decodeJWT, isJWTExpired, createRefreshQueue } from "tempest-react-sdk";

const decoded = decodeJWT<{ sub: string; exp: number; role: string }>(token);
const expired = isJWTExpired(token, 30); // 30-second skew

// Coalesce concurrent refreshes so only one network call runs at a time:
const refresh = createRefreshQueue(async () => {
  const newToken = await api.post<{ token: string }>("/auth/refresh");
  useAuthStore.getState().setToken(newToken.token);
  return newToken.token;
});

// Two concurrent calls → one request, both get the same resolved value:
await Promise.all([refresh(), refresh()]);

Code-splitting with retry recipe

lazyWithRetry wraps React.lazy so that a stale chunk error retries with exponential backoff instead of crashing the page. A common cause is users on a tab with an old index.html after a deploy — the retry usually picks up the new bundle; a final location.reload() recovers from a stale index.html.

import { lazyWithRetry } from "tempest-react-sdk";

const Settings = lazyWithRetry(() => import("./Settings"), {
  retries: 3,
  initialDelay: 400,
  reloadOnFinalFailure: true,
});

<Route
  path="/settings"
  element={
    <Suspense fallback={<Spinner />}>
      <Settings />
    </Suspense>
  }
/>;

React Query recipe

import { QueryProvider, createQueryKeys, STALE_TIME } from "tempest-react-sdk";

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryProvider defaultOptions={{ queries: { staleTime: STALE_TIME.MEDIUM } }}>
      {children}
    </QueryProvider>
  );
}

STALE_TIME, CACHE_TIME, and REFETCH_TIME ship as named constants (SHORT, MEDIUM, LONG) so cache windows stay consistent across features.

Typed query-key factory:

import { createQueryKeys } from "tempest-react-sdk";

export const eventKeys = createQueryKeys("event", {
  all: ["all"] as const,
  list: (filters: { page: number; size: number }) => ["list", filters] as const,
  byId: (id: string) => [id] as const,
});

// eventKeys.list({ page: 1, size: 20 }) === ["event", "list", { page: 1, size: 20 }]
// eventKeys.byId("42") === ["event", "42"]

Form layout recipe

Form is a <form> wrapper with a built-in layout variant — pick stack for stacked fields (one per row), inline for a wrapping horizontal row, or grid for an N-column layout. Pair with FormSection (titled subgroup), FormRow (forces side-by-side inside a stacked form), and FormActions (footer button row).

Stacked (default) — one field per row:

import { Form, FormActions, Input, Button } from "tempest-react-sdk";

<Form layout="stack" gap={4} onSubmit={onSubmit}>
  <Input label="Nome" {...form.register("name")} />
  <Input label="Email" type="email" {...form.register("email")} />
  <Input label="Senha" type="password" {...form.register("password")} />
  <FormActions align="end">
    <Button type="submit">Criar conta</Button>
  </FormActions>
</Form>;

Grid — side-by-side columns:

import { Form, FormActions, Input, Button } from "tempest-react-sdk";

<Form layout="grid" columns={2} gap={4} onSubmit={onSubmit}>
  <Input label="Nome" {...register("name")} />
  <Input label="Sobrenome" {...register("last_name")} />
  <Input label="Email" type="email" {...register("email")} />
  <Input label="Telefone" {...register("phone")} />
  <FormActions align="end" style={{ gridColumn: "1 / -1" }}>
    <Button type="submit">Salvar</Button>
  </FormActions>
</Form>;

columns accepts a number (repeat(N, minmax(0, 1fr))) or a raw grid-template-columns string (e.g. "2fr 1fr").

Inline — search-style filter row:

import { Form, Input, Select, Button } from "tempest-react-sdk";

<Form layout="inline" gap={2} onSubmit={onSubmit}>
  <Input label="Buscar" placeholder="nome…" />
  <Select label="Status" options={statusOptions} />
  <Button type="submit">Filtrar</Button>
</Form>;

inline aligns children at flex-end and wraps — perfect for filter bars or short login forms.

Sections + grouped rows:

FormSection lets you nest layouts (e.g. a stacked form with a 3-column "Address" group inside):

import { Form, FormSection, FormRow, FormActions, Input, Button } from "tempest-react-sdk";

<Form layout="stack" gap={5}>
  <Input label="Email" {...register("email")} />

  <FormSection title="Endereço" description="Usado para entrega" layout="grid" columns={3} gap={3}>
    <Input label="CEP" {...register("cep")} />
    <Input label="Cidade" {...register("city")} />
    <Input label="UF" {...register("state")} />
    <Input label="Rua" style={{ gridColumn: "1 / -1" }} {...register("street")} />
  </FormSection>

  <FormRow>
    <Input label="Validade" placeholder="MM/AA" {...register("expiry")} />
    <Input label="CVV" {...register("cvv")} />
  </FormRow>

  <FormActions align="between">
    <Button variant="ghost" type="button" onClick={onCancel}>
      Cancelar
    </Button>
    <Button type="submit">Salvar</Button>
  </FormActions>
</Form>;

| Component | Default layout | What it does | | ------------- | ------------------------- | --------------------------------------------------------------------------------------------- | | Form | stack | <form> element + flex/grid container. onSubmit works as expected. | | FormSection | stack body | Titled subgroup with its own independent layout / columns / gap. | | FormRow | always horizontal | Forces a wrapping side-by-side row regardless of parent layout. Children share width equally. | | FormActions | horizontal, align="end" | Footer button row. align accepts start / center / end / between. |

Both Form and FormSection accept gap (number → multiple of 4px, or any CSS length string) and columns (number → repeat(N, minmax(0, 1fr)), or any grid-template-columns string).

Forms (zod) recipe

Three levels of integration — pick the one that fits the form complexity.

1. Standalone validation — independent of any form library:

import { validateForm } from "tempest-react-sdk";
import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

const result = validateForm(schema, formValues);
if (!result.success) {
  setErrors(result.errors); // { email: "...", password: "..." }
  return;
}
await login(result.data);

2. react-hook-form resolver — drop-in replacement for @hookform/resolvers/zod:

import { useForm } from "react-hook-form";
import { zodResolver } from "tempest-react-sdk";

const form = useForm<LoginForm>({ resolver: zodResolver(loginSchema) });

3. All-in-one hook — the schema infers the form type:

import { useZodForm } from "tempest-react-sdk";

function LoginForm() {
  const form = useZodForm(loginSchema, {
    defaultValues: { email: "", password: "" },
  });

  return (
    <form onSubmit={form.handleSubmit((data) => login(data))}>
      <Input {...form.register("email")} label="Email" />
      <Input {...form.register("password")} type="password" label="Senha" />
      <Button type="submit" loading={form.formState.isSubmitting}>
        Entrar
      </Button>
    </form>
  );
}

react-hook-form is an optional peer dep — only install when you use zodResolver or useZodForm.

BR validators & masked inputs recipe

Algorithmic validators (full check-digit math, rejects all-same-digit edge cases) plus masked inputs that play nicely with react-hook-form:

import { validateCPF, validateCNPJ, formatCEP, formatCNPJ, unmask } from "tempest-react-sdk";

validateCPF("000.000.000-00"); // false (all-same)
validateCPF("12345678909"); // true
validateCNPJ("11.222.333/0001-81"); // true
formatCEP("01001000"); // "01001-000"
unmask("(11) 99876-5432"); // "11998765432"
import { CPFInput, PhoneInput, CEPInput, CNPJInput, MoneyInput } from "tempest-react-sdk";
import { Controller, useForm } from "react-hook-form";

function CheckoutForm() {
  const { control, register } = useForm<Checkout>();
  return (
    <>
      <CPFInput {...register("cpf")} label="CPF" />
      <PhoneInput {...register("phone")} label="Telefone" />
      <CEPInput {...register("cep")} label="CEP" />
      <Controller
        name="total"
        control={control}
        render={({ field }) => <MoneyInput {...field} label="Total" currency="BRL" />}
      />
    </>
  );
}

MoneyInput exposes a numeric value to your form state while rendering a formatted string in the input.

ViaCEP lookup recipe

import { useViaCEP } from "tempest-react-sdk";

function AddressFields({ form }: { form: UseFormReturn<Address> }) {
  const cep = form.watch("cep");
  const { result, loading, error } = useViaCEP(cep);

  useEffect(() => {
    if (result) {
      form.setValue("street", result.logradouro);
      form.setValue("neighborhood", result.bairro);
      form.setValue("city", result.localidade);
      form.setValue("state", result.uf);
    }
  }, [result]);

  return <CEPInput {...form.register("cep")} loading={loading} error={error?.message} />;
}

useViaCEP debounces requests, caches by CEP, and ignores partial input (length < 8).

WebSocket recipe

Wrapper around WebSocket with exponential reconnect (up to 10 attempts), optional ping heartbeat, JSON parsing, and send that no-ops while the socket isn't open.

import { useWebSocket } from "tempest-react-sdk";

type ChatEvent = { type: "message"; user: string; text: string };

function Chat({ apiUrl, enabled }: { apiUrl: string; enabled: boolean }) {
  const ws = useWebSocket<ChatEvent>(`${apiUrl}/chat`, {
    enabled,
    pingInterval: 30_000,
    onMessage: ({ data }) => console.log(data),
  });

  return (
    <button disabled={ws.status !== "open"} onClick={() => ws.send(JSON.stringify({ text: "hi" }))}>
      Enviar
    </button>
  );
}

Imperative (outside React):

import { createWebSocket } from "tempest-react-sdk";

const socket = createWebSocket(`${apiUrl}/chat`, {
  pingInterval: 30_000,
  onMessage: ({ data }) => console.log(data),
});

socket.send("hello");
socket.close();

Server-Sent Events (SSE) recipe

Stream with exponential reconnect (up to 10 attempts), ping heartbeat, JSON parsing by default. For cookie-auth endpoints, pass withCredentials: true.

import { useEventStream } from "tempest-react-sdk";
import { useNotificationsStore } from "@/store/notifications";

type StreamEvent =
  | { type: "NOTIFY"; message: string }
  | { type: "PAYMENT-SUCCESS"; order_id: string }
  | { type: "PING" };

export function NotificationsListener({ apiUrl, enabled }: { apiUrl: string; enabled: boolean }) {
  const add = useNotificationsStore((s) => s.add);

  useEventStream<StreamEvent>(`${apiUrl}/notifications/stream`, {
    enabled,
    withCredentials: true,
    namedEvents: ["notification", "payment"],
    onMessage: ({ data }) => {
      if (data.type === "PING") return;
      add(data);
    },
  });

  return null;
}

Imperative form (e.g., outside React, in a worker, in tests):

import { createEventStream } from "tempest-react-sdk";

const stream = createEventStream(`${apiUrl}/notifications/stream`, {
  withCredentials: true,
  onMessage: ({ data }) => console.log(data),
  onError: (event) => console.warn("SSE error", event),
});

stream.close();

Web Push recipe

The SDK owns the browser-side flow: permission, pushManager.subscribe, reading the active subscription, and unsubscribing. The endpoint is your responsibility — you supply onSubscribe / onUnsubscribe callbacks that POST/DELETE to your API.

Pre-requisite: register the service worker before calling subscribe() (via vite-plugin-pwa, registerServiceWorker, or raw navigator.serviceWorker.register).

import { usePushSubscription, Button } from "tempest-react-sdk";
import { api } from "@/services/api";

export function PushToggle() {
  const push = usePushSubscription({
    vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,
    onSubscribe: (subscription) => api.post("/webpush/subscribe", { body: subscription }),
    onUnsubscribe: () => api.delete("/webpush/my"),
  });

  if (!push.supported) return <p>Push não suportado neste navegador.</p>;

  return (
    <Button
      loading={push.loading}
      variant={push.subscribed ? "danger" : "primary"}
      onClick={() => (push.subscribed ? push.unsubscribe() : push.subscribe())}
    >
      {push.subscribed ? "Desinscrever notificações" : "Receber notificações"}
    </Button>
  );
}

Imperative version:

import { WebPushClient } from "tempest-react-sdk";

const push = new WebPushClient({
  vapidPublicKey: VAPID_PUBLIC_KEY,
  onSubscribe: (sub) => api.post("/webpush/subscribe", { body: sub }),
  onUnsubscribe: () => api.delete("/webpush/my"),
});

await push.subscribe();
const active = await push.isSubscribed();
await push.unsubscribe();

Errors thrown: WebPushUnsupportedError, WebPushPermissionDeniedError. Anything else bubbles up as the underlying browser exception.

Service Worker helpers recipe

Main thread — register the SW, react to updates, expose a "skip waiting" hook:

import { registerServiceWorker, skipWaiting } from "tempest-react-sdk";

registerServiceWorker({
  url: "/sw.js",
  onUpdate: (waiting) => {
    if (confirm("Nova versão disponível. Recarregar?")) {
      skipWaiting(waiting);
      window.location.reload();
    }
  },
  onError: (err) => console.error("SW falhou", err),
});

Worker thread — inside sw.ts/sw.js, install handlers for push, notification click, and the skip-waiting message:

/// <reference lib="webworker" />
import {
  installPushHandler,
  installNotificationClickHandler,
  installSkipWaitingListener,
} from "tempest-react-sdk";

installSkipWaitingListener();

installPushHandler({
  defaultTitle: "Tempest",
  defaultIcon: "/icons/Logo.png",
  transform: (payload) => {
    if (payload.tag === "silent-ping") return null; // drop silently
    return payload;
  },
});

installNotificationClickHandler({
  focusOrOpenWindow: true,
  fallbackUrl: "/",
});

The SDK does not ship the worker file or the bundler config — pair it with vite-plugin-pwa (injectManifest) or a separately bundled worker.

Audio playback recipe

playAudio for one-shot sounds (notification chime, payment success), createAudioPlayer for isolated channels, useAudio for a per-component player.

import { playAudio } from "tempest-react-sdk";

await playAudio("/audio/plim.wav", { volume: 0.4 });
import { useAudio } from "tempest-react-sdk";

function NotifyBell() {
  const audio = useAudio();
  return <button onClick={() => audio.play("/audio/bell.wav")}>Notify</button>;
}

Browsers block autoplay until the user interacts with the page. playAudio resolves to null when blocked — the UI is expected to "unlock" on first click.

Offline storage (IndexedDB / Dexie) recipe

Owner-scoped store per domain. Persists SSE history, drafts, offline cache. dexie is an optional peernpm i dexie only when you import this module.

import { createOfflineStore } from "tempest-react-sdk";

type Notification = {
  message_id: string;
  owner_id: string;
  type: "NOTIFY" | "PAYMENT-SUCCESS";
  message: string;
  created_at: string;
  read: boolean;
};

export const notificationsStore = createOfflineStore<Notification, string>({
  databaseName: "TempestNotifications",
  version: 1,
  tableName: "notifications",
  indexes: "&message_id, owner_id, read, created_at",
  keyPath: "message_id",
  ownerField: "owner_id",
});

await notificationsStore.put(
  {
    /* … */
  } as Notification,
  "u1",
);
const items = await notificationsStore.list("u1", {
  orderBy: "created_at",
  reverse: true,
  limit: 50,
});
await notificationsStore.updateMany("u1", { read: true });
await notificationsStore.clear("u1");

API: put / bulkPut / get / list / update / updateMany / delete / clear / count. raw (Dexie table) and db (Dexie instance) are exposed for advanced queries.

Error boundary recipe

Renders a fallback (static element or render-prop), auto-resets via resetKeys, forwards errors to onError for telemetry. useErrorHandler re-throws async errors inside the nearest boundary.

import { ErrorBoundary, ErrorState } from "tempest-react-sdk";
import { useLocation } from "react-router-dom";

export function AppShell({ children }: { children: React.ReactNode }) {
  const location = useLocation();
  return (
    <ErrorBoundary
      resetKeys={[location.pathname]}
      onError={(err, info) => reportToSentry(err, info)}
      fallback={({ error, reset }) => <ErrorState description={error.message} onRetry={reset} />}
    >
      {children}
    </ErrorBoundary>
  );
}
import { useErrorHandler } from "tempest-react-sdk";

function Streamer() {
  const throwError = useErrorHandler();
  useEffect(() => {
    const stream = openSocket();
    stream.onerror = (err) => throwError(err);
    return () => stream.close();
  }, [throwError]);
  return null;
}

Toast notifications recipe

import { ToastProvider, useToast, Button } from "tempest-react-sdk";

// Wrap app once (already in the Quickstart):
<ToastProvider placement="top-right" duration={4000}>
  <App />
</ToastProvider>;

// Use anywhere:
function SaveButton() {
  const toast = useToast();
  return (
    <Button
      onClick={async () => {
        try {
          await save();
          toast.success("Alterações salvas");
        } catch (error) {
          toast.error(String(error));
        }
      }}
    >
      Salvar
    </Button>
  );
}

useToast() returns { show, success, info, warning, error, dismiss }show takes the full ToastOptions.

Modal & ConfirmDialog recipe

import { useState } from "react";
import { Modal, ConfirmDialog, Button } from "tempest-react-sdk";

function DeleteUser({ user }: { user: User }) {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button variant="danger" onClick={() => setOpen(true)}>
        Excluir
      </Button>
      <ConfirmDialog
        open={open}
        title="Excluir usuário"
        description={`Esta ação é permanente. Excluir ${user.name}?`}
        confirmLabel="Sim, excluir"
        cancelLabel="Cancelar"
        tone="danger"
        onConfirm={async () => {
          await deleteUser(user.id);
          setOpen(false);
        }}
        onCancel={() => setOpen(false)}
      />
    </>
  );
}

Modal is the lower-level primitive (focus trap + scroll lock + ESC to close + backdrop click). ConfirmDialog is the opinionated yes/no wrapper.

Tables & pagination recipe

import { Table, Pagination, usePagination } from "tempest-react-sdk";

const columns = [
  { key: "id", label: "ID", align: "right" as const },
  { key: "name", label: "Nome" },
  { key: "email", label: "Email" },
  {
    key: "actions",
    label: "",
    render: (row) => (
      <Button size="sm" onClick={() => edit(row.id)}>
        Editar
      </Button>
    ),
  },
];

function UsersList() {
  const { page, size, setPage, setSize } = usePagination({ initialSize: 20 });
  const { data } = useQuery({
    queryKey: userKeys.list({ page, size }),
    queryFn: () => api.get<Paginated<User>>(`/users?page=${page}&size=${size}`),
  });

  return (
    <>
      <Table columns={columns} rows={data?.items ?? []} loading={!data} />
      <Pagination
        page={page}
        pageSize={size}
        total={data?.total ?? 0}
        onPageChange={setPage}
        onPageSizeChange={setSize}
      />
    </>
  );
}

Layout primitives recipe

Stack, Grid, and Container are zero-dependency layout helpers (flex, grid, max-width).

import { Container, Stack, Grid, Card } from "tempest-react-sdk";

<Container size="lg">
  <Stack gap="md" direction="column">
    <h1>Dashboard</h1>
    <Grid columns={3} gap="md">
      <Card>Visitas</Card>
      <Card>Pedidos</Card>
      <Card>Receita</Card>
    </Grid>
  </Stack>
</Container>;

Virtual list recipe

Render thousands of rows without blowing the DOM. Built on top of useResizeObserver — supports dynamic row heights.

import { VirtualList } from "tempest-react-sdk";

<VirtualList
  items={messages}
  estimatedItemHeight={64}
  overscan={5}
  renderItem={(message) => <MessageRow key={message.id} message={message} />}
/>;

Theming (light / dark) recipe

import { ThemeProvider, useTheme } from "tempest-react-sdk";

<ThemeProvider defaultMode="system" persistKey="tempest-theme">
  <App />
</ThemeProvider>;

function ThemeToggle() {
  const { mode, setMode, resolved } = useTheme();
  return (
    <select value={mode} onChange={(e) => setMode(e.target.value as ThemeMode)}>
      <option value="light">Claro</option>
      <option value="dark">Escuro</option>
      <option value="system">Sistema ({resolved})</option>
    </select>
  );
}

ThemeProvider writes data-tempest-theme="dark" (or removes it) on the root element. To avoid the white flash on initial paint, inline themeInitScript() in <head> before the React bundle:

<script>
  __INIT_THEME__;
</script>
// build.ts
import { themeInitScript } from "tempest-react-sdk";
const html = template.replace("__INIT_THEME__", themeInitScript());

i18n recipe

Minimal in-house i18n (~1.5 KB gzip). Use this when you need light interpolation + a couple of locales; reach for i18next when you need plural rules, namespaces, or async loaders.

import { createI18n } from "tempest-react-sdk";

const i18n = createI18n({
  locale: "pt-BR",
  fallback: "en",
  messages: {
    "pt-BR": {
      greeting: "Olá, {name}",
      inbox: { empty: "Caixa vazia" },
    },
    en: {
      greeting: "Hello, {name}",
      inbox: { empty: "Empty inbox" },
    },
  },
  persistKey: "tempest-locale",
});
import { I18nProvider, useTranslate, useI18n } from "tempest-react-sdk";

<I18nProvider i18n={i18n}>
  <App />
</I18nProvider>;

function Header() {
  const t = useTranslate();
  const { locale, setLocale } = useI18n();
  return (
    <header>
      <span>{t("greeting", { name: "Mauricio" })}</span>
      <button onClick={() => setLocale(locale === "pt-BR" ? "en" : "pt-BR")}>
        {locale === "pt-BR" ? "EN" : "PT"}
      </button>
    </header>
  );
}

Feature flags recipe

FeatureFlagsProvider takes an adapter matching the FeatureFlagsAdapter interface (isEnabled, get, onChange?). Ship the InMemory adapter while you build, swap for GrowthBook / LaunchDarkly when you're ready.

import {
  FeatureFlagsProvider,
  useFeatureFlag,
  useFlagValue,
  createInMemoryFlags,
} from "tempest-react-sdk";

const flags = createInMemoryFlags({
  initial: { "new-checkout": true, "max-items": 10 },
});

<FeatureFlagsProvider adapter={flags}>
  <App />
</FeatureFlagsProvider>;

function CheckoutButton() {
  const isNew = useFeatureFlag("new-checkout");
  const maxItems = useFlagValue<number>("max-items", 5);
  return isNew ? <NewCheckout maxItems={maxItems} /> : <LegacyCheckout />;
}

GrowthBook adapter — wraps a GrowthBook instance. The app initialises GrowthBook (so it controls apiHost, clientKey, attributes, loadFeatures()), the adapter only routes lookups.

import { GrowthBook } from "@growthbook/growthbook";
import { FeatureFlagsProvider, createGrowthBookFeatureFlagsAdapter } from "tempest-react-sdk";

const gb = new GrowthBook({
    apiHost: import.meta.env.VITE_GROWTHBOOK_API_HOST,
    clientKey: import.meta.env.VITE_GROWTHBOOK_KEY,
    attributes: { id: userId },
});
await gb.loadFeatures();

const adapter = createGrowthBookFeatureFlagsAdapter({ growthbook: gb });

<FeatureFlagsProvider adapter={adapter}>
    <App />
</FeatureFlagsProvider>;

Mapping: isEnabled(key)growthbook.isOn(key); get(key, default)growthbook.getFeatureValue(key, default); onChange(listener)growthbook.setRenderer(...) (installed lazily on first subscription, multiplexes to all listeners).

LaunchDarkly adapter — wraps launchdarkly-js-client-sdk.

import * as LDClient from "launchdarkly-js-client-sdk";
import { FeatureFlagsProvider, createLaunchDarklyFeatureFlagsAdapter } from "tempest-react-sdk";

const client = LDClient.initialize(import.meta.env.VITE_LD_CLIENT_ID, {
    kind: "user",
    key: userId,
});
await client.waitUntilReady();

const adapter = createLaunchDarklyFeatureFlagsAdapter({ client });

<FeatureFlagsProvider adapter={adapter}>
    <App />
</FeatureFlagsProvider>;

Mapping: isEnabled(key)client.variation(key, default) === true; get(key, default)client.variation(key, default); onChange(listener)client.on("change", listener) + client.off on unsubscribe.

Neither @growthbook/growthbook nor launchdarkly-js-client-sdk is declared as a peer dep — the adapter only touches the instance you hand it. Install whichever you opt into: npm install @growthbook/growthbook or npm install launchdarkly-js-client-sdk.

The FeatureFlagsAdapter interface is intentionally tiny — any third-party SDK can be wrapped into an adapter in ~20 lines.

Telemetry recipe

TelemetryProvider accepts an adapter matching TelemetryAdapter (init?, identify, track, captureException, flush?). The bundled consoleTelemetryAdapter logs every event — useful for dev and tests.

import { TelemetryProvider, useTelemetry, consoleTelemetryAdapter } from "tempest-react-sdk";

<TelemetryProvider adapter={consoleTelemetryAdapter}>
  <App />
</TelemetryProvider>;

function CheckoutForm() {
  const telemetry = useTelemetry();
  return (
    <Button
      onClick={() => {
        telemetry?.track({ name: "checkout.completed", properties: { total: 100 } });
      }}
    >
      Pagar
    </Button>
  );
}

useTelemetry() returns null when no provider is mounted — call sites should optional-chain (telemetry?.track(...)).

Sentry adapter — wraps @sentry/browser so the SDK never depends on Sentry directly. The Sentry namespace is supplied by the caller; if the app already initialises Sentry at startup, just pass that instance.

import * as Sentry from "@sentry/browser";
import { createSentryTelemetryAdapter, TelemetryProvider } from "tempest-react-sdk";

const adapter = createSentryTelemetryAdapter({
    sentry: Sentry,
    initOptions: {
        dsn: import.meta.env.VITE_SENTRY_DSN,
        environment: import.meta.env.MODE,
        tracesSampleRate: 0.1,
    },
    flushTimeout: 2000,
    breadcrumbCategory: "app",
});

<TelemetryProvider adapter={adapter}>
    <App />
</TelemetryProvider>;

Mapping:

| TelemetryAdapter call | @sentry/browser API | | -------------------------------- | ------------------------------------------------------------------ | | init() | Sentry.init(initOptions) (only when initOptions is provided) | | identify(user) | Sentry.setUser({ id, email, username, ...traits }) | | track({ name, properties }) | Sentry.addBreadcrumb({ category, message, level: "info", data }) | | captureException(err, context) | Sentry.captureException(err, { extra: context }) | | flush() | Sentry.flush(flushTimeout) |

@sentry/browser is not declared as a peer dep — the adapter only ever touches the namespace you hand it, so apps that don't use Sentry never pay for it. Install Sentry yourself when you opt in: npm install @sentry/browser.

PostHog adapter — wraps posthog-js.

import posthog from "posthog-js";
import { createPostHogTelemetryAdapter, TelemetryProvider } from "tempest-react-sdk";

const adapter = createPostHogTelemetryAdapter({
    posthog,
    init: {
        apiKey: import.meta.env.VITE_POSTHOG_KEY,
        options: { api_host: "https://us.i.posthog.com" },
    },
});

<TelemetryProvider adapter={adapter}>
    <App />
</TelemetryProvider>;

Mapping:

| TelemetryAdapter call | posthog-js API | | ----------------------------- | ------------------------------------------------------------------------------------------------ | | init() | posthog.init(apiKey, options) (only when init is provided) | | identify({id, ...}) | posthog.identify(id, { email, name, ...traits }) | | identify(null) | posthog.reset() | | track({ name, properties }) | posthog.capture(name, properties) | | captureException(err, ctx) | posthog.captureException(err, ctx) when available, else posthog.capture("$exception", { … }) |

posthog-js is not declared as a peer dep — install only when you opt in: npm install posthog-js.

Concrete adapters for Datadog / Amplitude / others are part of the v0.2 roadmap — for now you can write one in ~20 lines following the Sentry / PostHog adapters as templates.

Logger recipe

Leveled logger with pluggable sinks:

import { createLogger, consoleSink } from "tempest-react-sdk";

export const log = createLogger({
  level: "info",
  sinks: [consoleSink({ pretty: true })],
  context: { app: "alofans", version: __APP_VERSION__ },
});

log.info("user.signed-in", { user_id: user.id });
log.error("payment.failed", { reason }, error);

A sink is any function (entry: LogEntry) => void — wire a sink that POSTs to your log ingestion endpoint, batches every 5 seconds, etc.

Web Share API recipe

Share via the OS share sheet on mobile and supported desktop browsers. Falls back gracefully when navigator.share is missing.

import { share, isShareSupported } from "tempest-react-sdk";

if (!isShareSupported()) {
  copyToClipboard(url);
  return;
}

const result = await share({ title: "Tempest", text: "Check this out", url });
if (result.shared) toast.success("Compartilhado");
if (result.cancelled) {
  /* user dismissed */
}
if (result.unsupported) {
  /* defensive — should not happen after isShareSupported */
}

Hooks catalogue recipe

| Hook | Purpose | | --------------------------------------------- | -------------------------------------------------------------------------------- | | useDebounce(value, delay) | Debounce a value (search bars, autosave). | | usePagination({ initialPage, initialSize }) | page/size/setPage/setSize triplet with bounds. | | useClientFilter(items, predicate) | Memoised in-memory filter. | | useMediaQuery(query) | Subscribe to a matchMedia query ((min-width: 1024px)). | | useOnline() | Returns true/false from navigator.onLine + online/offline events. | | useDocumentVisibility() | "visible" / "hidden", subscribing to visibilitychange. | | useIntersectionObserver(ref, opts) | Returns the latest IntersectionObserverEntry. | | useResizeObserver(ref) | Returns the latest width/height. | | useClipboard() | { copy(text), copied, error } with execCommand fallback. | | useKeyboardShortcut(shortcut, handler) | "Meta+K" / "Ctrl+Shift+P" patterns. | | useBeforeInstallPrompt() | Capture the PWA install prompt and trigger it later. | | useIdle(timeout) | true after timeout ms of no input. | | useGeolocation() | One-shot or watch position with permission state. | | useScrollLock(active) | Lock body scroll while a modal is open. | | useFocusTrap(ref, active) | Trap focus inside a container. | | useStableCallback(fn) | A useCallback that always points at the latest fn without churning identity. | | useDeepMemo(value) | useMemo with deep equality. |

Each hook is independently importable and tested — see docs/hooks.md for full signatures and edge cases.

Utility helpers recipe

import {
  cn,
  formatCurrency,
  formatDate,
  formatDateTime,
  formatPhone,
  formatCPF,
  formatPercent,
  storage,
} from "tempest-react-sdk";

cn("btn", isPrimary && "btn-primary", { "btn-disabled": disabled });
formatCurrency(1234.5, "BRL"); // "R$ 1.234,50"
formatDate("2026-05-17"); // "17/05/2026"
formatDateTime("2026-05-17T14:30Z"); // "17/05/2026 11:30"
formatPhone("11998765432"); // "(11) 99876-5432"
formatCPF("12345678909"); // "123.456.789-09"
formatPercent(0.1234, 1); // "12,3%"

storage.set("draft", { title: "..." });
const draft = storage.get<Draft>("draft");
storage.remove("draft");

storage is an SSR-safe wrapper over localStorage — every call is try/catch-protected and returns null when window is unavailable or quota is exceeded.


Theming reference

All tokens live on :root (light) and [data-tempest-theme="dark"] (dark). Override globally or in a subtree:

:root {
  --tempest-primary: #ff3366;
  --tempest-radius-md: 6px;
  --tempest-font-family: "Inter", system-ui, sans-serif;
}

[data-tempest-theme="dark"] {
  --tempest-bg-default: #0b0e14;
  --tempest-text-primary: #e6e6e6;
}

[data-tempest-theme="dark"] .my-card {
  /* per-subtree dark override */
}

Categories of tokens:

  • Color: --tempest-primary, --tempest-success, --tempest-warning, --tempest-danger, --tempest-info, --tempest-bg-*, --tempest-text-*, --tempest-border-*.
  • Radius: --tempest-radius-sm / -md / -lg / -full.
  • Shadow: --tempest-shadow-sm / -md / -lg.
  • Spacing: --tempest-space-1--tempest-space-8.
  • Typography: --tempest-font-family, --tempest-font-size-sm / -md / -lg, --tempest-line-height-base.

Tokens are stable public API — breaking changes always bump the SDK minor (or major, on rename).


Conventions

These conventions are enforced across the SDK source and are the same patterns the SDK encourages in consumer apps.

  • TypeScript strict — no any implicit, verbatimModuleSyntax, every export typed.
  • Double quotes everywhere — "foo" never 'foo'.
  • JSDoc in English on every public export (description + @example).
  • CSS Modules with the tempest_ prefix — no