Overrides
The src/overrides/ registries - replace a generated list, form, detail page or icon with your own component.
The file to edit is src/overrides/. Everything in it is a registry that src/core/ reads at startup. You never modify a generated screen - you register a replacement for it.
Before anything below: src/core/ is read-only. It is overwritten by deno task generate:app --forceSync. See Project Structure.
Keys in every registry are SDK module names, group names, or the built-in route keys overview, wallet and notifications.
The Registries
Lists
Replace a module's whole list page:
export const lists: TListPagesOverride = { posts: PostList }
// props: TListPageProps = { group?: string; name: string }Forms
Replace a module's whole create/edit form. The component takes no props:
export const forms: TFormsOverride = { posts: PostForm }Views
Supply a detail page for a module. Like forms, the component takes no props:
export const views: TViewsOverride = { posts: PostDetail }Read the record id from the route and fetch it however you like - data loading is entirely yours:
import React from "react"
import { useParams } from "react-router"
import { ThunderSDK } from "thunder-sdk"
import { use } from "@/core/hooks/use"
import { Skeleton } from "@/components/ui/skeleton"
export default function PostDetail() {
const { id } = useParams<{ id: string }>()
const request = React.useMemo(
() =>
ThunderSDK.useCaching(
["posts", id],
async ({ signal }) =>
(await ThunderSDK.posts.get({ signal, params: { id } })) as {
results: unknown[]
}
),
[id]
)
const { data, isLoading } = use(request)
const post = data?.results?.[0]
if (isLoading) return <Skeleton className="h-40 w-full" />
return <pre>{JSON.stringify(post, null, 2)}</pre>
}use() (from @/core/hooks/use) pairs with ThunderSDK.useCaching and gives you { data, error, isLoading, refetch }. A module's get with params: { id } returns { results: [record] }.
When no view is registered for a module, its detail route goes to that module's edit form.
List Actions
The file to edit is src/overrides/crud/list-actions.tsx. It adds a button beside Create:
import type { TListActionsOverride } from "@/core/types"
export const listActions: TListActionsOverride = {
manageLeavesRequests: LeavesReportButton,
}The component takes no props - read whatever it needs from hooks. It renders immediately before Create, in both the table and the card view, and is replaced by a skeleton until the first page of data arrives.
This is the right place for anything self-contained: an export, an import dialog, a report, a link to a related screen.
List Toolbar
The file to edit is src/overrides/crud/list-toolbar.tsx. It replaces the list page's whole header row:
import type { TListToolbarOverride } from "@/core/types"
export const listToolbar: TListToolbarOverride = {
orders: ({ isLoading }) => (
<div className="flex w-full items-center justify-end gap-3">
<Button variant="outline" disabled={isLoading}>Import CSV</Button>
<Button variant="outline" disabled={isLoading}>Export</Button>
</div>
),
}Registering one drops everything the default row holds: the filter bar, the column-visibility menu, the card select-all badge, the Create button, the table/cards toggle - and that module's list-actions.tsx entry, which lives inside the row you just replaced. Rebuild only the parts you want.
TListToolbarProps is { name, isLoading } and nothing else. That is a deliberately narrow contract, and it is not the one cards.tsx gets:
TCardProps | TListToolbarProps | |
|---|---|---|
| Drive the query | setFilters, setSort, setProject, setPage | — |
| Selection | selectedIds, toggleSelect | — |
| Data | data, isLoading, isRefetching, fetcher | isLoading |
So a replacement toolbar can hold controls that act on their own - navigate, call the SDK, open a dialog, kick off a download - but it cannot filter, sort, page or toggle columns, because the state those drive is never handed to it.
A trap this makes easy: dropping <MongoFilters schema={name} /> into a replacement toolbar compiles and renders a filter UI that does nothing. MongoFilters is fully controlled, so with no filters/onChange the selections have nowhere to go. If a toolbar needs to own filtering, replace the list itself through lists.tsx instead, where the query is yours.
Icons
Set the navigation icon per module or group:
export const icons: TIconsOverride = { posts: IconNotebook }Layouts
Register your own app shells, loaded on demand. Names here win over the built-ins:
export const layouts: TLayoutsOverride = {
compact: () => import("@/layouts/compact"),
}See Layouts & Theming.
The Two Big Ones
cards.tsx and routes.tsx do the most work and have their own pages:
Card Views
Replace the table with your own cards, and drive the query from your own UI.
Route Overrides
Hide, reorder, group, deep-link and rebuild routes - the most used extension point.
Replacing a Built-in Page
Built-in keys are overview, wallet and notifications. Supplying just a Component keeps the built-in's icon, priority and permission checks:
overview: { Component: MyDashboard },Breadcrumbs From Your Component
An overridden screen is still inside the layout shell, so it can set the trailing breadcrumb:
import { useBreadcrumbTitle } from "@/core/layouts/shared/breadcrumb"
useBreadcrumbTitle(post?.title)