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

@sito/dashboard-app

v0.0.73

Published

UI Library with prefab components

Downloads

1,854

Readme

@sito/dashboard-app

@sito/dashboard-app is a React 18 component and utilities library for building Sito-style admin dashboards, CRUD screens, and internal tools. It packages UI components, hooks, providers, typed API helpers, and styles in a single npm package.

Documentation scope and source of truth

Use documentation by target package:

| Document | Primary audience | Source of truth for | | ----------------------- | ----------------------------- | ------------------------------------------------------- | | README.md (this file) | Consumer apps and maintainers | Public usage of @sito/dashboard-app | | AGENTS.md | AI agents and maintainers | Implementation rules for @sito/dashboard-app | | .sito/*.md | Internal team and agents | Upstream reference notes for @sito/dashboard behavior |

Important:

  • .sito/*.md is not the canonical integration guide for this package.
  • @sito/dashboard-app is not SSR-compatible. Treat it as browser-only (client-rendered apps).
  • For auth-enabled apps, use ConfigProvider -> ManagerProvider -> AuthProvider -> NotificationProvider -> DrawerMenuProvider (NavbarProvider when needed; BottomNavActionProvider optional for dynamic mobile center actions).
  • Drawer and Onboarding can run without AuthProvider; in that case they behave as guest-mode UI defaults.
  • IconButton differs by package:
    • @sito/dashboard: icon accepts a React node.
    • @sito/dashboard-app: icon expects IconDefinition (FontAwesome wrapper export).

Installation

npm install @sito/dashboard-app
# or
yarn add @sito/dashboard-app
# or
pnpm add @sito/dashboard-app

Requirements

  • Node.js 20.x (see .nvmrc)
  • Browser runtime only (no SSR/server rendering for this package)
  • React 18.3.1
  • React DOM 18.3.1
  • @tanstack/react-query 5.83.0
  • @supabase/supabase-js 2.100.0 (optional; only if using Supabase backend)
  • react-hook-form 7.61.1
  • @sito/dashboard ^0.0.82
  • Font Awesome peers defined in package.json

Install all peers in consumer apps:

npm install \
  [email protected] [email protected] \
  @sito/dashboard@^0.0.82 \
  @tanstack/[email protected] \
  [email protected] \
  @fortawesome/[email protected] \
  @fortawesome/[email protected] \
  @fortawesome/[email protected] \
  @fortawesome/[email protected] \
  @fortawesome/[email protected]

If your app uses the Supabase backend:

npm install @supabase/[email protected]

Core exports

  • Layout and navigation: Page, Navbar, Drawer, BottomNavigation, TabsLayout, PrettyGrid, ToTop
  • Actions and menus: Actions, Action, Dropdown, button components
  • Dialogs and forms: Dialog, FormDialog, ImportDialog, form inputs
  • Feedback: Notification, Loading, Empty, Error, Onboarding
  • Hooks: useFormDialog (generic state/entity), usePostDialog, usePutDialog, useImportDialog, useDeleteDialog, useMutationForm (usePostForm deprecated alias), useDeleteAction, useNavbar, and more — all action hooks ship with default sticky, multiple, id, icon, and tooltip values so only onClick is required
  • Providers and utilities: ConfigProvider, ManagerProvider, SupabaseManagerProvider, AuthProvider, SupabaseAuthProvider, NotificationProvider, DrawerMenuProvider, NavbarProvider, BottomNavActionProvider, useBottomNavAction, useOptionalBottomNavAction, useRegisterBottomNavAction, DTOs, API clients (BaseClient, IndexedDBClient, SupabaseDataClient), and useSupabase

Component usage patterns

Error component

Error supports two modes:

  • Default mode: icon + message + retry (uses Button internally)
  • Custom mode: pass children for fully custom content
import { Error } from "@sito/dashboard-app";

<Error
  error={error}
  onRetry={() => refetch()}
  retryLabel="Retry"
/>

<Error>
  <CustomErrorPanel />
</Error>

Do not combine default props (error, message, onRetry, etc.) with children in the same instance.

TabsLayout link mode

TabsLayout renders route links by default (useLinks = true). If your tabs are local UI state and should not navigate, set useLinks={false}.

import { TabsLayout } from "@sito/dashboard-app";

<TabsLayout
  useLinks={false}
  tabButtonProps={{ variant: "outlined", color: "secondary" }}
  tabs={tabs}
/>;

tabButtonProps lets you customize each tab button style/behavior (except onClick and children, which are controlled by TabsLayout).

Onboarding

Onboarding accepts structured steps instead of translation keys. Each step provides required title and body, plus optional content, image, and alt. Step copy is provided by the consumer app, so any i18n for the step itself should be resolved before rendering Onboarding.

import { Onboarding } from "@sito/dashboard-app";

<Onboarding
  steps={[
    {
      title: "Welcome",
      body: "This flow explains the main features.",
    },
    {
      title: "Almost done",
      body: "Add custom content when a step needs extra UI.",
      content: <MyStepContent />,
      image: "/images/setup.png",
      alt: "Setup preview",
    },
  ]}
/>;

The action buttons still use the package's internal accessibility/button translation keys.

ImportDialog custom preview

ImportDialog supports optional custom preview rendering via renderCustomPreview. If provided, it receives current parsed previewItems and replaces the default JSON preview. If omitted, the default preview remains unchanged.

import { ImportDialog } from "@sito/dashboard-app";

<ImportDialog<ProductImportPreviewDto>
  open={open}
  title="Import products"
  handleClose={close}
  handleSubmit={submit}
  fileProcessor={parseFile}
  renderCustomPreview={(items) => <ProductsPreviewTable items={items ?? []} />}
/>;

useImportDialog also accepts and forwards renderCustomPreview:

import { useImportDialog } from "@sito/dashboard-app";

const importDialog = useImportDialog<ProductDto, ProductImportPreviewDto>({
  queryKey: ["products"],
  entity: "products",
  mutationFn: api.products.import,
  fileProcessor: parseFile,
  renderCustomPreview: (items) => <ProductsPreviewTable items={items ?? []} />,
});

Dialog extra actions

ConfirmationDialog, FormDialog, and ImportDialog support optional extraActions. Use this when you need secondary actions in the dialog footer (for example, "Save draft", "Help", or "Download template").

import { ConfirmationDialog, type ButtonPropsType } from "@sito/dashboard-app";

const extraActions: ButtonPropsType[] = [
  {
    id: "help-action",
    type: "button",
    variant: "outlined",
    color: "secondary",
    children: "Help",
    onClick: () => openHelpPanel(),
  },
];

<ConfirmationDialog
  open={open}
  title="Confirm delete"
  handleClose={close}
  handleSubmit={confirm}
  extraActions={extraActions}
/>;

For FormDialog, set type: "button" on extra actions unless you explicitly want submit behavior.

PrettyGrid infinite scroll

PrettyGrid supports optional infinite loading with IntersectionObserver. Current usage without infinite props keeps the same behavior.

import { PrettyGrid, Loading } from "@sito/dashboard-app";

<PrettyGrid<ProductDto>
  data={products}
  loading={isLoading}
  renderComponent={(item) => <ProductCard item={item} />}
  hasMore={hasMore}
  loadingMore={isFetchingNextPage}
  onLoadMore={fetchNextPage}
  loadMoreComponent={<Loading className="!w-auto" loaderClass="w-5 h-5" />}
/>;

Defaults:

  • hasMore = false
  • loadingMore = false
  • loadMoreComponent = null
  • observerRootMargin = "0px 0px 200px 0px"
  • observerThreshold = 0

ToTop customization

ToTop is now customizable while preserving current defaults.

import { ToTop } from "@sito/dashboard-app";

<ToTop
  threshold={120}
  tooltip="Back to top"
  variant="outlined"
  color="secondary"
  className="right-8 bottom-8"
  scrollTop={0}
  scrollLeft={0}
/>;

Main optional props:

  • threshold?: number (default 200)
  • scrollTop?: number / scrollLeft?: number (default 0 / 0)
  • icon?: IconDefinition
  • tooltip?: string
  • scrollOnClick?: boolean (default true)
  • onClick?: () => void

BottomNavigation

BottomNavigation is a mobile-first fixed navigation bar intended for compact app shells. It is router-agnostic and uses ConfigProvider primitives (location, navigate, linkComponent).

import {
  BottomNavigation,
  type BottomNavigationItemType,
} from "@sito/dashboard-app";
import {
  faBox,
  faHome,
  faPlus,
  faUser,
} from "@fortawesome/free-solid-svg-icons";

type BottomNavId = "home" | "products" | "profile";

const bottomItems: BottomNavigationItemType<BottomNavId>[] = [
  { id: "home", label: "Home", to: "/", icon: faHome, position: "left" },
  {
    id: "products",
    label: "Products",
    to: "/products",
    icon: faBox,
    position: "left",
  },
  {
    id: "profile",
    label: "Profile",
    to: "/profile",
    icon: faUser,
    position: "right",
  },
];

<BottomNavigation
  items={bottomItems}
  centerAction={{
    icon: faPlus,
    to: "/products/new",
    ariaLabel: "Create product",
  }}
/>;

Dynamic center action with provider (optional):

import {
  BottomNavActionProvider,
  BottomNavigation,
  useRegisterBottomNavAction,
  type BottomNavigationItemType,
} from "@sito/dashboard-app";
import { faTags } from "@fortawesome/free-solid-svg-icons";

function ProductsCenterAction() {
  useRegisterBottomNavAction({
    icon: faTags,
    ariaLabel: "Create category",
    to: "/categories/new",
    color: "secondary",
  });
  return null;
}

<BottomNavActionProvider>
  <ProductsCenterAction />
  <BottomNavigation
    items={bottomItems}
    centerAction={{ to: "/products/new" }}
  />
</BottomNavActionProvider>;

Key notes:

  • Use position: "right" to place items in the right group; omitted/"left" stays on the left group.
  • Use isItemActive={(pathname, item) => ...} for custom active matching (default uses path-prefix matching, with exact match for /).
  • Use hidden/disabled on each item and centerAction.hidden for conditional rendering.
  • centerAction supports IconButton visual props and optional to; navigation runs after onClick unless event.preventDefault() is called.
  • BottomNavActionProvider is optional. When mounted, useRegisterBottomNavAction can override center-action fields dynamically from active page scope; registered fields take precedence over static centerAction props.

Dialog hook migration (v0.0.54)

v0.0.54 removes the legacy entity-coupled useFormDialog contract.

Breaking changes

  • useFormDialog is now core lifecycle only (mode: "state" | "entity").
  • Legacy props were removed from useFormDialog: mutationFn, queryKey, getFunction, dtoToForm, formToDto.
  • Deprecated aliases were removed:
    • useFormDialogLegacy
    • useEntityFormDialog

What to use now

  • Local/state-only dialog (filters/settings): useFormDialog
  • Create flow (POST): usePostDialog
  • Edit flow (PUT + get by id): usePutDialog

Before -> After

// BEFORE (no longer supported in v0.0.54+)
const createDialog = useFormDialog<
  ProductDto,
  CreateProductDto,
  ProductDto,
  ProductForm
>({
  title: "Create product",
  defaultValues: { name: "", price: 0 },
  mutationFn: (dto) => api.products.insert(dto),
  formToDto: (form) => ({ name: form.name, price: form.price }),
  queryKey: ["products"],
});

// AFTER
const createDialog = usePostDialog<CreateProductDto, ProductDto, ProductForm>({
  title: "Create product",
  defaultValues: { name: "", price: 0 },
  mutationFn: (dto) => api.products.insert(dto),
  formToDto: (form) => ({ name: form.name, price: form.price }),
  queryKey: ["products"],
});
// BEFORE (no longer supported in v0.0.54+)
const editDialog = useFormDialog<
  ProductDto,
  UpdateProductDto,
  ProductDto,
  ProductForm
>({
  title: "Edit product",
  defaultValues: { name: "", price: 0 },
  getFunction: (id) => api.products.getById(id),
  dtoToForm: (dto) => ({ name: dto.name, price: dto.price }),
  mutationFn: (dto) => api.products.update(dto),
  formToDto: (form) => ({ id: 0, ...form }),
  queryKey: ["products"],
});

// AFTER
const editDialog = usePutDialog<
  ProductDto,
  UpdateProductDto,
  ProductDto,
  ProductForm
>({
  title: "Edit product",
  defaultValues: { name: "", price: 0 },
  getFunction: (id) => api.products.getById(id),
  dtoToForm: (dto) => ({ name: dto.name, price: dto.price }),
  mutationFn: (dto) => api.products.update(dto),
  formToDto: (form, dto) => ({ id: dto?.id ?? 0, ...form }),
  queryKey: ["products"],
});

Core useFormDialog error handling

useFormDialog supports a core onError callback for failures in submit/apply/clear paths.

const filtersDialog = useFormDialog<ProductFilters>({
  mode: "state",
  title: "Filters",
  defaultValues: { search: "", minPrice: 0 },
  onSubmit: async (values) => setTableFilters(values),
  onError: (error, { phase, values }) => {
    console.error("Dialog error", { error, phase, values });
  },
});

Opening useFormDialog with values

openDialog now supports these signatures:

  • openDialog()
  • openDialog(id: number)
  • openDialog({ id?: number, values?: DefaultValues<TFormType> })

Use values when you want to hydrate the form at open-time, for example re-opening with the last submitted filters:

const [lastSubmittedFilters, setLastSubmittedFilters] =
  useState<ProductFilters>({
    search: "",
    minPrice: 0,
  });

const filtersDialog = useFormDialog<ProductFilters>({
  mode: "state",
  title: "Filters",
  defaultValues: { search: "", minPrice: 0 },
  onSubmit: (values) => {
    setLastSubmittedFilters(values);
    setTableFilters(values);
  },
});

const reopenWithLastSubmitted = () => {
  filtersDialog.openDialog({ values: lastSubmittedFilters });
};

If both openDialog({ values }) and reinitializeOnOpen/dtoToForm are configured, explicit values passed to openDialog take priority for that opening.

Storybook reference: Hooks/Dialogs/FormDialogs includes StateModeSetValuesOnOpen and StateModeReopenWithSubmittedValues scenarios.

Initial setup example

Wrap your app with providers in this order to enable routing integration, React Query, auth, notifications, and drawer/navbar state. ManagerProvider mounts QueryClientProvider internally and creates an isolated default QueryClient per provider instance. If you need custom React Query defaults or want to share a client intentionally, pass your own client with queryClient={queryClient}.

import type { ReactNode } from "react";
import {
  BrowserRouter,
  Link,
  useLocation,
  useNavigate,
} from "react-router-dom";
import {
  AuthProvider,
  BottomNavActionProvider,
  ConfigProvider,
  DrawerMenuProvider,
  IManager,
  ManagerProvider,
  NavbarProvider,
  NotificationProvider,
} from "@sito/dashboard-app";

const authStorageKeys = {
  user: "user",
  remember: "remember",
  refreshTokenKey: "refreshToken",
  accessTokenExpiresAtKey: "accessTokenExpiresAt",
};

const manager = new IManager(
  import.meta.env.VITE_API_URL,
  authStorageKeys.user,
  {
    rememberKey: authStorageKeys.remember,
    refreshTokenKey: authStorageKeys.refreshTokenKey,
    accessTokenExpiresAtKey: authStorageKeys.accessTokenExpiresAtKey,
  },
);

function AppProviders({ children }: { children: ReactNode }) {
  const go = useNavigate();
  const location = useLocation();

  return (
    <ConfigProvider
      location={location}
      navigate={(route) => {
        if (typeof route === "number") go(route);
        else go(route);
      }}
      linkComponent={Link}
    >
      <ManagerProvider manager={manager}>
        <AuthProvider
          user={authStorageKeys.user}
          remember={authStorageKeys.remember}
          refreshTokenKey={authStorageKeys.refreshTokenKey}
          accessTokenExpiresAtKey={authStorageKeys.accessTokenExpiresAtKey}
        >
          <NotificationProvider>
            <DrawerMenuProvider>
              <NavbarProvider>
                {/* Optional: only when pages register dynamic BottomNavigation center actions */}
                <BottomNavActionProvider>{children}</BottomNavActionProvider>
              </NavbarProvider>
            </DrawerMenuProvider>
          </NotificationProvider>
        </AuthProvider>
      </ManagerProvider>
    </ConfigProvider>
  );
}

