@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/*.mdis not the canonical integration guide for this package.@sito/dashboard-appis not SSR-compatible. Treat it as browser-only (client-rendered apps).- For auth-enabled apps, use
ConfigProvider -> ManagerProvider -> AuthProvider -> NotificationProvider -> DrawerMenuProvider(NavbarProviderwhen needed;BottomNavActionProvideroptional for dynamic mobile center actions). DrawerandOnboardingcan run withoutAuthProvider; in that case they behave as guest-mode UI defaults.IconButtondiffers by package:@sito/dashboard:iconaccepts a React node.@sito/dashboard-app:iconexpectsIconDefinition(FontAwesome wrapper export).
Installation
npm install @sito/dashboard-app
# or
yarn add @sito/dashboard-app
# or
pnpm add @sito/dashboard-appRequirements
- 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-query5.83.0@supabase/supabase-js2.100.0(optional; only if using Supabase backend)react-hook-form7.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(usePostFormdeprecated alias),useDeleteAction,useNavbar, and more — all action hooks ship with defaultsticky,multiple,id,icon, andtooltipvalues so onlyonClickis required - Providers and utilities:
ConfigProvider,ManagerProvider,SupabaseManagerProvider,AuthProvider,SupabaseAuthProvider,NotificationProvider,DrawerMenuProvider,NavbarProvider,BottomNavActionProvider,useBottomNavAction,useOptionalBottomNavAction,useRegisterBottomNavAction, DTOs, API clients (BaseClient,IndexedDBClient,SupabaseDataClient), anduseSupabase
Component usage patterns
Error component
Error supports two modes:
- Default mode: icon + message + retry (uses
Buttoninternally) - Custom mode: pass
childrenfor 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 = falseloadingMore = falseloadMoreComponent = nullobserverRootMargin = "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(default200)scrollTop?: number/scrollLeft?: number(default0/0)icon?: IconDefinitiontooltip?: stringscrollOnClick?: boolean(defaulttrue)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/disabledon each item andcenterAction.hiddenfor conditional rendering. centerActionsupportsIconButtonvisual props and optionalto; navigation runs afteronClickunlessevent.preventDefault()is called.BottomNavActionProvideris optional. When mounted,useRegisterBottomNavActioncan override center-action fields dynamically from active page scope; registered fields take precedence over staticcenterActionprops.
Dialog hook migration (v0.0.54)
v0.0.54 removes the legacy entity-coupled useFormDialog contract.
Breaking changes
useFormDialogis now core lifecycle only (mode: "state" | "entity").- Legacy props were removed from
useFormDialog:mutationFn,queryKey,getFunction,dtoToForm,formToDto. - Deprecated aliases were removed:
useFormDialogLegacyuseEntityFormDialog
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
ManagerProvideraboveAuthProvider. DrawerandOnboardingare auth-optional wrappers. WithoutAuthProvider,Drawertreats the session as logged-out andOnboardingskipssetGuestMode.NavbarProvideris required when usingNavbaroruseNavbar; otherwise it can be omitted.BottomNavActionProvideris optional; mount it around your app shell when pages/components useuseRegisterBottomNavAction.- If you customize auth storage keys in
AuthProvider, pass the same keys toIManager/BaseClientauth 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_URLVITE_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+BaseClientdo not need changes. - You can migrate entity by entity: move one resource client at a time from
BaseClienttoSupabaseDataClient. - During migration, mixed data backends are valid (
BaseClientfor some entities,SupabaseDataClientfor 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 withManagerProvider+AuthProvider.
Built-in auth refresh behavior
APIClient and BaseClient already include refresh/retry behavior for secured requests:
- If
accessTokenExpiresAtis 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)
- Clone the repository:
git clone https://github.com/sito8943/-sito-dashboard-app.git cd ./-sito-dashboard-app - Use the project Node version:
nvm use - Install dependencies:
npm install - Start the dev server:
npm run dev - (Optional) Run Storybook:
npm run storybook
Scripts
npm run dev: start Vite dev servernpm run build: compile TypeScript and build the librarynpm run preview: preview the Vite build locallynpm run lint: run ESLint + Prettier check + depcheck (no file writes)npm run lint:fix: run ESLint autofix + Prettier write modenpm run docs:check: validate docs policy markers, relative links, and docs consistency rulesnpm run test: run unit/component tests once (Vitest)npm run test:watch: run tests in watch modenpm run format: run Prettier write modenpm run storybook: run Storybook locallynpm 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:
IndexedDBClientrequires a browser environment. It will not work in SSR/Node contexts.
Contract and filtering notes:
- Preferred update contract is
update(value)(aligned withBaseClient.update(value)). - Legacy
update(id, value)remains temporarily supported for backward compatibility. - Filtering uses strict equality for regular keys.
deletedAtremains a date filter (Date | null) for exact-match filtering.- Use
softDeleteScopefor trash filters:softDeleteScope: "ACTIVE"=> active rows (deletedAtnull/undefined)softDeleteScope: "DELETED"=> deleted rows (deletedAtnot 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
dbNameto every client that belongs to the same logical database. versionis the minimum schema version for that client; the actual open runs atmax(registered versions, current db version)and bumps automatically when a registered store is missing.- Opens are serialized per
dbNamevia an internal lock, so parallelPromise.all([...])calls across clients are safe.
Tests
Automated tests are configured with Vitest + @testing-library/react.
Run all tests once:
npm run testRun tests in watch mode:
npm run test:watchCurrent validation stack:
npm run lintnpm run docs:checknpm run testnpm run build- Storybook/manual behavior checks (optional visual validation)
Linting and formatting
Run linters:
npm run lintRun automatic fixes:
npm run lint:fixRun formatting:
npm run formatDeployment / release
CI is available through GitHub Actions:
.github/workflows/ci.yml: runstest + buildonpushandpull_request.github/workflows/lint.yml: runslint + docs:checkonpull_request
Package release/publish is still handled manually.
Recommended release flow:
- Ensure your branch is up to date and the working tree is clean.
- Update version:
npm version patch # or: npm version minor / npm version major - Validate package:
npm run lint npm run test npm run build - Publish to npm:
npm publish --access public - Push commit and tag:
git push --follow-tags
Contributing
- Fork the repository.
- Create a branch for your feature/fix.
- Open a PR with a clear change summary and validation notes.
License
MIT (see LICENSE).
