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.

lists.tsx
cards.tsx
forms.tsx
views.tsx
list-actions.tsx
list-toolbar.tsx
icons.tsx
layouts.ts
routes.tsx

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:

src/overrides/crud/lists.tsx
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:

src/overrides/crud/forms.tsx
export const forms: TFormsOverride = { posts: PostForm }

Views

Supply a detail page for a module. Like forms, the component takes no props:

src/overrides/crud/views.tsx
export const views: TViewsOverride = { posts: PostDetail }

Read the record id from the route and fetch it however you like - data loading is entirely yours:

src/pages/posts/PostDetail.tsx
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:

src/overrides/crud/list-actions.tsx
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:

src/overrides/crud/list-toolbar.tsx
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:

TCardPropsTListToolbarProps
Drive the querysetFilters, setSort, setProject, setPage—
SelectionselectedIds, toggleSelect—
Datadata, isLoading, isRefetching, fetcherisLoading

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:

src/overrides/icons.tsx
export const icons: TIconsOverride = { posts: IconNotebook }

Layouts

Register your own app shells, loaded on demand. Names here win over the built-ins:

src/overrides/layouts.ts
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:

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:

src/overrides/routes.tsx
overview: { Component: MyDashboard },

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)

On this page