@vertz/ui-primitives 0.1.1
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/README.md +541 -0
- package/dist/index.d.ts +295 -0
- package/dist/index.js +1215 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# @vertz/primitives
|
|
2
|
+
|
|
3
|
+
Headless, WAI-ARIA compliant UI primitives for Vertz. Provides pre-built, accessible components with keyboard handling, focus management, and reactive state — you bring the styles.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`@vertz/primitives` offers low-level UI components that handle:
|
|
8
|
+
|
|
9
|
+
- **Accessibility** — Proper ARIA roles, attributes, and live regions
|
|
10
|
+
- **Keyboard interaction** — Enter/Space for activation, Arrow keys for navigation, Escape for closing
|
|
11
|
+
- **Focus management** — Tab order, focus trapping, roving tabindex
|
|
12
|
+
- **State management** — Reactive state via `@vertz/ui` signals
|
|
13
|
+
|
|
14
|
+
These primitives are **intentionally imperative** — they return pre-wired DOM elements that you compose with your JSX. This gives you full control over styling and layout while ensuring accessibility best practices.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @vertz/primitives
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This package depends on `@vertz/ui` for reactive state.
|
|
23
|
+
|
|
24
|
+
## Available Primitives
|
|
25
|
+
|
|
26
|
+
| Primitive | Description |
|
|
27
|
+
|-----------|-------------|
|
|
28
|
+
| **Accordion** | Collapsible sections with single or multiple expansion |
|
|
29
|
+
| **Button** | Accessible button with press state |
|
|
30
|
+
| **Checkbox** | Checkbox with indeterminate state support |
|
|
31
|
+
| **Combobox** | Searchable dropdown with filtering |
|
|
32
|
+
| **Dialog** | Modal dialog with focus trap and backdrop |
|
|
33
|
+
| **Menu** | Dropdown menu with arrow key navigation |
|
|
34
|
+
| **Popover** | Floating content anchored to a trigger |
|
|
35
|
+
| **Progress** | Progress bar with indeterminate mode |
|
|
36
|
+
| **Radio** | Radio button group with arrow key navigation |
|
|
37
|
+
| **Select** | Single-select dropdown |
|
|
38
|
+
| **Slider** | Range input with thumb and track |
|
|
39
|
+
| **Switch** | Toggle switch (on/off) |
|
|
40
|
+
| **Tabs** | Tabbed interface with keyboard navigation |
|
|
41
|
+
| **Toast** | Notification toast with auto-dismiss |
|
|
42
|
+
| **Tooltip** | Hover tooltip with pointer positioning |
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Basic Pattern
|
|
47
|
+
|
|
48
|
+
All primitives follow this structure:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { Button } from '@vertz/primitives';
|
|
52
|
+
|
|
53
|
+
// Create the primitive
|
|
54
|
+
const { root, state } = Button.Root({
|
|
55
|
+
disabled: false,
|
|
56
|
+
onPress: () => console.log('Pressed!'),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Customize the element
|
|
60
|
+
root.textContent = 'Click Me';
|
|
61
|
+
root.classList.add('my-button-class');
|
|
62
|
+
|
|
63
|
+
// Use in JSX or append to DOM
|
|
64
|
+
document.body.appendChild(root);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Key concepts:**
|
|
68
|
+
|
|
69
|
+
- `Root()` returns an object with DOM elements and reactive state
|
|
70
|
+
- Elements have ARIA attributes and event handlers pre-configured
|
|
71
|
+
- `state` contains reactive signals you can read or modify
|
|
72
|
+
- You control styling, layout, and composition
|
|
73
|
+
|
|
74
|
+
### Button
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { Button } from '@vertz/primitives';
|
|
78
|
+
import { css } from '@vertz/ui/css';
|
|
79
|
+
|
|
80
|
+
const styles = css({
|
|
81
|
+
btn: ['px:4', 'py:2', 'bg:blue.600', 'text:white', 'rounded:md'],
|
|
82
|
+
disabled: ['opacity:50', 'cursor:not-allowed'],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function MyButton() {
|
|
86
|
+
const { root, state } = Button.Root({
|
|
87
|
+
disabled: false,
|
|
88
|
+
onPress: () => alert('Button pressed!'),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
root.textContent = 'Submit';
|
|
92
|
+
root.classList.add(styles.classNames.btn);
|
|
93
|
+
|
|
94
|
+
// React to state changes
|
|
95
|
+
effect(() => {
|
|
96
|
+
if (state.disabled.value) {
|
|
97
|
+
root.classList.add(styles.classNames.disabled);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return root;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Button API:**
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
Button.Root(options: ButtonOptions): {
|
|
109
|
+
root: HTMLButtonElement;
|
|
110
|
+
state: {
|
|
111
|
+
disabled: Signal<boolean>;
|
|
112
|
+
pressed: Signal<boolean>;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Checkbox
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { Checkbox } from '@vertz/primitives';
|
|
121
|
+
|
|
122
|
+
function TodoItem() {
|
|
123
|
+
let isDone = false;
|
|
124
|
+
|
|
125
|
+
const { root, state } = Checkbox.Root({
|
|
126
|
+
checked: isDone,
|
|
127
|
+
onCheckedChange: (checked) => {
|
|
128
|
+
isDone = checked === true;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const label = document.createElement('label');
|
|
133
|
+
label.textContent = 'Complete task';
|
|
134
|
+
label.prepend(root);
|
|
135
|
+
|
|
136
|
+
return label;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Checkbox API:**
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
Checkbox.Root(options: CheckboxOptions): {
|
|
144
|
+
root: HTMLInputElement;
|
|
145
|
+
state: {
|
|
146
|
+
checked: Signal<CheckedState>; // true | false | 'indeterminate'
|
|
147
|
+
disabled: Signal<boolean>;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Dialog
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { Dialog } from '@vertz/primitives';
|
|
156
|
+
|
|
157
|
+
function LoginDialog() {
|
|
158
|
+
const { overlay, content, state } = Dialog.Root({
|
|
159
|
+
open: false,
|
|
160
|
+
onOpenChange: (open) => console.log(`Dialog ${open ? 'opened' : 'closed'}`),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
content.innerHTML = `
|
|
164
|
+
<h2>Log In</h2>
|
|
165
|
+
<input type="email" placeholder="Email" />
|
|
166
|
+
<input type="password" placeholder="Password" />
|
|
167
|
+
<button>Submit</button>
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const trigger = document.createElement('button');
|
|
171
|
+
trigger.textContent = 'Open Dialog';
|
|
172
|
+
trigger.onclick = () => state.open.value = true;
|
|
173
|
+
|
|
174
|
+
const container = document.createElement('div');
|
|
175
|
+
container.appendChild(trigger);
|
|
176
|
+
container.appendChild(overlay);
|
|
177
|
+
|
|
178
|
+
return container;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Dialog API:**
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
Dialog.Root(options: DialogOptions): {
|
|
186
|
+
overlay: HTMLDivElement; // Backdrop element
|
|
187
|
+
content: HTMLDivElement; // Dialog content (focus trapped)
|
|
188
|
+
state: {
|
|
189
|
+
open: Signal<boolean>;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The dialog handles:
|
|
195
|
+
- Focus trapping (Tab cycles through focusable elements)
|
|
196
|
+
- Escape key to close
|
|
197
|
+
- Click outside overlay to close
|
|
198
|
+
- Backdrop element with `aria-hidden` when open
|
|
199
|
+
|
|
200
|
+
### Menu
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { Menu } from '@vertz/primitives';
|
|
204
|
+
|
|
205
|
+
function Dropdown() {
|
|
206
|
+
const { trigger, content, state } = Menu.Root({
|
|
207
|
+
open: false,
|
|
208
|
+
onOpenChange: (open) => console.log(`Menu ${open ? 'opened' : 'closed'}`),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
trigger.textContent = 'Actions';
|
|
212
|
+
|
|
213
|
+
const item1 = Menu.Item({
|
|
214
|
+
onSelect: () => console.log('Edit clicked'),
|
|
215
|
+
});
|
|
216
|
+
item1.textContent = 'Edit';
|
|
217
|
+
|
|
218
|
+
const item2 = Menu.Item({
|
|
219
|
+
onSelect: () => console.log('Delete clicked'),
|
|
220
|
+
});
|
|
221
|
+
item2.textContent = 'Delete';
|
|
222
|
+
|
|
223
|
+
content.append(item1, item2);
|
|
224
|
+
|
|
225
|
+
const container = document.createElement('div');
|
|
226
|
+
container.append(trigger, content);
|
|
227
|
+
return container;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Menu API:**
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
Menu.Root(options: MenuOptions): {
|
|
235
|
+
trigger: HTMLButtonElement;
|
|
236
|
+
content: HTMLDivElement;
|
|
237
|
+
state: {
|
|
238
|
+
open: Signal<boolean>;
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Menu.Item(options: { onSelect?: () => void }): HTMLDivElement
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Menu handles:
|
|
246
|
+
- Arrow Up/Down navigation
|
|
247
|
+
- Enter/Space to select
|
|
248
|
+
- Escape to close
|
|
249
|
+
- Auto-focus first item on open
|
|
250
|
+
|
|
251
|
+
### Select
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { Select } from '@vertz/primitives';
|
|
255
|
+
|
|
256
|
+
function LanguageSelect() {
|
|
257
|
+
const { trigger, content, state } = Select.Root({
|
|
258
|
+
value: 'en',
|
|
259
|
+
onValueChange: (value) => console.log(`Selected: ${value}`),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const options = [
|
|
263
|
+
{ value: 'en', label: 'English' },
|
|
264
|
+
{ value: 'es', label: 'Spanish' },
|
|
265
|
+
{ value: 'fr', label: 'French' },
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
for (const opt of options) {
|
|
269
|
+
const option = Select.Option({ value: opt.value });
|
|
270
|
+
option.textContent = opt.label;
|
|
271
|
+
content.appendChild(option);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return <div>{trigger}{content}</div>;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Select API:**
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
Select.Root(options: SelectOptions): {
|
|
282
|
+
trigger: HTMLButtonElement;
|
|
283
|
+
content: HTMLDivElement;
|
|
284
|
+
state: {
|
|
285
|
+
value: Signal<string>;
|
|
286
|
+
open: Signal<boolean>;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
Select.Option(options: { value: string }): HTMLDivElement
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Tabs
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
import { Tabs } from '@vertz/primitives';
|
|
297
|
+
|
|
298
|
+
function Settings() {
|
|
299
|
+
const { list, state } = Tabs.Root({
|
|
300
|
+
defaultValue: 'general',
|
|
301
|
+
onValueChange: (value) => console.log(`Tab: ${value}`),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const tab1 = Tabs.Trigger({ value: 'general' });
|
|
305
|
+
tab1.textContent = 'General';
|
|
306
|
+
|
|
307
|
+
const tab2 = Tabs.Trigger({ value: 'privacy' });
|
|
308
|
+
tab2.textContent = 'Privacy';
|
|
309
|
+
|
|
310
|
+
list.append(tab1, tab2);
|
|
311
|
+
|
|
312
|
+
const panel1 = Tabs.Content({ value: 'general' });
|
|
313
|
+
panel1.textContent = 'General settings...';
|
|
314
|
+
|
|
315
|
+
const panel2 = Tabs.Content({ value: 'privacy' });
|
|
316
|
+
panel2.textContent = 'Privacy settings...';
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div>
|
|
320
|
+
{list}
|
|
321
|
+
{panel1}
|
|
322
|
+
{panel2}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Tabs API:**
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
Tabs.Root(options: TabsOptions): {
|
|
332
|
+
list: HTMLDivElement;
|
|
333
|
+
state: {
|
|
334
|
+
value: Signal<string>;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
Tabs.Trigger(options: { value: string }): HTMLButtonElement
|
|
339
|
+
Tabs.Content(options: { value: string }): HTMLDivElement
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Tabs handle:
|
|
343
|
+
- Arrow Left/Right navigation
|
|
344
|
+
- Home/End keys to jump to first/last tab
|
|
345
|
+
- Automatic panel switching
|
|
346
|
+
- Proper `aria-selected` and `aria-controls` attributes
|
|
347
|
+
|
|
348
|
+
## Advanced: Custom Primitives
|
|
349
|
+
|
|
350
|
+
If you need to build your own headless components, use the utilities from `@vertz/primitives/utils`:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import {
|
|
354
|
+
uniqueId,
|
|
355
|
+
setLabelledBy,
|
|
356
|
+
setDescribedBy,
|
|
357
|
+
getFocusableElements,
|
|
358
|
+
trapFocus,
|
|
359
|
+
handleListNavigation,
|
|
360
|
+
Keys,
|
|
361
|
+
} from '@vertz/primitives/utils';
|
|
362
|
+
|
|
363
|
+
function CustomCombobox() {
|
|
364
|
+
const inputId = uniqueId();
|
|
365
|
+
const listboxId = uniqueId();
|
|
366
|
+
|
|
367
|
+
const input = document.createElement('input');
|
|
368
|
+
input.id = inputId;
|
|
369
|
+
input.setAttribute('role', 'combobox');
|
|
370
|
+
input.setAttribute('aria-controls', listboxId);
|
|
371
|
+
|
|
372
|
+
const listbox = document.createElement('ul');
|
|
373
|
+
listbox.id = listboxId;
|
|
374
|
+
listbox.setAttribute('role', 'listbox');
|
|
375
|
+
setLabelledBy(listbox, inputId);
|
|
376
|
+
|
|
377
|
+
input.addEventListener('keydown', (e) => {
|
|
378
|
+
if (Keys.isArrowDown(e)) {
|
|
379
|
+
const focusable = getFocusableElements(listbox);
|
|
380
|
+
focusable[0]?.focus();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return { input, listbox };
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Available Utilities
|
|
389
|
+
|
|
390
|
+
**ARIA Helpers:**
|
|
391
|
+
|
|
392
|
+
- `setLabelledBy(el, id)` — Set `aria-labelledby`
|
|
393
|
+
- `setDescribedBy(el, id)` — Set `aria-describedby`
|
|
394
|
+
- `setControls(el, id)` — Set `aria-controls`
|
|
395
|
+
- `setExpanded(el, expanded)` — Set `aria-expanded`
|
|
396
|
+
- `setSelected(el, selected)` — Set `aria-selected`
|
|
397
|
+
- `setChecked(el, checked)` — Set `aria-checked`
|
|
398
|
+
- `setDisabled(el, disabled)` — Set `aria-disabled` and `disabled` attribute
|
|
399
|
+
- `setHidden(el, hidden)` — Set `aria-hidden`
|
|
400
|
+
- `setDataState(el, state)` — Set `data-state` attribute
|
|
401
|
+
- `setValueRange(el, value, min, max)` — Set `aria-valuenow/min/max`
|
|
402
|
+
- `toggleExpanded(el)` — Toggle `aria-expanded`
|
|
403
|
+
|
|
404
|
+
**Focus Management:**
|
|
405
|
+
|
|
406
|
+
- `getFocusableElements(container)` — Get all focusable elements in a container
|
|
407
|
+
- `focusFirst(container)` — Focus the first focusable element
|
|
408
|
+
- `saveFocus()` — Save the currently focused element (returns restore function)
|
|
409
|
+
- `trapFocus(container)` — Trap focus within a container (returns cleanup function)
|
|
410
|
+
- `setRovingTabindex(elements, activeIndex)` — Set roving tabindex pattern
|
|
411
|
+
|
|
412
|
+
**ID Generation:**
|
|
413
|
+
|
|
414
|
+
- `uniqueId(prefix?)` — Generate unique ID (e.g., `vertz-1`, `vertz-2`)
|
|
415
|
+
- `linkedIds(prefix, count)` — Generate multiple related IDs
|
|
416
|
+
- `resetIdCounter()` — Reset counter (useful for testing)
|
|
417
|
+
|
|
418
|
+
**Keyboard Handling:**
|
|
419
|
+
|
|
420
|
+
- `handleActivation(event, callback)` — Call callback on Enter/Space
|
|
421
|
+
- `handleListNavigation(event, items, currentIndex, onChange)` — Arrow key navigation
|
|
422
|
+
- `Keys` — Key name constants (`Keys.Enter`, `Keys.Escape`, `Keys.ArrowUp`, etc.)
|
|
423
|
+
- `isKey(event, key)` — Check if event matches key
|
|
424
|
+
|
|
425
|
+
## State Management
|
|
426
|
+
|
|
427
|
+
All primitives use `@vertz/ui` signals for state. You can read and modify state imperatively:
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
const { state } = Dialog.Root({ open: false });
|
|
431
|
+
|
|
432
|
+
// Read
|
|
433
|
+
console.log(state.open.value); // false
|
|
434
|
+
|
|
435
|
+
// Write
|
|
436
|
+
state.open.value = true;
|
|
437
|
+
|
|
438
|
+
// React
|
|
439
|
+
effect(() => {
|
|
440
|
+
console.log(`Dialog is ${state.open.value ? 'open' : 'closed'}`);
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
State properties are read-only at the type level but writable at runtime (for internal use). Prefer using the component's callbacks (`onOpenChange`, `onValueChange`, etc.) for external state changes.
|
|
445
|
+
|
|
446
|
+
## Styling
|
|
447
|
+
|
|
448
|
+
Primitives provide **zero styles**. You can style them with:
|
|
449
|
+
|
|
450
|
+
- **CSS classes** — `root.classList.add('my-button')`
|
|
451
|
+
- **Inline styles** — `root.style.padding = '8px'`
|
|
452
|
+
- **@vertz/ui/css** — `root.classList.add(css({ btn: [...] }).classNames.btn)`
|
|
453
|
+
- **Tailwind/UnoCSS** — `root.className = 'px-4 py-2 bg-blue-500'`
|
|
454
|
+
|
|
455
|
+
**Data attributes for styling:**
|
|
456
|
+
|
|
457
|
+
Most primitives set `data-state` attributes you can use for CSS:
|
|
458
|
+
|
|
459
|
+
```css
|
|
460
|
+
button[data-state="idle"] { /* ... */ }
|
|
461
|
+
button[data-state="pressed"] { /* ... */ }
|
|
462
|
+
button[data-state="disabled"] { /* ... */ }
|
|
463
|
+
|
|
464
|
+
[aria-expanded="true"] { /* ... */ }
|
|
465
|
+
[aria-checked="true"] { /* ... */ }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Accessibility
|
|
469
|
+
|
|
470
|
+
All primitives follow WAI-ARIA authoring practices:
|
|
471
|
+
|
|
472
|
+
- **Buttons** — `role="button"`, Enter/Space activation
|
|
473
|
+
- **Checkboxes** — Native `<input type="checkbox">` with `aria-checked`
|
|
474
|
+
- **Dialogs** — Focus trap, Escape to close, `aria-modal`, `aria-labelledby`
|
|
475
|
+
- **Menus** — Arrow key navigation, `role="menu"`, `role="menuitem"`
|
|
476
|
+
- **Tabs** — Arrow navigation, `aria-selected`, `aria-controls`, `role="tab"` / `role="tabpanel"`
|
|
477
|
+
- **Sliders** — `aria-valuenow/min/max`, Arrow keys for adjustment
|
|
478
|
+
- **Comboboxes** — `aria-autocomplete`, `aria-expanded`, `aria-controls`
|
|
479
|
+
|
|
480
|
+
Keyboard navigation is always included where applicable.
|
|
481
|
+
|
|
482
|
+
## Type Definitions
|
|
483
|
+
|
|
484
|
+
Each primitive exports:
|
|
485
|
+
|
|
486
|
+
- **`[Component].Root(options)`** — Creates the primitive with options
|
|
487
|
+
- **`[Component]Options`** — Options interface
|
|
488
|
+
- **`[Component]State`** — Reactive state interface
|
|
489
|
+
- **`[Component]Elements`** — DOM element references
|
|
490
|
+
|
|
491
|
+
Example:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
import type { ButtonOptions, ButtonState, ButtonElements } from '@vertz/primitives';
|
|
495
|
+
|
|
496
|
+
const options: ButtonOptions = {
|
|
497
|
+
disabled: false,
|
|
498
|
+
onPress: () => {},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const button: ButtonElements & { state: ButtonState } = Button.Root(options);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Performance
|
|
505
|
+
|
|
506
|
+
Primitives are **lightweight**:
|
|
507
|
+
|
|
508
|
+
- No virtual DOM
|
|
509
|
+
- No framework overhead
|
|
510
|
+
- Direct DOM manipulation
|
|
511
|
+
- Fine-grained reactivity via signals
|
|
512
|
+
- Event delegation where applicable
|
|
513
|
+
|
|
514
|
+
A typical primitive (e.g., `Button`) is ~2KB gzipped including reactivity.
|
|
515
|
+
|
|
516
|
+
## Relationship to @vertz/ui
|
|
517
|
+
|
|
518
|
+
`@vertz/primitives` builds on top of `@vertz/ui`:
|
|
519
|
+
|
|
520
|
+
- Uses `signal()` and `effect()` for reactive state
|
|
521
|
+
- Compatible with Vertz's JSX and compiler
|
|
522
|
+
- Can be composed with `@vertz/ui` components
|
|
523
|
+
|
|
524
|
+
However, primitives are **intentionally imperative** — they return DOM elements rather than JSX components. This gives you full control over composition and styling while maintaining accessibility.
|
|
525
|
+
|
|
526
|
+
## Related Packages
|
|
527
|
+
|
|
528
|
+
- **[@vertz/ui](../ui)** — The main UI framework (JSX, reactivity, compiler)
|
|
529
|
+
- **[@vertz/ui-compiler](../ui-compiler)** — Vite plugin for compiling Vertz components
|
|
530
|
+
|
|
531
|
+
## Inspiration
|
|
532
|
+
|
|
533
|
+
This package is inspired by:
|
|
534
|
+
|
|
535
|
+
- [Radix UI](https://www.radix-ui.com/) — Unstyled, accessible components
|
|
536
|
+
- [Headless UI](https://headlessui.com/) — Unstyled component behaviors
|
|
537
|
+
- [Ark UI](https://ark-ui.com/) — Framework-agnostic UI primitives
|
|
538
|
+
|
|
539
|
+
## License
|
|
540
|
+
|
|
541
|
+
MIT
|