Skip to main content

Prism

@omnitron-dev/prism is the design system. Pre-composed, theme-aware components and entire UI blocks for building production React frontends — built on MUI v9 + React 19, integrated with react-hook-form + zod, ready for Vite / Next / Remix out of the box.

pnpm add @omnitron-dev/prism
React 19 + MUI v9 idioms

Prism uses the latest patterns from both upstreams:

  • No forwardRef — React 19 routes ref through props directly, so every component takes ref as a normal prop.
  • slotProps API — the v9 replacement for the legacy InputProps={...} / MenuProps={...} / TabIndicatorProps={...} / BackdropProps={...} / componentsProps={...} families.
  • Unified <Grid> with size={{ xs, sm, md }} — v1's <Grid item xs={...}> shorthand is gone.
  • Position classes for grouped controlsToggleButtonGroup and ButtonGroup mark children with firstButton / middleButton / lastButton; theme overrides target those classes instead of the deprecated :not(:first-of-type) sibling selector.

See Components → Refs (React 19) and Components → MUI v9 slot props for the consumer-side migration cheat sheet.

Three layers of API

Pick the level that matches your need:

  • Blocks — copy a <DashboardBlock>, fill the slots, ship a screen.
  • Layouts — own the routing; use <DashboardLayout> for shell.
  • Components — assemble from <Card> / <Table> / <Drawer>.
  • Theme — palette, typography, shadows, density, dark mode.
  • Forms — schema-aware forms with <Field> + SchemaProvider.
  • Hooks catalog — all 48 React hooks.
  • Maps — self-hosted MapLibre map, markers, point picker and coverage layer.

Subpath exports

There is no per-component subpath. ./components/* and ./blocks/* globs used to appear in the package's exports, which made every component directory resolvable to TypeScript while only three of them shipped JavaScript — so @omnitron-dev/prism/components/alert typechecked and then failed in the browser, and these pages recommended that form in 55 places. The globs are gone: such an import is now a compile error, which is where it belongs.

<Editor> and <EmojiPicker> keep their own subpaths because they are deliberately code-split — the emoji dataset alone is ~80 kB gzipped and should not sit on the critical path. Everything else comes from the root or from ./components; the bundler tree-shakes either.

SubpathWhat it exports
@omnitron-dev/prismEverything; convenient but largest
@omnitron-dev/prism/themecreatePrismTheme(), palette, typography, shadows, presets
@omnitron-dev/prism/core<PrismProvider>, <ProviderStack>, context primitives
@omnitron-dev/prism/layouts<DashboardLayout>, <AuthCenteredLayout> / <AuthSplitLayout> / <AuthSimpleLayout>, <LayoutProvider>
@omnitron-dev/prism/blocks<AuthBlock>, <DashboardBlock>, <DataGridBlock>
@omnitron-dev/prism/componentsAll 50+ components
@omnitron-dev/prism/components/editor<Editor> alone, as its own chunk
@omnitron-dev/prism/components/emoji-picker<EmojiPicker> alone — the dataset is ~80 kB gz
@omnitron-dev/prism/formsSchema-aware form helpers
@omnitron-dev/prism/hooks48 React hooks
@omnitron-dev/prism/stateZustand-based store factory
@omnitron-dev/prism/accessibilityA11y primitives + ARIA helpers
@omnitron-dev/prism/netronPre-wired Netron auth/UI bindings
@omnitron-dev/prism/httpHTTP fetcher helpers
@omnitron-dev/prism/cliCLI helpers (used by prism bin)

Tree-shaking works on every subpath — import the smallest scope your bundler needs.

Minimum wiring

import { PrismProvider } from '@omnitron-dev/prism/core';

function App() {
return (
<PrismProvider
defaultSettings={{ mode: 'dark', preset: 'midnight', primaryColor: '#7c4dff' }}
>
<Outlet />
</PrismProvider>
);
}

<PrismProvider> builds the theme itself — you do not call createPrismTheme() and hand it over, and there is no theme prop to hand it to. It reads the settings store (persisted, so a user's own choices survive a reload), falls back to defaultSettings for anything unset, resolves mode: 'system' against the OS preference, and calls createPrismTheme() with the result. Both production consumers — the DAOS portal and the Omnitron console — pass exactly { mode, preset, direction }.

defaultSettings accepts mode, preset, direction, primaryColor, contrast, fontSize, fontFamily and navLayout. For MUI-level escapes use themeOverrides, which is merged into the generated theme; config carries non-theme configuration. createPrismTheme() is exported for building a theme outside the provider (a standalone MUI tree, a snapshot test), not for feeding one back in.

Beyond the theme, the provider sets up MUI's ThemeProvider, CssBaseline, the snackbar host, the icon registry, and the localization context (dateAdapterLocale, dateLocaleText).

For more sophisticated apps, use <ProviderStack> to layer multiple providers cleanly:

<ProviderStack
providers={[
[QueryClientProvider, { client: queryClient }],
[AuthProvider, { client: authClient }],
[PrismProvider, { defaultSettings: { mode: 'dark', preset: 'midnight' } }],
[RouterProvider, { router }],
]}
>
<Outlet />
</ProviderStack>

State management — createPrismStore / createPersistedStore

Prism ships Zustand-based store factories (Immer + DevTools + optional localStorage persistence baked in). Use createPrismStore for general state and createPersistedStore for persisted state with version-aware migration:

import { createPrismStore } from '@omnitron-dev/prism/state';

interface UIState {
sidebarOpen: boolean;
toggleSidebar: () => void;
}

// Immer middleware is enabled — mutate `state` directly in `set`.
export const useUIStore = createPrismStore<UIState>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((s) => { s.sidebarOpen = !s.sidebarOpen; }),
}), {
name: 'ui-store',
persist: { partialize: (s) => ({ sidebarOpen: s.sidebarOpen }) },
});

For persisted preferences with migrations, pass a version plus a migrate callback through the persist options:

import { createPersistedStore } from '@omnitron-dev/prism/state';

export const useSettings = createPersistedStore<SettingsState>(
(set) => ({ /* … */ }),
'settings', // store name (positional)
{
version: 2,
migrate: (persisted, version) => {
// version-aware migration
return persisted as SettingsState;
},
},
);

