@uidu/skills 0.3.0 → 0.5.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 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 | 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`. |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uidu/skills",
3
- "version": "0.3.0",
3
+ "version": "0.5.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`, `theme` and `accent` (the page's colour, to set as `--primary`). 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",
@@ -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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "uidu-design",
3
+ "version": "0.1.0",
4
+ "author": "uidu",
5
+ "license": "MIT",
6
+ "homepage": "https://developers.uidu.org",
7
+ "repository": "https://github.com/uidu-org/api.js/tree/main/packages/skills/skills/uidu-design"
8
+ }