@close-by/clay 0.2.9 → 0.2.11

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
@@ -116,7 +116,7 @@ Resolve docs programmatically: `resolveClayDocsDir()` from `@close-by/clay/docs-
116
116
 
117
117
  ## Optional compile-time reactive `let`
118
118
 
119
- Opt-in via `// @clay-reactive` + `clay app.ts --reactive-let`. Prefer explicit `ui.state` + `ui.auto` for production/async-heavy apps.
119
+ Auto-registers when pages use `// @clay-reactive` / `"use reactive"` (`clay` / `loadPages` default). Prefer explicit `ui.state` + `ui.auto` for production/async-heavy apps; force with `--reactive-let` if needed.
120
120
 
121
121
  ## Package map (monorepo)
122
122
 
package/docs/SKILL.md CHANGED
@@ -116,7 +116,7 @@ Resolve docs programmatically: `resolveClayDocsDir()` from `@close-by/clay/docs-
116
116
 
117
117
  ## Optional compile-time reactive `let`
118
118
 
119
- Opt-in via `// @clay-reactive` + `clay app.ts --reactive-let`. Prefer explicit `ui.state` + `ui.auto` for production/async-heavy apps.
119
+ Auto-registers when pages use `// @clay-reactive` / `"use reactive"` (`clay` / `loadPages` default). Prefer explicit `ui.state` + `ui.auto` for production/async-heavy apps; force with `--reactive-let` if needed.
120
120
 
121
121
  ## Package map (monorepo)
122
122
 
