@close-by/clay 0.2.23 → 0.2.25

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/docs/README.md CHANGED
@@ -11,6 +11,7 @@ For coding agents: the repo-root [llms.txt](../llms.txt) is a short map of these
11
11
  | Doc | Description |
12
12
  |-----|-------------|
13
13
  | [Getting started](./getting-started.md) | Install, `clay` CLI, create a page |
14
+ | [Mobile and installable apps](./mobile.md) | Phone tab bar (`mobileNav`), installable app (`pwa`, `--pwa`) |
14
15
  | [Boot: CLI vs library](./boot.md) | Production `ui.run` path, `clientDir`, `_run.ts`, reload stubs |
15
16
  | [Tailwind](./tailwind.md) | Zero-config utilities from page modules; optional tokens |
16
17
  | [Sample app](./tutorial.md) | Step-by-step tasks list, then two pages + shell |
@@ -24,6 +25,7 @@ For coding agents: the repo-root [llms.txt](../llms.txt) is a short map of these
24
25
  | [Elements](./elements.md) | All element types and props |
25
26
  | [Maps](./maps.md) | `ui.map`: MapLibre markers, routes, arcs, GeoJSON, clusters; palette theming and in-place updates |
26
27
  | [WebSocket protocol](./protocol.md) | Client ↔ server message formats |
28
+ | [Compatibility](./compatibility.md) | Public API promise, aliases, and compatibility checks |
27
29
  | [Architecture](./architecture.md) | Packages, data flow, and ownership |
28
30
  | [Examples](./examples.md) | Demo apps and patterns |
29
31
  | [Reactive `let`](./reactive-let.md) | NiceGUI-style `let` transform (opt-in `--reactive-let`) |