export function App() {
  return (
    <BrowserRouter>
      <AppProviders>{/* Your app routes/pages */}</AppProviders>
    </BrowserRouter>
  );
}

Notes:

  • Keep ManagerProvider above AuthProvider.
  • Drawer and Onboarding are auth-optional wrappers. Without AuthProvider, Drawer treats the session as logged-out and Onboarding skips setGuestMode.
  • NavbarProvider is required when using Navbar or useNavbar; otherwise it can be omitted.
  • BottomNavActionProvider is optional; mount it around your app shell when pages/components use useRegisterBottomNavAction.
  • If you customize auth storage keys in AuthProvider, pass the same keys to IManager/BaseClient auth config.

Supabase setup (optional backend)

The library does not read .env values directly. The consumer app must create the Supabase client and pass it to SupabaseManagerProvider.

Use frontend-safe keys only:

  • VITE_SUPABASE_URL
  • VITE_SUPABASE_ANON_KEY

Do not expose service-role keys in the browser.

import type { ReactNode } from "react";
import { createClient } from "@supabase/supabase-js";
import { Link } from "react-router-dom";
import {
  BottomNavActionProvider,
  ConfigProvider,
  SupabaseManagerProvider,
  SupabaseAuthProvider,
  NotificationProvider,
  DrawerMenuProvider,
  NavbarProvider,
} from "@sito/dashboard-app";

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY,
);

