native-document 1.0.166 → 1.0.168

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 (108) hide show
  1. package/.vitepress/config.js +166 -0
  2. package/CHANGELOG.md +153 -0
  3. package/components.js +2 -1
  4. package/dist/native-document.components.min.js +495 -228
  5. package/dist/native-document.dev.js +7 -0
  6. package/dist/native-document.dev.js.map +1 -1
  7. package/dist/native-document.min.js +1 -1
  8. package/docs/advanced-components.md +213 -608
  9. package/docs/anchor.md +173 -312
  10. package/docs/cache.md +95 -803
  11. package/docs/cli.md +179 -0
  12. package/docs/components/accordion.md +172 -0
  13. package/docs/components/alert.md +99 -0
  14. package/docs/components/avatar.md +160 -0
  15. package/docs/components/badge.md +102 -0
  16. package/docs/components/breadcrumb.md +89 -0
  17. package/docs/components/button.md +183 -0
  18. package/docs/components/card.md +69 -0
  19. package/docs/components/context-menu.md +118 -0
  20. package/docs/components/data-table.md +345 -0
  21. package/docs/components/dropdown.md +214 -0
  22. package/docs/components/form/autocomplete-field.md +81 -0
  23. package/docs/components/form/checkbox-field.md +41 -0
  24. package/docs/components/form/checkbox-group-field.md +54 -0
  25. package/docs/components/form/color-field.md +64 -0
  26. package/docs/components/form/date-field.md +92 -0
  27. package/docs/components/form/field-collection.md +63 -0
  28. package/docs/components/form/file-field.md +203 -0
  29. package/docs/components/form/form-control.md +87 -0
  30. package/docs/components/form/image-field.md +90 -0
  31. package/docs/components/form/index.md +115 -0
  32. package/docs/components/form/number-field.md +65 -0
  33. package/docs/components/form/radio-field.md +51 -0
  34. package/docs/components/form/select-field.md +123 -0
  35. package/docs/components/form/slider.md +136 -0
  36. package/docs/components/form/string-field.md +134 -0
  37. package/docs/components/form/textarea-field.md +65 -0
  38. package/docs/components/form-fields.md +372 -0
  39. package/docs/components/getting-started.md +264 -0
  40. package/docs/components/index.md +337 -0
  41. package/docs/components/layout.md +279 -0
  42. package/docs/components/list.md +73 -0
  43. package/docs/components/menu.md +215 -0
  44. package/docs/components/modal.md +156 -0
  45. package/docs/components/pagination.md +95 -0
  46. package/docs/components/popover.md +131 -0
  47. package/docs/components/progress.md +111 -0
  48. package/docs/components/shortcut-manager.md +221 -0
  49. package/docs/components/simple-table.md +107 -0
  50. package/docs/components/skeleton.md +155 -0
  51. package/docs/components/spinner.md +100 -0
  52. package/docs/components/splitter.md +133 -0
  53. package/docs/components/stepper.md +163 -0
  54. package/docs/components/switch.md +113 -0
  55. package/docs/components/tabs.md +153 -0
  56. package/docs/components/toast.md +119 -0
  57. package/docs/components/tooltip.md +151 -0
  58. package/docs/components/traits.md +261 -0
  59. package/docs/conditional-rendering.md +170 -588
  60. package/docs/contributing.md +300 -25
  61. package/docs/core-concepts.md +205 -374
  62. package/docs/elements.md +251 -367
  63. package/docs/extending-native-document-element.md +192 -207
  64. package/docs/filters.md +153 -1122
  65. package/docs/getting-started.md +193 -267
  66. package/docs/i18n.md +241 -0
  67. package/docs/index.md +76 -0
  68. package/docs/lifecycle-events.md +143 -75
  69. package/docs/list-rendering.md +227 -852
  70. package/docs/memory-management.md +134 -47
  71. package/docs/native-document-element.md +337 -186
  72. package/docs/native-fetch.md +99 -630
  73. package/docs/observable-resource.md +364 -0
  74. package/docs/observables.md +592 -526
  75. package/docs/routing.md +244 -653
  76. package/docs/state-management.md +134 -241
  77. package/docs/svg-elements.md +231 -0
  78. package/docs/theming.md +409 -0
  79. package/docs/tutorials/.gitkeep +0 -0
  80. package/docs/validation.md +95 -97
  81. package/docs/vitepress-conventions.md +219 -0
  82. package/package.json +34 -13
  83. package/readme.md +269 -89
  84. package/src/components/card/Card.js +93 -39
  85. package/src/components/card/index.js +1 -1
  86. package/src/components/list/HasListItem.js +171 -0
  87. package/src/components/list/List.js +41 -107
  88. package/src/components/list/ListDivider.js +39 -0
  89. package/src/components/list/ListGroup.js +76 -59
  90. package/src/components/list/ListItem.js +117 -69
  91. package/src/components/list/index.js +3 -1
  92. package/src/components/list/types/ListItem.d.ts +45 -34
  93. package/src/components/spacer/Spacer.js +1 -1
  94. package/src/core/data/ObservableResource.js +5 -0
  95. package/src/core/data/observable-helpers/observable.prototypes.js +2 -0
  96. package/src/ui/components/card/CardRender.js +133 -0
  97. package/src/ui/components/card/card.css +169 -0
  98. package/src/ui/components/contextmenu/ContextmenuRender.js +1 -1
  99. package/src/ui/components/list/ListRender.js +18 -0
  100. package/src/ui/components/list/divider/ListDividerRender.js +10 -0
  101. package/src/ui/components/list/divider/list-divider.css +12 -0
  102. package/src/ui/components/list/group/ListGroupRender.js +61 -0
  103. package/src/ui/components/list/group/list-group.css +62 -0
  104. package/src/ui/components/list/item/ListItemRender.js +238 -0
  105. package/src/ui/components/list/item/list-item.css +191 -0
  106. package/src/ui/components/list/list.css +24 -0
  107. package/src/ui/components/spacer/SpacerRender.js +10 -0
  108. package/src/ui/index.js +8 -0
