create-questpie 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/index.mjs +362 -119
  2. package/package.json +2 -3
  3. package/templates/elysia/AGENTS.md +56 -0
  4. package/templates/elysia/CLAUDE.md +39 -0
  5. package/templates/elysia/Dockerfile +24 -0
  6. package/templates/elysia/README.md +148 -0
  7. package/templates/elysia/docker/init-extensions.sql +11 -0
  8. package/templates/elysia/docker-compose.yml +21 -0
  9. package/templates/elysia/env.example +16 -0
  10. package/templates/elysia/gitignore +6 -0
  11. package/templates/elysia/package.json +47 -0
  12. package/templates/elysia/questpie.config.ts +12 -0
  13. package/templates/elysia/src/index.ts +21 -0
  14. package/templates/elysia/src/lib/auth-client.ts +32 -0
  15. package/templates/elysia/src/lib/client.ts +13 -0
  16. package/templates/elysia/src/lib/env.ts +24 -0
  17. package/templates/elysia/src/lib/query-client.ts +18 -0
  18. package/templates/elysia/src/lib/query.ts +18 -0
  19. package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
  20. package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
  21. package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
  22. package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
  23. package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
  24. package/templates/elysia/src/questpie/server/app.ts +10 -0
  25. package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
  26. package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
  27. package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
  28. package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
  29. package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
  30. package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
  31. package/templates/elysia/src/questpie/server/modules.ts +8 -0
  32. package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
  33. package/templates/elysia/tsconfig.json +28 -0
  34. package/templates/hono/AGENTS.md +56 -0
  35. package/templates/hono/CLAUDE.md +39 -0
  36. package/templates/hono/Dockerfile +24 -0
  37. package/templates/hono/README.md +148 -0
  38. package/templates/hono/docker/init-extensions.sql +11 -0
  39. package/templates/hono/docker-compose.yml +21 -0
  40. package/templates/hono/env.example +16 -0
  41. package/templates/hono/gitignore +6 -0
  42. package/templates/hono/package.json +47 -0
  43. package/templates/hono/questpie.config.ts +12 -0
  44. package/templates/hono/src/index.ts +30 -0
  45. package/templates/hono/src/lib/auth-client.ts +32 -0
  46. package/templates/hono/src/lib/client.ts +13 -0
  47. package/templates/hono/src/lib/env.ts +24 -0
  48. package/templates/hono/src/lib/query-client.ts +18 -0
  49. package/templates/hono/src/lib/query.ts +18 -0
  50. package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
  51. package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
  52. package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
  53. package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
  54. package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
  55. package/templates/hono/src/questpie/server/app.ts +10 -0
  56. package/templates/hono/src/questpie/server/collections/index.ts +1 -0
  57. package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
  58. package/templates/hono/src/questpie/server/config/auth.ts +8 -0
  59. package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
  60. package/templates/hono/src/questpie/server/globals/index.ts +1 -0
  61. package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
  62. package/templates/hono/src/questpie/server/modules.ts +8 -0
  63. package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
  64. package/templates/hono/tsconfig.json +28 -0
  65. package/templates/next/AGENTS.md +55 -0
  66. package/templates/next/CLAUDE.md +39 -0
  67. package/templates/next/Dockerfile +25 -0
  68. package/templates/next/README.md +148 -0
  69. package/templates/next/components.json +22 -0
  70. package/templates/next/docker/init-extensions.sql +11 -0
  71. package/templates/next/docker-compose.yml +21 -0
  72. package/templates/next/env.example +16 -0
  73. package/templates/next/gitignore +10 -0
  74. package/templates/next/next-env.d.ts +5 -0
  75. package/templates/next/next.config.ts +20 -0
  76. package/templates/next/package.json +54 -0
  77. package/templates/next/postcss.config.mjs +8 -0
  78. package/templates/next/public/.gitkeep +0 -0
  79. package/templates/next/questpie.config.ts +12 -0
  80. package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
  81. package/templates/next/src/app/admin/admin.css +4 -0
  82. package/templates/next/src/app/admin/layout.tsx +63 -0
  83. package/templates/next/src/app/api/[...all]/route.ts +24 -0
  84. package/templates/next/src/app/layout.tsx +24 -0
  85. package/templates/next/src/app/not-found.tsx +18 -0
  86. package/templates/next/src/app/page.tsx +74 -0
  87. package/templates/next/src/app/providers.tsx +11 -0
  88. package/templates/next/src/lib/auth-client.ts +12 -0
  89. package/templates/next/src/lib/client.ts +13 -0
  90. package/templates/next/src/lib/env.ts +24 -0
  91. package/templates/next/src/lib/query-client.ts +18 -0
  92. package/templates/next/src/lib/query.ts +18 -0
  93. package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
  94. package/templates/next/src/questpie/admin/admin.ts +9 -0
  95. package/templates/next/src/questpie/admin/modules.ts +3 -0
  96. package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
  97. package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
  98. package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
  99. package/templates/next/src/questpie/server/.generated/index.ts +139 -0
  100. package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
  101. package/templates/next/src/questpie/server/app.ts +10 -0
  102. package/templates/next/src/questpie/server/collections/index.ts +1 -0
  103. package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
  104. package/templates/next/src/questpie/server/config/admin.ts +80 -0
  105. package/templates/next/src/questpie/server/config/auth.ts +8 -0
  106. package/templates/next/src/questpie/server/config/openapi.ts +10 -0
  107. package/templates/next/src/questpie/server/globals/index.ts +1 -0
  108. package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
  109. package/templates/next/src/questpie/server/modules.ts +9 -0
  110. package/templates/next/src/questpie/server/questpie.config.ts +21 -0
  111. package/templates/next/src/styles.css +125 -0
  112. package/templates/next/tsconfig.json +37 -0
  113. package/templates/tanstack-start/AGENTS.md +35 -607
  114. package/templates/tanstack-start/CLAUDE.md +26 -134
  115. package/templates/tanstack-start/README.md +13 -1
  116. package/templates/tanstack-start/docker/init-extensions.sql +11 -0
  117. package/templates/tanstack-start/docker-compose.yml +1 -0
  118. package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
  119. package/templates/tanstack-start/src/lib/client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/query.ts +18 -0
  121. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  122. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  123. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  124. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  125. package/templates/tanstack-start/src/routes/api/$.ts +1 -1
  126. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  127. package/skills/questpie/AGENTS.md +0 -2871
  128. package/skills/questpie/SKILL.md +0 -293
  129. package/skills/questpie/coverage.json +0 -213
  130. package/skills/questpie/references/auth.md +0 -236
  131. package/skills/questpie/references/business-logic.md +0 -620
  132. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  133. package/skills/questpie/references/crud-api.md +0 -580
  134. package/skills/questpie/references/data-modeling.md +0 -509
  135. package/skills/questpie/references/extend.md +0 -584
  136. package/skills/questpie/references/field-types.md +0 -398
  137. package/skills/questpie/references/infrastructure-adapters.md +0 -720
  138. package/skills/questpie/references/mcp.md +0 -147
  139. package/skills/questpie/references/multi-tenancy.md +0 -363
  140. package/skills/questpie/references/production.md +0 -640
  141. package/skills/questpie/references/query-operators.md +0 -125
  142. package/skills/questpie/references/quickstart.md +0 -562
  143. package/skills/questpie/references/rules.md +0 -454
  144. package/skills/questpie/references/sandbox.md +0 -110
  145. package/skills/questpie/references/tanstack-query.md +0 -543
  146. package/skills/questpie/references/type-inference.md +0 -167
  147. package/skills/questpie/references/workflows.md +0 -155
  148. package/skills/questpie-admin/AGENTS.md +0 -1515
  149. package/skills/questpie-admin/SKILL.md +0 -443
  150. package/skills/questpie-admin/references/blocks.md +0 -331
  151. package/skills/questpie-admin/references/custom-ui.md +0 -305
  152. package/skills/questpie-admin/references/views.md +0 -449
