Supernova is a small library that plugs into a TanStack Start app and generates an admin dashboard from a Quark REST API.
It does not own your application. You keep a normal TanStack Start router, mount the Supernova route factories where you want the admin dashboard, and add custom CRUD views from your app code through a Vite glob registry.
pnpm install
pnpm example:devThe example listens on http://localhost:3015 and targets Quark on
http://localhost:3014 by default.
VITE_QUARK_API_URL=http://localhost:3014 pnpm example:devImport the CSS once from your app stylesheet:
@import 'tailwindcss' source('../');
@source '../../node_modules/@leanscript/supernova/src';
@import '@leanscript/supernova/styles.css';
@import './supernova.theme.css';You can also use one of the packaged themes instead of your local theme file:
@import 'tailwindcss' source('../');
@source '../../node_modules/@leanscript/supernova/src';
@import '@leanscript/supernova/styles.css';
@import '@leanscript/supernova/themes/tasty-crousty.css';Available packaged themes:
@leanscript/supernova/themes/tasty-crousty.css@leanscript/supernova/themes/neo-brutalism.css
Create a theme file next to your app stylesheet and override the Supernova UI tokens there. Because the theme is imported after Supernova, these values drive the glass surfaces, controls, focus states, and colors without changing the components:
/* src/styles/supernova.theme.css */
@layer base {
:root {
--supernova-background:
linear-gradient(135deg, #eef7fb 0%, #f8fbff 46%, #f4f2ff 100%);
--supernova-surface: rgb(255 255 255 / 0.66);
--supernova-surface-soft: rgb(255 255 255 / 0.44);
--supernova-line: rgb(83 101 132 / 0.24);
--supernova-blur: 20px;
--supernova-radius: 8px;
--supernova-button-radius: 999px;
--supernova-accent: #2563eb;
--supernova-success: #059669;
--supernova-danger: #e11d48;
}
}Keep TanStack Start's generated route tree out of your app source. Start still
expects a tiny internal root for its manifest, but your app router can use the
manual route-tree.tsx below:
// vite.config.ts
tanstackStart({
srcDirectory: 'src',
router: {
routesDirectory: 'start-routes',
generatedRouteTree: '../.tanstack/routeTree.gen.ts',
},
})// src/start-routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute()Create the Supernova router context. The API client and custom view registry are created for you:
import { createRouter } from '@tanstack/react-router'
import {
createSupernovaRouterContext,
type SupernovaRouterContext,
} from '@leanscript/supernova'
import { routeTree } from './route-tree'
export function getRouter() {
return createRouter({
routeTree,
context: createSupernovaRouterContext({
views: import.meta.glob('./supernova/**/*.tsx', { eager: true }),
}) satisfies SupernovaRouterContext,
})
}Create the app shell, then let Supernova register its dashboard routes:
// src/route-tree.tsx
import {
Outlet,
createRootRouteWithContext,
} from '@tanstack/react-router'
import {
SupernovaShell,
loadSupernovaShell,
registerSupernovaDashboard,
type SupernovaRouterContext,
type SupernovaShellData,
} from '@leanscript/supernova'
const rootRoute = createRootRouteWithContext<SupernovaRouterContext>()({
loader: ({ context }) => loadSupernovaShell(context.api),
component: RootRoute,
})
export const routeTree = registerSupernovaDashboard(rootRoute)
function RootRoute() {
const shell = rootRoute.useLoaderData() as SupernovaShellData
return (
<SupernovaShell {...shell}>
<Outlet />
</SupernovaShell>
)
}Then add app-owned custom CRUD views by convention:
// src/supernova/users/users.create.tsx
import { z } from 'zod'
import {
SupernovaForm,
createZodFormValidator,
getRecordId,
type QuarkRecord,
type SupernovaFieldDefinition,
type SupernovaResourceCreateData,
type SupernovaResourceViewProps,
} from '@leanscript/supernova'
const userFields = [
{ name: 'name', label: 'Nom', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'bio', label: 'Bio', type: 'textarea' },
{ name: 'score', label: 'Score', type: 'number', defaultValue: 0 },
{ name: 'birthDate', label: 'Date', type: 'date' },
{ name: 'avatar', label: 'Avatar', type: 'upload', accept: 'image/*' },
] satisfies SupernovaFieldDefinition[]
const userFormValidator = createZodFormValidator(
z
.object({
name: z.string().trim().min(1, 'Le nom est requis.'),
email: z.string().trim().email('Email invalide.'),
score: z.number().min(0, 'Le score doit être positif.'),
})
.passthrough(),
)
export default function UsersCreateView({
api,
resourceId,
actions,
}: SupernovaResourceViewProps<SupernovaResourceCreateData>) {
async function createUser(payload: QuarkRecord) {
const created = await api.resource(resourceId).create(payload)
const createdId = getRecordId(created)
await (createdId === null
? actions.navigateToIndex()
: actions.navigateToDetail(String(createdId)))
}
return (
<SupernovaForm
title="Utilisateur"
fields={userFields}
submitLabel="Créer"
variant="create"
validationAdapter={userFormValidator}
onSubmit={createUser}
/>
)
}Supported resource view filenames:
users/users.index.tsxorusers/users.list.tsxusers/users.create.tsx,users/users.add.tsx, orusers/users.new.tsxusers/users.detail.tsxorusers/users.show.tsxusers/users.edit.tsx
Field components:
SupernovaFormrenders create/edit forms from field definitions.SupernovaFieldrenders one controlled field.SupernovaFieldValuerenders one display value.SupernovaRecordFieldsrenders a display grid from a record.
Supported field types are text, email, url, password, textarea,
number, date, datetime, upload, select, checkbox, and json.
The upload field stores selected files as JSON data URLs, which is useful for
small admin payloads or prototyping before adding a dedicated file backend.
Form validation:
SupernovaFormacceptsvalidationAdapter.createZodFormValidator(schema)adapts a Zod schema to Supernova.- Zod issues are mapped to field errors by their first path segment.
- On success,
onSubmitreceives the validated payload.
Available helpers include:
context.api.openApi()context.api.discoverResources()context.api.resource('users').list({ page, filters, sort, with, fields })context.api.resource('users').get(id)context.api.resource('users').create(payload)context.api.resource('users').update(id, payload)context.api.resource('users').delete(id)context.api.request('/custom/path', { method, query, body })