function AppProviders({ children }: { children: ReactNode }) {
  return (
    <ConfigProvider
      location={window.location}
      navigate={() => {}}
      linkComponent={Link}
    >
      <SupabaseManagerProvider supabase={supabase}>
        <SupabaseAuthProvider>
          <NotificationProvider>
            <DrawerMenuProvider>
              <NavbarProvider>
                {/* Optional: only when pages register dynamic BottomNavigation center actions */}
                <BottomNavActionProvider>{children}</BottomNavActionProvider>
              </NavbarProvider>
            </DrawerMenuProvider>
          </NotificationProvider>
        </SupabaseAuthProvider>
      </SupabaseManagerProvider>
    </ConfigProvider>
  );
}

useAuth keeps the same contract with SupabaseAuthProvider (account, logUser, logoutUser, logUserFromLocal, isInGuestMode, setGuestMode).

SupabaseDataClient follows the same generic surface as BaseClient and IndexedDBClient, so entity clients can switch backend with minimal UI/hook changes. It also supports optional configuration for conventional columns: idColumn (default "id"), deletedAtColumn (default "deletedAt"), and defaultSortColumn.

Supabase entity client example

import type { SupabaseClient } from "@supabase/supabase-js";
import {
  BaseCommonEntityDto,
  BaseEntityDto,
  BaseFilterDto,
  DeleteDto,
  ImportPreviewDto,
  SupabaseDataClient,
} from "@sito/dashboard-app";

