@close-by/clay 0.2.18 → 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 +1 -0
- package/docs/api.md +42 -0
- package/docs/demo-deployment.md +64 -0
- package/docs/examples.md +6 -1
- package/docs/maps.md +214 -0
- package/package.json +5 -5
- package/src/index.ts +35 -0
package/docs/README.md
CHANGED
|
@@ -22,6 +22,7 @@ For coding agents: the repo-root [llms.txt](../llms.txt) is a short map of these
|
|
|
22
22
|
| [Ops patterns](./ops-patterns.md) | Dense consoles: chips, master–detail, feeds, sensitive logs, auth nav |
|
|
23
23
|
| [API reference](./api.md) | `ui.*`, `Element` methods, `reactive`, helpers |
|
|
24
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 |
|
|
25
26
|
| [WebSocket protocol](./protocol.md) | Client ↔ server message formats |
|
|
26
27
|
| [Architecture](./architecture.md) | Packages, data flow, and ownership |
|
|
27
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 |
|
|
@@ -1804,6 +1805,10 @@ onClick: async () => {
|
|
|
1804
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 |
|
|
1805
1806
|
| `ui.theme.getPalette()` | `ThemePalette \| null` | Last palette set, else the one the client reported on connect |
|
|
1806
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 |
|
|
1807
1812
|
| `ui.runJavaScript(code)` | `void` | Eval trusted snippet in the browser |
|
|
1808
1813
|
| `ui.scroll.to(opts)` | `void` | Scroll window (`top` / `bottom` / px) |
|
|
1809
1814
|
| `ui.scroll.intoView(selector, opts?)` | `void` | `Element.scrollIntoView` via selector |
|
|
@@ -1918,6 +1923,7 @@ All layout helpers accept either `(fn, props?)` or `(props, fn)`.
|
|
|
1918
1923
|
| `ui.hero` | Centered hero region |
|
|
1919
1924
|
| `ui.card` | Card; callback receives the card element |
|
|
1920
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) |
|
|
1921
1927
|
|
|
1922
1928
|
#### `ui.app(props, fn)`
|
|
1923
1929
|
|
|
@@ -2016,6 +2022,15 @@ ui.container(() => {
|
|
|
2016
2022
|
}, { centered: true, width: 'md' });
|
|
2017
2023
|
```
|
|
2018
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
|
+
|
|
2019
2034
|
---
|
|
2020
2035
|
|
|
2021
2036
|
## `Element` methods (`@close-by/clay-core`)
|
|
@@ -2183,6 +2198,33 @@ import { button, input /* … */ } from '@close-by/clay-components';
|
|
|
2183
2198
|
|
|
2184
2199
|
---
|
|
2185
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
|
+
|
|
2186
2228
|
## DuckDB (`@close-by/clay-duckdb`)
|
|
2187
2229
|
|
|
2188
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/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** → **Forms & controls** → **Overlays & layout** → **Apps** → **Account** → **ReUI batches**. Order within a group is `pageMeta.order`.
|
|
10
|
+
Each page sets `group` / `groupIcon` in its `pageMeta`, so the sidebar and the home page's catalog share one structure: **Start here** (themes, fill layout, counter, todo, kitchen sink) → **Reactivity** → **Data** → **Maps** (gallery, delivery tracker, store locator, globe, uptime monitor) → **Forms & controls** → **Overlays & layout** → **Apps** → **Account** → **ReUI batches**. Order within a group is `pageMeta.order`.
|
|
11
11
|
|
|
12
12
|
## Catalog
|
|
13
13
|
|
|
@@ -44,6 +44,11 @@ Each page sets `group` / `groupIcon` in its `pageMeta`, so the sidebar and the h
|
|
|
44
44
|
| `/examples/gantt` | `GanttDemo.ts` | `ui.gantt` owned rows/dates/markers/deps, cross-row drag, header double-click markers, `itemMove` / `markerAdd` settle |
|
|
45
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`) |
|
|
46
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` |
|
|
47
52
|
| `/examples/kitchen-sink` | `KitchenSink.ts` | ShadCN catalog preview (client `KitchenSink`) |
|
|
48
53
|
|
|
49
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@close-by/clay",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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.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
|
@@ -113,6 +113,9 @@ import {
|
|
|
113
113
|
list as listFactory,
|
|
114
114
|
imageCrop as imageCropFactory,
|
|
115
115
|
gantt as ganttFactory,
|
|
116
|
+
map as mapFactory,
|
|
117
|
+
type MapElement,
|
|
118
|
+
type MapProps,
|
|
116
119
|
chart as chartNamespace,
|
|
117
120
|
table as tableFactory,
|
|
118
121
|
flow as flowFactory,
|
|
@@ -346,6 +349,14 @@ import {
|
|
|
346
349
|
type GanttDependency,
|
|
347
350
|
type GanttRange,
|
|
348
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,
|
|
349
360
|
type FlowProps,
|
|
350
361
|
type FlowNodeProps,
|
|
351
362
|
type FlowNodeKind,
|
|
@@ -455,6 +466,7 @@ import {
|
|
|
455
466
|
timer as timerCore,
|
|
456
467
|
storage as storageCore,
|
|
457
468
|
theme as themeCore,
|
|
469
|
+
sound as soundCore,
|
|
458
470
|
reactive as reactiveCore,
|
|
459
471
|
state as stateCore,
|
|
460
472
|
subscribe as subscribeCore,
|
|
@@ -642,6 +654,15 @@ export type {
|
|
|
642
654
|
GanttDependency,
|
|
643
655
|
GanttRange,
|
|
644
656
|
GanttItemMovePayload,
|
|
657
|
+
MapProps,
|
|
658
|
+
MapMarkerDatum,
|
|
659
|
+
MapRouteDatum,
|
|
660
|
+
MapArcGroup,
|
|
661
|
+
MapGeoJSONLayer,
|
|
662
|
+
MapClusterGroup,
|
|
663
|
+
MapHeatmapLayer,
|
|
664
|
+
LngLat,
|
|
665
|
+
MapTone,
|
|
645
666
|
FlowProps,
|
|
646
667
|
FlowNodeProps,
|
|
647
668
|
FlowNodeKind,
|
|
@@ -1180,6 +1201,15 @@ export function gantt(props?: GanttProps): GanttElement {
|
|
|
1180
1201
|
return ganttFactory(props);
|
|
1181
1202
|
}
|
|
1182
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
|
+
|
|
1183
1213
|
/**
|
|
1184
1214
|
* Interactive flow diagram (`@xyflow/react`). Owns edges + positions;
|
|
1185
1215
|
* node bodies are Clay trees; drag by card chrome (controls are nodrag);
|
|
@@ -1677,6 +1707,9 @@ export const storage = storageCore;
|
|
|
1677
1707
|
/** Server-driven appearance (`light` | `dark` | `system`). */
|
|
1678
1708
|
export const theme = themeCore;
|
|
1679
1709
|
|
|
1710
|
+
/** Interface sound cues: `ui.sound.set(on)`, `ui.sound.volume(0..1)`, `ui.sound.play(name)`. */
|
|
1711
|
+
export const sound = soundCore;
|
|
1712
|
+
|
|
1680
1713
|
/** Proxy that notifies subscribers on property change. Prefer this over importing from `@close-by/clay-core`. */
|
|
1681
1714
|
export const reactive = reactiveCore;
|
|
1682
1715
|
|
|
@@ -1924,6 +1957,7 @@ export const ui = {
|
|
|
1924
1957
|
list,
|
|
1925
1958
|
imageCrop,
|
|
1926
1959
|
gantt,
|
|
1960
|
+
map,
|
|
1927
1961
|
flow,
|
|
1928
1962
|
stat,
|
|
1929
1963
|
dataTable,
|
|
@@ -1991,6 +2025,7 @@ export const ui = {
|
|
|
1991
2025
|
timer,
|
|
1992
2026
|
storage,
|
|
1993
2027
|
theme,
|
|
2028
|
+
sound,
|
|
1994
2029
|
reactive,
|
|
1995
2030
|
state,
|
|
1996
2031
|
urlState,
|