@close-by/clay 0.2.17 → 0.2.19

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
@@ -15,12 +15,14 @@ For coding agents: the repo-root [llms.txt](../llms.txt) is a short map of these
15
15
  | [Tailwind](./tailwind.md) | Zero-config utilities from page modules; optional tokens |
16
16
  | [Sample app](./tutorial.md) | Step-by-step tasks list, then two pages + shell |
17
17
  | [Concepts](./concepts.md) | Mental model: sessions, elements, patches; **canonical `state`/`auto`/`timer` recipe**; reactive `let` alternative |
18
+ | [Updating in place](./updating-in-place.md) | Patch instead of rebuild: `setText`, keyed `setRows`, keyed `refreshable` / `auto` lists (no flicker) |
18
19
  | [Browser APIs](./browser-apis.md) | Prefer `ui.clipboard` / hash / search / `openExternal` — never `window` in page code |
19
20
  | [URL search](./url-search.md) | `ui.urlState` + query helpers for shareable filters |
20
21
  | [Production & security](./production.md) | Auth, server exposure, rate limits, boot parity |
21
22
  | [Ops patterns](./ops-patterns.md) | Dense consoles: chips, master–detail, feeds, sensitive logs, auth nav |
22
23
  | [API reference](./api.md) | `ui.*`, `Element` methods, `reactive`, helpers |
23
24
  | [Elements](./elements.md) | All element types and props |
25
+ | [Maps](./maps.md) | `ui.map`: MapLibre markers, routes, arcs, GeoJSON, clusters; palette theming and in-place updates |
24
26
  | [WebSocket protocol](./protocol.md) | Client ↔ server message formats |
25
27
  | [Architecture](./architecture.md) | Packages, data flow, and ownership |
26
28
  | [Examples](./examples.md) | Demo apps and patterns |
