@vertz/ui 0.2.0 → 0.2.2

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.
Files changed (36) hide show
  1. package/README.md +339 -857
  2. package/dist/css/public.d.ts +24 -27
  3. package/dist/css/public.js +5 -1
  4. package/dist/form/public.d.ts +94 -38
  5. package/dist/form/public.js +5 -3
  6. package/dist/index.d.ts +754 -167
  7. package/dist/index.js +606 -84
  8. package/dist/internals.d.ts +192 -23
  9. package/dist/internals.js +151 -102
  10. package/dist/jsx-runtime/index.d.ts +44 -17
  11. package/dist/jsx-runtime/index.js +26 -7
  12. package/dist/query/public.d.ts +73 -7
  13. package/dist/query/public.js +12 -4
  14. package/dist/router/public.d.ts +199 -26
  15. package/dist/router/public.js +22 -7
  16. package/dist/shared/chunk-0xcmwgdb.js +288 -0
  17. package/dist/shared/{chunk-j8vzvne3.js → chunk-9e92w0wt.js} +4 -1
  18. package/dist/shared/chunk-g4rch80a.js +33 -0
  19. package/dist/shared/chunk-hh0dhmb4.js +528 -0
  20. package/dist/shared/{chunk-pgymxpn1.js → chunk-hrd0mft1.js} +136 -34
  21. package/dist/shared/chunk-jrtrk5z4.js +125 -0
  22. package/dist/shared/chunk-ka5ked7n.js +188 -0
  23. package/dist/shared/chunk-n91rwj2r.js +483 -0
  24. package/dist/shared/chunk-prj7nm08.js +67 -0
  25. package/dist/shared/chunk-q6cpe5k7.js +230 -0
  26. package/dist/shared/{chunk-f1ynwam4.js → chunk-qacth5ah.js} +162 -36
  27. package/dist/shared/chunk-ryb49346.js +374 -0
  28. package/dist/shared/chunk-v3yyf79g.js +48 -0
  29. package/dist/test/index.d.ts +67 -6
  30. package/dist/test/index.js +4 -3
  31. package/package.json +14 -9
  32. package/dist/shared/chunk-bp3v6s9j.js +0 -62
  33. package/dist/shared/chunk-d8h2eh8d.js +0 -141
  34. package/dist/shared/chunk-tsdpgmks.js +0 -98
  35. package/dist/shared/chunk-xd9d7q5p.js +0 -115
  36. package/dist/shared/chunk-zbbvx05f.js +0 -202
package/README.md CHANGED
@@ -1,1138 +1,620 @@
1
1
  # @vertz/ui
2
2
 