@@ -0,0 +1,372 @@
1
+ ---
2
+ title: Form Fields
3
+ description: Complete form field components with built-in validation - StringField, EmailField, PasswordField, NumberField, TextAreaField, DateField, TimeField, ColorField, RangeField, ImageField, AutocompleteField, HiddenField, FieldCollection, FormControl
4
+ ---
5
+
6
+ # Form Fields
7
+
8
+ ```javascript
9
+ import {
10
+ Field, FormControl, FieldCollection,
11
+ StringField, EmailField, PasswordField,
12
+ NumberField, TelField, UrlField, HiddenField,
13
+ TextAreaField, ColorField, DateField, TimeField,
14
+ RangeField, ImageField, AutocompleteField
15
+ } from '@native-document/components';
16
+ ```
17
+
18
+ All form fields extend `Field`, which provides a consistent API for labels, validation, binding, and rendering.
19
+
20
+ ---
21
+
22
+ ## `Field` - Base API
23
+
24
+ All fields share these methods:
25
+
26
+ ### Binding
27
+
28
+ ```javascript
29
+ .model(observable) // two-way binding - alias: .bind()
30
+ .default('value') // default value
31
+ ```
32
+
33
+ ### Labels & hints
34
+
35
+ ```javascript
36
+ .label('Email address')
37
+ .placeholder('you@example.com')
38
+ .help('We will never share your email.')
39
+ .hint('Required')
40
+ ```
41
+
42
+ ### State
43
+
44
+ ```javascript
45
+ .disabled(Observable(false))
46
+ .readonly(true)
47
+ .clearable() // show clear button
48
+ ```
49
+
50
+ ### Slots
51
+
52
+ ```javascript
53
+ .leading(Icon) // prefix content
54
+ .trailing(Icon) // suffix content
55
+ .bottom(Div('Helper text'))
56
+ ```
57
+
58
+ ### Element props
59
+
60
+ ```javascript
61
+ .wrapperProps({ class: 'form-group' })
62
+ .inputProps({ class: 'form-control', autocomplete: 'off' })
63
+ .labelProps({ class: 'form-label' })
64
+ .errorProps({ class: 'invalid-feedback' })
65
+ .hintProps({ class: 'form-text' })
66
+ ```
67
+
68
+ ### Validation
69
+
70
+ All fields use `HasValidation`:
71
+
72
+ ```javascript
73
+ .required('This field is required')
74
+ .requiredIf(condition, 'Required when condition is true')
75
+ .custom((value, allValues) => {
76
+ if (value.startsWith('bad')) {
77
+ return 'Value cannot start with "bad"';
78
+ }
79
+ return true; // pass
80
+ })
81
+ .validateOn('blur') // 'blur' | 'input' | 'change' (default: 'blur')
82
+ .clearErrorOn('focus') // 'focus' | 'input' (default: 'focus')
83
+ .showErrors()
84
+ .hideErrors()
85
+ .setError('Server-side error message')
86
+ .validate(allValues) // returns true | errors array
87
+ ```
88
+
89
+ ### Observables
90
+
91
+ ```javascript
92
+ field.value() // get current value
93
+ field.setValue('new value')
94
+ field.$description.hasErrors // Observable<boolean>
95
+ field.$description.errors // ObservableArray
96
+ field.$description.isDirty // Observable<boolean>
97
+ field.$description.isTouched // Observable<boolean>
98
+ field.$description.focus // Observable<boolean>
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Text Fields
104
+
105
+ ### `StringField(name, type?, props?)`
106
+
107
+ General text input. Type defaults to `'text'`.
108
+
109
+ ```javascript
110
+ StringField('username')
111
+ .label('Username')
112
+ .placeholder('Choose a username')
113
+ .model(username)
114
+ .required()
115
+ .minLength(3, 'At least 3 characters')
116
+ .maxLength(20)
117
+ .pattern(/^[a-z0-9_]+$/, 'Lowercase, numbers, underscores only')
118
+ .nd
119
+ ```
120
+
121
+ ### `EmailField(name)`
122
+
123
+ ```javascript
124
+ EmailField('email').label('Email').model(email).required().nd
125
+ ```
126
+
127
+ ### `PasswordField(name)`
128
+
129
+ ```javascript
130
+ PasswordField('password')
131
+ .label('Password')
132
+ .model(password)
133
+ .required()
134
+ .minLength(8)
135
+ .showToggle() // show/hide password button
136
+ .strengthIndicator() // password strength meter
137
+ .nd
138
+ ```
139
+
140
+ ### `TelField(name)` / `UrlField(name)` / `HiddenField(name)`
141
+
142
+ ```javascript
143
+ TelField('phone').label('Phone').model(phone).nd
144
+ UrlField('website').label('Website').model(url).nd
145
+ HiddenField('csrf').model(csrfToken).nd
146
+ ```
147
+
148
+ ---
149
+
150
+ ## `TextAreaField(name)`
151
+
152
+ ```javascript
153
+ TextAreaField('message')
154
+ .label('Message')
155
+ .placeholder('Write your message...')
156
+ .model(message)
157
+ .required()
158
+ .minLength(10)
159
+ .maxLength(500)
160
+ .rows(5)
161
+ .autoResize()
162
+ .nd
163
+ ```
164
+
165
+ ---
166
+
167
+ ## `NumberField(name)`
168
+
169
+ ```javascript
170
+ NumberField('quantity')
171
+ .label('Quantity')
172
+ .model(quantity)
173
+ .min(1, 'Minimum 1')
174
+ .max(100, 'Maximum 100')
175
+ .step(1)
176
+ .nd
177
+ ```
178
+
179
+ ---
180
+
181
+ ## `DateField(name)` / `TimeField(name)`
182
+
183
+ ```javascript
184
+ DateField('birthdate')
185
+ .label('Date of birth')
186
+ .model(birthdate)
187
+ .min('1900-01-01')
188
+ .max(new Date())
189
+ .format('DD/MM/YYYY')
190
+ .nd
191
+
192
+ TimeField('appointment')
193
+ .label('Time')
194
+ .model(time)
195
+ .min('09:00')
196
+ .max('18:00')
197
+ .clearable()
198
+ .nd
199
+ ```
200
+
201
+ ---
202
+
203
+ ## `ColorField(name)`
204
+
205
+ ```javascript
206
+ ColorField('brandColor')
207
+ .label('Brand Color')
208
+ .model(color)
209
+ .format('hex') // 'hex' | 'rgb' | 'hsl'
210
+ .nd
211
+ ```
212
+
213
+ ---
214
+
215
+ ## `RangeField(name)`
216
+
217
+ ```javascript
218
+ RangeField('volume')
219
+ .label('Volume')
220
+ .model(volume)
221
+ .min(0)
222
+ .max(100)
223
+ .step(5)
224
+ .showValue()
225
+ .nd
226
+ ```
227
+
228
+ ---
229
+
230
+ ## `ImageField(name)`
231
+
232
+ Image upload with preview:
233
+
234
+ ```javascript
235
+ ImageField('cover')
236
+ .label('Cover Image')
237
+ .model(imageUrl)
238
+ .accept(['image/jpeg', 'image/png'])
239
+ .maxSize(5 * 1024 * 1024)
240
+ .preview()
241
+ .nd
242
+ ```
243
+
244
+ ---
245
+
246
+ ## `AutocompleteField(name)`
247
+
248
+ ```javascript
249
+ AutocompleteField('country')
250
+ .label('Country')
251
+ .model(country)
252
+ .source(Observable.array(countries))
253
+ .searchKey('name')
254
+ .valueKey('code')
255
+ .placeholder('Search country...')
256
+ .minChars(2)
257
+ .nd
258
+ ```
259
+
260
+ ---
261
+
262
+ ## `FormControl` - Form Container
263
+
264
+ `FormControl` wraps multiple fields and provides group validation:
265
+
266
+ ```javascript
267
+ FormControl(props?)
268
+ .field(StringField('name').label('Name').required())
269
+ .field(EmailField('email').label('Email').required())
270
+ .field(PasswordField('password').label('Password').required())
271
+ .onSubmit(async (values) => {
272
+ const valid = await control.validate();
273
+ if (valid) {
274
+ await createUser(values);
275
+ }
276
+ })
277
+ .nd
278
+ ```
279
+
280
+ ### Methods
281
+
282
+ ```javascript
283
+ .field(fieldInstance) // add a field
284
+ .fields([field1, field2])
285
+ .validate() // validate all fields, returns true | false
286
+ .reset() // reset all fields
287
+ .values() // get { name: value } object
288
+ .getField('name') // get field by name
289
+ .onSubmit((values) => {})
290
+ .onReset(() => {})
291
+ .props({ class: 'form' })
292
+ ```
293
+
294
+ ---
295
+
296
+ ## `FieldCollection` - Repeatable Fields
297
+
298
+ A dynamic list of field groups that the user can add/remove:
299
+
300
+ ```javascript
301
+ FieldCollection('contacts')
302
+ .fields((group) => {
303
+ group.add(StringField('name').label('Name').required())
304
+ group.add(EmailField('email').label('Email'))
305
+ })
306
+ .data({ name: '', email: '' }) // default item
307
+ .model(contacts)
308
+ .renderAdd(() => Button('+ Add contact').ghost().nd)
309
+ .renderItem(($item, index, remove) =>
310
+ HStack([
311
+ $item.name.nd,
312
+ $item.email.nd,
313
+ Button('Remove').danger().small().nd.onClick(() => remove())
314
+ ]).spacing(8).nd
315
+ )
316
+ .nd
317
+ ```
318
+
319
+
320
+ ---
321
+
322
+ ## Theming
323
+
324
+ Tokens partagés par tous les champs (`StringField`, `NumberField`, `TextAreaField`, etc.) :
325
+
326
+ ```css
327
+ :root {
328
+ --field-gap: var(--space-cozy);
329
+ --field-font-size: var(--description-size);
330
+ --field-label-size: var(--note-size);
331
+ --field-label-color: var(--text-color);
332
+ --field-label-weight: 500;
333
+ --field-input-height: 36px;
334
+ --field-input-padding: 0 var(--space-comfortable);
335
+ --field-input-radius: var(--radius-button);
336
+ --field-input-border: var(--gray-lite-3);
337
+ --field-input-border-focus: var(--color-primary);
338
+ --field-input-bg: var(--background);
339
+ --field-input-color: var(--text-color);
340
+ --field-input-placeholder-color: var(--gray-lite-2);
341
+ --field-input-disabled-bg: var(--gray-lite-5);
342
+ --field-input-disabled-color: var(--gray-lite-2);
343
+ --field-error-size: var(--note-size);
344
+ --field-error-color: var(--color-danger);
345
+ --field-hint-size: var(--note-size);
346
+ --field-hint-color: var(--gray);
347
+ }
348
+ ```
349
+
350
+ Tokens spécifiques au `Slider` / `RangeField` :
351
+
352
+ ```css
353
+ :root {
354
+ --slider-track-height: 4px;
355
+ --slider-thumb-size: 18px;
356
+ --slider-fill-color: var(--color-primary);
357
+ --slider-track-color: var(--gray-lite-4);
358
+ --slider-thumb-color: var(--background);
359
+ --slider-thumb-border: var(--slider-fill-color);
360
+ --slider-vertical-height: 200px;
361
+ }
362
+ ```
363
+
364
+ Tokens spécifiques à `FileAvatarMode` :
365
+
366
+ ```css
367
+ :root {
368
+ --file-avatar-radius-circle: 50%;
369
+ --file-avatar-radius-square: 12px;
370
+ --file-avatar-badge-size: 26px;
371
+ }
372
+ ```
@@ -0,0 +1,264 @@
1
+ ---
2
+ title: Components - Getting Started
3
+ description: Set up NativeDocument components with default or custom renderers
4
+ ---
5
+
6
+ # Getting Started with Components
7
+
8
+ ## Import
9
+
10
+ Components are included in `native-document` - no separate installation needed:
11
+
12
+ ```javascript
13
+ import { Button, Modal, Tabs, Accordion } from 'native-document/components';
14
+ ```
15
+
16
+ To use the default theme and behavioral CSS:
17
+
18
+ ```javascript
19
+ import 'native-document/src/ui/theme.scss';
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Option 1 - Use the Default Renderers
25
+
26
+ The package ships a ready-to-use renderer for every component. Import them from `'native-document/ui'` and register them once at app startup.
27
+
28
+ Create `src/core/renderers.js`:
29
+
30
+ ```javascript
31
+ import {
32
+ Button, Alert, Badge, Spinner, Modal, Tabs,
33
+ Dropdown, DropdownItem, DropdownDivider, DropdownGroup,
34
+ Menu, MenuItem, MenuGroup, MenuLink, MenuDivider,
35
+ ContextMenu, Accordion, AccordionItem,
36
+ // ... all components you use
37
+ } from 'native-document/components';
38
+
39
+ import {
40
+ ButtonRender, AlertRender, BadgeRender, SpinnerRender, ModalRender, TabsRender,
41
+ DropdownRender, DropdownItemRender, DropdownDividerRender, DropdownGroupRender,
42
+ MenuRender, MenuItemRender, MenuGroupRender, MenuLinkRender, MenuDividerRender,
43
+ ContextMenuRender, contextMenuHandler,
44
+ AccordionRender, AccordionItemRender,
45
+ // ... matching renders
46
+ } from 'native-document/ui';
47
+
48
+ Button.use(ButtonRender);
49
+ Alert.use(AlertRender);
50
+ Badge.use(BadgeRender);
51
+ Spinner.use(SpinnerRender);
52
+ Modal.use(ModalRender);
53
+ Tabs.use(TabsRender);
54
+ Dropdown.use(DropdownRender);
55
+ DropdownItem.use(DropdownItemRender);
56
+ DropdownDivider.use(DropdownDividerRender);
57
+ DropdownGroup.use(DropdownGroupRender);
58
+ Menu.use(MenuRender);
59
+ MenuItem.use(MenuItemRender);
60
+ MenuGroup.use(MenuGroupRender);
61
+ MenuLink.use(MenuLinkRender);
62
+ MenuDivider.use(MenuDividerRender);
63
+ ContextMenu.use(ContextMenuRender, contextMenuHandler); // note: two arguments
64
+ Accordion.use(AccordionRender);
65
+ AccordionItem.use(AccordionItemRender);
66
+ // ...
67
+ ```
68
+
69
+ Then import it in `main.js` - renderers first, then the rest of the app:
70
+
71
+ ```javascript
72
+ import { Router } from 'native-document/router';
73
+ import './core/renderers.js';
74
+ // ... rest of app setup
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Option 2 - Write Your Own Renderers
80
+
81
+ Skip the defaults entirely and write renderers that match your own design system:
82
+
83
+ ```javascript
84
+ import { Button, Spinner } from 'native-document/components';
85
+ import { NativeButton, ShowIf } from 'native-document/elements';
86
+
87
+ Button.use(($description) => {
88
+ const classes = ['btn'];
89
+
90
+ if($description.variant) {
91
+ classes.push(`btn-${$description.variant}`);
92
+ }
93
+ if($description.size) {
94
+ classes.push(`btn-${$description.size}`);
95
+ }
96
+ if($description.block) {
97
+ classes.push('btn-block');
98
+ }
99
+ if($description.outline) {
100
+ classes.push('btn-outline');
101
+ }
102
+ if($description.borderRadiusType) {
103
+ classes.push(`btn-${$description.borderRadiusType}`);
104
+ }
105
+
106
+ return NativeButton({
107
+ type: $description.type || 'button',
108
+ class: classes.join(' '),
109
+ disabled: $description.disabled,
110
+ ...$description.props
111
+ }, [
112
+ ShowIf($description.loading, () => Spinner()),
113
+ $description.label
114
+ ]);
115
+ });
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Option 3 - Mix Both
121
+
122
+ Use the defaults for most components and override only the ones that need a custom look:
123
+
124
+ ```javascript
125
+ import { ButtonRender, AlertRender } from 'native-document/ui';
126
+
127
+ Alert.use(AlertRender);
128
+
129
+ Button.use(($description) => {
130
+ return NativeButton({
131
+ class: myTailwindClasses($description),
132
+ ...$description.props
133
+ }, $description.label);
134
+ });
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Using Components
140
+
141
+ Once renderers are registered, components work just like HTML elements. Only call `.nd` when you need to chain a specific `.nd` method:
142
+
143
+ ```javascript
144
+ import { Button, Alert } from 'native-document/components';
145
+
146
+ Div({ class: 'form' }, [
147
+ Alert('Please fix the errors below').error(),
148
+ Input({ type: 'text', value: name }),
149
+ Button('Submit')
150
+ .primary()
151
+ .loading(isLoading)
152
+ .nd
153
+ .onClick(() => submit())
154
+ ])
155
+ ```
156
+
157
+ ---
158
+
159
+ ## The `$description` Contract
160
+
161
+ Every renderer receives a `$description` object describing the component's current state. Here is what a fully configured `Button` looks like:
162
+
163
+ ```javascript
164
+ {
165
+ label: 'Submit',
166
+ type: 'submit', // 'button' | 'submit' | 'reset' | null
167
+ variant: 'primary',
168
+ size: 'large', // 'small' | 'medium' | 'large' | null
169
+ icon: SvgIcon, // DOM element | null
170
+ iconPosition: 'leading', // 'leading' | 'trailing' | 'top' | 'bottom'
171
+ iconOnly: false,
172
+ loading: Observable(false), // reactive
173
+ disabled: Observable(false), // reactive
174
+ outline: false,
175
+ block: false,
176
+ borderRadiusType: 'rounded',
177
+ props: {} // HTML attributes for the root element
178
+ render: null // per-instance renderer override
179
+ }
180
+ ```
181
+
182
+ Each component page documents its own `$description` structure.
183
+
184
+ ---
185
+
186
+ ## Renderer Tips
187
+
188
+ ### Use `$description` not `$d`
189
+
190
+ Use the full name `$description` in your renderers for clarity:
191
+
192
+ ```javascript
193
+ Button.use(($description) => {
194
+ return NativeButton({
195
+ class: buildClasses($description),
196
+ disabled: $description.disabled,
197
+ ...$description.props
198
+ }, $description.label);
199
+ });
200
+ ```
201
+
202
+ ### Spread `$description.props` last
203
+
204
+ Always spread `$description.props` last so per-instance attributes can override defaults:
205
+
206
+ ```javascript
207
+ NativeButton({
208
+ type: 'button',
209
+ class: buildClasses($description),
210
+ ...$description.props
211
+ }, $description.label)
212
+ ```
213
+
214
+ ### Observable values are reactive
215
+
216
+ Some `$description` values are observables (`loading`, `disabled`, etc.). Pass them directly as attributes - NativeDocument handles the reactive binding:
217
+
218
+ ```javascript
219
+ NativeButton({
220
+ disabled: $description.disabled, // Observable<boolean> - reactive
221
+ class: buildClasses($description)
222
+ }, $description.label)
223
+ ```
224
+
225
+ ### Per-instance override
226
+
227
+ A component can override the global renderer for a specific instance via `.render()`:
228
+
229
+ ```javascript
230
+ Button('Special')
231
+ .render(($description) => NativeButton({ class: 'special-btn' }, $description.label))
232
+ .nd.onClick(() => doSomething())
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Presets
238
+
239
+ Presets create named factory shortcuts for commonly configured variants:
240
+
241
+ ```javascript
242
+ Button.preset('save', (label, props) => {
243
+ return Button(label || 'Save', props).primary();
244
+ });
245
+ Button.preset('cancel', (label, props) => {
246
+ return Button(label || 'Cancel', props).ghost();
247
+ });
248
+ Button.preset('delete', (label, props) => {
249
+ return Button(label || 'Delete', props).danger().outline();
250
+ });
251
+
252
+ Button.save()
253
+ Button.cancel('Go back')
254
+ Button.delete('Remove account')
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Next Steps
260
+
261
+ - **[Traits](./traits.md)** - HasEventEmitter, HasDraggable, HasResizable
262
+ - **[Button](./button.md)** - Full Button API
263
+ - **[Layout](./layout.md)** - Stack, Row, Col, Divider
264
+ - **[Components Overview](./index.md)** - Philosophy and BaseComponent API