create-questpie 2.0.2 → 2.0.4

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 (40) hide show
  1. package/dist/index.mjs +244 -30
  2. package/package.json +1 -1
  3. package/skills/questpie/AGENTS.md +310 -103
  4. package/skills/questpie/SKILL.md +196 -84
  5. package/skills/questpie/coverage.json +213 -0
  6. package/skills/questpie/references/auth.md +119 -4
  7. package/skills/questpie/references/business-logic.md +126 -56
  8. package/skills/questpie/references/crud-api.md +231 -29
  9. package/skills/questpie/references/data-modeling.md +26 -6
  10. package/skills/questpie/references/extend.md +98 -7
  11. package/skills/questpie/references/field-types.md +14 -2
  12. package/skills/questpie/references/infrastructure-adapters.md +207 -32
  13. package/skills/questpie/references/mcp.md +147 -0
  14. package/skills/questpie/references/multi-tenancy.md +1 -2
  15. package/skills/questpie/references/production.md +218 -53
  16. package/skills/questpie/references/quickstart.md +31 -18
  17. package/skills/questpie/references/rules.md +140 -13
  18. package/skills/questpie/references/sandbox.md +110 -0
  19. package/skills/questpie/references/tanstack-query.md +34 -11
  20. package/skills/questpie/references/type-inference.md +167 -0
  21. package/skills/questpie/references/workflows.md +155 -0
  22. package/skills/questpie-admin/AGENTS.md +141 -68
  23. package/skills/questpie-admin/SKILL.md +96 -63
  24. package/skills/questpie-admin/references/blocks.md +28 -4
  25. package/skills/questpie-admin/references/custom-ui.md +1 -1
  26. package/skills/questpie-admin/references/views.md +21 -5
  27. package/templates/tanstack-start/AGENTS.md +15 -8
  28. package/templates/tanstack-start/CLAUDE.md +12 -5
  29. package/templates/tanstack-start/README.md +7 -6
  30. package/templates/tanstack-start/package.json +1 -0
  31. package/templates/tanstack-start/src/lib/query-client.ts +10 -1
  32. package/templates/tanstack-start/src/questpie/admin/modules.ts +3 -1
  33. package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
  34. package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
  35. package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
  36. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +2 -1
  37. package/templates/tanstack-start/src/routes/admin/$.tsx +12 -1
  38. package/templates/tanstack-start/src/routes/admin/index.tsx +12 -5
  39. package/templates/tanstack-start/src/routes/api/$.ts +1 -2
  40. package/templates/tanstack-start/vite.config.ts +2 -2
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: questpie-core/workflows
3
+ description:
4
+ QUESTPIE durable workflows long-running business processes replay steps sleep waitForEvent invoke compensation cron admin UI workflow service
5
+ - questpie-core
6
+ ---
7
+
8
+ # Durable Workflows
9
+
10
+ Use `@questpie/workflows` when business logic spans multiple steps, waits on time or external events, needs retry-safe side effects, or should survive process restarts.
11
+
12
+ ## Install And Register
13
+
14
+ ```bash
15
+ bun add @questpie/workflows
16
+ ```
17
+
18
+ ```ts title="modules.ts"
19
+ import { workflowsModule } from "@questpie/workflows/modules/workflows";
20
+ export default [workflowsModule] as const;
21
+ ```
22
+
23
+ `workflowsModule` carries its codegen plugin. Do not also add `workflowsPlugin()` to `questpie.config.ts` unless you are doing a custom module setup that deliberately omits `workflowsModule`.
24
+
25
+ Runtime options (route access rule — default admin-only — and execution-lease settings) go in plugin-discovered `config/workflows.ts` using the `workflowsConfig()` factory from `@questpie/workflows/server`.
26
+
27
+ For admin UI pages/widgets, register the client module:
28
+
29
+ ```ts title="questpie/admin/modules.ts"
30
+ import adminClientModule from "@questpie/admin/client-module";
31
+ import { workflowsClientModule } from "@questpie/workflows/client/modules/workflows";
32
+ export default {
33
+ name: "app-admin" as const,
34
+ views: { ...adminClientModule.views, ...workflowsClientModule.views },
35
+ components: {
36
+ ...adminClientModule.components,
37
+ ...workflowsClientModule.components,
38
+ },
39
+ fields: { ...adminClientModule.fields, ...workflowsClientModule.fields },
40
+ pages: { ...adminClientModule.pages, ...workflowsClientModule.pages },
41
+ widgets: { ...adminClientModule.widgets, ...workflowsClientModule.widgets },
42
+ blocks: { ...adminClientModule.blocks, ...workflowsClientModule.blocks },
43
+ };
44
+ ```
45
+
46
+ ## Define A Workflow
47
+
48
+ Put workflow definitions in `workflows/*.ts`:
49
+
50
+ ```ts title="workflows/production-order.ts"
51
+ import { workflow } from "@questpie/workflows";
52
+ import { z } from "zod";
53
+
54
+ export default workflow({
55
+ name: "production-order",
56
+ schema: z.object({
57
+ orderId: z.string(),
58
+ }),
59
+ timeout: "7d",
60
+ handler: async ({ input, step, ctx, log }) => {
61
+ const order = await step.run("load-order", async () => {
62
+ return ctx.collections.productionOrders.findOne({
63
+ where: { id: input.orderId },
64
+ with: { toy: true },
65
+ });
66
+ });
67
+ if (!order) throw new Error("Production order not found");
68
+
69
+ await step.run("reserve-materials", async () => {
70
+ await ctx.queue.recalculateMaterialPlan.publish({
71
+ orderId: input.orderId,
72
+ });
73
+ });
74
+
75
+ await step.waitForEvent("materials-ready", {
76
+ event: "materials.available",
77
+ match: { orderId: input.orderId },
78
+ timeout: "2d",
79
+ });
80
+
81
+ await step.run("notify-scheduled", async () => {
82
+ await ctx.email.sendTemplate({
83
+ template: "productionScheduled",
84
+ input: { orderId: input.orderId },
85
+ to: order.ownerEmail,
86
+ });
87
+ });
88
+
89
+ log.info("Production order workflow completed");
90
+ return { status: "scheduled" };
91
+ },
92
+ });
93
+ ```
94
+
95
+ Run codegen after adding workflow files:
96
+
97
+ ```bash
98
+ bun questpie generate
99
+ ```
100
+
101
+ ## Trigger And Signal
102
+
103
+ Use injected `workflows` from route, job, hook, or service context:
104
+
105
+ ```ts title="routes/start-production.ts"
106
+ import { route } from "questpie/services";
107
+ import { z } from "zod";
108
+
109
+ export default route()
110
+ .post()
111
+ .schema(z.object({ orderId: z.string() }))
112
+ .handler(async ({ input, workflows }) => {
113
+ const result = await workflows.trigger("production-order", {
114
+ orderId: input.orderId,
115
+ });
116
+ return { instanceId: result.instanceId };
117
+ });
118
+ ```
119
+
120
+ Signal waiting workflows:
121
+
122
+ ```ts
123
+ await workflows.sendEvent(
124
+ "materials.available",
125
+ { receivedAt: new Date().toISOString() },
126
+ { orderId },
127
+ );
128
+ ```
129
+
130
+ ## Cron Workflows
131
+
132
+ Use workflow-level `cron` for recurring long-running processes:
133
+
134
+ ```ts
135
+ export default workflow({
136
+ name: "nightly-material-plan",
137
+ schema: z.object({}),
138
+ cron: { schedule: "0 2 * * *", overlap: "skip" },
139
+ handler: async ({ step, ctx }) => {
140
+ await step.run("recalculate", async () => {
141
+ await ctx.queue.recalculateMaterialPlan.publish({});
142
+ });
143
+ },
144
+ });
145
+ ```
146
+
147
+ On Node/Bun workers, `app.queue.listen()` runs workflow jobs and maintenance. On Cloudflare Workers, use `cloudflareQueuesAdapter`, export `createCloudflareWorkerHandlers(app)`, and configure a Cron Trigger for workflow maintenance.
148
+
149
+ ## Rules
150
+
151
+ - Keep external side effects inside `step.run()` so replay does not repeat them.
152
+ - Use stable step names. Renaming a step changes replay identity.
153
+ - Use idempotency keys when calling external APIs.
154
+ - Use `step.waitForEvent()` for durable waits instead of polling loops.
155
+ - Keep workflow definitions in `workflows/`; do not define them inside route/job files.
@@ -20,7 +20,8 @@ For the complete admin reference with all topics expanded: `AGENTS.md`
20
20
  - **@base-ui/react** primitives (NOT @radix-ui)
