Blocks
Blocks sit one level above layouts and components. They're higher-level composites — auth forms + guards, a collapsible dashboard panel, a full data-grid — that you drop into a route, wire to your data callbacks, and ship.
Import AuthBlock, DashboardBlock and DataGridBlock from @omnitron-dev/prism/blocks.
AuthBlock — auth forms + route guards
AuthBlock is a namespace of ready-made auth forms —
AuthBlock.Login, AuthBlock.Register, AuthBlock.ForgotPassword,
AuthBlock.ResetPassword, AuthBlock.VerifyCode (the same
components are also exported individually as LoginForm,
RegisterForm, etc.). There is no single <AuthBlock mode="…">
component — render the form you need and wire its onSubmit:
import { AuthBlock } from '@omnitron-dev/prism/blocks';
function SignInPage() {
return (
<AuthBlock.Login
onSubmit={async ({ email, password, rememberMe }) => {
await authService.signIn({ email, password });
navigate('/');
}}
showRememberMe
showForgotPassword
onForgotPassword={() => navigate('/recover')}
showSocialLogin
socialProviders={[
{ id: 'google', name: 'Google', icon: <GoogleIcon />, onClick: oauthGoogle },
{ id: 'github', name: 'GitHub', icon: <GithubIcon />, onClick: oauthGithub },
]}
/>
);
}The forms
| Member (namespace) | Standalone export | Submit payload |
AuthBlock.Login | LoginForm | { email, password, rememberMe? } |
AuthBlock.Register | RegisterForm | RegisterFormData |
AuthBlock.ForgotPassword | ForgotPasswordForm | { email } |
AuthBlock.ResetPassword | ResetPasswordForm | ResetPasswordFormData |
AuthBlock.VerifyCode | VerifyCodeForm | { code } |
Common form props
| Prop | Type | Notes |
onSubmit | (data) => Promise<void> | void | Submit handler for the form |
loading | boolean | Disables + shows progress. Optional — see below |
disabled | boolean | Disable the form |
schema | z.ZodSchema | Override the built-in Zod validation |
labels | per-field label overrides | i18n / copy customisation |
sx | SxProps<Theme> | Styles for the form wrapper |
slotProps | { wrapper, submitButton, textField } | MUI slot overrides |
The forms use react-hook-form + Zod internally — you get email validation and field-level errors for free.
You do not have to pass loading to prevent a double submit.
The form awaits your onSubmit and falls back to react-hook-form's
own isSubmitting, so every control is disabled for as long as the
promise is pending. Pass loading when the parent owns a pending
state the form cannot see — a redirect that runs after sign-in, say,
where the button should stay disabled past the resolution of
onSubmit.
Error messages from onSubmit are shown, sanitized. A rejection
is caught and its message rendered above the form, unless it looks
like something internal — a stack frame, an errno, a SQL fragment, a
file path, or a leaked credential value. It matches the value, so
"Password must contain at least 2 digits" and "Reset token expired"
reach the user; password=hunter2 and a JWT do not.
Per-form props
| Form | Prop | Notes |
| Login | showRememberMe, showForgotPassword, onForgotPassword | |
| Login | showSocialLogin, socialProviders | { id, name, icon, onClick }[] |
| Register | showNameFields, requireTerms, onTermsClick, onPrivacyClick | |
| Register / ResetPassword | minPasswordLength | Feeds the built-in schema |
| ForgotPassword | onBackToLogin | |
| VerifyCode | codeLength | Default 6 |
| VerifyCode | onResendCode, resendCooldown | Seconds before Resend re-enables (default 60) |
Resend is guarded against a second click while the first request is still open, so a double-click sends one code rather than two — which matters on a rate-limited endpoint, where the second attempt is what locks the user out of the flow they are finishing.
Route guards
The block also exports composable route guards (each takes a
useAuth hook so you can plug in your own auth store):
import { AuthGuard, GuestGuard, RoleBasedGuard }
from '@omnitron-dev/prism/blocks';
<AuthGuard useAuth={useAuth} onUnauthenticated={() => navigate('/sign-in')}>
<Dashboard />
</AuthGuard>
<RoleBasedGuard
useAuth={useAuth}
roles={['admin', 'moderator']}
roleMatchStrategy="any"
accessDeniedComponent={<Forbidden />}
>
<AdminPanel />
</RoleBasedGuard>hasRole / hasPermission / createConditionalRender helpers
are exported for inline checks.
<DashboardBlock> — collapsible content panel
<DashboardBlock> is a flexible panel/card primitive — a
titled, optionally-collapsible container with header, content,
and footer slots, plus built-in loading and error states. It is
the building block you compose dashboards from (it does not
itself render stat tiles or charts — drop those in as children):
import { DashboardBlock } from '@omnitron-dev/prism/blocks';
function RequestsPanel({ loading, series }) {
return (
<DashboardBlock
title="Requests / min"
subtitle="last 24h"
icon={<ActivityIcon />}
actions={<IconButton><RefreshIcon /></IconButton>}
loading={loading}
collapsible
>
<Chart series={series} type="area" />
</DashboardBlock>
);
}| Prop | Type | Purpose |
title / subtitle | string | Header text |
icon | ReactNode | Leading header icon |
actions | ReactNode | Top-right header slot |
children | ReactNode | Panel body |
footer | ReactNode | Footer slot |
variant / size | DashboardBlockVariant / …Size | Visual style |
loading | boolean | Shows the loading skeleton |
error | boolean | Shows the error state (errorConfig) |
collapsible | boolean | Enables collapse controls; default false |
defaultCollapsed | boolean | Initial state for an uncontrolled block |
collapsed / onCollapseChange | boolean / callback | Controlled state and requested changes |
DashboardBlockHeader, DashboardBlockContent, and
DashboardBlockFooter are exported for fully custom composition,
and useDashboardBlock drives collapse/loading state.
Capability and state are separate: without collapsible, the block remains
expanded, renders no collapse button and ignores context collapse actions, even
if a collapsed state was supplied. Custom headers read the same collapsible
flag from useDashboardBlockContext. An enabled block collapses its complete
body, including loading, error and empty states; its header stays available.
Use defaultCollapsed for internal state, or pair collapsed with
onCollapseChange for state owned by the caller. The callback reports requested
changes in both modes. The public hook supplies state props, so enable the
capability explicitly when spreading them:
import { DashboardBlock, useDashboardBlock } from '@omnitron-dev/prism/blocks';
function ActivityPanel() {
const { blockProps } = useDashboardBlock({ defaultCollapsed: false });
return (
<DashboardBlock title="Activity" collapsible {...blockProps}>
<ActivityFeed />
</DashboardBlock>
);
}External controls can use the hook's actions.expand(), actions.collapse() and
actions.toggleCollapse() to update that same controlled state.
<DataGridBlock> — filterable / sortable / paginated table
The most-used block. It wraps MUI X DataGrid and adds a
toolbar, row actions, selection, and empty/error states behind
one prop API. Columns are MUI GridColDefs and rows are passed
directly via the rows prop:
import { DataGridBlock } from '@omnitron-dev/prism/blocks';
function UsersPage({ users, loading }) {
return (
<DataGridBlock
rows={users}
getRowId={(row) => row.id}
loading={loading}
columns={[
{ field: 'email', headerName: 'Email', flex: 1, sortable: true },
{ field: 'role', headerName: 'Role' },
{ field: 'status', headerName: 'Status',
renderCell: (p) => <StatusChip status={p.value} /> },
{ field: 'createdAt', headerName: 'Created',
renderCell: (p) => <DateCell value={p.value} /> },
]}
onRowClick={(params) => navigate(`/users/${params.id}`)}
rowActions={[
{ key: 'edit', label: 'Edit', icon: <EditIcon />, onClick: (row) => navigate(`/users/${row.id}/edit`) },
{ key: 'remove', label: 'Remove', icon: <TrashIcon />, color: 'error', onClick: handleRemove },
]}
selection={{ enabled: true, checkboxSelection: true }}
onSelectionChange={(model) => setSelected(model)}
toolbar={{
quickFilter: { enabled: true, placeholder: 'Search by email' },
export: { csv: true },
columnVisibility: { enabled: true },
densitySelector: true,
}}
pagination={{ defaultPageSize: 25, pageSizeOptions: [25, 50, 100] }}
empty={{
title: 'No users yet',
description: 'Invite your first user.',
action: { label: 'Invite', onClick: onInvite },
}}
/>
);
}Columns
Columns are standard MUI X GridColDefs — field, headerName,
flex / width, sortable, renderCell, valueGetter, etc.
(Not a bespoke ColumnDef.) Column visibility is controlled via
columnVisibilityModel / onColumnVisibilityChange, or the
toolbar's columnVisibility toggler.
Row actions
rowActions render in a per-row menu — each is keyed and gets
the row + event:
rowActions={[
{ key: 'edit', label: 'Edit', icon: <EditIcon />, onClick: (row, e) => { /* … */ } },
{ key: 'view', label: 'View', icon: <EyeIcon />, onClick: (row, e) => { /* … */ } },
{ key: 'remove', label: 'Remove', icon: <TrashIcon />, color: 'error',
disabled: (row) => row.locked,
divider: true,
onClick: (row, e) => { /* … */ },
},
]}Actions support disabled / hidden (boolean or (row) =>
boolean), color, and a divider. (There is no built-in
confirm field — open your own <ConfirmDialog> from onClick.)
Selection & bulk operations
<DataGridBlock
selection={{ enabled: true, mode: 'multiple', checkboxSelection: true }}
onSelectionChange={(model) => setSelected(model)}
// …
/>onSelectionChange reports the MUI GridRowSelectionModel; drive
your own bulk-action bar from that selection.
Toolbar config
interface DataGridToolbarConfig {
show?: boolean;
quickFilter?: { enabled?, placeholder?, debounceMs? };
export?: { csv?, excel?, print?, fileName? };
columnVisibility?: { enabled?, defaultModel? };
densitySelector?: boolean;
actions?: ReactNode; // custom toolbar actions
startContent?: ReactNode; // left-aligned custom content
endContent?: ReactNode; // right-aligned custom content
}Server-side data — useDataGridBlock
For server-driven paging/sorting/filtering, use the
useDataGridBlock hook: pass a fetcher and spread its return
onto the block (server pagination mode):
import { DataGridBlock, useDataGridBlock } from '@omnitron-dev/prism/blocks';
function UsersPage() {
const grid = useDataGridBlock({
fetcher: async ({ page, pageSize, sortModel, filterModel, quickFilterValue }) => {
const res = await usersService.list({ page, pageSize, sortModel, filterModel, q: quickFilterValue });
return { rows: res.items, total: res.total }; // DataGridFetchResult
},
initialPageSize: 25,
});
return (
<DataGridBlock
columns={columns}
rows={grid.rows}
loading={grid.loading}
pagination={{ mode: 'server', rowCount: grid.total }}
paginationModel={grid.paginationModel}
onPaginationChange={grid.onPaginationChange}
sortModel={grid.sortModel}
onSortChange={grid.onSortChange}
filterModel={grid.filterModel}
onFilterChange={grid.onFilterChange}
/>
);
}The toolbar applies space-separated search terms to
filterModel.quickFilterValues. Client grids filter their rows immediately
after the debounce window. Server grids pass the same terms to the fetcher,
with quickFilterValue available as joined text. Forward the request's
signal to your HTTP client to cancel superseded work. The grid displays
only the latest response, even when the server completes an older request
later. Escape clears search. Pass toolbar={{ show: false }} to omit it.
The hook also returns refresh() and reset(). For client-side
data, just pass the full rows array and use the default
('client') pagination mode.
When to use a block vs compose your own
| Reach for a block when… | Compose components when… |
| The screen matches a standard pattern | Layout differs from the template |
| You want consistency across many similar screens | The screen is unique |
| Speed of iteration matters more than control | Pixel-precision matters more than speed |
| You're building admin / operator surfaces | You're building a marketing / brochure page |
The blocks are compositions of public components + hooks, so
dropping out is cheap. If you outgrow <DataGridBlock>, drop to
MUI X <DataGrid> directly (or <FilterToolbar> + the Prism
<Table>) without changing the rest of your page.
Subpath imports
import { AuthBlock, DashboardBlock, DataGridBlock } from '@omnitron-dev/prism/blocks';See also
Components catalog — what blocks are composed of
Layouts — shells that hold blocks
Forms —
<AuthBlock>uses these primitives
Guard-only entry point
Use @omnitron-dev/prism/auth when you need guards without forms or layout dependencies:
import { RoleBasedGuard, hasPermission, type AuthState } from '@omnitron-dev/prism/auth';
function TerminalArea() {
return (
<RoleBasedGuard useAuth={useConsoleAuthorization} permissions={['OmnitronTerminal.open']}
loadingComponent={<p>Checking access…</p>}
accessDeniedComponent={<p>Access unavailable</p>}>
<TerminalWorkspace />
</RoleBasedGuard>
);
}The entry exports AuthGuard, GuestGuard, RoleBasedGuard, hasRole, hasPermission, createConditionalRender and their types. Guards are router-independent. Supply an authentication hook backed by the application's session and current server policy. RoleBasedGuard evaluates grants during rendering, so revoked children cannot appear for a stale render while an effect updates state. UI guards never replace backend authorization.
Omnitron uses this entry with RPC-derived capabilities, preserving the same role policy in Console and CLI.