interface ProductDto extends BaseEntityDto {
  name: string;
  price: number;
  categoryId?: number;
}

interface ProductCommonDto extends BaseCommonEntityDto {
  name: string;
}

interface CreateProductDto {
  name: string;
  price: number;
  categoryId?: number;
}

interface UpdateProductDto extends DeleteDto {
  name?: string;
  price?: number;
  categoryId?: number;
}

interface ProductFilterDto extends BaseFilterDto {
  categoryId?: number;
}

interface ProductImportPreviewDto extends ImportPreviewDto {
  id: number;
  name: string;
  price: number;
}

class ProductsSupabaseClient extends SupabaseDataClient<
  "products",
  ProductDto,
  ProductCommonDto,
  CreateProductDto,
  UpdateProductDto,
  ProductFilterDto,
  ProductImportPreviewDto
> {
  constructor(supabase: SupabaseClient) {
    super("products", supabase, {
      defaultSortColumn: "id",
    });
  }
}

const productsClient = new ProductsSupabaseClient(supabase);

Compatibility and incremental migration

  • The REST flow stays intact: existing apps using ManagerProvider + AuthProvider + BaseClient do not need changes.
  • You can migrate entity by entity: move one resource client at a time from BaseClient to SupabaseDataClient.
  • During migration, mixed data backends are valid (BaseClient for some entities, SupabaseDataClient for others) as long as each UI flow uses the corresponding client methods.
  • If you switch auth to Supabase, use SupabaseManagerProvider + SupabaseAuthProvider; if you keep REST auth, continue with ManagerProvider + AuthProvider.