21
21
  - **@iconify/react** with Phosphor icon set (`ph:icon-name`)
22
22
  - **sonner** for toasts — `toast.error()`, `toast.success()`
23
- - Brutalist flat design: `--radius: 0px`, no shadows
23
+ - QUESTPIE Neutral Design: flat surfaces, soft neutral geometry,
24
+ tokenized radius, and restrained floating shadows
24
25
 
25
26
  ## Setup
26
27
 
@@ -33,8 +34,7 @@ bun add @questpie/admin
33
34
  ### 2. Runtime Config
34
35
 
35
36
  ```ts title="questpie.config.ts"
36
- import { runtimeConfig } from "questpie";
37
-
37
+ import { runtimeConfig } from "questpie/app";
38
38
  export default runtimeConfig({
39
39
  app: { url: process.env.APP_URL || "http://localhost:3000" },
40
40
  db: { url: process.env.DATABASE_URL },
@@ -47,7 +47,8 @@ The admin module contributes the codegen plugin automatically. It discovers `con
47
47
  ### 3. Modules
48
48
 
49
49
  ```ts title="modules.ts"
50
- import { adminModule, auditModule } from "@questpie/admin/server";
50
+ import { adminModule } from "@questpie/admin/modules/admin";
51
+ import { auditModule } from "@questpie/admin/modules/audit";
51
52
 
52
53
  export default [adminModule, auditModule] as const;
53
54
  ```
