@uidu/skills 0.3.0 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uidu/skills",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Agent skills for the uidu SDK — installable via the open agent skills ecosystem (skills.sh).",
5
5
  "license": "MIT",
6
6
  "author": "uidu",
@@ -1,18 +1,19 @@
1
1
  ---
2
2
  name: uidu
3
- description: Use when building on the uidu platform — 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; 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: "0.3.0"
7
+ version: "0.5.0"
8
8
  ---
9
9
 
10
10
  # uidu SDK
11
11
 
12
- The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into two packages:
12
+ The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powered by the uidu platform. It is split into three packages:
13
13
 
14
14
  - **`@uidu/client`** — framework-agnostic GraphQL client. Safe for server-side use (RSC, route handlers, server actions, scripts).
15
- - **`@uidu/react`** — React hooks (`useFields`, `useQuery`, `useForm`, `useChannel`) and components (`<PageBlocks>`, `<RichText>`).
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
17
 
17
18
  `@uidu/api.js` is a legacy bridge package and is **deprecated** — use `@uidu/react` directly.
18
19
 
@@ -20,7 +21,7 @@ The uidu SDK is a TypeScript GraphQL toolkit for building websites and apps powe
20
21
 
21
22
  Use this skill when the user is:
22
23
 
23
- - Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API
24
+ - Building a Next.js / Remix / Astro / React app powered by the uidu GraphQL API — a public site, or a **custom app** that uidu shows inside a Space or workspace to signed-in members (same stack, different auth: see the rule below)
24
25
  - Fetching CMS pages, events, blog stories, donation campaigns, or help center content
25
26
  - Rendering uidu blocks with `<PageBlocks>` and a component map
26
27
  - Rendering WYSIWYG body fields with `<RichText>`
@@ -34,6 +35,7 @@ Use this skill when the user is:
34
35
  |---|---|---|
35
36
  | `@uidu/client` | Server-side queries (RSC, server actions, scripts) | Framework-agnostic |
36
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>` |
37
39
  | `@uidu/api.js` | **Do not use for new code.** | Deprecated; re-exports `@uidu/react` |
38
40
 
39
41
  Install:
@@ -76,18 +78,97 @@ bookings, calls, campaigns, kb-collections, kb-articles, channel` (read) and
76
78
  reports `does not support <verb>` when it doesn't. Config resolves **flags → env
77
79
  (`UIDU_*`) → `~/.uidu/config.json`** (written by `uidu login`).
78
80
 
79
- ## Reading vs writing — one rule
81
+ ## Who the app acts for — one rule
82
+
83
+ What an app *looks like* (landing page, blog, dashboard, booking tool) changes nothing.
84
+ What changes the code is **who the request acts for**:
80
85
 
81
86
  | Operation | Auth | Where it runs |
82
87
  |---|---|---|
83
- | **Reads** (`get*` / `list*`, `<entity> list/get`) | `publicToken` | anywhere — server, CLI, **and the browser** |
84
- | **Writes** (`create*` / `update*` / `delete*`, `<entity> create/update/delete`) | account **Bearer** (`apiKey`) | CLI and **server-side** only |
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** |
85
91
 
86
92
  The write functions live in `@uidu/client`, so anything the CLI provisions you can also do
87
93
  from a Next.js server action / route handler / RSC. **Never put the account Bearer token in a
88
94
  browser client component.** Authoring mutations return a payload with an `errors` array —
89
95
  always check it (non-empty = validation failure).
90
96
 
