@uidu/skills 0.4.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/README.md +4 -3
- package/package.json +1 -1
- package/skills/uidu/SKILL.md +188 -62
- package/skills/uidu-design/SKILL.md +136 -0
- package/skills/uidu-design/metadata.json +8 -0
package/README.md
CHANGED
|
@@ -14,9 +14,10 @@ Replace `claude-code` with your agent of choice (`cursor`, `copilot`, `windsurf`
|
|
|
14
14
|
|
|
15
15
|
## Available skills
|
|
16
16
|
|
|
17
|
-
| Skill
|
|
18
|
-
|
|
19
|
-
| `uidu`
|
|
17
|
+
| Skill | Description |
|
|
18
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `uidu` | Comprehensive guide to the uidu SDK — package map, env setup, fetch patterns for CMS pages, events, stories, donations, help center, and forms, plus RichText rendering and scaffolding via `create-uidu-app`. |
|
|
20
|
+
| `uidu-design` | How an app framed inside uidu (a custom app) should look: uidu's tokens and components, layout inside the iframe, states, icons. Ships inside the `custom-app` template, so the app builder's agent has it too. |
|
|
20
21
|
|
|
21
22
|
More skills will be split out from `uidu` as individual areas grow.
|
|
22
23
|
|
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
|
|
|
167
|
-
|
|
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
|
+
|
|
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
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: uidu-design
|
|
3
|
+
description: Use when building or restyling the UI of an app that runs inside uidu (a custom app framed in a Space or workspace, including one made in the uidu app builder) — layout, components, colours, typography, empty and loading states. Makes the app look like the uidu page around it instead of a patch sewn on.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: uidu
|
|
7
|
+
version: '0.1.0'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Looking like uidu
|
|
11
|
+
|
|
12
|
+
A custom app is an iframe in the middle of a uidu page. Above it sits uidu's header, with
|
|
13
|
+
the app's name and its buttons; around it, uidu's sidebar. Whatever the app draws has those
|
|
14
|
+
on every side, so the only good outcome is an app that reads as one more uidu page.
|
|
15
|
+
|
|
16
|
+
That is mostly already done for you. The template ships **uidu's own tokens**
|
|
17
|
+
(`src/app/globals.css`) and **uidu's own components** (`src/components/ui/`), generated from
|
|
18
|
+
uidu's source. The rules below are about not undoing it.
|
|
19
|
+
|
|
20
|
+
## 1. Use the kit, don't restyle it
|
|
21
|
+
|
|
22
|
+
- Build from `src/components/ui/*`: `Button`, `Input`, `Field`, `Select`, `Checkbox`,
|
|
23
|
+
`Switch`, `Textarea`, `Table`, `Tabs`, `Dialog`, `Sheet`, `DropdownMenu`, `Popover`,
|
|
24
|
+
`Tooltip`, `Badge`, `Alert`, `Empty`, `Skeleton`, `Spinner`, `Item`, `Card`, `Calendar`,
|
|
25
|
+
`Toaster` (`toast()` from `sonner`)…
|
|
26
|
+
- A `<button>`, `<input>`, `<table>` or `<select>` written by hand is almost always a
|
|
27
|
+
component you didn't import. A `<div className="rounded-lg border p-4">` is a `Card`, a
|
|
28
|
+
coloured pill is a `Badge`, a status line is an `Alert`.
|
|
29
|
+
- Don't edit the files in `src/components/ui/`: they are regenerated from uidu. Compose them,
|
|
30
|
+
and pass `className` for layout (margins, width, flex), not for colour or size.
|
|
31
|
+
- Need one that isn't there? `npx shadcn add @uidu/<name>` works where the network allows it
|
|
32
|
+
(not in the builder's sandbox). Otherwise build it from the ones you have.
|
|
33
|
+
|
|
34
|
+
## 2. Colours are tokens, never a palette
|
|
35
|
+
|
|
36
|
+
Only the semantic classes, which flip with uidu's light/dark theme:
|
|
37
|
+
|
|
38
|
+
| For | Use |
|
|
39
|
+
| -------------------------------------------------- | ------------------------------------------------------------- |
|
|
40
|
+
| page, text | `bg-background`, `text-foreground` |
|
|
41
|
+
| secondary text, captions, labels | `text-muted-foreground` |
|
|
42
|
+
| panels, popovers | `bg-card`, `bg-popover` (usually via `Card`, `Popover`) |
|
|
43
|
+
| hover, selected row | `bg-accent`, `hover:bg-accent` |
|
|
44
|
+
| subtle fill (a weekend, a disabled row) | `bg-muted` |
|
|
45
|
+
| the app's accent (main button, active item, links) | `bg-primary`, `text-primary` |
|
|
46
|
+
| error, destructive action | `text-destructive`, `bg-destructive/10` |
|
|
47
|
+
| done / needs attention / neutral info | `text-success`, `text-warning`, `text-info` (and `/10` fills) |
|
|
48
|
+
| lines | `border` (already the right colour), `divide-y` |
|
|
49
|
+
| charts | `bg-chart-1` … `bg-chart-5`, `var(--chart-1)` |
|
|
50
|
+
|
|
51
|
+
Never `text-gray-*`, `bg-white`, `text-black`, `bg-blue-500`, `text-red-600`, a hex value or
|
|
52
|
+
an inline `style={{ color }}`: they don't follow the theme, and in dark mode they break.
|
|
53
|
+
`--primary` is the page's accent: uidu sends it (`context.accent`, set by
|
|
54
|
+
`app-provider.tsx`), so it changes with the workspace and the app. Don't set it yourself,
|
|
55
|
+
and don't add a brand colour of your own — `bg-primary` already is one.
|
|
56
|
+
|
|
57
|
+
## 3. Typography: uidu's density
|
|
58
|
+
|
|
59
|
+
- Body text is `text-sm` (14px): paragraphs, table cells, list items, form fields.
|
|
60
|
+
- `text-base` only for emphasis: the figure a card is about, a record's name.
|
|
61
|
+
- `text-xs` is rare: counters, hints.
|
|
62
|
+
- Headings inside the app are `text-sm font-semibold` (a section) or `text-base
|
|
63
|
+
font-semibold` at most. No `text-2xl` hero titles: this is a tool, not a landing page.
|
|
64
|
+
- A label never outweighs its value: label `text-sm text-muted-foreground`, value
|
|
65
|
+
`font-medium`.
|
|
66
|
+
- The font is Inter, set in `layout.tsx`. Don't load another one.
|
|
67
|
+
|
|
68
|
+
## 4. Layout: fill the frame, don't frame it again
|
|
69
|
+
|
|
70
|
+
- **No title that repeats the app's name.** uidu's header already shows it. If the page needs
|
|
71
|
+
a bar, make it a toolbar: `flex h-14 shrink-0 items-center justify-between gap-2 border-b
|
|
72
|
+
px-4` with context (a count, a filter, a period) on the left and actions on the right.
|
|
73
|
+
- **No second navigation shell**: no sidebar, no top nav, no footer, no logo. For a few views
|
|
74
|
+
use `Tabs` in the toolbar; for a detail, a `Sheet` or a `Dialog`.
|
|
75
|
+
- **Edge to edge.** The page fills the iframe (`flex min-h-screen flex-col`): no centred
|
|
76
|
+
`max-w-2xl` column with a card floating in it, no outer margin. Content is inset `px-4`
|
|
77
|
+
(16px), the same as uidu's header above, so their left edges line up.
|
|
78
|
+
- Sections are separated by `border-b`, not by stacking cards with gaps. Use `Card` for a
|
|
79
|
+
real sub-surface (one card per related thing), never a card inside a card.
|
|
80
|
+
- **No dead space.** When the data is short, the empty area is filled by an `Empty` state
|
|
81
|
+
(it grows with `flex-1`) or the page ends where the data ends. Never leave the bottom
|
|
82
|
+
third of the frame blank.
|
|
83
|
+
- Spacing on Tailwind's scale (`gap-2`, `gap-3`, `p-4`, `px-4`); no `px-[13px]`.
|
|
84
|
+
|
|
85
|
+
## 5. Data
|
|
86
|
+
|
|
87
|
+
- Rows of things are a `Table` (`TableHeader`/`TableHead`/`TableBody`/`TableRow`/`TableCell`),
|
|
88
|
+
with `pl-4` on the first column and `pr-4` on the last, to align with the toolbar.
|
|
89
|
+
- A table that scrolls keeps its header visible: bound the table's own container with
|
|
90
|
+
`containerClassName="h-full overflow-y-auto"` and make the `TableHead`s `sticky top-0
|
|
91
|
+
bg-background`. Wrapping the table in your own scroller does not work.
|
|
92
|
+
- Dates and numbers through `Intl` with `context.locale` (`toLocaleString(locale)`,
|
|
93
|
+
`Intl.NumberFormat(locale, …)`), never hand-formatted.
|
|
94
|
+
- A thing that spans several days or columns is **one** element across them, not one per cell.
|
|
95
|
+
|
|
96
|
+
## 6. States
|
|
97
|
+
|
|
98
|
+
Every screen has four, and each has a shape:
|
|
99
|
+
|
|
100
|
+
| State | Shape |
|
|
101
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| connecting / loading | `Skeleton`s shaped like the content (rows, a card) — not a centred spinner, not "Loading…" text alone |
|
|
103
|
+
| empty | `Empty` with an icon (`EmptyMedia variant="icon"`), a title, one sentence and, if there is one, the action |
|
|
104
|
+
| error | `Alert variant="destructive"` near what failed, with what to do; a failed write keeps what the person typed |
|
|
105
|
+
| saving | the button stays, disabled, with a `Spinner` inside; `toast()` for a result that lands elsewhere |
|
|
106
|
+
|
|
107
|
+
## 7. Icons
|
|
108
|
+
|
|
109
|
+
- `lucide-react` only, imported by name: `import { Plus } from 'lucide-react'`.
|
|
110
|
+
- Inside a component (`Button`, `DropdownMenuItem`, `Badge`, `Alert`, `EmptyMedia`…) write
|
|
111
|
+
`<Plus />` with **no** `size-*` class: the component sizes it. Elsewhere `size-4`.
|
|
112
|
+
- Never change `strokeWidth`. Colour with `text-*` tokens.
|
|
113
|
+
- An icon-only button is `variant="ghost" size="icon"` with an `aria-label`, and the icon
|
|
114
|
+
`aria-hidden="true"`.
|
|
115
|
+
|
|
116
|
+
## 8. Accessible by default
|
|
117
|
+
|
|
118
|
+
- Every field has a label (`Field` + `FieldLabel htmlFor`), every icon-only button an
|
|
119
|
+
`aria-label`.
|
|
120
|
+
- Actions are buttons, navigation is links; nothing clickable is a `<div onClick>`.
|
|
121
|
+
- Messages that appear after an action (`Alert`, a saved state) are announced: `role="alert"`
|
|
122
|
+
for errors, `aria-live="polite"` for the rest.
|
|
123
|
+
- Motion stays small (the components' own); spatial animations get `motion-safe:`.
|
|
124
|
+
|
|
125
|
+
## 9. Words
|
|
126
|
+
|
|
127
|
+
- Short, plain, in the person's language (`context.locale`); sentence case ("New booking",
|
|
128
|
+
not "New Booking").
|
|
129
|
+
- Say what happened and what to do next, not an error code.
|
|
130
|
+
|
|
131
|
+
## Before you finish
|
|
132
|
+
|
|
133
|
+
Run `npm run check`: it type-checks and flags raw palette colours, hex values, hand-written
|
|
134
|
+
`<button>`/`<input>`/`<table>`, other icon sets and `strokeWidth`. Fix what it reports.
|
|
135
|
+
Then look at the page in dark mode too: if something is invisible or glaring there, it is
|
|
136
|
+
using a colour instead of a token.
|