@@ -57,6 +58,26 @@ export default [adminModule, auditModule] as const;
57
58
  | `adminModule` | User collection, auth pages, admin UI |
58
59
  | `auditModule` | Audit log collection, timeline widget |
59
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
+
60
81
  ### 4. Admin Config
61
82
 
62
83
  ```ts title="config/admin.ts"
@@ -129,44 +150,31 @@ export default adminConfig({
129
150
 
130
151
  The admin uses CSS variables for all theming. Override them in your own CSS file.
131
152
 
132
- ### Light Theme (`:root`)
133
-
134
- | Variable | Default | Purpose |
135
- | ---------------------- | --------- | -------------------------------- |
136
- | `--background` | `#FFFFFF` | Page background |
137
- | `--foreground` | `#0A0A0A` | Primary text |
138
- | `--card` | `#F8F8F8` | Cards, panels, sidebar |
139
- | `--popover` | `#FFFFFF` | Dropdowns, tooltips, dialogs |
140
- | `--muted` | `#F0F0F0` | Hover states, table headers |
141
- | `--muted-foreground` | `#666666` | Secondary text, placeholders |
142
- | `--primary` | `#B700FF` | Brand accent (CTAs, focus rings) |
143
- | `--primary-foreground` | `#FFFFFF` | Text on primary backgrounds |
144
- | `--destructive` | `#FF3D57` | Errors, delete actions |
145
- | `--success` | `#00E676` | Positive states |
146
- | `--warning` | `#FFB300` | Caution states |
147
- | `--info` | `#40C4FF` | Informational emphasis |
148
- | `--border` | `#E0E0E0` | All structural borders |
149
- | `--ring` | `#B700FF` | Focus ring color |
150
- | `--radius` | `0px` | Border radius (0 = brutalist) |
151
-
152
- ### Dark Theme (`.dark` class)
153
-
154
- Dark mode uses the `.dark` class on the root element. Key overrides:
155
-
156
- | Variable | Dark Value |
157
- | -------------- | ---------- |
158
- | `--background` | `#0A0A0A` |
159
- | `--foreground` | `#FFFFFF` |
160
- | `--card` | `#111111` |
161
- | `--border` | `#333333` |
162
- | `--muted` | `#1A1A1A` |
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 |
163
171
 
164
172
  ### Typography
165
173
 
166
174
  | Variable | Value |
167
175
  | ------------- | ------------------------------------------------------------------- |
168
- | `--font-sans` | `"Geist Variable"` — body text, descriptions |
169
- | `--font-mono` | `"JetBrains Mono Variable"` — UI chrome: nav, buttons, tabs, badges |
176
+ | `--font-sans` | `"Geist Variable"` — UI, prose, headings, labels, navigation |
177
+ | `--font-mono` | `"JetBrains Mono Variable"` — code, file paths, commands, IDs |
170
178
 
171
179
  ### Sidebar Variables
172
180
 
@@ -184,8 +192,7 @@ Separate tokens for independent sidebar theming: `--sidebar`, `--sidebar-foregro
184
192
  When collections have `.localized()` fields, the admin shows a locale switcher:
185
193
 
186
194
  ```ts title="config/app.ts"