97
+ One app can mix the rows: public pages rendered on the server with `publicToken`, and a members'
98
+ area that only works when uidu frames it.
99
+
100
+ ### Inside uidu: the third row
101
+
102
+ uidu renders a custom app in a sandboxed iframe and hands it a short-lived session (5 minutes,
103
+ refreshed by the bridge) over `postMessage`. That session is the member's: the app can do what
104
+ that person can do, and no more. It changes five things:
105
+
106
+ 1. **The token only exists in the browser.** It arrives after the page loads, so a Server
107
+ Component or a server action never has it. Pages that show member data are `'use client'`
108
+ and read through the bridge-backed client. Fetching them in an RSC renders an empty page.
109
+ 2. **The token only works from the app's own origin.** uidu checks `Origin` against the origin
110
+ it signed the token for. Don't forward it to your own backend to call uidu from there — that
111
+ needs a server credential uidu doesn't issue yet.
112
+ 3. **Treat the token as opaque.** Never decode it. The expiry comes with it, the bridge refreshes
113
+ it, and `graphqlUrl` comes with it too: never build `https://<workspace>.uidu.org` from a slug.
114
+ 4. **Data goes in the app's own Models.** The session reaches the app's own Models, fields and
115
+ items through `node(id:)`, and the data-engine mutations. Everything else in the workspace comes
116
+ back as an error. Keep your data there rather than in a database of your own: it stays
117
+ searchable and permissioned in uidu.
118
+ `listModelItems` reads uidu's **search index**, which catches up a moment after a write:
119
+ after `createModelItem` / `deleteModelItem`, update your list from the mutation payload
120
+ instead of re-listing, or the new item is missing. And load models once per app instance
121
+ (memoize the promise): two concurrent `ensureModel` calls each create the model.
122
+ 5. **No cookies, no `localStorage` for shared state.** In a cross-origin iframe cookies are
123
+ third-party (Safari drops them). `localStorage` is this browser only. Use it only as the
124
+ fallback when the page is opened outside uidu (the bridge rejects `connect()` there).
125
+
126
+ ```tsx
127
+ 'use client'; // app/layout.tsx renders <AppProvider> around {children}
128
+ import { useEffect, useState } from 'react';
129
+ import { UiduAppProvider, useUiduApp } from '@uidu/react';
130
+ import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';
131
+ import {
132
+ createModelItem, ensureModel, listModelItems, toFieldValuesAttributes,
133
+ type ModelItem, type UiduClient,
134
+ } from '@uidu/client';
135
+
136
+ export function AppProvider({ children }: { children: React.ReactNode }) {
137
+ // https://*.uidu.org by default; add a local uidu or a custom domain
138
+ return <UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS]}>{children}</UiduAppProvider>;
139
+ }
140
+
141
+ async function loadBookings(client: UiduClient, workspaceAppId: string) {
142
+ // interim: the app creates its own model on first load (install-time manifest comes later)
143
+ const model = await ensureModel(client, {
144
+ workspaceAppId,
145
+ name: 'Booking',
146
+ fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
147
+ });
148
+ await createModelItem(client, {
149
+ input: { attributes: { modelId: model.id, fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Blu' }) } },
150
+ });
151
+ return listModelItems(client, { modelId: model.id }); // item.fieldValuesByShortname.room
152
+ }
153
+
154
+ export function Bookings() {
155
+ const app = useUiduApp(); // 'connecting' | 'ready' | 'error'
156
+ const [items, setItems] = useState<ModelItem[]>([]);
157
+
158
+ useEffect(() => {
159
+ if (app.status === 'ready') loadBookings(app.client, app.context.workspaceApp.id).then(setItems);
160
+ }, [app]);
161
+
162
+ if (app.status === 'error') return <p>Open this app from uidu ({app.error.code})</p>; // NOT_EMBEDDED, TIMEOUT…
163
+ return <ul>{items.map((item) => <li key={item.id}>{item.fieldValuesByShortname?.room}</li>)}</ul>;
164
+ }
165
+ ```
166
+
167
+ `app.context` carries `user`, `space`, `workspaceApp`, `locale` and `theme`. Outside React: `connect()` from
168
+ `@uidu/app-bridge`, then `createClient(fromBridge(bridge))` from `@uidu/client`. Start a new
169
+ one with `npm create uidu-app@latest my-app -- -t custom-app`: it declares its Models in
170
+ `public/uidu.app.json` and sets `frame-ancestors` so only uidu can frame it.
171
+
91
172
  ## Working transparently
92
173
 
93
174
  The user often can't tell what you're doing when you drive the CLI. Stay legible:
@@ -280,29 +361,103 @@ const channel = await getChannel(uidu, { id: channelId });
280
361
 
281
362
  ### Forms
282
363
 
283
- Forms are fetched server-side, then rendered with the `useForm` hook on the client:
364
+ Fetch the schema server-side, render it with `<DynamicForm>`, and submit through a
365
+ **server-side** action that calls `createFormResponse`. There is no `useForm` hook.
284
366
 
285
367
  ```tsx
286
- // Server: fetch form schema
368
+ // Server: fetch the form schema
287
369
  import { getForm } from '@uidu/client';
288
370
  const form = await getForm(uidu, { id: formId });
289
371
  ```
290
372
 
