Skip to main content

Blocks

Blocks sit one level above layouts and components. They're complete user-flow surfaces — sign-in screen, dashboard overview, data-grid page — that you drop into a route, wire to your data callbacks, and ship.

Three blocks ship out of the box. Each is also available as its own subpath import.

<AuthBlock> — full sign-in screen

import { AuthBlock } from '@omnitron-dev/prism/blocks';

function SignInPage() {
return (
<AuthBlock
mode="sign-in"
onSignIn={async ({ email, password, totpCode }) => {
await authService.signIn({ email, password, totpCode });
navigate('/');
}}
oauth={[
{ provider: 'google', label: 'Continue with Google', onClick: oauthGoogle },
{ provider: 'github', label: 'Continue with GitHub', onClick: oauthGithub },
]}
links={{
signUp: { label: 'Create account', href: '/sign-up' },
forgotPassword:{ label: 'Forgot password', href: '/recover' },
}}
logo={<Logo size={48} />}
tagline="Welcome back"
/>
);
}

Modes

ModePurpose
'sign-in'Email + password + 2FA
'sign-up'Registration
'verify'Email/phone verification code entry
'forgot-password'Initiate password reset
'reset-password'Set new password (from email link)
'2fa-setup'TOTP secret QR + initial code

Props

PropTypeNotes
modeone of modes above
onSignIn / onSignUp / onVerify / etc.(data) => Promise<void>Handler for the active mode
oauthArray<{provider, label, onClick}>Buttons above the form
linksper-mode navigation links
logoReactNodeTop of the card
taglinestringSubtitle
background'gradient' | 'image' | ReactNodeRight-pane illustration
formErrorstring | ReactNodeInline error above the form

The block uses <Field> + SchemaProvider internally — you get email validation, password-strength meter, TOTP-code mask free.

2FA flow

const [pendingMfa, setPendingMfa] = useState(false);

<AuthBlock
mode={pendingMfa ? 'verify' : 'sign-in'}
onSignIn={async (data) => {
const result = await authService.signIn(data);
if (result.requires2fa) {
setPendingMfa(true);
} else {
navigate('/');
}
}}
onVerify={async ({ code }) => {
await authService.verifyTotp({ code });
navigate('/');
}}
/>

<DashboardBlock> — overview page

import { DashboardBlock } from '@omnitron-dev/prism/blocks';

function DashboardPage() {
return (
<DashboardBlock
title="Platform overview"
tiles={[
{ label: 'Apps online', value: 12, delta: +2, icon: 'box', color: 'primary' },
{ label: 'Active users', value: '4.2k', delta: +12.5, icon: 'users', color: 'success' },
{ label: 'Storage used', value: '230GB', delta: +5.1, icon: 'hard-drive', color: 'warning' },
{ label: 'Error rate', value: '0.12%', delta: -0.04, icon: 'alert', color: 'error' },
]}
charts={[
{ title: 'Requests / min', chart: <Chart series={requestsSeries} type="area" /> },
{ title: 'Latency p95', chart: <Chart series={latencySeries} type="line" /> },
]}
recentActivity={
<ActivityFeed items={activity} />
}
/>
);
}
PropPurpose
titlePage heading
tilesArray of <Stat> cards
chartsChart panels — typically 2-4
recentActivityRight-rail feed
actionsTop-right action buttons
loadingBoolean — shows skeleton tiles & charts

The layout is responsive — tiles wrap, charts collapse to stack on small screens.

<DataGridBlock> — filterable / sortable / paginated table

The most-used block. Composes <FilterToolbar> + <Table> + <Pagination> + per-row actions into one prop API:

import { DataGridBlock } from '@omnitron-dev/prism/blocks';