When you bump version, persisted state from older versions runs through migrate before being adopted. (See also the versioned-settings helpers in @omnitron-dev/prism/state.)

Accessibility

@omnitron-dev/prism/accessibility ships primitives that the components use internally and that you can reuse:

  • <VisuallyHidden> — screen-reader-only text.
  • useFocusTrap — trap focus in modals.
  • useEscapeKey — fire on Esc with optional stop-propagation.
  • useReturnFocus — restore focus when an overlay closes.
  • ARIA helpers for combobox / listbox / tablist patterns.

All <Field>-based forms produce correct labelling automatically.

Netron integration — @omnitron-dev/prism/netron

Pre-wired auth + UI bindings for apps that talk to a Titan backend via Netron:

import { NetronProvider, createNetronClient }
from '@omnitron-dev/prism/netron';

const client = createNetronClient({ transport: 'http', url: '/api' });

function App() {
return (
<NetronProvider client={client}>
<Outlet />
</NetronProvider>
);
}

For multi-backend setups:

import { createMultiBackendClient, MultiBackendProvider }
from '@omnitron-dev/prism/netron';

const client = createMultiBackendClient({
baseUrl: '',
backends: {
main: { path: '/api/main' },
storage: { path: '/api/storage' },
realtime:{ path: '/api/realtime' },
},
defaultBackend: 'main',
});

<MultiBackendProvider client={client} autoConnect>
<Outlet />
</MultiBackendProvider>

The bindings re-export @omnitron-dev/netron-react hooks — see netron/react for the full hook reference.

CLI — prism binary

prism init # scaffold Prism config in current project
prism add component card # generate boilerplate using registered template
prism list components # show available components

The CLI uses templates from templates/ (shipped with the package) plus the schema in registry.json for available component metadata.

Best practices

  • Pick the smallest layer. A <DashboardBlock> is faster than composing it from 12 components, but constrains you to its prop API. Drop to <DashboardLayout>
    • components when you need flexibility.
  • One <PrismProvider> per app. Multiple providers create duplicate snackbar hosts and confused theme contexts.
  • Subpath imports for bundle size. Per-component imports keep first-paint fast.
  • Use schema-driven forms. <Field> + SchemaProvider produces consistent UX with zero per-field boilerplate.
  • Surface form errors with <FormAlert> inline; reserve toasts (<Snackbar>) for transient background events.
  • createPrismStore / createPersistedStore over raw Zustand for any state that persists — the versioned-settings helpers handle migrations.

See also