jsswift 1.0.48 → 1.0.50

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.
@@ -7,61 +7,443 @@ Exact JSswift UI contract. Shared definitions listed in `commonProps` are in [co
7
7
  "signature": "_.Select(props)",
8
8
  "description": "Select premium: keyboard nav, option groups, async options, filter, clearable, multi select, valori custom da filtro. Wrappato in _.FormField.",
9
9
  "props": {
10
- "options": {"type": "Array<option|group> | () => Array<option|group> | async () => Promise<Array<option|group>>", "description": "Array of selectable options or option groups.", "default": null, "values": null, "category": "data"},
11
- "value": {"type": "any", "description": "Current value (controlled).", "default": null, "values": null, "category": "data"},
12
- "model": {"type": "rod | [get,set] signal", "description": "Two-way bound value (alias to value in some components).", "default": null, "values": null, "category": "data"},
13
- "label": {"type": "String|Node|Function (floating)", "description": "Label text or node for the component.", "default": null, "values": null, "category": "general"},
14
- "topLabel": {"type": "String|Node|Function", "description": "Label displayed above the control.", "default": null, "values": null, "category": "general"},
15
- "hint": {"type": "String|Node|Function", "description": "Helper text shown near the control.", "default": null, "values": null, "category": "state"},
16
- "error": {"type": "String|Node|Function", "description": "Error state or message displayed with the component.", "default": null, "values": null, "category": "state"},
17
- "success": {"type": "String|Node|Function", "description": "Marks the component with success state styling.", "default": null, "values": null, "category": "state"},
18
- "warning": {"type": "String|Node|Function", "description": "Marks the component with warning state styling.", "default": null, "values": null, "category": "state"},
19
- "note": {"type": "String|Node|Function", "description": "Secondary note text.", "default": null, "values": null, "category": "state"},
20
- "clearable": {"type": "boolean", "description": "Shows a clear action to reset the current value.", "default": null, "values": null, "category": "general"},
21
- "filterable": {"type": "boolean", "description": "Enables filtering UI for option lists.", "default": null, "values": null, "category": "general"},
22
- "multiple": {"type": "boolean", "description": "Allows selecting multiple values.", "default": null, "values": null, "category": "general"},
23
- "multi": {"type": "boolean (alias multiple)", "description": "Enables multi-selection behavior.", "default": null, "values": null, "category": "general"},
24
- "allowCustom": {"type": "boolean (consenti valori custom dal filtro)", "description": "Allows values not present in the options list.", "default": null, "values": null, "category": "general"},
25
- "allowCustomValue": {"type": "boolean (alias allowCustom)", "description": "Accepts free-form values in addition to predefined options.", "default": null, "values": null, "category": "general"},
26
- "filterPlaceholder": {"type": "string", "description": "Placeholder text for the filter/search input.", "default": null, "values": null, "category": "general"},
27
- "emptyText": {"type": "string", "description": "Text shown when there is no data to display.", "default": null, "values": null, "category": "general"},
28
- "shortcode": {"type": "string|Array<string>|object", "description": "Keyboard shortcut string, object, or array bound to the component action.", "default": null, "values": null, "category": "general"},
29
- "showShortcode": {"type": "boolean", "description": "Displays the configured keyboard shortcut badge when the component supports it.", "default": null, "values": null, "category": "general"},
30
- "icon": {"type": "String|Node|Function", "description": "Icon name or node rendered with the component.", "default": null, "values": ["#name_tabler", "name_material", "class"], "category": "general"},
31
- "iconRight": {"type": "String|Node|Function", "description": "Icon name or node rendered on the right side of the component.", "default": null, "values": ["#name_tabler", "name_material", "class"], "category": "general"},
32
- "prefix": {"type": "String|Node|Function", "description": "Content rendered before the main control.", "default": null, "values": null, "category": "general"},
33
- "suffix": {"type": "String|Node|Function", "description": "Content rendered after the main control.", "default": null, "values": null, "category": "general"},
34
- "disabled": {"type": "boolean | () => boolean", "description": "Disables interaction and applies disabled styling/ARIA.", "default": false, "values": null, "category": "state"},
35
- "dense": {"type": "boolean", "description": "Uses compact spacing and sizing.", "default": false, "values": null, "category": "layout"},
36
- "wrapClass": {"type": "string", "description": "Additional CSS classes applied to the outer wrapper.", "default": null, "values": null, "category": "general"},
37
- "slots": {"type": "{ clear?, arrow?, filter?, option?, group?, empty?, loading? }", "description": "Named slots map for render overrides.", "default": null, "values": null, "category": "general"},
38
- "onChange": {"type": "(value:any) => void", "description": "Callback fired when the value or selection changes.", "default": null, "values": null, "category": "events"},
39
- "children": {"type": "Node|Array|Function", "description": "Default child content (nodes, arrays, or render function).", "default": null, "values": null, "category": "general"}
10
+ "options": {
11
+ "type": "Array<option|group> | () => Array<option|group> | async () => Promise<Array<option|group>>",
12
+ "description": "Array of selectable options or option groups.",
13
+ "default": null,
14
+ "values": null,
15
+ "category": "data"
16
+ },
17
+ "value": {
18
+ "type": "any",
19
+ "description": "Current value (controlled).",
20
+ "default": null,
21
+ "values": null,
22
+ "category": "data"
23
+ },
24
+ "model": {
25
+ "type": "rod | [get,set] signal",
26
+ "description": "Two-way bound value (alias to value in some components).",
27
+ "default": null,
28
+ "values": null,
29
+ "category": "data"
30
+ },
31
+ "label": {
32
+ "type": "String|Node|Function (floating)",
33
+ "description": "Label text or node for the component.",
34
+ "default": null,
35
+ "values": null,
36
+ "category": "general"
37
+ },
38
+ "topLabel": {
39
+ "type": "String|Node|Function",
40
+ "description": "Label displayed above the control.",
41
+ "default": null,
42
+ "values": null,
43
+ "category": "general"
44
+ },
45
+ "hint": {
46
+ "type": "String|Node|Function",
47
+ "description": "Helper text shown near the control.",
48
+ "default": null,
49
+ "values": null,
50
+ "category": "state"
51
+ },
52
+ "error": {
53
+ "type": "String|Node|Function",
54
+ "description": "Error state or message displayed with the component.",
55
+ "default": null,
56
+ "values": null,
57
+ "category": "state"
58
+ },
59
+ "success": {
60
+ "type": "String|Node|Function",
61
+ "description": "Marks the component with success state styling.",
62
+ "default": null,
63
+ "values": null,
64
+ "category": "state"
65
+ },
66
+ "warning": {
67
+ "type": "String|Node|Function",
68
+ "description": "Marks the component with warning state styling.",
69
+ "default": null,
70
+ "values": null,
71
+ "category": "state"
72
+ },
73
+ "note": {
74
+ "type": "String|Node|Function",
75
+ "description": "Secondary note text.",
76
+ "default": null,
77
+ "values": null,
78
+ "category": "state"
79
+ },
80
+ "clearable": {
81
+ "type": "boolean",
82
+ "description": "Shows a clear action to reset the current value.",
83
+ "default": null,
84
+ "values": null,
85
+ "category": "general"
86
+ },
87
+ "filterable": {
88
+ "type": "boolean",
89
+ "description": "Enables filtering UI for option lists.",
90
+ "default": null,
91
+ "values": null,
92
+ "category": "general"
93
+ },
94
+ "multiple": {
95
+ "type": "boolean",
96
+ "description": "Allows selecting multiple values.",
97
+ "default": null,
98
+ "values": null,
99
+ "category": "general"
100
+ },
101
+ "multi": {
102
+ "type": "boolean (alias multiple)",
103
+ "description": "Enables multi-selection behavior.",
104
+ "default": null,
105
+ "values": null,
106
+ "category": "general"
107
+ },
108
+ "allowCustom": {
109
+ "type": "boolean (consenti valori custom dal filtro)",
110
+ "description": "Allows values not present in the options list.",
111
+ "default": null,
112
+ "values": null,
113
+ "category": "general"
114
+ },
115
+ "allowCustomValue": {
116
+ "type": "boolean (alias allowCustom)",
117
+ "description": "Accepts free-form values in addition to predefined options.",
118
+ "default": null,
119
+ "values": null,
120
+ "category": "general"
121
+ },
122
+ "filterPlaceholder": {
123
+ "type": "string",
124
+ "description": "Placeholder text for the filter/search input.",
125
+ "default": null,
126
+ "values": null,
127
+ "category": "general"
128
+ },
129
+ "emptyText": {
130
+ "type": "string",
131
+ "description": "Text shown when there is no data to display.",
132
+ "default": null,
133
+ "values": null,
134
+ "category": "general"
135
+ },
136
+ "shortcode": {
137
+ "type": "string|Array<string>|object",
138
+ "description": "Keyboard shortcut string, object, or array bound to the component action.",
139
+ "default": null,
140
+ "values": null,
141
+ "category": "general"
142
+ },
143
+ "showShortcode": {
144
+ "type": "boolean",
145
+ "description": "Displays the configured keyboard shortcut badge when the component supports it.",
146
+ "default": null,
147
+ "values": null,
148
+ "category": "general"
149
+ },
150
+ "icon": {
151
+ "type": "String|Node|Function",
152
+ "description": "Icon name or node rendered with the component.",
153
+ "default": null,
154
+ "values": [
155
+ "#name_tabler",
156
+ "name_material",
157
+ "class"
158
+ ],
159
+ "category": "general"
160
+ },
161
+ "iconRight": {
162
+ "type": "String|Node|Function",
163
+ "description": "Icon name or node rendered on the right side of the component.",
164
+ "default": null,
165
+ "values": [
166
+ "#name_tabler",
167
+ "name_material",
168
+ "class"
169
+ ],
170
+ "category": "general"
171
+ },
172
+ "prefix": {
173
+ "type": "String|Node|Function",
174
+ "description": "Content rendered before the main control.",
175
+ "default": null,
176
+ "values": null,
177
+ "category": "general"
178
+ },
179
+ "suffix": {
180
+ "type": "String|Node|Function",
181
+ "description": "Content rendered after the main control.",
182
+ "default": null,
183
+ "values": null,
184
+ "category": "general"
185
+ },
186
+ "disabled": {
187
+ "type": "boolean | () => boolean",
188
+ "description": "Disables interaction and applies disabled styling/ARIA.",
189
+ "default": false,
190
+ "values": null,
191
+ "category": "state"
192
+ },
193
+ "dense": {
194
+ "type": "boolean",
195
+ "description": "Uses compact spacing and sizing.",
196
+ "default": false,
197
+ "values": null,
198
+ "category": "layout"
199
+ },
200
+ "wrapClass": {
201
+ "type": "string",
202
+ "description": "Additional CSS classes applied to the outer wrapper.",
203
+ "default": null,
204
+ "values": null,
205
+ "category": "general"
206
+ },
207
+ "slots": {
208
+ "type": "{ clear?, arrow?, filter?, option?, group?, empty?, loading? }",
209
+ "description": "Named slots map for render overrides.",
210
+ "default": null,
211
+ "values": null,
212
+ "category": "general"
213
+ },
214
+ "onChange": {
215
+ "type": "(value:any) => void",
216
+ "description": "Callback fired when the value or selection changes.",
217
+ "default": null,
218
+ "values": null,
219
+ "category": "events"
220
+ },
221
+ "children": {
222
+ "type": "Node|Array|Function",
223
+ "description": "Default child content (nodes, arrays, or render function).",
224
+ "default": null,
225
+ "values": null,
226
+ "category": "general"
227
+ },
228
+ "ariaLabel": {
229
+ "type": "string|number|reactive",
230
+ "description": "Applied to the interactive control; see public control accessibility and focus.",
231
+ "default": null,
232
+ "values": null,
233
+ "category": "accessibility"
234
+ },
235
+ "ariaLabelledBy": {
236
+ "type": "string|number|reactive",
237
+ "description": "Applied to the interactive control; see public control accessibility and focus.",
238
+ "default": null,
239
+ "values": null,
240
+ "category": "accessibility"
241
+ },
242
+ "ariaDescribedBy": {
243
+ "type": "string|number|reactive",
244
+ "description": "Applied to the interactive control; see public control accessibility and focus.",
245
+ "default": null,
246
+ "values": null,
247
+ "category": "accessibility"
248
+ },
249
+ "ariaInvalid": {
250
+ "type": "boolean|reactive",
251
+ "description": "Applied to the interactive control; see public control accessibility and focus.",
252
+ "default": null,
253
+ "values": null,
254
+ "category": "accessibility"
255
+ },
256
+ "required": {
257
+ "type": "boolean|reactive",
258
+ "description": "Applied to the interactive control; see public control accessibility and focus.",
259
+ "default": null,
260
+ "values": null,
261
+ "category": "general"
262
+ }
40
263
  },
41
264
  "slots": {
42
- "clear": {"type": "Clear button (via FormField slots.clear)", "description": "Slot for the clear control/content.", "default": null, "values": null, "category": "slot"},
43
- "arrow": {"type": "Arrow slot (ctx: { open })", "description": "Slot for the tooltip/popover arrow element.", "default": null, "values": null, "category": "slot"},
44
- "filter": {"type": "Filter input slot (ctx: { value, setValue, close })", "description": "Slot for custom filter/search _.", "default": null, "values": null, "category": "slot"},
45
- "option": {"type": "Option renderer (ctx: { opt, selected, active, disabled, select })", "description": "Slot for custom option rendering.", "default": null, "values": null, "category": "slot"},
46
- "group": {"type": "Group header (ctx: { label })", "description": "Slot for grouped content container.", "default": null, "values": null, "category": "slot"},
47
- "empty": {"type": "Empty state (ctx: { filter })", "description": "Slot for empty-state content.", "default": null, "values": null, "category": "slot"},
48
- "loading": {"type": "Loading state (ctx: {})", "description": "Slot for loading state content.", "default": null, "values": null, "category": "state"},
49
- "label": {"type": "Floating label (via FormField slots.label)", "description": "Slot for label content.", "default": null, "values": null, "category": "slot"},
50
- "topLabel": {"type": "Top label (via FormField slots.topLabel)", "description": "Slot for top label content.", "default": null, "values": null, "category": "slot"},
51
- "prefix": {"type": "Left addon (via FormField slots.prefix)", "description": "Slot for prefix content.", "default": null, "values": null, "category": "slot"},
52
- "suffix": {"type": "Right addon (via FormField slots.suffix)", "description": "Slot for suffix content.", "default": null, "values": null, "category": "slot"},
53
- "shortcode": {"type": "Shortcut badge (via FormField slots.shortcode)", "description": "Slot content for \"shortcode\". Accepts nodes, arrays, or render functions.", "default": null, "values": null, "category": "slot"},
54
- "icon": {"type": "Left icon (via FormField slots.icon)", "description": "Slot for icon content.", "default": null, "values": ["#name_tabler", "name_material", "class"], "category": "slot"},
55
- "iconRight": {"type": "Right icon (via FormField slots.iconRight)", "description": "Slot for right-aligned icon content.", "default": null, "values": ["#name_tabler", "name_material", "class"], "category": "slot"},
56
- "hint": {"type": "Hint content (via FormField slots.hint)", "description": "Slot for helper/hint text.", "default": null, "values": null, "category": "state"},
57
- "errorMessage": {"type": "Error content (via FormField slots.errorMessage)", "description": "Slot for custom error message rendering.", "default": null, "values": null, "category": "slot"},
58
- "success": {"type": "Success content (via FormField slots.success)", "description": "Slot for success message/content.", "default": null, "values": null, "category": "state"},
59
- "warning": {"type": "Warning content (via FormField slots.warning)", "description": "Slot for warning message/content.", "default": null, "values": null, "category": "state"},
60
- "note": {"type": "Note content (via FormField slots.note)", "description": "Slot for note/secondary text.", "default": null, "values": null, "category": "state"}
265
+ "clear": {
266
+ "type": "Clear button (via FormField slots.clear)",
267
+ "description": "Slot for the clear control/content.",
268
+ "default": null,
269
+ "values": null,
270
+ "category": "slot"
271
+ },
272
+ "arrow": {
273
+ "type": "Arrow slot (ctx: { open })",
274
+ "description": "Slot for the tooltip/popover arrow element.",
275
+ "default": null,
276
+ "values": null,
277
+ "category": "slot"
278
+ },
279
+ "filter": {
280
+ "type": "Filter input slot (ctx: { value, setValue, close })",
281
+ "description": "Slot for custom filter/search _.",
282
+ "default": null,
283
+ "values": null,
284
+ "category": "slot"
285
+ },
286
+ "option": {
287
+ "type": "Option renderer (ctx: { opt, selected, active, disabled, select })",
288
+ "description": "Slot for custom option rendering.",
289
+ "default": null,
290
+ "values": null,
291
+ "category": "slot"
292
+ },
293
+ "group": {
294
+ "type": "Group header (ctx: { label })",
295
+ "description": "Slot for grouped content container.",
296
+ "default": null,
297
+ "values": null,
298
+ "category": "slot"
299
+ },
300
+ "empty": {
301
+ "type": "Empty state (ctx: { filter })",
302
+ "description": "Slot for empty-state content.",
303
+ "default": null,
304
+ "values": null,
305
+ "category": "slot"
306
+ },
307
+ "loading": {
308
+ "type": "Loading state (ctx: {})",
309
+ "description": "Slot for loading state content.",
310
+ "default": null,
311
+ "values": null,
312
+ "category": "state"
313
+ },
314
+ "label": {
315
+ "type": "Floating label (via FormField slots.label)",
316
+ "description": "Slot for label content.",
317
+ "default": null,
318
+ "values": null,
319
+ "category": "slot"
320
+ },
321
+ "topLabel": {
322
+ "type": "Top label (via FormField slots.topLabel)",
323
+ "description": "Slot for top label content.",
324
+ "default": null,
325
+ "values": null,
326
+ "category": "slot"
327
+ },
328
+ "prefix": {
329
+ "type": "Left addon (via FormField slots.prefix)",
330
+ "description": "Slot for prefix content.",
331
+ "default": null,
332
+ "values": null,
333
+ "category": "slot"
334
+ },
335
+ "suffix": {
336
+ "type": "Right addon (via FormField slots.suffix)",
337
+ "description": "Slot for suffix content.",
338
+ "default": null,
339
+ "values": null,
340
+ "category": "slot"
341
+ },
342
+ "shortcode": {
343
+ "type": "Shortcut badge (via FormField slots.shortcode)",
344
+ "description": "Slot content for \"shortcode\". Accepts nodes, arrays, or render functions.",
345
+ "default": null,
346
+ "values": null,
347
+ "category": "slot"
348
+ },
349
+ "icon": {
350
+ "type": "Left icon (via FormField slots.icon)",
351
+ "description": "Slot for icon content.",
352
+ "default": null,
353
+ "values": [
354
+ "#name_tabler",
355
+ "name_material",
356
+ "class"
357
+ ],
358
+ "category": "slot"
359
+ },
360
+ "iconRight": {
361
+ "type": "Right icon (via FormField slots.iconRight)",
362
+ "description": "Slot for right-aligned icon content.",
363
+ "default": null,
364
+ "values": [
365
+ "#name_tabler",
366
+ "name_material",
367
+ "class"
368
+ ],
369
+ "category": "slot"
370
+ },
371
+ "hint": {
372
+ "type": "Hint content (via FormField slots.hint)",
373
+ "description": "Slot for helper/hint text.",
374
+ "default": null,
375
+ "values": null,
376
+ "category": "state"
377
+ },
378
+ "errorMessage": {
379
+ "type": "Error content (via FormField slots.errorMessage)",
380
+ "description": "Slot for custom error message rendering.",
381
+ "default": null,
382
+ "values": null,
383
+ "category": "slot"
384
+ },
385
+ "success": {
386
+ "type": "Success content (via FormField slots.success)",
387
+ "description": "Slot for success message/content.",
388
+ "default": null,
389
+ "values": null,
390
+ "category": "state"
391
+ },
392
+ "warning": {
393
+ "type": "Warning content (via FormField slots.warning)",
394
+ "description": "Slot for warning message/content.",
395
+ "default": null,
396
+ "values": null,
397
+ "category": "state"
398
+ },
399
+ "note": {
400
+ "type": "Note content (via FormField slots.note)",
401
+ "description": "Slot for note/secondary text.",
402
+ "default": null,
403
+ "values": null,
404
+ "category": "state"
405
+ }
61
406
  },
62
- "keyboard": ["Enter/Space", "ArrowUp", "ArrowDown", "Home", "End", "Escape"],
407
+ "keyboard": [
408
+ "Enter/Space",
409
+ "ArrowUp",
410
+ "ArrowDown",
411
+ "Home",
412
+ "End",
413
+ "Escape"
414
+ ],
63
415
  "returns": "HTMLDivElement (wrapper field) con ._select, ._dispose()",
64
- "commonProps": {"path": "ui/common-props.md", "keys": ["size", "color", "outline", "clickable", "padding", "margin", "width", "minWidth", "maxWidth", "height", "minHeight", "maxHeight", "radius", "borderRadius", "shadow", "mobile", "tablet", "pc", "class", "style"]}
416
+ "commonProps": {
417
+ "path": "ui/common-props.md",
418
+ "keys": [
419
+ "size",
420
+ "color",
421
+ "outline",
422
+ "clickable",
423
+ "padding",
424
+ "margin",
425
+ "width",
426
+ "minWidth",
427
+ "maxWidth",
428
+ "height",
429
+ "minHeight",
430
+ "maxHeight",
431
+ "radius",
432
+ "borderRadius",
433
+ "shadow",
434
+ "mobile",
435
+ "tablet",
436
+ "pc",
437
+ "class",
438
+ "style"
439
+ ]
440
+ },
441
+ "methods": {
442
+ "focus": {
443
+ "signature": "focus(options?)",
444
+ "description": "Focus the native input or Select combobox without opening Select."
445
+ }
446
+ }
65
447
  }