package/docs/api.md CHANGED
@@ -91,6 +91,7 @@ ui.run({
91
91
  | `title` | `string` | `'Clay'` | HTML `<title>` |
92
92
  | `clientDir` | `string` | `resolveClayClientDir()` | Built Vite assets (`@close-by/clay-cli/client-dist` when installed). Prefer `resolveClayClientDir()` over hardcoding `node_modules` paths — see [Boot](./boot.md). |
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
+ | `sound` | `boolean` | | Play short synthesized interface sounds by default (`false`). See [Sound](#sound). |
94
95
  | `app` | `AppProps` | | Global dashboard shell; client keeps chrome sticky across navigate |
95
96
  | `authSecret` | `string` | | Enables signed auth cookies (`POST`/`DELETE /auth/session`), `ui.establishAuthSession` / `ui.clearAuthSession`, and default `resolveUserId` from the cookie |
96
97
  | `authCookieMaxAgeSec` | `number` | `43200` (12h) | Auth cookie Max-Age |
@@ -1801,6 +1802,13 @@ onClick: async () => {
1801
1802
  | `ui.openExternal(url)` | `void` | `window.open` in a new tab (`noopener`) |
1802
1803
  | `ui.theme.set(mode)` | `void` | Push `light` \| `dark` \| `system` to this client |
1803
1804
  | `ui.theme.get()` | `ThemeMode \| null` | Last value set on this session |
1805
+ | `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 |
1806
+ | `ui.theme.getPalette()` | `ThemePalette \| null` | Last palette set, else the one the client reported on connect |
1807
+ | `ui.theme.palettes` | `readonly string[]` | Built-in palette names |
1808
+ | `ui.sound.set(on)` | `void` | Turn automatic interface sounds on/off for this client; remembered across reloads. See [Sound](#sound) |
1809
+ | `ui.sound.volume(v)` | `void` | Master volume `0`–`1` (default `0.5`); remembered across reloads |
1810
+ | `ui.sound.play(name)` | `void` | Play one cue now, even when automatic sounds are off |
1811
+ | `ui.sound.names` | `readonly string[]` | Built-in cue names |
1804
1812
  | `ui.runJavaScript(code)` | `void` | Eval trusted snippet in the browser |
1805
1813
  | `ui.scroll.to(opts)` | `void` | Scroll window (`top` / `bottom` / px) |
1806
1814
  | `ui.scroll.intoView(selector, opts?)` | `void` | `Element.scrollIntoView` via selector |
@@ -1911,9 +1919,11 @@ All layout helpers accept either `(fn, props?)` or `(props, fn)`.
1911
1919
  | `ui.row` | Horizontal flex |
1912
1920
  | `ui.column` | Vertical flex |
1913
1921
  | `ui.container` | Width-constrained wrapper |
1922
+ | `ui.grid` | Responsive grid that fills the width (auto-fit columns, or fixed `cols`) |
1914
1923
  | `ui.hero` | Centered hero region |
1915
1924
  | `ui.card` | Card; callback receives the card element |
1916
1925
  | `ui.app` | SPA shell: sidebar nav + centered main content |
1926
+ | `ui.map` | Live map (MapLibre): markers, routes, arcs, GeoJSON, clusters, popup. See [Maps](./maps.md) |
1917
1927
 
1918
1928
  #### `ui.app(props, fn)`
1919
1929
 
@@ -1965,8 +1975,44 @@ Shared layout props:
1965
1975
  | `className` | `string` | | Extra classes |
1966
1976
  | `centered` | `boolean` | | `mx-auto` (container) |
1967
1977
  | `width` | `'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| '3xl' \| '4xl' \| '5xl' \| '6xl' \| '7xl' \| 'full'` | `'lg'` for container | Max width |
1978
+ | `align` | `'start' \| 'center' \| 'end' \| 'baseline' \| 'stretch'` | row: `center` (`stretch` when the row grows) | Cross-axis alignment |
1979
+ | `justify` | `'start' \| 'center' \| 'end' \| 'between' \| 'around'` | | Main-axis distribution |
1980
+ | `wrap` | `boolean` | row: `true` | Flex wrap |
1981
+ | `grow` | `boolean \| number` | | Take the remaining space in the parent row/column. A number sets the share (`grow: 2` gets twice `grow: 1`) |
1982
+ | `scroll` | `boolean \| 'x' \| 'y'` | | Scroll inside this element instead of growing the page. On a card, the body scrolls and the header stays |
1983
+ | `fillViewport` | `boolean` | | Height = visible page area (viewport minus the app shell header/padding). Use on the outermost page column |
1968
1984
 
1969
- Card also accepts `title?: string`.
1985
+ Card also accepts `title?: string` and `description?: string`; `grow` / `scroll` / `fillViewport` work on cards too.
1986
+
1987
+ #### `ui.grid(props, fn)`
1988
+
1989
+ | Prop | Type | Default | Description |
1990
+ |------|------|---------|-------------|
1991
+ | `minColWidth` | `string \| number` | `'16rem'` | Auto-fit as many columns as fit, each at least this wide (numbers are px). Wraps to one column on phones |
1992
+ | `cols` | `number` | | Fixed column count instead of auto-fit (one column below `sm`) |
1993
+ | `gap` | `number` | `4` | Gap scale |
1994
+
1995
+ ```typescript
1996
+ ui.grid({ minColWidth: '14rem' }, () => {
1997
+ for (const kpi of kpis) ui.card({ title: kpi.label }, () => ui.label(kpi.value));
1998
+ });
1999
+ ```
2000
+
2001
+ #### Filling the page
2002
+
2003
+ Make a page fill the window and let panels scroll instead of the page:
2004
+
2005
+ ```typescript
2006
+ ui.column({ fillViewport: true, gap: 4 }, () => {
2007
+ ui.grid({ minColWidth: '12rem' }, () => { /* KPI cards — natural height */ });
2008
+ ui.row({ grow: true, wrap: false, gap: 4 }, () => { // takes the remaining height
2009
+ ui.card({ title: 'Inbox', scroll: true, className: 'w-80 shrink-0' }, () => { /* list */ });
2010
+ ui.card({ title: 'Detail', grow: true, scroll: true }, () => { /* body */ });
2011
+ });
2012
+ });
2013
+ ```
2014
+
2015
+ Without `fillViewport`, `grow` still stretches to fill free space (the app shell content area is a flex column), but the page scrolls when content is taller than the window.
1970
2016
 
1971
2017
  ```typescript
1972
2018
  ui.container(() => {
@@ -1976,6 +2022,15 @@ ui.container(() => {
1976
2022
  }, { centered: true, width: 'md' });
1977
2023
  ```
1978
2024
 
2025
+ #### `ui.map(props?)`
2026
+
2027
+ A live map: MapLibre with markers, routes, arcs, GeoJSON layers, clusters and a popup. Setters (`setMarkers`, `upsertMarker`, `updateRoute`, `flyTo`, `fitBounds`, ...) update it in place. Props, tones, events and layout are in the [Maps guide](./maps.md).
2028
+
2029
+ ```typescript
2030
+ const fleet = ui.map({ fill: true, markers: vehicles(), onMarkerClick: (m) => select(m.id) });
2031
+ fleet.fitBounds('markers', { padding: 48 });
2032
+ ```
2033
+
1979
2034
  ---
1980
2035
 
1981
2036
  ## `Element` methods (`@close-by/clay-core`)
@@ -2143,6 +2198,33 @@ import { button, input /* … */ } from '@close-by/clay-components';
2143
2198
 
2144
2199
  ---
2145
2200
 
2201
+ ## Sound
2202
+
2203
+ Clay can answer interactions with short interface sounds, synthesized in the browser with Web Audio (no audio files). They're **off by default**. Turn them on for every visitor with `createServer({ sound: true })`, or per session with `ui.sound.set(true)`. The client remembers the last choice.
2204
+
2205
+ Once sound is on, a mute button appears next to the Clay connection chip in the bottom-left corner, so each user can silence it. A user's mute choice is remembered for their browser. Apps that never turn sound on don't show the button.
2206
+
2207
+ When enabled, a page-wide listener picks the cue from the element, so components need no wiring:
2208
+
2209
+ | Interaction | Cue |
2210
+ |---|---|
2211
+ | Button, link, `role="button"` | `tap` (`soft` for `ghost` / `link` variants, `destructive` for the destructive variant) |
2212
+ | Tab, menu item, option, radio | `select` (pitch rises with position) |
2213
+ | Switch, checkbox | `toggleOn` / `toggleOff` |
2214
+ | Disabled control (when it still receives the press) | `blocked` |
2215
+ | Dialog / sheet / alert dialog opens or closes | `open` / `close` |
2216
+ | Slider value changes | `tick` |
2217
+ | OTP digit typed | `key` |
2218
+ | `ui.notify` toast | `success` / `error` / `warning` / `notification` by type |
2219
+ | `ui.clipboard` / code block copy | `copy` |
2220
+
2221
+ Set `data-sound="<cue>"` on an element to override its cue, or `data-sound="none"` to silence it. Use `ui.sound.play(name)` to play a cue from server code, for example when a long job finishes. Cues: `tap`, `soft`, `select`, `destructive`, `blocked`, `toggleOn`, `toggleOff`, `open`, `close`, `tick`, `key`, `copy`, `success`, `error`, `warning`, `notification`.
2222
+
2223
+ ```ts
2224
+ // A settings switch that lets each user turn sounds on or off
2225
+ ui.switch({ label: 'Interface sounds', onChange: (on) => ui.sound.set(on) });
2226
+ ```
2227
+
2146
2228
  ## DuckDB (`@close-by/clay-duckdb`)
2147
2229
 
2148
2230
  See [DuckDB](./duckdb.md) for the multi-database wrapper (`connect`, `attach`, CRUD).
@@ -0,0 +1,64 @@
1
+ # Demo deployment on Pi
2
+
3
+ The demo runs at https://clay.close-by.org/ on Pi as the `bad-ui-demo` systemd service. It runs Bun as the `pi` user from `/home/pi/bad-ui-demo` on port 4000. The existing Flock Cloudflare tunnel forwards `clay.close-by.org` to `http://localhost:4000`; the Cloudflare DNS record is a proxied CNAME to that tunnel.
4
+
5
+ ## Update the demo
6
+
7
+ From the repository root on a machine with SSH access to the `pi` host, copy the working tree and rebuild on Pi:
8
+
9
+ ```sh
10
+ tar --exclude-vcs --exclude=node_modules --exclude=dist-pack \
11
+ --exclude=.env --exclude='.env.*' --exclude=.badui-user-data \
12
+ --exclude=.clay-user-data --exclude=.clay --exclude=.tmp \
13
+ -czf /tmp/clay-demo-deploy.tgz .
14
+ scp /tmp/clay-demo-deploy.tgz pi:/tmp/clay-demo-deploy.tgz
15
+ ssh pi 'tar -xzf /tmp/clay-demo-deploy.tgz -C ~/bad-ui-demo && \
16
+ cd ~/bad-ui-demo && export PATH="$HOME/.local/bin:$PATH" && \
17
+ bun install --frozen-lockfile && bun run build:client && \
18
+ sudo systemctl restart bad-ui-demo'
19
+ ```
20
+
21
+ This copies the current working tree, including uncommitted changes. Extraction does not delete files removed from the source tree; remove obsolete deployed files deliberately when needed. The service keeps running the previous process until the restart, but the copy and build happen in its working directory.
22
+
23
+ Check the service and public endpoint:
24
+
25
+ ```sh
26
+ ssh pi 'systemctl status bad-ui-demo --no-pager; journalctl -u bad-ui-demo -n 50 --no-pager'
27
+ curl -I https://clay.close-by.org/
28
+ ```
29
+
30
+ ## Service and tunnel setup
31
+
32
+ The service is enabled with `sudo systemctl enable --now bad-ui-demo`. Its unit is `/etc/systemd/system/bad-ui-demo.service`:
33
+
34
+ ```ini
35
+ [Unit]
36
+ Description=Clay demo
37
+ After=network.target
38
+
39
+ [Service]
40
+ User=pi
41
+ WorkingDirectory=/home/pi/bad-ui-demo
42
+ Environment=PORT=4000
43
+ Environment=PATH=/home/pi/.local/bin:/usr/local/bin:/usr/bin:/bin
44
+ EnvironmentFile=/home/pi/.config/bad-ui-demo/service.env
45
+ ExecStart=/home/pi/.local/bin/bun apps/demo/src/main.ts
46
+ Restart=on-failure
47
+ RestartSec=2
48
+
49
+ [Install]
50
+ WantedBy=multi-user.target
51
+ ```
52
+
53
+ The environment file contains `CLAY_AUTH_SECRET`; keep it private and stable across restarts. To create it on a fresh deployment, use `umask 077`, create `/home/pi/.config/bad-ui-demo`, and write `CLAY_AUTH_SECRET=` followed by a random secret (for example, from `openssl rand -hex 32`). Changing it invalidates existing signed sessions. After changing a unit, run `sudo systemctl daemon-reload` and restart the service.
54
+
55
+ The tunnel configuration is `/etc/cloudflared/flock.yml`, managed by `flock-tunnel.service`. Its Clay ingress entry must precede the final catch-all:
56
+
57
+ ```yaml
58
+ - hostname: clay.close-by.org
59
+ service: http://localhost:4000
60
+ ```
61
+
62
+ Validate changes with `sudo cloudflared tunnel --config /etc/cloudflared/flock.yml ingress validate`, then restart `flock-tunnel`. This is shared with Flock, so preserve its existing ingress entries. The DNS CNAME must point to the existing tunnel's `<tunnel-id>.cfargotunnel.com` and be proxied; the tunnel credentials on Pi alone do not grant permission to edit Cloudflare DNS.
63
+
64
+ This is a public demo with example authentication and data, not a hardened production application. See [Production & security](./production.md) before deploying real data or applications.
package/docs/elements.md CHANGED
@@ -211,4 +211,6 @@ ui.label('Hello').classes('text-3xl font-bold text-muted-foreground');
211
211
 
212
212
  Layout `gap` uses a small numeric scale mapped to `gap-0` … `gap-8` in the client.
213
213
 
214
- The client ships a light ShadCN-style theme (CSS variables in `packages/client/src/index.css`). Override tokens at runtime with `ui.run({ css: './globals.css' })`.
214
+ 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
+
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')`.
package/docs/examples.md CHANGED
@@ -5,11 +5,15 @@ Pages: [`apps/demo/src/examples/`](../apps/demo/src/examples/) (auto-loaded via
5
5
 
6
6
  After `bun run build:client && bun run demo` (or `bun run demo:cli`), open http://localhost:4000.
7
7
 
8
+ ## Sidebar groups
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`.
11
+
8
12
  ## Catalog
9
13
 
10
14
  | Route | File | Patterns shown |
11
15
  |-------|------|----------------|
12
- | `/` | `Home.ts` | `ui.hero`, `ui.icon`, `ui.link`, `ui.navigate`; catalog via `ui.navFromPages()` (same as sidebar) |
16
+ | `/` | `Home.ts` | Landing page: hero, live counter + source side by side (`ui.codeBlock`), feature grid, catalog grouped via `ui.navFromPages()` (same as sidebar) |
13
17
  | `/examples/counter` | `Counter.ts` | `setText`, `refreshable`, button variants |
14
18
  | `/examples/todo` | `Todo.ts` | `reactive`, `bindValue`, list `refreshable`, filters |
15
19
  | `/examples/chat` | `Chat.ts` | `ui.storage.app` (persisted messages + ephemeral presence), async `get`/`set` |
@@ -23,6 +27,8 @@ After `bun run build:client && bun run demo` (or `bun run demo:cli`), open http:
23
27
  | `/examples/reui-batch-7` | `ReuiBatch7Demo.ts` | ReUI batch 7: `inputGroup`, `toggle`, `descriptionList`, `staticTable`, `aspectRatio`, `itemList`, `checkboxGroup` |
24
28
  | `/examples/charts` | `ChartDemo.ts` | `ui.chart.*`, `ui.sparkline`, composed/scatter charts |
25
29
  | `/examples/slider-demo` | `SliderDemo.ts` | Slider, checkbox, select + bindings |
30
+ | `/examples/themes` | `ThemesDemo.ts` | Palette gallery: `ui.theme.palette`, light/dark, component preview |
31
+ | `/examples/layout` | `LayoutDemo.ts` | Fill layout: `fillViewport`, `grow`, `scroll`, `ui.grid` |
26
32
  | `/examples/feedback` | `FeedbackDemo.ts` | Alerts, progress, timer, `ui.theme`, `ui.runJavaScript` / `ui.scroll` |
27
33
  | `/examples/form-demo` | `FormDemo.ts` | Form + validate + `ui.draft` + rating / colorPicker / tags |
28
34
  | `/examples/auth` | `Auth.ts` (+ login/admin/change-password) | Signed cookie session, hashed passwords, login lockout, role-gated admin + audit log |
@@ -38,6 +44,11 @@ After `bun run build:client && bun run demo` (or `bun run demo:cli`), open http:
38
44
  | `/examples/gantt` | `GanttDemo.ts` | `ui.gantt` owned rows/dates/markers/deps, cross-row drag, header double-click markers, `itemMove` / `markerAdd` settle |
39
45
  | `/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`) |
40
46
  | `/examples/data-clients` | `DataClientsDemo.ts` | DuckDB / Kibana / ClickHouse integration story (mock-friendly) |
47
+ | `/examples/maps` | `MapsGallery.ts` | `ui.map` gallery: markers and popups, routes, arcs, choropleth, clusters, compact analytics map |
48
+ | `/examples/delivery-tracker` | `DeliveryTracker.ts` | `ui.map` route progress and courier marker: `updateRoute` / `upsertMarker` on a timer, `fill` layout |
49
+ | `/examples/store-locator` | `StoreLocator.ts` | `ui.map` filtered markers, `flyTo` / `fitBounds('markers')`, popups; list and map select each other by key |
50
+ | `/examples/globe` | `CommunityGlobe.ts` | `ui.map` globe projection, arcs, `setMarkers` / `setArcs` on a 30 s timer |
51
+ | `/examples/uptime` | `UptimeMonitor.ts` | `ui.map` status markers on a globe, refreshed every 2 s with `setMarkers` |
41
52
  | `/examples/kitchen-sink` | `KitchenSink.ts` | ShadCN catalog preview (client `KitchenSink`) |
42
53
 
43
54
  Each page file exports optional `pageMeta` (`label`, `icon`, `order`, optional `nav: false`, optional `roles`) for `ui.navFromPages()`.
package/docs/maps.md ADDED
@@ -0,0 +1,214 @@
1
+ # Maps
2
+
3
+ `ui.map(props)` is a live map with markers, routes, arcs, GeoJSON layers, clusters, heatmaps and a popup. It renders with [MapLibre GL](https://maplibre.org/) through a vendored copy of [mapcn](https://mapcn.vercel.app) (MIT). You create the map on the server, change it with setters, and handle clicks on the server.
4
+
5
+ - **Lazy.** The client loads the map code only when a page renders a map (`React.lazy` in the element renderer). Other pages do not download MapLibre.
6
+ - **Worker bundled.** The MapLibre worker ships with the client build. No CDN script is loaded.
7
+
8
+ ## Quick start
9
+
10
+ ```typescript
11
+ import { ui } from '@close-by/clay';
12
+
13
+ ui.page('/fleet', () => {
14
+ const fleet = ui.map({
15
+ center: [-9.14, 38.72],
16
+ zoom: 11,
17
+ height: '28rem',
18
+ markers: [
19
+ { id: 'hq', lngLat: [-9.142, 38.7108], label: 'HQ', popup: { title: 'Head office' } },
20
+ { id: 'truck-1', lngLat: [-9.1393, 38.7097], tone: 'chart-2', icon: 'truck', pulse: true },
21
+ ],
22
+ onMarkerClick: (marker) => ui.notify(`Selected ${marker.id}`),
23
+ });
24
+
25
+ fleet.fitBounds('markers', { padding: 48 });
26
+ });
27
+ ```
28
+
29
+ Coordinates are always `[longitude, latitude]`.
30
+
31
+ ## Theming
32
+
33
+ - **Light and dark.** The basemap follows the page theme. Markers, routes, arcs and fills take their colours from the active palette.
34
+ - **Tones.** Colour props accept a tone name or a CSS colour such as `#2563eb`:
35
+
36
+ | Tone | Palette token |
37
+ |------|---------------|
38
+ | `primary` | `--primary` (default for markers, routes and fills) |
39
+ | `chart-1` … `chart-5` | `--chart-1` … `--chart-5` |
40
+ | `destructive` | `--destructive` |
41
+ | `muted` | `--muted-foreground` |
42
+ | `foreground` | `--foreground` |
43
+
44
+ - **Live palette changes.** The map watches the root element's `class`, `data-palette` and `style`, and the system colour scheme. A palette change re-resolves the colours in place. The map is not remounted, so the camera and open popups stay put.
45
+ - **`tint`** (default `true`) lightly tints the basemap toward the palette: background, water, and parks or green areas.
46
+ - **`basemap`** (default `'auto'`) pins the basemap to `'light'` or `'dark'`, whatever the page theme.
47
+ - **`styles`** replaces the basemap style URLs: `{ light?: string; dark?: string }`. See [Tiles](#tiles).
48
+
49
+ ## Props
50
+
51
+ | Prop | Type | Default | Notes |
52
+ |------|------|---------|-------|
53
+ | `center` | `LngLat` | `[0, 20]` | Initial centre, read at mount. See [Camera](#camera) |
54
+ | `zoom` | `number` | `1.5` | Initial zoom |
55
+ | `pitch` / `bearing` | `number` | `0` | Initial pitch and bearing |
56
+ | `projection` | `'mercator' \| 'globe'` | `'mercator'` | |
57
+ | `height` | `string` | `'24rem'` | CSS length. Ignored with `fill` |
58
+ | `fill` | `boolean` | `false` | Fill the parent. See [Layout](#layout) |
59
+ | `interactive` | `boolean` | `true` | `false` disables pan, zoom and click |
60
+ | `basemap` | `'auto' \| 'light' \| 'dark'` | `'auto'` | |
61
+ | `styles` | `{ light?, dark? }` | CARTO styles | Custom MapLibre style URLs |
62
+ | `tint` | `boolean` | `true` | Palette tint on the basemap |
63
+ | `controls` | `MapControls \| false` | `{ zoom: true, position: 'bottom-right' }` | Object values merge over the default. `false` hides all controls |
64
+ | `markers` | `MapMarkerDatum[]` | `[]` | |
65
+ | `routes` | `MapRouteDatum[]` | `[]` | |
66
+ | `arcs` | `MapArcGroup[]` | `[]` | |
67
+ | `geojson` | `MapGeoJSONLayer[]` | `[]` | |
68
+ | `clusters` | `MapClusterGroup[]` | `[]` | |
69
+ | `heatmaps` | `MapHeatmapLayer[]` | `[]` | |
70
+ | `popup` | `{ lngLat, title?, description? } \| null` | closed | Standalone popup |
71
+
72
+ `MapControls` is `{ zoom?, compass?, locate?, fullscreen?, position? }`. `compass`, `locate` and `fullscreen` are off by default. `position` is `top-left`, `top-right`, `bottom-left` or `bottom-right`.
73
+
74
+ ## Data layers
75
+
76
+ **Markers.** `id` (required, unique) and `lngLat` are required. Optional: `tone`, `label` (always visible), `tooltip` (on hover), `popup` (on click: `title`, `description`, `meta: [{ label, value }]`), `icon` (Lucide kebab-case name, drawn in a 28 px pin), `size` (`sm` / `md` / `lg` dots of 10 / 14 / 20 px, used when there is no icon), `pulse` (animated ring), `draggable`.
77
+
78
+ **Routes.** `id` and `coordinates: [lng, lat][]` are required. Optional: `tone`, `width` (px), `opacity`, `dashed`, `progress` (0..1: draws a progress overlay and a marker there), `progressIcon` (default `navigation`), `progressMarker` (default `true`; set `false` when you track the vehicle with your own marker), `progressTone` (colour of the travelled part, default `primary`), `active` (emphasise the selected route), `showEndpoints` (start ring and end dot). Change one route with `updateRoute` (see [Live data](#live-data--in-place-updates)).
79
+
80
+ **Arcs.** Grouped. A group has `id`, `tone`, `width`, `curvature` and `data: { id, from, to, label? }[]`. Arc ids are unique within their group.
81
+
82
+ ```typescript
83
+ ui.map({
84
+ projection: 'globe',
85
+ arcs: [{ id: 'lanes', tone: 'chart-1', data: [{ id: 'lis-mad', from: [-9.14, 38.72], to: [-3.7, 40.42], label: 'Lisbon to Madrid' }] }],
86
+ onArcClick: (arc, group) => ui.notify(`${arc.label ?? arc.id} (${group.id})`),
87
+ });
88
+ ```
89
+
90
+ **GeoJSON.** `{ id, data, fillTone?, lineTone?, fillOpacity?, choropleth?, interactive?, promoteId? }`. `data` is a FeatureCollection, a Feature, or a URL string. A choropleth is `{ property, stops: [value, tone][] }`, with at least two stops. Without a choropleth the layer uses `fillTone`. The default fill opacity is `0.6` for a choropleth and `0.25` otherwise. The server does not validate GeoJSON.
91
+
92
+ **Clusters.** Grouped points: `{ id, points: [{ id, lngLat, properties? }], tone?, clusterRadius?, clusterMaxZoom? }`. Cluster bubbles use the group tone, `chart-2` and `chart-3`.
93
+
94
+ **Heatmaps.** `{ id, points: [{ lngLat, weight? }], ramp?, radius?, intensity?, opacity?, maxWeight?, showPointsAtZoom? }`. Weights default to `1` and must be finite and at least `0`. `maxWeight` is the weight that reaches full intensity; it defaults to the largest weight. The `ramp` runs from low to high density and defaults to `['chart-3', 'chart-4', 'primary', 'destructive']`, so it follows the palette and light or dark mode. `radius` (default `20` px at zoom 9) scales with zoom. Set `showPointsAtZoom` to fade the heatmap into individual circles when zoomed in. Replace the data with `setHeatmaps(layers)`; the client updates the source in place.
95
+
96
+ ```typescript
97
+ const heat = ui.map({ projection: 'globe', heatmaps: [{ id: 'quakes', points, maxWeight: 7, showPointsAtZoom: 6 }] });
98
+ heat.setHeatmaps([{ id: 'quakes', points: points.filter((p) => (p.weight ?? 0) >= 5) }]);
99
+ ```
100
+
101
+ **Standalone popup.** `showPopup([lng, lat], { title?, description? })` opens one, and `closePopup()` closes it. The `popup` prop does the same declaratively. Only marker popups take `meta` rows.
102
+
103
+ ## Camera
104
+
105
+ | Method | Behaviour |
106
+ |--------|-----------|
107
+ | `flyTo(center, zoom?, { duration? })` | Animates. Keeps the current zoom when `zoom` is omitted |
108
+ | `fitBounds([sw, ne], { padding? })` | Fits the given bounds |
109
+ | `fitBounds('markers', { padding? })` | Fits all current markers. Does nothing when there are none |
110
+ | `jumpTo(center, zoom?)` | Moves without animation |
111
+ | `setProjection('mercator' \| 'globe')` | Sends a new `projection` |
112
+
113
+ Each camera call runs once on the client. The server attaches a nonce, so repeating a call moves the map again.
114
+
115
+ The client reads `center`, `zoom`, `pitch`, `bearing` and `interactive` once, at mount. Changing those props later does not move an existing map. Use the camera methods instead.
116
+
117
+ ## Events
118
+
119
+ Handlers are registered only when you pass them. Each handler runs on the server, and receives:
120
+
121
+ | Handler | Fires when | Argument |
122
+ |---------|------------|----------|
123
+ | `onMarkerClick(marker)` | A marker is clicked | The current marker datum |
124
+ | `onMarkerDragEnd(marker, lngLat)` | A draggable marker is dropped | The updated marker and its new `lngLat`. The stored position is updated first |
125
+ | `onRouteClick(route)` | A route is clicked | The current route datum |
126
+ | `onArcClick(arc, group)` | An arc is clicked | The arc and its group |
127
+ | `onFeatureClick(feature)` | A GeoJSON feature is clicked | `{ layerId, featureId?, properties }` |
128
+ | `onClusterPointClick(point, group)` | A clustered point is clicked | The point and its group |
129
+ | `onMapClick(lngLat)` | The map is clicked | `[lng, lat]` |
130
+ | `onViewportChange(viewport)` | The camera stops moving (`moveend`) | `{ center, zoom, bearing, pitch }` |
131
+ | `onLocate(lngLat)` | The locate control is used (needs `controls.locate`) | `[lng, lat]` |
132
+
133
+
134
+ `markerDragEnd` is always recorded on the server, even without `onMarkerDragEnd`, so a later `fitBounds('markers')` uses the dropped position.
135
+
136
+ ## Live data & in-place updates
137
+
138
+ The map owns its marker, route, arc, cluster and popup state. Each setter sends only the prop it changes, so the client patches the map instead of rebuilding it. Markers are keyed by `id`: when a marker's `lngLat` changes, it moves, and its DOM node stays.
139
+
140
+ | Method | What it does |
141
+ |--------|--------------|
142
+ | `setMarkers(list)` | Replaces all markers |
143
+ | `upsertMarker(marker)` | Inserts, or replaces the marker with the same id |
144
+ | `removeMarker(id)` | Removes a marker. No-op if absent |
145
+ | `setRoutes(list)` / `updateRoute(id, patch)` | Replaces all routes, or merges `patch` into one. The id cannot change |
146
+ | `setArcs(groups)` / `setGeoJSON(layers)` / `setClusters(groups)` / `setHeatmaps(layers)` | Replace that layer type |
147
+
148
+ A courier moving along a route:
149
+
150
+ ```typescript
151
+ ui.timer(0.25, () => {
152
+ progress = Math.min(1, progress + 0.006);
153
+ map.updateRoute('delivery-route', { progress });
154
+ map.upsertMarker({ id: 'courier', lngLat: pointAt(progress), tone: 'chart-2', icon: 'truck' });
155
+ });
156
+ ```
157
+
158
+ **Validation** runs when you create the map or call a setter, before anything is sent. It throws on:
159
+
160
+ - a missing or empty id, or a duplicate id within its list (markers, routes, arc groups, arcs in a group, cluster groups, points in a group). The message is `duplicate id "…"`.
161
+ - a coordinate that is not `[lng, lat]` with two finite numbers. This covers markers, cluster points, route coordinates, arc endpoints, popups, `flyTo` and `jumpTo` centres, and `fitBounds` corners.
162
+
163
+ ## Layout
164
+
165
+ `height` gives a fixed-size map in normal flow:
166
+
167
+ ```typescript
168
+ ui.card({ title: 'Stores', className: 'p-0 overflow-hidden' }, () => {
169
+ ui.map({ height: '26rem', center: [-6, 40], zoom: 5 });
170
+ });
171
+ ```
172
+
173
+ `fill: true` makes the map fill its parent (`h-full min-h-0 flex-1`), so the parent needs a definite height. On a page that fills the window, put `fillViewport` on the outer column and `grow` on the panel that holds the map:
174
+
175
+ ```typescript
176
+ ui.column({ fillViewport: true, gap: 4 }, () => {
177
+ ui.row({ grow: true, wrap: false, gap: 4 }, () => {
178
+ ui.card({ title: 'Stores', scroll: true, className: 'w-80 shrink-0' }, () => { /* list */ });
179
+ ui.column({ grow: true }, () => {
180
+ ui.map({ fill: true, center: [-6, 40], zoom: 5 });
181
+ });
182
+ });
183
+ });
184
+ ```
185
+
186
+ See [API reference: Filling the page](./api.md#filling-the-page).
187
+
188
+ ## Tiles
189
+
190
+ By default the basemap uses CARTO's hosted styles: Positron for light and Dark Matter for dark, served from `basemaps.cartocdn.com`. Those styles are built on OpenStreetMap data. CARTO's free tier and terms apply, and OpenStreetMap data requires attribution.
191
+
192
+ Before production, check CARTO's current terms and attribution rules, or pass your own styles. Any MapLibre style URL works:
193
+
194
+ ```typescript
195
+ ui.map({ styles: { light: 'https://your-host.example/light.json', dark: 'https://your-host.example/dark.json' } });
196
+ ```
197
+
198
+ [OpenFreeMap](https://openfreemap.org) publishes free style URLs. Check its terms before you rely on it.
199
+
200
+ ## Demos
201
+
202
+ All five are under the **Maps** group in the sidebar.
203
+
204
+ - [`/examples/maps`](../apps/demo/src/examples/MapsGallery.ts): markers and popups, routes, arcs, a choropleth, clusters, a seismic heatmap on a globe, and a compact analytics map.
205
+ - [`/examples/delivery-tracker`](../apps/demo/src/examples/DeliveryTracker.ts): a courier moving along a route, with `updateRoute` and `upsertMarker` on a timer.
206
+ - [`/examples/store-locator`](../apps/demo/src/examples/StoreLocator.ts): a filterable store list where list rows and markers select each other.
207
+ - [`/examples/globe`](../apps/demo/src/examples/CommunityGlobe.ts): a globe with arcs and markers that refresh every 30 seconds.
208
+ - [`/examples/uptime`](../apps/demo/src/examples/UptimeMonitor.ts): a status globe of edge locations, refreshed every 2 seconds.
209
+
210
+ ## See also
211
+
212
+ - [Updating in place](./updating-in-place.md): the general rules for patching instead of rebuilding
213
+ - [API reference](./api.md#uimapprops): `ui.map` in the Layout section
214
+ - [Examples](./examples.md): the demo catalog
package/docs/protocol.md CHANGED
@@ -19,7 +19,7 @@ On unexpected socket close, the client reconnects with exponential backoff (500m
19
19
  { "op": "hello", "path": "/examples/counter", "userId": "optional-stable-id", "hash": "optional-hash-without-hash-sign", "browserStorage": {}, "clientStorage": {}, "tabStorage": {} }
20
20
  ```
21
21
 
22
- Client connects (or sends another `hello` after navigate on the same socket) and identifies the page. Optional `userId` (from localStorage/cookie) enables `ui.storage.user` unless overridden by `resolveUserId`. Optional `hash` (no leading `#`) hydrates `ui.getUrlHash()`. Optional `search` (no leading `?`) hydrates `ui.getUrlSearch()`. Optional `browserStorage` / `clientStorage` / `tabStorage` bags hydrate `ui.storage.browser` / `ui.storage.client` / `ui.storage.tab`.
22
+ Client connects (or sends another `hello` after navigate on the same socket) and identifies the page. Optional `userId` (from localStorage/cookie) enables `ui.storage.user` unless overridden by `resolveUserId`. Optional `hash` (no leading `#`) hydrates `ui.getUrlHash()`. Optional `search` (no leading `?`) hydrates `ui.getUrlSearch()`. Optional `browserStorage` / `clientStorage` / `tabStorage` bags hydrate `ui.storage.browser` / `ui.storage.client` / `ui.storage.tab`. Optional `palette` reports the palette the page shows, so `ui.theme.getPalette()` is correct after a reload.
23
23
 
24
24
  Sent on connect and after client-side navigation. The React client keeps matching `app` shell chrome mounted across remounts; only inset content is replaced.
25
25
 
@@ -199,6 +199,18 @@ Client runs `window.open(url, '_blank', 'noopener,noreferrer')`. Prefer `ui.open
199
199
 
200
200
  Sent by `ui.theme.set(...)`.
201
201
 
202
+ ### `palette`
203
+
204
+ ```json
205
+ { "op": "palette", "palette": "ocean" }
206
+ ```
207
+
208
+ | Field | Type | Notes |
209
+ |-------|------|-------|
210
+ | `palette` | `string` | Sets `<html data-palette>` (`kiln` / empty clears to default); persisted under `clay-palette` and restored before first paint |
211
+
212
+ Sent by `ui.theme.palette(...)`.
213
+
202
214
  ### `runJavaScript`
203
215
 
204
216
  ```json
@@ -0,0 +1,124 @@
1
+ # Updating in place (no flicker)
2
+
3
+ Clay sends every change over one WebSocket as patches. How your server code changes the element tree decides which patches go out, and therefore what the user sees.
4
+
5
+ New data should patch what is on screen, not rebuild it.
6
+
7
+ ## The rule
8
+
9
+ There are two ways to show a new value:
10
+
11
+ - **Rebuild.** You create new `ui.*` elements for the section (a new table, a new row of cards, a new list). Each new element gets a fresh id. The client receives a `setChildren` with those new ids and mounts new DOM. The old DOM is removed. Whatever lived in it is gone: scroll position, focus, what the user typed in an input, the selected row, and any enter animation runs again. The user sees a blank or jumping section.
12
+ - **Patch.** You keep the element you already have and change one prop on it, for example `label.setText(...)`. The client receives a single `updateProps` for that id, such as `{"op":"updateProps","id":"…","props":{"text":"42"}}`. The DOM node stays the same, so nothing else on the screen moves.
13
+
14
+ The flicker is caused by the remount, not by the new value. If a frame carries only the changed prop, the browser has nothing to redraw except that text.
15
+
16
+ ## Pick the right tool
17
+
18
+ | Situation | API | What goes out |
19
+ |-----------|-----|---------------|
20
+ | Change one value (a label, badge, or input) | `label.setText(…)`, `el.setValue(…)`, `el.update({ … })` | `updateProps` for that element |
21
+ | Live table data | `table.setRows(rows)` on a table created with a stable `keyField` (add `highlightChanges: true` to flash changed rows) | `updateProps` on the table; rows keyed by `keyField` keep their DOM |
22
+ | Table refetch | `table.setLoading(true)` / `table.withLoading(fn)` | `updateProps` (`loading`); previous rows stay, dimmed, instead of blanking |
23
+ | A section whose structure depends on data | `ui.refreshable(() => …)` + `.refresh()`, or `ui.auto(() => …)` | Reconciled: matched children get `updateProps` only; added, removed, or reordered children cause one `setChildren` per affected parent |
24
+ | Items in a list inside either of the above | `.key(item.id)` on each item | Items keep their identity through reorder, filter, and prepend |
25
+
26
+ Use the first row whenever the shape of the screen does not change. Reach for `refreshable` or `auto` only when children really are added, removed, or reordered.
27
+
28
+ ## How reconciliation matches
29
+
30
+ When a `refreshable` or `auto` block re-runs, Clay builds a fresh tree and matches it against the children already on screen:
31
+
32
+ 1. **Keyed first.** A new child with `.key(k)` matches the old child with the same key and the same element type. Keyed children are matched only by key.
33
+ 2. **Unkeyed by position.** Unkeyed children are matched in order, by element type. If the next old child matches the current new one, the current old child was removed and is skipped. If the current old child matches the next new one, the new child was inserted. Otherwise the pair is treated as a replacement at that position. This lookahead covers a single insert or remove; larger reshuffles should be keyed.
34
+ 3. **Matched pairs keep the old element and its id.** The fresh element's handlers and props are copied onto the old one. Only props that actually changed are sent, as one `updateProps`.
35
+ 4. **Leftovers.** Old children with no match are destroyed. New children with no match are mounted. The parent sends one `setChildren` only if its list of ids changed. A child whose type changed at a position is replaced; its siblings keep their ids.
36
+ 5. **Nested blocks keep their instance.** A nested `refreshable` or `auto` keeps its id and its subscriptions and adopts the new builder, so its own refreshes keep working.
37
+
38
+ ## Examples
39
+
40
+ ### Live orders list (`ui.auto` with keys)
41
+
42
+ ```ts
43
+ import { ui } from '@close-by/clay';
44
+
45
+ type Order = { id: string; customer: string; status: string; total: number };
46
+ const orders = ui.state({ list: [] as Order[], filter: '' });
47
+
48
+ ui.auto(() => {
49
+ const shown = orders.list.filter((o) => o.status.includes(orders.filter));
50
+ for (const o of shown) {
51
+ ui.row(() => {
52
+ ui.label(o.customer);
53
+ ui.badge(o.status, { size: 'xs' });
54
+ ui.label(`$${o.total}`);
55
+ }).key(o.id);
56
+ }
57
+ });
58
+
59
+ // From your feed handler. Replacing the array re-runs the block once.
60
+ // Each order keeps its element because its key is unchanged, so a
61
+ // status change is an updateProps on that row's badge.
62
+ orders.list = nextSnapshot;
63
+ ```
64
+
65
+ Keys must be stable. Use the order id, not the array index or a timestamp.
66
+
67
+ ### Dashboard KPI row (`setText`)
68
+
69
+ ```ts
70
+ let revenue: ReturnType<typeof ui.label>;
71
+ let openCount: ReturnType<typeof ui.label>;
72
+
73
+ ui.row(() => {
74
+ revenue = ui.label('$0').classes('text-2xl', 'font-semibold');
75
+ openCount = ui.label('0').classes('text-2xl', 'font-semibold');
76
+ });
77
+
78
+ // Called whenever new metrics arrive. Two updateProps frames, no rebuild.
79
+ function onMetrics(m: { revenue: number; open: number }) {
80
+ revenue.setText(`$${m.revenue.toLocaleString()}`);
81
+ openCount.setText(String(m.open));
82
+ }
83
+ ```
84
+
85
+ ### Live table (`setRows` + `highlightChanges` + `setLoading`)
86
+
87
+ ```ts
88
+ const table = ui.dataTable([], {
89
+ keyField: 'id',
90
+ highlightChanges: true,
91
+ searchable: true,
92
+ });
93
+
94
+ // fetchOrders() is your own data call.
95
+ async function refresh() {
96
+ await table.withLoading(async () => {
97
+ table.setRows(await fetchOrders());
98
+ });
99
+ }
100
+ ```
101
+
102
+ `withLoading` sets loading before `fn` runs and clears it afterwards, so the previous rows stay on screen under a dimmed body until `setRows` runs. Rows whose `id` did not change keep their DOM, and changed or new rows flash.
103
+
104
+ If you want columns to stay fixed across refreshes, pass `columns` explicitly. Otherwise columns are inferred again on each `setRows`.
105
+
106
+ ## Anti-patterns
107
+
108
+ - **Rebuilding a whole section when one value changes.** Wrapping a whole page in `ui.refreshable` and calling `.refresh()` every second to update a clock. Keep a reference to the clock label and call `setText`.
109
+ - **Unkeyed lists that get sorted or filtered.** Without `.key()`, position is the identity. After a sort, the element in slot 3 is reused for whatever item is now third, so focus and typed text stay in the slot instead of following the row.
110
+ - **Creating a new table instead of `setRows`.** A `ui.dataTable(...)` created inside a refreshable on every tick remounts the table and loses sort, scroll, selection, and search. Create the table once and call `setRows`.
111
+ - **Tables without `keyField`.** Rows are stamped `__rowId` by their index, so inserting or re-sorting the source shifts every row's identity. Set `keyField` to your primary key.
112
+ - **Toggling a constructor option by rebuilding.** Recreating a table with `zebra: on` inside an `auto` block. Use the setters instead: `setZebra`, `setDensity`, `setHighlightChanges`.
113
+
114
+ ## Checking it works
115
+
116
+ 1. **Devtools, DOM node.** Open the browser Elements panel and select a row, label, or input. Trigger an update (wait for the next feed tick or change the filter). The same node should still be selected and highlighted. For an input, click into it, let an update arrive, and confirm the caret stays. If the node is replaced, the block was rebuilt.
117
+ 2. **Devtools, WebSocket frames.** Open the Network panel, filter to WS, select the connection, and watch the frames as data arrives. A value change should show `updateProps` with a small payload for one id. `setChildren` should appear only when rows are added, removed, or reordered. If an unchanged list sends `setChildren` on every tick, the child ids are changing: check that `.key()` values are stable and that the block is not creating new parent elements on each run.
118
+
119
+ ## See also
120
+
121
+ - [Concepts: Updates](./concepts.md#updates-settext-vs-refreshable) for the basic `setText` vs `refreshable` split
122
+ - [API reference](./api.md) for `ui.refreshable`, `ui.auto`, `ui.dataTable`, and `Element` methods
123
+ - [Reactive `let`](./reactive-let.md) for the compile-time alternative to hand-written `ui.auto`
124
+ - [WebSocket protocol](./protocol.md) for the patch message formats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@close-by/clay",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
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.17",
41
- "@close-by/clay-core": "0.2.17",
42
- "@close-by/clay-components": "0.2.17",
43
- "@close-by/clay-server": "0.2.17",
40
+ "@close-by/clay-compiler": "0.2.19",
41
+ "@close-by/clay-core": "0.2.19",
42
+ "@close-by/clay-components": "0.2.19",
43
+ "@close-by/clay-server": "0.2.19",
44
44
  "@tailwindcss/cli": "^4.3.3",
45
45
  "tailwindcss": "^4.3.3"
46
46
  }
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  column as columnFactory,
67
67
  container as containerFactory,
68
68
  hero as heroFactory,
69
+ grid as gridFactory,
69
70
  card as cardFactory,
70
71
  dialog as dialogFactory,
71
72
  dialogStack as dialogStackFactory,
@@ -112,6 +113,9 @@ import {
112
113
  list as listFactory,
113
114
  imageCrop as imageCropFactory,
114
115
  gantt as ganttFactory,
116
+ map as mapFactory,
117
+ type MapElement,
118
+ type MapProps,
115
119
  chart as chartNamespace,
116
120
  table as tableFactory,
117
121
  flow as flowFactory,
@@ -242,6 +246,8 @@ import {
242
246
  type LineChartStyleProps,
243
247
  type SparklineProps,
244
248
  type CardProps,
249
+ type GridProps,
250
+ type LayoutProps,
245
251
  type DialogProps,
246
252
  type DialogStackProps,
247
253
  type DialogStackStepOptions,
@@ -343,6 +349,14 @@ import {
343
349
  type GanttDependency,
344
350
  type GanttRange,
345
351
  type GanttItemMovePayload,
352
+ type MapMarkerDatum,
353
+ type MapRouteDatum,
354
+ type MapArcGroup,
355
+ type MapGeoJSONLayer,
356
+ type MapClusterGroup,
357
+ type MapHeatmapLayer,
358
+ type LngLat,
359
+ type MapTone,
346
360
  type FlowProps,
347
361
  type FlowNodeProps,
348
362
  type FlowNodeKind,
@@ -452,6 +466,7 @@ import {
452
466
  timer as timerCore,
453
467
  storage as storageCore,
454
468
  theme as themeCore,
469
+ sound as soundCore,
455
470
  reactive as reactiveCore,
456
471
  state as stateCore,
457
472
  subscribe as subscribeCore,
@@ -471,6 +486,7 @@ import {
471
486
  TimerHandle,
472
487
  type TimerOptions,
473
488
  type ThemeMode,
489
+ type ThemePalette,
474
490
  type DraftStorage,
475
491
  type DraftOptions,
476
492
  type UrlStateOptions,
@@ -512,6 +528,8 @@ export {
512
528
 
513
529
 
514
530
  export type {
531
+ GridProps,
532
+ LayoutProps,
515
533
  ButtonProps,
516
534
  IconButtonProps,
517
535
  ButtonVariant,
@@ -636,6 +654,15 @@ export type {
636
654
  GanttDependency,
637
655
  GanttRange,
638
656
  GanttItemMovePayload,
657
+ MapProps,
658
+ MapMarkerDatum,
659
+ MapRouteDatum,
660
+ MapArcGroup,
661
+ MapGeoJSONLayer,
662
+ MapClusterGroup,
663
+ MapHeatmapLayer,
664
+ LngLat,
665
+ MapTone,
639
666
  FlowProps,
640
667
  FlowNodeProps,
641
668
  FlowNodeKind,
@@ -698,6 +725,7 @@ export type {
698
725
  NotifyType,
699
726
  ToastPosition,
700
727
  ThemeMode,
728
+ ThemePalette,
701
729
  PageMeta,
702
730
  NavFromPagesOptions,
703
731
  PageOptions,
@@ -1173,6 +1201,15 @@ export function gantt(props?: GanttProps): GanttElement {
1173
1201
  return ganttFactory(props);
1174
1202
  }
1175
1203
 
1204
+ /**
1205
+ * Live map (MapLibre, styled to the Clay theme) with markers, routes, arcs,
1206
+ * GeoJSON layers, clusters and a popup. Owns its marker / route / popup /
1207
+ * camera state; setters update in place. See {@link MapProps}.
1208
+ */
1209
+ export function map(props?: MapProps): MapElement {
1210
+ return mapFactory(props);
1211
+ }
1212
+
1176
1213
  /**
1177
1214
  * Interactive flow diagram (`@xyflow/react`). Owns edges + positions;
1178
1215
  * node bodies are Clay trees; drag by card chrome (controls are nodrag);
@@ -1255,33 +1292,43 @@ export function sparkline(props: SparklineProps): Element {
1255
1292
  }
1256
1293
 
1257
1294
  /** Horizontal flex row (`fn` or props-first). Prefer `ui.row`. */
1258
- export function row(fn: () => void, props?: Parameters<typeof rowFactory>[1]): Element;
1259
- export function row(props: Parameters<typeof rowFactory>[0], fn: () => void): Element;
1295
+ export function row(fn: () => void, props?: LayoutProps): Element;
1296
+ export function row(props: LayoutProps, fn: () => void): Element;
1260
1297
  export function row(a: any, b?: any): Element {
1261
1298
  return rowFactory(a, b);
1262
1299
  }
1263
1300
 
1264
1301
  /** Vertical flex column (`fn` or props-first). Prefer `ui.column`. */
1265
- export function column(fn: () => void, props?: Parameters<typeof columnFactory>[1]): Element;
1266
- export function column(props: Parameters<typeof columnFactory>[0], fn: () => void): Element;
1302
+ export function column(fn: () => void, props?: LayoutProps): Element;
1303
+ export function column(props: LayoutProps, fn: () => void): Element;
1267
1304
  export function column(a: any, b?: any): Element {
1268
1305
  return columnFactory(a, b);
1269
1306
  }
1270
1307
 
1271
1308
  /** Layout container (`fn` or props-first). Prefer `ui.container`. */
1272
- export function container(fn: () => void, props?: Parameters<typeof containerFactory>[1]): Element;
1273
- export function container(props: Parameters<typeof containerFactory>[0], fn: () => void): Element;
1309
+ export function container(fn: () => void, props?: LayoutProps): Element;
1310
+ export function container(props: LayoutProps, fn: () => void): Element;
1274
1311
  export function container(a: any, b?: any): Element {
1275
1312
  return containerFactory(a, b);
1276
1313
  }
1277
1314
 
1278
1315
  /** Hero section (`fn` or props-first). Prefer `ui.hero`. */
1279
- export function hero(fn: () => void, props?: Parameters<typeof heroFactory>[1]): Element;
1280
- export function hero(props: Parameters<typeof heroFactory>[0], fn: () => void): Element;
1316
+ export function hero(fn: () => void, props?: LayoutProps): Element;
1317
+ export function hero(props: LayoutProps, fn: () => void): Element;
1281
1318
  export function hero(a: any, b?: any): Element {
1282
1319
  return heroFactory(a, b);
1283
1320
  }
1284
1321
 
1322
+ /**
1323
+ * Responsive grid that fills the width (`fn` or props-first). Prefer `ui.grid`.
1324
+ * `minColWidth` auto-fits columns (default 16rem); `cols` fixes the count. See {@link GridProps}.
1325
+ */
1326
+ export function grid(fn: () => void, props?: GridProps): Element;
1327
+ export function grid(props: GridProps, fn: () => void): Element;
1328
+ export function grid(a: any, b?: any): Element {
1329
+ return gridFactory(a, b);
1330
+ }
1331
+
1285
1332
  /** Card with optional header/footer (`fn` or props-first). Prefer `ui.card`. See {@link CardProps}. */
1286
1333
  export function card(fn: (card: Element) => void, props?: CardProps): Element;
1287
1334
  export function card(props: CardProps, fn: (card: Element) => void): Element;
@@ -1660,6 +1707,9 @@ export const storage = storageCore;
1660
1707
  /** Server-driven appearance (`light` | `dark` | `system`). */
1661
1708
  export const theme = themeCore;
1662
1709
 
1710
+ /** Interface sound cues: `ui.sound.set(on)`, `ui.sound.volume(0..1)`, `ui.sound.play(name)`. */
1711
+ export const sound = soundCore;
1712
+
1663
1713
  /** Proxy that notifies subscribers on property change. Prefer this over importing from `@close-by/clay-core`. */
1664
1714
  export const reactive = reactiveCore;
1665
1715
 
@@ -1907,6 +1957,7 @@ export const ui = {
1907
1957
  list,
1908
1958
  imageCrop,
1909
1959
  gantt,
1960
+ map,
1910
1961
  flow,
1911
1962
  stat,
1912
1963
  dataTable,
@@ -1925,6 +1976,7 @@ export const ui = {
1925
1976
  row,
1926
1977
  column,
1927
1978
  container,
1979
+ grid,
1928
1980
  hero,
1929
1981
  card,
1930
1982
  app,
@@ -1973,6 +2025,7 @@ export const ui = {
1973
2025
  timer,
1974
2026
  storage,
1975
2027
  theme,
2028
+ sound,
1976
2029
  reactive,
1977
2030
  state,
1978
2031
  urlState,