create-questpie 2.0.0 → 2.0.2

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 (45) hide show
  1. package/README.md +10 -6
  2. package/dist/index.mjs +140 -25
  3. package/package.json +5 -3
  4. package/skills/questpie/AGENTS.md +2664 -0
  5. package/skills/questpie/SKILL.md +181 -0
  6. package/skills/questpie/references/auth.md +121 -0
  7. package/skills/questpie/references/business-logic.md +550 -0
  8. package/skills/questpie/references/codegen-plugin-api.md +382 -0
  9. package/skills/questpie/references/crud-api.md +378 -0
  10. package/skills/questpie/references/data-modeling.md +489 -0
  11. package/skills/questpie/references/extend.md +493 -0
  12. package/skills/questpie/references/field-types.md +386 -0
  13. package/skills/questpie/references/infrastructure-adapters.md +545 -0
  14. package/skills/questpie/references/multi-tenancy.md +364 -0
  15. package/skills/questpie/references/production.md +475 -0
  16. package/skills/questpie/references/query-operators.md +125 -0
  17. package/skills/questpie/references/quickstart.md +549 -0
  18. package/skills/questpie/references/rules.md +327 -0
  19. package/skills/questpie/references/tanstack-query.md +520 -0
  20. package/skills/questpie-admin/AGENTS.md +1442 -0
  21. package/skills/questpie-admin/SKILL.md +410 -0
  22. package/skills/questpie-admin/references/blocks.md +307 -0
  23. package/skills/questpie-admin/references/custom-ui.md +305 -0
  24. package/skills/questpie-admin/references/views.md +433 -0
  25. package/templates/tanstack-start/AGENTS.md +71 -62
  26. package/templates/tanstack-start/CLAUDE.md +26 -23
  27. package/templates/tanstack-start/README.md +32 -20
  28. package/templates/tanstack-start/env.example +1 -1
  29. package/templates/tanstack-start/package.json +20 -6
  30. package/templates/tanstack-start/src/lib/client.ts +2 -2
  31. package/templates/tanstack-start/src/lib/env.ts +1 -1
  32. package/templates/tanstack-start/src/questpie/admin/.generated/client.ts +13 -0
  33. package/templates/tanstack-start/src/questpie/admin/modules.ts +1 -0
  34. package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +117 -241
  35. package/templates/tanstack-start/src/questpie/server/.generated/index.ts +129 -81
  36. package/templates/tanstack-start/src/questpie/server/app.ts +1 -1
  37. package/templates/tanstack-start/src/questpie/server/config/admin.ts +27 -30
  38. package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +1 -1
  39. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  40. package/templates/tanstack-start/src/routeTree.gen.ts +138 -0
  41. package/templates/tanstack-start/src/routes/__root.tsx +0 -2
  42. package/templates/tanstack-start/src/routes/admin.tsx +8 -1
  43. package/templates/tanstack-start/src/tanstack-start.d.ts +1 -0
  44. package/templates/tanstack-start/src/vite-env.d.ts +1 -0
  45. package/templates/tanstack-start/vite.config.ts +1 -3