Built-in auth refresh behavior

APIClient and BaseClient already include refresh/retry behavior for secured requests:

  • If accessTokenExpiresAt is close to expiry, a refresh runs before sending the request.
  • If a secured request returns 401, the client attempts one refresh and retries once.
  • Concurrent refresh calls are deduplicated with a shared in-flight promise.
  • If refresh fails, local session keys are cleared (user, remember, refreshToken, accessTokenExpiresAt).

Local development (step-by-step)

  1. Clone the repository:
    git clone https://github.com/sito8943/-sito-dashboard-app.git
    cd ./-sito-dashboard-app
  2. Use the project Node version:
    nvm use
  3. Install dependencies:
    npm install
  4. Start the dev server:
    npm run dev
  5. (Optional) Run Storybook:
    npm run storybook

Scripts

  • npm run dev: start Vite dev server
  • npm run build: compile TypeScript and build the library
  • npm run preview: preview the Vite build locally
  • npm run lint: run ESLint + Prettier check + depcheck (no file writes)
  • npm run lint:fix: run ESLint autofix + Prettier write mode
  • npm run docs:check: validate docs policy markers, relative links, and docs consistency rules
  • npm run test: run unit/component tests once (Vitest)
  • npm run test:watch: run tests in watch mode
  • npm run format: run Prettier write mode
  • npm run storybook: run Storybook locally
  • npm run build-storybook: generate static Storybook build

