@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 +2 -0
- package/docs/api.md +8 -2
- package/docs/compatibility.md +79 -0
- package/docs/elements.md +7 -1
- package/docs/examples.md +13 -17
- package/docs/getting-started.md +2 -1
- package/docs/llms.txt +4 -0
- package/docs/mobile.md +111 -0
- package/docs/ops-patterns.md +1 -1
- package/docs/production.md +29 -0
- package/package.json +5 -5
- package/src/index.ts +38 -1
- package/src/pages.ts +5 -0
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
|
|
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
|
-
|
|
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** → **
|
|
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/
|
|
25
|
-
| `/examples/
|
|
26
|
-
| `/examples/
|
|
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` |
|
|
34
|
-
| `/examples/
|
|
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/
|
|
37
|
-
| `/examples/
|
|
38
|
-
| `/examples/
|
|
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`
|
|
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
|
|
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 |
|
package/docs/getting-started.md
CHANGED
|
@@ -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/
|
|
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
|
+
```
|
package/docs/ops-patterns.md
CHANGED
|
@@ -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
|
|
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
|
|
package/docs/production.md
CHANGED
|
@@ -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.
|
|
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.
|
|
41
|
-
"@close-by/clay-core": "0.2.
|
|
42
|
-
"@close-by/clay-components": "0.2.
|
|
43
|
-
"@close-by/clay-server": "0.2.
|
|
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
|