3
- A compiler-driven UI framework with fine-grained reactivity. Write plain variables and JSX -- the compiler transforms them into efficient reactive DOM operations. No virtual DOM.
4
-
5
- ## Table of Contents
6
-
7
- - [Quick Start](#quick-start)
8
- - [Reactivity](#reactivity)
9
- - [Components](#components)
10
- - [Conditional Rendering](#conditional-rendering)
11
- - [List Rendering](#list-rendering)
12
- - [Styling](#styling)
13
- - [Data Fetching](#data-fetching)
14
- - [Routing](#routing)
15
- - [Forms](#forms)
16
- - [Lifecycle](#lifecycle)
17
- - [Primitives](#primitives)
18
- - [When to Use effect()](#when-to-use-effect)
19
-
20
- ---
21
-
22
- ## Quick Start
23
-
24
- ### Install
25
-
26
- ```bash
27
- npm install @vertz/ui @vertz/ui-compiler
28
- ```
29
-
30
- ### Vite Config
31
-
32
- ```ts
33
- // vite.config.ts
34
- import vertz from '@vertz/ui-compiler/vite';
35
- import { defineConfig } from 'vite';
36
-
37
- export default defineConfig({
38
- plugins: [vertz()],
39
- });
40
- ```
41
-
42
- The plugin transforms all `.tsx` and `.jsx` files by default. You can customize with `include` and `exclude` globs:
43
-
44
- ```ts
45
- vertz({
46
- include: ['**/*.tsx'],
47
- exclude: ['**/vendor/**'],
48
- cssExtraction: true, // default: true in production
49
- })
50
- ```
51
-
52
- ### Hello World
3
+ **Use `let` for state. Use `const` for derived. Write JSX. Done.**
53
4
 
54
5
  ```tsx
55
- function App() {
56
- let name = 'World';
6
+ function Counter() {
7
+ let count = 0;
57
8
 
58
9
  return (
59
10
  <div>
60
- <h1>Hello, {name}!</h1>
61
- <input
62
- value={name}
63
- onInput={(e) => (name = (e.target as HTMLInputElement).value)}
64
- />
11
+ <p>Count: {count}</p>
12
+ <button onClick={() => count++}>Increment</button>
65
13
  </div>
66
14
  );
67
15
  }
68
-
69
- document.body.appendChild(App());
70
16
  ```
71
17
 
72
- That's it. `name` is reactive. Typing in the input updates the heading. No hooks, no store setup, no subscriptions.
18
+ That's it. `count` is reactive. The compiler transforms your code into efficient reactive DOM updates. No virtual DOM, no hooks, no boilerplate.
73
19
 
74
20
  ---
75
21
 
76
- ## Reactivity
22
+ ## Installation
77
23
 
78
- This is the core mental model of the framework. The compiler does the heavy lifting -- you write normal-looking code and get fine-grained reactive DOM updates.
24
+ ```bash
25
+ npm install @vertz/ui @vertz/ui-compiler
26
+ ```
79
27
 
80
- ### `let` = Reactive State
28
+ ### Bun Setup
81
29
 
82
- Any `let` declaration inside a component that is read in JSX becomes a signal:
30
+ The `@vertz/ui-server/bun-plugin` handles compiler transforms, CSS extraction, and Fast Refresh automatically when using `Bun.serve()` with HTML imports or `vertz dev`.
83
31
 
84
- ```tsx
85
- // What you write:
86
- function Counter() {
87
- let count = 0;
88
- return <span>{count}</span>;
89
- }
32
+ ---
90
33
 
91
- // What the compiler produces:
92
- function Counter() {
93
- const count = signal(0);
94
- const __el = __element("span");
95
- __el.appendChild(__text(() => count.value));
96
- return __el;
97
- }
98
- ```
34
+ ## The Basics
99
35
 
100
- Assignments work naturally:
36
+ ### Reactive State: `let`
37
+
38
+ Any `let` variable read in JSX becomes reactive:
101
39
 
102
40
  ```tsx
103
- function Counter() {
104
- let count = 0;
41
+ function TodoInput() {
42
+ let text = '';
105
43
 
106
44
  return (
107
45
  <div>
108
- <span>{count}</span>
109
- <button onClick={() => count++}>+</button>
46
+ <input value={text} onInput={(e) => (text = e.target.value)} />
47
+ <p>You typed: {text}</p>
110
48
  </div>
111
49
  );
112
50
  }
113
51
  ```
114
52
 
115
- The compiler transforms `count++` to `count.value++`, `count = 5` to `count.value = 5`, and `count += 1` to `count.value += 1`.
53
+ Assignments work naturally: `text = 'hello'`, `count++`, `items.push(item)`.
116
54
 
117
- ### `const` = Derived State
55
+ ### Derived State: `const`
118
56
 
119
- A `const` that references a signal becomes a computed:
57
+ A `const` that reads a reactive variable becomes computed (cached, lazy):
120
58
 
121
59
  ```tsx
122
- // What you write:
123
- function Pricing() {
60
+ function Cart() {
124
61
  let quantity = 1;
125
- const total = 10 * quantity;
126
- const formatted = `$${total}`;
127
-
128
- return <span>{formatted}</span>;
129
- }
130
-
131
- // What the compiler produces:
132
- function Pricing() {
133
- const quantity = signal(1);
134
- const total = computed(() => 10 * quantity.value);
135
- const formatted = computed(() => `$${total.value}`);
136
- // ...
137
- }
138
- ```
62
+ let price = 10;
139
63
 
140
- Computeds are lazy and cached -- they only recompute when their dependencies change.
141
-
142
- Destructuring also works:
143
-
144
- ```tsx
145
- function Profile() {
146
- let user = { name: 'Alice', age: 30 };
147
- const { name, age } = user;
64
+ const total = quantity * price;
65
+ const formatted = `$${total}`;
148
66
 
149
- return <span>{name} - {age}</span>;
67
+ return <p>Total: {formatted}</p>;
150
68
  }
151
- // Compiler produces:
152
- // const name = computed(() => user.name)
153
- // const age = computed(() => user.age)
154
- ```
155
-
156
- ### JSX Text Interpolation
157
-
158
- `{expr}` in JSX creates a reactive text node when `expr` depends on signals:
159
-
160
- ```tsx
161
- <span>{count}</span>
162
- // becomes: __text(() => count.value)
163
69
  ```
164
70
 
165
- Static expressions produce plain text nodes:
166
-
167
- ```tsx
168
- const title = "Hello";
169
- <span>{title}</span>
170
- // becomes: document.createTextNode(title)
171
- ```
71
+ `total` only recalculates when `quantity` or `price` change.
172
72
 
173
- ### JSX Reactive Attributes
73
+ ### Components
174
74
 
175
- Attributes that depend on signals auto-update:
75
+ Components are plain functions returning DOM:
176
76
 
177
77
  ```tsx
178
- function App() {
179
- let isActive = false;
180
-
181
- return (
182
- <div className={isActive ? 'active' : 'inactive'}>
183
- <button onClick={() => isActive = !isActive}>Toggle</button>
184
- </div>
185
- );
78
+ function Greeting({ name }) {
79
+ return <h1>Hello, {name}!</h1>;
186
80
  }
187
- // The compiler wraps the reactive className in __attr(el, "className", () => ...)
188
- ```
189
-
190
- ### Mutations
191
-
192
- The compiler intercepts mutations on signal-backed variables and generates peek/notify calls so the reactivity system is notified:
193
81
 
194
- ```tsx
195
82
  function App() {
196
- let items = ['a', 'b'];
197
-
198
- // .push(), .splice(), etc. all work:
199
- items.push('c');
200
- // compiles to: (items.peek().push('c'), items.notify())
201
-
202
- // Property assignment:
203
- let user = { name: 'Alice' };
204
- user.name = 'Bob';
205
- // compiles to: (user.peek().name = 'Bob', user.notify())
206
-
207
- // Index assignment:
208
- items[0] = 'z';
209
- // compiles to: (items.peek()[0] = 'z', items.notify())
210
-
211
- // Object.assign:
212
- Object.assign(user, { age: 30 });
213
- // compiles to: (Object.assign(user.peek(), { age: 30 }), user.notify())
214
-
215
- // delete:
216
- let config = { debug: true };
217
- delete config.debug;
218
- // compiles to: (delete config.peek().debug, config.notify())
219
- }
220
- ```
221
-
222
- ### Event Handlers
223
-
224
- `onClick`, `onInput`, etc. are transformed to `__on(el, "click", handler)`:
225
-
226
- ```tsx
227
- <button onClick={() => count++}>+</button>
228
- ```
229
-
230
- ### DO vs DON'T
231
-
232
- ```tsx
233
- // DON'T -- imperative DOM manipulation
234
- // This ignores the compiler entirely. You're doing its job by hand, badly.
235
- import { signal, effect } from '@vertz/ui';
236
-
237
- function Counter() {
238
- const count = signal(0);
239
- const label = document.createElement('span');
240
- effect(() => { label.textContent = String(count.value); });
241
- const btn = document.createElement('button');
242
- btn.onclick = () => { count.value++; };
243
- btn.textContent = '+';
244
- const div = document.createElement('div');
245
- div.append(label, btn);
246
- return div;
247
- }
83
+ let name = 'World';
248
84
 
249
- // DO -- declarative JSX (let the compiler handle reactivity)
250
- function Counter() {
251
- let count = 0;
252
85
  return (
253
86
  <div>
254
- <span>{count}</span>
255
- <button onClick={() => count++}>+</button>
87
+ <Greeting name={name} />
88
+ <button onClick={() => (name = 'Alice')}>Change Name</button>
256
89
  </div>
257
90
  );
258
91
  }
259
92
  ```
260
93
 
261
- The declarative version is shorter, clearer, and produces the same (or better) runtime code. The compiler generates exactly the reactive bindings needed -- no more, no less.
262
-
263
- ---
264
-
265
- ## Components
266
-
267
- Components are functions that return `HTMLElement` (or `Node`). There is no component class, no `render()` method.
94
+ ### Mounting
268
95
 
269
- ### Basic Component
96
+ Mount your app to the DOM with `mount()`:
270
97
 
271
98
  ```tsx
272
- function Greeting() {
273
- return <h1>Hello</h1>;
274
- }
99
+ import { mount } from '@vertz/ui';
275
100
 
276
- // Use it:
277
- document.body.appendChild(Greeting());
278
- ```
279
-
280
- ### Props
281
-
282
- Props are plain objects. For reactive props (values that can change), use getter functions:
283
-
284
- ```tsx
285
- interface CardProps {
286
- title: string; // static -- value captured once
287
- count: () => number; // reactive -- getter re-evaluated on access
288
- }
289
-
290
- function Card(props: CardProps) {
291
- return (
292
- <div>
293
- <h2>{props.title}</h2>
294
- <span>{props.count()}</span>
295
- </div>
296
- );
297
- }
298
-
299
- // Usage:
300
101
  function App() {
301
- let n = 0;
302
- return (
303
- <div>
304
- <Card title="Score" count={() => n} />
305
- <button onClick={() => n++}>+</button>
306
- </div>
307
- );
102
+ let count = 0;
103
+ return <button onClick={() => count++}>Count: {count}</button>;
308
104
  }
309
- ```
310
-
311
- The `() => n` getter ensures `Card` re-reads the value reactively. A bare `n` would capture the value once and never update.
312
105
 
313
- ### Children
106
+ const { unmount, root } = mount(App, '#app');
107
+ ```
314
108
 
315
- Components can accept children via the `children` helper:
109
+ **With options:**
316
110
 
317
111
  ```tsx
318
- import { children, type ChildrenAccessor } from '@vertz/ui';
112
+ import { mount } from '@vertz/ui';
113
+ import { defineTheme } from '@vertz/ui/css';
319
114
 
320
- interface PanelProps {
321
- children: ChildrenAccessor;
322
- }
115
+ const theme = defineTheme({
116
+ colors: { primary: { 500: '#3b82f6' } },
117
+ });
323
118
 
324
- function Panel(props: PanelProps) {
325
- const getChildren = children(props.children);
326
- const el = <div className="panel" />;
327
- for (const child of getChildren()) {
328
- (el as HTMLElement).appendChild(child);
329
- }
330
- return el;
331
- }
119
+ mount(App, '#app', {
120
+ theme,
121
+ styles: ['body { margin: 0; }'],
122
+ onMount: (root) => console.log('Mounted to', root),
123
+ });
332
124
  ```
333
125
 
334
- ### Composition
126
+ `mount(app, selector, options?)` accepts:
335
127
 
336
- Components compose by returning DOM nodes. No special syntax needed:
128
+ - `selector` CSS selector string or `HTMLElement`
129
+ - `options.theme` — theme definition for CSS vars
130
+ - `options.styles` — global CSS strings to inject
131
+ - `options.hydration` — `'replace'` (default) or `false`
132
+ - `options.registry` — component registry for per-component hydration
133
+ - `options.onMount` — callback after mount completes
337
134
 
338
- ```tsx
339
- function App() {
340
- return (
341
- <div>
342
- {Header()}
343
- {MainContent()}
344
- {Footer()}
345
- </div>
346
- );
347
- }
348
- ```
349
-
350
- ---
135
+ Returns a `MountHandle` with `unmount()` and `root`.
351
136
 
352
- ## Conditional Rendering
137
+ ### Lifecycle: `onMount`
353
138
 
354
- Use standard JSX conditional patterns. The compiler transforms them into efficient reactive DOM operations with automatic disposal:
139
+ Run code once when the component is created:
355
140
 
356
141
  ```tsx
357
- function Toggle() {
358
- let show = false;
142
+ import { onMount } from '@vertz/ui';
359
143
 
360
- return (
361
- <div>
362
- {show && <p>Now you see me</p>}
363
- <button onClick={() => show = !show}>Toggle</button>
364
- </div>
365
- );
366
- }
367
- ```
368
-
369
- Ternaries work too:
144
+ function Timer() {
145
+ let seconds = 0;
370
146
 
371
- ```tsx
372
- function StatusBadge() {
373
- let online = true;
147
+ onMount(() => {
148
+ const id = setInterval(() => seconds++, 1000);
149
+ return () => clearInterval(id);
150
+ });
374
151
 
375
- return (
376
- <div>
377
- {online ? <span className="green">Online</span> : <span className="gray">Offline</span>}
378
- </div>
379
- );
152
+ return <p>{seconds}s</p>;
380
153
  }
381
154
  ```
382
155
 
383
- Under the hood, the compiler transforms these into `__conditional()` calls that manage DOM insertion, replacement, and cleanup automatically. When the condition changes, the old branch is disposed and the new branch is rendered in place.
384
-
385
- ### Manual Control
156
+ ### Data Fetching: `query`
386
157
 
387
- For advanced use cases where you need direct access to the dispose function or more control over the condition lifecycle, you can use `__conditional()` from `@vertz/ui/internals` directly:
158
+ Fetch data reactively:
388
159
 
389
160
  ```tsx
390
- import { __conditional } from '@vertz/ui/internals';
161
+ import { query } from '@vertz/ui';
391
162
 
392
- function Toggle() {
393
- let show = false;
163
+ function UserProfile() {
164
+ let userId = 1;
394
165
 
395
- return (
396
- <div>
397
- {__conditional(
398
- () => show,
399
- () => <p>Now you see me</p>,
400
- () => <p>Now you don't</p>
401
- )}
402
- <button onClick={() => show = !show}>Toggle</button>
403
- </div>
166
+ const { data, loading } = query(() =>
167
+ fetch(`/api/users/${userId}`).then((r) => r.json())
404
168
  );
405
- }
406
- ```
407
-
408
- `__conditional` takes three arguments:
409
-
410
- 1. `condFn: () => boolean` -- reactive condition
411
- 2. `trueFn: () => Node` -- rendered when true
412
- 3. `falseFn: () => Node` -- rendered when false
413
-
414
- ---
415
-
416
- ## List Rendering
417
-
418
- Use `.map()` in JSX with a `key` prop for efficient keyed reconciliation. The compiler transforms it into optimized list operations:
419
-
420
- ```tsx
421
- function TodoList() {
422
- let todos = [
423
- { id: '1', text: 'Learn vertz' },
424
- { id: '2', text: 'Build something' },
425
- ];
426
169
 
427
170
  return (
428
171
  <div>
429
- <ul>
430
- {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
431
- </ul>
432
- <button onClick={() => {
433
- todos = [...todos, { id: String(Date.now()), text: 'New todo' }];
434
- }}>
435
- Add
436
- </button>
172
+ {loading.value ? 'Loading...' : <p>{data.value?.name}</p>}
173
+ <button onClick={() => userId++}>Next User</button>
437
174
  </div>
438
175
  );
439
176
  }
440
177
  ```
441
178
 
442
- The `key` prop is extracted by the compiler for efficient keyed reconciliation -- existing DOM nodes are reused and reordered, not recreated. Always provide a stable, unique key for each item.
443
-
444
- ### Manual Control
445
-
446
- For advanced use cases where you need direct access to the list lifecycle or want to work with the signal directly, you can use `__list()` from `@vertz/ui/internals`:
447
-
448
- ```tsx
449
- import { signal } from '@vertz/ui';
450
- import { __list } from '@vertz/ui/internals';
451
-
452
- function TodoList() {
453
- const todosSignal = signal([
454
- { id: '1', text: 'Learn vertz' },
455
- { id: '2', text: 'Build something' },
456
- ]);
457
-
458
- const container = <ul /> as HTMLElement;
459
-
460
- __list(
461
- container,
462
- todosSignal,
463
- (todo) => todo.id, // key function
464
- (todo) => <li>{todo.text}</li> // render function (called once per key)
465
- );
466
-
467
- return (
468
- <div>
469
- {container}
470
- <button onClick={() => {
471
- todosSignal.value = [...todosSignal.value, { id: String(Date.now()), text: 'New todo' }];
472
- }}>
473
- Add
474
- </button>
475
- </div>
476
- );
477
- }
478
- ```
179
+ ---
479
180
 
480
- `__list` arguments:
181
+ ## You're Done (Probably)
481
182
 
482
- 1. `container: HTMLElement` -- parent element
483
- 2. `items: Signal<T[]>` -- reactive array
484
- 3. `keyFn: (item: T) => string | number` -- unique key extractor
485
- 4. `renderFn: (item: T) => Node` -- creates DOM for each item (called once per key)
183
+ **90% of apps only need the above.** The rest is for special cases.
486
184
 
487
185
  ---
488
186
 
489
187
  ## Styling
490
188
 
491
- Import from `@vertz/ui/css` or from the main `@vertz/ui` export.
492
-
493
- ### `css()` -- Scoped Style Blocks
189
+ ### `css` Scoped Styles
494
190
 
495
191
  ```tsx
496
192
  import { css } from '@vertz/ui/css';
497
193
 
498
194
  const styles = css({
499
- card: ['p:4', 'bg:background', 'rounded:lg'],
500
- title: ['font:xl', 'weight:bold', 'text:foreground'],
195
+ card: ['p:4', 'bg:white', 'rounded:lg', 'shadow:md'],
196
+ title: ['font:xl', 'weight:bold', 'mb:2'],
501
197
  });
502
198
 
503
- function Card() {
199
+ function Card({ title, children }) {
504
200
  return (
505
- <div className={styles.classNames.card}>
506
- <h2 className={styles.classNames.title}>Hello</h2>
201
+ <div className={styles.card}>
202
+ <h2 className={styles.title}>{title}</h2>
203
+ {children}
507
204
  </div>
508
205
  );
509
206
  }
510
207
  ```
511
208
 
512
- Shorthand syntax: `property:value` maps to CSS custom properties and design tokens. Pseudo-states are supported:
513
-
514
- ```tsx
515
- const button = css({
516
- root: ['p:4', 'bg:primary', 'hover:bg:primary.700', 'rounded:md'],
517
- });
518
- ```
519
-
520
- Object form for complex selectors:
521
-
522
- ```tsx
523
- const fancy = css({
524
- card: [
525
- 'p:4', 'bg:background',
526
- { '&::after': ['content:empty', 'block'] },
527
- ],
528
- });
529
- ```
530
-
531
- ### `variants()` -- Typed Component Variants
209
+ ### `variants` Typed Variants
532
210
 
533
211
  ```tsx
534
212
  import { variants } from '@vertz/ui/css';
535
213
 
536
214
  const button = variants({
537
- base: ['flex', 'font:medium', 'rounded:md'],
215
+ base: ['px:4', 'py:2', 'rounded:md', 'font:medium'],
538
216
  variants: {
539
217
  intent: {
540
- primary: ['bg:primary.600', 'text:foreground'],
541
- secondary: ['bg:background', 'text:muted'],
218
+ primary: ['bg:blue.600', 'text:white'],
219
+ secondary: ['bg:gray.100', 'text:gray.800'],
542
220
  },
543
221
  size: {
544
- sm: ['text:xs', 'h:8'],
545
- md: ['text:sm', 'h:10'],
546
- lg: ['text:base', 'h:12'],
222
+ sm: ['px:2', 'py:1', 'text:sm'],
223
+ lg: ['px:6', 'py:3', 'text:lg'],
547
224
  },
548
225
  },
549
226
  defaultVariants: { intent: 'primary', size: 'md' },
550
- compoundVariants: [
551
- { intent: 'primary', size: 'sm', styles: ['px:2'] },
552
- ],
553
227
  });
554
228
 
555
- // Returns a className string:
556
- button({ intent: 'secondary', size: 'sm' }); // => "base_abc secondary_def sm_ghi"
557
- button(); // => uses defaults
229
+ function Button({ intent, size, children }) {
230
+ return <button className={button({ intent, size })}>{children}</button>;
231
+ }
558
232
  ```
559
233
 
560
- The variant function is fully typed -- TypeScript infers the allowed values for `intent` and `size`.
234
+ ### `s` Inline Dynamic Styles
235
+
236
+ ```tsx
237
+ import { s } from '@vertz/ui/css';
238
+
239
+ function ProgressBar({ percent }) {
240
+ return <div style={s([`w:${percent}%`, 'bg:green.500', 'h:4'])} />;
241
+ }
242
+ ```
561
243
 
562
- ### `defineTheme()` and `ThemeProvider`
244
+ ### Theming
563
245
 
564
246
  ```tsx
565
247
  import { defineTheme, compileTheme, ThemeProvider } from '@vertz/ui/css';
566
248
 
567
249
  const theme = defineTheme({
568
250
  colors: {
569
- primary: { 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8' },
251
+ primary: { 500: '#3b82f6', 600: '#2563eb' },
570
252
  background: { DEFAULT: '#ffffff', _dark: '#111827' },
571
- foreground: { DEFAULT: '#111827', _dark: '#f9fafb' },
572
- },
573
- spacing: {
574
- 1: '0.25rem',
575
- 2: '0.5rem',
576
- 4: '1rem',
577
- 8: '2rem',
578
253
  },
579
254
  });
580
255
 
581
- // Generate CSS custom properties:
582
256
  const compiled = compileTheme(theme);
583
- // compiled.css contains:
584
- // :root { --color-primary-500: #3b82f6; --color-background: #ffffff; ... }
585
- // [data-theme="dark"] { --color-background: #111827; ... }
586
-
587
- // Apply theme to a subtree:
588
- const app = ThemeProvider({
589
- theme: 'dark',
590
- children: [MyApp()],
591
- });
592
- document.body.appendChild(app);
593
- ```
594
-
595
- Contextual tokens use `DEFAULT` for the base value and `_dark` for the dark variant. The `ThemeProvider` sets `data-theme` on a wrapper element.
596
-
597
- ### `globalCss()` -- Global Styles
598
257
 
599
- ```tsx
600
- import { globalCss } from '@vertz/ui/css';
601
-
602
- globalCss({
603
- '*, *::before, *::after': {
604
- boxSizing: 'border-box',
605
- margin: '0',
606
- },
607
- body: {
608
- fontFamily: 'system-ui, sans-serif',
609
- lineHeight: '1.5',
610
- },
611
- });
258
+ ThemeProvider({ theme: 'dark', children: [<App />] });
612
259
  ```
613
260
 
614
- Properties use camelCase and are converted to kebab-case. CSS custom properties (`--*`) are passed through as-is.
261
+ ---
615
262
 
616
- ### `s()` -- Inline Styles
263
+ ## Forms
617
264
 
618
- For truly dynamic styles that can't be static:
265
+ Bind forms to server actions with type-safe validation:
619
266
 
620
267
  ```tsx
621
- import { s } from '@vertz/ui/css';
622
-
623
- function Bar(props: { width: number }) {
624
- return <div style={s([`w:${props.width}px`, 'h:4', 'bg:primary.500'])} />;
625
- }
626
- ```
627
-
628
- Pseudo-states are not supported in `s()` -- use `css()` for those.
268
+ import { form } from '@vertz/ui';
629
269
 
630
- ---
631
-
632
- ## Data Fetching
270
+ const createUser = Object.assign(
271
+ async (body: { name: string; email: string }) => {
272
+ const res = await fetch('/api/users', { method: 'POST', body: JSON.stringify(body) });
273
+ return res.json() as Promise<{ id: string }>;
274
+ },
275
+ { url: '/api/users', method: 'POST' }
276
+ );
633
277
 
634
- Import from `@vertz/ui/query` or the main `@vertz/ui` export.
278
+ const userSchema = { /* validation schema */ };
635
279
 
636
- ```tsx
637
- import { query } from '@vertz/ui/query';
280
+ function CreateUser() {
281
+ const f = form(createUser, {
282
+ schema: userSchema,
283
+ onSuccess: (result) => console.log('User created:', result.id),
284
+ });
638
285
 
639
- function UserProfile() {
640
- let userId = 1;
286
+ return (
287
+ <form action={f.action} method={f.method} onSubmit={f.onSubmit}>
288
+ <input name="name" placeholder="Name" />
289
+ {f.name.error && <span class="error">{f.name.error}</span>}
641
290
 
642
- const { data, loading, error, refetch } = query(
643
- () => fetch(`/api/users/${userId}`).then(r => r.json()),
644
- );
291
+ <input name="email" type="email" placeholder="Email" />
292
+ {f.email.error && <span class="error">{f.email.error}</span>}
645
293
 
646
- return (
647
- <div>
648
- {/* data, loading, error are signals -- read .value in reactive contexts */}
649
- <span>{loading.value ? 'Loading...' : ''}</span>
650
- <span>{data.value?.name}</span>
651
- <button onClick={() => userId++}>Next User</button>
652
- <button onClick={refetch}>Refresh</button>
653
- </div>
294
+ <button type="submit" disabled={f.submitting}>
295
+ {f.submitting.value ? 'Creating...' : 'Create User'}
296
+ </button>
297
+ </form>
654
298
  );
655
299
  }
656
300
  ```
657
301
 
658
- The thunk runs inside an effect, so reactive dependencies read before the `await` are automatically tracked. When `userId` changes, the query re-fetches.
659
-
660
- ### Options
661
-
662
- ```tsx
663
- const result = query(() => fetchData(), {
664
- initialData: cachedValue, // skip initial fetch
665
- debounce: 300, // debounce re-fetches (ms)
666
- enabled: true, // set false to disable
667
- key: 'custom-cache-key', // explicit cache key
668
- cache: myCustomCache, // custom CacheStore implementation
669
- });
670
- ```
671
-
672
- ### Cleanup
673
-
674
- ```tsx
675
- const { dispose } = query(() => fetchData());
676
-
677
- // Stop the reactive effect and clean up in-flight requests:
678
- dispose();
679
- ```
680
-
681
- `revalidate()` is an alias for `refetch()`.
682
-
683
302
  ---
684
303
 
685
304
  ## Routing
686
305
 
687
- Import from `@vertz/ui/router` or the main `@vertz/ui` export.
688
-
689
- ### Define Routes
690
-
691
306
  ```tsx
692
- import { defineRoutes, createRouter, createLink, createOutlet } from '@vertz/ui/router';
307
+ import { defineRoutes, createRouter, createLink } from '@vertz/ui/router';
693
308
 
694
309
  const routes = defineRoutes({
695
- '/': {
696
- component: () => HomePage(),
697
- },
310
+ '/': { component: () => <HomePage /> },
698
311
  '/users/:id': {
699
- component: () => UserPage(),
700
- loader: async ({ params, signal }) => {
701
- const res = await fetch(`/api/users/${params.id}`, { signal });
312
+ component: () => <UserPage />,
313
+ loader: async ({ params }) => {
314
+ const res = await fetch(`/api/users/${params.id}`);
702
315
  return res.json();
703
316
  },
704
- errorComponent: (error) => <div>Failed: {error.message}</div>,
705
- },
706
- '/about': {
707
- component: () => AboutPage(),
708
317
  },
709
318
  });
710
- ```
711
-
712
- ### Create Router
713
-
714
- ```tsx
715
- const router = createRouter(routes, window.location.pathname + window.location.search);
716
-
717
- // Reactive state:
718
- router.current; // Signal<RouteMatch | null>
719
- router.loaderData; // Signal<unknown[]>
720
- router.loaderError; // Signal<Error | null>
721
- router.searchParams; // Signal<Record<string, unknown>>
722
-
723
- // Navigation:
724
- await router.navigate('/users/42');
725
- await router.navigate('/home', { replace: true });
726
-
727
- // Re-run loaders:
728
- await router.revalidate();
729
319
 
730
- // Cleanup:
731
- router.dispose();
732
- ```
733
-
734
- ### Link Component
735
-
736
- ```tsx
737
- const Link = createLink(router.current, (url) => router.navigate(url));
320
+ const router = createRouter(routes);
321
+ const Link = createLink(router.current, router.navigate);
738
322
 
739
323
  function Nav() {
740
324
  return (
741
325
  <nav>
742
- {Link({ href: '/', children: 'Home', activeClass: 'active' })}
743
- {Link({ href: '/about', children: 'About', activeClass: 'active' })}
326
+ <Link href="/">Home</Link>
327
+ <Link href="/users/1">User 1</Link>
744
328
  </nav>
745
329
  );
746
330
  }
747
331
  ```
748
332
 
749
- Links intercept clicks for SPA navigation (modifier-key clicks still open new tabs). The `activeClass` is applied reactively when the link's `href` matches the current path.
750
-
751
- ### Nested Routes and Outlets
333
+ ---
752
334
 
753
- ```tsx
754
- import { createContext } from '@vertz/ui';
755
- import { createOutlet, type OutletContext } from '@vertz/ui/router';
335
+ ## Context
756
336
 
757
- const outletCtx = createContext<OutletContext>();
758
- const Outlet = createOutlet(outletCtx);
759
-
760
- const routes = defineRoutes({
761
- '/dashboard': {
762
- component: () => DashboardLayout(),
763
- children: {
764
- '/': {
765
- component: () => DashboardHome(),
766
- },
767
- '/settings': {
768
- component: () => DashboardSettings(),
769
- },
770
- },
771
- },
772
- });
773
- ```
774
-
775
- ### Search Params
337
+ Share values without passing props:
776
338
 
777
339
  ```tsx
778
- import { parseSearchParams, useSearchParams } from '@vertz/ui/router';
779
-
780
- // Parse raw URLSearchParams, optionally through a schema:
781
- const params = parseSearchParams(new URLSearchParams('?page=1&sort=name'), mySchema);
782
-
783
- // Read reactively inside an effect or computed:
784
- const search = useSearchParams(router.searchParams);
785
- ```
340
+ import { createContext, useContext } from '@vertz/ui';
786
341
 
787
- ### Type-Safe Params
342
+ const ThemeContext = createContext<'light' | 'dark'>('light');
788
343
 
789
- Route params are extracted from the path pattern at the type level:
344
+ function App() {
345
+ let theme = 'light';
790
346
 
791
- ```tsx
792
- import type { ExtractParams } from '@vertz/ui/router';
347
+ return (
348
+ <ThemeContext.Provider value={theme}>
349
+ <ThemeToggle />
350
+ </ThemeContext.Provider>
351
+ );
352
+ }
793
353
 
794
- type Params = ExtractParams<'/users/:id/posts/:postId'>;
795
- // => { id: string; postId: string }
354
+ function ThemeToggle() {
355
+ const theme = useContext(ThemeContext);
356
+ return <p>Current theme: {theme}</p>;
357
+ }
796
358
  ```
797
359
 
798
360
  ---
799
361
 
800
- ## Forms
801
-
802
- Import from `@vertz/ui/form` or the main `@vertz/ui` export.
362
+ ## Error Handling
803
363
 
804
364
  ```tsx
805
- import { form } from '@vertz/ui/form';
806
- import type { SdkMethod } from '@vertz/ui/form';
807
-
808
- // An SDK method with endpoint metadata (typically from @vertz/codegen):
809
- declare const createUser: SdkMethod<{ name: string; email: string }, { id: string }>;
810
-
811
- const userForm = form(createUser, {
812
- schema: userSchema, // any object with parse(data): T
813
- });
365
+ import { ErrorBoundary } from '@vertz/ui';
814
366
 
815
- function CreateUserForm() {
367
+ function App() {
816
368
  return (
817
- <form
818
- {...userForm.attrs()}
819
- onSubmit={userForm.handleSubmit({
820
- onSuccess: (result) => console.log('Created:', result.id),
821
- onError: (errors) => console.log('Errors:', errors),
822
- })}
369
+ <ErrorBoundary
370
+ fallback={(error, retry) => (
371
+ <div>
372
+ <p>Error: {error.message}</p>
373
+ <button onClick={retry}>Retry</button>
374
+ </div>
375
+ )}
823
376
  >
824
- <input name="name" />
825
- {userForm.error('name') && <span className="error">{userForm.error('name')}</span>}
826
-
827
- <input name="email" type="email" />
828
- {userForm.error('email') && <span className="error">{userForm.error('email')}</span>}
829
-
830
- <button type="submit" disabled={userForm.submitting.value}>
831
- {userForm.submitting.value ? 'Saving...' : 'Create'}
832
- </button>
833
- </form>
377
+ {() => <RiskyComponent />}
378
+ </ErrorBoundary>
834
379
  );
835
380
  }
836
381
  ```
837
382
 
838
- ### API
839
-
840
- - `form(sdkMethod, { schema })` -- creates a form instance
841
- - `.attrs()` -- returns `{ action, method }` for progressive enhancement
842
- - `.handleSubmit({ onSuccess?, onError? })` -- returns an event handler or accepts `FormData` directly
843
- - `.error(field)` -- returns the error message for a field (reactive)
844
- - `.submitting` -- `Signal<boolean>` for loading state
845
-
846
- The schema can be any object with a `parse(data: unknown): T` method (compatible with `@vertz/schema`). On failure, if the error has a `fieldErrors` property, those are surfaced per-field. Otherwise, a generic `_form` error is set.
847
-
848
383
  ---
849
384
 
850
- ## Lifecycle
851
-
852
- ### `onMount(callback)`
853
-
854
- Runs once when the component is created. Does not re-run on signal changes. Supports `onCleanup` inside for teardown:
855
-
856
- ```tsx
857
- import { onMount, onCleanup } from '@vertz/ui';
858
-
859
- function Timer() {
860
- let elapsed = 0;
861
-
862
- onMount(() => {
863
- const id = setInterval(() => elapsed++, 1000);
864
- onCleanup(() => clearInterval(id));
865
- });
866
-
867
- return <span>{elapsed}s</span>;
868
- }
869
- ```
870
-
871
- ### `onCleanup(fn)`
872
-
873
- Registers a teardown function with the current disposal scope. Called in LIFO order when the scope is disposed:
874
-
875
- ```tsx
876
- import { onCleanup } from '@vertz/ui';
877
-
878
- function WebSocketView() {
879
- const ws = new WebSocket('wss://example.com');
880
- onCleanup(() => ws.close());
881
- // ...
882
- }
883
- ```
884
-
885
- Must be called inside a disposal scope (`effect()`, `watch()`, `onMount()`, or a `pushScope()/popScope()` block). Throws `DisposalScopeError` if called outside a scope.
385
+ ## Advanced
886
386
 
887
- ### `watch(dep, callback)`
387
+ ### Watch
888
388
 
889
- Watches a reactive dependency and runs the callback whenever it changes. Runs immediately with the current value:
389
+ Watch a dependency and run a callback when it changes:
890
390
 
891
391
  ```tsx
892
- import { watch, onCleanup } from '@vertz/ui';
392
+ import { watch } from '@vertz/ui';
893
393
 
894
394
  function Logger() {
895
395
  let count = 0;
896
396
 
897
397
  watch(
898
398
  () => count,
899
- (value) => {
900
- console.log('count changed to', value);
901
- const id = setTimeout(() => console.log('delayed log', value), 1000);
902
- onCleanup(() => clearTimeout(id));
903
- }
399
+ (value) => console.log('count changed to', value)
904
400
  );
905
401
 
906
- return <button onClick={() => count++}>+</button>;
402
+ return <button onClick={() => count++}>Increment</button>;
907
403
  }
908
404
  ```
909
405
 
910
- The `dep` function is the only tracked dependency. The callback runs untracked, so signal reads inside it don't create additional subscriptions. Before each re-run, any `onCleanup` registered in the previous callback execution runs first.
406
+ ### Refs
911
407
 
912
- ### `ref()`
913
-
914
- Access a DOM element after creation:
408
+ Access DOM elements after mount:
915
409
 
916
410
  ```tsx
917
411
  import { ref, onMount } from '@vertz/ui';
918
412
 
919
- function FocusInput() {
413
+ function AutoFocus() {
920
414
  const inputRef = ref<HTMLInputElement>();
921
415
 
922
416
  onMount(() => {
923
417
  inputRef.current?.focus();
924
418
  });
925
419
 
926
- // Assign ref.current after element creation:
927
- const el = <input /> as HTMLInputElement;
928
- inputRef.current = el;
929
-
930
- return el;
420
+ return <input ref={inputRef} placeholder="Auto-focused" />;
931
421
  }
932
422
  ```
933
423
 
934
- ### Context
935
-
936
- Share values down the component tree without prop-drilling:
424
+ ---
937
425
 
938
- ```tsx
939
- import { createContext, useContext } from '@vertz/ui';
426
+ ## Testing
940
427
 
941
- const ThemeCtx = createContext<'light' | 'dark'>('light');
428
+ Import from `@vertz/ui/test`:
942
429
 
943
- function App() {
944
- const el = document.createDocumentFragment();
430
+ ```tsx
431
+ import { renderTest, findByText, click, waitFor } from '@vertz/ui/test';
945
432
 
946
- ThemeCtx.Provider('dark', () => {
947
- el.appendChild(ThemedCard());
948
- });
433
+ // Mount a component for testing
434
+ const { container, findByText, click, unmount } = renderTest(<Counter />);
949
435
 
950
- return el;
951
- }
436
+ // Query the DOM
437
+ const button = findByText('Increment');
438
+ await click(button);
439
+ const label = findByText('Count: 1');
952
440
 
953
- function ThemedCard() {
954
- const theme = useContext(ThemeCtx); // => 'dark'
955
- return <div className={theme === 'dark' ? 'card-dark' : 'card-light'}>Themed</div>;
956
- }
441
+ // Clean up
442
+ unmount();
957
443
  ```
958
444
 
959
- `useContext` works in both synchronous component code and inside `effect`/`watch` callbacks (the context scope is captured when the effect is created).
445
+ ### Query Helpers
960
446
 
961
- ### ErrorBoundary
447
+ | Export | Description |
448
+ |---|---|
449
+ | `findByTestId(id)` | Find element by `data-testid` — throws if not found |
450
+ | `findByText(text)` | Find element by text content — throws if not found |
451
+ | `queryByTestId(id)` | Find element by `data-testid` — returns `null` if not found |
452
+ | `queryByText(text)` | Find element by text content — returns `null` if not found |
453
+ | `waitFor(fn, options?)` | Retry an assertion until it passes |
962
454
 
963
- Catch errors thrown by child components:
455
+ ### Interaction Helpers
964
456
 
965
- ```tsx
966
- import { ErrorBoundary } from '@vertz/ui';
967
-
968
- function App() {
969
- return ErrorBoundary({
970
- children: () => RiskyComponent(),
971
- fallback: (error, retry) => (
972
- <div>
973
- <p>Something broke: {error.message}</p>
974
- <button onClick={retry}>Retry</button>
975
- </div>
976
- ),
977
- });
978
- }
979
- ```
457
+ | Export | Description |
458
+ |---|---|
459
+ | `click(el)` | Simulate a click event |
460
+ | `type(el, text)` | Simulate typing into an input |
461
+ | `press(key)` | Simulate a key press |
462
+ | `fillForm(form, values)` | Fill multiple form fields |
463
+ | `submitForm(form)` | Submit a form |
980
464
 
981
- The `retry` function re-invokes `children()` and swaps the result into the DOM if it succeeds.
982
-
983
- ### Suspense
984
-
985
- Handle async boundaries (components that throw promises):
465
+ ### Route Testing
986
466
 
987
467
  ```tsx
988
- import { Suspense } from '@vertz/ui';
468
+ import { createTestRouter } from '@vertz/ui/test';
989
469
 
990
- function App() {
991
- return Suspense({
992
- children: () => AsyncComponent(),
993
- fallback: () => <div>Loading...</div>,
994
- });
995
- }
996
- ```
470
+ const { component, router, navigate } = await createTestRouter(
471
+ {
472
+ '/': { component: () => <Home /> },
473
+ '/about': { component: () => <About /> },
474
+ },
475
+ { initialPath: '/' }
476
+ );
997
477
 
998
- If `children()` throws a `Promise`, the fallback is rendered. When the promise resolves, `children()` is called again and the result replaces the fallback in the DOM. Non-promise errors are re-thrown for `ErrorBoundary` to catch.
478
+ await navigate('/about');
479
+ ```
999
480
 
1000
481
  ---
1001
482
 
1002
- ## Primitives
483
+ ## JSX Runtime
1003
484
 
1004
- `@vertz/primitives` provides headless, WAI-ARIA compliant UI components. These are intentionally imperative -- they create pre-wired DOM elements with proper ARIA attributes, keyboard handling, and state management.
485
+ The `@vertz/ui/jsx-runtime` subpath provides the JSX factory used by the compiler. This is configured automatically by the Bun plugin you don't need to set it up manually.
1005
486
 
1006
- ```bash
1007
- npm install @vertz/primitives
1008
- ```
487
+ ---
1009
488
 
1010
- Available components: `Accordion`, `Button`, `Checkbox`, `Combobox`, `Dialog`, `Menu`, `Popover`, `Progress`, `Radio`, `Select`, `Slider`, `Switch`, `Tabs`, `Toast`, `Tooltip`.
489
+ ## Gotchas
1011
490
 
1012
- ### Usage Pattern
491
+ ### `onMount` runs synchronously
1013
492
 
1014
- Primitives return DOM elements and reactive state. Compose them with your JSX:
493
+ `onMount` fires during component initialization, not after DOM insertion. This means the DOM node exists but may not be in the document yet. If you need to measure layout or interact with the painted DOM, use `requestAnimationFrame` inside `onMount`:
1015
494
 
1016
495
  ```tsx
1017
- import { Button } from '@vertz/primitives';
1018
- import { css } from '@vertz/ui/css';
1019
-
1020
- const styles = css({
1021
- btn: ['px:4', 'py:2', 'bg:primary.600', 'text:foreground', 'rounded:md'],
1022
- });
1023
-
1024
- function MyButton() {
1025
- const { root, state } = Button.Root({
1026
- disabled: false,
1027
- onPress: () => console.log('pressed!'),
496
+ onMount(() => {
497
+ // DOM node exists but may not be painted yet
498
+ requestAnimationFrame(() => {
499
+ // Now it's safe to measure layout
1028
500
  });
1029
-
1030
- root.textContent = 'Click me';
1031
- root.classList.add(styles.classNames.btn);
1032
-
1033
- return root;
1034
- }
501
+ });
1035
502
  ```
1036
503
 
1037
- Primitives handle:
1038
- - ARIA roles and attributes (`role="button"`, `aria-pressed`, `aria-expanded`, etc.)
1039
- - Keyboard interaction (Enter/Space for buttons, arrow keys for menus, Escape for dialogs)
1040
- - Focus management
1041
- - State via signals (`state.disabled`, `state.pressed`, `state.open`, etc.)
1042
-
1043
- You provide the styling. They provide the behavior and accessibility.
1044
-
1045
- ---
504
+ ### Cleanup uses the return-callback pattern
1046
505
 
1047
- ## When to Use `effect()`
1048
-
1049
- `effect()` is a low-level reactive primitive. In most cases, the compiler handles reactivity for you through JSX. Reach for `effect()` only when you need side effects that the compiler cannot express:
1050
-
1051
- ### Appropriate Uses
506
+ Register cleanup logic by returning a function from `onMount`. This runs when the component unmounts:
1052
507
 
1053
508
  ```tsx
1054
- import { effect, onCleanup } from '@vertz/ui';
1055
-
1056
- function Analytics() {
1057
- let page = '/home';
1058
-
1059
- // Side effect: send analytics
1060
- effect(() => {
1061
- sendPageView(page);
1062
- });
1063
-
1064
- // Third-party library integration
1065
- effect(() => {
1066
- chart.updateData(chartData);
1067
- });
1068
-
1069
- // DOM operations the compiler can't handle
1070
- effect(() => {
1071
- element.scrollTo({ top: scrollPosition, behavior: 'smooth' });
1072
- });
1073
-
1074
- // localStorage sync
1075
- effect(() => {
1076
- localStorage.setItem('preference', preference);
1077
- });
1078
- }
509
+ onMount(() => {
510
+ const id = setInterval(() => seconds++, 1000);
511
+ return () => clearInterval(id);
512
+ });
1079
513
  ```
1080
514
 
1081
- ### NOT Appropriate
515
+ ### Primitives are uncontrolled only
1082
516
 
1083
- ```tsx
1084
- // DON'T: manual DOM text updates -- use JSX interpolation instead
1085
- effect(() => { span.textContent = String(count); });
1086
- // DO:
1087
- <span>{count}</span>
1088
-
1089
- // DON'T: manual attribute updates -- use JSX attributes instead
1090
- effect(() => { div.className = isActive ? 'on' : 'off'; });
1091
- // DO:
1092
- <div className={isActive ? 'on' : 'off'} />
1093
-
1094
- // DON'T: manual child rendering -- use JSX conditionals and .map() instead
1095
- effect(() => {
1096
- container.innerHTML = '';
1097
- if (show) container.appendChild(createChild());
1098
- });
1099
- ```
517
+ `@vertz/ui-primitives` components (Dialog, Select, Tabs, etc.) currently only support uncontrolled mode with `defaultValue` + callbacks. Controlled mode (where a parent prop overrides internal state) is not yet supported.
1100
518
 
1101
- `effect()` returns a dispose function. It auto-registers with the current disposal scope, so cleanup happens automatically when the parent scope is disposed:
519
+ ### Popover has no focus trap
1102
520
 
1103
- ```tsx
1104
- const dispose = effect(() => {
1105
- console.log('count is', count);
1106
- });
521
+ `Popover` focuses the first element on open but does not trap focus. Tab will move focus outside the popover. This is correct for non-modal popovers (tooltips, menus), but if you need modal behavior with a focus trap, use `Dialog` instead.
1107
522
 
1108
- // Manual cleanup if needed:
1109
- dispose();
1110
- ```
523
+ ---
1111
524
 
1112
- ### `batch()`
525
+ ## What You Don't Need to Know
1113
526
 
1114
- Group multiple signal writes to avoid redundant effect runs:
527
+ - How the compiler transforms your code
528
+ - Internal signal implementation details
529
+ - The reactive graph structure
530
+ - How dependency tracking works under the hood
1115
531
 
1116
- ```tsx
1117
- import { batch } from '@vertz/ui';
532
+ **Write ordinary JavaScript. The compiler handles the rest.**
1118
533
 
1119
- batch(() => {
1120
- firstName = 'Jane';
1121
- lastName = 'Doe';
1122
- age = 30;
1123
- });
1124
- // Effects that depend on any of these signals run once, not three times.
1125
- ```
534
+ ---
1126
535
 
1127
- ### `untrack()`
536
+ ## API Reference
537
+
538
+ ### Lifecycle
539
+
540
+ | Export | Description |
541
+ |---|---|
542
+ | `onMount` | Run code once when a component mounts (return a function for cleanup) |
543
+ | `watch` | Watch a dependency and run a callback on change |
544
+
545
+ ### Components
546
+
547
+ | Export | Description |
548
+ |---|---|
549
+ | `createContext` | Create a context for dependency injection |
550
+ | `useContext` | Read a context value |
551
+ | `children` | Access resolved children |
552
+ | `ref` | Create a ref for DOM element access |
553
+ | `ErrorBoundary` | Catch errors in a component tree |
554
+ | `Suspense` | Show fallback while async content loads |
555
+
556
+ ### Mounting
557
+
558
+ | Export | Description |
559
+ |---|---|
560
+ | `mount` | Mount an app to a DOM element |
561
+
562
+ ### CSS (`@vertz/ui/css`)
563
+
564
+ | Export | Description |
565
+ |---|---|
566
+ | `css` | Create scoped styles |
567
+ | `variants` | Create typed variant styles |
568
+ | `s` | Inline dynamic styles |
569
+ | `defineTheme` | Define a theme |
570
+ | `compileTheme` | Compile a theme to CSS |
571
+ | `ThemeProvider` | Provide a theme to descendants |
572
+ | `globalCss` | Inject global CSS |
573
+
574
+ ### Forms
575
+
576
+ | Export | Description |
577
+ |---|---|
578
+ | `form` | Create a form bound to an SDK method |
579
+ | `formDataToObject` | Convert FormData to a plain object |
580
+ | `validate` | Run schema validation |
581
+
582
+ ### Data
583
+
584
+ | Export | Description |
585
+ |---|---|
586
+ | `query` | Reactive data fetching |
587
+
588
+ ### Routing (`@vertz/ui/router`)
589
+
590
+ | Export | Description |
591
+ |---|---|
592
+ | `defineRoutes` | Define route configuration |
593
+ | `createRouter` | Create a router instance |
594
+ | `createLink` | Create a `<Link>` component |
595
+ | `createOutlet` | Create a route outlet |
596
+ | `parseSearchParams` | Parse URL search parameters |
597
+ | `useSearchParams` | Reactive search parameters |
598
+
599
+ ### Testing (`@vertz/ui/test`)
600
+
601
+ | Export | Description |
602
+ |---|---|
603
+ | `renderTest` | Mount a component for testing |
604
+ | `findByTestId` | Find element by `data-testid` (throws) |
605
+ | `findByText` | Find element by text content (throws) |
606
+ | `queryByTestId` | Find element by `data-testid` (nullable) |
607
+ | `queryByText` | Find element by text content (nullable) |
608
+ | `waitFor` | Retry an assertion until it passes |
609
+ | `click` | Simulate a click |
610
+ | `type` | Simulate typing |
611
+ | `press` | Simulate a key press |
612
+ | `fillForm` | Fill multiple form fields |
613
+ | `submitForm` | Submit a form |
614
+ | `createTestRouter` | Create a router for testing |
1128
615
 
1129
- Read a signal without subscribing to it:
616
+ ---
1130
617
 
1131
- ```tsx
1132
- import { untrack } from '@vertz/ui';
618
+ ## License
1133
619
 
1134
- effect(() => {
1135
- const tracked = count; // subscribes to count
1136
- const notTracked = untrack(() => other); // reads other without subscribing
1137
- });
1138
- ```
620
+ MIT