291
373
  ```tsx
292
- // Client: render with @uidu/react
374
+ // Client: render it
293
375
  'use client';
294
- import { useForm } from '@uidu/react';
376
+ import { DynamicForm } from '@uidu/react';
377
+ import { submitForm } from '@/lib/actions';
295
378
 
296
379
  export function ContactForm({ form }) {
297
- const { register, handleSubmit, onSubmit } = useForm(uidu, { form });
298
- return (
299
- <form onSubmit={handleSubmit(onSubmit)}>
300
- {/* render fields by iterating form.formQuestions.edges */}
301
- </form>
302
- );
380
+ return <DynamicForm form={form} action={submitForm} />;
381
+ }
382
+ ```
383
+
384
+ `<DynamicForm>` renders the questions itself and hands your `action` a
385
+ `DynamicFormValues`: `{ contact?: { firstName, lastName, email }, fieldValues:
386
+ Array<{ fieldId, questionId, value }>, formData }`. Return `{ ok: true }` or
387
+ `{ ok: false, errors }` and it renders the right state.
388
+
389
+ #### `content` is always `{ value: … }` — never a bare value
390
+
391
+ This is the one rule that breaks form writes. The API stores each answer in a JSON
392
+ `content` column and reads it back as `content['value']`. A flat scalar is **not**
393
+ rejected loudly: the response saves with empty typed columns, and reading that field
394
+ later throws. `FieldValueAttributes` exposes no `value` key — `content` is the only way in.
395
+
396
+ ```ts
397
+ // ✅ Correct — every answer wrapped
398
+ fieldValuesAttributes: [{ fieldId: 'f1', content: { value: 'hello@example.com' } }]
399
+
400
+ // ❌ Wrong — silently loses the answer
401
+ fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }]
402
+ ```
403
+
404
+ It holds for **every** field kind, not just strings — `{ value: 42 }`, `{ value: true }`,
405
+ `{ value: '2026-07-27' }`, `{ value: ['gid://…', 'gid://…'] }` for `multipleSelect`,
406
+ `{ value: <doc> }` for `richText`/`json`. And it holds for `fieldValuesAttributes`
407
+ **wherever** it appears — on a page block, a contact, a story, a booking — not just on
408
+ form responses.
409
+
410
+ Full submit action:
411
+
412
+ ```ts
413
+ // src/lib/actions.ts
414
+ 'use server';
415
+ import { createFormResponse } from '@uidu/client';
416
+ import type { DynamicFormValues, DynamicFormResult } from '@uidu/react';
417
+ import { uidu } from '@/lib/uidu';
418
+
419
+ export async function submitForm(
420
+ values: DynamicFormValues,
421
+ ): Promise<DynamicFormResult> {
422
+ const result = await createFormResponse(uidu, {
423
+ input: {
424
+ event: 'complete!', // omit to save as a draft
425
+ attributes: {
426
+ formId: FORM_ID,
427
+ fieldValuesAttributes: values.fieldValues.map((fv) => ({
428
+ fieldId: fv.fieldId,
429
+ content: { value: fv.value }, // ← wrapped
430
+ })),
431
+ contactAttributes: values.contact && {
432
+ email: values.contact.email,
433
+ contactableAttributes: {
434
+ kind: 'person',
435
+ firstName: values.contact.firstName,
436
+ lastName: values.contact.lastName,
437
+ },
438
+ },
439
+ },
440
+ },
441
+ });
442
+
443
+ if (result?.errors?.length) return { ok: false, errors: result.errors };
444
+ return { ok: true, meta: { id: result?.formResponse?.id } };
303
445
  }
304
446
  ```
305
447
 
448
+ Notes:
449
+
450
+ - **Never put arbitrary keys in `attributes`.** Only what `FormResponseAttributes`
451
+ declares: `formId`, `fieldValuesAttributes`, `contactAttributes`, `contactId`,
452
+ `formPageId`, `completedAt`, `responsableId`, `id`. Unknown keys fail the mutation.
453
+ - `event` lives on `input`, **not** inside `attributes`, and is a state-machine
454
+ transition (`'complete!'`, `'save_draft!'`) — not free text.
455
+ - To record a timestamp, use `completedAt` on `attributes`, not a synthetic field value.
456
+ - `createFormResponse` **returns** `{ errors, formResponse }` instead of throwing —
457
+ always check `errors` before treating a submit as successful.
458
+ - Use `updateFormResponse` for multi-step or save-as-you-type flows; pass each
459
+ answer's existing `id` in `fieldValuesAttributes` to update it rather than add a new one.
460
+
306
461
  ## Rendering WYSIWYG Content
307
462
 
308
463
  All `body` fields on Event, Story, DonationCampaign, etc. are structured documents (not strings). Render them with `<RichText>` from `@uidu/react`:
@@ -317,7 +472,7 @@ The prop is `doc`, not `children`. The component handles paragraphs, headings, l
317
472
 
318
473
  ## Provider Setup (Client-Side Hooks)
319
474
 
320
- Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `useFields`, `useForm`), wrap your app in `<UiduProvider>`:
475
+ Server-side `await getX(...)` calls do not need a provider. For React client-side hooks (`useQuery`, `useUiduClient`), wrap your app in `<UiduProvider>`. (`useFields` and `toText` are pure functions — they work without a provider, on the server too.)
321
476
 
322
477
  ```tsx
323
478
  // app/layout.tsx
@@ -368,13 +523,12 @@ npm create uidu-app@latest my-app -- -t events
368
523
  | `events` | Event listing + `/event/[id]` detail |
369
524
  | `stories` | Blog listing + `/story/[id]` detail (magazine layout) |
370
525
  | `donations` | Campaigns listing + `/campaign/[id]` detail with progress bars |
526
+ | `custom-app` | An app inside a uidu Space: `<UiduAppProvider>`, data in its own Models (`public/uidu.app.json`) |
371
527
 
372
- Every template ships with:
373
- - Next.js 16 App Router
374
- - Tailwind v4
375
- - A configured `src/lib/uidu.ts`
376
- - A server-rendered listing + dynamic detail page
377
- - `.env.example` with required vars (and the CLI offers to fill them in for you)
528
+ Every template ships with Next.js 16 App Router, Tailwind v4 and an `.env.example` (the CLI
529
+ offers to fill it in for you). The site templates add a configured `src/lib/uidu.ts` and a
530
+ server-rendered listing + dynamic detail page; `custom-app` has no token to configure and
531
+ reads everything in client components.
378
532
 
379
533
  ## See Also
380
534
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uidu",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "author": "uidu",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uidu.org",