**Config-driven, batteries-included admin UI for QUESTPIE CMS**
npm install @questpie/adminConfig-driven, batteries-included admin UI for QUESTPIE CMS
A fully-featured admin interface using the qa() builder pattern for type-safe configuration.
- Builder Pattern - Type-safe qa() builder with autocomplete
- Config-Driven - UI auto-generated from builder configuration
- Type-Safe - Full TypeScript inference with module augmentation
- Tailwind v4 - Built with Tailwind CSS v4 + shadcn/ui components
- Rich Text - Tiptap editor with toolbar controls
- Relations - Single select, multi-picker with drag-and-drop
- Layouts - Sections, tabs, columns, grids, sidebars
- Realtime - SSE-powered live updates
- Versioning - Built-in version history
- i18n Ready - Localization support for content
``bash`
bun add @questpie/admin questpie @questpie/tanstack-query
bun add @tanstack/react-query
`typescript
// src/admin/builder.ts
import { qa, adminModule } from "@questpie/admin/client";
import type { AppCMS } from "./server/cms";
// Create typed builder with admin fields/views
export const qab = qa
`
`typescript
// src/admin/collections/posts.ts
import { qab } from "../builder";
export const postsAdmin = qab
.collection("posts")
.meta({
label: "Blog Posts",
icon: FileTextIcon, // React component
})
// Fields with registry callback - ({ r }) gives autocomplete!
.fields(({ r }) => ({
title: r.text({
label: "Title",
placeholder: "Post title...",
maxLength: 200,
}),
slug: r.text({
label: "Slug",
}),
content: r.textarea({
label: "Content",
}),
status: r.select({
label: "Status",
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
}),
publishedAt: r.datetime({
label: "Publish Date",
}),
authorId: r.relation({
label: "Author",
targetCollection: "users",
}),
}))
// List view with field proxy - ({ v, f }) gives autocomplete!
.list(({ v, f }) =>
v.table({
columns: [f.title, f.status, f.authorId, f.publishedAt],
}),
)
// Form view
.form(({ v, f }) =>
v.form({
sections: [
{
title: "Content",
fields: [f.title, f.slug, f.content],
},
{
title: "Publishing",
fields: [f.status, f.publishedAt, f.authorId],
},
],
}),
);
`
`typescript
// src/admin/admin.ts
import { qa, adminModule } from "@questpie/admin/client";
import { postsAdmin } from "./collections/posts";
import { sidebarConfig } from "./sidebar";
export const admin = qa()
.use(adminModule)
.branding({
name: "My Admin",
})
.collections({
posts: postsAdmin,
})
.sidebar(sidebarConfig);
// Module augmentation for global type inference
declare module "@questpie/admin/client" {
interface AdminTypeRegistry {
cms: AppCMS;
admin: typeof admin;
}
}
`
`typescript
// src/admin/sidebar.ts
import { qa } from "@questpie/admin/client";
import { HouseIcon, FileTextIcon } from "@phosphor-icons/react";
export const sidebarConfig = qa
.sidebar()
.section("main", (s) =>
s.items([
{ type: "link", label: "Dashboard", href: "/admin", icon: HouseIcon },
]),
)
.section("content", (s) =>
s
.title("Content")
.icon(FileTextIcon)
.items([{ type: "collection", collection: "posts", icon: FileTextIcon }]),
);
`
In your main CSS file, import admin styles and configure Tailwind to scan the admin package:
`css
/ src/styles.css /
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@questpie/admin/styles/index.css";
/ IMPORTANT: Tell Tailwind to scan admin package for utility classes /
@source "../node_modules/@questpie/admin/dist";
/ Your app's theme variables... /
`
`tsx
// routes/admin.tsx (TanStack Router example)
import { Admin, AdminLayoutProvider } from "@questpie/admin/client";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { admin } from "~/admin/admin";
import { cmsClient } from "~/lib/cms-client";
import { queryClient } from "~/lib/query-client";
// Create runtime instance
const adminInstance = Admin.from(admin);
function AdminLayout() {
const location = useLocation();
return (
client={cmsClient}
queryClient={queryClient}
LinkComponent={Link}
activeRoute={location.pathname}
basePath="/admin"
>
);
}
`
`typescript
import { qa, adminModule } from "@questpie/admin/client";
// Start from admin module (includes built-in fields/views + user management)
const admin = qa()
.use(adminModule)
.branding({ name: "My Admin" })
.collections({ posts: postsAdmin })
.globals({ settings: settingsAdmin })
.sidebar(sidebarConfig)
.dashboard(dashboardConfig);
`
`typescript`
const postsAdmin = qa
.collection("posts")
.meta({ label: "Posts", icon: PostIcon })
.fields(({ r }) => ({
// r = FieldRegistryProxy with autocomplete for registered fields
title: r.text({ maxLength: 200 }),
content: r.richText(),
status: r.select({ options: [...] }),
}))
.list(({ v, f }) => v.table({
// v = ViewRegistryProxy, f = FieldProxy
columns: [f.title, f.status],
}))
.form(({ v, f }) => v.form({
fields: [f.title, f.content, f.status],
}));
`typescript`
const settingsAdmin = qa
.global("settings")
.meta({ label: "Site Settings", icon: SettingsIcon })
.fields(({ r }) => ({
siteName: r.text({ maxLength: 100 }),
logo: r.text(), // TODO: image field
}))
.form(({ v, f }) =>
v.form({
fields: [f.siteName, f.logo],
}),
);
`typescript`
const sidebarConfig = qa
.sidebar()
.section("main", (s) =>
s.items([
{ type: "link", label: "Dashboard", href: "/admin", icon: HomeIcon },
]),
)
.section("content", (s) =>
s
.title("Content")
.items([
{ type: "collection", collection: "posts" },
{ type: "collection", collection: "pages" },
{ type: "divider" },
{ type: "global", global: "settings" },
]),
);
Typed Sidebar (Recommended)
`typescript
import type { AppCMS } from "./server/cms";
import type { SidebarItemForApp } from "@questpie/admin/client";
const sidebarConfig = qa.sidebar
s.title("Content").items([
{ type: "collection", collection: "posts" },
{ type: "global", global: "settings" },
]),
);
// Standalone typed items
const items: SidebarItemForApp
{ type: "collection", collection: "posts" },
{ type: "global", global: "settings" },
];
`
The adminModule provides these field types:
| Field | Usage | Description |
| ---------- | ---------------------------------- | --------------------------- |
| text | r.text({ maxLength }) | Single-line text input |email
| | r.email() | Email input with validation |password
| | r.password() | Password input |textarea
| | r.textarea({ rows }) | Multi-line text |number
| | r.number({ min, max }) | Numeric input |checkbox
| | r.checkbox() | Boolean checkbox |switch
| | r.switch() | Toggle switch |select
| | r.select({ options }) | Dropdown select |date
| | r.date() | Date picker |datetime
| | r.datetime() | Date + time picker |relation
| | r.relation({ targetCollection }) | Relation field |json
| | r.json() | JSON editor |richText
| | r.richText() | Rich text editor (Tiptap) |
`typescript`
.form(({ v, f }) => v.form({
sections: [
{
title: "Basic Info",
description: "Main details",
fields: [f.title, f.slug],
collapsible: true,
defaultOpen: true,
},
{
title: "Content",
fields: [f.content],
},
],
}))
`typescript`
sections: [
{
title: "Contact",
layout: "columns",
columns: 2,
fields: [f.firstName, f.lastName, f.email, f.phone],
},
];
`typescript`
sections: [
{
layout: "grid",
grid: { columns: 4, gap: 4 },
fields: [
{ field: f.title, span: 4 }, // full width
{ field: f.price, span: 1 }, // 1/4
{ field: f.currency, span: 1 }, // 1/4
{ field: f.stock, span: 2 }, // 2/4
],
},
];
`typescript`
.form(({ v, f }) => v.form({
tabs: [
{
id: "content",
label: "Content",
fields: [f.title, f.content],
},
{
id: "meta",
label: "Metadata",
fields: [f.seo, f.tags],
},
],
}))
`typescript`
.form(({ v, f }) => v.form({
layout: "with-sidebar",
sections: [
{ title: "Content", fields: [f.title, f.content] },
],
sidebar: {
position: "right",
width: "300px",
fields: [f.status, f.publishedAt],
},
}))
`typescript`
.fields(({ r }) => ({
status: r.select({
options: [
{ label: "Active", value: "active" },
{ label: "Cancelled", value: "cancelled" },
],
}),
// Show only when status is "cancelled"
cancellationReason: r.textarea({
visible: (values) => values.status === "cancelled",
required: (values) => values.status === "cancelled",
}),
// Readonly unless draft
publishedAt: r.datetime({
readOnly: (values) => values.status !== "draft",
}),
}))
`typescript`
.fields(({ r }) => ({
// Single relation
authorId: r.relation({
label: "Author",
targetCollection: "users",
}),
// Multiple relations with ordering
tags: r.relation({
label: "Tags",
targetCollection: "tags",
type: "multiple",
orderable: true,
}),
}))
`typescript
import {
useAdminStore,
useAdminContext,
useCollectionList,
useCollectionItem,
useCollectionCreate,
useCollectionUpdate,
useCollectionDelete,
useGlobal,
useGlobalUpdate,
useAuthClient,
useAdminRoutes,
} from "@questpie/admin/client";
// Collection list with pagination
const { data, isLoading } = useCollectionList("posts", {
limit: 10,
offset: 0,
});
// Single item
const { data: post } = useCollectionItem("posts", id);
// CRUD mutations
const createPost = useCollectionCreate("posts");
const updatePost = useCollectionUpdate("posts");
const deletePost = useCollectionDelete("posts");
// Global data
const { data: settings } = useGlobal("settings");
const updateSettings = useGlobalUpdate("settings");
// Admin store
const { admin, client, basePath } = useAdminStore((s) => ({
admin: s.admin,
client: s.client,
basePath: s.basePath,
}));
// Auth
const authClient = useAuthClient();
const session = authClient.useSession();
// Routes
const { routes, navigate } = useAdminRoutes();
navigate({ collection: "posts", action: "create" });
`
`typescript
import {
// Navigation
AdminLink,
CollectionLink,
CollectionCreateLink,
CollectionEditLink,
GlobalLink,
DashboardLink,
// Fields
TextField,
EmailField,
NumberField,
SelectField,
CheckboxField,
SwitchField,
DateField,
RelationField,
JsonField,
// UI (shadcn)
Button,
Card,
Dialog,
Sheet,
Tabs,
Table,
// ... 27+ shadcn components
} from "@questpie/admin/client";
`
`typescript
// Main client exports (recommended)
import {
qa,
Admin,
AdminBuilder,
adminModule,
AdminProvider,
useAdminStore,
useCollectionList,
useAdminRoutes,
AdminLayoutProvider,
CollectionList,
CollectionForm,
} from "@questpie/admin/client";
// Server exports (for backend CMS setup)
import { adminModule } from "@questpie/admin/server";
// Styles
import "@questpie/admin/client/styles/index.css";
`
Built with Tailwind CSS v4 + shadcn/ui:
- 27+ pre-built components
- Light/dark theme support
- oklch color space
- Customizable via Tailwind tokens
- Styles scoped under .questpie-admin`
See tanstack-barbershop for a complete working example.
MIT