@close-by/clay 0.2.14 → 0.2.15

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/SKILL.md CHANGED
@@ -62,6 +62,7 @@ ui.label(() => `Count: ${s.count}`);
62
62
  | API | Use when |
63
63
  |-----|----------|
64
64
  | `ui.state(initial)` | Reactive object; writes trigger tracked rebuilds |
65
+ | `ui.urlState(defaults, opts?)` | Like `state`, hydrated from / synced to the query string |
65
66
  | `ui.auto(fn)` | Rebuild a UI block when tracked state changes |
66
67
  | `ui.label(() => …)` / `bindText` | Patch text only (stable tree) |
67
68
  | `bindValue` | Two-way inputs |
@@ -94,7 +95,7 @@ ui.page('/dashboard', () => {
94
95
  ## Do not
95
96
 
96
97
  - Import React, write JSX, or add client components for screens.
97
- - Use `window`, `document`, `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.openExternal`.
98
+ - Use `window`, `document`, `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.getUrlSearch` / `ui.updateUrlSearch`, `ui.openExternal`.
98
99
  - Assume `let` is shared across tabs or users.
99
100
  - Use `ui.ai.*` as an LLM runtime — it is visual primitives only.
100
101
 
package/docs/README.md CHANGED
@@ -15,7 +15,8 @@ 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
- | [Browser APIs](./browser-apis.md) | Prefer `ui.clipboard` / hash / `openExternal` — never `window` in page code |
18
+ | [Browser APIs](./browser-apis.md) | Prefer `ui.clipboard` / hash / search / `openExternal` — never `window` in page code |
19
+ | [URL search](./url-search.md) | `ui.urlState` + query helpers for shareable filters |
19
20
  | [Production & security](./production.md) | Auth, server exposure, rate limits, boot parity |
20
21
  | [Ops patterns](./ops-patterns.md) | Dense consoles: chips, master–detail, feeds, sensitive logs, auth nav |
21
22
  | [API reference](./api.md) | `ui.*`, `Element` methods, `reactive`, helpers |
package/docs/SKILL.md CHANGED
@@ -62,6 +62,7 @@ ui.label(() => `Count: ${s.count}`);
62
62
  | API | Use when |
63
63
  |-----|----------|
64
64
  | `ui.state(initial)` | Reactive object; writes trigger tracked rebuilds |
65
+ | `ui.urlState(defaults, opts?)` | Like `state`, hydrated from / synced to the query string |
65
66
  | `ui.auto(fn)` | Rebuild a UI block when tracked state changes |
66
67
  | `ui.label(() => …)` / `bindText` | Patch text only (stable tree) |
67
68
  | `bindValue` | Two-way inputs |
@@ -94,7 +95,7 @@ ui.page('/dashboard', () => {
94
95
  ## Do not
95
96
 
96
97
  - Import React, write JSX, or add client components for screens.
97
- - Use `window`, `document`, `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.openExternal`.
98
+ - Use `window`, `document`, `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.getUrlSearch` / `ui.updateUrlSearch`, `ui.openExternal`.
98
99
  - Assume `let` is shared across tabs or users.
99
100
  - Use `ui.ai.*` as an LLM runtime — it is visual primitives only.
100
101
 
package/docs/api.md CHANGED
@@ -1793,6 +1793,11 @@ onClick: async () => {
1793
1793
  | `ui.clipboard(content)` | `void` | Write text to clipboard (prefer over `navigator.clipboard`) |
1794
1794
  | `ui.getUrlHash()` | `string` | Current hash without `#` (from hello / last set) |
1795
1795
  | `ui.setUrlHash(hash)` | `void` | Set `location.hash` on the client |
1796
+ | `ui.getUrlSearch()` | `string` | Current search without `?` (from hello / last set) |
1797
+ | `ui.getUrlSearchParams()` | `URLSearchParams` | Mutable parse of `getUrlSearch()` |
1798
+ | `ui.setUrlSearch(search, opts?)` | `void` | Set `location.search` (`opts.mode`: `replace` \| `push`) |
1799
+ | `ui.updateUrlSearch(patch, opts?)` | `void` | Merge query keys; `null`/`''` deletes |
1800
+ | `ui.urlState(defaults, opts?)` | reactive object | `ui.state` + hydrate/sync query string (prefer this) |
1796
1801
  | `ui.openExternal(url)` | `void` | `window.open` in a new tab (`noopener`) |
1797
1802
  | `ui.theme.set(mode)` | `void` | Push `light` \| `dark` \| `system` to this client |
1798
1803
  | `ui.theme.get()` | `ThemeMode \| null` | Last value set on this session |
@@ -8,6 +8,7 @@ Page builders and event handlers run on the **server** (Bun), not in the browser
8
8
  |------|-----|
9
9
  | Copy text | `ui.clipboard(text)` |
10
10
  | Read / set URL hash | `ui.getUrlHash()` / `ui.setUrlHash(hash)` |
11
+ | Read / set URL search (query) | `ui.urlState(defaults)` (prefer) or `ui.getUrlSearch` / `ui.updateUrlSearch` |
11
12
  | Open a URL in a new tab | `ui.openExternal(url)` |
12
13
  | In-app route change | `ui.navigate(path)` |
13
14
  | Scroll | `ui.scroll.to` / `ui.scroll.intoView` |
@@ -23,17 +24,23 @@ ui.button('Copy id', {
23
24
 
24
25
  const focus = ui.getUrlHash(); // from last hello / setUrlHash
25
26
  ui.setUrlHash(traceId);
27
+
28
+ const partner = ui.getUrlSearchParams().get('partner');
29
+ ui.updateUrlSearch({ partner: 'UBER', page: null }); // omit empty defaults
26
30
  ui.openExternal('https://signoz.example/trace/' + traceId);
27
31
  ```
28
32
 
29
33
  `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
34
 
35
+ `getUrlSearch` returns the query string **without** a leading `?` (same hydration / write path). Prefer **`ui.urlState`** for filter objects; use `ui.updateUrlSearch` for one-off merges. See [URL search](./url-search.md).
36
+
31
37
  ## Don’t
32
38
 
33
39
  ```ts
34
40
  // Wrong — server has no clipboard / location
35
41
  await navigator.clipboard.writeText(value);
36
42
  window.location.hash = traceId;
43
+ window.location.search = '?partner=UBER';
37
44
  window.open(url, '_blank');
38
45
  ```
39
46
 
@@ -61,5 +68,5 @@ With **`clay --reactive-let`**, transformed files that import known-fragile CJS
61
68
  ## Related
62
69
 
63
70
  - Helpers table: [API — dialogs and feedback](./api.md#dialogs-and-feedback)
64
- - Protocol: `clipboard`, `setUrlHash`, `openExternal` in [WebSocket protocol](./protocol.md)
71
+ - Protocol: `clipboard`, `setUrlHash`, `setUrlSearch`, `openExternal` in [WebSocket protocol](./protocol.md)
65
72
  - Phase 1 reactivity (preferred over DOM hacks): [reactive-let](./reactive-let.md)
package/docs/llms.txt CHANGED
@@ -9,7 +9,7 @@ Notes for agents:
9
9
  - Public API and wire protocol are camelCase (`onClick`, `bindValue`, `setText`).
10
10
  - Page builders run once per WebSocket session (per browser tab). `let` is per-tab, not shared.
11
11
  - Update existing nodes with `setText` / `setValue` / bindings. Prefer `ui.state` + `ui.auto` (+ `ui.timer`) for async/live UIs; `ui.refreshable` for simple one-shot rebuilds. Shared or persisted state: `ui.storage` (`tab` / `user` / `app`).
12
- - Never use `window` / `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.openExternal`.
12
+ - Never use `window` / `navigator` in page code — use `ui.clipboard`, `ui.setUrlHash`, `ui.getUrlSearch` / `ui.updateUrlSearch`, `ui.openExternal`.
13
13
  - Runtime is Bun. In this repo: `bun run build:client`, then `bun run demo` (http://localhost:4000) or `bun run clay hello.ts`.
14
14
  - `ui.ai.*` is visual primitives only — no model or streaming runtime.
15
15
  - Agent skill (Cursor / compatible tools): `@close-by/clay/SKILL.md` — copy to `.cursor/skills/clay/SKILL.md` or read from `node_modules` when installed.
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 `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`.
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
 
@@ -163,6 +163,14 @@ Client writes `content` with `navigator.clipboard.writeText` (shows an error toa
163
163
 
164
164
  Client sets `location.hash` via `history.replaceState` (`hash` has no leading `#`; empty string clears). Hydrated the other way on `hello.hash`. Prefer `ui.setUrlHash` / `ui.getUrlHash`.
165
165
 
166
+ ### `setUrlSearch`
167
+
168
+ ```json
169
+ { "op": "setUrlSearch", "search": "tab=orders&partner=UBER", "mode": "replace" }
170
+ ```
171
+
172
+ Client sets `location.search` via `history.replaceState` or `pushState` (`mode`, default `replace`). `search` has no leading `?`; empty string clears. Pathname and hash are preserved. Hydrated the other way on `hello.search`. Prefer `ui.setUrlSearch` / `ui.updateUrlSearch` / `ui.getUrlSearch`. See [URL search](./url-search.md).
173
+
166
174
  ### `openExternal`
167
175
 
168
176
  ```json
@@ -0,0 +1,97 @@
1
+ # URL search (query string) helpers
2
+
3
+ Ops consoles often need **shareable filter state** in the address bar
4
+ (`?tab=orders&partner=UBER&from=2026-01-01`). Page builders run on the server, so
5
+ they must not read `window.location`. Clay mirrors the existing hash helpers.
6
+
7
+ ## Prefer `ui.urlState`
8
+
9
+ Like `ui.state`, but hydrated from the query string and write-through synced:
10
+
11
+ ```ts
12
+ import { ui } from '@close-by/clay';
13
+
14
+ ui.page('/lastmile', () => {
15
+ const filters = ui.urlState({
16
+ tab: 'dashboard',
17
+ partner: '',
18
+ from: '',
19
+ page: 1,
20
+ });
21
+
22
+ // ?partner=UBER&page=2 — defaults (dashboard / '' / 1) are omitted
23
+ filters.partner = 'UBER';
24
+ filters.page = 2;
25
+
26
+ ui.select({
27
+ label: 'Partner',
28
+ value: filters.partner || '__all__',
29
+ options: [
30
+ { value: '__all__', label: 'All' },
31
+ { value: 'UBER', label: 'Uber Eats' },
32
+ ],
33
+ onChange: (v) => {
34
+ filters.partner = v === '__all__' ? '' : v;
35
+ },
36
+ });
37
+ });
38
+ ```
39
+
40
+ | Option | Default | Role |
41
+ |--------|---------|------|
42
+ | `omitDefaults` | `true` | Drop keys whose value equals the default |
43
+ | `mode` | `'replace'` | History write (`'push'` when Back should undo) |
44
+ | `keys` | identity | Remap state keys → query names (`{ page: 'p' }`) |
45
+
46
+ Works with `ui.auto` / `bindValue` the same way as `ui.state`.
47
+
48
+ ## Low-level helpers
49
+
50
+ Escape hatches when you need a one-off read/write without a reactive object:
51
+
52
+ | Helper | Role |
53
+ |--------|------|
54
+ | `ui.getUrlSearch()` | Current search **without** leading `?` (from `hello` / last set) |
55
+ | `ui.getUrlSearchParams()` | Mutable `URLSearchParams` copy of that string |
56
+ | `ui.setUrlSearch(search, opts?)` | Replace the whole query string (empty clears) |
57
+ | `ui.updateUrlSearch(patch, opts?)` | Merge keys; `null` / `undefined` / `''` deletes |
58
+
59
+ `opts.mode` is `'replace'` (default) or `'push'`. Prefer **replace** for filter
60
+ tweaks so Back is not flooded.
61
+
62
+ ## Protocol
63
+
64
+ **Client → server** (`hello`):
65
+
66
+ ```json
67
+ { "op": "hello", "path": "/lastmile", "hash": "", "search": "tab=orders&partner=UBER" }
68
+ ```
69
+
70
+ **Server → client**:
71
+
72
+ ```json
73
+ { "op": "setUrlSearch", "search": "tab=orders&partner=UBER", "mode": "replace" }
74
+ ```
75
+
76
+ Client writes `pathname + ?search + hash` via `history.replaceState` / `pushState`.
77
+ Hash is preserved (same as `setUrlHash` preserves search).
78
+
79
+ ## Path vs query
80
+
81
+ | Concern | Prefer |
82
+ |---------|--------|
83
+ | Resource identity | Path (`/orders/:id`) via `ui.page` / `ui.navigate` |
84
+ | Active tab, filters, sort, page | **Query string** (`ui.urlState`) |
85
+ | In-page focus / trace id | Hash (`ui.setUrlHash`) when you already use it |
86
+
87
+ ## Limits / follow-ups
88
+
89
+ - Hydration runs on each `hello` (connect, reconnect, SPA path change). Pure
90
+ Back/Forward that only changes the query does **not** remount yet — same
91
+ limitation as hash today. A future `urlchange` client event (or popstate →
92
+ hello) would close that gap and re-hydrate `urlState` in place.
93
+
94
+ ## Related
95
+
96
+ - [Browser APIs](./browser-apis.md)
97
+ - [WebSocket protocol](./protocol.md) — `hello.search`, `setUrlSearch`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@close-by/clay",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
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.14",
41
- "@close-by/clay-core": "0.2.14",
42
- "@close-by/clay-components": "0.2.14",
43
- "@close-by/clay-server": "0.2.14",
40
+ "@close-by/clay-compiler": "0.2.15",
41
+ "@close-by/clay-core": "0.2.15",
42
+ "@close-by/clay-components": "0.2.15",
43
+ "@close-by/clay-server": "0.2.15",
44
44
  "@tailwindcss/cli": "^4.3.3",
45
45
  "tailwindcss": "^4.3.3"
46
46
  }
package/src/index.ts CHANGED
@@ -443,6 +443,10 @@ import {
443
443
  runJavaScript as runJavaScriptCore,
444
444
  getUrlHash as getUrlHashCore,
445
445
  setUrlHash as setUrlHashCore,
446
+ getUrlSearch as getUrlSearchCore,
447
+ getUrlSearchParams as getUrlSearchParamsCore,
448
+ setUrlSearch as setUrlSearchCore,
449
+ updateUrlSearch as updateUrlSearchCore,
446
450
  openExternal as openExternalCore,
447
451
  scroll as scrollCore,
448
452
  timer as timerCore,
@@ -451,6 +455,7 @@ import {
451
455
  reactive as reactiveCore,
452
456
  state as stateCore,
453
457
  subscribe as subscribeCore,
458
+ urlState as urlStateCore,
454
459
  auto as autoCore,
455
460
  draft as draftCore,
456
461
  validate as validateCore,
@@ -468,6 +473,8 @@ import {
468
473
  type ThemeMode,
469
474
  type DraftStorage,
470
475
  type DraftOptions,
476
+ type UrlStateOptions,
477
+ type UrlStateValue,
471
478
  } from '@close-by/clay-core';
472
479
  import {
473
480
  ClayServer,
@@ -1593,6 +1600,35 @@ export function setUrlHash(hash: string): void {
1593
1600
  setUrlHashCore(hash);
1594
1601
  }
1595
1602
 
1603
+ /** URL search without `?` (from hello / last `setUrlSearch`). Prefer over `window.location`. */
1604
+ export function getUrlSearch(): string {
1605
+ return getUrlSearchCore();
1606
+ }
1607
+
1608
+ /** Parse {@link getUrlSearch} as `URLSearchParams` (mutable copy). */
1609
+ export function getUrlSearchParams(): URLSearchParams {
1610
+ return getUrlSearchParamsCore();
1611
+ }
1612
+
1613
+ /** Set the browser query string. Prefer over `window.history` / `location.search`. */
1614
+ export function setUrlSearch(
1615
+ search: string,
1616
+ opts?: { mode?: 'replace' | 'push' },
1617
+ ): void {
1618
+ setUrlSearchCore(search, opts);
1619
+ }
1620
+
1621
+ /**
1622
+ * Merge keys into the current query string.
1623
+ * `null` / `undefined` / `''` deletes the key.
1624
+ */
1625
+ export function updateUrlSearch(
1626
+ patch: Record<string, string | number | boolean | null | undefined>,
1627
+ opts?: { mode?: 'replace' | 'push' },
1628
+ ): void {
1629
+ updateUrlSearchCore(patch, opts);
1630
+ }
1631
+
1596
1632
  /** Open a URL in a new tab. Prefer over `runJavaScript('window.open…')`. */
1597
1633
  export function openExternal(url: string): void {
1598
1634
  openExternalCore(url);
@@ -1630,6 +1666,18 @@ export const reactive = reactiveCore;
1630
1666
  /** Alias for `reactive` — NiceGUI-ish mutable page state. Prefer `ui.state`. */
1631
1667
  export const state = stateCore;
1632
1668
 
1669
+ /**
1670
+ * Like `ui.state`, but hydrated from the query string and write-through synced
1671
+ * via `ui.updateUrlSearch`. Prefer for shareable filters / tabs.
1672
+ * See {@link UrlStateOptions}.
1673
+ */
1674
+ export function urlState<T extends Record<string, UrlStateValue>>(
1675
+ defaults: T,
1676
+ opts?: UrlStateOptions<T>,
1677
+ ): T {
1678
+ return urlStateCore(defaults, opts);
1679
+ }
1680
+
1633
1681
  /** Listen for a reactive property change. Prefer this over importing from `@close-by/clay-core`. */
1634
1682
  export const subscribe = subscribeCore;
1635
1683
 
@@ -1915,6 +1963,10 @@ export const ui = {
1915
1963
  clipboard,
1916
1964
  getUrlHash,
1917
1965
  setUrlHash,
1966
+ getUrlSearch,
1967
+ getUrlSearchParams,
1968
+ setUrlSearch,
1969
+ updateUrlSearch,
1918
1970
  openExternal,
1919
1971
  runJavaScript,
1920
1972
  scroll,
@@ -1923,6 +1975,7 @@ export const ui = {
1923
1975
  theme,
1924
1976
  reactive,
1925
1977
  state,
1978
+ urlState,
1926
1979
  subscribe,
1927
1980
  draft,
1928
1981
  validate,