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.
- package/README.md +10 -6
- package/dist/index.mjs +140 -25
- package/package.json +5 -3
- package/skills/questpie/AGENTS.md +2664 -0
- package/skills/questpie/SKILL.md +181 -0
- package/skills/questpie/references/auth.md +121 -0
- package/skills/questpie/references/business-logic.md +550 -0
- package/skills/questpie/references/codegen-plugin-api.md +382 -0
- package/skills/questpie/references/crud-api.md +378 -0
- package/skills/questpie/references/data-modeling.md +489 -0
- package/skills/questpie/references/extend.md +493 -0
- package/skills/questpie/references/field-types.md +386 -0
- package/skills/questpie/references/infrastructure-adapters.md +545 -0
- package/skills/questpie/references/multi-tenancy.md +364 -0
- package/skills/questpie/references/production.md +475 -0
- package/skills/questpie/references/query-operators.md +125 -0
- package/skills/questpie/references/quickstart.md +549 -0
- package/skills/questpie/references/rules.md +327 -0
- package/skills/questpie/references/tanstack-query.md +520 -0
- package/skills/questpie-admin/AGENTS.md +1442 -0
- package/skills/questpie-admin/SKILL.md +410 -0
- package/skills/questpie-admin/references/blocks.md +307 -0
- package/skills/questpie-admin/references/custom-ui.md +305 -0
- package/skills/questpie-admin/references/views.md +433 -0
- package/templates/tanstack-start/AGENTS.md +71 -62
- package/templates/tanstack-start/CLAUDE.md +26 -23
- package/templates/tanstack-start/README.md +32 -20
- package/templates/tanstack-start/env.example +1 -1
- package/templates/tanstack-start/package.json +20 -6
- package/templates/tanstack-start/src/lib/client.ts +2 -2
- package/templates/tanstack-start/src/lib/env.ts +1 -1
- package/templates/tanstack-start/src/questpie/admin/.generated/client.ts +13 -0
- package/templates/tanstack-start/src/questpie/admin/modules.ts +1 -0
- package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +117 -241
- package/templates/tanstack-start/src/questpie/server/.generated/index.ts +129 -81
- package/templates/tanstack-start/src/questpie/server/app.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/config/admin.ts +27 -30
- package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
- package/templates/tanstack-start/src/routeTree.gen.ts +138 -0
- package/templates/tanstack-start/src/routes/__root.tsx +0 -2
- package/templates/tanstack-start/src/routes/admin.tsx +8 -1
- package/templates/tanstack-start/src/tanstack-start.d.ts +1 -0
- package/templates/tanstack-start/src/vite-env.d.ts +1 -0
- package/templates/tanstack-start/vite.config.ts +1 -3
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: questpie-admin/custom-ui
|
|
3
|
+
description: QUESTPIE custom-fields custom-views registries field-registry view-registry component-registry reactive-fields dynamic-options widgets field-renderer cell-renderer
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# QUESTPIE Custom UI
|
|
7
|
+
|
|
8
|
+
This skill builds on questpie-admin.
|
|
9
|
+
|
|
10
|
+
Extend the QUESTPIE admin with custom field types, custom view types, custom components, and reactive field behaviors.
|
|
11
|
+
|
|
12
|
+
## Registries
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
Server: f.text().required()
|
|
18
|
+
|
|
|
19
|
+
Generated: { type: "text", options: {...} }
|
|
20
|
+
|
|
|
21
|
+
Admin Client: fieldRegistry.get("text")
|
|
22
|
+
|
|
|
23
|
+
React: <TextFieldRenderer value={...} onChange={...} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Built-in Field Registry
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
text -> TextInput
|
|
30
|
+
textarea -> TextareaInput
|
|
31
|
+
richText -> RichTextEditor (TipTap)
|
|
32
|
+
number -> NumberInput
|
|
33
|
+
boolean -> Checkbox / Switch
|
|
34
|
+
date -> DatePicker
|
|
35
|
+
datetime -> DateTimePicker
|
|
36
|
+
select -> SelectDropdown
|
|
37
|
+
relation -> RelationPicker
|
|
38
|
+
upload -> FileUpload
|
|
39
|
+
object -> NestedForm
|
|
40
|
+
array -> RepeatableItems
|
|
41
|
+
blocks -> BlockEditor
|
|
42
|
+
json -> JSONEditor
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Extending Registries
|
|
46
|
+
|
|
47
|
+
Place files in the admin directory. Codegen discovers them automatically:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
questpie/admin/
|
|
51
|
+
fields/
|
|
52
|
+
color.tsx # Custom color field renderer
|
|
53
|
+
currency.tsx # Custom currency field renderer
|
|
54
|
+
views/
|
|
55
|
+
kanban.tsx # Custom kanban list view
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
These are merged with built-in defaults during codegen and exported in `.generated/client.ts`.
|
|
59
|
+
|
|
60
|
+
## Custom Fields
|
|
61
|
+
|
|
62
|
+
### Server-Side Registration
|
|
63
|
+
|
|
64
|
+
Register custom fields through modules:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const myModule = module({
|
|
68
|
+
name: "custom-fields",
|
|
69
|
+
fields: {
|
|
70
|
+
color: colorField,
|
|
71
|
+
currency: currencyField,
|
|
72
|
+
phone: phoneField,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Once registered and codegen runs, the field becomes available on the `f` builder:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
.fields(({ f }) => ({
|
|
81
|
+
brandColor: f.color().default("#000000"),
|
|
82
|
+
price: f.currency({ currency: "USD" }),
|
|
83
|
+
}))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Admin Field Renderer
|
|
87
|
+
|
|
88
|
+
Create a React component for the field's edit form:
|
|
89
|
+
|
|
90
|
+
```tsx title="admin/fields/color.tsx"
|
|
91
|
+
import { Icon } from "@iconify/react";
|
|
92
|
+
|
|
93
|
+
function ColorFieldRenderer({ value, onChange }) {
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<input
|
|
97
|
+
type="color"
|
|
98
|
+
value={value || "#000000"}
|
|
99
|
+
onChange={(e) => onChange(e.target.value)}
|
|
100
|
+
className="w-10 h-10 border border-border cursor-pointer"
|
|
101
|
+
/>
|
|
102
|
+
<span className="font-mono text-sm text-muted-foreground">
|
|
103
|
+
{value || "#000000"}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Cell Renderer
|
|
111
|
+
|
|
112
|
+
For custom table column rendering, provide a `cell` component alongside the field renderer:
|
|
113
|
+
|
|
114
|
+
```tsx title="admin/fields/color.tsx"
|
|
115
|
+
// Cell component for list view table
|
|
116
|
+
export function ColorCell({ value }) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<div
|
|
120
|
+
className="w-4 h-4 border border-border"
|
|
121
|
+
style={{ backgroundColor: value || "transparent" }}
|
|
122
|
+
/>
|
|
123
|
+
<span className="text-xs font-mono">{value}</span>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Custom Views
|
|
130
|
+
|
|
131
|
+
Create view types beyond built-in table and form — kanban boards, calendars, galleries.
|
|
132
|
+
|
|
133
|
+
### Server-Side Declaration
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const myModule = module({
|
|
137
|
+
name: "custom-views",
|
|
138
|
+
views: {
|
|
139
|
+
kanban: kanbanViewDefinition,
|
|
140
|
+
calendar: calendarViewDefinition,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Usage in Collections
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
.list(({ v }) => v.kanban({
|
|
149
|
+
columns: "status",
|
|
150
|
+
cardTitle: "title",
|
|
151
|
+
}))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Client Rendering
|
|
155
|
+
|
|
156
|
+
```tsx title="admin/views/kanban.tsx"
|
|
157
|
+
function KanbanView({ data, columns, onDrop }) {
|
|
158
|
+
return (
|
|
159
|
+
<div className="flex gap-4">
|
|
160
|
+
{columns.map((col) => (
|
|
161
|
+
<div key={col.id} className="flex-1">
|
|
162
|
+
<h3 className="font-mono text-sm font-semibold mb-2">{col.label}</h3>
|
|
163
|
+
{data
|
|
164
|
+
.filter((item) => item.status === col.id)
|
|
165
|
+
.map((item) => (
|
|
166
|
+
<div
|
|
167
|
+
key={item.id}
|
|
168
|
+
className="border border-border bg-card p-3 mb-2"
|
|
169
|
+
>
|
|
170
|
+
{item.title}
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Reactive Field System
|
|
181
|
+
|
|
182
|
+
Fields support reactive behaviors configured in the collection's `.form()` view or on the field definition itself.
|
|
183
|
+
|
|
184
|
+
### Conditional Visibility
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
{
|
|
188
|
+
field: f.cancellationReason,
|
|
189
|
+
hidden: ({ data }) => data.status !== "cancelled",
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Read-Only
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
{
|
|
197
|
+
field: f.customerName,
|
|
198
|
+
readOnly: ({ data }) => !!data.customer,
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Computed Values
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
{
|
|
206
|
+
field: f.slug,
|
|
207
|
+
compute: {
|
|
208
|
+
handler: ({ data }) => {
|
|
209
|
+
if (data.name && !data.slug?.trim()) {
|
|
210
|
+
return slugify(data.name);
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
},
|
|
214
|
+
deps: ({ data }) => [data.name, data.slug],
|
|
215
|
+
debounce: 300,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Dynamic Options (Server-Side)
|
|
221
|
+
|
|
222
|
+
For select/relation fields with options that depend on other field values:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
city: f.relation("cities").admin({
|
|
226
|
+
options: {
|
|
227
|
+
handler: async ({ data, search, ctx }) => {
|
|
228
|
+
const cities = await ctx.db.query.cities.findMany({
|
|
229
|
+
where: { countryId: data.country },
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
options: cities.map((c) => ({ value: c.id, label: c.name })),
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
deps: ({ data }) => [data.country],
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The `handler` runs **server-side** with full access to `ctx.db`, `ctx.user`, `ctx.req`. It re-executes when any value in `deps` changes.
|
|
241
|
+
|
|
242
|
+
## UI Component Reference
|
|
243
|
+
|
|
244
|
+
When building custom admin UI, use these patterns:
|
|
245
|
+
|
|
246
|
+
### Icons
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import { Icon } from "@iconify/react";
|
|
250
|
+
|
|
251
|
+
// Phosphor icon set with ph: prefix
|
|
252
|
+
<Icon icon="ph:house" width={20} height={20} />
|
|
253
|
+
<Icon icon="ph:caret-down-bold" width={16} height={16} /> // bold weight
|
|
254
|
+
<Icon icon="ph:heart-fill" width={16} height={16} /> // fill weight
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Toasts
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { toast } from "sonner";
|
|
261
|
+
|
|
262
|
+
toast.success("Record saved");
|
|
263
|
+
toast.error("Failed to save");
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Primitives (base-ui)
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
// CORRECT — render prop
|
|
270
|
+
<DialogTrigger render={<Button>Open</Button>} />
|
|
271
|
+
|
|
272
|
+
// WRONG — asChild is Radix, not base-ui
|
|
273
|
+
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Responsive Components
|
|
277
|
+
|
|
278
|
+
- `ResponsivePopover` — Popover on desktop, Drawer on mobile
|
|
279
|
+
- `ResponsiveDialog` — Dialog on desktop, fullscreen Drawer on mobile
|
|
280
|
+
- Hooks: `useIsMobile()`, `useIsDesktop()`, `useMediaQuery()`
|
|
281
|
+
|
|
282
|
+
## Common Mistakes
|
|
283
|
+
|
|
284
|
+
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`.
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
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.
|
|
289
|
+
|
|
290
|
+
4. **MEDIUM: Using `onChange` wrong in field components** — the field renderer receives `onChange` that expects the **value directly**, not a DOM event.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
// WRONG
|
|
294
|
+
onChange={(e) => onChange(e)}
|
|
295
|
+
// CORRECT
|
|
296
|
+
onChange={(e) => onChange(e.target.value)}
|
|
297
|
+
// Or for non-DOM values:
|
|
298
|
+
onChange={newValue}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
5. **MEDIUM: Importing from `@radix-ui/*`** — QUESTPIE admin uses `@base-ui/react`. Never import Radix primitives.
|
|
302
|
+
|
|
303
|
+
6. **MEDIUM: Using `@phosphor-icons/react` or `lucide-react`** — use `@iconify/react` with `ph:` prefix for all icons.
|
|
304
|
+
|
|
305
|
+
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.
|