187
- import { appConfig } from "questpie";
188
-
195
+ import { appConfig } from "questpie/app";
189
196
  export default appConfig({
190
197
  locale: {
191
198
  locales: [
@@ -213,9 +220,9 @@ The admin renders drag-and-drop upload, image preview, file info, and remove but
213
220
 
214
221
  ## Live Preview
215
222
 
216
- Live Preview uses a split-screen iframe. The current implementation refreshes the iframe after save/autosave and uses `postMessage` for field/block focus sync.
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.
217
224
 
218
- Preview V2 patch-based docs are design notes until `useQuestpiePreview`, `PreviewRoot`, and `PreviewBlock` are exported.
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.
219
226
 
220
227
  ### Server Config
221
228
 
@@ -239,35 +246,57 @@ export const pages = collection("pages")
239
246
  });
240
247
  ```
241
248
 
242
- Current preview refreshes the iframe after save/autosave and supports field focus through `postMessage`.
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
243
252
 
244
- ### Frontend Integration
253
+ Use exported APIs only: `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and `BlockRenderer`.
245
254
 
246
- Use `useCollectionPreview` with `PreviewProvider` and `PreviewField`:
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.
247
266
 
248
267
  ```tsx
249
268
  import {
269
+ BlockRenderer,
250
270
  PreviewField,
251
271
  PreviewProvider,
252
272
  useCollectionPreview,
253
273
  } from "@questpie/admin/client";
274
+ import admin from "@/questpie/admin/.generated/client";
254
275
 
255
- function PagePreview({ initialData }) {
276
+ function PagePreview({ page }) {
256
277
  const router = useRouter();
257
278
  const preview = useCollectionPreview({
258
- initialData,
279
+ initialData: page,
259
280
  onRefresh: () => router.invalidate(),
260
281
  });
261
282
 
262
283
  return (
263
- <PreviewProvider
264
- isPreviewMode={preview.isPreviewMode}
265
- focusedField={preview.focusedField}
266
- onFieldClick={preview.handleFieldClick}
267
- >
268
- <PreviewField field="title" as="h1">
269
- {preview.data.title}
270
- </PreviewField>
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>
271
300
  </PreviewProvider>
272
301
  );
273
302
  }
@@ -275,11 +304,15 @@ function PagePreview({ initialData }) {
275
304
 
276
305
  ### Key Principles
277
306
 
278
- - Current preview = save/autosave refresh plus field/block focus sync
279
- - `useCollectionPreview` sends `PREVIEW_READY`, `FIELD_CLICKED`, and `BLOCK_CLICKED`
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
280
311
  - `PreviewProvider` supplies preview context to `PreviewField`
281
- - Each message carries `sessionId`, `seq`, `timestamp`, `protocolVersion`
282
- - Preview wrappers must prevent accidental navigation in the iframe
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
283
316
 
284
317
  ## History & Versions
285
318
 
@@ -560,7 +593,7 @@ Section-level visibility:
560
593
  {
561
594
  type: "section",
562
595
  label: { en: "SEO" },
563
- hidden: ({ data }) => !data.isPublished,
596
+ hidden: ({ data }) => !data.showSeo,
564
597
  fields: [f.metaTitle, f.metaDescription],
565
598
  }
566
599
  ```
@@ -803,7 +836,7 @@ export const logs = collection("logs")
803
836
 
804
837
  ## Form Views and Live Preview
805
838
 
806
- Form views connect to the Live Preview V2 system when the collection has `.preview()` configured. The form editor becomes the source of `postMessage` patches every field change emits a patch through the bus, giving the preview iframe instant updates.
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.
807
840
 
808
841
  ### Enabling Preview on a Collection
809
842
 
@@ -827,9 +860,25 @@ export const pages = collection("pages")
827
860
  ### How It Works
828
861
 
829
862
  1. The form view detects `.preview()` config and opens a split-screen layout
830
- 2. Save/autosave sends a `PREVIEW_REFRESH` message to the preview iframe
831
- 3. The preview page handles refreshes through `useCollectionPreview({ initialData, onRefresh })`
832
- 4. `PreviewProvider` and `PreviewField` wire field focus and click-to-focus messages
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.
833
882
 
834
883
  ---
835
884
 
@@ -1111,11 +1160,35 @@ function PageRenderer({ page }) {
1111
1160
 
1112
1161
  ## Blocks in Live Preview
1113
1162
 
1114
- When a collection has `.preview()` configured, blocks can participate in preview focus by combining `BlockScopeProvider` with `PreviewField`.
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`.
1115
1188
 
1116
- ### BlockScopeProvider Wrapper
1189
+ ### Manual BlockScopeProvider Wrapper
1117
1190
 
1118
- Use `BlockScopeProvider` in your frontend to scope field paths inside a block:
1191
+ Use `BlockScopeProvider` only when you manually render blocks outside `BlockRenderer`:
1119
1192
 
1120
1193
  ```tsx
1121
1194
  import { BlockScopeProvider } from "@questpie/admin/client";
@@ -1134,7 +1207,7 @@ function PageRenderer({ blocks, previewData }) {
1134
1207
 
1135
1208
  `PreviewField` components inside the provider resolve paths like `content._values.{blockId}.title`.
1136
1209
 
1137
- 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.
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.
1138
1211
 
1139
1212
  ---
1140
1213
 
@@ -1437,6 +1510,6 @@ toast.error("Failed to save");
1437
1510
 
1438
1511
  6. **MEDIUM: Using `@phosphor-icons/react` or `lucide-react`** — use `@iconify/react` with `ph:` prefix for all icons.
1439
1512
 
1440
- 7. **LOW: Not using shadcn components** — prefer `<Button>`, `<Card>`, `<Input>` from the shadcn component library instead of raw HTML elements. The admin has a consistent brutalist design system.
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.
1441
1514
 
1442
1515
  ---