66
448
  ```
67
449
 
@@ -92,3 +474,11 @@ const save = _.Button({ onClick: () => console.log(category[0]()) }, 'Salva');
92
474
  ```
93
475
 
94
476
  Mount both controls; choose Casa through the menu, then save. The logged model value must be Casa.
477
+
478
+ ## Public control accessibility and focus
479
+
480
+ `focus(options?)` on the returned field focuses the interactive input/combobox (it does not open Select). Prefer this method over private DOM references.
481
+
482
+ `ariaLabel` sets the control's accessible name; a string `label` supplies the default. Use `ariaLabelledBy` for an external label ID and `ariaDescribedBy` for hint/error IDs. `ariaInvalid` overrides the invalid state; otherwise a truthy `error` marks the control invalid. `required` sets `aria-required` (and native required for Input). These props accept reactive values.
483
+
484
+ Rendered field messages (error, warning, success, note or hint) receive a stable ID and are automatically associated with the interactive control through `aria-describedby`. External `ariaDescribedBy` IDs are preserved; the generated ID is removed when its message disappears.
@@ -0,0 +1,65 @@
1
+ # Compact verified CRUD
2
+
3
+ Load JSswift and its UI CSS, then run the following code with an `#app` element. Uses public field APIs; selectors below identify application-owned buttons only. Persistence failures keep the editor open. Filtering applies to names and category; five rows per page. Create fresh fields for each opening; closing an overlay cleans up its mounted content.
4
+
5
+ ```js
6
+ const _ = window._;
7
+ const key = 'jsswift-crud-example';
8
+ const categories = ['Casa', 'Ufficio'];
9
+ const priceFormat = new Intl.NumberFormat('it-IT', {style:'currency', currency:'EUR'});
10
+ const initial = () => Array.from({length: 6}, (_, i) => ({id:i+1, name:`Prodotto ${i+1}`, category:categories[i%2], price:10, quantity:2}));
11
+ const valid = rows => Array.isArray(rows) && new Set(rows.map(r => r?.id)).size === rows.length && rows.every(r => r && Number.isSafeInteger(r.id) && r.id > 0 && typeof r.name === 'string' && r.name.trim() && categories.includes(r.category) && Number.isFinite(r.price) && r.price >= 0 && Number.isSafeInteger(r.quantity) && r.quantity >= 0);
12
+ let rows = initial(), editing = null, origin = 'add-product', query = '', category = '', page = 1;
13
+ const notice = _.p({role:'status'});
14
+ try { const stored = localStorage.getItem(key); if (stored) { const data = JSON.parse(stored); if (valid(data)) rows = data; else notice.textContent = 'Archivio non valido'; } } catch { notice.textContent = 'Archivio non disponibile'; }
15
+ let models, errors, fields, dialog, save;
16
+ function edit(record) {
17
+ editing = record?.id ?? null; origin = record ? `edit-${record.id}` : 'add-product';
18
+ models = Object.fromEntries(['name','category','price','quantity'].map(k => [k, _.rod('')]));
19
+ errors = Object.fromEntries(Object.keys(models).map(k => [k, _.rod('')]));
20
+ fields = {
21
+ name: _.Input({label:'Nome', required:true, model:models.name, error:errors.name}),
22
+ category: _.Select({label:'Categoria', required:true, model:models.category, error:errors.category, options:categories.map(value => ({value,label:value}))}),
23
+ price: _.Input({label:'Prezzo', type:'number', required:true, min:0, step:'any', model:models.price, error:errors.price}),
24
+ quantity: _.Input({label:'Quantità', type:'number', required:true, min:0, step:1, model:models.quantity, error:errors.quantity})
25
+ };
26
+ dialog = _.Dialog({title:'Prodotto', ariaLabel:'Prodotto', trapFocus:true, autoFocus:true, closeOnEsc:true,
27
+ content:_.div({}, ...Object.values(fields)), onOpen:() => fields.name.focus(),
28
+ onClose:() => requestAnimationFrame(() => (document.getElementById(origin) ?? add).focus())});
29
+ save = _.Button({id:'save-product', onClick:() => {
30
+ const value = {name:models.name.value.trim(),category:models.category.value,price:Number(models.price.value),quantity:Number(models.quantity.value)};
31
+ errors.name.value = value.name ? '' : 'Nome obbligatorio';
32
+ errors.category.value = categories.includes(value.category) ? '' : 'Categoria obbligatoria';
33
+ errors.price.value = String(models.price.value).trim() && Number.isFinite(value.price) && value.price >= 0 ? '' : 'Prezzo non valido';
34
+ errors.quantity.value = String(models.quantity.value).trim() && Number.isSafeInteger(value.quantity) && value.quantity >= 0 ? '' : 'Quantità non valida';
35
+ const invalid = Object.keys(errors).find(k => errors[k].value);
36
+ if (invalid) { fields[invalid].focus(); return; }
37
+ const record = {id:editing ?? Math.max(0,...rows.map(r => r.id))+1,...value};
38
+ const next = editing == null ? [...rows,record] : rows.map(r => r.id === editing ? record : r);
39
+ try { localStorage.setItem(key, JSON.stringify(next)); } catch { notice.textContent = 'Salvataggio non riuscito'; return; }
40
+ rows = next; render(); dialog.close(); notice.textContent = 'Salvato';
41
+ }}, 'Salva');
42
+ dialog.update({actions:[_.Button({onClick:() => dialog.close()},'Annulla'),save]});
43
+ for (const k of Object.keys(models)) models[k].value = String(record?.[k] ?? '');
44
+ dialog.open();
45
+ }
46
+ const host = _.div({style:{maxWidth:'100%',overflowX:'auto'}}), pager = _.div();
47
+ const add = _.Button({id:'add-product',onClick:() => edit()},'Nuovo prodotto');
48
+ const search = _.Input({label:'Cerca',onInput:value => {query=value;page=1;render();}});
49
+ const filter = _.Select({label:'Filtro categoria',options:[{value:'',label:'Tutte'},...categories.map(value => ({value,label:value}))],onChange:value => {category=value;page=1;render();}});
50
+ function render() {
51
+ const found = rows.filter(r => r.name.toLowerCase().includes(query.trim().toLowerCase()) && (!category || r.category === category));
52
+ const pages = Math.max(1,Math.ceil(found.length/5)); page = Math.min(page,pages);
53
+ host.replaceChildren(_.Table({rows:found.slice((page-1)*5,page*5),rowKey:'id',pagination:false,hideFooter:true,minTableWidth:600,
54
+ columns:[{key:'name',label:'Nome'},{key:'category',label:'Categoria'},{key:'price',label:'Prezzo',format:value => priceFormat.format(value)},{key:'quantity',label:'Quantità'},
55
+ {key:'actions',label:'Azioni',render:r => _.Button({id:`edit-${r.id}`,onClick:() => edit(r)},'Modifica')}] }));
56
+ pager.replaceChildren(_.span(`${found.length} prodotti — pagina ${page}/${pages}`),
57
+ _.Button({id:'previous',disabled:page===1,onClick:() => {page--;render();document.getElementById('previous').focus();}},'Precedente'),
58
+ _.Button({id:'next',disabled:page===pages,onClick:() => {page++;render();document.getElementById('next').focus();}},'Successiva'));
59
+ }
60
+ _.mount('#app', _.div({style:{minWidth:0}}, add, search, filter, host, pager, notice)); render();
61
+ ```
62
+
63
+ Application validation remains explicit. `error` marks the control invalid; rendered field messages are associated automatically. Use `ariaDescribedBy` for additional application-owned descriptions. This example does not provide delete, server synchronization or a complete visual design.
64
+
65
+ Performance: the currency formatter is reused. This small example rebuilds its table for clarity; profile larger datasets before changing rendering strategy. `onOpen` focuses the name field and automatic focus respects that choice.
@@ -205,6 +205,21 @@
205
205
  "paths": [
206
206
  "ai-development.md"
207
207
  ]
208
+ },
209
+ {
210
+ "id": "crud-example",
211
+ "title": "Compact CRUD example",
212
+ "keywords": [
213
+ "CRUD",
214
+ "products",
215
+ "aggiunta",
216
+ "modifica",
217
+ "persistenza",
218
+ "focus"
219
+ ],
220
+ "paths": [
221
+ "examples/crud.md"
222
+ ]
208
223
  }
209
224
  ],
210
225
  "components": [
@@ -968,5 +983,5 @@
968
983
  "documentAliases": {
969
984
  "components/button.md": "components/btn.md"
970
985
  },
971
- "packageVersion": "1.0.48"
986
+ "packageVersion": "1.0.50"
972
987
  }