@@ -1,1515 +0,0 @@
1
- # QUESTPIE Admin Panel
2
-
3
- The QUESTPIE admin panel is a **projection of your server schema** — not the framework itself. It reads collections, globals, and config via introspection and generates a full admin interface. Your backend works without it.
4
-
5
- ## Reference Topics
6
-
7
- | Topic | File | Covers |
8
- | --------- | ------------------------- | -------------------------------------------------------------------------------------- |
9
- | Views | `references/views.md` | List views, form views, dashboard, sidebar, filters, bulk actions, visibility, history |
10
- | Blocks | `references/blocks.md` | Block definitions, fields, prefetch, renderers, block picker |
11
- | Custom UI | `references/custom-ui.md` | Custom fields, custom views, registries, reactive fields, widgets |
12
-
13
- ## Full Compiled Document
14
-
15
- For the complete admin reference with all topics expanded: `AGENTS.md`
16
-
17
- ## Tech Stack
18
-
19
- - **React** + **Tailwind CSS v4** + **shadcn** components
20
- - **@base-ui/react** primitives (NOT @radix-ui)
21
- - **@iconify/react** with Phosphor icon set (`ph:icon-name`)
22
- - **sonner** for toasts — `toast.error()`, `toast.success()`
23
- - QUESTPIE Neutral Design: flat surfaces, soft neutral geometry,
24
- tokenized radius, and restrained floating shadows
25
-
26
- ## Setup
27
-
28
- ### 1. Install
29
-
30
- ```bash
31
- bun add @questpie/admin
32
- ```
33
-
34
- ### 2. Runtime Config
35
-
36
- ```ts title="questpie.config.ts"
37
- import { runtimeConfig } from "questpie/app";
38
- export default runtimeConfig({
39
- app: { url: process.env.APP_URL || "http://localhost:3000" },
40
- db: { url: process.env.DATABASE_URL },
41
- secret: process.env.APP_SECRET,
42
- });
43
- ```
44
-
45
- The admin module contributes the codegen plugin automatically. It discovers `config/admin.ts`, `blocks/`, views, components, and admin client modules.
46
-
47
- ### 3. Modules
48
-
49
- ```ts title="modules.ts"
50
- import { adminModule } from "@questpie/admin/modules/admin";
51
- import { auditModule } from "@questpie/admin/modules/audit";
52
-
53
- export default [adminModule, auditModule] as const;
54
- ```
55
-
56
- | Module | Provides |
57
- | ------------- | ------------------------------------- |
58
- | `adminModule` | User collection, auth pages, admin UI |
59
- | `auditModule` | Audit log collection, timeline widget |
60
-
61
- ### Auth/User Contract - Critical
62
-
63
- `adminModule` includes the starter auth model and owns the canonical Better Auth `user` collection shape used by admin setup and login guards. That contract includes `user.role` with at least `admin` and `user` values. The built-in setup route checks for `role = "admin"`, and the admin `AuthGuard` expects `session.user.role === "admin"`.
64
-
65
- Do not create a replacement `collection("user")` from scratch in an app that uses `adminModule`. If the app needs to customize the user admin UI or add fields, merge the starter user collection and extend it:
66
-
67
- ```ts title="collections/user.ts"
68
- import { starterModule } from "questpie/app";
69
- import { collection } from "#questpie/factories";
70
-
71
- export default collection("user")
72
- .merge(starterModule.collections.user)
73
- .fields(({ f }) => ({
74
- // add app-specific user fields here
75
- internalNotes: f.textarea(),
76
- }));
77
- ```
78
-
79
- App-specific authorization tables are fine, but they must not replace or remove the admin user contract unless the app also replaces the built-in admin setup route and auth guard.
80
-
81
- ### 4. Admin Config
82
-
83
- ```ts title="config/admin.ts"
84
- import { adminConfig } from "#questpie/factories";
85
-
86
- export default adminConfig({
87
- branding: {
88
- name: { en: "My App Admin" },
89
- },
90
- sidebar: {
91
- sections: [
92
- { id: "overview", title: { en: "Overview" } },
93
- { id: "content", title: { en: "Content" } },
94
- ],
95
- items: [
96
- {
97
- sectionId: "overview",
98
- type: "link",
99
- label: { en: "Dashboard" },
100
- href: "/admin",
101
- icon: { type: "icon", props: { name: "ph:house" } },
102
- },
103
- {
104
- sectionId: "content",
105
- type: "collection",
106
- collection: "posts",
107
- },
108
- ],
109
- },
110
- });
111
- ```
112
-
113
- ### 5. Codegen
114
-
115
- ```bash
116
- bunx questpie generate
117
- ```
118
-
119
- ### 6. Mount the Admin
120
-
121
- ```ts title="routes/admin/$.tsx"
122
- import { AdminRouter } from "@questpie/admin/client";
123
- import { admin } from "@/questpie/admin/admin";
124
-
125
- export default function AdminPage() {
126
- return <AdminRouter admin={admin} />;
127
- }
128
- ```
129
-
130
- The admin client config is auto-generated by codegen at `admin/.generated/client.ts`. No manual builder setup needed.
131
-
132
- ## Branding
133
-
134
- ```ts title="config/admin.ts"
135
- import { adminConfig } from "#questpie/factories";
136
-
137
- export default adminConfig({
138
- branding: {
139
- name: { en: "Barbershop Control", sk: "Riadenie barbershopu" },
140
- },
141
- });
142
- ```
143
-
144
- | Option | Type | Description |
145
- | ------ | ---------------- | -------------------------------- |
146
- | `name` | `string \| i18n` | App name shown in sidebar header |
147
- | `logo` | `string` | Logo URL or path |
148
-
149
- ## Theming (CSS Variables)
150
-
151
- The admin uses CSS variables for all theming. Override them in your own CSS file.
152
-
153
- The full source of truth is `packages/admin/DESIGN.md`. Key defaults:
154
-
155
- | Role | Dark | Light |
156
- | ------------- | --------- | --------- |
157
- | Background | `#121212` | `#fafafa` |
158
- | Foreground | `#ececec` | `#1c1c1c` |
159
- | Card | `#1b1b1b` | `#ffffff` |
160
- | Surface high | `#2a2a2a` | `#e8e8e8` |
161
- | Border | `#343434` | `#e2e2e2` |
162
- | Border subtle | `#262626` | `#ebebeb` |
163
- | Brand primary | `#b700ff` | `#b700ff` |
164
-
165
- | Token | Default | Use |
166
- | ------------------------ | ------- | ------------------------------------------- |
167
- | `--control-radius-inner` | `8px` | Icon buttons/actions nested inside controls |
168
- | `--control-radius` | `12px` | Inputs, selects, buttons, compact controls |
169
- | `--surface-radius` | `14px` | Cards, panels, grouped fields, docs blocks |
170
- | `--floating-radius` | `14px` | Menus, popovers, dialogs, command panels |
171
-
172
- ### Typography
173
-
174
- | Variable | Value |
175
- | ------------- | ------------------------------------------------------------------- |
176
- | `--font-sans` | `"Geist Variable"` — UI, prose, headings, labels, navigation |
177
- | `--font-mono` | `"JetBrains Mono Variable"` — code, file paths, commands, IDs |
178
-
179
- ### Sidebar Variables
180
-
181
- Separate tokens for independent sidebar theming: `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, `--sidebar-accent`, `--sidebar-border`, `--sidebar-ring`.
182
-
183
- ### Custom Theme
184
-
185
- 1. Copy the admin CSS file
186
- 2. Change variable values
187
- 3. Import your copy instead
188
- 4. Zero component changes needed
189
-
190
- ## Content Localization
191
-
192
- When collections have `.localized()` fields, the admin shows a locale switcher:
193
-
194
- ```ts title="config/app.ts"
195
- import { appConfig } from "questpie/app";
196
- export default appConfig({
197
- locale: {
198
- locales: [
199
- { code: "en", label: { en: "English" }, flagCountryCode: "gb" },
200
- { code: "sk", label: { en: "Slovak" } },
201
- ],
202
- defaultLocale: "en",
203
- },
204
- });
205
- ```
206
-
207
- The admin tracks content locale separately from UI locale. Only localized field values change when switching.
208
-
209
- ## Media & Uploads
210
-
211
- ```ts
212
- avatar: f.upload({
213
- to: "assets",
214
- mimeTypes: ["image/*"],
215
- maxSize: 5_000_000,
216
- }),
217
- ```
218
-
219
- The admin renders drag-and-drop upload, image preview, file info, and remove button.
220
-
221
- ## Live Preview
222
-
223
- Live Preview is one system: the existing collection `FormView`, Preview button, `LivePreviewMode`, and frontend iframe. Preserve the normal form lifecycle. Do not introduce a separate visual-edit form API, a second default form view, or a parallel preview surface.
224
-
225
- The admin form is authoritative. The iframe mirrors form state through `postMessage`, supports field/block focus, and may request inline scalar edits. Persistence still goes through existing save, autosave, Cmd+S, history, workflow, locks, and actions.
226
-
227
- ### Server Config
228
-
229
- Add `.preview()` to a collection to enable split-screen editing:
230
-
231
- ```ts title="collections/pages.ts"
232
- export const pages = collection("pages")
233
- .fields(({ f }) => ({
234
- title: f.text().required().localized(),
235
- slug: f.text().required(),
236
- content: f.blocks().localized(),
237
- }))
238
- .preview({
239
- enabled: true,
240
- position: "right",
241
- defaultWidth: 50,
242
- url: ({ record }) => {
243
- const slug = record.slug as string;
244
- return slug === "home" ? "/?preview=true" : `/${slug}?preview=true`;
245
- },
246
- });
247
- ```
248
-
249
- Preview opens the existing split-screen editor. Patches and refresh/resync messages update the iframe mirror; save/autosave still writes through the form.
250
-
251
- ### Prepare a Frontend Page
252
-
253
- Use exported APIs only: `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and `BlockRenderer`.
254
-
255
- Checklist:
256
-
257
- 1. Configure `.preview({ url })` on the collection.
258
- 2. Load the same record shape the page normally renders.
259
- 3. For workflow-published pages, public reads use `stage: "published"`; if the public client/HTTP API is exposed, anonymous read access also checks `input?.stage === "published"`. Authorized preview/draft reads can load the working record.
260
- 4. Call `useCollectionPreview({ initialData, onRefresh })` in the page renderer.
261
- 5. Wrap the visual output in `PreviewProvider`.
262
- 6. Render from `preview.data`, not the original loader object.
263
- 7. Wrap editable scalar text with `PreviewField field="..." editable="text" | "textarea"`.
264
- 8. Render block content with `BlockRenderer`; pass `selectedBlockId` and `onBlockClick`.
265
- 9. Keep add/remove/reorder/nesting block operations in the existing block editor.
266
-
267
- ```tsx
268
- import {
269
- BlockRenderer,
270
- PreviewField,
271
- PreviewProvider,
272
- useCollectionPreview,
273
- } from "@questpie/admin/client";
274
- import admin from "@/questpie/admin/.generated/client";
275
-
276
- function PagePreview({ page }) {
277
- const router = useRouter();
278
- const preview = useCollectionPreview({
279
- initialData: page,
280
- onRefresh: () => router.invalidate(),
281
- });
282
-
283
- return (
284
- <PreviewProvider preview={preview}>
285
- <main className={preview.isPreviewMode ? "questpie-preview" : undefined}>
286
- <PreviewField field="title" editable="text" as="h1">
287
- {preview.data.title}
288
- </PreviewField>
289
-
290
- <BlockRenderer
291
- content={preview.data.content}
292
- data={preview.data.content?._data}
293
- renderers={admin.blocks}
294
- selectedBlockId={preview.selectedBlockId}
295
- onBlockClick={
296
- preview.isPreviewMode ? preview.handleBlockClick : undefined
297
- }
298
- />
299
- </main>
300
- </PreviewProvider>
301
- );
302
- }
303
- ```
304
-
305
- ### Key Principles
306
-
307
- - Keep `FormView`, the Preview button, and `LivePreviewMode`
308
- - Never add a separate visual-edit form API, a second default form view, or parallel preview API names
309
- - Preserve save/autosave/Cmd+S/history/workflow/locks/actions
310
- - `useCollectionPreview` handles preview mode, mirrored data, refresh/resync, and focus state
311
- - `PreviewProvider` supplies preview context to `PreviewField`
312
- - `PreviewField` annotates scalar fields and can opt into inline editing with `editable`
313
- - `BlockRenderer` preserves block IDs and block scopes for block annotations
314
- - Use `BlockScopeProvider` only for custom/manual block rendering outside `BlockRenderer`
315
- - Validate all iframe messages before updating form state
316
-
317
- ## History & Versions
318
-
319
- Enable `auditModule` for activity timeline. Enable versioning on collections for snapshot restore:
320
-
321
- ```ts
322
- export const pages = collection("pages")
323
- .fields(({ f }) => ({ ... }))
324
- .options({ versioning: true });
325
- ```
326
-
327
- ## Scope (Multi-Tenancy)
328
-
329
- The admin provides scope primitives for multi-tenant applications. Import from `@questpie/admin/client`.
330
-
331
- ### ScopeProvider
332
-
333
- Wraps the admin to enable scope selection. Manages scope ID in React state and persists to localStorage.
334
-
335
- ```tsx
336
- import { ScopeProvider } from "@questpie/admin/client";
337
-
338
- <ScopeProvider
339
- headerName="x-selected-workspace" // HTTP header for scope ID
340
- storageKey="admin-workspace" // localStorage key
341
- defaultScope={null} // default value
342
- >
343
- <AdminLayout>...</AdminLayout>
344
- </ScopeProvider>;
345
- ```
346
-
347
- ### ScopePicker
348
-
349
- Dropdown for selecting the current scope. Place in sidebar via `slots.afterBrand`:
350
-
351
- ```tsx
352
- import { ScopePicker } from "@questpie/admin/client";
353
-
354
- <AdminLayout
355
- admin={admin}
356
- basePath="/admin"
357
- slots={{
358
- afterBrand: (
359
- <div className="px-3 py-2 border-b">
360
- <ScopePicker
361
- collection="workspaces"
362
- labelField="name"
363
- allowClear
364
- compact
365
- />
366
- </div>
367
- ),
368
- }}
369
- />;
370
- ```
371
-
372
- Three data sources: `collection` (queries a collection), `options` (static array), `loadOptions` (async function).
373
-
374
- ### useScopedFetch / createScopedFetch
375
-
376
- Inject scope header into all API calls:
377
-
378
- ```tsx
379
- import { useScopedFetch, createScopedFetch } from "@questpie/admin/client";
380
-
381
- // React hook
382
- const scopedFetch = useScopedFetch();
383
- const client = useMemo(
384
- () => createClient({ baseURL: "/api", fetch: scopedFetch }),
385
- [scopedFetch],
386
- );
387
-
388
- // Non-React
389
- const scopedFetch = createScopedFetch("x-selected-workspace", () =>
390
- getScopeId(),
391
- );
392
- ```
393
-
394
- ### useScope / useScopeSafe
395
-
396
- Access current scope state in any component:
397
-
398
- ```tsx
399
- import { useScope, useScopeSafe } from "@questpie/admin/client";
400
-
401
- const { scopeId, setScope, clearScope, headerName } = useScope(); // throws outside ScopeProvider
402
- const scope = useScopeSafe(); // returns null outside ScopeProvider
403
- ```
404
-
405
- For the full server-side setup (context resolver, type augmentation, access rules), see the `questpie` skill's `references/multi-tenancy.md`.
406
-
407
- ## Common Mistakes
408
-
409
- 1. **CRITICAL: Using `asChild` prop** — QUESTPIE admin uses `@base-ui/react`, which uses the `render` prop. `asChild` is a Radix pattern and does NOT work here.
410
-
411
- ```tsx
412
- // WRONG
413
- <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
414
- // CORRECT
415
- <DialogTrigger render={<Button>Open</Button>} />
416
- ```
417
-
418
- 2. **CRITICAL: Importing from `@radix-ui/*`** — use `@base-ui/react` instead.
419
-
420
- 3. **HIGH: Using `@phosphor-icons/react`** — use `@iconify/react` with `ph:` prefix.
421
-
422
- ```tsx
423
- // WRONG
424
- import { CaretDown } from "@phosphor-icons/react";
425
- // CORRECT
426
- import { Icon } from "@iconify/react";
427
- <Icon icon="ph:caret-down" width={16} height={16} />;
428
- ```
429
-
430
- 4. **HIGH: Using lucide-react icons** — use `@iconify/react` with Phosphor icon set.
431
-
432
- 5. **MEDIUM: Custom `<button>` or `<div>` instead of shadcn components** — use `<Button>`, `<Card>`, etc.
433
-
434
- 6. **MEDIUM: `console.error` for user errors** — use `toast.error()` from `sonner`.
435
-
436
- ---
437
-
438
- # QUESTPIE Admin Views
439
-
440
- This skill builds on questpie-admin.
441
-
442
- Views control how data appears in the QUESTPIE admin panel. They are configured **server-side** on collections and globals, then rendered by the admin client via registries.
443
-
444
- ```text
445
- Server Config Admin UI
446
- .list(({ v }) => v.collectionTable({})) -> Table with columns, sort, search
447
- .form(({ v, f }) => v.collectionForm({})) -> Form with sections, sidebar, tabs
448
- sidebar({...}) -> Navigation sidebar
449
- dashboard({...}) -> Dashboard with widgets
450
- ```
451
-
452
- ## List Views
453
-
454
- Configure table views with `.list()` on a collection.
455
-
456
- ### Basic Table
457
-
458
- ```ts
459
- .list(({ v }) => v.collectionTable({}))
460
- ```
461
-
462
- Shows all fields as columns with default rendering.
463
-
464
- ### Custom Columns and Search
465
-
466
- ```ts
467
- .list(({ v, f }) =>
468
- v.collectionTable({
469
- columns: [f.name, f.email, f.isActive, f.createdAt],
470
- searchableFields: [f.name, f.email],
471
- defaultSort: { field: f.createdAt, direction: "desc" },
472
- }),
473
- )
474
- ```
475
-
476
- | Option | Type | Description |
477
- | ------------------ | ---------------------- | ------------------------------ |
478
- | `columns` | `Field[]` | Fields to show as columns |
479
- | `searchableFields` | `Field[]` | Fields included in text search |
480
- | `defaultSort` | `{ field, direction }` | Default sort order |
481
-
482
- ## Form Views
483
-
484
- Configure edit forms with `.form()`.
485
-
486
- ### Basic Form
487
-
488
- ```ts
489
- .form(({ v, f }) => v.collectionForm({}))
490
- ```
491
-
492
- Renders all fields in a single column.
493
-
494
- ### Sections
495
-
496
- Group fields into labeled sections with optional grid layout:
497
-
498
- ```ts
499
- .form(({ v, f }) =>
500
- v.collectionForm({
501
- fields: [
502
- {
503
- type: "section",
504
- label: { en: "Contact Information" },
505
- layout: "grid",
506
- columns: 2,
507
- fields: [f.name, f.email, f.phone],
508
- },
509
- {
510
- type: "section",
511
- label: { en: "Profile" },
512
- fields: [f.bio],
513
- },
514
- ],
515
- }),
516
- )
517
- ```
518
-
519
- | Option | Type | Description |
520
- | ------------- | ------------------- | ------------------------------------ |
521
- | `type` | `"section"` | Required |
522
- | `label` | `string \| i18n` | Section heading |
523
- | `description` | `string \| i18n` | Section description |
524
- | `layout` | `"grid" \| "stack"` | Field layout |
525
- | `columns` | `number` | Grid columns (with `layout: "grid"`) |
526
- | `fields` | `Field[]` | Fields in this section |
527
-
528
- ### Form Sidebar
529
-
530
- Place fields in a right sidebar panel:
531
-
532
- ```ts
533
- .form(({ v, f }) =>
534
- v.collectionForm({
535
- sidebar: {
536
- position: "right",
537
- fields: [f.isActive, f.avatar, f.status],
538
- },
539
- fields: [ /* main content sections */ ],
540
- }),
541
- )
542
- ```
543
-
544
- ### Computed Fields
545
-
546
- Auto-compute values from other fields:
547
-
548
- ```ts
549
- {
550
- field: f.slug,
551
- compute: {
552
- handler: ({ data }) => {
553
- if (data.name && !data.slug?.trim()) {
554
- return slugify(data.name);
555
- }
556
- return undefined;
557
- },
558
- deps: ({ data }) => [data.name, data.slug],
559
- debounce: 300,
560
- },
561
- }
562
- ```
563
-
564
- | Option | Type | Description |
565
- | ---------- | ---------------- | ------------------------ |
566
- | `handler` | `(ctx) => value` | Compute function |
567
- | `deps` | `(ctx) => any[]` | Reactive dependencies |
568
- | `debounce` | `number` | Debounce in milliseconds |
569
-
570
- ### Conditional Visibility
571
-
572
- Show or hide fields based on other field values:
573
-
574
- ```ts
575
- {
576
- field: f.cancellationReason,
577
- hidden: ({ data }) => data.status !== "cancelled",
578
- }
579
- ```
580
-
581
- Read-only fields:
582
-
583
- ```ts
584
- {
585
- field: f.customerName,
586
- readOnly: ({ data }) => !!data.customer,
587
- }
588
- ```
589
-
590
- Section-level visibility:
591
-
592
- ```ts
593
- {
594
- type: "section",
595
- label: { en: "SEO" },
596
- hidden: ({ data }) => !data.showSeo,
597
- fields: [f.metaTitle, f.metaDescription],
598
- }
599
- ```
600
-
601
- ## Dashboard
602
-
603
- Configure in `config/admin.ts` under the `dashboard` key:
604
-
605
- ```ts title="config/admin.ts"
606
- import { adminConfig } from "#questpie/factories";
607
-
608
- export default adminConfig({
609
- dashboard: {
610
- title: { en: "Dashboard" },
611
- description: { en: "Overview of your app" },
612
- columns: 4,
613
- actions: [
614
- {
615
- id: "new-post",
616
- href: "/admin/collections/posts?create=true",
617
- label: { en: "New Post" },
618
- icon: { type: "icon", props: { name: "ph:plus" } },
619
- variant: "primary",
620
- },
621
- ],
622
- sections: [
623
- { id: "today", label: { en: "Today" }, layout: "grid", columns: 4 },
624
- { id: "business", label: { en: "Business" }, layout: "grid", columns: 4 },
625
- ],
626
- items: [
627
- /* widget items — see widget types below */
628
- ],
629
- },
630
- });
631
- ```
632
-
633
- ### Widget Types
634
-
635
- **Stats** — count records with optional filter:
636
-
637
- ```ts
638
- {
639
- sectionId: "today",
640
- id: "pending",
641
- type: "stats",
642
- collection: "appointments",
643
- label: { en: "Pending" },
644
- filter: { status: "pending" },
645
- span: 1,
646
- }
647
- ```
648
-
649
- **Value** — custom-loaded value with trend:
650
-
651
- ```ts
652
- {
653
- sectionId: "business",
654
- id: "revenue",
655
- type: "value",
656
- span: 2,
657
- refreshInterval: 1000 * 60 * 5,
658
- loader: async ({ app }) => ({
659
- value: 42000,
660
- formatted: "42,000 EUR",
661
- label: { en: "Monthly Revenue" },
662
- trend: { value: "+12%" },
663
- }),
664
- }
665
- ```
666
-
667
- **Progress** — progress bar toward a goal:
668
-
669
- ```ts
670
- {
671
- sectionId: "business",
672
- id: "goal",
673
- type: "progress",
674
- span: 1,
675
- showPercentage: true,
676
- label: { en: "Monthly Goal" },
677
- loader: async ({ app }) => ({ current: 350, target: 500 }),
678
- }
679
- ```
680
-
681
- **Chart** — chart from field values:
682
-
683
- ```ts
684
- {
685
- sectionId: "business",
686
- id: "by-status",
687
- type: "chart",
688
- collection: "appointments",
689
- field: "status",
690
- chartType: "pie",
691
- label: { en: "By Status" },
692
- span: 1,
693
- }
694
- ```
695
-
696
- **Recent Items** — list recent records:
697
-
698
- ```ts
699
- {
700
- sectionId: "ops",
701
- id: "recent",
702
- type: "recentItems",
703
- collection: "appointments",
704
- label: { en: "Recent" },
705
- limit: 6,
706
- dateField: "scheduledAt",
707
- span: 2,
708
- }
709
- ```
710
-
711
- **Timeline** — activity stream:
712
-
713
- ```ts
714
- {
715
- sectionId: "ops",
716
- id: "activity",
717
- type: "timeline",
718
- label: { en: "Activity" },
719
- maxItems: 8,
720
- showTimestamps: true,
721
- timestampFormat: "relative",
722
- loader: async ({ app }) => {
723
- const res = await app.collections.appointments.find({
724
- limit: 8,
725
- orderBy: { updatedAt: "desc" },
726
- });
727
- return res.docs.map((apt) => ({
728
- id: apt.id,
729
- title: apt.displayTitle,
730
- description: `Status: ${apt.status}`,
731
- timestamp: apt.updatedAt,
732
- variant: apt.status === "completed" ? "success" : "warning",
733
- href: `/admin/collections/appointments/${apt.id}`,
734
- }));
735
- },
736
- span: 2,
737
- }
738
- ```
739
-
740
- ## Sidebar
741
-
742
- Configure in `config/admin.ts` under the `sidebar` key:
743
-
744
- ```ts title="config/admin.ts"
745
- import { adminConfig } from "#questpie/factories";
746
-
747
- export default adminConfig({
748
- sidebar: {
749
- sections: [
750
- { id: "overview", title: { en: "Overview" } },
751
- { id: "content", title: { en: "Content" } },
752
- { id: "external", title: { en: "External" } },
753
- ],
754
- items: [
755
- {
756
- sectionId: "overview",
757
- type: "link",
758
- label: { en: "Dashboard" },
759
- href: "/admin",
760
- icon: { type: "icon", props: { name: "ph:house" } },
761
- },
762
- { sectionId: "overview", type: "global", global: "siteSettings" },
763
- { sectionId: "content", type: "collection", collection: "posts" },
764
- {
765
- sectionId: "external",
766
- type: "link",
767
- label: { en: "Open Website" },
768
- href: "/",
769
- external: true,
770
- icon: { type: "icon", props: { name: "ph:arrow-square-out" } },
771
- },
772
- ],
773
- },
774
- });
775
- ```
776
-
777
- Items appear in definition order within their section.
778
-
779
- Modules contribute sidebar items automatically. `adminModule` adds "Administration" with user management. `auditModule` adds an audit log item.
780
-
781
- ## Filters & Saved Views
782
-
783
- Filters are auto-generated from field definitions:
784
-
785
- | Field Type | Filter Operators |
786
- | ------------------- | ---------------------------------------- |
787
- | `text` | Contains, equals, starts with, ends with |
788
- | `number` | Equals, greater than, less than, between |
789
- | `boolean` | Is true, is false |
790
- | `select` | Is, is not, in |
791
- | `date` / `datetime` | Before, after, between |
792
- | `relation` | Is (picker) |
793
-
794
- Users can save filter + sort + column combinations as named views.
795
-
796
- ## Bulk Actions
797
-
798
- List views support multi-select. Check rows, then use the floating toolbar. Built-in: **Delete** (with confirmation). Soft-delete collections soft-delete instead of permanent removal.
799
-
800
- ## History & Versions
801
-
802
- Click the clock icon in the form toolbar. Two tabs:
803
-
804
- | Tab | Shows | Requires |
805
- | ------------ | ---------------------------------------------- | ----------------------------- |
806
- | **Activity** | Audit log (create, update, delete, transition) | `auditModule` |
807
- | **Versions** | Full document snapshots with restore | `.versioning()` on collection |
808
-
809
- Enable versioning:
810
-
811
- ```ts
812
- export const pages = collection("pages")
813
- .fields(({ f }) => ({ ... }))
814
- .options({ versioning: true });
815
- ```
816
-
817
- Disable audit for a specific collection:
818
-
819
- ```ts
820
- export const logs = collection("logs")
821
- .admin(() => ({ audit: false }))
822
- .fields(({ f }) => ({ ... }));
823
- ```
824
-
825
- ## Common Mistakes
826
-
827
- 1. **HIGH: Defining columns that don't match field names** — `columns: [f.name]` requires a `name` field in the collection's `.fields()`. Mismatches cause empty columns.
828
-
829
- 2. **MEDIUM: Not specifying `searchableFields`** — table search bar won't work unless you explicitly list which fields to search.
830
-
831
- 3. **MEDIUM: Forgetting sidebar section ordering** — items appear in definition order. If you want "Dashboard" at the top, define it first in the `items` array.
832
-
833
- 4. **MEDIUM: Missing `sectionId` on sidebar items** — every item must reference an existing section ID.
834
-
835
- 5. **LOW: Not setting `defaultSort`** — records appear in database insertion order which is usually not what users expect.
836
-
837
- ## Form Views and Live Preview
838
-
839
- Form views connect to the existing Live Preview system when the collection has `.preview()` configured. Keep `v.collectionForm()` as the form surface; do not introduce a separate visual-edit form API, a second default form view, or parallel preview API names. The form editor remains the source of patches, refreshes, commits, and resyncs.
840
-
841
- ### Enabling Preview on a Collection
842
-
843
- Add `.preview()` to the collection definition:
844
-
845
- ```ts
846
- export const pages = collection("pages")
847
- .fields(({ f }) => ({
848
- title: f.text().required().localized(),
849
- slug: f.text().required(),
850
- content: f.blocks().localized(),
851
- }))
852
- .preview({
853
- enabled: true,
854
- position: "right",
855
- defaultWidth: 50,
856
- url: ({ record }) => `/${record.slug}?preview=true`,
857
- });
858
- ```
859
-
860
- ### How It Works
861
-
862
- 1. The form view detects `.preview()` config and opens a split-screen layout
863
- 2. The preview iframe mirrors form state with snapshot, patch, refresh, commit, and resync messages
864
- 3. Save/autosave/Cmd+S/history/workflow/locks/actions stay in the form lifecycle
865
- 4. The preview page uses `useCollectionPreview({ initialData, onRefresh })`
866
- 5. `PreviewProvider`, `PreviewField`, and `BlockRenderer` wire field and block annotations
867
-
868
- ### Frontend Preparation Checklist
869
-
870
- Use this checklist before expecting visual editing to work:
871
-
872
- 1. The collection has `.preview({ url })`.
873
- 2. The page loader returns the complete rendered record shape.
874
- 3. Public workflow reads use `stage: "published"`; if public client/HTTP access is enabled, anonymous read access also requires `input?.stage === "published"`. Preview/draft-mode reads load the working record for authorized editors.
875
- 4. The page renderer calls `useCollectionPreview({ initialData, onRefresh })`.
876
- 5. The rendered page is wrapped in `PreviewProvider preview={preview}`.
877
- 6. UI reads from `preview.data`, not directly from loader data.
878
- 7. Scalar text uses `PreviewField field="..." editable="text" | "textarea"` when inline editing is expected.
879
- 8. Blocks render through `BlockRenderer` with `selectedBlockId` and `onBlockClick`.
880
- 9. `BlockScopeProvider` is only needed for custom/manual block rendering outside `BlockRenderer`.
881
- 10. Add/remove/reorder/nesting block operations stay in the existing block editor.
882
-
883
- ---
884
-
885
- # QUESTPIE Blocks
886
-
887
- This skill builds on questpie-admin.
888
-
889
- Blocks are reusable content components for page builders. Define them server-side with fields and admin metadata, then render them client-side with React components.
890
-
891
- ```text
892
- Server: block("hero") Client: HeroRenderer
893
- .fields({ title, image }) -> Receives { values, data }
894
- .admin({ label, icon }) Returns JSX
895
- .prefetch({ with: {...} })
896
- ```
897
-
898
- ## Defining Blocks
899
-
900
- Blocks are defined in `blocks/` using the `block()` factory:
901
-
902
- ```ts title="blocks/hero.ts"
903
- import { block } from "#questpie/factories";
904
-
905
- export const heroBlock = block("hero")
906
- .admin(({ c }) => ({
907
- label: { en: "Hero Section", sk: "Hero sekcia" },
908
- icon: c.icon("ph:image"),
909
- category: "sections",
910
- }))
911
- .fields(({ f }) => ({
912
- title: f.text().localized().required(),
913
- subtitle: f.textarea().localized(),
914
- backgroundImage: f.upload({ to: "assets" }),
915
- overlayOpacity: f.number().default(60),
916
- alignment: f
917
- .select([
918
- { value: "left", label: "Left" },
919
- { value: "center", label: "Center" },
920
- { value: "right", label: "Right" },
921
- ])
922
- .default("center"),
923
- ctaText: f.text().localized(),
924
- ctaLink: f.text(),
925
- }))
926
- .prefetch({ with: { backgroundImage: true } });
927
- ```
928
-
929
- ### Admin Metadata
930
-
931
- ```ts
932
- .admin(({ c }) => ({
933
- label: { en: "Hero Section" }, // Display name in block picker
934
- icon: c.icon("ph:image"), // Icon in block picker (Phosphor set)
935
- category: "sections", // Group in block picker
936
- }))
937
- ```
938
-
939
- ### Multiple Blocks Per File
940
-
941
- Export multiple named blocks from one file:
942
-
943
- ```ts title="blocks/layout.ts"
944
- import { block } from "#questpie/factories";
945
-
946
- export const twoColumnBlock = block("twoColumn")
947
- .admin(({ c }) => ({
948
- label: { en: "Two Columns" },
949
- icon: c.icon("ph:columns"),
950
- category: "layout",
951
- }))
952
- .fields(({ f }) => ({
953
- left: f.blocks(),
954
- right: f.blocks(),
955
- }));
956
-
957
- export const spacerBlock = block("spacer")
958
- .admin(({ c }) => ({
959
- label: { en: "Spacer" },
960
- icon: c.icon("ph:arrows-out-line-vertical"),
961
- category: "layout",
962
- }))
963
- .fields(({ f }) => ({
964
- height: f
965
- .select([
966
- { value: "sm", label: "Small" },
967
- { value: "md", label: "Medium" },
968
- { value: "lg", label: "Large" },
969
- { value: "xl", label: "Extra Large" },
970
- ])
971
- .default("md"),
972
- }));
973
- ```
974
-
975
- ## Using Blocks in Collections
976
-
977
- Add a `blocks` field to any collection:
978
-
979
- ```ts title="collections/pages.ts"
980
- import { collection } from "#questpie/factories";
981
-
982
- export const pages = collection("pages").fields(({ f }) => ({
983
- title: f.text().required().localized(),
984
- slug: f.text().required(),
985
- content: f.blocks().localized(),
986
- }));
987
- ```
988
-
989
- The admin renders a visual block editor for this field.
990
-
991
- ## Prefetch
992
-
993
- Blocks often reference related data (images, linked records). Use `.prefetch()` to load them alongside block values.
994
-
995
- ### Declarative Prefetch
996
-
997
- ```ts
998
- .prefetch({
999
- with: {
1000
- backgroundImage: true, // Load the full image record
1001
- },
1002
- })
1003
- ```
1004
-
1005
- ### Nested Prefetch
1006
-
1007
- ```ts
1008
- .prefetch({
1009
- with: {
1010
- featuredBarber: {
1011
- with: {
1012
- avatar: true,
1013
- services: true,
1014
- },
1015
- },
1016
- },
1017
- })
1018
- ```
1019
-
1020
- ### Functional Prefetch
1021
-
1022
- For complex queries, use a function. The `ctx` parameter provides fully typed `collections` and `globals` via `AppContext` augmentation — no imports needed:
1023
-
1024
- ```ts title="blocks/featured.ts"
1025
- import { block } from "#questpie/factories";
1026
-
1027
- export const featuredBlock = block("featured")
1028
- .fields(({ f }) => ({
1029
- heading: f.text().required(),
1030
- }))
1031
- .prefetch(async ({ values, ctx }) => {
1032
- return {
1033
- posts: (await ctx.collections.posts.find({ limit: 5 })).docs,
1034
- };
1035
- });
1036
- ```
1037
-
1038
- ### Using Prefetched Data in Renderers
1039
-
1040
- ```tsx
1041
- function HeroRenderer({ values, data }: BlockProps<"hero">) {
1042
- // values.backgroundImage = "asset-id-123" (just the ID)
1043
- // data.backgroundImage = { url: "/api/assets/...", filename: "hero.jpg", ... }
1044
-
1045
- return (
1046
- <section>
1047
- {data?.backgroundImage?.url && (
1048
- <img src={data.backgroundImage.url} alt="" />
1049
- )}
1050
- <h1>{values.title}</h1>
1051
- </section>
1052
- );
1053
- }
1054
- ```
1055
-
1056
- ## Block Renderers
1057
-
1058
- React components that receive block data and return JSX.
1059
-
1060
- ### Defining a Renderer
1061
-
1062
- ```tsx title="admin/blocks/hero.tsx"
1063
- import type { BlockProps } from "../.generated/client";
1064
-
1065
- export function HeroRenderer({ values, data }: BlockProps<"hero">) {
1066
- return (
1067
- <section
1068
- className="relative flex items-center justify-center"
1069
- style={{ minHeight: "60vh" }}
1070
- >
1071
- {data?.backgroundImage?.url && (
1072
- <img
1073
- src={data.backgroundImage.url}
1074
- alt=""
1075
- className="absolute inset-0 w-full h-full object-cover"
1076
- />
1077
- )}
1078
- <div className="relative text-center">
1079
- <h1 className="text-5xl font-bold">{values.title}</h1>
1080
- {values.subtitle && <p className="text-xl mt-4">{values.subtitle}</p>}
1081
- {values.ctaText && (
1082
- <a href={values.ctaLink} className="mt-6 inline-block btn">
1083
- {values.ctaText}
1084
- </a>
1085
- )}
1086
- </div>
1087
- </section>
1088
- );
1089
- }
1090
- ```
1091
-
1092
- ### BlockProps
1093
-
1094
- | Property | Type | Description |
1095
- | ---------- | ----------- | -------------------------------------------------- |
1096
- | `values` | `object` | Block field values (title, subtitle, etc.) |
1097
- | `data` | `object` | Prefetched relation data (images, related records) |
1098
- | `children` | `ReactNode` | Nested block content |
1099
-
1100
- ### Registering Renderers
1101
-
1102
- ```tsx title="admin/blocks/index.tsx"
1103
- import { HeroRenderer } from "./hero";
1104
- import { GalleryRenderer } from "./gallery";
1105
- import { CTARenderer } from "./cta";
1106
-
1107
- export const renderers = {
1108
- hero: HeroRenderer,
1109
- gallery: GalleryRenderer,
1110
- cta: CTARenderer,
1111
- };
1112
- ```
1113
-
1114
- ### Frontend Rendering
1115
-
1116
- Use block renderers on the public frontend:
1117
-
1118
- ```tsx title="components/page-renderer.tsx"
1119
- import { renderers } from "@/questpie/admin/blocks";
1120
-
1121
- function PageRenderer({ page }) {
1122
- return (
1123
- <div>
1124
- {page.content?.map((block, i) => {
1125
- const Renderer = renderers[block.type];
1126
- if (!Renderer) return null;
1127
- return <Renderer key={i} values={block.values} data={block.data} />;
1128
- })}
1129
- </div>
1130
- );
1131
- }
1132
- ```
1133
-
1134
- ## Common Mistakes
1135
-
1136
- 1. **HIGH: Not using `ctx.collections.*` in functional prefetch** — use the context-injected collections directly. Do NOT import `app` from `#questpie` inside block files (causes circular dependencies).
1137
-
1138
- ```ts
1139
- // WRONG — importing app creates circular dependency
1140
- import { app } from "#questpie";
1141
- .prefetch(async ({ values, ctx }) => {
1142
- const posts = await app.collections.posts.find({});
1143
- })
1144
-
1145
- // CORRECT — use ctx.collections directly
1146
- .prefetch(async ({ values, ctx }) => {
1147
- const posts = await ctx.collections.posts.find({});
1148
- })
1149
- ```
1150
-
1151
- 2. **HIGH: Importing from `.generated/` inside block files** — block files are imported BY `.generated/index.ts`, so importing from it back creates circular dependencies. Use the `ctx` parameter instead.
1152
-
1153
- 3. **MEDIUM: Block renderer not exported as default or named export** — codegen discovers named exports from block renderer files. Ensure the component is exported.
1154
-
1155
- 4. **MEDIUM: Using `{ with: { field: true } }` prefetch for complex queries** — declarative prefetch only loads related records by ID. For filtered/sorted/limited queries, use functional prefetch instead.
1156
-
1157
- 5. **MEDIUM: Forgetting `.prefetch()` for upload fields** — without prefetch, `values.backgroundImage` is just an ID string. Add `{ with: { backgroundImage: true } }` to get the full asset record with `url`.
1158
-
1159
- 6. **LOW: Missing `category` in `.admin()`** — blocks without a category won't be grouped in the block picker, making it harder to find them.
1160
-
1161
- ## Blocks in Live Preview
1162
-
1163
- When a collection has `.preview()` configured, blocks participate in the existing Live Preview system through `BlockRenderer` and `PreviewField`. The form remains authoritative; block annotations only mirror values, focus fields, select blocks, or request inline scalar edits.
1164
-
1165
- ### Preferred: BlockRenderer
1166
-
1167
- Use `BlockRenderer` for normal frontend page rendering. It preserves `data-block-id`, routes block selection, and scopes nested `PreviewField` paths automatically.
1168
-
1169
- ```tsx
1170
- <BlockRenderer
1171
- content={preview.data.content}
1172
- data={preview.data.content?._data}
1173
- renderers={admin.blocks}
1174
- selectedBlockId={preview.selectedBlockId}
1175
- onBlockClick={preview.isPreviewMode ? preview.handleBlockClick : undefined}
1176
- />
1177
- ```
1178
-
1179
- Inside custom block renderers, annotate scalar values with `PreviewField`:
1180
-
1181
- ```tsx
1182
- <PreviewField field="title" editable="text" as="h2">
1183
- {values.title}
1184
- </PreviewField>
1185
- ```
1186
-
1187
- This resolves to `content._values.{blockId}.title` when rendered inside `BlockRenderer`.
1188
-
1189
- ### Manual BlockScopeProvider Wrapper
1190
-
1191
- Use `BlockScopeProvider` only when you manually render blocks outside `BlockRenderer`:
1192
-
1193
- ```tsx
1194
- import { BlockScopeProvider } from "@questpie/admin/client";
1195
-
1196
- function PageRenderer({ blocks, previewData }) {
1197
- return blocks.map((block) => {
1198
- const Renderer = renderers[block.type];
1199
- return (
1200
- <BlockScopeProvider key={block.id} blockId={block.id}>
1201
- <Renderer values={block.values} data={block.data} />
1202
- </BlockScopeProvider>
1203
- );
1204
- });
1205
- }
1206
- ```
1207
-
1208
- `PreviewField` components inside the provider resolve paths like `content._values.{blockId}.title`.
1209
-
1210
- Inline block edits target `_values`, for example `content._values.<blockId>.title`. Tree edits such as add, remove, reorder, and nesting stay in the existing block editor and should trigger refresh or resync when patching is not safe.
1211
-
1212
- ---
1213
-
1214
- # QUESTPIE Custom UI
1215
-
1216
- This skill builds on questpie-admin.
1217
-
1218
- Extend the QUESTPIE admin with custom field types, custom view types, custom components, and reactive field behaviors.
1219
-
1220
- ## Registries
1221
-
1222
- Registries connect server-side schema to client-side rendering. When the admin encounters a field type, it looks up the renderer in the field registry.
1223
-
1224
- ```text
1225
- Server: f.text().required()
1226
- |
1227
- Generated: { type: "text", options: {...} }
1228
- |
1229
- Admin Client: fieldRegistry.get("text")
1230
- |
1231
- React: <TextFieldRenderer value={...} onChange={...} />
1232
- ```
1233
-
1234
- ### Built-in Field Registry
1235
-
1236
- ```
1237
- text -> TextInput
1238
- textarea -> TextareaInput
1239
- richText -> RichTextEditor (TipTap)
1240
- number -> NumberInput
1241
- boolean -> Checkbox / Switch
1242
- date -> DatePicker
1243
- datetime -> DateTimePicker
1244
- select -> SelectDropdown
1245
- relation -> RelationPicker
1246
- upload -> FileUpload
1247
- object -> NestedForm
1248
- array -> RepeatableItems
1249
- blocks -> BlockEditor
1250
- json -> JSONEditor
1251
- ```
1252
-
1253
- ### Extending Registries
1254
-
1255
- Place files in the admin directory. Codegen discovers them automatically:
1256
-
1257
- ```
1258
- questpie/admin/
1259
- fields/
1260
- color.tsx # Custom color field renderer
1261
- currency.tsx # Custom currency field renderer
1262
- views/
1263
- kanban.tsx # Custom kanban list view
1264
- ```
1265
-
1266
- These are merged with built-in defaults during codegen and exported in `.generated/client.ts`.
1267
-
1268
- ## Custom Fields
1269
-
1270
- ### Server-Side Registration
1271
-
1272
- Register custom fields through modules:
1273
-
1274
- ```ts
1275
- const myModule = module({
1276
- name: "custom-fields",
1277
- fields: {
1278
- color: colorField,
1279
- currency: currencyField,
1280
- phone: phoneField,
1281
- },
1282
- });
1283
- ```
1284
-
1285
- Once registered and codegen runs, the field becomes available on the `f` builder:
1286
-
1287
- ```ts
1288
- .fields(({ f }) => ({
1289
- brandColor: f.color().default("#000000"),
1290
- price: f.currency({ currency: "USD" }),
1291
- }))
1292
- ```
1293
-
1294
- ### Admin Field Renderer
1295
-
1296
- Create a React component for the field's edit form:
1297
-
1298
- ```tsx title="admin/fields/color.tsx"
1299
- import { Icon } from "@iconify/react";
1300
-
1301
- function ColorFieldRenderer({ value, onChange }) {
1302
- return (
1303
- <div className="flex items-center gap-2">
1304
- <input
1305
- type="color"
1306
- value={value || "#000000"}
1307
- onChange={(e) => onChange(e.target.value)}
1308
- className="w-10 h-10 border border-border cursor-pointer"
1309
- />
1310
- <span className="font-mono text-sm text-muted-foreground">
1311
- {value || "#000000"}
1312
- </span>
1313
- </div>
1314
- );
1315
- }
1316
- ```
1317
-
1318
- ### Cell Renderer
1319
-
1320
- For custom table column rendering, provide a `cell` component alongside the field renderer:
1321
-
1322
- ```tsx title="admin/fields/color.tsx"
1323
- // Cell component for list view table
1324
- export function ColorCell({ value }) {
1325
- return (
1326
- <div className="flex items-center gap-2">
1327
- <div
1328
- className="w-4 h-4 border border-border"
1329
- style={{ backgroundColor: value || "transparent" }}
1330
- />
1331
- <span className="text-xs font-mono">{value}</span>
1332
- </div>
1333
- );
1334
- }
1335
- ```
1336
-
1337
- ## Custom Views
1338
-
1339
- Create view types beyond built-in table and form — kanban boards, calendars, galleries.
1340
-
1341
- ### Server-Side Declaration
1342
-
1343
- ```ts
1344
- const myModule = module({
1345
- name: "custom-views",
1346
- views: {
1347
- kanban: kanbanViewDefinition,
1348
- calendar: calendarViewDefinition,
1349
- },
1350
- });
1351
- ```
1352
-
1353
- ### Usage in Collections
1354
-
1355
- ```ts
1356
- .list(({ v }) => v.kanban({
1357
- columns: "status",
1358
- cardTitle: "title",
1359
- }))
1360
- ```
1361
-
1362
- ### Client Rendering
1363
-
1364
- ```tsx title="admin/views/kanban.tsx"
1365
- function KanbanView({ data, columns, onDrop }) {
1366
- return (
1367
- <div className="flex gap-4">
1368
- {columns.map((col) => (
1369
- <div key={col.id} className="flex-1">
1370
- <h3 className="font-mono text-sm font-semibold mb-2">{col.label}</h3>
1371
- {data
1372
- .filter((item) => item.status === col.id)
1373
- .map((item) => (
1374
- <div
1375
- key={item.id}
1376
- className="border border-border bg-card p-3 mb-2"
1377
- >
1378
- {item.title}
1379
- </div>
1380
- ))}
1381
- </div>
1382
- ))}
1383
- </div>
1384
- );
1385
- }
1386
- ```
1387
-
1388
- ## Reactive Field System
1389
-
1390
- Fields support reactive behaviors configured in the collection's `.form()` view or on the field definition itself.
1391
-
1392
- ### Conditional Visibility
1393
-
1394
- ```ts
1395
- {
1396
- field: f.cancellationReason,
1397
- hidden: ({ data }) => data.status !== "cancelled",
1398
- }
1399
- ```
1400
-
1401
- ### Read-Only
1402
-
1403
- ```ts
1404
- {
1405
- field: f.customerName,
1406
- readOnly: ({ data }) => !!data.customer,
1407
- }
1408
- ```
1409
-
1410
- ### Computed Values
1411
-
1412
- ```ts
1413
- {
1414
- field: f.slug,
1415
- compute: {
1416
- handler: ({ data }) => {
1417
- if (data.name && !data.slug?.trim()) {
1418
- return slugify(data.name);
1419
- }
1420
- return undefined;
1421
- },
1422
- deps: ({ data }) => [data.name, data.slug],
1423
- debounce: 300,
1424
- },
1425
- }
1426
- ```
1427
-
1428
- ### Dynamic Options (Server-Side)
1429
-
1430
- For select/relation fields with options that depend on other field values:
1431
-
1432
- ```ts
1433
- city: f.relation("cities").admin({
1434
- options: {
1435
- handler: async ({ data, search, ctx }) => {
1436
- const cities = await ctx.db.query.cities.findMany({
1437
- where: { countryId: data.country },
1438
- });
1439
- return {
1440
- options: cities.map((c) => ({ value: c.id, label: c.name })),
1441
- };
1442
- },
1443
- deps: ({ data }) => [data.country],
1444
- },
1445
- }),
1446
- ```
1447
-
1448
- The `handler` runs **server-side** with full access to `ctx.db`, `ctx.user`, `ctx.req`. It re-executes when any value in `deps` changes.
1449
-
1450
- ## UI Component Reference
1451
-
1452
- When building custom admin UI, use these patterns:
1453
-
1454
- ### Icons
1455
-
1456
- ```tsx
1457
- import { Icon } from "@iconify/react";
1458
-
1459
- // Phosphor icon set with ph: prefix
1460
- <Icon icon="ph:house" width={20} height={20} />
1461
- <Icon icon="ph:caret-down-bold" width={16} height={16} /> // bold weight
1462
- <Icon icon="ph:heart-fill" width={16} height={16} /> // fill weight
1463
- ```
1464
-
1465
- ### Toasts
1466
-
1467
- ```tsx
1468
- import { toast } from "sonner";
1469
-
1470
- toast.success("Record saved");
1471
- toast.error("Failed to save");
1472
- ```
1473
-
1474
- ### Primitives (base-ui)
1475
-
1476
- ```tsx
1477
- // CORRECT — render prop
1478
- <DialogTrigger render={<Button>Open</Button>} />
1479
-
1480
- // WRONG — asChild is Radix, not base-ui
1481
- <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
1482
- ```
1483
-
1484
- ### Responsive Components
1485
-
1486
- - `ResponsivePopover` — Popover on desktop, Drawer on mobile
1487
- - `ResponsiveDialog` — Dialog on desktop, fullscreen Drawer on mobile
1488
- - Hooks: `useIsMobile()`, `useIsDesktop()`, `useMediaQuery()`
1489
-
1490
- ## Common Mistakes
1491
-
1492
- 1. **HIGH: Not registering custom field in the field registry** — if codegen doesn't discover the field renderer file, the admin will render nothing for that field type. Place it in `questpie/admin/fields/<name>.tsx`.
1493
-
1494
- 2. **HIGH: Missing `cell` component for custom fields** — without a cell component, the list view table shows raw values for your custom field instead of a formatted display.
1495
-
1496
- 3. **MEDIUM: Reactive field handlers running client-side** — `options.handler`, `compute.handler`, and other reactive handlers run **SERVER-SIDE** with access to `ctx.db`, `ctx.user`. Do not import client-side modules or use browser APIs in them.
1497
-
1498
- 4. **MEDIUM: Using `onChange` wrong in field components** — the field renderer receives `onChange` that expects the **value directly**, not a DOM event.
1499
-
1500
- ```tsx
1501
- // WRONG
1502
- onChange={(e) => onChange(e)}
1503
- // CORRECT
1504
- onChange={(e) => onChange(e.target.value)}
1505
- // Or for non-DOM values:
1506
- onChange={newValue}
1507
- ```
1508
-
1509
- 5. **MEDIUM: Importing from `@radix-ui/*`** — QUESTPIE admin uses `@base-ui/react`. Never import Radix primitives.
1510
-
1511
- 6. **MEDIUM: Using `@phosphor-icons/react` or `lucide-react`** — use `@iconify/react` with `ph:` prefix for all icons.
1512
-
1513
- 7. **LOW: Not using shadcn components** — prefer `<Button>`, `<Card>`, `<Input>` from the shadcn component library instead of raw HTML elements. The admin follows QUESTPIE Neutral Design.
1514
-
1515
- ---