@uidu/skills 0.5.0 → 0.6.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.
- package/package.json +1 -1
- package/skills/uidu/SKILL.md +187 -61
package/package.json
CHANGED
package/skills/uidu/SKILL.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: uidu
|
|
3
|
-
description: Use when building on the uidu platform — a public website or an app that runs inside uidu (a custom app in a Space or workspace); reading CMS pages, events, stories, donations, help center, forms; rendering WYSIWYG content; scaffolding projects; OR provisioning/authoring uidu content (create/update/delete) from the terminal via the @uidu/cli (`uidu`). Triggers on any task involving uidu, the @uidu/client SDK, @uidu/react bindings, or the uidu CLI.
|
|
3
|
+
description: Use when building on the uidu platform — a public website or an app that runs inside uidu (a custom app in a Space or workspace); reading CMS pages, events, stories, donations, help center, forms, or the workspace's own data (goals/OKRs, contacts, deals, people); rendering WYSIWYG content; scaffolding projects; OR provisioning/authoring uidu content (create/update/delete) from the terminal via the @uidu/cli (`uidu`). Triggers on any task involving uidu, the @uidu/client SDK, @uidu/react bindings, or the uidu CLI.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: uidu
|
|
7
|
-
version:
|
|
7
|
+
version: '0.6.0'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# uidu SDK
|
|
@@ -13,7 +13,7 @@ The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powe
|
|
|
13
13
|
|
|
14
14
|
- **`@uidu/client`** — framework-agnostic GraphQL client. Safe for server-side use (RSC, route handlers, server actions, scripts).
|
|
15
15
|
- **`@uidu/app-bridge`** — the browser half of a custom app: the handshake with the uidu page that frames it, and its short-lived session token. Only for apps that run inside uidu.
|
|
16
|
-
- **`@uidu/react`** — React hooks (`useUiduClient`, `useQuery`, `useFields`, `toText`, `useUiduApp`) and components (`<UiduAppProvider>`, `<PageBlocks>`, `<BlockRenderer>`, `<RichText>`, `<DynamicForm>`). That list is the **complete** public surface — there is no `useForm`, `usePage` or `useChannel`; for anything else, call a `@uidu/client` function.
|
|
16
|
+
- **`@uidu/react`** — React hooks (`useUiduClient`, `useQuery`, `useFields`, `toText`, `useUiduApp`) and components (`<UiduAppProvider>`, `<PageBlocks>`, `<BlockRenderer>`, `<RichText>`, `<DynamicForm>`). That list is the **complete** public surface — there is no `useForm`, `usePage` or `useChannel`; for anything else, call a `@uidu/client` function. `@uidu/client` has one for each entity uidu already holds (goals, contacts, deals, people, events… — see [Workspace data](#workspace-data)): look there before writing a query or creating a Model.
|
|
17
17
|
|
|
18
18
|
`@uidu/api.js` is a legacy bridge package and is **deprecated** — use `@uidu/react` directly.
|
|
19
19
|
|
|
@@ -31,12 +31,12 @@ Use this skill when the user is:
|
|
|
31
31
|
|
|
32
32
|
## Package Map
|
|
33
33
|
|
|
34
|
-
| Package
|
|
35
|
-
|
|
36
|
-
| `@uidu/client`
|
|
37
|
-
| `@uidu/react`
|
|
38
|
-
| `@uidu/app-bridge` | An app that runs inside uidu (custom app)
|
|
39
|
-
| `@uidu/api.js`
|
|
34
|
+
| Package | When to use | Notes |
|
|
35
|
+
| ------------------ | -------------------------------------------------- | ---------------------------------------------- |
|
|
36
|
+
| `@uidu/client` | Server-side queries (RSC, server actions, scripts) | Framework-agnostic |
|
|
37
|
+
| `@uidu/react` | React hooks, providers, and components | Some hooks are client-only |
|
|
38
|
+
| `@uidu/app-bridge` | An app that runs inside uidu (custom app) | Browser only; in React use `<UiduAppProvider>` |
|
|
39
|
+
| `@uidu/api.js` | **Do not use for new code.** | Deprecated; re-exports `@uidu/react` |
|
|
40
40
|
|
|
41
41
|
Install:
|
|
42
42
|
|
|
@@ -80,14 +80,14 @@ reports `does not support <verb>` when it doesn't. Config resolves **flags → e
|
|
|
80
80
|
|
|
81
81
|
## Who the app acts for — one rule
|
|
82
82
|
|
|
83
|
-
What an app
|
|
83
|
+
What an app _looks like_ (landing page, blog, dashboard, booking tool) changes nothing.
|
|
84
84
|
What changes the code is **who the request acts for**:
|
|
85
85
|
|
|
86
|
-
| Operation
|
|
87
|
-
|
|
88
|
-
| **Reads** for anyone (`get*` / `list*`, `<entity> list/get`)
|
|
89
|
-
| **Writes** as the workspace (`create*` / `update*` / `delete*`, `<entity> create/update/delete`) | account **Bearer** (`apiKey`)
|
|
90
|
-
| **Anything as the signed-in member**, inside uidu (a custom app)
|
|
86
|
+
| Operation | Auth | Where it runs |
|
|
87
|
+
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------- |
|
|
88
|
+
| **Reads** for anyone (`get*` / `list*`, `<entity> list/get`) | `publicToken` | anywhere — server, CLI, **and the browser** |
|
|
89
|
+
| **Writes** as the workspace (`create*` / `update*` / `delete*`, `<entity> create/update/delete`) | account **Bearer** (`apiKey`) | CLI and **server-side** only |
|
|
90
|
+
| **Anything as the signed-in member**, inside uidu (a custom app) | session token from the host, over `@uidu/app-bridge` | **the browser only** |
|
|
91
91
|
|
|
92
92
|
The write functions live in `@uidu/client`, so anything the CLI provisions you can also do
|
|
93
93
|
from a Next.js server action / route handler / RSC. **Never put the account Bearer token in a
|
|
@@ -111,10 +111,13 @@ that person can do, and no more. It changes five things:
|
|
|
111
111
|
needs a server credential uidu doesn't issue yet.
|
|
112
112
|
3. **Treat the token as opaque.** Never decode it. The expiry comes with it, the bridge refreshes
|
|
113
113
|
it, and `graphqlUrl` comes with it too: never build `https://<workspace>.uidu.org` from a slug.
|
|
114
|
-
4. **
|
|
115
|
-
items
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
4. **Read what the workspace already has; keep only new data in the app's own Models.** The
|
|
115
|
+
session reaches the app's own Models, fields and items (`node(id:)` and the data-engine
|
|
116
|
+
mutations) and, read-only, the workspace's **goals and timeframes** (`listGoals`, `getGoal`,
|
|
117
|
+
`listTimeframes`) — those of the workspace and of the Space the app sits in. Everything else in
|
|
118
|
+
the workspace comes back as an error for now: see [Workspace data](#workspace-data) for which
|
|
119
|
+
entity is reachable from where. Keep the app's own data in Models rather than in a database of
|
|
120
|
+
your own: it stays searchable and permissioned in uidu.
|
|
118
121
|
`listModelItems` reads uidu's **search index**, which catches up a moment after a write:
|
|
119
122
|
after `createModelItem` / `deleteModelItem`, update your list from the mutation payload
|
|
120
123
|
instead of re-listing, or the new item is missing. And load models once per app instance
|
|
@@ -129,24 +132,39 @@ import { useEffect, useState } from 'react';
|
|
|
129
132
|
import { UiduAppProvider, useUiduApp } from '@uidu/react';
|
|
130
133
|
import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';
|
|
131
134
|
import {
|
|
132
|
-
createModelItem,
|
|
133
|
-
|
|
135
|
+
createModelItem,
|
|
136
|
+
ensureModel,
|
|
137
|
+
listModelItems,
|
|
138
|
+
toFieldValuesAttributes,
|
|
139
|
+
type ModelItem,
|
|
140
|
+
type UiduClient,
|
|
134
141
|
} from '@uidu/client';
|
|
135
142
|
|
|
136
143
|
export function AppProvider({ children }: { children: React.ReactNode }) {
|
|
137
144
|
// https://*.uidu.org by default; add a local uidu or a custom domain
|
|
138
|
-
return
|
|
145
|
+
return (
|
|
146
|
+
<UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS]}>
|
|
147
|
+
{children}
|
|
148
|
+
</UiduAppProvider>
|
|
149
|
+
);
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
async function loadBookings(client: UiduClient, workspaceAppId: string) {
|
|
142
|
-
// interim: the app creates its own model on first load (install-time manifest comes later)
|
|
153
|
+
// interim: the app creates its own model on first load (install-time manifest comes later).
|
|
154
|
+
// Bookings of a room are data uidu has no place for — hence a Model. Goals, contacts or
|
|
155
|
+
// people already exist: read them (see Workspace data).
|
|
143
156
|
const model = await ensureModel(client, {
|
|
144
157
|
workspaceAppId,
|
|
145
158
|
name: 'Booking',
|
|
146
159
|
fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
|
|
147
160
|
});
|
|
148
161
|
await createModelItem(client, {
|
|
149
|
-
input: {
|
|
162
|
+
input: {
|
|
163
|
+
attributes: {
|
|
164
|
+
modelId: model.id,
|
|
165
|
+
fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Blu' }),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
150
168
|
});
|
|
151
169
|
return listModelItems(client, { modelId: model.id }); // item.fieldValuesByShortname.room
|
|
152
170
|
}
|
|
@@ -156,19 +174,83 @@ export function Bookings() {
|
|
|
156
174
|
const [items, setItems] = useState<ModelItem[]>([]);
|
|
157
175
|
|
|
158
176
|
useEffect(() => {
|
|
159
|
-
if (app.status === 'ready')
|
|
177
|
+
if (app.status === 'ready')
|
|
178
|
+
loadBookings(app.client, app.context.workspaceApp.id).then(setItems);
|
|
160
179
|
}, [app]);
|
|
161
180
|
|
|
162
|
-
if (app.status === 'error')
|
|
163
|
-
|
|
181
|
+
if (app.status === 'error')
|
|
182
|
+
return <p>Open this app from uidu ({app.error.code})</p>; // NOT_EMBEDDED, TIMEOUT…
|
|
183
|
+
return (
|
|
184
|
+
<ul>
|
|
185
|
+
{items.map((item) => (
|
|
186
|
+
<li key={item.id}>{item.fieldValuesByShortname?.room}</li>
|
|
187
|
+
))}
|
|
188
|
+
</ul>
|
|
189
|
+
);
|
|
164
190
|
}
|
|
165
191
|
```
|
|
166
192
|
|
|
193
|
+
Field `kind`s for `ensureModel` (and `public/uidu.app.json`): `string` (one line), `text`
|
|
194
|
+
(several lines), `number`, `currency`, `percent`, `date`, `datetime`, `checkbox`, `email`,
|
|
195
|
+
`phone`, `url`, `rating`, `member` (a person of the workspace). `singleSelect` /
|
|
196
|
+
`multipleSelect` need their options created too (`createFieldOption`) — prefer `string` unless
|
|
197
|
+
the choices matter. uidu knows more (`attachments`, `linkedRecord`, `formula`, `progress`,
|
|
198
|
+
`richText`…); leave them out unless the person asks for one.
|
|
199
|
+
|
|
167
200
|
`app.context` carries `user`, `space`, `workspaceApp`, `locale`, `theme` and `accent` (the page's colour, to set as `--primary`). Outside React: `connect()` from
|
|
168
201
|
`@uidu/app-bridge`, then `createClient(fromBridge(bridge))` from `@uidu/client`. Start a new
|
|
169
202
|
one with `npm create uidu-app@latest my-app -- -t custom-app`: it declares its Models in
|
|
170
203
|
`public/uidu.app.json` and sets `frame-ancestors` so only uidu can frame it.
|
|
171
204
|
|
|
205
|
+
## Workspace data
|
|
206
|
+
|
|
207
|
+
**A uidu workspace is not empty.** It already holds the organisation's goals, contacts, deals,
|
|
208
|
+
people, events, courses… An app that asks for "a dashboard of the OKRs behind schedule" or "our
|
|
209
|
+
open deals" is asking about data uidu has — read it. **Create a Model only for data uidu has no
|
|
210
|
+
place for** (the app's own bookings, a checklist, a log). Copying existing records into a Model
|
|
211
|
+
gives the person a second copy that drifts from the real one.
|
|
212
|
+
|
|
213
|
+
| The workspace's… | Functions (`@uidu/client`) | Inside a custom app |
|
|
214
|
+
| ----------------------------------- | ----------------------------------------------------------------- | ------------------------------ |
|
|
215
|
+
| Goals / OKRs (key results = subgoals) | `listGoals`, `getGoal`, `listTimeframes`; `updateGoal` | **read** (no `updateGoal`) |
|
|
216
|
+
| Contacts, organisations | `listContacts`, `getContact`; `createContact` | not yet |
|
|
217
|
+
| Deals (CRM) | `listDeals`, `getDeal`; `createDeal`, `updateDeal` | not yet |
|
|
218
|
+
| People (employees) | `listEmployees`, `getEmployee`, `listOffices`, `listCircles`, `listRoles` | not yet |
|
|
219
|
+
| Tasks, notes | `updateTask`, `createTask`, `updateNote`, `createNote` (no list) | not yet |
|
|
220
|
+
| Events, courses, bookings, calendars | `listEvents`, `listCourses`, `listBookings`, `listCalendars`, … | not yet |
|
|
221
|
+
| Forms, donations, stories, help center | see [Patterns by Scope](#patterns-by-scope) | not yet |
|
|
222
|
+
| The app's own Models | `ensureModel`, `listModelItems`, `createModelItem`, … | read and write |
|
|
223
|
+
|
|
224
|
+
On a server with an account token (`apiKey`) every row works; "not yet" means a custom app's
|
|
225
|
+
session token is refused for it. **When the data exists in uidu but is not reachable from a custom
|
|
226
|
+
app yet, tell the person** — don't rebuild it in a Model. If the functions you need aren't in the
|
|
227
|
+
table, look for `listX` / `getX` in `@uidu/client` before concluding they don't exist.
|
|
228
|
+
|
|
229
|
+
### Goals (OKRs)
|
|
230
|
+
|
|
231
|
+
An objective is a `Goal`; its key results are its `subgoals`; the period it runs over is its
|
|
232
|
+
`timeframe` (`startDate`, `endDate`). `metricKind` is `number`, `percentage`, `currency`,
|
|
233
|
+
`checkbox` or `subgoal` (measured by its key results); `status` is the owner's own judgement:
|
|
234
|
+
`on_track`, `needs_attention`, `off_track`, `accomplished`.
|
|
235
|
+
|
|
236
|
+
**Values are stored x100.** `initialValue`, `currentValue` and `targetValue` never hold the number
|
|
237
|
+
a person typed, and `progress` is percent x100 (10000 = done). Use the helpers, don't divide by hand:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { listGoals, goalValue, goalProgress, isGoalBehind } from '@uidu/client';
|
|
241
|
+
|
|
242
|
+
const goals = await listGoals(app.client); // all of them, objectives and key results
|
|
243
|
+
const objectives = goals.filter((g) => !g.parentId);
|
|
244
|
+
const behind = objectives.filter((g) => isGoalBehind(g, { tolerance: 0.1 }));
|
|
245
|
+
|
|
246
|
+
goalValue(goal, goal.targetValue); // 45 for a 45% target, 1200 for €1,200
|
|
247
|
+
goalProgress(goal); // 0.45 — done fraction
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`isGoalBehind` compares progress with the share of the timeframe already gone (`goalTimeElapsed`);
|
|
251
|
+
show the goal's `status` next to it, it says something different. `updateGoal` takes raw values:
|
|
252
|
+
`toGoalRaw(45)`. A goal's owner, members and activity are not readable from a custom app.
|
|
253
|
+
|
|
172
254
|
## Working transparently
|
|
173
255
|
|
|
174
256
|
The user often can't tell what you're doing when you drive the CLI. Stay legible:
|
|
@@ -215,23 +297,66 @@ import { createClient } from '@uidu/client';
|
|
|
215
297
|
export const uidu = createClient({
|
|
216
298
|
workspace: process.env.UIDU_WORKSPACE ?? '',
|
|
217
299
|
publicToken: process.env.UIDU_PUBLIC_TOKEN, // safe to expose
|
|
218
|
-
apiKey: process.env.UIDU_API_KEY,
|
|
300
|
+
apiKey: process.env.UIDU_API_KEY, // server-only secret
|
|
219
301
|
});
|
|
220
302
|
```
|
|
221
303
|
|
|
222
304
|
Required environment:
|
|
223
305
|
|
|
224
|
-
| Variable
|
|
225
|
-
|
|
226
|
-
| `UIDU_WORKSPACE`
|
|
227
|
-
| `UIDU_PUBLIC_TOKEN` | no
|
|
228
|
-
| `UIDU_API_KEY`
|
|
306
|
+
| Variable | Required | Notes |
|
|
307
|
+
| ------------------- | -------- | -------------------------------------- |
|
|
308
|
+
| `UIDU_WORKSPACE` | yes | Your workspace slug (e.g. `acme`) |
|
|
309
|
+
| `UIDU_PUBLIC_TOKEN` | no | Public read-only token, safe to expose |
|
|
310
|
+
| `UIDU_API_KEY` | no | Server-only secret for privileged ops |
|
|
229
311
|
|
|
230
312
|
## Patterns by Scope
|
|
231
313
|
|
|
232
|
-
### CMS
|
|
314
|
+
### CMS Sites (new CMS)
|
|
233
315
|
|
|
234
|
-
CMS
|
|
316
|
+
uidu has two CMS generations. **Sites** are the new one: a `Site` has page types (page-kind Models), pages (ModelItems of a page type) and, on each page, an ordered list of blocks (ModelItems of a block-kind Model). **Projects** (`getPage`/`listPages`, next section) are the legacy CMS — keep using them only for sites already deployed on a Project. Build new sites on Sites. Every read works with a `publicToken`, in RSC and in the browser.
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
// src/app/[[...slug]]/page.tsx — Server Component
|
|
320
|
+
import { getPageBySlug, getSiteByDomain, type SiteBlock } from '@uidu/client';
|
|
321
|
+
import { notFound } from 'next/navigation';
|
|
322
|
+
import { uidu } from '@/lib/uidu';
|
|
323
|
+
|
|
324
|
+
const components: Record<string, React.ComponentType<{ block: SiteBlock }>> = {
|
|
325
|
+
Hero,
|
|
326
|
+
FAQ,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export default async function Page({
|
|
330
|
+
params,
|
|
331
|
+
}: {
|
|
332
|
+
params: Promise<{ slug?: string[] }>;
|
|
333
|
+
}) {
|
|
334
|
+
const { slug = ['home'] } = await params;
|
|
335
|
+
const site = await getSiteByDomain(uidu, {
|
|
336
|
+
domain: process.env.SITE_DOMAIN!,
|
|
337
|
+
});
|
|
338
|
+
const page =
|
|
339
|
+
site &&
|
|
340
|
+
(await getPageBySlug(uidu, { siteId: site.id, slug: slug.join('/') }));
|
|
341
|
+
if (!page) notFound();
|
|
342
|
+
return page.blocks.map((block) => {
|
|
343
|
+
const Component = components[block.model.name ?? ''];
|
|
344
|
+
return Component ? <Component key={block.id} block={block} /> : null;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- `getSiteByDomain(client, { domain })` / `getSite(client, { id })` — the Site (by id in previews, where there is no domain)
|
|
350
|
+
- `getPageBySlug(client, { siteId, slug, includeDrafts? })` → `{ id, slug, model, fields, blocks: [{ id, model, fields, position }] }`, published only by default
|
|
351
|
+
- `listSitePages(client, { siteId, pageType?, includeDrafts? })` — nav/sitemaps; published only, singletons excluded; `pageType` = Model id or name
|
|
352
|
+
- `getSingletonBlock(client, { siteId, shortname })` — header/footer/nav
|
|
353
|
+
- `fields` is already a `{ shortname: value }` map — no `useFields` needed.
|
|
354
|
+
- Block kinds have a `shortname`, but `Model.shortname` isn't exposed by the API yet: key component maps on `block.model.name` for now.
|
|
355
|
+
- Don't call the legacy `listPages`/`getPage` with a Site id — they read Projects only.
|
|
356
|
+
|
|
357
|
+
### CMS Pages (legacy Projects)
|
|
358
|
+
|
|
359
|
+
Legacy CMS pages are made of `pageBlocks`. Fetch a page server-side, pass blocks to `<PageBlocks>` with a component map keyed by the block's `shortname`:
|
|
235
360
|
|
|
236
361
|
```tsx
|
|
237
362
|
// src/app/page.tsx — Server Component
|
|
@@ -249,10 +374,7 @@ export default async function HomePage() {
|
|
|
249
374
|
if (!page) return <div>Not found</div>;
|
|
250
375
|
|
|
251
376
|
return (
|
|
252
|
-
<PageBlocks
|
|
253
|
-
pageBlocks={page.pageBlocks}
|
|
254
|
-
components={{ Hero, Feature }}
|
|
255
|
-
/>
|
|
377
|
+
<PageBlocks pageBlocks={page.pageBlocks} components={{ Hero, Feature }} />
|
|
256
378
|
);
|
|
257
379
|
}
|
|
258
380
|
```
|
|
@@ -296,19 +418,19 @@ const event = await getEvent(uidu, { id });
|
|
|
296
418
|
|
|
297
419
|
Event fields:
|
|
298
420
|
|
|
299
|
-
| Field
|
|
300
|
-
|
|
301
|
-
| `event.id`
|
|
302
|
-
| `event.name`
|
|
303
|
-
| `event.body`
|
|
304
|
-
| `event.cover`
|
|
305
|
-
| `event.description`
|
|
306
|
-
| `event.instance.beginsAt`
|
|
307
|
-
| `event.instance.finishesAt` | ISO datetime of end
|
|
308
|
-
| `event.instance.id`
|
|
309
|
-
| `event.primaryAddress`
|
|
310
|
-
| `event.currentCapacity`
|
|
311
|
-
| `event.isPaidEvent`
|
|
421
|
+
| Field | Notes |
|
|
422
|
+
| --------------------------- | -------------------------------------- |
|
|
423
|
+
| `event.id` | |
|
|
424
|
+
| `event.name` | Display name |
|
|
425
|
+
| `event.body` | WYSIWYG doc — render with `<RichText>` |
|
|
426
|
+
| `event.cover` | Image URL (string) or null |
|
|
427
|
+
| `event.description` | Plain string summary |
|
|
428
|
+
| `event.instance.beginsAt` | ISO datetime of start |
|
|
429
|
+
| `event.instance.finishesAt` | ISO datetime of end |
|
|
430
|
+
| `event.instance.id` | Instance identifier |
|
|
431
|
+
| `event.primaryAddress` | Location data |
|
|
432
|
+
| `event.currentCapacity` | Numeric capacity |
|
|
433
|
+
| `event.isPaidEvent` | Boolean |
|
|
312
434
|
|
|
313
435
|
### Stories (Blog)
|
|
314
436
|
|
|
@@ -395,10 +517,12 @@ later throws. `FieldValueAttributes` exposes no `value` key — `content` is the
|
|
|
395
517
|
|
|
396
518
|
```ts
|
|
397
519
|
// ✅ Correct — every answer wrapped
|
|
398
|
-
fieldValuesAttributes: [
|
|
520
|
+
fieldValuesAttributes: [
|
|
521
|
+
{ fieldId: 'f1', content: { value: 'hello@example.com' } },
|
|
522
|
+
];
|
|
399
523
|
|
|
400
524
|
// ❌ Wrong — silently loses the answer
|
|
401
|
-
fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }]
|
|
525
|
+
fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }];
|
|
402
526
|
```
|
|
403
527
|
|
|
404
528
|
It holds for **every** field kind, not just strings — `{ value: 42 }`, `{ value: true }`,
|
|
@@ -465,7 +589,9 @@ All `body` fields on Event, Story, DonationCampaign, etc. are structured documen
|
|
|
465
589
|
```tsx
|
|
466
590
|
import { RichText } from '@uidu/react';
|
|
467
591
|
|
|
468
|
-
{
|
|
592
|
+
{
|
|
593
|
+
story.body != null && <RichText doc={story.body} />;
|
|
594
|
+
}
|
|
469
595
|
```
|
|
470
596
|
|
|
471
597
|
The prop is `doc`, not `children`. The component handles paragraphs, headings, lists, links, and inline marks.
|
|
@@ -517,12 +643,12 @@ Or pick a template directly:
|
|
|
517
643
|
npm create uidu-app@latest my-app -- -t events
|
|
518
644
|
```
|
|
519
645
|
|
|
520
|
-
| Template
|
|
521
|
-
|
|
522
|
-
| `minimal`
|
|
523
|
-
| `events`
|
|
524
|
-
| `stories`
|
|
525
|
-
| `donations`
|
|
646
|
+
| Template | What it builds |
|
|
647
|
+
| ------------ | ------------------------------------------------------------------------------------------------ |
|
|
648
|
+
| `minimal` | CMS landing page with `PageBlocks` + Hero/Feature blocks |
|
|
649
|
+
| `events` | Event listing + `/event/[id]` detail |
|
|
650
|
+
| `stories` | Blog listing + `/story/[id]` detail (magazine layout) |
|
|
651
|
+
| `donations` | Campaigns listing + `/campaign/[id]` detail with progress bars |
|
|
526
652
|
| `custom-app` | An app inside a uidu Space: `<UiduAppProvider>`, data in its own Models (`public/uidu.app.json`) |
|
|
527
653
|
|
|
528
654
|
Every template ships with Next.js 16 App Router, Tailwind v4 and an `.env.example` (the CLI
|