function UsersPage() {
const users = useService<UserService>('users');

return (
<DataGridBlock
title="Users"
columns={[
{ field: 'email', header: 'Email', sortable: true },
{ field: 'role', header: 'Role', filterable: { type: 'select', options: ['admin','user','viewer'] } },
{ field: 'status', header: 'Status', render: (row) => <StatusChip status={row.status} /> },
{ field: 'createdAt', header: 'Created', render: (row) => <DateCell value={row.createdAt} /> },
]}
query={({ page, pageSize, sort, filter }) =>
users.list.useQuery([{ page, pageSize, sort, filter }])
}
rowKey="id"
onRowClick={(row) => navigate(`/users/${row.id}`)}
rowActions={[
{ id: 'edit', label: 'Edit', icon: 'edit', onClick: (row) => navigate(`/users/${row.id}/edit`) },
{ id: 'remove', label: 'Remove', icon: 'trash', danger: true, onClick: handleRemove },
]}
bulkActions={[
{ id: 'export', label: 'Export selected', onClick: handleExport },
]}
toolbar={{
search: { placeholder: 'Search by email', field: 'email' },
filters: ['role', 'status'],
export: { formats: ['csv', 'json'] },
}}
pagination={{ defaultPageSize: 25, pageSizeOptions: [25, 50, 100] }}
emptyState={
<EmptyContent
title="No users yet"
description="Invite your first user."
action={<Button onClick={onInvite}>Invite</Button>}
/>
}
/>
);
}

Column definitions

interface ColumnDef<TRow> {
field: keyof TRow | string;
header: string | ReactNode;
width?: number | string;
align?: 'left' | 'center' | 'right';
sortable?: boolean;
filterable?: boolean | FilterConfig;
render?: (row: TRow) => ReactNode;
exportValue?: (row: TRow) => string; // for CSV/JSON export
hidden?: boolean; // user can re-show via column toggler
}

Server-side vs client-side

// Server-side (recommended — scales): query callback receives state
query={({ page, pageSize, sort, filter, search }) =>
myService.list.useQuery([{ page, pageSize, sort, filter, search }])
}

// Client-side: provide all data upfront
data={allItems}

Server-side is the default; <DataGridBlock> debounces filter

  • search inputs before calling query.

Row actions

rowActions={[
{ id: 'edit', label: 'Edit', icon: 'edit', onClick: (row) => { ... } },
{ id: 'view', label: 'View', icon: 'eye', onClick: (row) => { ... } },
{ id: 'remove', label: 'Remove', icon: 'trash', danger: true,
onClick: (row) => { ... },
confirm: { title: 'Remove user?', confirmLabel: 'Remove' },
},
]}

The confirm field wires <ConfirmDialog> automatically — no manual state.

Bulk actions

bulkActions={[
{ id: 'export', label: 'Export selected', icon: 'download', onClick: (rows) => handleExport(rows) },
{ id: 'delete', label: 'Delete selected', icon: 'trash', danger: true,
onClick: (rows) => handleDelete(rows),
confirm: { title: 'Delete N selected?', confirmLabel: 'Delete' },
},
]}

Selection appears as checkboxes; bulk-action bar shows when ≥1 row is selected.

Toolbar config

interface ToolbarConfig {
search?: { placeholder, field, debounceMs? };
filters?: string[]; // column ids that should appear as filter chips
export?: { formats: ('csv' | 'json' | 'xlsx')[] };
refresh?: boolean; // refresh button
columnVisibility?: boolean; // column toggler
savedViews?: { provider: SavedViewsProvider }; // save & restore filter sets
}

Saved views

import { useSavedViews } from '@omnitron-dev/prism/hooks';

const provider = useSavedViews({
scope: 'users',
storage: 'localStorage', // or pass a server-side provider
});

<DataGridBlock
// ...
toolbar={{ savedViews: { provider } }}
/>

Users can save the current filter + sort + column-visibility combo as a named view and restore it later.

When to use a block vs compose your own

Reach for a block when…Compose components when…
The screen matches a standard patternLayout differs from the template
You want consistency across many similar screensThe screen is unique
Speed of iteration matters more than controlPixel-precision matters more than speed
You're building admin / operator surfacesYou're building a marketing / brochure page

Both blocks are designed so dropping out is cheap — they're compositions of public components + hooks. If you outgrow <DataGridBlock>, replace it with <FilterToolbar> + <Table> + <Pagination> without changing the rest of your page.

Subpath imports

import { AuthBlock } from '@omnitron-dev/prism/blocks/auth-block';
import { DashboardBlock } from '@omnitron-dev/prism/blocks/dashboard-block';
import { DataGridBlock } from '@omnitron-dev/prism/blocks/data-grid-block';

See also