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.
- package/.vitepress/config.js +166 -0
- package/CHANGELOG.md +153 -0
- package/components.js +2 -1
- package/dist/native-document.components.min.js +495 -228
- package/dist/native-document.dev.js +7 -0
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +213 -608
- package/docs/anchor.md +173 -312
- package/docs/cache.md +95 -803
- package/docs/cli.md +179 -0
- package/docs/components/accordion.md +172 -0
- package/docs/components/alert.md +99 -0
- package/docs/components/avatar.md +160 -0
- package/docs/components/badge.md +102 -0
- package/docs/components/breadcrumb.md +89 -0
- package/docs/components/button.md +183 -0
- package/docs/components/card.md +69 -0
- package/docs/components/context-menu.md +118 -0
- package/docs/components/data-table.md +345 -0
- package/docs/components/dropdown.md +214 -0
- package/docs/components/form/autocomplete-field.md +81 -0
- package/docs/components/form/checkbox-field.md +41 -0
- package/docs/components/form/checkbox-group-field.md +54 -0
- package/docs/components/form/color-field.md +64 -0
- package/docs/components/form/date-field.md +92 -0
- package/docs/components/form/field-collection.md +63 -0
- package/docs/components/form/file-field.md +203 -0
- package/docs/components/form/form-control.md +87 -0
- package/docs/components/form/image-field.md +90 -0
- package/docs/components/form/index.md +115 -0
- package/docs/components/form/number-field.md +65 -0
- package/docs/components/form/radio-field.md +51 -0
- package/docs/components/form/select-field.md +123 -0
- package/docs/components/form/slider.md +136 -0
- package/docs/components/form/string-field.md +134 -0
- package/docs/components/form/textarea-field.md +65 -0
- package/docs/components/form-fields.md +372 -0
- package/docs/components/getting-started.md +264 -0
- package/docs/components/index.md +337 -0
- package/docs/components/layout.md +279 -0
- package/docs/components/list.md +73 -0
- package/docs/components/menu.md +215 -0
- package/docs/components/modal.md +156 -0
- package/docs/components/pagination.md +95 -0
- package/docs/components/popover.md +131 -0
- package/docs/components/progress.md +111 -0
- package/docs/components/shortcut-manager.md +221 -0
- package/docs/components/simple-table.md +107 -0
- package/docs/components/skeleton.md +155 -0
- package/docs/components/spinner.md +100 -0
- package/docs/components/splitter.md +133 -0
- package/docs/components/stepper.md +163 -0
- package/docs/components/switch.md +113 -0
- package/docs/components/tabs.md +153 -0
- package/docs/components/toast.md +119 -0
- package/docs/components/tooltip.md +151 -0
- package/docs/components/traits.md +261 -0
- package/docs/conditional-rendering.md +170 -588
- package/docs/contributing.md +300 -25
- package/docs/core-concepts.md +205 -374
- package/docs/elements.md +251 -367
- package/docs/extending-native-document-element.md +192 -207
- package/docs/filters.md +153 -1122
- package/docs/getting-started.md +193 -267
- package/docs/i18n.md +241 -0
- package/docs/index.md +76 -0
- package/docs/lifecycle-events.md +143 -75
- package/docs/list-rendering.md +227 -852
- package/docs/memory-management.md +134 -47
- package/docs/native-document-element.md +337 -186
- package/docs/native-fetch.md +99 -630
- package/docs/observable-resource.md +364 -0
- package/docs/observables.md +592 -526
- package/docs/routing.md +244 -653
- package/docs/state-management.md +134 -241
- package/docs/svg-elements.md +231 -0
- package/docs/theming.md +409 -0
- package/docs/tutorials/.gitkeep +0 -0
- package/docs/validation.md +95 -97
- package/docs/vitepress-conventions.md +219 -0
- package/package.json +34 -13
- package/readme.md +269 -89
- package/src/components/card/Card.js +93 -39
- package/src/components/card/index.js +1 -1
- package/src/components/list/HasListItem.js +171 -0
- package/src/components/list/List.js +41 -107
- package/src/components/list/ListDivider.js +39 -0
- package/src/components/list/ListGroup.js +76 -59
- package/src/components/list/ListItem.js +117 -69
- package/src/components/list/index.js +3 -1
- package/src/components/list/types/ListItem.d.ts +45 -34
- package/src/components/spacer/Spacer.js +1 -1
- package/src/core/data/ObservableResource.js +5 -0
- package/src/core/data/observable-helpers/observable.prototypes.js +2 -0
- package/src/ui/components/card/CardRender.js +133 -0
- package/src/ui/components/card/card.css +169 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +1 -1
- package/src/ui/components/list/ListRender.js +18 -0
- package/src/ui/components/list/divider/ListDividerRender.js +10 -0
- package/src/ui/components/list/divider/list-divider.css +12 -0
- package/src/ui/components/list/group/ListGroupRender.js +61 -0
- package/src/ui/components/list/group/list-group.css +62 -0
- package/src/ui/components/list/item/ListItemRender.js +238 -0
- package/src/ui/components/list/item/list-item.css +191 -0
- package/src/ui/components/list/list.css +24 -0
- package/src/ui/components/spacer/SpacerRender.js +10 -0
- 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
|