@close-by/clay 0.2.7 → 0.2.9

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.
@@ -0,0 +1,103 @@
1
+ # Architecture
2
+
3
+ ## Package responsibilities
4
+
5
+ | Package | Role |
6
+ |---------|------|
7
+ | `@close-by/clay` | App-facing `ui` object: factories, `page`, `loadPages`, `navFromPages`, `run` |
8
+ | `@close-by/clay-cli` | `clay` binary: run a `.ts` file or page directory with sane defaults |
9
+ | `@close-by/clay-components` | Thin `Element` factories (no HTML strings) |
10
+ | `@close-by/clay-core` | Element tree, session, page wrapper, reactive, protocol, `storage` (`tab` / `user` / `app`) + PersistenceAdapter |
11
+ | `@close-by/clay-persistence-file` | File-backed `PersistenceAdapter` for `storage.configure` |
12
+ | `@close-by/clay-server` | Bun.serve: static SPA assets + `/ws` upgrade, `/auth/session`, session timeouts |
13
+ | `@close-by/clay-auth` | Optional helpers: password hash, login limiter, `requireAuth` / `requireRole`, `auditRecord` |
14
+ | `@close-by/clay-client` | Vite/React app: WS session hook + element → ShadCN (Sonner, BoundDataTable, …). **Private** — build output is copied into `@close-by/clay-cli` via `bun run build:client` / `pack:publishable`. |
15
+
16
+ `@close-by/clay-compiler` (old Datastar `let` transform) was removed; compile-time reactive `let` remains a Later backlog item.
17
+
18
+ ## Runtime data flow
19
+
20
+ ```
21
+ clay <file|dir> ──► import / loadPages
22
+ │
23
+ ui.run({ app? }) ──► setPageWrapper(app shell)
24
+ │
25
+ ▼
26
+ ClayServer ──HTTP──► SPA shell (index.html + assets)
27
+ ```
28
+
29
+ Or library entry:
30
+
31
+ ```
32
+ ui.loadPages(dir) ──► ui.page registrations + pageMeta
33
+ │
34
+ ui.run({ app }) ──► setPageWrapper(app shell)
35
+ │
36
+ ▼
37
+ ClayServer ──HTTP──► SPA shell (index.html + assets)
38
+ │
39
+ └──WS /ws──► ClientSession
40
+ │
41
+ ├─ mount: wrapper? → builder() → Element tree → JSON
42
+ ├─ event: element.handleEvent → handlers
43
+ └─ patch: updateProps / setChildren / …
44
+ │
45
+ ▼
46
+ React useClaySession + ElementRenderer
47
+ (notify → Sonner toast)
48
+ ```
49
+
50
+ ## Ownership boundaries
51
+
52
+ - **Server** owns truth for structure and business state.
53
+ - **Client** owns presentation and optimistic form UX.
54
+ - **Handlers never run in the browser**; only event names and values cross the wire.
55
+
56
+ ## Session lifecycle
57
+
58
+ 1. Upgrade to WebSocket; `data.session = null`
59
+ 2. On first `hello`, `new ClientSession(path, send)` → `mount()`
60
+ 3. Page path change → destroy previous session, mount new path (same socket; client sends another `hello`)
61
+ 4. Socket close → `session.destroy()`
62
+
63
+ On the client, a durable WebSocket + sticky React key for `type: 'app'` keeps sidebar/header mounted across navigate when chrome identity matches; only the inset tree remounts.
64
+
65
+ Per-tab isolation: local `let` / `reactive` state inside a page builder is not shared across tabs. Use `ui.storage.app` when you need a process-wide store. Optional `PersistenceAdapter` (via `storage.configure({ app })` or `ui.run({ appStorageDir })`) persists keys by default; `{ persist: false }` keeps a key in memory only. Use `createFilePersistence` from `@close-by/clay-persistence-file` for disk-backed JSON, or bring your own adapter. Persisted `get()` always reloads from the adapter.
66
+
67
+ ## Extending the system
68
+
69
+ ### New element type
70
+
71
+ 1. Add a factory in `packages/components/src/index.ts` (`new Element('myType', props)`).
72
+ 2. Re-export via `packages/ui/src/index.ts` on the `ui` object.
73
+ 3. Add a `case 'myType':` in `packages/client/src/ElementRenderer.tsx`.
74
+ 4. Rebuild the client (`bun run build:client`).
75
+
76
+ ### Custom server
77
+
78
+ ```typescript
79
+ import { ClayServer } from '@close-by/clay-server';
80
+ import { setPageWrapper } from '@close-by/clay-core';
81
+ import { app } from '@close-by/clay-components';
82
+ import './pages';
83
+
84
+ setPageWrapper((pageFn) =>
85
+ app({ title: 'App', nav: [{ label: 'Home', href: '/' }] }, pageFn),
86
+ );
87
+
88
+ const server = new ClayServer({ port: 4000, title: 'App' });
89
+ server.start();
90
+ ```
91
+
92
+ Or use `ui.run({ app, … })` which sets the page wrapper and starts the server.
93
+
94
+ ## Retired stack
95
+
96
+ The previous Datastar + DaisyUI + SSE signal-sync architecture is removed:
97
+
98
+ - No `/clay/stream` or `/clay/events`
99
+ - No DaisyUI CDN page template
100
+ - No signal cache / PatchBus as the primary update path
101
+ - Compiler preload (`bunfig.toml`) removed
102
+
103
+ Historical notes may remain under `iterations/`.
package/docs/boot.md ADDED
@@ -0,0 +1,103 @@
1
+ # Boot: CLI vs library
2
+
3
+ Clay has two entry styles. Prefer **one production path** (library mode) and treat the CLI as a thin dev wrapper so port, CSS, and `clientDir` do not drift.
4
+
5
+ ## Recommended production path — library mode
6
+
7
+ ```typescript
8
+ // index.ts
9
+ import { ui, resolveClayClientDir } from '@close-by/clay';
10
+ import { join } from 'path';
11
+
12
+ await ui.loadPages(new URL('./pages', import.meta.url));
13
+
14
+ ui.run({
15
+ port: Number(process.env.PORT) || 3000,
16
+ title: 'My App',
17
+ clientDir: resolveClayClientDir(),
18
+ css: join(import.meta.dir, 'src/globals.generated.css'),
19
+ app: {
20
+ title: 'My App',
21
+ nav: ui.navFromPages(),
22
+ },
23
+ });
24
+ ```
25
+
26
+ ```bash
27
+ bun ./index.ts
28
+ # Docker / ship: CMD ["bun", "./index.ts"]
29
+ ```
30
+
31
+ ### `clientDir`
32
+
33
+ | Approach | Notes |
34
+ |----------|--------|
35
+ | **Omit `clientDir`** | `ui.run` defaults via `resolveClayClientDir()` — prefers `@close-by/clay-cli/client-dist` when that package is installed |
36
+ | **`resolveClayClientDir()`** | Explicit; same resolution — use this instead of `join(..., 'node_modules/@close-by/clay-cli/client-dist')` |
37
+ | **Custom path** | Only if you ship your own built client |
38
+
39
+ `resolveClayClientDir` is exported from `@close-by/clay`. The CLI also exposes `resolveBundledClientDir` from `@close-by/clay-cli/helpers` for advanced tooling.
40
+
41
+ ## Dev path — `clay` CLI
42
+
43
+ ```bash
44
+ bunx clay ./pages --app --title "My App" --reload
45
+ ```
46
+
47
+ Optional `pages/_run.ts` merges into the same `RunConfig` the CLI would pass to `ui.run`:
48
+
49
+ ```typescript
50
+ import type { RunConfig } from '@close-by/clay';
51
+ import { resolve } from 'path';
52
+
53
+ export function configureRun(base: RunConfig): RunConfig {
54
+ return {
55
+ ...base,
56
+ port: 4200,
57
+ css: resolve(import.meta.dir, '../src/globals.generated.css'),
58
+ // clientDir already set by the CLI to shipped client-dist
59
+ };
60
+ }
61
+ ```
62
+
63
+ Keep `_run.ts` as the single place for port/CSS/auth so CLI and library mode stay aligned — library `index.ts` should mirror those values (or import a shared `createRunConfig()` helper).
64
+
65
+ ### Reload stubs
66
+
67
+ `--reload` writes stubs under **`.clay-reload/`** in the project cwd (not inside `pages/`). Add to `.gitignore`:
68
+
69
+ ```
70
+ .clay-reload
71
+ ```
72
+
73
+ Stubs are dev-only transient files (`_clay-reload-<pid>.ts`). They must not land in `pages/` or page discovery will pick them up.
74
+
75
+ ### CI and `tsc`
76
+
77
+ `bunx clay --reload` is for local dev only — do **not** pass `--reload` in CI or production starts.
78
+
79
+ If you run **`tsc --noEmit`** (or IDE typecheck) on the app repo, exclude reload artifacts explicitly — **`tsc` does not read `.gitignore`**:
80
+
81
+ ```json
82
+ {
83
+ "exclude": [".clay-reload", "**/_clay-reload-*.ts"]
84
+ }
85
+ ```
86
+
87
+ Without that exclude, a leftover stub from a crashed dev session can produce spurious TS errors until you delete `.clay-reload/`. Clay apps that only typecheck `pages/` and `src/` rarely need more; monorepos should exclude `.clay-reload` at the app root `tsconfig.json`.
88
+
89
+ Legacy `_clay-reload-*.ts` files under `pages/` (pre-0.2.5) should be deleted; modern Clay writes outside the tree.
90
+
91
+ ## Reactive-let
92
+
93
+ The CLI leaves the compile-time `let` transform **off** unless you pass `--reactive-let`. Library mode never registers the Bun loader unless you call `registerReactiveLetPlugin()` yourself. Prefer Phase 1 `ui.state` / `ui.auto` for real apps — see [reactive-let](./reactive-let.md).
94
+
95
+ ## Checklist (avoid drift)
96
+
97
+ | Setting | CLI | Library |
98
+ |---------|-----|---------|
99
+ | Port | `-p` / `_run.ts` | `ui.run({ port })` |
100
+ | CSS | `_run.ts` → `css` | `ui.run({ css })` |
101
+ | Client assets | CLI sets `clientDir` | `resolveClayClientDir()` or omit |
102
+ | Reactive-let | off by default | off unless you register the plugin |
103
+ | Nav / shell | `--app` | `app: { nav: ui.navFromPages() }` |
@@ -0,0 +1,65 @@
1
+ # Browser APIs in Clay pages
2
+
3
+ Page builders and event handlers run on the **server** (Bun), not in the browser. DOM globals such as `window`, `document`, `navigator`, and `location` are missing or wrong there — using them is a common source of silent no-ops (e.g. URL hash deep-links that never update).
4
+
5
+ ## Do
6
+
7
+ | Need | Use |
8
+ |------|-----|
9
+ | Copy text | `ui.clipboard(text)` |
10
+ | Read / set URL hash | `ui.getUrlHash()` / `ui.setUrlHash(hash)` |
11
+ | Open a URL in a new tab | `ui.openExternal(url)` |
12
+ | In-app route change | `ui.navigate(path)` |
13
+ | Scroll | `ui.scroll.to` / `ui.scroll.intoView` |
14
+ | Escape hatch | `ui.runJavaScript(code)` (trusted snippets only) |
15
+
16
+ ```ts
17
+ ui.button('Copy id', {
18
+ onClick: () => {
19
+ ui.clipboard(orderId);
20
+ ui.notify('Copied', 'success');
21
+ },
22
+ });
23
+
24
+ const focus = ui.getUrlHash(); // from last hello / setUrlHash
25
+ ui.setUrlHash(traceId);
26
+ ui.openExternal('https://signoz.example/trace/' + traceId);
27
+ ```
28
+
29
+ `getUrlHash` returns the hash **without** a leading `#`. It is hydrated from the client on each `hello` (connect, reconnect, SPA navigate) and updated when you call `setUrlHash`.
30
+
31
+ ## Don’t
32
+
33
+ ```ts
34
+ // Wrong — server has no clipboard / location
35
+ await navigator.clipboard.writeText(value);
36
+ window.location.hash = traceId;
37
+ window.open(url, '_blank');
38
+ ```
39
+
40
+ Even with `typeof window !== 'undefined'` guards, page code still runs on the server: those branches never run, so the UI looks fine while deep-links and copy do nothing.
41
+
42
+ ## TypeScript: keep DOM globals out of page folders
43
+
44
+ Clay page modules run on Bun, not in the browser. Extend **`@close-by/clay/page-tsconfig`** for folders that hold page builders so `window`, `navigator`, and friends are compile-time errors:
45
+
46
+ ```json
47
+ {
48
+ "extends": "@close-by/clay/page-tsconfig",
49
+ "include": ["pages/**/*.ts", "src/examples/**/*.ts"]
50
+ }
51
+ ```
52
+
53
+ That tsconfig omits the DOM `lib` — only `ESNext` + `bun-types`.
54
+
55
+ ## Dev warnings at load time
56
+
57
+ When the CLI or `ui.loadPages` imports a page module, Clay logs **`[clay-page]`** warnings for forbidden DOM globals (line numbers + suggested helpers). Disable with `CLAY_NO_PAGE_CHECKS=1`.
58
+
59
+ With **`clay --reactive-let`**, transformed files that import known-fragile CJS packages (e.g. `@clickhouse/client`) also emit **`[clay-reactive-let]`** warnings — the Bun loader can break CJS named-export interop.
60
+
61
+ ## Related
62
+
63
+ - Helpers table: [API — dialogs and feedback](./api.md#dialogs-and-feedback)
64
+ - Protocol: `clipboard`, `setUrlHash`, `openExternal` in [WebSocket protocol](./protocol.md)
65
+ - Phase 1 reactivity (preferred over DOM hacks): [reactive-let](./reactive-let.md)
@@ -0,0 +1,59 @@
1
+ # ClickHouse (`@close-by/clay-clickhouse`)
2
+
3
+ Multi-connection ClickHouse wrapper with the same shape as [`@close-by/clay-duckdb`](./duckdb.md): named clients, `exec` / `query` / `queryOne`, and insert/update/delete helpers. Also includes `stream` for large SELECTs.
4
+
5
+ ```typescript
6
+ import { ClickHouse } from '@close-by/clay-clickhouse';
7
+
8
+ const ch = new ClickHouse();
9
+
10
+ await ch.connect('analytics', {
11
+ url: process.env.CLICKHOUSE_URL ?? 'http://localhost:8123',
12
+ username: process.env.CLICKHOUSE_USER ?? 'default',
13
+ password: process.env.CLICKHOUSE_PASSWORD ?? '',
14
+ database: 'analytics',
15
+ });
16
+
17
+ const db = ch.db('analytics');
18
+ ```
19
+
20
+ | Method | Description |
21
+ |--------|-------------|
22
+ | `connect(name, config)` | Open HTTP client and register under `name` |
23
+ | `db(name)` | Get a named `Connection` |
24
+ | `has(name)` / `list()` | Registry helpers |
25
+ | `close(name)` / `closeAll()` | Disconnect and drop from registry |
26
+
27
+ ## Query / exec
28
+
29
+ ClickHouse query parameters use `{name:Type}` in SQL:
30
+
31
+ ```typescript
32
+ const rows = await db.query<{ id: number; name: string }>(
33
+ `SELECT id, name FROM users WHERE day = {day:String}`,
34
+ { day: '2026-07-01' },
35
+ );
36
+
37
+ const one = await db.queryOne('SELECT count() AS c FROM users');
38
+ await db.exec('CREATE TABLE IF NOT EXISTS scratch (id UInt64) ENGINE = Memory');
39
+
40
+ for await (const row of db.stream('SELECT * FROM events')) {
41
+ // large result sets
42
+ }
43
+ ```
44
+
45
+ ## CRUD
46
+
47
+ ```typescript
48
+ await db.insert('users', { id: 1, name: 'Ada' });
49
+ await db.update('users', { name: 'Ada Lovelace' }, { id: 1 });
50
+ await db.delete('users', { id: 1 });
51
+ ```
52
+
53
+ `update` / `delete` require a non-empty `where` and emit ClickHouse mutations (`ALTER TABLE … UPDATE/DELETE`).
54
+
55
+ Use `db.raw` for the underlying `@clickhouse/client` when needed.
56
+
57
+ ```typescript
58
+ await ch.closeAll();
59
+ ```
@@ -0,0 +1,269 @@
1
+ # Concepts
2
+
3
+ ## Mental model
4
+
5
+ Clay is **server-owned UI**:
6
+
7
+ 1. Your page callback runs on the server and builds an **element tree**.
8
+ 2. Each browser tab gets a **`ClientSession`** over WebSocket.
9
+ 3. The React client mounts that tree and renders ShadCN components.
10
+ 4. User events go to the server as `{ op: "event", id, type, value? }`.
11
+ 5. Server handlers mutate elements; Clay sends **patches** back.
12
+
13
+ You do not write React for app screens. You write `ui.button`, `ui.input`, `ui.refreshable`, etc.
14
+
15
+ ```
16
+ ┌─────────────┐ hello(path) ┌──────────────────┐
17
+ │ React client│ ─────────────► │ ClayServer / WS │
18
+ │ (ShadCN) │ ◄── mount ──── │ ClientSession │
19
+ │ │ ◄── patch ──── │ element tree │
20
+ │ │ ── event ────► │ page handlers │
21
+ └─────────────┘ └──────────────────┘
22
+ ```
23
+
24
+ ## Pages and sessions
25
+
26
+ ```typescript
27
+ ui.page('/examples/todo', () => {
28
+ // runs once per WebSocket session when the client connects to this path
29
+ });
30
+ ```
31
+
32
+ - Register routes with `ui.page(path, fn)` (or `page` from `@close-by/clay-core`).
33
+ - On `hello`, the server creates a session, runs the page builder, and sends `mount`.
34
+ - State in local variables (`let count = 0`) is **per session** (per tab), not shared.
35
+ - Use `ui.storage.app` for process-wide shared data (e.g. chat messages). Configure a `PersistenceAdapter` via `storage.configure({ app })` or `ui.run({ appStorageDir })` to persist stores (default on when configured; opt out with `{ persist: false }`). `await state.get()` reloads from the adapter when persisted.
36
+
37
+ ## Elements
38
+
39
+ Every `ui.*` factory returns an `Element`:
40
+
41
+ ```typescript
42
+ const label = ui.label('Hello');
43
+ label.classes('text-xl font-bold');
44
+ label.setText('Hello again');
45
+ ```
46
+
47
+ Elements:
48
+
49
+ - Have an `id`, `type`, `props`, and `children`
50
+ - Attach to the current parent (context stack inside `ui.row(() => { … })`)
51
+ - Serialize to JSON for the client (handlers stay on the server; only event *names* are sent)
52
+
53
+ ## Updates: `setText` vs `refreshable`
54
+
55
+ ### Prop updates
56
+
57
+ For changing text/value on an existing node:
58
+
59
+ ```typescript
60
+ label.setText(`Count: ${count}`);
61
+ input.setValue('');
62
+ ```
63
+
64
+ These enqueue `updateProps` patches.
65
+
66
+ ### Structural updates
67
+
68
+ When children appear/disappear or lists change, wrap the region in `ui.refreshable`:
69
+
70
+ ```typescript
71
+ const list = ui.refreshable(() => {
72
+ for (const item of items) {
73
+ ui.label(item);
74
+ }
75
+ });
76
+
77
+ // later
78
+ items.push('new');
79
+ list.refresh(); // rebuilds children and sends setChildren
80
+ ```
81
+
82
+ `refresh()` clears the refreshable’s children, re-runs the builder, and patches the client.
83
+
84
+ ## Canonical recipe: `state` + `auto` (+ `timer`)
85
+
86
+ For async loads, filters, and live panels, prefer Phase 1 APIs over `refreshable` + manual `.refresh()`:
87
+
88
+ ```typescript
89
+ const live = ui.state({
90
+ units: [] as Unit[],
91
+ loading: true,
92
+ error: null as string | null,
93
+ });
94
+
95
+ async function load() {
96
+ live.loading = true;
97
+ try {
98
+ live.units = await fetchUnits();
99
+ live.error = null;
100
+ } catch (e) {
101
+ live.error = e instanceof Error ? e.message : String(e);
102
+ } finally {
103
+ live.loading = false;
104
+ }
105
+ }
106
+
107
+ ui.auto(() => {
108
+ if (live.loading) ui.spinner();
109
+ else if (live.error) ui.alert(live.error, { variant: 'destructive' });
110
+ else {
111
+ for (const u of live.units) {
112
+ ui.button(u.id, { onClick: () => { /* … */ } });
113
+ }
114
+ }
115
+ });
116
+
117
+ void load();
118
+ ui.timer(30, () => void load()); // poll; mutates `live` → auto rebuilds
119
+ ```
120
+
121
+ **Rules**
122
+
123
+ - Keep mutable state in `ui.state` **outside** `ui.auto` (locals inside the builder reset each rebuild).
124
+ - One `auto` can wrap many widgets that share the same reads; split `auto` regions when list vs detail should update independently.
125
+ - Prefer `ui.label(() => …)` / `bindValue` when only text/value changes.
126
+
127
+ ### When to use what
128
+
129
+ | Need | Use |
130
+ |------|-----|
131
+ | Data that drives the tree (lists, loading, selection) | `ui.state` + `ui.auto` |
132
+ | Polling / intervals | `ui.timer` mutating state |
133
+ | One-shot structural rebuild you trigger yourself | `ui.refreshable` + `.refresh()` |
134
+ | Single label/input sync | `ui.label(() => …)` / `bindValue` / `bindText` |
135
+ | NiceGUI-style `let` sugar (demos, counters) | `// @clay-reactive` + `clay --reactive-let` — see [reactive-let](./reactive-let.md#happy-path-let--clay-reactive) |
136
+ | Production async / dense ops consoles | `ui.state` + `ui.auto` (Phase 1) |
137
+
138
+ ### Alternative: reactive `let` (Phase 2)
139
+
140
+ For teaching and small demos you can write plain `let` and opt in with a file pragma:
141
+
142
+ ```typescript
143
+ // @clay-reactive
144
+ import { ui } from '@close-by/clay';
145
+
146
+ export default function () {
147
+ let count = 0;
148
+ ui.label(`Count: ${count}`);
149
+ ui.button('+', { onClick: () => { count++; } });
150
+ }
151
+ ```
152
+
153
+ Run with `clay page.ts --reactive-let`. The compiler lifts `count` to `ui.state` and wires labels automatically. Full rules and limits: [reactive-let.md](./reactive-let.md).
154
+
155
+ ## Bindings and `reactive`
156
+
157
+ Two-way bind form controls to a reactive object:
158
+
159
+ ```typescript
160
+ import { ui, reactive } from '@close-by/clay';
161
+
162
+ const draft = reactive({ text: '' });
163
+ const input = ui.input({ placeholder: '…' });
164
+ input.bindValue(draft, 'text');
165
+
166
+ // read
167
+ const value = draft.text;
168
+
169
+ // write (also updates the input via patch)
170
+ draft.text = '';
171
+ ```
172
+
173
+ - `reactive(obj)` — Proxy that notifies subscribers on property change
174
+ - `ui.state(obj)` — alias for `reactive` (prefer in app code)
175
+ - `ui.auto(fn)` — rebuild a region when tracked `state`/`reactive` reads change; stable trees patch via `updateProps` (see [`reactive-let.md`](./reactive-let.md))
176
+ - `ui.label(() => string)` / `.bindText(fn)` — computed label text without wrapping `auto`
177
+ - `bindValue(obj, key)` — syncs element value ↔ `obj[key]` on `input`/`change`
178
+ - `bindTextFrom(obj, key)` — one-way sync into label text
179
+ - `subscribe(obj, key, fn)` — run a callback when a key changes (e.g. refresh a summary panel)
180
+
181
+ ## Events
182
+
183
+ Declare handlers with camelCase props (`onClick`, `onChange`, `onInput`) or `.on(event, handler)`:
184
+
185
+ ```typescript
186
+ ui.button('Save', {
187
+ onClick: async () => {
188
+ await save();
189
+ },
190
+ });
191
+
192
+ ui.input({
193
+ onInput: (value) => console.log(value),
194
+ });
195
+ ```
196
+
197
+ The client keeps local optimistic state for inputs, textareas, checkboxes, switches, selects, radio groups, comboboxes, dates, sliders, tabs, accordions, collapsibles, and DataTable filter inputs so typing/toggling feels instant while the server confirms.
198
+
199
+ ## Layout context
200
+
201
+ Layout helpers push a parent onto a stack:
202
+
203
+ ```typescript
204
+ ui.column(() => {
205
+ ui.label('Title');
206
+ ui.row(() => {
207
+ ui.button('A');
208
+ ui.button('B');
209
+ });
210
+ });
211
+ ```
212
+
213
+ Children created inside the callback become children of that layout element.
214
+
215
+ ### App shell (`ui.run({ app })`)
216
+
217
+ Configure chrome once at startup; page modules only register content. `ui.loadPages(dir)` imports every page file under a directory; optional `pageMeta` feeds `ui.navFromPages()` for the primary sidebar.
218
+
219
+ ```typescript
220
+ await ui.loadPages(new URL('./pages', import.meta.url));
221
+
222
+ ui.run({
223
+ app: {
224
+ title: 'My App',
225
+ nav: ui.navFromPages(),
226
+ },
227
+ });
228
+ ```
229
+
230
+ ```typescript
231
+ // pages/Charts.ts
232
+ export const pageMeta = { label: 'Charts', icon: 'chart-area', order: 90 };
233
+
234
+ ui.page('/examples/charts', () => {
235
+ ui.label('Charts'); // inherits global shell
236
+ });
237
+ ```
238
+
239
+ Opt out per route with `ui.page(path, fn, { shell: false })`. Keep explicit `ui.app(props, fn)` for advanced cases.
240
+
241
+ The client keeps this chrome **sticky** across SPA navigate: the WebSocket stays open, a new `hello` remounts the server session, and React reuses the `app` shell (sidebar / header) when the chrome structure matches. Only the inset page content remounts; nav `active` flags update from the new tree.
242
+
243
+ ## Notifications and navigation
244
+
245
+ ```typescript
246
+ ui.notify('Saved!', 'success');
247
+ ui.notify('Look here', {
248
+ type: 'warning',
249
+ duration: 0,
250
+ position: 'top-right',
251
+ description: 'Sticky toast via Sonner',
252
+ });
253
+
254
+ // still available:
255
+ import { notify, navigate } from '@close-by/clay-core';
256
+ navigate('/examples/todo');
257
+ ```
258
+
259
+ `ui.notify` / `notify` map to [Sonner](https://ui.shadcn.com/docs/components/sonner) on the client (`toast.success` / `info` / `warning` / `error`). `duration: 0` is sticky; optional `description` is Sonner’s secondary line. `navigate` tells the client to change path and send a new `hello` on the same WebSocket (server remounts the page; client keeps matching `app` chrome mounted).
260
+
261
+ ## Imperative overlays
262
+
263
+ ```typescript
264
+ const sure = await ui.confirm('Continue?');
265
+ const name = await ui.prompt('Your name');
266
+ const pick = await ui.choose('Pick one', ['A', 'B']);
267
+ ```
268
+
269
+ These open ephemeral dialogs, return Promises, and only work inside an active session (e.g. `onClick` / `onAction`). They are not synchronous — a WebSocket round-trip is required.
package/docs/duckdb.md ADDED
@@ -0,0 +1,68 @@
1
+ # DuckDB (`@close-by/clay-duckdb`)
2
+
3
+ Multi-database DuckDB wrapper: named instances, `ATTACH` for DuckDB/SQLite/Postgres/MySQL, and insert/update/delete helpers.
4
+
5
+ ```bash
6
+ bun add @close-by/clay-duckdb # or depend on workspace:* inside this monorepo
7
+ ```
8
+
9
+ ## Connect multiple databases
10
+
11
+ ```typescript
12
+ import { DuckDB } from '@close-by/clay-duckdb';
13
+
14
+ const duck = new DuckDB();
15
+
16
+ await duck.connect('app', './data/app.duckdb');
17
+ await duck.connect('scratch', ':memory:');
18
+
19
+ const app = duck.db('app');
20
+ await app.exec(`CREATE TABLE IF NOT EXISTS users (
21
+ id INTEGER PRIMARY KEY,
22
+ name VARCHAR,
23
+ email VARCHAR
24
+ )`);
25
+ ```
26
+
27
+ | Method | Description |
28
+ |--------|-------------|
29
+ | `connect(name, path?)` | Open/create a DuckDB file (default `:memory:`) |
30
+ | `db(name)` | Get a named `Connection` |
31
+ | `has(name)` / `list()` | Registry helpers |
32
+ | `close(name)` / `closeAll()` | Disconnect and drop from registry |
33
+
34
+ ## CRUD
35
+
36
+ ```typescript
37
+ await app.insert('users', { id: 1, name: 'Ada', email: 'a@x.com' });
38
+ await app.update('users', { name: 'Ada Lovelace' }, { id: 1 });
39
+ await app.delete('users', { id: 1 });
40
+
41
+ const rows = await app.query<{ id: number; name: string }>(
42
+ 'SELECT * FROM users WHERE id = $id',
43
+ { id: 1 },
44
+ );
45
+ const one = await app.queryOne('SELECT * FROM users WHERE id = $id', { id: 1 });
46
+ ```
47
+
48
+ `update` and `delete` require a non-empty `where` object (safety guard).
49
+
50
+ Use `app.raw` for the underlying `@duckdb/node-api` connection when you need appenders or streaming.
51
+
52
+ ## ATTACH external databases
53
+
54
+ ```typescript
55
+ await duck.attach('app', {
56
+ alias: 'legacy',
57
+ type: 'sqlite', // 'duckdb' | 'sqlite' | 'postgres' | 'mysql'
58
+ path: './legacy.sqlite', // file path or DSN for postgres/mysql
59
+ readOnly: false,
60
+ });
61
+
62
+ await app.query('SELECT * FROM legacy.users');
63
+ await app.insert('legacy.users', { id: 2, name: 'Grace' });
64
+
65
+ await duck.detach('app', 'legacy');
66
+ ```
67
+
68
+ Extensions (`sqlite`, `postgres`, `mysql`) are `INSTALL`/`LOAD`ed once per process when first needed. Prefer `type: 'duckdb'` for sibling DuckDB files (no extension).