package/docs/api.md CHANGED
@@ -93,12 +93,14 @@ ui.run({
93
93
  | `css` | `string \| string[]` | | Extra CSS file path(s) served after the client bundle (absolute or cwd-relative). Override theme tokens without rebuilding the client. |
94
94
  | `sound` | `boolean` | | Play short synthesized interface sounds by default (`false`). See [Sound](#sound). |
95
95
  | `app` | `AppProps` | | Global dashboard shell; client keeps chrome sticky across navigate |
96
+ | `pwa` | `boolean \| ClayPwaConfig` | | Installable app: web manifest, icons, and a service worker that caches the app shell. See [Mobile and installable apps](./mobile.md#installable-app-pwa) |
96
97
  | `authSecret` | `string` | | Enables signed auth cookies (`POST`/`DELETE /auth/session`), `ui.establishAuthSession` / `ui.clearAuthSession`, and default `resolveUserId` from the cookie |
97
98
  | `authCookieMaxAgeSec` | `number` | `43200` (12h) | Auth cookie Max-Age |
98
99
  | `sessionIdleMs` | `number` | | Optional: clear auth + redirect after this idle time |
99
100
  | `sessionAbsoluteMs` | `number` | | Optional: clear auth + redirect after this time since hello |
100
101
  | `sessionExpiredPath` | `string` | `'/'` | SPA path after idle/absolute expiry |
101
102
  | `resolveUserId` | `(ctx) => …` | cookie helper when `authSecret` set | Trusted identity on WebSocket hello |
103
+ | `getRoles` | `() => string \| string[] \| null` | | Current user's roles. Enables `ui.forRoles`, `ui.hasRole`, `.roles()`, and default filtering in `ui.navFromPages()` / `requireRole` — see [Production — role-based content](./production.md#role-based-content) |
102
104
  | `routes` | `Record<path, (req, ctx) => Response>` | | Extra HTTP routes, exact path, any method. `ctx.signIn(userId, location)` / `ctx.signOut(location)` redirect and set/clear the auth cookie. Used for OAuth callbacks — see [Production — single sign-on](./production.md#single-sign-on-oidc) |
103
105
 
104
106
  #### Auth cookie recipe
@@ -1816,10 +1818,12 @@ onClick: async () => {
1816
1818
  | `ui.updateUrlSearch(patch, opts?)` | `void` | Merge query keys; `null`/`''` deletes |
1817
1819
  | `ui.urlState(defaults, opts?)` | reactive object | `ui.state` + hydrate/sync query string (prefer this) |
1818
1820
  | `ui.openExternal(url)` | `void` | `window.open` in a new tab (`noopener`) |
1821
+ | `ui.forRoles(roles, build, fallback?)` | `void` | Run `build` only for users with one of `roles`, else `fallback`; skipped content never reaches the browser |
1822
+ | `ui.hasRole(roles)` | `boolean` | Current user has one of `roles` |
1819
1823
  | `ui.redirect(url)` | `void` | Full-page navigation in the current tab (`location.assign`) — for sign-in routes / external pages; use `ui.navigate` for app pages |
1820
1824
  | `ui.theme.set(mode)` | `void` | Push `light` \| `dark` \| `system` to this client |
1821
1825
  | `ui.theme.get()` | `ThemeMode \| null` | Last value set on this session |
1822
- | `ui.theme.palette(name)` | `void` | Switch colour palette for this client (`kiln`, `cobalt`, `ocean`, `forest`, `orchid`, `citrus`, `graphite`, `neon`, `sky`, `navy`, `indigo`, `denim`, `midnight`, or a custom one); remembered across reloads |
1826
+ | `ui.theme.palette(name)` | `void` | Switch colour palette for this client (`kiln`, `cobalt`, `ocean`, `forest`, `orchid`, `citrus`, `graphite`, `chalk`, `brutal`, `terminal`, `editorial`, `bubble`, `soothe`, `nebula`, `studio`, `iris`, `ledger`, `contrast`, `dim`, `neon`, `sky`, `navy`, `indigo`, `denim`, `midnight`, or a custom one); remembered across reloads |
1823
1827
  | `ui.theme.getPalette()` | `ThemePalette \| null` | Last palette set, else the one the client reported on connect |
1824
1828
  | `ui.theme.palettes` | `readonly string[]` | Built-in palette names |
1825
1829
  | `ui.sound.set(on)` | `void` | Turn automatic interface sounds on/off for this client; remembered across reloads. See [Sound](#sound) |
@@ -1957,6 +1961,7 @@ Sidebar layout for multi-page apps. Prefer `ui.run({ app })` so every page inher
1957
1961
  | `user` | `AppUser` | Optional user menu (`name`, `email`, `avatar?`) |
1958
1962
  | `variant` | `'sidebar' \| 'inset'` | Shell layout variant |
1959
1963
  | `collapsible` | `'offcanvas' \| 'icon' \| 'none'` | Sidebar collapse behavior |
1964
+ | `mobileNav` | `'tabs' \| 'menu'` | Phone navigation. Default `'tabs'` (bottom tab bar); `'menu'` keeps only the sidebar sheet. See [Mobile](./mobile.md#keep-only-the-menu) |
1960
1965
  | `className` | `string` | Extra classes on the shell |
1961
1966
 
1962
1967
  `AppNavItem`: `{ label, href, icon?, description?, items? }` — `icon` is a Lucide kebab-case name (full set; e.g. `home`, `gauge`, `sparkles`). Nested `items` render as collapsible sub-links in the sidebar.
@@ -2057,6 +2062,7 @@ Returned by every `ui.*` factory.
2057
2062
  | Method | Description |
2058
2063
  |--------|-------------|
2059
2064
  | `.classes(...names)` | Append Tailwind/utility classes; patches `className` |
2065
+ | `.roles(list)` | Keep only for users with one of the roles (`ui.run({ getRoles })`); otherwise removed before it reaches the client. Call while building |
2060
2066
  | `.style(string \| Record)` | Set inline style string / object |
2061
2067
  | `.on(event, handler)` | Register additional event handler |
2062
2068
  | `.setText(text)` | Set `props.text` and patch |
@@ -2110,7 +2116,7 @@ form.name = 'Ada'; // persisted
2110
2116
  ui.draft.clear('formDemo'); // drop storage key; reset the object yourself if needed
2111
2117
  ```
2112
2118
 
2113
- See Form Demo (`/examples/form-demo`).
2119
+ See Forms (`/examples/forms`).
2114
2120
 
2115
2121
  ### `ui.auto(fn)`
2116
2122
 
@@ -0,0 +1,79 @@
1
+ # Compatibility
2
+
3
+ An app written for a Clay release keeps working on later releases unchanged.
4
+ If an app breaks after a Clay update, that is a bug in Clay unless the release
5
+ notes list the change under **Breaking**.
6
+
7
+ ## What this covers
8
+
9
+ The promise covers the public surface apps use:
10
+
11
+ - `ui.*` functions and namespaces, including `ui.ai`, `ui.storage`, and `ui.theme`.
12
+ - Element methods such as `setText`, `classes`, and `bindValue`.
13
+ - Package exports, including exports moved to another module.
14
+ - Factory option names and element prop names.
15
+ - Event names and their payloads.
16
+ - Element wire types and protocol operations.
17
+ - CLI flags, defaults, and behavior.
18
+
19
+ The client and server ship together, but the wire format still matters.
20
+ Apps use `ui.runJavaScript` and custom clients, and old browser tabs reconnect
21
+ to new servers. Updating the server must account for those callers too.
22
+
23
+ ## Rules for changes
24
+
25
+ 1. What a release shipped stays available in later releases, unless its removal
26
+ is explicitly listed under Breaking in the release notes.
27
+ 2. A rename keeps the old name as an alias of the same implementation.
28
+ Mark the old name with JSDoc `@deprecated Use newName.` so editors can guide
29
+ callers to the replacement. Moving an export keeps it in the old module too.
30
+ 3. Types only widen: options gain optional members, unions gain cases, and
31
+ functions gain optional trailing parameters or overloads. Apps should handle
32
+ values they do not know, for example with a default branch in a switch.
33
+ 4. Behavior does not change silently. Defaults, units, payload shapes, and
34
+ callback order are part of the contract even when the names stay the same.
35
+
36
+ For example, a renamed helper keeps a direct alias:
37
+
38
+ ```typescript
39
+ export function newName(text: string) {
40
+ return ui.label(text);
41
+ }
42
+
43
+ /** @deprecated Use newName. */
44
+ export const oldName = newName;
45
+ ```
46
+
47
+ ## How we check it
48
+
49
+ [compat/surface.json](../compat/surface.json) records the public names.
50
+ [packages/ui/src/compat.test.ts](../packages/ui/src/compat.test.ts) compares that
51
+ record with the current source as part of `bun run test`.
52
+ Missing names fail the test; new names also fail until the record is updated.
53
+
54
+ The record contains:
55
+
56
+ - `ui.<path>`: functions and plain-object namespaces, up to three levels deep.
57
+ Session-created values and getters requiring a session are excluded.
58
+ - `export:<package-name>:<name>`: runtime exports from Clay, core, components,
59
+ server, auth, and file persistence. Type-only exports are absent at runtime.
60
+ - `element.<method>`: methods on the core Element prototype chain.
61
+ - `class.<ExportName>.<method>`: own methods of other Element classes exported
62
+ by `@close-by/clay`.
63
+ - `wire.<type>`: element types handled by the client renderer.
64
+ - `op.server.<op>`, `op.client.<op>`, and `op.patch.<op>`: protocol operations.
65
+ - `cli.<flag>`: CLI flags, including short aliases.
66
+
67
+ After adding public API, run `bun scripts/compat-surface.ts --write` and commit
68
+ `compat/surface.json` with the implementation. Review its diff before committing.
69
+ For a rename, restore the old alias before updating the record.
70
+ For an intentional removal, list it under Breaking in the release notes and
71
+ regenerate the record. Run `bun test packages/ui` to check it.
72
+ Run `bun scripts/compat-surface.ts` for counts without changing the record.
73
+
74
+ This check records names, not full signatures or behavior. Option and prop names,
75
+ event payload shapes, defaults, units, and callback order are not checked
76
+ automatically yet. Reviewers check these, along with type compatibility.
77
+ Native and network adapter packages are outside the mechanical record.
78
+
79
+ Practice adapted from [Native Shell's compatibility policy](https://nsh.dev/docs/compatibility).
package/docs/elements.md CHANGED
@@ -201,6 +201,12 @@ Server patches still reconcile when `props.value` changes (e.g. `draft.text = ''
201
201
 
202
202
  Field `error` is **server-owned** (via initial props, `.setError`, or `ui.validate`). It is not optimistic — the client shows `props.error` after `updateProps` patches.
203
203
 
204
+ ## On phones
205
+
206
+ On phone-sized touch screens (max-width 767px and coarse pointer), `select` uses the native picker
207
+ and `date` uses the native date picker. `dropdownmenu` and `combobox` open bottom sheets.
208
+ Props and events stay the same. Desktop rendering is unchanged.
209
+
204
210
  ## Styling
205
211
 
206
212
  Prefer Tailwind utility classes via `.classes(...)`:
@@ -213,4 +219,4 @@ Layout `gap` uses a small numeric scale mapped to `gap-0` … `gap-8` in the cli
213
219
 
214
220
  The client ships the "kiln" theme — warm neutrals, a fired-clay primary, vivid chart colours, light + dark (ShadCN-style CSS variables in `packages/client/src/index.css`, Plus Jakarta Sans bundled). Override tokens at runtime with `ui.run({ css: './globals.css' })`; Clay extras include `--clay-brand-from` / `--clay-brand-to` and the `clay-brand-gradient`, `clay-brand-text`, `clay-ambient` classes.
215
221
 
216
- Twelve more palettes ship alongside kiln — `cobalt`, `ocean`, `forest`, `orchid`, `citrus`, `graphite`, `neon`, plus the blues `sky`, `navy`, `indigo`, `denim`, `midnight` (each light + dark, in `packages/client/src/palettes.css`). Pick the app default with `ui.run({ palette: 'ocean' })` or switch per session with `ui.theme.palette('neon')`. Custom palettes: define `:root[data-palette="mine"] { … }` and `:root.dark[data-palette="mine"] { … }` in your `css` file, then `ui.theme.palette('mine')`.
222
+ Twenty-four more palettes ship alongside kiln — `cobalt`, `ocean`, `forest`, `orchid`, `citrus`, `graphite`, `chalk`, `brutal`, `terminal`, `editorial`, `bubble`, `soothe`, `nebula`, `studio`, `iris`, `ledger`, `contrast`, `dim`, `neon`, plus the blues `sky`, `navy`, `indigo`, `denim`, `midnight` (each light + dark, in `packages/client/src/palettes.css`). Pick the app default with `ui.run({ palette: 'ocean' })` or switch per session with `ui.theme.palette('neon')`. `chalk`, `brutal`, `terminal`, `editorial`, `bubble`, `soothe`, `nebula`, `studio`, `iris`, `ledger`, `contrast`, and `dim` also change font, radius and shadows. `contrast` is the accessibility option, with high-contrast colours and explicit focus indicators. Custom palettes: define `:root[data-palette="mine"] { … }` and `:root.dark[data-palette="mine"] { … }` in your `css` file, then `ui.theme.palette('mine')`.
package/docs/examples.md CHANGED
@@ -7,7 +7,7 @@ After `bun run build:client && bun run demo` (or `bun run demo:cli`), open http:
7
7
 
8
8
  ## Sidebar groups
9
9
 
10
- Each page sets `group` / `groupIcon` in its `pageMeta`, so the sidebar and the home page's catalog share one structure: **Start here** (themes, fill layout, counter, todo, kitchen sink) → **Reactivity** → **Data** → **Maps** (gallery, delivery tracker, store locator, globe, uptime monitor) → **Forms & controls** → **Overlays & layout** → **Apps** → **Account** → **ReUI batches**. Order within a group is `pageMeta.order`.
10
+ Each page sets `group` / `groupIcon` in its `pageMeta`, so the sidebar and the home page's catalog share one structure: **Start here** (themes, fill layout, counter, todo, kitchen sink) → **Reactivity** → **Data** → **Maps** (map gallery, delivery tracker, store locator, globe, uptime monitor) → **Forms & controls** → **Components** → **Apps** (including List, Kanban, Gantt, Flow, and Ops console) → **Account**. Order within a group is `pageMeta.order`.
11
11
 
12
12
  ## Catalog
13
13
 
@@ -16,34 +16,30 @@ Each page sets `group` / `groupIcon` in its `pageMeta`, so the sidebar and the h
16
16
  | `/` | `Home.ts` | Landing page: hero, live counter + source side by side (`ui.codeBlock`), feature grid, catalog grouped via `ui.navFromPages()` (same as sidebar) |
17
17
  | `/examples/counter` | `Counter.ts` | `setText`, `refreshable`, button variants |
18
18
  | `/examples/todo` | `Todo.ts` | `reactive`, `bindValue`, list `refreshable`, filters |
19
+ | `/examples/reactive-let-orders` | `ReactiveLetOrders.ts` | Order filters and master–detail views with `ui.state` / `ui.auto` |
20
+ | `/examples/reactive-let-orders-let` | `ReactiveLetOrdersLet.ts` | The same order workflow with reactive `let` variables and the Clay CLI transform |
19
21
  | `/examples/chat` | `Chat.ts` | `ui.storage.app` (persisted messages + ephemeral presence), async `get`/`set` |
20
22
  | `/examples/ai` | `AiDemo.ts` | `ui.ai.*` gallery — chat, prompt bar, loader/thinking, agent cards, diff/insights/fine-tune (static props; no model runtime) |
21
23
  | `/examples/upload` | `FileUpload.ts` | `ui.upload` button + dropzone (progress/abort/size), `ui.storage.tab` / `user`, `ui.download`, `ui.clipboard` |
22
24
  | `/examples/dashboard` | `Dashboard.ts` | `stat`, `areaChart`, full-chrome `dataTable` (views, editors, detail drawer) |
23
25
  | `/examples/datatable` | `DataTableDemo.ts` | Multi-sort (Shift+click / badges), footer aggregates + a11y hints, column pin, remote filter/sort + pagination; density/zebra, editors (Enter/Esc + focus restore), resize, virtualization (≥40, coexists with reorder via windowed drop targets); facets, bulk, collapse/expand, loading/empty; `ui.table` sugar |
24
- | `/examples/composed` | `ComposedDemo.ts` | ReUI batch 1: `ui.timeline`, `ui.stepper`, `ui.dateRange`, `ui.feedList`/`ui.feedRow`, `ui.sparkline` |
25
- | `/examples/ops-console` | `OpsConsoleDemo.ts` | Dogfood ops console: `filterBar` → `feedList` → `descriptionList` / `staticTable` (batches 1–7 in one surface) |
26
- | `/examples/reui-batches` | `ReuiBatchesDemo.ts` | ReUI batches 3–6: `buttonGroup`, `empty`, `pagination`, `filterBar`, form inputs, `navigationMenu`, `notice`, `stat`+sparkline, `eventCalendar`, datatable `showFilterChips` |
27
- | `/examples/reui-batch-7` | `ReuiBatch7Demo.ts` | ReUI batch 7: `inputGroup`, `toggle`, `descriptionList`, `staticTable`, `aspectRatio`, `itemList`, `checkboxGroup` |
28
- | `/examples/charts` | `ChartDemo.ts` | `ui.chart.*`, `ui.sparkline`, composed/scatter charts |
26
+ | `/examples/ops-console` | `OpsConsoleDemo.ts` | Order triage: `filterBar` → `feedList` → `descriptionList` / `staticTable` |
27
+ | `/examples/charts` | `ChartDemo.ts` | `ui.chart.*`, `ui.sparkline`, composed/scatter charts, `stat` + sparkline |
28
+ | `/examples/live-table` | `LiveTableStress.ts` | High-frequency table updates, configurable row count, and live price changes |
29
29
  | `/examples/live-charts` | `LiveCharts.ts` | Live charts: `appendData` sliding windows, `setData` tweens, `upsertData` buckets, live gauge and sparklines |
30
- | `/examples/slider-demo` | `SliderDemo.ts` | Slider, checkbox, select + bindings |
31
30
  | `/examples/themes` | `ThemesDemo.ts` | Palette gallery: `ui.theme.palette`, light/dark, component preview |
32
31
  | `/examples/layout` | `LayoutDemo.ts` | Fill layout: `fillViewport`, `grow`, `scroll`, `ui.grid` |
33
- | `/examples/feedback` | `FeedbackDemo.ts` | Alerts, progress, timer, `ui.theme`, `ui.runJavaScript` / `ui.scroll` |
34
- | `/examples/form-demo` | `FormDemo.ts` | Form + validate + `ui.draft` + rating / colorPicker / tags |
32
+ | `/examples/feedback` | `FeedbackDemo.ts` | Feedback & status: alert, spinner, progress, `runJavaScript` / scroll, live timer clock, notice, empty, timeline |
33
+ | `/examples/forms` | `FormDemo.ts` | Forms: validation, `ui.draft`, slider, checkbox, select, rating, color picker, tags, field, numberField, phoneInput, nativeSelect, inputGroup, toggle, checkboxGroup, dateRange, stepper |
35
34
  | `/examples/auth` | `Auth.ts` (+ login/admin/change-password) | Signed cookie session, hashed passwords, login lockout, role-gated admin + audit log |
36
- | `/examples/timer-content` | `TimerContent.ts` | `ui.timer`, markdown, html, image |
37
- | `/examples/overlays` | `OverlaysDemo.ts` | Breadcrumb, dropdown/context menu, hover card, popover, dialog/sheet/drawer |
38
- | `/examples/dialog-stack` | `DialogStackDemo.ts` | `ui.dialogStack` multi-step stacked modal |
39
- | `/examples/controls` | `ControlsDemo.ts` | Menubar (submenu/checkbox/radio), command dialog + inline, carousel, resizable, scroll-area, `ui.state` / `ui.auto` |
40
- | `/examples/controls-extra` | `ControlsExtra.ts` | `ui.codeBlock` (Shiki), `ui.tree` |
41
- | `/examples/media-extra` | `MediaExtra.ts` | `ui.relativeTime`, `ui.qrCode`, `ui.imageZoom`, `ui.imageCrop` |
35
+ | `/examples/overlays` | `OverlaysDemo.ts` | Breadcrumb, dropdown/context menu, hover card, popover, dialog/sheet/drawer, `dialogStack` multi-step stacked modal |
36
+ | `/examples/navigation` | `NavigationDemo.ts` | Navigation & layout: menubar, command, carousel, resizable, scrollArea, keybind, viewportEnter, navigationMenu, buttonGroup, pagination, aspectRatio |
37
+ | `/examples/content` | `ContentDemo.ts` | Content & media: markdown, html, codeBlock, descriptionList, staticTable, itemList, tree, image, imageZoom, imageCrop, qrCode, iframe, relativeTime, eventCalendar |
42
38
  | `/examples/editor` | `EditorDemo.ts` | `ui.editor` HTML + Markdown side-by-side, bindValue |
43
39
  | `/examples/kanban` | `KanbanDemo.ts` | `ui.kanban` swimlanes, card detail drawer, element-owned `cardMove`/`cardSelect`, reset via APIs |
44
- | `/examples/list` | `ListDemo.ts` | `ui.list` element-owned groups, `itemMove` settle, reset via `setGroups` |
40
+ | `/examples/list` | `ListDemo.ts` | Apps: `ui.list` groups, drag to reorder, reset via `setGroups` |
45
41
  | `/examples/gantt` | `GanttDemo.ts` | `ui.gantt` owned rows/dates/markers/deps, cross-row drag, header double-click markers, `itemMove` / `markerAdd` settle |
46
- | `/examples/flow` | `FlowDemo.ts` | Four `ui.flow` demos: ETL with in-node controls + labeled edges + dagre auto-layout, multi-handle approval branches, fan-in/out + `addNode` dynamic stages, group/`parentId` nesting (flow-owned edges/positions; no outer diagram `ui.auto`) |
42
+ | `/examples/flow` | `FlowDemo.ts` | Four `ui.flow` demos: ETL with in-node controls + labeled edges + dagre auto-layout, multi-handle approval branches, fan-in/out + `addNode` dynamic stages, group/`parentId` nesting with interactive edges and positions |
47
43
  | `/examples/data-clients` | `DataClientsDemo.ts` | DuckDB / Kibana / ClickHouse integration story (mock-friendly) |
48
44
  | `/examples/maps` | `MapsGallery.ts` | `ui.map` gallery: markers and popups, routes, arcs, choropleth, clusters, compact analytics map |
49
45
  | `/examples/delivery-tracker` | `DeliveryTracker.ts` | `ui.map` route progress and courier marker: `updateRoute` / `upsertMarker` on a timer, `fill` layout |
@@ -149,6 +149,7 @@ bun run clay ./pages --app --title "My App"
149
149
  | `-p, --port` | Port (default `3000`) |
150
150
  | `-t, --title` | HTML / shell title |
151
151
  | `--app` | Dashboard shell + nav from discovered pages |
152
+ | `--pwa` | Installable app: manifest + service worker ([Mobile and installable apps](./mobile.md)) |
152
153
  | `--no-open` | Do not open the browser |
153
154
  | `--reload` | Restart on file changes (`bun --watch`); opens the browser once; prints `↻ clay: reloading…` and re-imports pages (clears `require.cache`) |
154
155
  | `--reactive-let` | Enable compile-time reactive `let` Bun loader (**off** by default; still needs `// @clay-reactive` or `"use reactive";` in source) |
@@ -181,7 +182,7 @@ Open:
181
182
  - http://localhost:4000 — home with links to examples
182
183
  - http://localhost:4000/examples/counter
183
184
  - http://localhost:4000/examples/todo
184
- - http://localhost:4000/examples/form-demo
185
+ - http://localhost:4000/examples/forms
185
186
  - http://localhost:4000/examples/auth
186
187
 
187
188
  | Script | What it does |
package/docs/llms.txt CHANGED
@@ -11,6 +11,8 @@ Notes for agents:
11
11
  - Update existing nodes with `setText` / `setValue` / bindings. Prefer `ui.state` + `ui.auto` (+ `ui.timer`) for async/live UIs; `ui.refreshable` for simple one-shot rebuilds. Shared or persisted state: `ui.storage` (`tab` / `user` / `app`).
12
12
  - Never use `window` / `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.getUrlSearch` / `ui.updateUrlSearch`, `ui.openExternal`.
13
13
  - Runtime is Bun. In this repo: `bun run build:client`, then `bun run demo` (http://localhost:4000) or `bun run clay hello.ts`.
14
+ - Installable app is opt-in: `ui.run({ pwa: true })` or `clay ./pages --app --pwa` adds a manifest and an app-shell service worker (see docs/mobile.md).
15
+ - On phones the `app` nav becomes a bottom tab bar; `app: { mobileNav: 'menu' }` keeps only the sidebar sheet.
14
16
  - `ui.ai.*` is visual primitives only — no model or streaming runtime.
15
17
  - Agent skill (Cursor / compatible tools): `@close-by/clay/SKILL.md` — copy to `.cursor/skills/clay/SKILL.md` or read from `node_modules` when installed.
16
18
 
@@ -20,6 +22,7 @@ Notes for agents:
20
22
  - [Getting started](docs/getting-started.md): Install, `clay` CLI, first app, publishing
21
23
  - [Boot](docs/boot.md): CLI vs library, `clientDir`, `_run.ts`, reload stubs
22
24
  - [Tailwind](docs/tailwind.md): Scanned utilities + `ui.run({ css })`
25
+ - [Mobile and installable apps](docs/mobile.md): Phone tab bar (`mobileNav`), installable PWA (`ui.run({ pwa })`, `--pwa`)
23
26
  - [Sample app](docs/tutorial.md): Step-by-step tasks list, then two pages + shell
24
27
  - [Concepts](docs/concepts.md): Sessions, element tree, patches, refreshable, bindings, storage
25
28
  - [Browser APIs](docs/browser-apis.md): `ui.clipboard` / hash / `openExternal` — no `window` in page code
@@ -37,6 +40,7 @@ Notes for agents:
37
40
 
38
41
  - [Architecture](docs/architecture.md): Packages and data flow
39
42
  - [WebSocket protocol](docs/protocol.md): Message formats
43
+ - [Compatibility](docs/compatibility.md): Public API promise, aliases, and compatibility checks
40
44
  - [AI UI](docs/ai.md): `ui.ai.*` visual primitives
41
45
  - [Reactive let](docs/reactive-let.md): `ui.state` / `ui.auto` and the compile-time `let` transform
42
46
  - [DuckDB](docs/duckdb.md): `@close-by/clay-duckdb`
package/docs/mobile.md ADDED
@@ -0,0 +1,111 @@
1
+ # Mobile and installable apps
2
+
3
+ Apps built with the `app` shell work on phones without changes. You can also make an app installable to the home screen, which is opt-in.
4
+
5
+ ## Phone layout
6
+
7
+ On phone-sized screens (narrow, with a touch pointer) the shell changes on its own:
8
+
9
+ - **Bottom tab bar.** Up to 4 top-level `nav` items become tabs. When there are more items, or `navSecondary`, `documents` or `user` is set, a **More** tab opens the sidebar sheet. A tab is highlighted when its page, or one of its child pages, is active.
10
+ - **Bottom sheets.** Dialogs open as bottom sheets.
11
+ - **Safe areas.** Content clears the iPhone notch and home indicator.
12
+ - **Toasts and the connection chip** sit above the tab bar.
13
+ - **16px form text.** iOS doesn't zoom when a field is focused.
14
+
15
+ `select`, `combobox`, `date` and `dropdownMenu` also use native pickers or bottom drawers on phones. See [Elements](./elements.md#on-phones).
16
+
17
+ ### Keep only the menu
18
+
19
+ Set `mobileNav: 'menu'` to keep the hamburger sheet and skip the tab bar:
20
+
21
+ ```typescript
22
+ ui.run({
23
+ app: {
24
+ title: 'My App',
25
+ nav: ui.navFromPages(),
26
+ mobileNav: 'menu', // default: 'tabs'
27
+ },
28
+ });
29
+ ```
30
+
31
+ `mobileNav` is also a [`ui.app()`](./api.md#uiappprops-fn) prop.
32
+
33
+ ## Installable app (PWA)
34
+
35
+ Opt in with the CLI flag or the `pwa` option:
36
+
37
+ ```bash
38
+ clay ./pages --app --pwa
39
+ ```
40
+
41
+ ```typescript
42
+ ui.run({ pwa: true }); // defaults
43
+
44
+ ui.run({
45
+ title: 'My App',
46
+ pwa: {
47
+ shortName: 'App',
48
+ description: 'Track my work',
49
+ themeColor: '#1d4ed8',
50
+ backgroundColor: '#ffffff',
51
+ display: 'standalone',
52
+ startUrl: '/',
53
+ icon: './assets/icon-512.png',
54
+ },
55
+ });
56
+ ```
57
+
58
+ `ClayPwaConfig` is exported from `@close-by/clay-server`.
59
+
60
+ | Option | Default | Description |
61
+ |--------|---------|-------------|
62
+ | `name` | `title` | App name in the install prompt and splash screen |
63
+ | `shortName` | `name` | Label under the home-screen icon |
64
+ | `description` | | Manifest description |
65
+ | `themeColor` | Clay light/dark background | Browser and OS chrome color |
66
+ | `backgroundColor` | Clay light background | Splash screen background |
67
+ | `display` | `'standalone'` | `'standalone' \| 'fullscreen' \| 'minimal-ui' \| 'browser'` |
68
+ | `startUrl` | `'/'` | URL opened from the home screen |
69
+ | `icon` | Clay's bundled icons | Square PNG (512px+ recommended) or SVG, relative to the working directory |
70
+
71
+ It adds:
72
+
73
+ - `/manifest.webmanifest`, with name, icons, colors and display mode.
74
+ - `theme-color` and Apple meta tags, plus `viewport-fit=cover` so content can use the full screen.
75
+ - `/sw.js`, a service worker that caches the app shell (HTML, JS, CSS and icons). It uses network-first, so online users always get the latest build.
76
+
77
+ ### Icons
78
+
79
+ - Without `icon`, Clay's bundled icons are used.
80
+ - `icon` must be a `.png` or `.svg` file. A missing or unsupported file throws at startup.
81
+ - iOS home-screen icons need a PNG. SVG gets no apple-touch-icon, so use a PNG if the app will be installed on iOS.
82
+
83
+ ### Offline
84
+
85
+ The installed app still opens without a connection and shows Clay's reconnecting state. There is no offline mode for pages, since every screen runs on the server.
86
+
87
+ ### HTTPS
88
+
89
+ Service workers require HTTPS. `localhost` is exempt, so you can test locally. Serve the app over HTTPS before asking people to install it.
90
+
91
+ ### Install
92
+
93
+ - **Android and desktop Chrome:** an install prompt or menu item appears when the app is installable.
94
+ - **iOS Safari:** Share → Add to Home Screen.
95
+
96
+ ### Turn it off
97
+
98
+ Remove `pwa` (or the `--pwa` flag). Clay then serves a `/sw.js` that clears the app cache and unregisters itself. Browsers that installed the app earlier clean up the next time they load it.
99
+
100
+ ### Override the worker or manifest
101
+
102
+ `/sw.js` and `/manifest.webmanifest` can be replaced with `routes`, which match by exact path before pages:
103
+
104
+ ```typescript
105
+ ui.run({
106
+ pwa: true,
107
+ routes: {
108
+ '/sw.js': () => new Response(mySource, { headers: { 'Content-Type': 'text/javascript' } }),
109
+ },
110
+ });
111
+ ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  Dense internal tools (live feeds, master–detail, logs/traces) push Clay past card dashboards. Prefer Phase 1 reactivity ([Concepts](./concepts.md#canonical-recipe-state--auto--timer)) and the helpers below.
4
4
 
5
- **Runnable dogfood:** `/examples/ops-console` (`OpsConsoleDemo.ts`) — one Orders-shaped surface using `filterBar`, `inputGroup`, `feedList`/`feedRow`, `descriptionList`, `staticTable`, `pagination`, `empty`, `notice`, and `toggle`. Use that page to find API friction before adding more wire types.
5
+ **Runnable example:** `/examples/ops-console` (`OpsConsoleDemo.ts`) — one Orders-shaped surface using `filterBar`, `inputGroup`, `feedList`/`feedRow`, `descriptionList`, `staticTable`, `pagination`, `empty`, `notice`, and `toggle`. Use that page to find API friction before adding more wire types.
6
6
 
7
7
  ## Status chips
8
8
 
@@ -65,6 +65,35 @@ ui.page('/admin', () => {
65
65
 
66
66
  Nav filtering is not authorization. Always guard pages that must not leak data.
67
67
 
68
+ ### Role-based content
69
+
70
+ Tell Clay how to read the current user's roles once, then show or hide parts of a page by role:
71
+
72
+ ```ts
73
+ ui.run({
74
+ authSecret,
75
+ getRoles: () => getSessionUser()?.roles ?? [], // string, string[] or null
76
+ });
77
+
78
+ ui.page('/projects', () => {
79
+ if (!requireAuth()) return;
80
+
81
+ ui.forRoles(['admin', 'manager'], () => {
82
+ ui.button('Delete project', { onClick: deleteProject });
83
+ ui.dataTable(auditRows);
84
+ }, () => ui.label('Ask an admin for access')); // optional fallback
85
+
86
+ ui.button('Approve', { onClick: approve }).roles(['admin']);
87
+ if (ui.hasRole('admin')) { /* plain check */ }
88
+ });
89
+ ```
90
+
91
+ - Checks run on the server while the page builds. Hidden content is never sent to the browser, and its handlers don't exist, so they can't be triggered.
92
+ - With `getRoles` set, `ui.navFromPages()` (no arguments) and `requireRole(...)` use the same roles — no need to pass them separately.
93
+ - Roles are read when the page builds. Signing in or out reconnects and rebuilds, so the UI follows. A role changed while someone is signed in applies on their next page load.
94
+ - Without `getRoles`, role checks treat the user as having no roles (fail closed) and log a warning once.
95
+ - Still check the role inside handlers that change data — cheap protection against mistakes elsewhere.
96
+
68
97
  ## Single sign-on (OIDC)
69
98
 
70
99
  `@close-by/clay-auth` signs users in with any OpenID Connect provider (authorization code + PKCE). The provider is only used to sign in: once its ID token is verified, Clay sets its usual signed auth cookie, so everything above (`resolveUserId`, guards, idle expiry) works unchanged and there are no provider tokens to refresh.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@close-by/clay",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "NiceGUI-like ui.* facade for Clay apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@close-by/clay-compiler": "0.2.23",
41
- "@close-by/clay-core": "0.2.23",
42
- "@close-by/clay-components": "0.2.23",
43
- "@close-by/clay-server": "0.2.23",
40
+ "@close-by/clay-compiler": "0.2.25",
41
+ "@close-by/clay-core": "0.2.25",
42
+ "@close-by/clay-components": "0.2.25",
43
+ "@close-by/clay-server": "0.2.25",
44
44
  "@tailwindcss/cli": "^4.3.3",
45
45
  "tailwindcss": "^4.3.3"
46
46
  }
package/src/index.ts CHANGED
@@ -465,6 +465,10 @@ import {
465
465
  updateUrlSearch as updateUrlSearchCore,
466
466
  openExternal as openExternalCore,
467
467
  redirect as redirectCore,
468
+ setRoleResolver,
469
+ hasRole as hasRoleCore,
470
+ forRoles as forRolesCore,
471
+ type RoleResolver,
468
472
  scroll as scrollCore,
469
473
  timer as timerCore,
470
474
  storage as storageCore,
@@ -1690,6 +1694,30 @@ export function redirect(url: string): void {
1690
1694
  redirectCore(url);
1691
1695
  }
1692
1696
 
1697
+ /** True when the current user has at least one of `roles` (needs `ui.run({ getRoles })`). */
1698
+ export function hasRole(roles: string | readonly string[]): boolean {
1699
+ return hasRoleCore(roles);
1700
+ }
1701
+
1702
+ /**
1703
+ * Build `content` only for users with one of `roles`, else `fallback`.
1704
+ * Skipped content is never sent to the browser. Needs `ui.run({ getRoles })`.
1705
+ *
1706
+ * @example
1707
+ * ```ts
1708
+ * ui.forRoles(['admin', 'manager'], () => {
1709
+ * ui.button('Delete project', { onClick: remove });
1710
+ * }, () => ui.label('Ask an admin for access'));
1711
+ * ```
1712
+ */
1713
+ export function forRoles(
1714
+ roles: string | readonly string[],
1715
+ content: () => void,
1716
+ fallback?: () => void,
1717
+ ): void {
1718
+ forRolesCore(roles, content, fallback);
1719
+ }
1720
+
1693
1721
  /** Open a URL in a new tab. Prefer over `runJavaScript('window.open…')`. */
1694
1722
  export function openExternal(url: string): void {
1695
1723
  openExternalCore(url);
@@ -1789,6 +1817,12 @@ export function page(path: string, fn: () => void, options?: PageOptions): void
1789
1817
  * Extends {@link ClayServerConfig} (port, auth, storage, uploads, session timeouts).
1790
1818
  */
1791
1819
  export type RunConfig = ClayServerConfig & {
1820
+ /**
1821
+ * Current user's role(s), read while pages build. Enables `ui.forRoles`,
1822
+ * `element.roles([...])`, `ui.hasRole`, and default role filtering in
1823
+ * `ui.navFromPages()` / `requireRole`. Return `null` / `[]` when signed out.
1824
+ */
1825
+ getRoles?: RoleResolver;
1792
1826
  /** Global dashboard shell; wraps every page unless `ui.page(..., { shell: false })`. */
1793
1827
  app?: AppProps;
1794
1828
  /**
@@ -1887,7 +1921,8 @@ export function run(rootOrConfig: PageFn | RunConfig = {}, config: RunConfig = {
1887
1921
 
1888
1922
  cfg = applyClayTailwind(cfg);
1889
1923
 
1890
- const { app: appProps, tailwind: _tailwind, ...serverConfig } = cfg;
1924
+ const { app: appProps, tailwind: _tailwind, getRoles, ...serverConfig } = cfg;
1925
+ setRoleResolver(getRoles ?? null);
1891
1926
  if (appProps) {
1892
1927
  setPageWrapper((pageFn) => app(appProps, pageFn));
1893
1928
  } else {
@@ -2035,6 +2070,8 @@ export const ui = {
2035
2070
  updateUrlSearch,
2036
2071
  openExternal,
2037
2072
  redirect,
2073
+ forRoles,
2074
+ hasRole,
2038
2075
  runJavaScript,
2039
2076
  scroll,
2040
2077
  timer,
package/src/pages.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { currentRoles, hasRoleResolver } from '@close-by/clay-core';
1
2
  import { fileURLToPath, pathToFileURL } from 'url';
2
3
  import { join } from 'path';
3
4
  import { readFile } from 'fs/promises';
@@ -249,6 +250,10 @@ export function navFromPages(opts?: NavFromPagesOptions): AppNavItem[] {
249
250
  if (opts?.roles) {
250
251
  for (const r of opts.roles) viewer.add(r);
251
252
  }
253
+ // No explicit role given → use ui.run({ getRoles }) when configured.
254
+ if (opts?.role === undefined && opts?.roles === undefined && hasRoleResolver()) {
255
+ for (const r of currentRoles()) viewer.add(r);
256
+ }
252
257
 
253
258
  const paths = getRegisteredPaths();
254
259
  const ranked: RankedNavPage[] = paths