@@ -0,0 +1,410 @@
1
+ ---
2
+ name: questpie-admin
3
+ description: QUESTPIE admin panel — setup, branding, theming, sidebar, dashboard, views, blocks, custom fields, media, localization, live preview, auth, dark mode, CSS variables. Use when building or customizing the QUESTPIE admin UI.
4
+ license: MIT
5
+ metadata:
6
+ author: questpie
7
+ version: "3.0.0"
8
+ ---
9
+
10
+ # QUESTPIE Admin Panel
11
+
12
+ 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.
13
+
14
+ ## Reference Topics
15
+
16
+ | Topic | File | Covers |
17
+ |---|---|---|
18
+ | Views | `references/views.md` | List views, form views, dashboard, sidebar, filters, bulk actions, visibility, history |
19
+ | Blocks | `references/blocks.md` | Block definitions, fields, prefetch, renderers, block picker |
20
+ | Custom UI | `references/custom-ui.md` | Custom fields, custom views, registries, reactive fields, widgets |
21
+
22
+ ## Full Compiled Document
23
+
24
+ For the complete admin reference with all topics expanded: `AGENTS.md`
25
+
26
+ ## Tech Stack
27
+
28
+ - **React** + **Tailwind CSS v4** + **shadcn** components
29
+ - **@base-ui/react** primitives (NOT @radix-ui)
30
+ - **@iconify/react** with Phosphor icon set (`ph:icon-name`)
31
+ - **sonner** for toasts — `toast.error()`, `toast.success()`
32
+ - Brutalist flat design: `--radius: 0px`, no shadows
33
+
34
+ ## Setup
35
+
36
+ ### 1. Install
37
+
38
+ ```bash
39
+ bun add @questpie/admin
40
+ ```
41
+
42
+ ### 2. Runtime Config
43
+
44
+ ```ts title="questpie.config.ts"
45
+ import { runtimeConfig } from "questpie";
46
+
47
+ export default runtimeConfig({
48
+ app: { url: process.env.APP_URL || "http://localhost:3000" },
49
+ db: { url: process.env.DATABASE_URL },
50
+ secret: process.env.APP_SECRET,
51
+ });
52
+ ```
53
+
54
+ The admin module contributes the codegen plugin automatically. It discovers `config/admin.ts`, `blocks/`, views, components, and admin client modules.
55
+
56
+ ### 3. Modules
57
+
58
+ ```ts title="modules.ts"
59
+ import { adminModule, auditModule } from "@questpie/admin/server";
60
+
61
+ export default [adminModule, auditModule] as const;
62
+ ```
63
+
64
+ | Module | Provides |
65
+ | ------------- | ------------------------------------- |
66
+ | `adminModule` | User collection, auth pages, admin UI |
67
+ | `auditModule` | Audit log collection, timeline widget |
68
+
69
+ ### 4. Admin Config
70
+
71
+ ```ts title="config/admin.ts"
72
+ import { adminConfig } from "#questpie/factories";
73
+
74
+ export default adminConfig({
75
+ branding: {
76
+ name: { en: "My App Admin" },
77
+ },
78
+ sidebar: {
79
+ sections: [
80
+ { id: "overview", title: { en: "Overview" } },
81
+ { id: "content", title: { en: "Content" } },
82
+ ],
83
+ items: [
84
+ {
85
+ sectionId: "overview",
86
+ type: "link",
87
+ label: { en: "Dashboard" },
88
+ href: "/admin",
89
+ icon: { type: "icon", props: { name: "ph:house" } },
90
+ },
91
+ {
92
+ sectionId: "content",
93
+ type: "collection",
94
+ collection: "posts",
95
+ },
96
+ ],
97
+ },
98
+ });
99
+ ```
100
+
101
+ ### 5. Codegen
102
+
103
+ ```bash
104
+ bunx questpie generate
105
+ ```
106
+
107
+ ### 6. Mount the Admin
108
+
109
+ ```ts title="routes/admin/$.tsx"
110
+ import { AdminRouter } from "@questpie/admin/client";
111
+ import { admin } from "@/questpie/admin/admin";
112
+
113
+ export default function AdminPage() {
114
+ return <AdminRouter admin={admin} />;
115
+ }
116
+ ```
117
+
118
+ The admin client config is auto-generated by codegen at `admin/.generated/client.ts`. No manual builder setup needed.
119
+
120
+ ## Branding
121
+
122
+ ```ts title="config/admin.ts"
123
+ import { adminConfig } from "#questpie/factories";
124
+
125
+ export default adminConfig({
126
+ branding: {
127
+ name: { en: "Barbershop Control", sk: "Riadenie barbershopu" },
128
+ },
129
+ });
130
+ ```
131
+
132
+ | Option | Type | Description |
133
+ | ------ | ---------------- | -------------------------------- |
134
+ | `name` | `string \| i18n` | App name shown in sidebar header |
135
+ | `logo` | `string` | Logo URL or path |
136
+
137
+ ## Theming (CSS Variables)
138
+
139
+ The admin uses CSS variables for all theming. Override them in your own CSS file.
140
+
141
+ ### Light Theme (`:root`)
142
+
143
+ | Variable | Default | Purpose |
144
+ | ---------------------- | --------- | -------------------------------- |
145
+ | `--background` | `#FFFFFF` | Page background |
146
+ | `--foreground` | `#0A0A0A` | Primary text |
147
+ | `--card` | `#F8F8F8` | Cards, panels, sidebar |
148
+ | `--popover` | `#FFFFFF` | Dropdowns, tooltips, dialogs |
149
+ | `--muted` | `#F0F0F0` | Hover states, table headers |
150
+ | `--muted-foreground` | `#666666` | Secondary text, placeholders |
151
+ | `--primary` | `#B700FF` | Brand accent (CTAs, focus rings) |
152
+ | `--primary-foreground` | `#FFFFFF` | Text on primary backgrounds |
153
+ | `--destructive` | `#FF3D57` | Errors, delete actions |
154
+ | `--success` | `#00E676` | Positive states |
155
+ | `--warning` | `#FFB300` | Caution states |
156
+ | `--info` | `#40C4FF` | Informational emphasis |
157
+ | `--border` | `#E0E0E0` | All structural borders |
158
+ | `--ring` | `#B700FF` | Focus ring color |
159
+ | `--radius` | `0px` | Border radius (0 = brutalist) |
160
+
161
+ ### Dark Theme (`.dark` class)
162
+
163
+ Dark mode uses the `.dark` class on the root element. Key overrides:
164
+
165
+ | Variable | Dark Value |
166
+ | -------------- | ---------- |
167
+ | `--background` | `#0A0A0A` |
168
+ | `--foreground` | `#FFFFFF` |
169
+ | `--card` | `#111111` |
170
+ | `--border` | `#333333` |
171
+ | `--muted` | `#1A1A1A` |
172
+
173
+ ### Typography
174
+
175
+ | Variable | Value |
176
+ | ------------- | ------------------------------------------------------------------- |
177
+ | `--font-sans` | `"Geist Variable"` — body text, descriptions |
178
+ | `--font-mono` | `"JetBrains Mono Variable"` — UI chrome: nav, buttons, tabs, badges |
179
+
180
+ ### Sidebar Variables
181
+
182
+ Separate tokens for independent sidebar theming: `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, `--sidebar-accent`, `--sidebar-border`, `--sidebar-ring`.
183
+
184
+ ### Custom Theme
185
+
186
+ 1. Copy the admin CSS file
187
+ 2. Change variable values
188
+ 3. Import your copy instead
189
+ 4. Zero component changes needed
190
+
191
+ ## Content Localization
192
+
193
+ When collections have `.localized()` fields, the admin shows a locale switcher:
194
+
195
+ ```ts title="config/app.ts"
196
+ import { appConfig } from "questpie";
197
+
198
+ export default appConfig({
199
+ locale: {
200
+ locales: [
201
+ { code: "en", label: { en: "English" }, flagCountryCode: "gb" },
202
+ { code: "sk", label: { en: "Slovak" } },
203
+ ],
204
+ defaultLocale: "en",
205
+ },
206
+ });
207
+ ```
208
+
209
+ The admin tracks content locale separately from UI locale. Only localized field values change when switching.
210
+
211
+ ## Media & Uploads
212
+
213
+ ```ts
214
+ avatar: f.upload({
215
+ to: "assets",
216
+ mimeTypes: ["image/*"],
217
+ maxSize: 5_000_000,
218
+ }),
219
+ ```
220
+
221
+ The admin renders drag-and-drop upload, image preview, file info, and remove button.
222
+
223
+ ## Live Preview
224
+
225
+ Live Preview uses a split-screen iframe. The current implementation refreshes the iframe after save/autosave and uses `postMessage` for field/block focus sync.
226
+
227
+ Preview V2 patch-based docs are design notes until `useQuestpiePreview`, `PreviewRoot`, and `PreviewBlock` are exported.
228
+
229
+ ### Server Config
230
+
231
+ Add `.preview()` to a collection to enable split-screen editing:
232
+
233
+ ```ts title="collections/pages.ts"
234
+ export const pages = collection("pages")
235
+ .fields(({ f }) => ({
236
+ title: f.text().required().localized(),
237
+ slug: f.text().required(),
238
+ content: f.blocks().localized(),
239
+ }))
240
+ .preview({
241
+ enabled: true,
242
+ position: "right",
243
+ defaultWidth: 50,
244
+ url: ({ record }) => {
245
+ const slug = record.slug as string;
246
+ return slug === "home" ? "/?preview=true" : `/${slug}?preview=true`;
247
+ },
248
+ });
249
+ ```
250
+
251
+ Current preview refreshes the iframe after save/autosave and supports field focus through `postMessage`.
252
+
253
+ ### Frontend Integration
254
+
255
+ Use `useCollectionPreview` with `PreviewProvider` and `PreviewField`:
256
+
257
+ ```tsx
258
+ import {
259
+ PreviewField,
260
+ PreviewProvider,
261
+ useCollectionPreview,
262
+ } from "@questpie/admin/client";
263
+
264
+ function PagePreview({ initialData }) {
265
+ const router = useRouter();
266
+ const preview = useCollectionPreview({
267
+ initialData,
268
+ onRefresh: () => router.invalidate(),
269
+ });
270
+
271
+ return (
272
+ <PreviewProvider
273
+ isPreviewMode={preview.isPreviewMode}
274
+ focusedField={preview.focusedField}
275
+ onFieldClick={preview.handleFieldClick}
276
+ >
277
+ <PreviewField field="title" as="h1">
278
+ {preview.data.title}
279
+ </PreviewField>
280
+ </PreviewProvider>
281
+ );
282
+ }
283
+ ```
284
+
285
+ ### Key Principles
286
+
287
+ - Current preview = save/autosave refresh plus field/block focus sync
288
+ - `useCollectionPreview` sends `PREVIEW_READY`, `FIELD_CLICKED`, and `BLOCK_CLICKED`
289
+ - `PreviewProvider` supplies preview context to `PreviewField`
290
+ - Each message carries `sessionId`, `seq`, `timestamp`, `protocolVersion`
291
+ - Preview wrappers must prevent accidental navigation in the iframe
292
+
293
+ ## History & Versions
294
+
295
+ Enable `auditModule` for activity timeline. Enable versioning on collections for snapshot restore:
296
+
297
+ ```ts
298
+ export const pages = collection("pages")
299
+ .fields(({ f }) => ({ ... }))
300
+ .options({ versioning: true });
301
+ ```
302
+
303
+ ## Scope (Multi-Tenancy)
304
+
305
+ The admin provides scope primitives for multi-tenant applications. Import from `@questpie/admin/client`.
306
+
307
+ ### ScopeProvider
308
+
309
+ Wraps the admin to enable scope selection. Manages scope ID in React state and persists to localStorage.
310
+
311
+ ```tsx
312
+ import { ScopeProvider } from "@questpie/admin/client";
313
+
314
+ <ScopeProvider
315
+ headerName="x-selected-workspace" // HTTP header for scope ID
316
+ storageKey="admin-workspace" // localStorage key
317
+ defaultScope={null} // default value
318
+ >
319
+ <AdminLayout>...</AdminLayout>
320
+ </ScopeProvider>;
321
+ ```
322
+
323
+ ### ScopePicker
324
+
325
+ Dropdown for selecting the current scope. Place in sidebar via `slots.afterBrand`:
326
+
327
+ ```tsx
328
+ import { ScopePicker } from "@questpie/admin/client";
329
+
330
+ <AdminLayout
331
+ admin={admin}
332
+ basePath="/admin"
333
+ slots={{
334
+ afterBrand: (
335
+ <div className="px-3 py-2 border-b">
336
+ <ScopePicker
337
+ collection="workspaces"
338
+ labelField="name"
339
+ allowClear
340
+ compact
341
+ />
342
+ </div>
343
+ ),
344
+ }}
345
+ />;
346
+ ```
347
+
348
+ Three data sources: `collection` (queries a collection), `options` (static array), `loadOptions` (async function).
349
+
350
+ ### useScopedFetch / createScopedFetch
351
+
352
+ Inject scope header into all API calls:
353
+
354
+ ```tsx
355
+ import { useScopedFetch, createScopedFetch } from "@questpie/admin/client";
356
+
357
+ // React hook
358
+ const scopedFetch = useScopedFetch();
359
+ const client = useMemo(
360
+ () => createClient({ baseURL: "/api", fetch: scopedFetch }),
361
+ [scopedFetch],
362
+ );
363
+
364
+ // Non-React
365
+ const scopedFetch = createScopedFetch("x-selected-workspace", () =>
366
+ getScopeId(),
367
+ );
368
+ ```
369
+
370
+ ### useScope / useScopeSafe
371
+
372
+ Access current scope state in any component:
373
+
374
+ ```tsx
375
+ import { useScope, useScopeSafe } from "@questpie/admin/client";
376
+
377
+ const { scopeId, setScope, clearScope, headerName } = useScope(); // throws outside ScopeProvider
378
+ const scope = useScopeSafe(); // returns null outside ScopeProvider
379
+ ```
380
+
381
+ For the full server-side setup (context resolver, type augmentation, access rules), see the `questpie` skill's `references/multi-tenancy.md`.
382
+
383
+ ## Common Mistakes
384
+
385
+ 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.
386
+
387
+ ```tsx
388
+ // WRONG
389
+ <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
390
+ // CORRECT
391
+ <DialogTrigger render={<Button>Open</Button>} />
392
+ ```
393
+
394
+ 2. **CRITICAL: Importing from `@radix-ui/*`** — use `@base-ui/react` instead.
395
+
396
+ 3. **HIGH: Using `@phosphor-icons/react`** — use `@iconify/react` with `ph:` prefix.
397
+
398
+ ```tsx
399
+ // WRONG
400
+ import { CaretDown } from "@phosphor-icons/react";
401
+ // CORRECT
402
+ import { Icon } from "@iconify/react";
403
+ <Icon icon="ph:caret-down" width={16} height={16} />;
404
+ ```
405
+
406
+ 4. **HIGH: Using lucide-react icons** — use `@iconify/react` with Phosphor icon set.
407
+
408
+ 5. **MEDIUM: Custom `<button>` or `<div>` instead of shadcn components** — use `<Button>`, `<Card>`, etc.
409
+
410
+ 6. **MEDIUM: `console.error` for user errors** — use `toast.error()` from `sonner`.
@@ -0,0 +1,307 @@
1
+ ---
2
+ name: questpie-admin/blocks
3
+ description: QUESTPIE blocks content-blocks page-builder block-definition fields prefetch renderers block-picker categories nested-blocks upload relation
4
+ ---
5
+
6
+ # QUESTPIE Blocks
7
+
8
+ This skill builds on questpie-admin.
9
+
10
+ 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.
11
+
12
+ ```text
13
+ Server: block("hero") Client: HeroRenderer
14
+ .fields({ title, image }) -> Receives { values, data }
15
+ .admin({ label, icon }) Returns JSX
16
+ .prefetch({ with: {...} })
17
+ ```
18
+
19
+ ## Defining Blocks
20
+
21
+ Blocks are defined in `blocks/` using the `block()` factory:
22
+
23
+ ```ts title="blocks/hero.ts"
24
+ import { block } from "#questpie/factories";
25
+
26
+ export const heroBlock = block("hero")
27
+ .admin(({ c }) => ({
28
+ label: { en: "Hero Section", sk: "Hero sekcia" },
29
+ icon: c.icon("ph:image"),
30
+ category: "sections",
31
+ }))
32
+ .fields(({ f }) => ({
33
+ title: f.text().localized().required(),
34
+ subtitle: f.textarea().localized(),
35
+ backgroundImage: f.upload({ to: "assets" }),
36
+ overlayOpacity: f.number().default(60),
37
+ alignment: f
38
+ .select([
39
+ { value: "left", label: "Left" },
40
+ { value: "center", label: "Center" },
41
+ { value: "right", label: "Right" },
42
+ ])
43
+ .default("center"),
44
+ ctaText: f.text().localized(),
45
+ ctaLink: f.text(),
46
+ }))
47
+ .prefetch({ with: { backgroundImage: true } });
48
+ ```
49
+
50
+ ### Admin Metadata
51
+
52
+ ```ts
53
+ .admin(({ c }) => ({
54
+ label: { en: "Hero Section" }, // Display name in block picker
55
+ icon: c.icon("ph:image"), // Icon in block picker (Phosphor set)
56
+ category: "sections", // Group in block picker
57
+ }))
58
+ ```
59
+
60
+ ### Multiple Blocks Per File
61
+
62
+ Export multiple named blocks from one file:
63
+
64
+ ```ts title="blocks/layout.ts"
65
+ import { block } from "#questpie/factories";
66
+
67
+ export const twoColumnBlock = block("twoColumn")
68
+ .admin(({ c }) => ({
69
+ label: { en: "Two Columns" },
70
+ icon: c.icon("ph:columns"),
71
+ category: "layout",
72
+ }))
73
+ .fields(({ f }) => ({
74
+ left: f.blocks(),
75
+ right: f.blocks(),
76
+ }));
77
+
78
+ export const spacerBlock = block("spacer")
79
+ .admin(({ c }) => ({
80
+ label: { en: "Spacer" },
81
+ icon: c.icon("ph:arrows-out-line-vertical"),
82
+ category: "layout",
83
+ }))
84
+ .fields(({ f }) => ({
85
+ height: f
86
+ .select([
87
+ { value: "sm", label: "Small" },
88
+ { value: "md", label: "Medium" },
89
+ { value: "lg", label: "Large" },
90
+ { value: "xl", label: "Extra Large" },
91
+ ])
92
+ .default("md"),
93
+ }));
94
+ ```
95
+
96
+ ## Using Blocks in Collections
97
+
98
+ Add a `blocks` field to any collection:
99
+
100
+ ```ts title="collections/pages.ts"
101
+ import { collection } from "#questpie/factories";
102
+
103
+ export const pages = collection("pages").fields(({ f }) => ({
104
+ title: f.text().required().localized(),
105
+ slug: f.text().required(),
106
+ content: f.blocks().localized(),
107
+ }));
108
+ ```
109
+
110
+ The admin renders a visual block editor for this field.
111
+
112
+ ## Prefetch
113
+
114
+ Blocks often reference related data (images, linked records). Use `.prefetch()` to load them alongside block values.
115
+
116
+ ### Declarative Prefetch
117
+
118
+ ```ts
119
+ .prefetch({
120
+ with: {
121
+ backgroundImage: true, // Load the full image record
122
+ },
123
+ })
124
+ ```
125
+
126
+ ### Nested Prefetch
127
+
128
+ ```ts
129
+ .prefetch({
130
+ with: {
131
+ featuredBarber: {
132
+ with: {
133
+ avatar: true,
134
+ services: true,
135
+ },
136
+ },
137
+ },
138
+ })
139
+ ```
140
+
141
+ ### Functional Prefetch
142
+
143
+ For complex queries, use a function. The `ctx` parameter provides fully typed `collections` and `globals` via `AppContext` augmentation — no imports needed:
144
+
145
+ ```ts title="blocks/featured.ts"
146
+ import { block } from "#questpie/factories";
147
+
148
+ export const featuredBlock = block("featured")
149
+ .fields(({ f }) => ({
150
+ heading: f.text().required(),
151
+ }))
152
+ .prefetch(async ({ values, ctx }) => {
153
+ return {
154
+ posts: (await ctx.collections.posts.find({ limit: 5 })).docs,
155
+ };
156
+ });
157
+ ```
158
+
159
+ ### Using Prefetched Data in Renderers
160
+
161
+ ```tsx
162
+ function HeroRenderer({ values, data }: BlockProps<"hero">) {
163
+ // values.backgroundImage = "asset-id-123" (just the ID)
164
+ // data.backgroundImage = { url: "/api/assets/...", filename: "hero.jpg", ... }
165
+
166
+ return (
167
+ <section>
168
+ {data?.backgroundImage?.url && (
169
+ <img src={data.backgroundImage.url} alt="" />
170
+ )}
171
+ <h1>{values.title}</h1>
172
+ </section>
173
+ );
174
+ }
175
+ ```
176
+
177
+ ## Block Renderers
178
+
179
+ React components that receive block data and return JSX.
180
+
181
+ ### Defining a Renderer
182
+
183
+ ```tsx title="admin/blocks/hero.tsx"
184
+ import type { BlockProps } from "../.generated/client";
185
+
186
+ export function HeroRenderer({ values, data }: BlockProps<"hero">) {
187
+ return (
188
+ <section
189
+ className="relative flex items-center justify-center"
190
+ style={{ minHeight: "60vh" }}
191
+ >
192
+ {data?.backgroundImage?.url && (
193
+ <img
194
+ src={data.backgroundImage.url}
195
+ alt=""
196
+ className="absolute inset-0 w-full h-full object-cover"
197
+ />
198
+ )}
199
+ <div className="relative text-center">
200
+ <h1 className="text-5xl font-bold">{values.title}</h1>
201
+ {values.subtitle && <p className="text-xl mt-4">{values.subtitle}</p>}
202
+ {values.ctaText && (
203
+ <a href={values.ctaLink} className="mt-6 inline-block btn">
204
+ {values.ctaText}
205
+ </a>
206
+ )}
207
+ </div>
208
+ </section>
209
+ );
210
+ }
211
+ ```
212
+
213
+ ### BlockProps
214
+
215
+ | Property | Type | Description |
216
+ | ---------- | ----------- | -------------------------------------------------- |
217
+ | `values` | `object` | Block field values (title, subtitle, etc.) |
218
+ | `data` | `object` | Prefetched relation data (images, related records) |
219
+ | `children` | `ReactNode` | Nested block content |
220
+
221
+ ### Registering Renderers
222
+
223
+ ```tsx title="admin/blocks/index.tsx"
224
+ import { HeroRenderer } from "./hero";
225
+ import { GalleryRenderer } from "./gallery";
226
+ import { CTARenderer } from "./cta";
227
+
228
+ export const renderers = {
229
+ hero: HeroRenderer,
230
+ gallery: GalleryRenderer,
231
+ cta: CTARenderer,
232
+ };
233
+ ```
234
+
235
+ ### Frontend Rendering
236
+
237
+ Use block renderers on the public frontend:
238
+
239
+ ```tsx title="components/page-renderer.tsx"
240
+ import { renderers } from "@/questpie/admin/blocks";
241
+
242
+ function PageRenderer({ page }) {
243
+ return (
244
+ <div>
245
+ {page.content?.map((block, i) => {
246
+ const Renderer = renderers[block.type];
247
+ if (!Renderer) return null;
248
+ return <Renderer key={i} values={block.values} data={block.data} />;
249
+ })}
250
+ </div>
251
+ );
252
+ }
253
+ ```
254
+
255
+ ## Common Mistakes
256
+
257
+ 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).
258
+
259
+ ```ts
260
+ // WRONG — importing app creates circular dependency
261
+ import { app } from "#questpie";
262
+ .prefetch(async ({ values, ctx }) => {
263
+ const posts = await app.collections.posts.find({});
264
+ })
265
+
266
+ // CORRECT — use ctx.collections directly
267
+ .prefetch(async ({ values, ctx }) => {
268
+ const posts = await ctx.collections.posts.find({});
269
+ })
270
+ ```
271
+
272
+ 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.
273
+
274
+ 3. **MEDIUM: Block renderer not exported as default or named export** — codegen discovers named exports from block renderer files. Ensure the component is exported.
275
+
276
+ 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.
277
+
278
+ 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`.
279
+
280
+ 6. **LOW: Missing `category` in `.admin()`** — blocks without a category won't be grouped in the block picker, making it harder to find them.
281
+
282
+ ## Blocks in Live Preview
283
+
284
+ When a collection has `.preview()` configured, blocks can participate in preview focus by combining `BlockScopeProvider` with `PreviewField`.
285
+
286
+ ### BlockScopeProvider Wrapper
287
+
288
+ Use `BlockScopeProvider` in your frontend to scope field paths inside a block:
289
+
290
+ ```tsx
291
+ import { BlockScopeProvider } from "@questpie/admin/client";
292
+
293
+ function PageRenderer({ blocks, previewData }) {
294
+ return blocks.map((block) => {
295
+ const Renderer = renderers[block.type];
296
+ return (
297
+ <BlockScopeProvider key={block.id} blockId={block.id}>
298
+ <Renderer values={block.values} data={block.data} />
299
+ </BlockScopeProvider>
300
+ );
301
+ });
302
+ }
303
+ ```
304
+
305
+ `PreviewField` components inside the provider resolve paths like `content._values.{blockId}.title`.
306
+
307
+ Blocks with declarative prefetch (`{ with: { image: true } }`) resolve relations during reconcile — the preview shows the image URL immediately after the server round-trip completes, not just the asset ID.