Offline-first / IndexedDB fallback

IndexedDBClient is a drop-in offline alternative to BaseClient. It exposes the same method surface (insert, insertMany, update, get, getById, export, import, commonGet, softDelete, restore) but stores data locally in the browser's IndexedDB instead of calling a remote API.

When to use it

Use IndexedDBClient when the remote API is unreachable — for example in offline-capable dashboards, field apps, or PWAs. The pattern is to detect connectivity and swap the client transparently:

import { BaseClient, IndexedDBClient } from "@sito/dashboard-app";

// Online: hit the API. Offline: read/write from IndexedDB.
const productsClient = navigator.onLine
  ? new ProductsClient(import.meta.env.VITE_API_URL)
  : new ProductsIndexedDBClient();

Creating an offline client

Extend IndexedDBClient the same way you would extend BaseClient:

import {
  IndexedDBClient,
  BaseEntityDto,
  BaseCommonEntityDto,
  BaseFilterDto,
  DeleteDto,
  ImportPreviewDto,
} from "@sito/dashboard-app";

interface ProductDto extends BaseEntityDto {
  name: string;
  price: number;
}

interface ProductFilterDto extends BaseFilterDto {
  category?: string;
}

class ProductsIndexedDBClient extends IndexedDBClient<
  "products",
  ProductDto,
  ProductDto, // TCommonDto
  Omit<ProductDto, "id" | "createdAt" | "updatedAt" | "deletedAt">,
  ProductDto, // TUpdateDto (extends DeleteDto via BaseEntityDto)
  ProductFilterDto,
  ImportPreviewDto
