@close-by/clay 0.2.10 → 0.2.12

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
@@ -91,7 +91,7 @@ Each factory creates an `Element` with a wire `type` string. The React client ma
91
91
  | `command` | `command` (+ `commandgroup` / `commanditem` / `commandseparator`) | Command palette (`mode: 'dialog' \| 'inline'`; dialog owns `open`) |
92
92
  | `resizable` | `resizable` (+ `resizablepanel` / `resizablehandle`) | `react-resizable-panels` |
93
93
  | `scrollArea` | `scrollarea` | ShadCN `ScrollArea` (`onNearEnd` for infinite scroll) |
94
- | `viewportEnter` | `viewportEnter` | IntersectionObserver wrapper (`onEnter`, `once?`) |
94
+ | `viewportEnter` | `viewportEnter` | IntersectionObserver (`onEnter`, `once?`, `root?`) |
95
95
  | `keybind` | `keybind` | Headless `window` `keydown` chord listener (`return null`) |
96
96
  | `kbd` | `kbd` | Display-only chord glyphs (`Kbd` / `KbdGroup`; same tokens as `keybind`) |
97
97
  | `markdown` | `markdown` | Client `marked` + DOMPurify |
package/docs/examples.md CHANGED
@@ -18,6 +18,7 @@ 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` |
22
23
  | `/examples/reui-batch-7` | `ReuiBatch7Demo.ts` | ReUI batch 7: `inputGroup`, `toggle`, `descriptionList`, `staticTable`, `aspectRatio`, `itemList`, `checkboxGroup` |
23
24
  | `/examples/charts` | `ChartDemo.ts` | `ui.chart.*`, `ui.sparkline`, composed/scatter charts |
@@ -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
@@ -37,9 +39,9 @@ const live = ui.state({ selectedId: null as string | null, rows: [] as Row[] });
37
39
 
38
40
  ui.auto(() => {
39
41
  ui.resizable(
40
- { orientation: 'horizontal', className: 'h-[calc(100vh-8rem)] w-full min-h-[24rem]' },
42
+ { orientation: 'horizontal', fill: 'viewport' },
41
43
  (r) => {
42
- r.panel({ defaultSize: 40, minSize: 28 }, () => {
44
+ r.panel({ defaultSize: 40, minSize: 28, className: 'min-h-0 overflow-hidden' }, () => {
43
45
  ui.auto(() => {
44
46
  for (const row of live.rows) {
45
47
  ui.button(row.id, {
@@ -52,11 +54,12 @@ ui.auto(() => {
52
54
  }
53
55
  });
54
56
  });
55
- r.panel({ defaultSize: 60, minSize: 36 }, () => {
57
+ r.handle();
58
+ r.panel({ defaultSize: 60, minSize: 36, className: 'min-h-0 overflow-hidden' }, () => {
56
59
  ui.auto(() => {
57
60
  const row = live.rows.find((x) => x.id === live.selectedId);
58
61
  if (!row) {
59
- ui.label('Select a row').classes('text-sm text-muted-foreground');
62
+ ui.empty({ title: 'Select a row', density: 'inline' });
60
63
  return;
61
64
  }
62
65
  // detail…
@@ -68,7 +71,7 @@ ui.auto(() => {
68
71
  });
69
72
  ```
70
73
 
71
- Keep **separate** `ui.auto` regions for list vs detail so selection updates do not remount the whole shell. Default panel sizes (`defaultSize` / `minSize`) are percentages.
74
+ Keep **separate** `ui.auto` regions for list vs detail so selection updates do not remount the whole shell. Default panel sizes (`defaultSize` / `minSize`) are percentages. Prefer `fill: 'parent' | 'viewport'` over viewport `calc` height utilities.
72
75
 
73
76
  ## Live feed rows (not `ui.list`)
74
77
 