package/docs/api.md CHANGED
@@ -1682,17 +1682,18 @@ ui.scrollArea(
1682
1682
 
1683
1683
  #### `ui.viewportEnter(fn, props?)` / `ui.viewportEnter(props, fn)`
1684
1684
 
1685
- IntersectionObserver wrapper. Fires `onEnter` when the element intersects the viewport — default **once**.
1685
+ IntersectionObserver wrapper. Fires `onEnter` when the element intersects the scroll root — default **once**, root = browser viewport.
1686
1686
 
1687
1687
  ```typescript
1688
1688
  ui.viewportEnter(
1689
1689
  {
1690
1690
  once: true,
1691
+ root: 'nearest-scroll', // overflow panel / scroll-area, not only the window
1691
1692
  rootMargin: '80px',
1692
- onEnter: () => void loadSection(),
1693
+ onEnter: () => void loadMore(),
1693
1694
  },
1694
1695
  () => {
1695
- ui.label('Lazy block');
1696
+ ui.label('Load more…');
1696
1697
  },
1697
1698
  );
1698
1699
  ```
@@ -1702,9 +1703,12 @@ ui.viewportEnter(
1702
1703
  | `className` | `string` | |
1703
1704
  | `onEnter` | `() => void \| Promise<void>` | — client emits `enter` |
1704
1705
  | `once` | `boolean` | `true` |
1706
+ | `root` | `'viewport' \| 'nearest-scroll' \| string` | `'viewport'` |
1705
1707
  | `rootMargin` | `string` | `'0px'` |
1706
1708
  | `threshold` | `number \| number[]` | `0` |
1707
1709
 
1710
+ `nearest-scroll` walks ancestors for overflow scroll or `[data-slot="scroll-area-viewport"]`. A CSS selector uses `closest` / `querySelector`.
1711
+
1708
1712
  #### `ui.keybind(props)`
1709
1713
 
1710
1714
  Headless keyboard chord listener. Renders nothing; while the node is in the tree the client listens on `window` `keydown`, matches a chord, and emits `press` → `onPress`.
package/docs/boot.md CHANGED
@@ -90,7 +90,11 @@ Legacy `_clay-reload-*.ts` files under `pages/` (pre-0.2.5) should be deleted; m
90
90
 
91
91
  ## Reactive-let
92
92
 
93
- The CLI leaves the compile-time `let` transform **off** unless you pass `--reactive-let`. Library mode never registers the Bun loader unless you call `registerReactiveLetPlugin()` yourself. Prefer Phase 1 `ui.state` / `ui.auto` for real apps — see [reactive-let](./reactive-let.md).
93
+ The CLI **auto-registers** the compile-time `let` transform when any loaded page contains `// @clay-reactive` or `"use reactive"`. Force on with `--reactive-let`, force off with `--no-reactive-let`.
94
+
95
+ `ui.loadPages` does the same scan-before-import in library mode (default `reactiveLet: 'auto'`), so apps no longer need a manual `registerReactiveLetPlugin()` before `loadPages`. Prefer Phase 1 `ui.state` / `ui.auto` for dense/async production apps until you are ready for the pragma path — see [reactive-let](./reactive-let.md).
96
+
97
+ `configureRun` Tailwind options (`appendCss`, custom `content`, …) are **merged** with CLI defaults; the CLI no longer replaces the whole `tailwind` object.
94
98
 
95
99
  ## Checklist (avoid drift)
96
100
 
package/docs/elements.md CHANGED
@@ -31,6 +31,13 @@ Each factory creates an `Element` with a wire `type` string. The React client ma
31
31
  | `field` | `field` | Label + description + error wrapper for one control |
32
32
  | `nativeSelect` | `nativeSelect` | Styled native `<select>` |
33
33
  | `navigationMenu` | `navigationmenu` (+ sub/link children) | Top nav mega-menu |
34
+ | `inputGroup` | `inputGroup` | Input with prefix/suffix addons |
35
+ | `toggle` | `toggle` | Pressed toolbar toggle |
36
+ | `descriptionList` | `descriptionList` | Key-value detail rows |
37
+ | `staticTable` | `staticTable` | Read-only HTML table |
38
+ | `aspectRatio` | `aspectRatio` | Fixed-ratio child wrapper |
39
+ | `itemList` | `itemList` | Settings / notification rows |
40
+ | `checkboxGroup` | `checkboxGroup` | Multi-select checkboxes |
34
41
  | `notice` | `notice` | Dismissible app banner |
35
42
  | `eventCalendar` | `eventCalendar` | Month grid + day event list (`events` API → wire `items`) |
36
43
  | `timeline` | `timeline` | Vertical or horizontal event timeline |
@@ -84,7 +91,7 @@ Each factory creates an `Element` with a wire `type` string. The React client ma
84
91
  | `command` | `command` (+ `commandgroup` / `commanditem` / `commandseparator`) | Command palette (`mode: 'dialog' \| 'inline'`; dialog owns `open`) |
85
92
  | `resizable` | `resizable` (+ `resizablepanel` / `resizablehandle`) | `react-resizable-panels` |
86
93
  | `scrollArea` | `scrollarea` | ShadCN `ScrollArea` (`onNearEnd` for infinite scroll) |
87
- | `viewportEnter` | `viewportEnter` | IntersectionObserver wrapper (`onEnter`, `once?`) |
94
+ | `viewportEnter` | `viewportEnter` | IntersectionObserver (`onEnter`, `once?`, `root?`) |
88
95
  | `keybind` | `keybind` | Headless `window` `keydown` chord listener (`return null`) |
89
96
  | `kbd` | `kbd` | Display-only chord glyphs (`Kbd` / `KbdGroup`; same tokens as `keybind`) |
90
97
  | `markdown` | `markdown` | Client `marked` + DOMPurify |
package/docs/examples.md CHANGED
@@ -18,7 +18,9 @@ After `bun run build:client && bun run demo` (or `bun run demo:cli`), open http:
18
18
  | `/examples/dashboard` | `Dashboard.ts` | `stat`, `areaChart`, full-chrome `dataTable` (views, editors, detail drawer) |
19
19
  | `/examples/datatable` | `DataTableDemo.ts` | Multi-sort (Shift+click / badges), footer aggregates + a11y hints, column pin, remote filter/sort + pagination; density/zebra, editors (Enter/Esc + focus restore), resize, virtualization (≥40, coexists with reorder via windowed drop targets); facets, bulk, collapse/expand, loading/empty; `ui.table` sugar |
20
20
  | `/examples/composed` | `ComposedDemo.ts` | ReUI batch 1: `ui.timeline`, `ui.stepper`, `ui.dateRange`, `ui.feedList`/`ui.feedRow`, `ui.sparkline` |
21
+ | `/examples/ops-console` | `OpsConsoleDemo.ts` | Dogfood ops console: `filterBar` → `feedList` → `descriptionList` / `staticTable` (batches 1–7 in one surface) |
21
22
  | `/examples/reui-batches` | `ReuiBatchesDemo.ts` | ReUI batches 3–6: `buttonGroup`, `empty`, `pagination`, `filterBar`, form inputs, `navigationMenu`, `notice`, `stat`+sparkline, `eventCalendar`, datatable `showFilterChips` |
23
+ | `/examples/reui-batch-7` | `ReuiBatch7Demo.ts` | ReUI batch 7: `inputGroup`, `toggle`, `descriptionList`, `staticTable`, `aspectRatio`, `itemList`, `checkboxGroup` |
22
24
  | `/examples/charts` | `ChartDemo.ts` | `ui.chart.*`, `ui.sparkline`, composed/scatter charts |
23
25
  | `/examples/slider-demo` | `SliderDemo.ts` | Slider, checkbox, select + bindings |
24
26
  | `/examples/feedback` | `FeedbackDemo.ts` | Alerts, progress, timer, `ui.theme`, `ui.runJavaScript` / `ui.scroll` |
@@ -2,6 +2,8 @@
2
2
 
3
3
  Dense internal tools (live feeds, master–detail, logs/traces) push Clay past card dashboards. Prefer Phase 1 reactivity ([Concepts](./concepts.md#canonical-recipe-state--auto--timer)) and the helpers below.
4
4
 
5
+ **Runnable dogfood:** `/examples/ops-console` (`OpsConsoleDemo.ts`) — one Orders-shaped surface using `filterBar`, `inputGroup`, `feedList`/`feedRow`, `descriptionList`, `staticTable`, `pagination`, `empty`, `notice`, and `toggle`. Use that page to find API friction before adding more wire types.
6
+
5
7
  ## Status chips
6
8
 
7
9
  ```ts
@@ -154,6 +156,33 @@ ui.auto(() => {
154
156
 
155
157
  For **`ui.dataTable`**, pass `showFilterChips: true` to surface active search + column filters as removable chips in the table chrome (`views` remain the saved-view tabs API).
156
158
 
159
+ ## Infinite scroll / load-more
160
+
161
+ **Blessed pattern for overflow feeds** (master–detail panels): `ui.viewportEnter` with `root: 'nearest-scroll'`. Remount the sentinel while a page is in flight if you need to re-arm after `once: true`:
162
+
163
+ ```ts
164
+ ui.auto(() => {
165
+ if (live.loadingMore) {
166
+ ui.label('Loading…').classes('text-xs text-muted-foreground p-2');
167
+ return;
168
+ }
169
+ if (!live.hasMore) return;
170
+ ui.viewportEnter(
171
+ {
172
+ once: true,
173
+ root: 'nearest-scroll',
174
+ rootMargin: '120px',
175
+ onEnter: () => void loadNextPage(),
176
+ },
177
+ () => {
178
+ ui.label(' ').classes('h-1');
179
+ },
180
+ );
181
+ });
182
+ ```
183
+
184
+ Alternative when you already wrap the list in **`ui.scrollArea`**: use `onNearEnd` on the scroll area (scroll-root is built-in). Prefer one pattern per surface — `viewportEnter` + `nearest-scroll` does **not** require `scrollArea`.
185
+
157
186
  ## Logs / traces — sensitive bodies
158
187
 
159
188
  Structured logs often contain API keys or partner tokens. Scrubbing is the **app’s** job; Clay can still reduce shoulder-surfing:
@@ -75,17 +75,17 @@ Phase 1 remains the **escape hatch** — copy patterns from [`ReactiveLetOrders.
75
75
 
76
76
  Package `@close-by/clay-compiler` rewrites a **documented subset** of `let` into Phase 1 APIs.
77
77
 
78
- ### Opt-in (required)
78
+ ### Opt-in
79
79
 
80
- Both of:
81
-
82
- 1. **CLI / loader:** pass `--reactive-let` to `clay` (or call `registerReactiveLetPlugin()` yourself). Off by default.
83
- 2. **Source:** one of:
80
+ 1. **Source:** one of:
84
81
  - File pragma: `// @clay-reactive` (near the top of the file) — applies to **`ui.page` / `page` callbacks only**
85
82
  - Block directive as the first statement: `"use reactive";` — required for nested helpers / `ui.column(() => …)` lets
83
+ 2. **Loader:** by default, `clay` and `ui.loadPages` **auto-register** the Bun plugin when any scanned page has the pragma / directive. Force with `--reactive-let` / `loadPages(dir, { reactiveLet: true })`; disable with `--no-reactive-let` / `{ reactiveLet: false }`. Manual `registerReactiveLetPlugin()` is still fine (and idempotent).
86
84
 
87
85
  `ui.page(...)` alone does **not** opt in. Module-level helpers (`function mergeUnits() { … }`) and nested `function` / arrow bodies do **not** inherit the file pragma — use `"use reactive";` inside the page callback or a nested site when needed.
88
86
 
87
+ **Hard fail:** a local that shadows lifted state and whose initializer reads that name (`const detail = detail` or `const detail = rows.find(… detail …)`) **refuses to emit** — rename the local. Silent `undefined` after fetch was a hub dogfood failure mode.
88
+
89
89
  ### Who wraps `ui.auto`?
90
90
 
91
91
  Compile-time region analysis **does not** see into nested function bodies. Authors must still wrap rebuild regions when UI is built from helpers or widget callbacks:
@@ -111,7 +111,8 @@ Inline UI in widget callbacks (`ui.label(expr)`, `ui.icon(…, { className: expr
111
111
  | `units.filter(u => { … })` reading lifted state | Plain JS (no `ui.auto` inside filter) | Nothing |
112
112
  | `onClick` / `onChange` / `ui.timer` handler | Not a build region | Normal JS |
113
113
  | `const live = ui.state({…})` (no pragma) | Unchanged | Explicit `ui.auto` (Phase 1) |
114
- | Local `const detail = …` shadows lifted `let detail` | Renames in emit + compile warning | Prefer distinct names |
114
+ | Local `const detail = …` shadows lifted `let detail` (no self-read) | Renames in emit + compile warning | Prefer distinct names |
115
+ | Local `const detail = detail` / initializer reads `detail` | **Hard error** — transform refuses to emit | Rename the local |
115
116
 
116
117
  ### What transforms
117
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@close-by/clay",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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.9",
41
- "@close-by/clay-core": "0.2.9",
42
- "@close-by/clay-components": "0.2.9",
43
- "@close-by/clay-server": "0.2.9",
40
+ "@close-by/clay-compiler": "0.2.11",
41
+ "@close-by/clay-core": "0.2.11",
42
+ "@close-by/clay-components": "0.2.11",
43
+ "@close-by/clay-server": "0.2.11",
44
44
  "@tailwindcss/cli": "^4.3.3",
45
45
  "tailwindcss": "^4.3.3"
46
46
  }
package/src/index.ts CHANGED
@@ -43,6 +43,13 @@ import {
43
43
  notice as noticeFactory,
44
44
  eventCalendar as eventCalendarFactory,
45
45
  stat as statFactory,
46
+ inputGroup as inputGroupFactory,
47
+ toggleControl as toggleControlFactory,
48
+ descriptionList as descriptionListFactory,
49
+ staticTable as staticTableFactory,
50
+ aspectRatio as aspectRatioFactory,
51
+ itemList as itemListFactory,
52
+ checkboxGroup as checkboxGroupFactory,
46
53
  dataTable as dataTableFactory,
47
54
  areaChart as areaChartFactory,
48
55
  barChart as barChartFactory,
@@ -166,6 +173,19 @@ import {
166
173
  type NoticeVariant,
167
174
  type EventCalendarProps,
168
175
  type EventCalendarEvent,
176
+ type InputGroupProps,
177
+ type ToggleControlProps,
178
+ type ToggleControlVariant,
179
+ type ToggleControlSize,
180
+ type DescriptionListProps,
181
+ type DescriptionListItem,
182
+ type StaticTableProps,
183
+ type StaticTableColumn,
184
+ type AspectRatioProps,
185
+ type ItemListProps,
186
+ type ItemListEntry,
187
+ type CheckboxGroupProps,
188
+ type CheckboxGroupOption,
169
189
  type StatItem,
170
190
  type DataTableProps,
171
191
  type DataTableAction,
@@ -238,6 +258,7 @@ import {
238
258
  type ResizableHandleProps,
239
259
  type ScrollAreaProps,
240
260
  type ViewportEnterProps,
261
+ type ViewportEnterRoot,
241
262
  type KeybindProps,
242
263
  type KbdProps,
243
264
  type BreadcrumbItem,
@@ -264,7 +285,6 @@ import {
264
285
  type HtmlProps,
265
286
  type ImageProps,
266
287
  type IframeProps,
267
- type ViewportEnterProps,
268
288
  type UploadProps,
269
289
  type UploadVariant,
270
290
  type UploadedFile,
@@ -551,6 +571,7 @@ export type {
551
571
  ImageProps,
552
572
  IframeProps,
553
573
  ViewportEnterProps,
574
+ ViewportEnterRoot,
554
575
  UploadProps,
555
576
  UploadVariant,
556
577
  UploadedFile,
@@ -972,6 +993,43 @@ export function eventCalendar(props?: EventCalendarProps): Element {
972
993
  return eventCalendarFactory(props);
973
994
  }
974
995
 
996
+ /** Input with prefix/suffix addons. See {@link InputGroupProps}. */
997
+ export function inputGroup(props?: InputGroupProps): Element {
998
+ return inputGroupFactory(props);
999
+ }
1000
+
1001
+ /** Toolbar pressed toggle. See {@link ToggleControlProps}. */
1002
+ export function toggle(props?: ToggleControlProps): Element {
1003
+ return toggleControlFactory(props);
1004
+ }
1005
+
1006
+ /** Key-value detail rows. See {@link DescriptionListProps}. */
1007
+ export function descriptionList(props: DescriptionListProps): Element {
1008
+ return descriptionListFactory(props);
1009
+ }
1010
+
1011
+ /** Read-only HTML table. See {@link StaticTableProps}. */
1012
+ export function staticTable(props: StaticTableProps): Element {
1013
+ return staticTableFactory(props);
1014
+ }
1015
+
1016
+ /** Fixed aspect ratio wrapper. See {@link AspectRatioProps}. */
1017
+ export function aspectRatio(fn: () => void, props?: AspectRatioProps): Element;
1018
+ export function aspectRatio(props: AspectRatioProps, fn: () => void): Element;
1019
+ export function aspectRatio(a: any, b?: any): Element {
1020
+ return aspectRatioFactory(a, b);
1021
+ }
1022
+
1023
+ /** Settings / notification list rows. See {@link ItemListProps}. */
1024
+ export function itemList(props: ItemListProps): Element {
1025
+ return itemListFactory(props);
1026
+ }
1027
+
1028
+ /** Multi-select checkbox group. See {@link CheckboxGroupProps}. */
1029
+ export function checkboxGroup(props: CheckboxGroupProps): Element {
1030
+ return checkboxGroupFactory(props);
1031
+ }
1032
+
975
1033
  /** Render markdown to HTML. Prefer `ui.markdown`. See {@link MarkdownProps}. */
976
1034
  export function markdown(text?: string, props?: MarkdownProps): Element {
977
1035
  return markdownFactory(text, props);
@@ -1741,6 +1799,13 @@ export const ui = {
1741
1799
  navigationMenu,
1742
1800
  notice,
1743
1801
  eventCalendar,
1802
+ inputGroup,
1803
+ toggle,
1804
+ descriptionList,
1805
+ staticTable,
1806
+ aspectRatio,
1807
+ itemList,
1808
+ checkboxGroup,
1744
1809
  markdown,
1745
1810
  html,
1746
1811
  image,
package/src/pages.ts CHANGED
@@ -7,6 +7,10 @@ import {
7
7
  type PageFn,
8
8
  } from '@close-by/clay-core';
9
9
  import { warnClayPageIssues } from '@close-by/clay-compiler';
10
+ import {
11
+ ensureReactiveLetPluginForPaths,
12
+ registerReactiveLetPlugin,
13
+ } from '@close-by/clay-compiler/plugin';
10
14
  import type { AppNavItem } from '@close-by/clay-components';
11
15
 
12
16
  /**
@@ -109,11 +113,26 @@ function shouldSkipPageFile(relativePath: string): boolean {
109
113
  * Clears prior registrations first, then fresh-imports each module (cache-busted)
110
114
  * so `--reload` picks up edits. Each module should call `ui.page` once; optional
111
115
  * `pageMeta` is attached to the new path.
116
+ *
117
+ * When `reactiveLet` is `'auto'` (default), scans pages for `// @clay-reactive` /
118
+ * `"use reactive"` and registers the Bun plugin **before** imports so library
119
+ * boot does not need a manual `registerReactiveLetPlugin()` call.
112
120
  */
113
- export async function loadPages(dir: string | URL): Promise<string[]> {
121
+ export async function loadPages(
122
+ dir: string | URL,
123
+ opts?: { reactiveLet?: boolean | 'auto' },
124
+ ): Promise<string[]> {
114
125
  const absDir = typeof dir === 'string' ? dir : fileURLToPath(dir);
115
126
  const glob = new Bun.Glob('**/*.{ts,tsx}');
116
127
  const files = [...glob.scanSync({ cwd: absDir })].filter((f) => !shouldSkipPageFile(f)).sort();
128
+ const fullPaths = files.map((relative) => join(absDir, relative));
129
+
130
+ const reactiveMode = opts?.reactiveLet ?? 'auto';
131
+ if (reactiveMode === true) {
132
+ registerReactiveLetPlugin();
133
+ } else if (reactiveMode === 'auto') {
134
+ await ensureReactiveLetPluginForPaths(fullPaths);
135
+ }
117
136
 
118
137
  clearPages();
119
138
  clearPageMeta();