> {
  constructor() {
    super("products", "my-app-db");
  }
}

Reacting to connectivity changes at runtime

import { useState, useEffect } from "react";

function useProductsClient() {
  const [client, setClient] = useState(() =>
    navigator.onLine
      ? new ProductsClient(apiUrl)
      : new ProductsIndexedDBClient(),
  );

  useEffect(() => {
    const goOnline = () => setClient(new ProductsClient(apiUrl));
    const goOffline = () => setClient(new ProductsIndexedDBClient());
    window.addEventListener("online", goOnline);
    window.addEventListener("offline", goOffline);
    return () => {
      window.removeEventListener("online", goOnline);
      window.removeEventListener("offline", goOffline);
    };
  }, []);

  return client;
}

Note: IndexedDBClient requires a browser environment. It will not work in SSR/Node contexts.

Contract and filtering notes:

  • Preferred update contract is update(value) (aligned with BaseClient.update(value)).
  • Legacy update(id, value) remains temporarily supported for backward compatibility.
  • Filtering uses strict equality for regular keys.
  • deletedAt remains a date filter (Date | null) for exact-match filtering.
  • Use softDeleteScope for trash filters:
    • softDeleteScope: "ACTIVE" => active rows (deletedAt null/undefined)
    • softDeleteScope: "DELETED" => deleted rows (deletedAt not null/undefined)
    • softDeleteScope: "ALL" => all rows

Sharing a database across multiple entity clients

Multiple IndexedDBClient instances can share the same dbName to co-locate related stores (e.g. users, accounts, transactions) in a single database. Each client registers its table internally, so opening one store no longer drops stores registered by other clients.

class UsersIndexedDBClient extends IndexedDBClient<"users" /* ... */> {
  constructor() {
    super("users", "my-app-db");
  }
}

class AccountsIndexedDBClient extends IndexedDBClient<"accounts" /* ... */> {
  constructor() {
    super("accounts", "my-app-db");
  }
}

// Concurrent opens are serialized per `dbName` and share one upgrade pass.
const users = new UsersIndexedDBClient();
const accounts = new AccountsIndexedDBClient();

Notes:

  • Pass the same dbName to every client that belongs to the same logical database.
  • version is the minimum schema version for that client; the actual open runs at max(registered versions, current db version) and bumps automatically when a registered store is missing.
  • Opens are serialized per dbName via an internal lock, so parallel Promise.all([...]) calls across clients are safe.

Tests

Automated tests are configured with Vitest + @testing-library/react.

Run all tests once:

npm run test

Run tests in watch mode:

npm run test:watch

Current validation stack:

  • npm run lint
  • npm run docs:check
  • npm run test
  • npm run build
  • Storybook/manual behavior checks (optional visual validation)

Linting and formatting

Run linters:

npm run lint

Run automatic fixes:

npm run lint:fix

Run formatting:

npm run format

Deployment / release

CI is available through GitHub Actions:

  • .github/workflows/ci.yml: runs test + build on push and pull_request
  • .github/workflows/lint.yml: runs lint + docs:check on pull_request

Package release/publish is still handled manually.

Recommended release flow:

  1. Ensure your branch is up to date and the working tree is clean.
  2. Update version:
    npm version patch
    # or: npm version minor / npm version major
  3. Validate package:
    npm run lint
    npm run test
    npm run build
  4. Publish to npm:
    npm publish --access public
  5. Push commit and tag:
    git push --follow-tags

Contributing

  1. Fork the repository.
  2. Create a branch for your feature/fix.
  3. Open a PR with a clear change summary and validation notes.

License

MIT (see LICENSE).