@@ -154,6 +157,49 @@ ui.auto(() => {
154
157
 
155
158
  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
159
 
160
+ ## Infinite scroll / load-more
161
+
162
+ **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`:
163
+
164
+ ```ts
165
+ ui.auto(() => {
166
+ if (live.loadingMore) {
167
+ ui.label('Loading…', { tone: 'muted', size: 'xs' });
168
+ return;
169
+ }
170
+ if (!live.hasMore) return;
171
+ ui.viewportEnter(
172
+ {
173
+ once: true,
174
+ root: 'nearest-scroll',
175
+ rootMargin: '120px',
176
+ onEnter: () => void loadNextPage(),
177
+ },
178
+ () => {
179
+ ui.label(' ').classes('h-1');
180
+ },
181
+ );
182
+ });
183
+ ```
184
+
185
+ 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`.
186
+
187
+ ## Fewer classes (ops density)
188
+
189
+ Prefer semantic props over Tailwind soup:
190
+
191
+ | Instead of… | Prefer… |
192
+ |-------------|---------|
193
+ | `.classes('text-sm text-muted-foreground')` | `ui.label(…, { tone: 'muted', size: 'sm' })` |
194
+ | Title + subtitle stacks | `ui.pageHeading('Orders', { description: '…' })` |
195
+ | `className: 'flex-row w-[10rem]'` on filters | `ui.select({ inline: true, width: 'sm', … })` |
196
+ | `ui.spinner({ className: 'h-3 w-3' })` | `ui.spinner({ size: 'xs' })` |
197
+ | Feed empty muted label | `ui.empty({ title: '…', density: 'inline' })` |
198
+ | `h-[calc(100%-7rem)]!` on resizable | `ui.resizable({ fill: 'parent' \| 'viewport' }, …)` |
199
+ | `className: 'items-end justify-between'` on rows | `ui.row({ align: 'end', justify: 'between' }, …)` |
200
+
201
+ `className` remains an escape hatch.
202
+
157
203
  ## Logs / traces — sensitive bodies
158
204
 
159
205
  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.10",
3
+ "version": "0.2.12",
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.10",
41
- "@close-by/clay-core": "0.2.10",
42
- "@close-by/clay-components": "0.2.10",
43
- "@close-by/clay-server": "0.2.10",
40
+ "@close-by/clay-compiler": "0.2.12",
41
+ "@close-by/clay-core": "0.2.12",
42
+ "@close-by/clay-components": "0.2.12",
43
+ "@close-by/clay-server": "0.2.12",
44
44
  "@tailwindcss/cli": "^4.3.3",
45
45
  "tailwindcss": "^4.3.3"
46
46
  }
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  pagination as paginationFactory,
35
35
  filterBar as filterBarFactory,
36
36
  filterChips as filterChipsFactory,
37
+ pageHeading as pageHeadingFactory,
37
38
  numberField as numberFieldFactory,
38
39
  phoneInput as phoneInputFactory,
39
40
  field as fieldFactory,
@@ -120,7 +121,10 @@ import {
120
121
  type ButtonVariant,
121
122
  type ButtonSize,
122
123
  type LabelProps,
124
+ type LabelTone,
125
+ type LabelSize,
123
126
  type InputProps,
127
+ type FieldWidth,
124
128
  type CheckboxProps,
125
129
  type SwitchProps,
126
130
  type SelectProps,
@@ -144,6 +148,7 @@ import {
144
148
  type BadgeProps,
145
149
  type AlertProps,
146
150
  type SpinnerProps,
151
+ type SpinnerSize,
147
152
  type ProgressProps,
148
153
  type SeparatorProps,
149
154
  type IconProps,
@@ -157,6 +162,8 @@ import {
157
162
  type ButtonGroupProps,
158
163
  type ButtonGroupOrientation,
159
164
  type EmptyProps,
165
+ type EmptyDensity,
166
+ type PageHeadingProps,
160
167
  type PaginationProps,
161
168
  type FilterBarProps,
162
169
  type FilterChip,
@@ -253,11 +260,13 @@ import {
253
260
  type CommandMode,
254
261
  type CommandItemOptions,
255
262
  type ResizableProps,
263
+ type ResizableFill,
256
264
  type ResizableOrientation,
257
265
  type ResizablePanelProps,
258
266
  type ResizableHandleProps,
259
267
  type ScrollAreaProps,
260
268
  type ViewportEnterProps,
269
+ type ViewportEnterRoot,
261
270
  type KeybindProps,
262
271
  type KbdProps,
263
272
  type BreadcrumbItem,
@@ -284,7 +293,6 @@ import {
284
293
  type HtmlProps,
285
294
  type ImageProps,
286
295
  type IframeProps,
287
- type ViewportEnterProps,
288
296
  type UploadProps,
289
297
  type UploadVariant,
290
298
  type UploadedFile,
@@ -494,7 +502,10 @@ export type {
494
502
  ButtonVariant,
495
503
  ButtonSize,
496
504
  LabelProps,
505
+ LabelTone,
506
+ LabelSize,
497
507
  InputProps,
508
+ FieldWidth,
498
509
  CheckboxProps,
499
510
  BadgeProps,
500
511
  LinkProps,
@@ -555,6 +566,7 @@ export type {
555
566
  AvatarProps,
556
567
  SkeletonProps,
557
568
  SpinnerProps,
569
+ SpinnerSize,
558
570
  ProgressProps,
559
571
  SeparatorProps,
560
572
  IconProps,
@@ -571,6 +583,7 @@ export type {
571
583
  ImageProps,
572
584
  IframeProps,
573
585
  ViewportEnterProps,
586
+ ViewportEnterRoot,
574
587
  UploadProps,
575
588
  UploadVariant,
576
589
  UploadedFile,
@@ -930,6 +943,11 @@ export function empty(props: EmptyProps, footer?: () => void): Element {
930
943
  return emptyFactory(props, footer);
931
944
  }
932
945
 
946
+ /** Page title + optional description. Prefer over stacked muted labels. */
947
+ export function pageHeading(props: PageHeadingProps, trailing?: () => void): Element {
948
+ return pageHeadingFactory(props, trailing);
949
+ }
950
+
933
951
  /** Standalone pager. See {@link PaginationProps}. */
934
952
  export function pagination(props?: PaginationProps): Element {
935
953
  return paginationFactory(props);
@@ -1788,6 +1806,7 @@ export const ui = {
1788
1806
  feedList,
1789
1807
  buttonGroup,
1790
1808
  empty,
1809
+ pageHeading,
1791
1810
  pagination,
1792
1811
  filterBar,
1793
1812
  filterChips,
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();