@pure-ds/storybook 0.4.17 → 0.4.19

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 (35) hide show
  1. package/.storybook/addons/html-preview/Panel.jsx +21 -21
  2. package/.storybook/addons/html-preview/preview.js +4 -5
  3. package/.storybook/manager.js +337 -49
  4. package/.storybook/preview-head.html +2 -2
  5. package/.storybook/preview.js +2 -2
  6. package/README.md +2 -2
  7. package/dist/pds-reference.json +1915 -261
  8. package/package.json +2 -2
  9. package/public/assets/css/app.css +2 -2
  10. package/public/assets/js/app.js +41 -22
  11. package/public/assets/js/pds.js +60 -41
  12. package/public/assets/pds/components/{pds-jsonform.js → pds-form.js} +536 -45
  13. package/public/assets/pds/custom-elements.json +8 -8
  14. package/public/assets/pds/vscode-custom-data.json +63 -63
  15. package/scripts/build-pds-reference.mjs +112 -38
  16. package/scripts/generate-stories.js +2 -2
  17. package/src/js/common/ask.js +48 -21
  18. package/src/js/pds-configurator/pds-config-form.js +9 -9
  19. package/src/js/pds-configurator/pds-demo.js +2 -2
  20. package/src/js/pds-core/pds-config.js +14 -14
  21. package/src/js/pds-core/pds-generator.js +25 -12
  22. package/src/js/pds-core/pds-ontology.js +5 -5
  23. package/src/js/pds.d.ts +2 -2
  24. package/stories/GettingStarted.stories.js +3 -0
  25. package/stories/WhatIsPDS.stories.js +3 -0
  26. package/stories/components/PdsForm.stories.js +4356 -0
  27. package/stories/components/{PdsJsonformUiSchema.md → PdsFormUiSchema.md} +2 -2
  28. package/stories/foundations/Spacing.stories.js +5 -5
  29. package/stories/layout/LayoutOverview.stories.js +13 -11
  30. package/stories/primitives/{Forms.stories.js → FormElements.stories.js} +3 -3
  31. package/stories/primitives/HtmlFormElements.stories.js +128 -0
  32. package/stories/primitives/{FormGroups.stories.js → HtmlFormGroups.stories.js} +70 -21
  33. package/stories/utils/PdsAsk.stories.js +14 -13
  34. package/stories/components/PdsJsonform.stories.js +0 -2007
  35. /package/src/{pds-core → node-api}/pds-api.js +0 -0
@@ -0,0 +1,4356 @@
1
+ import { html, nothing } from "lit";
2
+ import { createComponentDocsPage } from "../reference/reference-docs.js";
3
+ import showdown from "showdown";
4
+
5
+ const markdownConverter = new showdown.Converter({ tables: true });
6
+
7
+ const uiSchemaReferenceMarkdown = `
8
+ # uiSchema Reference
9
+
10
+ Complete reference for all **uiSchema** configuration options in \`pds-form\`.
11
+
12
+ The uiSchema controls *how* fields are rendered while JSON Schema defines *what* data to collect.
13
+
14
+ ---
15
+
16
+ ## Path Syntax
17
+
18
+ Use **slash notation** (JSON Pointer) to target fields:
19
+
20
+ | Target | Path |
21
+ |--------|------|
22
+ | Root form | \`"/"\` or root-level \`ui:*\` keys |
23
+ | Top-level field | \`"/fieldName"\` or \`"fieldName"\` |
24
+ | Nested field | \`"/parent/child"\` or \`"parent/child"\` |
25
+
26
+ ---
27
+
28
+ ## Root-Level Layout
29
+
30
+ Apply layout to the **entire form** without modifying your JSON Schema:
31
+
32
+ \`\`\`javascript
33
+ const uiSchema = {
34
+ 'ui:layout': 'grid',
35
+ 'ui:layoutOptions': {
36
+ columns: 2,
37
+ gap: 'md'
38
+ }
39
+ };
40
+ \`\`\`
41
+
42
+ You can combine root-level layout with field-specific options:
43
+
44
+ \`\`\`javascript
45
+ const uiSchema = {
46
+ // Root form layout
47
+ 'ui:layout': 'grid',
48
+ 'ui:layoutOptions': { columns: 2, gap: 'md' },
49
+
50
+ // Field-specific options
51
+ '/email': { 'ui:icon': 'envelope' },
52
+ '/bio': { 'ui:widget': 'textarea' }
53
+ };
54
+ \`\`\`
55
+
56
+ ---
57
+
58
+ ## Widget Selection
59
+
60
+ ### \`ui:widget\`
61
+
62
+ Override the automatically selected widget for a field.
63
+
64
+ | Widget | Use For | Notes |
65
+ |--------|---------|-------|
66
+ | \`input-text\` | String | Default for strings |
67
+ | \`textarea\` | String | Multi-line text |
68
+ | \`password\` | String | Masked input |
69
+ | \`input-email\` | String | Email with validation |
70
+ | \`input-url\` | String | URL with validation |
71
+ | \`input-number\` | Number | Default for numbers |
72
+ | \`input-range\` | Number | Slider control |
73
+ | \`input-date\` | String | Date picker |
74
+ | \`input-time\` | String | Time picker |
75
+ | \`input-datetime\` | String | Date + time picker |
76
+ | \`input-color\` | String | Color picker |
77
+ | \`checkbox\` | Boolean | Default for booleans |
78
+ | \`toggle\` | Boolean | Toggle switch |
79
+ | \`select\` | String (enum) | Default for enums |
80
+ | \`radio\` | String (enum) | Radio button group |
81
+ | \`checkbox-group\` | Array (enum items) | Multi-select checkboxes |
82
+ | \`upload\` | String | File upload (pds-upload) |
83
+ | \`richtext\` | String | Rich text editor (pds-richtext) |
84
+ | \`const\` | Any | Read-only display |
85
+
86
+ ---
87
+
88
+ ## Widget Options
89
+
90
+ ### \`ui:options\`
91
+
92
+ Widget-specific configuration object.
93
+
94
+ #### Textarea Options
95
+ | Option | Type | Description |
96
+ |--------|------|-------------|
97
+ | \`rows\` | number | Number of visible rows |
98
+ | \`cols\` | number | Number of visible columns |
99
+
100
+ #### Range Slider Options
101
+ | Option | Type | Description |
102
+ |--------|------|-------------|
103
+ | \`min\` | number | Minimum value |
104
+ | \`max\` | number | Maximum value |
105
+ | \`step\` | number | Step increment |
106
+
107
+ #### Upload Options (pds-upload)
108
+ | Option | Type | Description |
109
+ |--------|------|-------------|
110
+ | \`accept\` | string | MIME types (e.g., \`"image/*"\`) |
111
+ | \`maxSize\` | number | Max file size in bytes |
112
+ | \`multiple\` | boolean | Allow multiple files |
113
+ | \`label\` | string | Upload button label |
114
+
115
+ #### Rich Text Options (pds-richtext)
116
+ | Option | Type | Description |
117
+ |--------|------|-------------|
118
+ | \`toolbar\` | string | \`"minimal"\`, \`"standard"\`, \`"full"\` |
119
+ | \`spellcheck\` | boolean | Enable spellcheck |
120
+ | \`format\` | string | \`"html"\` or \`"markdown"\` for output |
121
+ | \`submitOnEnter\` | boolean | Submit form on Enter |
122
+
123
+ ---
124
+
125
+ ## Layout Options
126
+
127
+ ### \`ui:layout\`
128
+
129
+ Control how nested object fields are arranged.
130
+
131
+ | Value | Description |
132
+ |-------|-------------|
133
+ | \`default\` | Standard vertical fieldset |
134
+ | \`flex\` | Flexbox row layout |
135
+ | \`grid\` | CSS Grid layout |
136
+ | \`accordion\` | Collapsible \`<details>\` sections |
137
+ | \`tabs\` | Tabbed interface (pds-tabstrip) |
138
+
139
+ ### \`ui:layoutOptions\`
140
+
141
+ Layout-specific configuration.
142
+
143
+ #### Flex Layout Options
144
+ | Option | Type | Description |
145
+ |--------|------|-------------|
146
+ | \`wrap\` | boolean | Allow wrapping |
147
+ | \`gap\` | string | Gap size: \`"sm"\`, \`"md"\`, \`"lg"\` |
148
+ | \`direction\` | string | \`"row"\` or \`"column"\` |
149
+
150
+ #### Grid Layout Options
151
+ | Option | Type | Description |
152
+ |--------|------|-------------|
153
+ | \`columns\` | number | Number of columns |
154
+ | \`gap\` | string | Gap size: \`"sm"\`, \`"md"\`, \`"lg"\` |
155
+
156
+ #### Accordion Layout Options
157
+ | Option | Type | Description |
158
+ |--------|------|-------------|
159
+ | \`openFirst\` | boolean | First section open by default |
160
+
161
+ ---
162
+
163
+ ## Field Customization
164
+
165
+ ### Display Options
166
+
167
+ | Option | Type | Description |
168
+ |--------|------|-------------|
169
+ | \`ui:help\` | string | Help text below field (overrides \`description\`) |
170
+ | \`ui:placeholder\` | string | Placeholder text (overrides \`examples\`) |
171
+ | \`ui:class\` | string | Additional CSS classes |
172
+ | \`ui:order\` | string[] | Field order within object |
173
+
174
+ ### State Options
175
+
176
+ | Option | Type | Description |
177
+ |--------|------|-------------|
178
+ | \`ui:hidden\` | boolean | Hide field from UI (still in data) |
179
+ | \`ui:readonly\` | boolean | Make field read-only |
180
+ | \`ui:disabled\` | boolean | Disable field |
181
+
182
+ ---
183
+
184
+ ## Icon Enhancement
185
+
186
+ ### \`ui:icon\`
187
+
188
+ Add an icon to the input field. Requires \`pds-icon\` component.
189
+
190
+ | Option | Type | Description |
191
+ |--------|------|-------------|
192
+ | \`ui:icon\` | string | Icon name from sprite |
193
+ | \`ui:iconPosition\` | string | \`"start"\` or \`"end"\` |
194
+
195
+ ---
196
+
197
+ ## Surface Wrapping
198
+
199
+ ### \`ui:surface\`
200
+
201
+ Wrap a fieldset in a styled container.
202
+
203
+ | Value | Description |
204
+ |-------|-------------|
205
+ | \`card\` | Card surface |
206
+ | \`elevated\` | Elevated surface with shadow |
207
+ | \`sunken\` | Sunken/inset surface |
208
+
209
+ ---
210
+
211
+ ## Dialog Forms
212
+
213
+ ### \`ui:dialog\`
214
+
215
+ Collect nested object data in a modal dialog instead of inline.
216
+
217
+ | Option | Type | Description |
218
+ |--------|------|-------------|
219
+ | \`ui:dialog\` | boolean | Enable dialog mode |
220
+
221
+ ### \`ui:dialogOptions\`
222
+
223
+ | Option | Type | Description |
224
+ |--------|------|-------------|
225
+ | \`buttonLabel\` | string | Trigger button label |
226
+ | \`dialogTitle\` | string | Dialog title |
227
+ | \`icon\` | string | Icon for trigger button |
228
+
229
+ ---
230
+
231
+ ## Autocomplete
232
+
233
+ ### \`ui:datalist\`
234
+
235
+ Provide autocomplete suggestions using native \`<datalist>\`.
236
+
237
+ | Option | Type | Description |
238
+ |--------|------|-------------|
239
+ | \`ui:datalist\` | string[] | Array of suggestion values |
240
+
241
+ ### \`ui:autocomplete\`
242
+
243
+ HTML autocomplete attribute hint.
244
+
245
+ | Option | Type | Description |
246
+ |--------|------|-------------|
247
+ | \`ui:autocomplete\` | string | e.g., \`"email"\`, \`"new-password"\`, \`"tel"\` |
248
+
249
+ ---
250
+
251
+ ## Enhanced Select
252
+
253
+ ### \`ui:dropdown\`
254
+
255
+ Use enhanced dropdown menu instead of native select.
256
+
257
+ | Option | Type | Description |
258
+ |--------|------|-------------|
259
+ | \`ui:dropdown\` | boolean | Enable enhanced dropdown |
260
+
261
+ ---
262
+
263
+ ## Conditional Logic (Interactions)
264
+
265
+ Declarative XForms-inspired conditions for dynamic form behavior.
266
+
267
+ ### Visibility
268
+
269
+ | Option | Type | Description |
270
+ |--------|------|-------------|
271
+ | \`ui:visibleWhen\` | object | Show field when condition is true |
272
+ | \`ui:hidden\` | boolean | Always hide field |
273
+
274
+ ### State
275
+
276
+ | Option | Type | Description |
277
+ |--------|------|-------------|
278
+ | \`ui:disabledWhen\` | object | Disable field when condition is true |
279
+ | \`ui:requiredWhen\` | object | Require field when condition is true |
280
+
281
+ ### Calculated Values
282
+
283
+ | Option | Type | Description |
284
+ |--------|------|-------------|
285
+ | \`ui:calculate\` | object | Compute value from expression |
286
+ | \`ui:calculateOverride\` | boolean | Allow user to edit calculated value |
287
+
288
+ ### Condition Operators
289
+
290
+ **Comparison**: \`$eq\`, \`$ne\`, \`$gt\`, \`$gte\`, \`$lt\`, \`$lte\`, \`$in\`, \`$nin\`, \`$exists\`, \`$regex\`
291
+
292
+ **Logical**: \`$and\`, \`$or\`, \`$not\`
293
+
294
+ **Calculation**: \`$concat\`, \`$sum\`, \`$subtract\`, \`$multiply\`, \`$divide\`, \`$if\`, \`$coalesce\`
295
+
296
+ ### Examples
297
+
298
+ \`\`\`javascript
299
+ // Show field conditionally
300
+ "/otherReason": {
301
+ "ui:visibleWhen": { "/reason": "other" }
302
+ }
303
+
304
+ // Disable based on condition
305
+ "/email": {
306
+ "ui:disabledWhen": { "/usePhone": true }
307
+ }
308
+
309
+ // Conditional requirement
310
+ "/companyName": {
311
+ "ui:requiredWhen": { "/accountType": "business" }
312
+ }
313
+
314
+ // Calculated value
315
+ "/fullName": {
316
+ "ui:calculate": { "$concat": ["/firstName", " ", "/lastName"] }
317
+ }
318
+
319
+ // Complex condition
320
+ "/premiumFeature": {
321
+ "ui:visibleWhen": {
322
+ "$and": [
323
+ { "/isPremium": true },
324
+ { "/country": { "$in": ["US", "CA", "UK"] } }
325
+ ]
326
+ }
327
+ }
328
+ \`\`\`
329
+
330
+ ---
331
+
332
+ ## Custom Content Injection
333
+
334
+ Inject custom HTML content before/after fields, create fully custom renderers, or customize field wrappers.
335
+
336
+ ### Content Injection
337
+
338
+ | Option | Type | Description |
339
+ |--------|------|-------------|
340
+ | \`ui:before\` | function \| string | Content rendered before the field |
341
+ | \`ui:after\` | function \| string | Content rendered after the field |
342
+
343
+ **Value Types:**
344
+ - **Function**: \`(field) => html\`...\`\` - receives render context
345
+ - **Slot reference**: \`"slot:mySlot"\` - renders slotted element by name
346
+
347
+ ### Custom Rendering
348
+
349
+ | Option | Type | Description |
350
+ |--------|------|-------------|
351
+ | \`ui:render\` | function | Complete custom field renderer |
352
+ | \`ui:wrapper\` | function | Custom wrapper around the control |
353
+
354
+ ### Render Context (\`field\` object)
355
+
356
+ | Property | Description |
357
+ |----------|-------------|
358
+ | \`id\` | Unique DOM ID for the field |
359
+ | \`path\` | JSON Pointer path (e.g., \`/address/city\`) |
360
+ | \`label\` | Display label |
361
+ | \`value\` | Current field value |
362
+ | \`schema\` | JSON Schema for this field |
363
+ | \`ui\` | UI schema for this path |
364
+ | \`attrs\` | Native constraint attributes |
365
+ | \`get\` | Get value at path: \`get("/otherField")\` |
366
+ | \`set\` | Set value: \`set(newValue)\` |
367
+ | \`host\` | Reference to pds-form element |
368
+ | \`control\` | (wrapper only) Rendered control template |
369
+ | \`help\` | (wrapper only) Rendered help text |
370
+
371
+ ### Examples
372
+
373
+ \`\`\`javascript
374
+ // Add content before/after fields
375
+ "/username": {
376
+ "ui:before": (field) => html\`<div class="alert">Section header</div>\`,
377
+ "ui:after": (field) => field.value?.length < 3
378
+ ? html\`<small class="text-danger">Too short</small>\`
379
+ : nothing
380
+ }
381
+
382
+ // Custom star rating widget
383
+ "/rating": {
384
+ "ui:render": (field) => html\`
385
+ <fieldset>
386
+ <legend>\${field.label}</legend>
387
+ <div class="flex gap-xs">
388
+ \${[1,2,3,4,5].map(n => html\`
389
+ <button type="button" @click=\${() => field.set(n)}>
390
+ \${n <= field.value ? '★' : '☆'}
391
+ </button>
392
+ \`)}
393
+ </div>
394
+ </fieldset>
395
+ \`
396
+ }
397
+
398
+ // Custom wrapper with colored border
399
+ "/email": {
400
+ "ui:wrapper": (field) => html\`
401
+ <div style="border-left: 5px solid var(--color-primary-500); padding-left: var(--spacing-sm);">
402
+ <label for=\${field.id}>
403
+ <span data-label>\${field.label}</span>
404
+ \${field.control}
405
+ </label>
406
+ </div>
407
+ \`
408
+ }
409
+
410
+ // Use slotted content
411
+ "/terms": {
412
+ "ui:before": "slot:terms-notice"
413
+ }
414
+ \`\`\`
415
+ `;
416
+
417
+ const docsParameters = {
418
+ description: {
419
+ component: `**⭐ Recommended for modern applications** - Automatically generate complete forms from [JSON Schema](https://json-schema.org/) definitions.
420
+
421
+ ### Key Features
422
+ - 🎯 **Zero boilerplate** - Define form structure in JSON, get a working form with validation
423
+ - ✅ **Built-in validation** - Automatic validation based on schema rules (required, min/max, patterns, etc.)
424
+ - 🔄 **Data binding** - Two-way data binding with form state management
425
+ - 🎨 **PDS styled** - Uses all PDS design tokens automatically
426
+ - 📱 **Responsive** - Mobile-friendly layouts out of the box
427
+ - 🧩 **Conditional logic** - Show/hide/disable fields, computed values
428
+ - 🌐 **Nested objects** - Support for complex nested data structures
429
+ - 🔧 **Extensible** - Custom field types and validators
430
+
431
+ ### Why Generate Forms from JSON Schema?
432
+ Instead of manually writing HTML for every form field, validation rule, and error message, you define your data schema once and get:
433
+ - Form UI generation
434
+ - Client-side validation
435
+ - Server-side validation (same schema)
436
+ - API documentation
437
+ - Type definitions
438
+ - Database schemas
439
+
440
+ See the examples below to get started, or check the [primitive forms](/story/primitives-form-elements--default) for manual form building.
441
+ `,
442
+ },
443
+ page: createComponentDocsPage("pds-form", {
444
+ hideStories: true,
445
+ additionalContent: markdownConverter.makeHtml(uiSchemaReferenceMarkdown),
446
+ }),
447
+ toc: true,
448
+ };
449
+
450
+ export default {
451
+ title: "Components/pds-form",
452
+ tags: ["autodocs", "forms", "json-schema", "validation", "input"],
453
+ parameters: {
454
+ pds: {
455
+ tags: [
456
+ "forms",
457
+ "json-schema",
458
+ "validation",
459
+ "input",
460
+ "pds-form",
461
+ "interaction",
462
+ ],
463
+ },
464
+ docs: docsParameters,
465
+ },
466
+ };
467
+
468
+ const simpleSchema = {
469
+ type: "object",
470
+ properties: {
471
+ name: {
472
+ type: "string",
473
+ title: "Full Name",
474
+ examples: ["John Doe"],
475
+ },
476
+ email: {
477
+ type: "string",
478
+ format: "email",
479
+ title: "Email Address",
480
+ examples: ["john.doe@example.com"],
481
+ },
482
+ age: {
483
+ type: "number",
484
+ title: "Age",
485
+ minimum: 18,
486
+ examples: [25],
487
+ },
488
+ newsletter: {
489
+ type: "boolean",
490
+ title: "Subscribe to newsletter",
491
+ },
492
+ },
493
+ required: ["name", "email"],
494
+ };
495
+
496
+ const complexSchema = {
497
+ type: "object",
498
+ properties: {
499
+ personalInfo: {
500
+ type: "object",
501
+ title: "Personal Information",
502
+ properties: {
503
+ firstName: { type: "string", title: "First Name", examples: ["John"] },
504
+ lastName: { type: "string", title: "Last Name", examples: ["Doe"] },
505
+ dateOfBirth: { type: "string", format: "date", title: "Date of Birth" },
506
+ },
507
+ },
508
+ address: {
509
+ type: "object",
510
+ title: "Address",
511
+ properties: {
512
+ street: {
513
+ type: "string",
514
+ title: "Street",
515
+ examples: ["123 Main Street"],
516
+ },
517
+ city: { type: "string", title: "City", examples: ["New York"] },
518
+ country: {
519
+ type: "string",
520
+ title: "Country",
521
+ enum: ["USA", "UK", "Canada", "Australia"],
522
+ },
523
+ },
524
+ },
525
+ preferences: {
526
+ type: "array",
527
+ title: "Interests",
528
+ items: {
529
+ type: "string",
530
+ enum: ["Technology", "Sports", "Music", "Travel", "Reading"],
531
+ },
532
+ },
533
+ },
534
+ };
535
+
536
+ export const SimpleForm = {
537
+ render: () => {
538
+ return html`
539
+ <pds-form
540
+ data-required
541
+ .jsonSchema=${simpleSchema}
542
+ @pw:submit=${(e) => toastFormData(e.detail)}
543
+ ></pds-form>
544
+ `;
545
+ },
546
+ };
547
+
548
+ export const ComplexForm = {
549
+ render: () => {
550
+ return html`
551
+ <pds-form
552
+ .jsonSchema=${complexSchema}
553
+ @pw:submit=${(e) => toastFormData(e.detail)}
554
+ ></pds-form>
555
+ `;
556
+ },
557
+ };
558
+
559
+ export const WithInitialData = {
560
+ render: () => {
561
+ const initialValues = {
562
+ name: "John Doe",
563
+ email: "john@example.com",
564
+ age: 25,
565
+ newsletter: true,
566
+ };
567
+
568
+ return html`
569
+ <pds-form
570
+ .jsonSchema=${simpleSchema}
571
+ .values=${initialValues}
572
+ @pw:value-change=${(e) => console.log("🔄 Value changed:", e.detail)}
573
+ @pw:submit=${(e) => toastFormData(e.detail)}
574
+ ></pds-form>
575
+ `;
576
+ },
577
+ };
578
+
579
+ export const WithTogglesSwitches = {
580
+ name: "Toggles & Switches",
581
+ render: () => {
582
+ const schema = {
583
+ type: "object",
584
+ properties: {
585
+ toggles: {
586
+ type: "object",
587
+ title: "Preferences",
588
+ properties: {
589
+ emailNotifications: {
590
+ type: "boolean",
591
+ title: "Email Notifications",
592
+ description: "Receive notifications via email",
593
+ },
594
+ pushNotifications: {
595
+ type: "boolean",
596
+ title: "Push Notifications",
597
+ description: "Receive push notifications on your device",
598
+ },
599
+ darkMode: {
600
+ type: "boolean",
601
+ title: "Dark Mode",
602
+ description: "Enable dark theme",
603
+ },
604
+ autoSave: {
605
+ type: "boolean",
606
+ title: "Auto-save",
607
+ description: "Automatically save changes",
608
+ },
609
+ },
610
+ },
611
+ },
612
+ };
613
+
614
+ const uiSchema = {
615
+ toggles: {
616
+ "ui:layout": "flex",
617
+ "ui:layoutOptions": {
618
+ direction: "column",
619
+ gap: "md",
620
+ },
621
+ },
622
+ };
623
+
624
+ const options = {
625
+ widgets: {
626
+ booleans: "toggle", // Use toggle switches instead of checkboxes
627
+ },
628
+ };
629
+
630
+ return html`
631
+ <pds-form
632
+ .jsonSchema=${schema}
633
+ .uiSchema=${uiSchema}
634
+ .options=${options}
635
+ @pw:submit=${(e) => toastFormData(e.detail)}
636
+ ></pds-form>
637
+ `;
638
+ },
639
+ };
640
+
641
+ export const WithRangeSliders = {
642
+ name: "Range Sliders with Output",
643
+ render: () => {
644
+ const schema = {
645
+ type: "object",
646
+ properties: {
647
+ volume: {
648
+ type: "number",
649
+ title: "Volume",
650
+ minimum: 0,
651
+ maximum: 100,
652
+ default: 50,
653
+ },
654
+ brightness: {
655
+ type: "number",
656
+ title: "Brightness",
657
+ minimum: 0,
658
+ maximum: 100,
659
+ default: 75,
660
+ },
661
+ fontSize: {
662
+ type: "number",
663
+ title: "Font Size",
664
+ minimum: 10,
665
+ maximum: 24,
666
+ default: 16,
667
+ },
668
+ quality: {
669
+ type: "integer",
670
+ title: "Quality",
671
+ minimum: 1,
672
+ maximum: 10,
673
+ default: 7,
674
+ },
675
+ },
676
+ };
677
+
678
+ const uiSchema = {
679
+ volume: { "ui:widget": "input-range" },
680
+ brightness: { "ui:widget": "input-range" },
681
+ fontSize: { "ui:widget": "input-range" },
682
+ quality: { "ui:widget": "input-range" },
683
+ };
684
+
685
+ const options = {
686
+ enhancements: {
687
+ rangeOutput: true, // Add live value display
688
+ },
689
+ };
690
+
691
+ return html`
692
+ <pds-form
693
+ .jsonSchema=${schema}
694
+ .uiSchema=${uiSchema}
695
+ .options=${options}
696
+ @pw:value-change=${(e) => console.log("🎚️ Value changed:", e.detail)}
697
+ @pw:submit=${(e) => toastFormData(e.detail)}
698
+ ></pds-form>
699
+ `;
700
+ },
701
+ };
702
+
703
+ export const WithIcons = {
704
+ name: "Icon-Enhanced Inputs",
705
+ render: () => {
706
+ const schema = {
707
+ type: "object",
708
+ properties: {
709
+ username: {
710
+ type: "string",
711
+ title: "Username",
712
+ examples: ["Enter your username"],
713
+ },
714
+ email: {
715
+ type: "string",
716
+ format: "email",
717
+ title: "Email",
718
+ examples: ["your.email@example.com"],
719
+ },
720
+ password: {
721
+ type: "string",
722
+ title: "Password",
723
+ examples: ["••••••••"],
724
+ },
725
+ website: {
726
+ type: "string",
727
+ format: "uri",
728
+ title: "Website",
729
+ examples: ["https://yourwebsite.com"],
730
+ },
731
+ phone: {
732
+ type: "string",
733
+ title: "Phone",
734
+ examples: ["+1 (555) 123-4567"],
735
+ },
736
+ location: {
737
+ type: "string",
738
+ title: "Location",
739
+ examples: ["City, Country"],
740
+ },
741
+ },
742
+ required: ["username", "email", "password"],
743
+ };
744
+
745
+ const uiSchema = {
746
+ username: {
747
+ "ui:icon": "user",
748
+ "ui:iconPosition": "start",
749
+ },
750
+ email: {
751
+ "ui:icon": "envelope",
752
+ "ui:iconPosition": "start",
753
+ },
754
+ password: {
755
+ "ui:icon": "lock",
756
+ "ui:iconPosition": "start",
757
+ "ui:widget": "password",
758
+ },
759
+ website: {
760
+ "ui:icon": "globe",
761
+ "ui:iconPosition": "start",
762
+ },
763
+ phone: {
764
+ "ui:icon": "phone",
765
+ "ui:iconPosition": "start",
766
+ },
767
+ location: {
768
+ "ui:icon": "map-pin",
769
+ "ui:iconPosition": "start",
770
+ },
771
+ };
772
+
773
+ return html`
774
+ <pds-form
775
+ .jsonSchema=${schema}
776
+ .uiSchema=${uiSchema}
777
+ @pw:submit=${(e) => toastFormData(e.detail)}
778
+ ></pds-form>
779
+ `;
780
+ },
781
+ };
782
+
783
+ export const WithPdsUpload = {
784
+ name: "File Upload (pds-upload)",
785
+ render: () => {
786
+ const schema = {
787
+ type: "object",
788
+ properties: {
789
+ profilePicture: {
790
+ type: "string",
791
+ title: "Profile Picture",
792
+ description: "Upload your profile photo (JPG, PNG)",
793
+ contentMediaType: "image/*",
794
+ contentEncoding: "base64",
795
+ },
796
+ resume: {
797
+ type: "string",
798
+ title: "Resume",
799
+ description: "Upload your resume (PDF)",
800
+ contentMediaType: "application/pdf",
801
+ contentEncoding: "base64",
802
+ },
803
+ portfolio: {
804
+ type: "string",
805
+ title: "Portfolio Files",
806
+ description: "Upload multiple portfolio items",
807
+ contentMediaType: "image/*,application/pdf",
808
+ contentEncoding: "base64",
809
+ },
810
+ },
811
+ };
812
+
813
+ const uiSchema = {
814
+ profilePicture: {
815
+ "ui:options": {
816
+ accept: "image/jpeg,image/png",
817
+ maxSize: 5242880, // 5MB
818
+ label: "Choose photo",
819
+ },
820
+ },
821
+ resume: {
822
+ "ui:options": {
823
+ accept: "application/pdf",
824
+ maxSize: 10485760, // 10MB
825
+ label: "Choose PDF",
826
+ },
827
+ },
828
+ portfolio: {
829
+ "ui:options": {
830
+ multiple: true,
831
+ accept: "image/*,application/pdf",
832
+ label: "Choose files",
833
+ },
834
+ },
835
+ };
836
+
837
+ return html`
838
+ <pds-form
839
+ .jsonSchema=${schema}
840
+ .uiSchema=${uiSchema}
841
+ @pw:submit=${(e) => toastFormData(e.detail)}
842
+ ></pds-form>
843
+ `;
844
+ },
845
+ };
846
+
847
+ export const WithPdsRichtext = {
848
+ name: "Rich Text Editor (pds-richtext)",
849
+ render: () => {
850
+ const schema = {
851
+ type: "object",
852
+ properties: {
853
+ bio: {
854
+ type: "string",
855
+ title: "Biography",
856
+ description: "Tell us about yourself",
857
+ examples: ["Write your biography..."],
858
+ },
859
+ coverLetter: {
860
+ type: "string",
861
+ title: "Cover Letter",
862
+ description: "Write your cover letter",
863
+ examples: ["Write your cover letter..."],
864
+ },
865
+ jobDescription: {
866
+ type: "string",
867
+ title: "Job Description",
868
+ examples: ["Describe the position..."],
869
+ },
870
+ },
871
+ };
872
+
873
+ const uiSchema = {
874
+ bio: {
875
+ "ui:widget": "richtext",
876
+ "ui:options": {
877
+ toolbar: "minimal",
878
+ },
879
+ },
880
+ coverLetter: {
881
+ "ui:widget": "richtext",
882
+ "ui:options": {
883
+ toolbar: "standard",
884
+ },
885
+ },
886
+ jobDescription: {
887
+ "ui:widget": "richtext",
888
+ "ui:options": {
889
+ toolbar: "full",
890
+ },
891
+ },
892
+ };
893
+
894
+ return html`
895
+ <pds-form
896
+ .jsonSchema=${schema}
897
+ .uiSchema=${uiSchema}
898
+ @pw:submit=${(e) => toastFormData(e.detail)}
899
+ ></pds-form>
900
+ `;
901
+ },
902
+ };
903
+
904
+ export const WithFlexLayout = {
905
+ name: "Flex Layout",
906
+ render: () => {
907
+ const schema = {
908
+ type: "object",
909
+ properties: {
910
+ contactInfo: {
911
+ type: "object",
912
+ title: "Contact Information",
913
+ properties: {
914
+ firstName: {
915
+ type: "string",
916
+ title: "First Name",
917
+ examples: ["Jane"],
918
+ },
919
+ lastName: {
920
+ type: "string",
921
+ title: "Last Name",
922
+ examples: ["Smith"],
923
+ },
924
+ email: {
925
+ type: "string",
926
+ format: "email",
927
+ title: "Email",
928
+ examples: ["jane.smith@example.com"],
929
+ },
930
+ phone: {
931
+ type: "string",
932
+ title: "Phone",
933
+ examples: ["+1 (555) 987-6543"],
934
+ },
935
+ },
936
+ },
937
+ },
938
+ };
939
+
940
+ const uiSchema = {
941
+ contactInfo: {
942
+ "ui:layout": "flex",
943
+ "ui:layoutOptions": {
944
+ gap: "md",
945
+ wrap: true,
946
+ direction: "row",
947
+ },
948
+ },
949
+ };
950
+
951
+ return html`
952
+ <pds-form
953
+ .jsonSchema=${schema}
954
+ .uiSchema=${uiSchema}
955
+ @pw:submit=${(e) => toastFormData(e.detail)}
956
+ ></pds-form>
957
+ `;
958
+ },
959
+ };
960
+
961
+ export const WithGridLayout = {
962
+ name: "Grid Layout",
963
+ render: () => {
964
+ const schema = {
965
+ type: "object",
966
+ properties: {
967
+ productInfo: {
968
+ type: "object",
969
+ title: "Product Information",
970
+ properties: {
971
+ name: {
972
+ type: "string",
973
+ title: "Product Name",
974
+ examples: ["Wireless Headphones"],
975
+ },
976
+ sku: { type: "string", title: "SKU", examples: ["WH-1000XM4"] },
977
+ price: { type: "number", title: "Price", examples: [299.99] },
978
+ quantity: { type: "integer", title: "Quantity", examples: [50] },
979
+ category: {
980
+ type: "string",
981
+ title: "Category",
982
+ enum: [
983
+ "Electronics",
984
+ "Clothing",
985
+ "Books",
986
+ "Home",
987
+ "Sports",
988
+ "Garden",
989
+ ],
990
+ },
991
+ brand: { type: "string", title: "Brand", examples: ["Sony"] },
992
+ weight: { type: "number", title: "Weight (kg)", examples: [0.25] },
993
+ dimensions: {
994
+ type: "string",
995
+ title: "Dimensions",
996
+ examples: ["20 x 18 x 8 cm"],
997
+ },
998
+ },
999
+ },
1000
+ },
1001
+ };
1002
+
1003
+ const uiSchema = {
1004
+ productInfo: {
1005
+ "ui:layout": "grid",
1006
+ "ui:layoutOptions": {
1007
+ columns: 3,
1008
+ gap: "md",
1009
+ },
1010
+ },
1011
+ };
1012
+
1013
+ return html`
1014
+ <pds-form
1015
+ .jsonSchema=${schema}
1016
+ .uiSchema=${uiSchema}
1017
+ @pw:submit=${(e) => toastFormData(e.detail)}
1018
+ ></pds-form>
1019
+ `;
1020
+ },
1021
+ };
1022
+
1023
+ export const WithAccordionLayout = {
1024
+ name: "Accordion Layout",
1025
+ render: () => {
1026
+ const schema = {
1027
+ type: "object",
1028
+ properties: {
1029
+ name: {
1030
+ type: "string",
1031
+ title: "Full Name",
1032
+ examples: ["Alex Rodriguez"],
1033
+ },
1034
+ email: {
1035
+ type: "string",
1036
+ format: "email",
1037
+ title: "Email",
1038
+ examples: ["alex.rodriguez@example.com"],
1039
+ },
1040
+ settings: {
1041
+ type: "object",
1042
+ title: "Settings",
1043
+ properties: {
1044
+ displaySettings: {
1045
+ type: "object",
1046
+ title: "Display Settings",
1047
+ properties: {
1048
+ theme: {
1049
+ type: "string",
1050
+ title: "Theme",
1051
+ enum: ["Light", "Dark", "Auto"],
1052
+ default: "Auto",
1053
+ },
1054
+ fontSize: {
1055
+ type: "number",
1056
+ title: "Font Size (px)",
1057
+ minimum: 12,
1058
+ maximum: 24,
1059
+ default: 16,
1060
+ },
1061
+ density: {
1062
+ type: "string",
1063
+ title: "Density",
1064
+ enum: ["Compact", "Comfortable", "Spacious"],
1065
+ default: "Comfortable",
1066
+ },
1067
+ animations: {
1068
+ type: "boolean",
1069
+ title: "Enable Animations",
1070
+ default: true,
1071
+ },
1072
+ },
1073
+ },
1074
+ notificationSettings: {
1075
+ type: "object",
1076
+ title: "Notification Settings",
1077
+ properties: {
1078
+ email: {
1079
+ type: "boolean",
1080
+ title: "Email Notifications",
1081
+ default: true,
1082
+ },
1083
+ push: {
1084
+ type: "boolean",
1085
+ title: "Push Notifications",
1086
+ default: false,
1087
+ },
1088
+ sms: {
1089
+ type: "boolean",
1090
+ title: "SMS Notifications",
1091
+ default: false,
1092
+ },
1093
+ frequency: {
1094
+ type: "string",
1095
+ title: "Frequency",
1096
+ enum: ["Real-time", "Daily", "Weekly"],
1097
+ default: "Daily",
1098
+ },
1099
+ },
1100
+ },
1101
+ privacySettings: {
1102
+ type: "object",
1103
+ title: "Privacy Settings",
1104
+ properties: {
1105
+ profileVisibility: {
1106
+ type: "string",
1107
+ title: "Profile Visibility",
1108
+ enum: ["Public", "Friends", "Private"],
1109
+ default: "Friends",
1110
+ },
1111
+ showEmail: {
1112
+ type: "boolean",
1113
+ title: "Show Email",
1114
+ default: false,
1115
+ },
1116
+ showActivity: {
1117
+ type: "boolean",
1118
+ title: "Show Activity",
1119
+ default: true,
1120
+ },
1121
+ allowMessages: {
1122
+ type: "boolean",
1123
+ title: "Allow Messages",
1124
+ default: true,
1125
+ },
1126
+ },
1127
+ },
1128
+ },
1129
+ },
1130
+ },
1131
+ required: ["name", "email"],
1132
+ };
1133
+
1134
+ const uiSchema = {
1135
+ settings: { "ui:layout": "accordion" },
1136
+ };
1137
+
1138
+ return html`
1139
+ <pds-form
1140
+ .jsonSchema=${schema}
1141
+ .uiSchema=${uiSchema}
1142
+ @pw:submit=${(e) => toastFormData(e.detail)}
1143
+ ></pds-form>
1144
+ `;
1145
+ },
1146
+ };
1147
+
1148
+ export const WithTabsLayout = {
1149
+ name: "Tabs Layout (pds-tabstrip)",
1150
+ render: () => {
1151
+ const schema = {
1152
+ type: "object",
1153
+ properties: {
1154
+ userSettings: {
1155
+ type: "object",
1156
+ title: "User Settings",
1157
+ properties: {
1158
+ account: {
1159
+ type: "object",
1160
+ title: "Account",
1161
+ properties: {
1162
+ username: {
1163
+ type: "string",
1164
+ title: "Username",
1165
+ examples: ["coolguy123"],
1166
+ },
1167
+ email: {
1168
+ type: "string",
1169
+ format: "email",
1170
+ title: "Email",
1171
+ examples: ["user@example.com"],
1172
+ },
1173
+ password: {
1174
+ type: "string",
1175
+ title: "New Password",
1176
+ examples: ["••••••••"],
1177
+ },
1178
+ },
1179
+ },
1180
+ profile: {
1181
+ type: "object",
1182
+ title: "Profile",
1183
+ properties: {
1184
+ displayName: {
1185
+ type: "string",
1186
+ title: "Display Name",
1187
+ examples: ["Cool Guy"],
1188
+ },
1189
+ bio: {
1190
+ type: "string",
1191
+ title: "Bio",
1192
+ examples: ["Tell us about yourself..."],
1193
+ },
1194
+ location: {
1195
+ type: "string",
1196
+ title: "Location",
1197
+ examples: ["San Francisco, CA"],
1198
+ },
1199
+ website: {
1200
+ type: "string",
1201
+ format: "uri",
1202
+ title: "Website",
1203
+ examples: ["https://mywebsite.com"],
1204
+ },
1205
+ },
1206
+ },
1207
+ privacy: {
1208
+ type: "object",
1209
+ title: "Privacy",
1210
+ properties: {
1211
+ publicProfile: { type: "boolean", title: "Public Profile" },
1212
+ showEmail: { type: "boolean", title: "Show Email" },
1213
+ allowMessages: { type: "boolean", title: "Allow Messages" },
1214
+ searchable: { type: "boolean", title: "Searchable" },
1215
+ },
1216
+ },
1217
+ },
1218
+ },
1219
+ },
1220
+ };
1221
+
1222
+ const uiSchema = {
1223
+ userSettings: { "ui:layout": "tabs" },
1224
+ };
1225
+
1226
+ return html`
1227
+ <pds-form
1228
+ .jsonSchema=${schema}
1229
+ .uiSchema=${uiSchema}
1230
+ @pw:submit=${(e) => toastFormData(e.detail)}
1231
+ ></pds-form>
1232
+ `;
1233
+ },
1234
+ };
1235
+
1236
+ export const WithSurfaces = {
1237
+ name: "Surface Wrapping (Cards)",
1238
+ render: () => {
1239
+ const schema = {
1240
+ type: "object",
1241
+ properties: {
1242
+ cardGroups: {
1243
+ type: "object",
1244
+ title: "Product Catalog",
1245
+ properties: {
1246
+ product1: {
1247
+ type: "object",
1248
+ title: "Premium Membership",
1249
+ properties: {
1250
+ name: {
1251
+ type: "string",
1252
+ title: "Product Name",
1253
+ default: "Premium Plan",
1254
+ examples: ["Premium Plan"],
1255
+ },
1256
+ price: {
1257
+ type: "number",
1258
+ title: "Price (USD)",
1259
+ default: 29.99,
1260
+ minimum: 0,
1261
+ examples: [29.99],
1262
+ },
1263
+ billing: {
1264
+ type: "string",
1265
+ title: "Billing Cycle",
1266
+ enum: ["Monthly", "Quarterly", "Yearly"],
1267
+ default: "Monthly",
1268
+ },
1269
+ autoRenew: {
1270
+ type: "boolean",
1271
+ title: "Auto-Renew",
1272
+ default: true,
1273
+ },
1274
+ },
1275
+ },
1276
+ product2: {
1277
+ type: "object",
1278
+ title: "Enterprise Solution",
1279
+ properties: {
1280
+ name: {
1281
+ type: "string",
1282
+ title: "Product Name",
1283
+ default: "Enterprise",
1284
+ examples: ["Enterprise"],
1285
+ },
1286
+ seats: {
1287
+ type: "integer",
1288
+ title: "Number of Seats",
1289
+ default: 10,
1290
+ minimum: 1,
1291
+ examples: [10],
1292
+ },
1293
+ support: {
1294
+ type: "string",
1295
+ title: "Support Level",
1296
+ enum: ["Standard", "Priority", "24/7"],
1297
+ default: "Priority",
1298
+ },
1299
+ sla: { type: "boolean", title: "SLA Agreement", default: true },
1300
+ },
1301
+ },
1302
+ product3: {
1303
+ type: "object",
1304
+ title: "Developer Tools",
1305
+ properties: {
1306
+ name: {
1307
+ type: "string",
1308
+ title: "Product Name",
1309
+ default: "Dev Tools Pro",
1310
+ examples: ["Dev Tools Pro"],
1311
+ },
1312
+ apiCalls: {
1313
+ type: "integer",
1314
+ title: "API Calls/Month",
1315
+ default: 100000,
1316
+ minimum: 1000,
1317
+ examples: [100000],
1318
+ },
1319
+ environments: {
1320
+ type: "integer",
1321
+ title: "Environments",
1322
+ default: 3,
1323
+ minimum: 1,
1324
+ maximum: 10,
1325
+ examples: [3],
1326
+ },
1327
+ monitoring: {
1328
+ type: "boolean",
1329
+ title: "Performance Monitoring",
1330
+ default: true,
1331
+ },
1332
+ },
1333
+ },
1334
+ product4: {
1335
+ type: "object",
1336
+ title: "Storage Package",
1337
+ properties: {
1338
+ name: {
1339
+ type: "string",
1340
+ title: "Product Name",
1341
+ default: "Cloud Storage+",
1342
+ examples: ["Cloud Storage+"],
1343
+ },
1344
+ storage: {
1345
+ type: "number",
1346
+ title: "Storage (TB)",
1347
+ default: 5,
1348
+ minimum: 1,
1349
+ maximum: 100,
1350
+ examples: [5],
1351
+ },
1352
+ bandwidth: {
1353
+ type: "number",
1354
+ title: "Bandwidth (TB)",
1355
+ default: 10,
1356
+ minimum: 1,
1357
+ examples: [10],
1358
+ },
1359
+ backup: {
1360
+ type: "boolean",
1361
+ title: "Automated Backup",
1362
+ default: true,
1363
+ },
1364
+ },
1365
+ },
1366
+ },
1367
+ },
1368
+ },
1369
+ };
1370
+
1371
+ const uiSchema = {
1372
+ cardGroups: {
1373
+ "ui:layout": "grid",
1374
+ "ui:layoutOptions": {
1375
+ columns: "auto",
1376
+ autoSize: "md",
1377
+ gap: "md",
1378
+ },
1379
+ },
1380
+ "cardGroups/product1": {
1381
+ "ui:surface": "surface-sunken",
1382
+ },
1383
+ "cardGroups/product2": {
1384
+ "ui:surface": "surface-inverse",
1385
+ },
1386
+ "cardGroups/product3": {
1387
+ "ui:surface": "card",
1388
+ },
1389
+ "cardGroups/product4": {
1390
+ "ui:surface": "elevated",
1391
+ },
1392
+ };
1393
+
1394
+ return html`
1395
+ <pds-form
1396
+ .jsonSchema=${schema}
1397
+ .uiSchema=${uiSchema}
1398
+ @pw:submit=${(e) => toastFormData(e.detail)}
1399
+ ></pds-form>
1400
+ `;
1401
+ },
1402
+ };
1403
+
1404
+ export const WithDialogForms = {
1405
+ name: "Dialog-Based Nested Forms",
1406
+ parameters: {
1407
+ docs: {
1408
+ description: {
1409
+ story:
1410
+ 'Dialog-based forms use `ui:dialog` to edit nested objects in modal dialogs. State is transferred via FormData when using `PDS.ask()` with `useForm: true`. Click "Edit" buttons to modify nested data, then submit the main form to see all changes preserved.',
1411
+ },
1412
+ },
1413
+ },
1414
+ render: () => {
1415
+ const schema = {
1416
+ type: "object",
1417
+ properties: {
1418
+ projectName: {
1419
+ type: "string",
1420
+ title: "Project Name",
1421
+ examples: ["Digital Transformation Initiative"],
1422
+ },
1423
+ teamLead: {
1424
+ type: "object",
1425
+ title: "Team Lead",
1426
+ properties: {
1427
+ name: {
1428
+ type: "string",
1429
+ title: "Full Name",
1430
+ examples: ["Sarah Johnson"],
1431
+ },
1432
+ email: {
1433
+ type: "string",
1434
+ format: "email",
1435
+ title: "Email Address",
1436
+ examples: ["sarah.johnson@company.com"],
1437
+ },
1438
+ phone: {
1439
+ type: "string",
1440
+ title: "Phone Number",
1441
+ examples: ["+1-555-0123"],
1442
+ },
1443
+ department: {
1444
+ type: "string",
1445
+ title: "Department",
1446
+ examples: ["Engineering"],
1447
+ },
1448
+ location: {
1449
+ type: "string",
1450
+ title: "Office Location",
1451
+ examples: ["New York Office"],
1452
+ },
1453
+ },
1454
+ },
1455
+ budget: {
1456
+ type: "object",
1457
+ title: "Budget Details",
1458
+ properties: {
1459
+ amount: {
1460
+ type: "number",
1461
+ title: "Budget Amount",
1462
+ examples: [250000],
1463
+ },
1464
+ currency: {
1465
+ type: "string",
1466
+ title: "Currency",
1467
+ enum: ["USD", "EUR", "GBP", "JPY", "AUD"],
1468
+ },
1469
+ fiscalYear: {
1470
+ type: "string",
1471
+ title: "Fiscal Year",
1472
+ examples: ["2025"],
1473
+ },
1474
+ department: {
1475
+ type: "string",
1476
+ title: "Cost Center",
1477
+ examples: ["IT-001"],
1478
+ },
1479
+ approved: { type: "boolean", title: "Budget Approved" },
1480
+ },
1481
+ },
1482
+ timeline: {
1483
+ type: "object",
1484
+ title: "Project Timeline",
1485
+ properties: {
1486
+ startDate: { type: "string", format: "date", title: "Start Date" },
1487
+ endDate: { type: "string", format: "date", title: "End Date" },
1488
+ milestones: {
1489
+ type: "integer",
1490
+ title: "Number of Milestones",
1491
+ minimum: 1,
1492
+ maximum: 20,
1493
+ examples: [8],
1494
+ },
1495
+ status: {
1496
+ type: "string",
1497
+ title: "Status",
1498
+ enum: ["Planning", "In Progress", "On Hold", "Completed"],
1499
+ default: "Planning",
1500
+ },
1501
+ },
1502
+ },
1503
+ },
1504
+ };
1505
+
1506
+ // Initial values to test state persistence
1507
+ const initialValues = {
1508
+ projectName: "Digital Transformation Initiative",
1509
+ teamLead: {
1510
+ name: "Sarah Johnson",
1511
+ email: "sarah.johnson@company.com",
1512
+ phone: "+1-555-0123",
1513
+ department: "Engineering",
1514
+ location: "New York Office",
1515
+ },
1516
+ budget: {
1517
+ amount: 250000,
1518
+ currency: "USD",
1519
+ fiscalYear: "2025",
1520
+ department: "IT-001",
1521
+ approved: true,
1522
+ },
1523
+ timeline: {
1524
+ startDate: "2025-01-15",
1525
+ endDate: "2025-12-31",
1526
+ milestones: 8,
1527
+ status: "In Progress",
1528
+ },
1529
+ };
1530
+
1531
+ const uiSchema = {
1532
+ projectName: {
1533
+ "ui:icon": "folder",
1534
+ "ui:iconPosition": "start",
1535
+ },
1536
+ teamLead: {
1537
+ "ui:dialog": true,
1538
+ "ui:dialogOptions": {
1539
+ buttonLabel: "Edit Team Lead",
1540
+ dialogTitle: "Team Lead Information",
1541
+ icon: "user-gear",
1542
+ },
1543
+ name: { "ui:icon": "user", "ui:iconPosition": "start" },
1544
+ email: { "ui:icon": "envelope", "ui:iconPosition": "start" },
1545
+ phone: { "ui:icon": "phone", "ui:iconPosition": "start" },
1546
+ department: { "ui:icon": "building", "ui:iconPosition": "start" },
1547
+ location: { "ui:icon": "map-pin", "ui:iconPosition": "start" },
1548
+ },
1549
+ budget: {
1550
+ "ui:dialog": true,
1551
+ "ui:dialogOptions": {
1552
+ buttonLabel: "Edit Budget",
1553
+ dialogTitle: "Budget Details",
1554
+ icon: "currency-dollar",
1555
+ },
1556
+ amount: { "ui:icon": "dollar-sign", "ui:iconPosition": "start" },
1557
+ currency: { "ui:icon": "coins", "ui:iconPosition": "start" },
1558
+ fiscalYear: { "ui:icon": "calendar", "ui:iconPosition": "start" },
1559
+ department: { "ui:icon": "building", "ui:iconPosition": "start" },
1560
+ },
1561
+ // Flat path for dialog inner form - currency field inside budget dialog
1562
+ "/currency": { "ui:widget": "select" },
1563
+ // Flat path for dialog inner form - status field inside timeline dialog
1564
+ "/status": { "ui:class": "buttons" },
1565
+ // Flat path for dialog inner form - email field inside teamLead dialog
1566
+ "/email": { "ui:icon": "at", "ui:iconPosition": "start" },
1567
+ // Flat path for dialog inner form - phone field inside teamLead dialog
1568
+ "/phone": { "ui:icon": "phone", "ui:iconPosition": "start" },
1569
+ timeline: {
1570
+ "ui:dialog": true,
1571
+ "ui:dialogOptions": {
1572
+ buttonLabel: "Edit Timeline",
1573
+ dialogTitle: "Project Timeline",
1574
+ icon: "calendar",
1575
+ },
1576
+ startDate: { "ui:icon": "calendar-check", "ui:iconPosition": "start" },
1577
+ endDate: { "ui:icon": "calendar-xmark", "ui:iconPosition": "start" },
1578
+ milestones: { "ui:icon": "flag", "ui:iconPosition": "start" },
1579
+ status: { "ui:icon": "list-check", "ui:iconPosition": "start" },
1580
+ },
1581
+ };
1582
+
1583
+ return html`
1584
+ <pds-form
1585
+ .jsonSchema=${schema}
1586
+ .uiSchema=${uiSchema}
1587
+ .values=${initialValues}
1588
+ @pw:submit=${(e) => toastFormData(e.detail)}
1589
+ @pw:dialog-submit=${(e) => console.log("📝 Dialog saved:", e.detail)}
1590
+ ></pds-form>
1591
+ `;
1592
+ },
1593
+ };
1594
+
1595
+ export const WithRadioGroupOpen = {
1596
+ name: "Radio Group Open (Single Selection)",
1597
+ parameters: {
1598
+ docs: {
1599
+ description: {
1600
+ story: `When an array has \`maxItems: 1\`, it renders as a Radio Group Open, allowing single selection with the ability to add custom options.
1601
+
1602
+ This is perfect for scenarios where users can choose one option from predefined choices or add their own custom value. The \`data-open\` enhancement automatically provides an input field to add new options dynamically.
1603
+
1604
+ ### Key Features:
1605
+ - **Single selection** - Only one option can be selected at a time (radio buttons)
1606
+ - **Add custom options** - Users can type new options in the input field
1607
+ - **Remove options** - Click the × button to remove options
1608
+ - **Pre-populated** - Start with default options from the schema
1609
+
1610
+ This pattern is ideal for fields like "Priority", "Status", "Category", or any single-choice field where users might need custom values.`,
1611
+ },
1612
+ },
1613
+ },
1614
+ render: () => {
1615
+ const schema = {
1616
+ type: "object",
1617
+ properties: {
1618
+ priority: {
1619
+ type: "array",
1620
+ title: "Project Priority",
1621
+ description: "Select one priority level or add your own",
1622
+ items: {
1623
+ type: "string",
1624
+ examples: ["High", "Medium", "Low"],
1625
+ },
1626
+ default: ["High", "Medium", "Low"],
1627
+ uniqueItems: true,
1628
+ maxItems: 1,
1629
+ },
1630
+ status: {
1631
+ type: "array",
1632
+ title: "Current Status",
1633
+ description: "Choose the current project status",
1634
+ items: {
1635
+ type: "string",
1636
+ examples: ["Planning", "In Progress", "Review", "Completed"],
1637
+ },
1638
+ default: ["Planning", "In Progress", "Review", "Completed"],
1639
+ uniqueItems: true,
1640
+ maxItems: 1,
1641
+ },
1642
+ department: {
1643
+ type: "array",
1644
+ title: "Department",
1645
+ description: "Select your department",
1646
+ items: {
1647
+ type: "string",
1648
+ examples: ["Engineering", "Design", "Marketing", "Sales"],
1649
+ },
1650
+ default: ["Engineering", "Design", "Marketing", "Sales"],
1651
+ uniqueItems: true,
1652
+ maxItems: 1,
1653
+ },
1654
+ },
1655
+ required: ["priority", "status"],
1656
+ };
1657
+
1658
+ const initialValues = {
1659
+ priority: ["High", "Medium", "Low"],
1660
+ status: ["Planning", "In Progress", "Review", "Completed"],
1661
+ department: ["Engineering", "Design", "Marketing", "Sales"],
1662
+ };
1663
+
1664
+ return html`
1665
+ <pds-form
1666
+ .jsonSchema=${schema}
1667
+ .values=${initialValues}
1668
+ @pw:value-change=${(e) => console.log("🔄 Value changed:", e.detail)}
1669
+ @pw:submit=${(e) => toastFormData(e.detail)}
1670
+ ></pds-form>
1671
+ `;
1672
+ },
1673
+ };
1674
+
1675
+ export const WithDatalistAutocomplete = {
1676
+ name: "Datalist Autocomplete",
1677
+ render: () => {
1678
+ const schema = {
1679
+ type: "object",
1680
+ properties: {
1681
+ country: {
1682
+ type: "string",
1683
+ title: "Country",
1684
+ examples: ["United States"],
1685
+ },
1686
+ city: {
1687
+ type: "string",
1688
+ title: "City",
1689
+ examples: ["New York"],
1690
+ },
1691
+ skillset: {
1692
+ type: "string",
1693
+ title: "Primary Skill",
1694
+ examples: ["JavaScript"],
1695
+ },
1696
+ company: {
1697
+ type: "string",
1698
+ title: "Company",
1699
+ examples: ["Microsoft"],
1700
+ },
1701
+ },
1702
+ };
1703
+
1704
+ const uiSchema = {
1705
+ country: {
1706
+ "ui:datalist": [
1707
+ "United States",
1708
+ "United Kingdom",
1709
+ "Canada",
1710
+ "Australia",
1711
+ "Germany",
1712
+ "France",
1713
+ "Spain",
1714
+ "Italy",
1715
+ "Japan",
1716
+ "China",
1717
+ "India",
1718
+ "Brazil",
1719
+ ],
1720
+ },
1721
+ city: {
1722
+ "ui:datalist": [
1723
+ "New York",
1724
+ "London",
1725
+ "Tokyo",
1726
+ "Paris",
1727
+ "Berlin",
1728
+ "Sydney",
1729
+ "Toronto",
1730
+ "Amsterdam",
1731
+ "Singapore",
1732
+ "Dubai",
1733
+ ],
1734
+ },
1735
+ skillset: {
1736
+ "ui:datalist": [
1737
+ "JavaScript",
1738
+ "Python",
1739
+ "Java",
1740
+ "C++",
1741
+ "Go",
1742
+ "Rust",
1743
+ "TypeScript",
1744
+ "Ruby",
1745
+ "PHP",
1746
+ "Swift",
1747
+ "Kotlin",
1748
+ ],
1749
+ },
1750
+ company: {
1751
+ "ui:datalist": [
1752
+ "Microsoft",
1753
+ "Google",
1754
+ "Apple",
1755
+ "Amazon",
1756
+ "Meta",
1757
+ "Tesla",
1758
+ "Netflix",
1759
+ "Adobe",
1760
+ "Salesforce",
1761
+ "Oracle",
1762
+ ],
1763
+ },
1764
+ };
1765
+
1766
+ return html`
1767
+ <pds-form
1768
+ .jsonSchema=${schema}
1769
+ .uiSchema=${uiSchema}
1770
+ @pw:submit=${(e) => toastFormData(e.detail)}
1771
+ ></pds-form>
1772
+ `;
1773
+ },
1774
+ };
1775
+
1776
+ export const WithArrayFields = {
1777
+ name: "Dynamic Arrays (Add/Remove)",
1778
+ parameters: {
1779
+ docs: {
1780
+ description: {
1781
+ story: `Arrays in JSON Schema forms allow users to dynamically add and remove items. This is perfect for managing lists like team members, tasks, or any collection that can grow or shrink.
1782
+
1783
+ ### Features:
1784
+ - **Add items** - Click "Add" button to create new entries
1785
+ - **Remove items** - Delete individual items with the "Remove" button
1786
+ - **Reorder items** - Use up/down arrows to change order
1787
+ - **Nested objects** - Each array item can contain complex nested data
1788
+ - **Initial values** - Pre-populate with default items
1789
+ - **Radio Group Open** - Arrays with \`maxItems: 1\` render as radio buttons for single selection`,
1790
+ },
1791
+ },
1792
+ },
1793
+ render: () => {
1794
+ const schema = {
1795
+ type: "object",
1796
+ properties: {
1797
+ projectName: {
1798
+ type: "string",
1799
+ title: "Project Name",
1800
+ examples: ["Website Redesign Project"],
1801
+ },
1802
+ priority: {
1803
+ type: "array",
1804
+ title: "Project Priority",
1805
+ items: {
1806
+ type: "string",
1807
+ examples: ["High", "Medium", "Low"],
1808
+ },
1809
+ default: ["High", "Medium", "Low"],
1810
+ uniqueItems: true,
1811
+ maxItems: 1,
1812
+ },
1813
+ tags: {
1814
+ type: "array",
1815
+ title: "Project Tags",
1816
+ items: {
1817
+ type: "string",
1818
+ },
1819
+ default: ["web", "design", "frontend"],
1820
+ },
1821
+ teamMembers: {
1822
+ type: "array",
1823
+ title: "Team Members",
1824
+ items: {
1825
+ type: "object",
1826
+ properties: {
1827
+ name: {
1828
+ type: "string",
1829
+ title: "Full Name",
1830
+ examples: ["Alice Johnson"],
1831
+ },
1832
+ role: {
1833
+ type: "string",
1834
+ title: "Role",
1835
+ enum: [
1836
+ "Developer",
1837
+ "Designer",
1838
+ "Project Manager",
1839
+ "QA Engineer",
1840
+ "DevOps",
1841
+ ],
1842
+ default: "Developer",
1843
+ },
1844
+ email: {
1845
+ type: "string",
1846
+ format: "email",
1847
+ title: "Email",
1848
+ examples: ["alice.johnson@company.com"],
1849
+ },
1850
+ hours: {
1851
+ type: "number",
1852
+ title: "Hours/Week",
1853
+ minimum: 1,
1854
+ maximum: 40,
1855
+ default: 40,
1856
+ },
1857
+ },
1858
+ required: ["name", "role", "email"],
1859
+ },
1860
+ minItems: 1,
1861
+ },
1862
+ milestones: {
1863
+ type: "array",
1864
+ title: "Project Milestones",
1865
+ items: {
1866
+ type: "object",
1867
+ properties: {
1868
+ title: {
1869
+ type: "string",
1870
+ title: "Milestone Title",
1871
+ examples: ["MVP Launch"],
1872
+ },
1873
+ dueDate: {
1874
+ type: "string",
1875
+ format: "date",
1876
+ title: "Due Date",
1877
+ },
1878
+ completed: {
1879
+ type: "boolean",
1880
+ title: "Completed",
1881
+ default: false,
1882
+ },
1883
+ },
1884
+ required: ["title", "dueDate"],
1885
+ },
1886
+ },
1887
+ },
1888
+ required: ["projectName", "teamMembers"],
1889
+ };
1890
+
1891
+ // Initial values to demonstrate pre-populated arrays
1892
+ const initialValues = {
1893
+ projectName: "Website Redesign Project",
1894
+ priority: ["High", "Medium", "Low"],
1895
+ teamMembers: [
1896
+ {
1897
+ name: "Alice Johnson",
1898
+ role: "Project Manager",
1899
+ email: "alice.johnson@company.com",
1900
+ hours: 40,
1901
+ },
1902
+ {
1903
+ name: "Bob Smith",
1904
+ role: "Developer",
1905
+ email: "bob.smith@company.com",
1906
+ hours: 35,
1907
+ },
1908
+ ],
1909
+ milestones: [
1910
+ {
1911
+ title: "Design Phase Complete",
1912
+ dueDate: "2025-02-01",
1913
+ completed: true,
1914
+ },
1915
+ {
1916
+ title: "MVP Launch",
1917
+ dueDate: "2025-04-15",
1918
+ completed: false,
1919
+ },
1920
+ ],
1921
+ tags: ["web", "design", "frontend", "responsive"],
1922
+ };
1923
+
1924
+ const uiSchema = {
1925
+ teamMembers: {
1926
+ "ui:layout": "default",
1927
+ role: {
1928
+ "ui:widget": "select",
1929
+ },
1930
+ },
1931
+ };
1932
+
1933
+ return html`
1934
+ <pds-form
1935
+ .jsonSchema=${schema}
1936
+ .uiSchema=${uiSchema}
1937
+ .values=${initialValues}
1938
+ @pw:array-add=${(e) => console.log("➕ Item added to:", e.detail.path)}
1939
+ @pw:array-remove=${(e) =>
1940
+ console.log(
1941
+ "➖ Item removed from:",
1942
+ e.detail.path,
1943
+ "at index:",
1944
+ e.detail.index
1945
+ )}
1946
+ @pw:array-reorder=${(e) =>
1947
+ console.log(
1948
+ "🔄 Item moved from",
1949
+ e.detail.from,
1950
+ "to",
1951
+ e.detail.to,
1952
+ "in:",
1953
+ e.detail.path
1954
+ )}
1955
+ @pw:value-change=${(e) => console.log("🔄 Value changed:", e.detail)}
1956
+ @pw:submit=${(e) => toastFormData(e.detail)}
1957
+ ></pds-form>
1958
+ `;
1959
+ },
1960
+ };
1961
+
1962
+ export const ComprehensiveExample = {
1963
+ name: "All Features Combined",
1964
+ render: () => {
1965
+ const schema = {
1966
+ type: "object",
1967
+ properties: {
1968
+ userProfile: {
1969
+ type: "object",
1970
+ title: "User Profile",
1971
+ properties: {
1972
+ personalInfo: {
1973
+ type: "object",
1974
+ title: "Personal Information",
1975
+ properties: {
1976
+ firstName: {
1977
+ type: "string",
1978
+ title: "First Name",
1979
+ examples: ["John"],
1980
+ },
1981
+ lastName: {
1982
+ type: "string",
1983
+ title: "Last Name",
1984
+ examples: ["Doe"],
1985
+ },
1986
+ email: {
1987
+ type: "string",
1988
+ format: "email",
1989
+ title: "Email",
1990
+ examples: ["john.doe@example.com"],
1991
+ },
1992
+ phone: {
1993
+ type: "string",
1994
+ title: "Phone",
1995
+ examples: ["+1 (555) 123-4567"],
1996
+ },
1997
+ dateOfBirth: {
1998
+ type: "string",
1999
+ format: "date",
2000
+ title: "Date of Birth",
2001
+ },
2002
+ },
2003
+ required: ["firstName", "lastName", "email"],
2004
+ },
2005
+ settings: {
2006
+ type: "object",
2007
+ title: "Settings",
2008
+ properties: {
2009
+ accountSettings: {
2010
+ type: "object",
2011
+ title: "Account Settings",
2012
+ properties: {
2013
+ notifications: {
2014
+ type: "boolean",
2015
+ title: "Email Notifications",
2016
+ },
2017
+ newsletter: {
2018
+ type: "boolean",
2019
+ title: "Newsletter Subscription",
2020
+ },
2021
+ twoFactor: {
2022
+ type: "boolean",
2023
+ title: "Two-Factor Authentication",
2024
+ },
2025
+ },
2026
+ },
2027
+ preferences: {
2028
+ type: "object",
2029
+ title: "Preferences",
2030
+ properties: {
2031
+ theme: {
2032
+ type: "string",
2033
+ title: "Theme",
2034
+ enum: ["Light", "Dark", "Auto"],
2035
+ },
2036
+ language: {
2037
+ type: "string",
2038
+ title: "Language",
2039
+ enum: [
2040
+ "English",
2041
+ "Spanish",
2042
+ "French",
2043
+ "German",
2044
+ "Chinese",
2045
+ "Japanese",
2046
+ ],
2047
+ },
2048
+ fontSize: {
2049
+ type: "number",
2050
+ title: "Font Size",
2051
+ minimum: 12,
2052
+ maximum: 20,
2053
+ default: 14,
2054
+ },
2055
+ lineHeight: {
2056
+ type: "number",
2057
+ title: "Line Height",
2058
+ minimum: 1.0,
2059
+ maximum: 2.0,
2060
+ default: 1.5,
2061
+ },
2062
+ },
2063
+ },
2064
+ },
2065
+ },
2066
+ },
2067
+ },
2068
+ profile: {
2069
+ type: "object",
2070
+ title: "Profile",
2071
+ properties: {
2072
+ avatar: {
2073
+ type: "string",
2074
+ title: "Profile Picture",
2075
+ contentMediaType: "image/*",
2076
+ contentEncoding: "base64",
2077
+ },
2078
+ bio: {
2079
+ type: "string",
2080
+ title: "Biography",
2081
+ examples: ["Tell us about yourself..."],
2082
+ },
2083
+ website: {
2084
+ type: "string",
2085
+ format: "uri",
2086
+ title: "Website",
2087
+ examples: ["https://yourwebsite.com"],
2088
+ },
2089
+ },
2090
+ },
2091
+ },
2092
+ };
2093
+
2094
+ const uiSchema = {
2095
+ userProfile: {
2096
+ "ui:layout": "accordion",
2097
+ "ui:layoutOptions": { openFirst: true },
2098
+ "ui:surface": "elevated",
2099
+ personalInfo: {
2100
+ "ui:layout": "grid",
2101
+ "ui:layoutOptions": { columns: 2, gap: "md" },
2102
+ "ui:surface": "sunken",
2103
+ email: { "ui:icon": "envelope", "ui:iconPosition": "start" },
2104
+ phone: { "ui:icon": "phone", "ui:iconPosition": "start" },
2105
+ },
2106
+ settings: {
2107
+ accountSettings: {
2108
+ "ui:surface": "sunken",
2109
+ },
2110
+ preferences: {
2111
+ "ui:surface": "sunken",
2112
+ theme: { "ui:class": "buttons" },
2113
+ fontSize: { "ui:widget": "input-range" },
2114
+ lineHeight: { "ui:widget": "input-range" },
2115
+ },
2116
+ },
2117
+ },
2118
+ profile: {
2119
+ "ui:dialog": true,
2120
+ "ui:dialogOptions": {
2121
+ buttonLabel: "Edit Profile",
2122
+ dialogTitle: "Your Profile Information",
2123
+ },
2124
+ avatar: {
2125
+ "ui:options": {
2126
+ accept: "image/*",
2127
+ maxSize: 5242880,
2128
+ label: "Upload Avatar",
2129
+ },
2130
+ },
2131
+ bio: {
2132
+ "ui:widget": "richtext",
2133
+ "ui:options": {
2134
+ toolbar: "standard",
2135
+ },
2136
+ },
2137
+ website: { "ui:icon": "globe", "ui:iconPosition": "start" },
2138
+ },
2139
+ };
2140
+
2141
+ const options = {
2142
+ widgets: {
2143
+ booleans: "toggle",
2144
+ },
2145
+ enhancements: {
2146
+ rangeOutput: true,
2147
+ },
2148
+ };
2149
+
2150
+ return html`
2151
+ <pds-form
2152
+ .jsonSchema=${schema}
2153
+ .uiSchema=${uiSchema}
2154
+ .options=${options}
2155
+ @pw:value-change=${(e) => console.log("🔄 Changed:", e.detail)}
2156
+ @pw:submit=${(e) => toastFormData(e.detail)}
2157
+ ></pds-form>
2158
+ `;
2159
+ },
2160
+ };
2161
+
2162
+ export const CustomFormActions = {
2163
+ name: "Custom Form Actions",
2164
+ parameters: {
2165
+ docs: {
2166
+ description: {
2167
+ story: `Demonstrates using \`hide-actions\` to provide custom form submission buttons and handling.
2168
+
2169
+ When \`hide-actions\` is set, the default Submit and Reset buttons are hidden, allowing you to create custom action buttons in the \`actions\` slot. You can then handle form submission programmatically using the form's \`submit()\` method or by manually triggering the form element.`,
2170
+ },
2171
+ },
2172
+ },
2173
+ render: () => {
2174
+ const schema = {
2175
+ type: "object",
2176
+ properties: {
2177
+ username: {
2178
+ type: "string",
2179
+ title: "Username",
2180
+ minLength: 3,
2181
+ examples: ["johndoe"],
2182
+ },
2183
+ email: {
2184
+ type: "string",
2185
+ format: "email",
2186
+ title: "Email",
2187
+ examples: ["john@example.com"],
2188
+ },
2189
+ password: {
2190
+ type: "string",
2191
+ format: "password",
2192
+ title: "Password",
2193
+ minLength: 8,
2194
+ },
2195
+ terms: {
2196
+ type: "boolean",
2197
+ title: "I agree to the terms and conditions",
2198
+ },
2199
+ },
2200
+ required: ["username", "email", "password", "terms"],
2201
+ };
2202
+
2203
+ const handleSaveDraft = (e) => {
2204
+ const form = e.target.closest("pds-form");
2205
+ const data = form.serialize();
2206
+ console.log("💾 Saving draft:", data.json);
2207
+ };
2208
+
2209
+ return html`
2210
+ <pds-form
2211
+ .jsonSchema=${schema}
2212
+ hide-actions
2213
+ @pw:value-change=${(e) => console.log("🔄 Field changed:", e.detail)}
2214
+ @pw:submit=${(e) => toastFormData(e.detail)}
2215
+ >
2216
+ <div slot="actions" class="flex gap-sm items-center">
2217
+ <button type="submit" class="btn btn-primary">
2218
+ <pds-icon icon="check"></pds-icon>
2219
+ Create Account
2220
+ </button>
2221
+ <button type="button" class="btn" @click=${handleSaveDraft}>
2222
+ <pds-icon icon="file"></pds-icon>
2223
+ Save Draft
2224
+ </button>
2225
+ <button
2226
+ type="button"
2227
+ class="btn btn-secondary btn-outline grow text-right"
2228
+ @click=${() => console.log("Registration cancelled")}
2229
+ >
2230
+ Cancel
2231
+ </button>
2232
+ </div>
2233
+ </pds-form>
2234
+ `;
2235
+ },
2236
+ };
2237
+
2238
+ // =============================================================================
2239
+ // Root-Level Layout Stories
2240
+ // =============================================================================
2241
+
2242
+ export const RootGridLayout = {
2243
+ name: "Root-Level Grid Layout",
2244
+ parameters: {
2245
+ docs: {
2246
+ description: {
2247
+ story: `Apply a grid layout to the entire form using root-level \`ui:layout\` and \`ui:layoutOptions\`.
2248
+
2249
+ This allows you to control the form layout **without modifying your JSON Schema** — keeping data structure separate from presentation.
2250
+
2251
+ \`\`\`javascript
2252
+ const uiSchema = {
2253
+ 'ui:layout': 'grid',
2254
+ 'ui:layoutOptions': {
2255
+ columns: 2,
2256
+ gap: 'md'
2257
+ }
2258
+ };
2259
+ \`\`\``,
2260
+ },
2261
+ },
2262
+ },
2263
+ render: () => {
2264
+ const schema = {
2265
+ type: "object",
2266
+ title: "Contact Form",
2267
+ properties: {
2268
+ firstName: { type: "string", title: "First Name", examples: ["John"] },
2269
+ lastName: { type: "string", title: "Last Name", examples: ["Doe"] },
2270
+ email: {
2271
+ type: "string",
2272
+ format: "email",
2273
+ title: "Email",
2274
+ examples: ["john.doe@example.com"],
2275
+ },
2276
+ phone: {
2277
+ type: "string",
2278
+ title: "Phone",
2279
+ examples: ["+1 (555) 123-4567"],
2280
+ },
2281
+ company: { type: "string", title: "Company", examples: ["Acme Inc."] },
2282
+ role: { type: "string", title: "Role", examples: ["Developer"] },
2283
+ },
2284
+ required: ["firstName", "lastName", "email"],
2285
+ };
2286
+
2287
+ const uiSchema = {
2288
+ "ui:layout": "grid",
2289
+ "ui:layoutOptions": {
2290
+ columns: 2,
2291
+ gap: "md",
2292
+ },
2293
+ };
2294
+
2295
+ return html`
2296
+ <pds-form
2297
+ .jsonSchema=${schema}
2298
+ .uiSchema=${uiSchema}
2299
+ @pw:submit=${(e) => toastFormData(e.detail)}
2300
+ ></pds-form>
2301
+ `;
2302
+ },
2303
+ };
2304
+
2305
+ export const RootFlexLayout = {
2306
+ name: "Root-Level Flex Layout",
2307
+ parameters: {
2308
+ docs: {
2309
+ description: {
2310
+ story: `Apply a flex layout to the entire form using root-level \`ui:layout\` and \`ui:layoutOptions\`.
2311
+
2312
+ \`\`\`javascript
2313
+ const uiSchema = {
2314
+ 'ui:layout': 'flex',
2315
+ 'ui:layoutOptions': {
2316
+ gap: 'lg',
2317
+ wrap: true
2318
+ }
2319
+ };
2320
+ \`\`\``,
2321
+ },
2322
+ },
2323
+ },
2324
+ render: () => {
2325
+ const schema = {
2326
+ type: "object",
2327
+ title: "Quick Settings",
2328
+ properties: {
2329
+ theme: {
2330
+ type: "string",
2331
+ enum: ["light", "dark", "system"],
2332
+ title: "Theme",
2333
+ default: "system",
2334
+ },
2335
+ notifications: {
2336
+ type: "boolean",
2337
+ title: "Notifications",
2338
+ default: true,
2339
+ },
2340
+ language: {
2341
+ type: "string",
2342
+ enum: ["en", "es", "fr", "de"],
2343
+ title: "Language",
2344
+ default: "en",
2345
+ },
2346
+ },
2347
+ };
2348
+
2349
+ const uiSchema = {
2350
+ "ui:layout": "flex",
2351
+ "ui:layoutOptions": {
2352
+ gap: "lg",
2353
+ wrap: true,
2354
+ },
2355
+ };
2356
+
2357
+ return html`
2358
+ <pds-form
2359
+ .jsonSchema=${schema}
2360
+ .uiSchema=${uiSchema}
2361
+ @pw:submit=${(e) => toastFormData(e.detail)}
2362
+ ></pds-form>
2363
+ `;
2364
+ },
2365
+ };
2366
+
2367
+ export const RootLayoutWithFieldOptions = {
2368
+ name: "Root Layout + Field Options",
2369
+ parameters: {
2370
+ docs: {
2371
+ description: {
2372
+ story: `Combine root-level layout with field-specific UI options.
2373
+
2374
+ Root-level \`ui:*\` properties control the overall form layout, while path-keyed entries (like \`'/email'\`) customize individual fields.
2375
+
2376
+ \`\`\`javascript
2377
+ const uiSchema = {
2378
+ // Root form layout
2379
+ 'ui:layout': 'grid',
2380
+ 'ui:layoutOptions': { columns: 2, gap: 'md' },
2381
+
2382
+ // Field-specific options
2383
+ '/email': { 'ui:icon': 'envelope' },
2384
+ '/bio': { 'ui:widget': 'textarea', 'ui:options': { rows: 4 } }
2385
+ };
2386
+ \`\`\``,
2387
+ },
2388
+ },
2389
+ },
2390
+ render: () => {
2391
+ const schema = {
2392
+ type: "object",
2393
+ title: "User Profile",
2394
+ properties: {
2395
+ username: {
2396
+ type: "string",
2397
+ title: "Username",
2398
+ minLength: 3,
2399
+ examples: ["johndoe"],
2400
+ },
2401
+ email: {
2402
+ type: "string",
2403
+ format: "email",
2404
+ title: "Email",
2405
+ examples: ["john@example.com"],
2406
+ },
2407
+ bio: {
2408
+ type: "string",
2409
+ title: "Bio",
2410
+ maxLength: 500,
2411
+ examples: ["Tell us about yourself..."],
2412
+ },
2413
+ website: {
2414
+ type: "string",
2415
+ format: "uri",
2416
+ title: "Website",
2417
+ examples: ["https://yoursite.com"],
2418
+ },
2419
+ },
2420
+ required: ["username", "email"],
2421
+ };
2422
+
2423
+ const uiSchema = {
2424
+ // Root form layout
2425
+ "ui:layout": "grid",
2426
+ "ui:layoutOptions": {
2427
+ columns: 2,
2428
+ gap: "md",
2429
+ },
2430
+ // Field-specific options
2431
+ "/username": {
2432
+ "ui:icon": "user",
2433
+ },
2434
+ "/email": {
2435
+ "ui:icon": "envelope",
2436
+ "ui:help": "We will never share your email",
2437
+ },
2438
+ "/bio": {
2439
+ "ui:widget": "textarea",
2440
+ "ui:options": { rows: 4 },
2441
+ "ui:class": "grid-col-span-2",
2442
+ },
2443
+ "/website": {
2444
+ "ui:icon": "globe",
2445
+ "ui:class": "grid-col-span-2",
2446
+ },
2447
+ };
2448
+
2449
+ return html`
2450
+ <pds-form
2451
+ .jsonSchema=${schema}
2452
+ .uiSchema=${uiSchema}
2453
+ @pw:submit=${(e) => toastFormData(e.detail)}
2454
+ ></pds-form>
2455
+ `;
2456
+ },
2457
+ };
2458
+
2459
+ export const RootThreeColumnGrid = {
2460
+ name: "Root 3-Column Grid",
2461
+ parameters: {
2462
+ docs: {
2463
+ description: {
2464
+ story: `A more complex form using a 3-column root grid layout with various field types.`,
2465
+ },
2466
+ },
2467
+ },
2468
+ render: () => {
2469
+ const schema = {
2470
+ type: "object",
2471
+ title: "Product Registration",
2472
+ properties: {
2473
+ productName: {
2474
+ type: "string",
2475
+ title: "Product Name",
2476
+ examples: ["Widget Pro"],
2477
+ },
2478
+ serialNumber: {
2479
+ type: "string",
2480
+ title: "Serial Number",
2481
+ examples: ["SN-12345"],
2482
+ },
2483
+ purchaseDate: {
2484
+ type: "string",
2485
+ format: "date",
2486
+ title: "Purchase Date",
2487
+ },
2488
+ retailer: { type: "string", title: "Retailer", examples: ["Amazon"] },
2489
+ price: {
2490
+ type: "number",
2491
+ title: "Price",
2492
+ minimum: 0,
2493
+ examples: [99.99],
2494
+ },
2495
+ currency: {
2496
+ type: "string",
2497
+ enum: ["USD", "EUR", "GBP"],
2498
+ title: "Currency",
2499
+ default: "USD",
2500
+ },
2501
+ condition: {
2502
+ type: "string",
2503
+ enum: ["new", "refurbished", "used"],
2504
+ title: "Condition",
2505
+ default: "new",
2506
+ },
2507
+ extendedWarranty: { type: "boolean", title: "Extended Warranty" },
2508
+ newsletter: { type: "boolean", title: "Subscribe to Newsletter" },
2509
+ },
2510
+ required: ["productName", "serialNumber", "purchaseDate"],
2511
+ };
2512
+
2513
+ const uiSchema = {
2514
+ "ui:layout": "grid",
2515
+ "ui:layoutOptions": {
2516
+ columns: 3,
2517
+ gap: "md",
2518
+ },
2519
+ };
2520
+
2521
+ const options = {
2522
+ widgets: { booleans: "toggle" },
2523
+ };
2524
+
2525
+ return html`
2526
+ <pds-form
2527
+ .jsonSchema=${schema}
2528
+ .uiSchema=${uiSchema}
2529
+ .options=${options}
2530
+ @pw:submit=${(e) => toastFormData(e.detail)}
2531
+ ></pds-form>
2532
+ `;
2533
+ },
2534
+ };
2535
+
2536
+ export const EnumWithOneOfAnyOf = {
2537
+ name: "Selection Fields with Custom Labels (oneOf/anyOf)",
2538
+ parameters: {
2539
+ docs: {
2540
+ description: {
2541
+ story: `Use \`oneOf\` or \`anyOf\` with \`const\` and \`title\` to provide human-friendly display labels for enum values.
2542
+
2543
+ This is useful when you want to store technical values (like language codes or IDs) while showing user-friendly labels.
2544
+
2545
+ \`\`\`javascript
2546
+ const schema = {
2547
+ properties: {
2548
+ language: {
2549
+ type: 'string',
2550
+ title: 'Language',
2551
+ oneOf: [ // Use oneOf or anyOf
2552
+ { const: 'en', title: 'English' },
2553
+ { const: 'es', title: 'Spanish' },
2554
+ { const: 'fr', title: 'French' },
2555
+ { const: 'de', title: 'German' },
2556
+ { const: 'zh', title: 'Chinese' },
2557
+ { const: 'ja', title: 'Japanese' }
2558
+ ]
2559
+ }
2560
+ }
2561
+ };
2562
+ \`\`\`
2563
+
2564
+ Both \`oneOf\` and \`anyOf\` work identically for this purpose. Each option should have \`const\` (the value stored) and \`title\` (the label displayed). Works with select dropdowns, radio buttons, and checkbox groups.`,
2565
+ },
2566
+ },
2567
+ },
2568
+ render: () => {
2569
+ const schema = {
2570
+ type: "object",
2571
+ title: "Preferences",
2572
+ properties: {
2573
+ language: {
2574
+ type: "string",
2575
+ title: "Language",
2576
+ oneOf: [
2577
+ { const: "en", title: "English" },
2578
+ { const: "es", title: "Spanish" },
2579
+ { const: "fr", title: "French" },
2580
+ { const: "de", title: "German" },
2581
+ { const: "zh", title: "Chinese" },
2582
+ { const: "ja", title: "Japanese" },
2583
+ ],
2584
+ default: "en",
2585
+ },
2586
+ country: {
2587
+ type: "string",
2588
+ title: "Country",
2589
+ anyOf: [
2590
+ { const: "US", title: "United States" },
2591
+ { const: "GB", title: "United Kingdom" },
2592
+ { const: "CA", title: "Canada" },
2593
+ { const: "AU", title: "Australia" },
2594
+ { const: "DE", title: "Germany" },
2595
+ { const: "FR", title: "France" },
2596
+ ],
2597
+ },
2598
+ priority: {
2599
+ type: "string",
2600
+ title: "Priority Level",
2601
+ oneOf: [
2602
+ { const: "p1", title: "🔴 Critical" },
2603
+ { const: "p2", title: "🟠 High" },
2604
+ { const: "p3", title: "🟡 Medium" },
2605
+ { const: "p4", title: "🟢 Low" },
2606
+ ],
2607
+ default: "p3",
2608
+ },
2609
+ interests: {
2610
+ type: "array",
2611
+ title: "Interests (checkbox group)",
2612
+ items: {
2613
+ type: "string",
2614
+ oneOf: [
2615
+ { const: "dev", title: "Development" },
2616
+ { const: "design", title: "Design" },
2617
+ { const: "pm", title: "Project Management" },
2618
+ { const: "qa", title: "Quality Assurance" },
2619
+ { const: "devops", title: "DevOps" },
2620
+ ],
2621
+ },
2622
+ uniqueItems: true,
2623
+ },
2624
+ },
2625
+ };
2626
+
2627
+ const uiSchema = {
2628
+ "/priority": { "ui:widget": "radio" },
2629
+ };
2630
+
2631
+ return html`
2632
+ <pds-form
2633
+ .jsonSchema=${schema}
2634
+ .uiSchema=${uiSchema}
2635
+ @pw:submit=${(e) => toastFormData(e.detail)}
2636
+ ></pds-form>
2637
+ `;
2638
+ },
2639
+ };
2640
+
2641
+ export const ConditionalShowHide = {
2642
+ parameters: {
2643
+ docs: {
2644
+ description: {
2645
+ story: `Use \`ui:visibleWhen\` to show/hide fields based on other field values.
2646
+
2647
+ In this example, selecting "Other" from the dropdown reveals a text field to specify details.
2648
+
2649
+ \`\`\`javascript
2650
+ const uiSchema = {
2651
+ '/otherReason': {
2652
+ 'ui:visibleWhen': { '/reason': 'other' },
2653
+ 'ui:requiredWhen': { '/reason': 'other' }
2654
+ }
2655
+ };
2656
+ \`\`\``,
2657
+ },
2658
+ },
2659
+ },
2660
+ render: () => {
2661
+ const schema = {
2662
+ type: "object",
2663
+ title: "Feedback Form",
2664
+ properties: {
2665
+ reason: {
2666
+ type: "string",
2667
+ title: "How did you hear about us?",
2668
+ oneOf: [
2669
+ { const: "search", title: "Search Engine" },
2670
+ { const: "social", title: "Social Media" },
2671
+ { const: "friend", title: "Friend Referral" },
2672
+ { const: "other", title: "Other... (please specify)" },
2673
+ ],
2674
+ },
2675
+ otherReason: {
2676
+ type: "string",
2677
+ title: "Please specify",
2678
+ examples: ["Tell us more..."],
2679
+ },
2680
+ },
2681
+ };
2682
+
2683
+ const uiSchema = {
2684
+ "/otherReason": {
2685
+ "ui:visibleWhen": { "/reason": "other" },
2686
+ "ui:requiredWhen": { "/reason": "other" },
2687
+ },
2688
+ };
2689
+
2690
+ return html`
2691
+ <pds-form
2692
+ data-required
2693
+ .jsonSchema=${schema}
2694
+ .uiSchema=${uiSchema}
2695
+ @pw:submit=${(e) => toastFormData(e.detail)}
2696
+ ></pds-form>
2697
+ `;
2698
+ },
2699
+ };
2700
+
2701
+ export const ConditionalRequired = {
2702
+ parameters: {
2703
+ docs: {
2704
+ description: {
2705
+ story: `Use \`ui:requiredWhen\` to make fields conditionally required.
2706
+
2707
+ In this example, toggling "Prefer phone contact" makes the phone field required and disables the email field.
2708
+
2709
+ \`\`\`javascript
2710
+ const uiSchema = {
2711
+ '/email': {
2712
+ 'ui:disabledWhen': { '/preferPhone': true }
2713
+ },
2714
+ '/phone': {
2715
+ 'ui:requiredWhen': { '/preferPhone': true }
2716
+ }
2717
+ };
2718
+ \`\`\``,
2719
+ },
2720
+ },
2721
+ },
2722
+ render: () => {
2723
+ const schema = {
2724
+ type: "object",
2725
+ title: "Contact Preferences",
2726
+ properties: {
2727
+ preferPhone: {
2728
+ type: "boolean",
2729
+ title: "I prefer to be contacted by phone",
2730
+ default: false,
2731
+ },
2732
+ email: {
2733
+ type: "string",
2734
+ format: "email",
2735
+ title: "Email Address",
2736
+ examples: ["you@example.com"],
2737
+ },
2738
+ phone: {
2739
+ type: "string",
2740
+ title: "Phone Number",
2741
+ examples: ["555-123-4567"],
2742
+ },
2743
+ },
2744
+ required: ["email"],
2745
+ };
2746
+
2747
+ const uiSchema = {
2748
+ "/email": {
2749
+ "ui:disabledWhen": { "/preferPhone": true },
2750
+ "ui:icon": "envelope",
2751
+ },
2752
+ "/phone": {
2753
+ "ui:requiredWhen": { "/preferPhone": true },
2754
+ "ui:icon": "phone",
2755
+ },
2756
+ };
2757
+
2758
+ return html`
2759
+ <pds-form
2760
+ data-required
2761
+ .jsonSchema=${schema}
2762
+ .uiSchema=${uiSchema}
2763
+ @pw:submit=${(e) => toastFormData(e.detail)}
2764
+ ></pds-form>
2765
+ `;
2766
+ },
2767
+ };
2768
+
2769
+ export const ConditionalComplex = {
2770
+ parameters: {
2771
+ docs: {
2772
+ description: {
2773
+ story: `A comprehensive example combining multiple conditional features: visibility, required states, and calculated values.
2774
+
2775
+ ### Features demonstrated:
2776
+ - **Show/Hide**: Company name appears for business accounts; shipping address hidden for pickup
2777
+ - **Conditional Required**: Company name required for business; phone required when preferred
2778
+ - **Disable**: Email disabled when phone is preferred
2779
+ - **Calculations**: Full name, subtotal, shipping cost, and total are all computed automatically`,
2780
+ },
2781
+ },
2782
+ },
2783
+ render: () => {
2784
+ const schema = {
2785
+ type: "object",
2786
+ title: "Order Form",
2787
+ properties: {
2788
+ accountType: {
2789
+ type: "string",
2790
+ title: "Account Type",
2791
+ oneOf: [
2792
+ { const: "personal", title: "Personal" },
2793
+ { const: "business", title: "Business" },
2794
+ ],
2795
+ default: "personal",
2796
+ },
2797
+ companyName: {
2798
+ type: "string",
2799
+ title: "Company Name",
2800
+ examples: ["Acme Inc."],
2801
+ },
2802
+ preferPhone: {
2803
+ type: "boolean",
2804
+ title: "I prefer to be contacted by phone",
2805
+ default: false,
2806
+ },
2807
+ email: {
2808
+ type: "string",
2809
+ format: "email",
2810
+ title: "Email",
2811
+ examples: ["you@example.com"],
2812
+ },
2813
+ phone: {
2814
+ type: "string",
2815
+ title: "Phone",
2816
+ examples: ["555-123-4567"],
2817
+ },
2818
+ firstName: {
2819
+ type: "string",
2820
+ title: "First Name",
2821
+ examples: ["John"],
2822
+ },
2823
+ lastName: {
2824
+ type: "string",
2825
+ title: "Last Name",
2826
+ examples: ["Doe"],
2827
+ },
2828
+ fullName: {
2829
+ type: "string",
2830
+ title: "Full Name (calculated)",
2831
+ },
2832
+ deliveryType: {
2833
+ type: "string",
2834
+ title: "Delivery Type",
2835
+ oneOf: [
2836
+ { const: "standard", title: "Standard (5-7 days)" },
2837
+ { const: "express", title: "Express (1-2 days)" },
2838
+ { const: "pickup", title: "Pickup" },
2839
+ ],
2840
+ default: "standard",
2841
+ },
2842
+ quantity: {
2843
+ type: "integer",
2844
+ title: "Quantity",
2845
+ minimum: 1,
2846
+ default: 1,
2847
+ },
2848
+ unitPrice: {
2849
+ type: "number",
2850
+ title: "Unit Price",
2851
+ default: 29.99,
2852
+ },
2853
+ subtotal: {
2854
+ type: "number",
2855
+ title: "Subtotal",
2856
+ },
2857
+ shippingCost: {
2858
+ type: "number",
2859
+ title: "Shipping Cost",
2860
+ },
2861
+ total: {
2862
+ type: "number",
2863
+ title: "Total",
2864
+ },
2865
+ shippingAddress: {
2866
+ type: "object",
2867
+ title: "Shipping Address",
2868
+ properties: {
2869
+ street: {
2870
+ type: "string",
2871
+ title: "Street",
2872
+ examples: ["123 Main St"],
2873
+ },
2874
+ city: { type: "string", title: "City", examples: ["New York"] },
2875
+ zip: { type: "string", title: "ZIP Code", examples: ["10001"] },
2876
+ },
2877
+ },
2878
+ },
2879
+ required: ["email", "firstName", "lastName", "deliveryType"],
2880
+ };
2881
+
2882
+ const uiSchema = {
2883
+ "ui:layout": "grid",
2884
+ "ui:layoutOptions": { columns: 2, gap: "md" },
2885
+
2886
+ "/companyName": {
2887
+ "ui:visibleWhen": { "/accountType": "business" },
2888
+ "ui:requiredWhen": { "/accountType": "business" },
2889
+ },
2890
+ "/email": {
2891
+ "ui:disabledWhen": { "/preferPhone": true },
2892
+ "ui:icon": "envelope",
2893
+ },
2894
+ "/phone": {
2895
+ "ui:requiredWhen": { "/preferPhone": true },
2896
+ "ui:icon": "phone",
2897
+ },
2898
+ "/fullName": {
2899
+ "ui:calculate": { $concat: ["/firstName", " ", "/lastName"] },
2900
+ },
2901
+ "/subtotal": {
2902
+ "ui:calculate": { $multiply: ["/quantity", "/unitPrice"] },
2903
+ },
2904
+ "/shippingCost": {
2905
+ "ui:calculate": {
2906
+ $if: {
2907
+ cond: { "/deliveryType": "express" },
2908
+ then: 25,
2909
+ else: {
2910
+ $if: {
2911
+ cond: { "/deliveryType": "pickup" },
2912
+ then: 0,
2913
+ else: 10,
2914
+ },
2915
+ },
2916
+ },
2917
+ },
2918
+ },
2919
+ "/total": {
2920
+ "ui:calculate": { $sum: ["/subtotal", "/shippingCost"] },
2921
+ },
2922
+ "/shippingAddress": {
2923
+ "ui:visibleWhen": { "/deliveryType": { $ne: "pickup" } },
2924
+ "ui:layout": "flex",
2925
+ "ui:layoutOptions": { gap: "sm", wrap: true },
2926
+ },
2927
+ };
2928
+
2929
+ return html`
2930
+ <div class="alert alert-info">
2931
+ <p><strong>Try these interactions:</strong></p>
2932
+ <ul>
2933
+ <li>
2934
+ Change <strong>Account Type</strong> to "Business" → Company Name
2935
+ field appears and becomes required
2936
+ </li>
2937
+ <li>
2938
+ Toggle <strong>"Prefer phone contact"</strong> → Email is disabled,
2939
+ Phone becomes required
2940
+ </li>
2941
+ <li>
2942
+ Type in <strong>First/Last Name</strong> → Full Name is calculated
2943
+ automatically
2944
+ </li>
2945
+ <li>
2946
+ Change <strong>Quantity</strong> or <strong>Unit Price</strong> →
2947
+ Subtotal and Total update
2948
+ </li>
2949
+ <li>
2950
+ Select <strong>Delivery Type</strong> → Shipping cost changes
2951
+ (Express: $25, Standard: $10, Pickup: $0)
2952
+ </li>
2953
+ <li>
2954
+ Select <strong>"Pickup"</strong> → Shipping Address section is
2955
+ hidden
2956
+ </li>
2957
+ </ul>
2958
+ </div>
2959
+ <pds-form
2960
+ data-required
2961
+ .jsonSchema=${schema}
2962
+ .uiSchema=${uiSchema}
2963
+ @pw:submit=${(e) => toastFormData(e.detail)}
2964
+ ></pds-form>
2965
+ `;
2966
+ },
2967
+ };
2968
+
2969
+ export const CalculatedValues = {
2970
+ parameters: {
2971
+ docs: {
2972
+ description: {
2973
+ story: `Use \`ui:calculate\` to compute values from other fields. Calculated fields are **read-only by default**.
2974
+
2975
+ ### Available Operators
2976
+ - \`$concat\`: Join strings → \`{ "$concat": ["/first", " ", "/last"] }\`
2977
+ - \`$sum\`: Add numbers → \`{ "$sum": ["/a", "/b", "/c"] }\`
2978
+ - \`$subtract\`: Subtract → \`{ "$subtract": ["/total", "/discount"] }\`
2979
+ - \`$multiply\`: Multiply → \`{ "$multiply": ["/qty", "/price"] }\`
2980
+ - \`$divide\`: Divide → \`{ "$divide": ["/total", "/count"] }\`
2981
+ - \`$coalesce\`: First non-empty → \`{ "$coalesce": ["/nickname", "/firstName"] }\``,
2982
+ },
2983
+ },
2984
+ },
2985
+ render: () => {
2986
+ const schema = {
2987
+ type: "object",
2988
+ title: "Invoice Calculator",
2989
+ properties: {
2990
+ firstName: { type: "string", title: "First Name", examples: ["John"] },
2991
+ lastName: { type: "string", title: "Last Name", examples: ["Doe"] },
2992
+ nickname: {
2993
+ type: "string",
2994
+ title: "Nickname (optional)",
2995
+ examples: ["Johnny"],
2996
+ },
2997
+ displayName: {
2998
+ type: "string",
2999
+ title: "Display Name (nickname or first name)",
3000
+ },
3001
+ fullName: { type: "string", title: "Full Name" },
3002
+ quantity: {
3003
+ type: "integer",
3004
+ title: "Quantity",
3005
+ minimum: 1,
3006
+ default: 2,
3007
+ },
3008
+ unitPrice: { type: "number", title: "Unit Price ($)", default: 49.99 },
3009
+ subtotal: { type: "number", title: "Subtotal" },
3010
+ taxRate: { type: "number", title: "Tax Rate (%)", default: 8.5 },
3011
+ taxAmount: { type: "number", title: "Tax Amount" },
3012
+ discount: { type: "number", title: "Discount ($)", default: 10 },
3013
+ total: { type: "number", title: "Grand Total" },
3014
+ },
3015
+ };
3016
+
3017
+ const uiSchema = {
3018
+ "ui:layout": "grid",
3019
+ "ui:layoutOptions": { columns: 2, gap: "md" },
3020
+
3021
+ // String concatenation
3022
+ "/fullName": {
3023
+ "ui:calculate": { $concat: ["/firstName", " ", "/lastName"] },
3024
+ },
3025
+
3026
+ // Coalesce: use nickname if provided, otherwise first name
3027
+ "/displayName": {
3028
+ "ui:calculate": { $coalesce: ["/nickname", "/firstName"] },
3029
+ },
3030
+
3031
+ // Multiply: quantity × price
3032
+ "/subtotal": {
3033
+ "ui:calculate": { $multiply: ["/quantity", "/unitPrice"] },
3034
+ },
3035
+
3036
+ // Divide + multiply for tax: (subtotal × taxRate) / 100
3037
+ "/taxAmount": {
3038
+ "ui:calculate": {
3039
+ $divide: [{ $multiply: ["/subtotal", "/taxRate"] }, 100],
3040
+ },
3041
+ },
3042
+
3043
+ // Sum and subtract: subtotal + tax - discount
3044
+ "/total": {
3045
+ "ui:calculate": {
3046
+ $subtract: [{ $sum: ["/subtotal", "/taxAmount"] }, "/discount"],
3047
+ },
3048
+ },
3049
+ };
3050
+
3051
+ return html`
3052
+ <div class="alert alert-info">
3053
+ <p><strong>All calculated fields update automatically:</strong></p>
3054
+ <ul>
3055
+ <li>
3056
+ <strong>Full Name</strong> = First + Last (using
3057
+ <code>$concat</code>)
3058
+ </li>
3059
+ <li>
3060
+ <strong>Display Name</strong> = Nickname if set, otherwise First
3061
+ Name (using <code>$coalesce</code>)
3062
+ </li>
3063
+ <li>
3064
+ <strong>Subtotal</strong> = Quantity × Unit Price (using
3065
+ <code>$multiply</code>)
3066
+ </li>
3067
+ <li>
3068
+ <strong>Tax Amount</strong> = Subtotal × Tax Rate ÷ 100 (using
3069
+ <code>$divide</code>)
3070
+ </li>
3071
+ <li>
3072
+ <strong>Grand Total</strong> = Subtotal + Tax - Discount (using
3073
+ <code>$sum</code> and <code>$subtract</code>)
3074
+ </li>
3075
+ </ul>
3076
+ </div>
3077
+ <pds-form
3078
+ .jsonSchema=${schema}
3079
+ .uiSchema=${uiSchema}
3080
+ @pw:submit=${(e) => toastFormData(e.detail)}
3081
+ ></pds-form>
3082
+ `;
3083
+ },
3084
+ };
3085
+
3086
+ export const CalculateWithOverride = {
3087
+ parameters: {
3088
+ docs: {
3089
+ description: {
3090
+ story: `Use \`ui:calculateOverride: true\` to allow users to edit calculated values.
3091
+
3092
+ The field starts with a computed value but the user can modify it. This is useful for:
3093
+ - Suggested values that can be customized
3094
+ - Default calculations that may need manual adjustment
3095
+ - "Smart defaults" that users can override
3096
+
3097
+ \`\`\`javascript
3098
+ '/suggestedPrice': {
3099
+ 'ui:calculate': { '$multiply': ['/baseCost', 1.25] },
3100
+ 'ui:calculateOverride': true // User can edit
3101
+ }
3102
+ \`\`\``,
3103
+ },
3104
+ },
3105
+ },
3106
+ render: () => {
3107
+ const schema = {
3108
+ type: "object",
3109
+ title: "Product Pricing",
3110
+ properties: {
3111
+ productName: {
3112
+ type: "string",
3113
+ title: "Product Name",
3114
+ examples: ["Widget Pro"],
3115
+ },
3116
+ baseCost: { type: "number", title: "Base Cost ($)", default: 100 },
3117
+ suggestedPrice: { type: "number", title: "Suggested Price (editable)" },
3118
+ finalPrice: { type: "number", title: "Final Price (read-only)" },
3119
+ profit: { type: "number", title: "Profit Margin" },
3120
+ },
3121
+ };
3122
+
3123
+ const uiSchema = {
3124
+ // Suggested price: calculated but editable
3125
+ "/suggestedPrice": {
3126
+ "ui:calculate": { $multiply: ["/baseCost", 1.5] },
3127
+ "ui:calculateOverride": true,
3128
+ "ui:help": "💡 Calculated as 1.5× base cost, but you can adjust it",
3129
+ },
3130
+
3131
+ // Final price: read-only calculation
3132
+ "/finalPrice": {
3133
+ "ui:calculate": {
3134
+ $coalesce: ["/suggestedPrice", { $multiply: ["/baseCost", 1.5] }],
3135
+ },
3136
+ "ui:help": "🔒 Read-only: uses your suggested price",
3137
+ },
3138
+
3139
+ // Profit: final - base
3140
+ "/profit": {
3141
+ "ui:calculate": { $subtract: ["/finalPrice", "/baseCost"] },
3142
+ },
3143
+ };
3144
+
3145
+ return html`
3146
+ <div class="alert alert-info">
3147
+ <p><strong>Compare editable vs read-only calculations:</strong></p>
3148
+ <ul>
3149
+ <li>
3150
+ <strong>Suggested Price</strong> starts at 1.5× base cost but
3151
+ <em>you can edit it</em>
3152
+ </li>
3153
+ <li>
3154
+ <strong>Final Price</strong> is read-only and reflects your
3155
+ suggested price
3156
+ </li>
3157
+ <li>
3158
+ <strong>Profit Margin</strong> updates based on final price - base
3159
+ cost
3160
+ </li>
3161
+ </ul>
3162
+ <p>
3163
+ Try changing the base cost, then manually adjusting the suggested
3164
+ price!
3165
+ </p>
3166
+ </div>
3167
+ <pds-form
3168
+ .jsonSchema=${schema}
3169
+ .uiSchema=${uiSchema}
3170
+ @pw:submit=${(e) => toastFormData(e.detail)}
3171
+ ></pds-form>
3172
+ `;
3173
+ },
3174
+ };
3175
+
3176
+ export const ConditionalWithIf = {
3177
+ parameters: {
3178
+ docs: {
3179
+ description: {
3180
+ story: `Use \`$if\` for conditional calculations based on other field values.
3181
+
3182
+ \`\`\`javascript
3183
+ '/shippingCost': {
3184
+ 'ui:calculate': {
3185
+ '$if': {
3186
+ 'cond': { '/membership': 'premium' },
3187
+ 'then': 0,
3188
+ 'else': 9.99
3189
+ }
3190
+ }
3191
+ }
3192
+ \`\`\`
3193
+
3194
+ You can nest \`$if\` expressions for multiple conditions.`,
3195
+ },
3196
+ },
3197
+ },
3198
+ render: () => {
3199
+ const schema = {
3200
+ type: "object",
3201
+ title: "Membership Pricing",
3202
+ properties: {
3203
+ membership: {
3204
+ type: "string",
3205
+ title: "Membership Level",
3206
+ oneOf: [
3207
+ { const: "basic", title: "Basic" },
3208
+ { const: "standard", title: "Standard" },
3209
+ { const: "premium", title: "Premium" },
3210
+ ],
3211
+ default: "basic",
3212
+ },
3213
+ orderAmount: { type: "number", title: "Order Amount ($)", default: 75 },
3214
+ discountPercent: { type: "number", title: "Discount (%)" },
3215
+ discountAmount: { type: "number", title: "Discount Amount ($)" },
3216
+ shippingCost: { type: "number", title: "Shipping Cost ($)" },
3217
+ finalTotal: { type: "number", title: "Final Total ($)" },
3218
+ },
3219
+ };
3220
+
3221
+ const uiSchema = {
3222
+ // Discount percent based on membership: basic=0%, standard=10%, premium=20%
3223
+ "/discountPercent": {
3224
+ "ui:calculate": {
3225
+ $if: {
3226
+ cond: { "/membership": "premium" },
3227
+ then: 20,
3228
+ else: {
3229
+ $if: {
3230
+ cond: { "/membership": "standard" },
3231
+ then: 10,
3232
+ else: 0,
3233
+ },
3234
+ },
3235
+ },
3236
+ },
3237
+ },
3238
+
3239
+ // Discount amount
3240
+ "/discountAmount": {
3241
+ "ui:calculate": {
3242
+ $divide: [{ $multiply: ["/orderAmount", "/discountPercent"] }, 100],
3243
+ },
3244
+ },
3245
+
3246
+ // Shipping: free for premium, $5.99 for standard, $9.99 for basic
3247
+ "/shippingCost": {
3248
+ "ui:calculate": {
3249
+ $if: {
3250
+ cond: { "/membership": "premium" },
3251
+ then: 0,
3252
+ else: {
3253
+ $if: {
3254
+ cond: { "/membership": "standard" },
3255
+ then: 5.99,
3256
+ else: 9.99,
3257
+ },
3258
+ },
3259
+ },
3260
+ },
3261
+ },
3262
+
3263
+ // Final total
3264
+ "/finalTotal": {
3265
+ "ui:calculate": {
3266
+ $sum: [
3267
+ { $subtract: ["/orderAmount", "/discountAmount"] },
3268
+ "/shippingCost",
3269
+ ],
3270
+ },
3271
+ },
3272
+ };
3273
+
3274
+ return html`
3275
+ <div class="alert alert-info">
3276
+ <p><strong>Pricing varies by membership level:</strong></p>
3277
+ <table style="width: 100%; text-align: left;">
3278
+ <thead>
3279
+ <tr>
3280
+ <th>Level</th>
3281
+ <th>Discount</th>
3282
+ <th>Shipping</th>
3283
+ </tr>
3284
+ </thead>
3285
+ <tbody>
3286
+ <tr>
3287
+ <td>Basic</td>
3288
+ <td>0%</td>
3289
+ <td>$9.99</td>
3290
+ </tr>
3291
+ <tr>
3292
+ <td>Standard</td>
3293
+ <td>10%</td>
3294
+ <td>$5.99</td>
3295
+ </tr>
3296
+ <tr>
3297
+ <td>Premium</td>
3298
+ <td>20%</td>
3299
+ <td>FREE</td>
3300
+ </tr>
3301
+ </tbody>
3302
+ </table>
3303
+ <p>
3304
+ Change the <strong>Membership Level</strong> to see all values update!
3305
+ </p>
3306
+ </div>
3307
+ <pds-form
3308
+ .jsonSchema=${schema}
3309
+ .uiSchema=${uiSchema}
3310
+ @pw:submit=${(e) => toastFormData(e.detail)}
3311
+ ></pds-form>
3312
+ `;
3313
+ },
3314
+ };
3315
+
3316
+ export const ConditionalWithRegex = {
3317
+ parameters: {
3318
+ docs: {
3319
+ description: {
3320
+ story: `Use \`$regex\` to match patterns in field values.
3321
+
3322
+ \`\`\`javascript
3323
+ '/warning': {
3324
+ 'ui:visibleWhen': {
3325
+ '/email': { '$regex': '@(gmail|yahoo|hotmail)\\\\.com$' }
3326
+ }
3327
+ }
3328
+ \`\`\`
3329
+
3330
+ This is useful for:
3331
+ - Validating formats (emails, phone numbers, URLs)
3332
+ - Showing warnings for specific patterns
3333
+ - Enabling features based on input format`,
3334
+ },
3335
+ },
3336
+ },
3337
+ render: () => {
3338
+ const schema = {
3339
+ type: "object",
3340
+ title: "Email Verification",
3341
+ properties: {
3342
+ email: {
3343
+ type: "string",
3344
+ format: "email",
3345
+ title: "Email Address",
3346
+ examples: ["you@company.com"],
3347
+ },
3348
+ personalEmailWarning: {
3349
+ type: "string",
3350
+ title: "Personal Email Notice",
3351
+ default:
3352
+ "⚠️ Personal email detected. Consider using a work email for business accounts.",
3353
+ },
3354
+ workEmailConfirmation: {
3355
+ type: "string",
3356
+ title: "Work Email Confirmed",
3357
+ default:
3358
+ "✅ Work email detected. You qualify for enterprise features.",
3359
+ },
3360
+ websiteUrl: {
3361
+ type: "string",
3362
+ title: "Website URL",
3363
+ examples: ["https://example.com"],
3364
+ },
3365
+ secureUrlBadge: {
3366
+ type: "string",
3367
+ title: "Security Status",
3368
+ default: "🔒 Secure connection (HTTPS)",
3369
+ },
3370
+ insecureUrlWarning: {
3371
+ type: "string",
3372
+ title: "Security Warning",
3373
+ default: "⚠️ Insecure connection. Consider using HTTPS.",
3374
+ },
3375
+ },
3376
+ };
3377
+
3378
+ const uiSchema = {
3379
+ // Show warning for personal email providers
3380
+ "/personalEmailWarning": {
3381
+ "ui:visibleWhen": {
3382
+ "/email": {
3383
+ $regex: "@(gmail|yahoo|hotmail|outlook|aol)\\.(com|net|org)$",
3384
+ },
3385
+ },
3386
+ "ui:widget": "const",
3387
+ },
3388
+
3389
+ // Show confirmation for non-personal emails (work domains)
3390
+ "/workEmailConfirmation": {
3391
+ "ui:visibleWhen": {
3392
+ $and: [
3393
+ { "/email": { $regex: "@.+\\..+$" } }, // Has @ and domain
3394
+ {
3395
+ "/email": {
3396
+ $regex: "^(?!.*@(gmail|yahoo|hotmail|outlook|aol)\\.)",
3397
+ },
3398
+ }, // NOT personal
3399
+ ],
3400
+ },
3401
+ "ui:widget": "const",
3402
+ },
3403
+
3404
+ // Show secure badge for HTTPS URLs
3405
+ "/secureUrlBadge": {
3406
+ "ui:visibleWhen": { "/websiteUrl": { $regex: "^https://" } },
3407
+ "ui:widget": "const",
3408
+ },
3409
+
3410
+ // Show warning for HTTP URLs
3411
+ "/insecureUrlWarning": {
3412
+ "ui:visibleWhen": { "/websiteUrl": { $regex: "^http://[^s]" } },
3413
+ "ui:widget": "const",
3414
+ },
3415
+ };
3416
+
3417
+ return html`
3418
+ <div class="alert alert-info">
3419
+ <p>
3420
+ <strong>Pattern matching with <code>$regex</code>:</strong>
3421
+ </p>
3422
+ <ul>
3423
+ <li>
3424
+ Type a <strong>personal email</strong> (gmail, yahoo, etc.) →
3425
+ Warning appears
3426
+ </li>
3427
+ <li>
3428
+ Type a <strong>work email</strong> (company.com) → Confirmation
3429
+ appears
3430
+ </li>
3431
+ <li>Enter an <strong>https://</strong> URL → Secure badge shown</li>
3432
+ <li>
3433
+ Enter an <strong>http://</strong> URL → Insecure warning shown
3434
+ </li>
3435
+ </ul>
3436
+ </div>
3437
+ <pds-form
3438
+ .jsonSchema=${schema}
3439
+ .uiSchema=${uiSchema}
3440
+ @pw:submit=${(e) => toastFormData(e.detail)}
3441
+ ></pds-form>
3442
+ `;
3443
+ },
3444
+ };
3445
+
3446
+ export const ConditionalWithLogicalOperators = {
3447
+ parameters: {
3448
+ docs: {
3449
+ description: {
3450
+ story: `Combine conditions with \`$and\`, \`$or\`, and \`$not\` for complex logic.
3451
+
3452
+ \`\`\`javascript
3453
+ // AND: All conditions must be true
3454
+ 'ui:visibleWhen': {
3455
+ '$and': [
3456
+ { '/age': { '$gte': 18 } },
3457
+ { '/country': 'US' }
3458
+ ]
3459
+ }
3460
+
3461
+ // OR: Any condition can be true
3462
+ 'ui:visibleWhen': {
3463
+ '$or': [
3464
+ { '/role': 'admin' },
3465
+ { '/role': 'moderator' }
3466
+ ]
3467
+ }
3468
+
3469
+ // NOT: Invert a condition
3470
+ 'ui:visibleWhen': {
3471
+ '$not': { '/status': 'banned' }
3472
+ }
3473
+ \`\`\``,
3474
+ },
3475
+ },
3476
+ },
3477
+ render: () => {
3478
+ const schema = {
3479
+ type: "object",
3480
+ title: "Feature Access Control",
3481
+ properties: {
3482
+ userRole: {
3483
+ type: "string",
3484
+ title: "User Role",
3485
+ oneOf: [
3486
+ { const: "guest", title: "Guest" },
3487
+ { const: "member", title: "Member" },
3488
+ { const: "moderator", title: "Moderator" },
3489
+ { const: "admin", title: "Admin" },
3490
+ ],
3491
+ default: "guest",
3492
+ },
3493
+ accountStatus: {
3494
+ type: "string",
3495
+ title: "Account Status",
3496
+ oneOf: [
3497
+ { const: "pending", title: "Pending Verification" },
3498
+ { const: "active", title: "Active" },
3499
+ { const: "suspended", title: "Suspended" },
3500
+ ],
3501
+ default: "active",
3502
+ },
3503
+ premiumMember: {
3504
+ type: "boolean",
3505
+ title: "Premium Membership",
3506
+ default: false,
3507
+ },
3508
+ age: {
3509
+ type: "integer",
3510
+ title: "Age",
3511
+ minimum: 13,
3512
+ default: 25,
3513
+ },
3514
+ // Feature flags (shown/hidden based on conditions)
3515
+ basicFeatures: {
3516
+ type: "string",
3517
+ title: "Basic Features",
3518
+ default: "✅ You have access to basic features",
3519
+ },
3520
+ premiumFeatures: {
3521
+ type: "string",
3522
+ title: "Premium Features",
3523
+ default: "⭐ Premium features unlocked!",
3524
+ },
3525
+ moderatorTools: {
3526
+ type: "string",
3527
+ title: "Moderator Tools",
3528
+ default: "🛡️ Moderator tools available",
3529
+ },
3530
+ adminPanel: {
3531
+ type: "string",
3532
+ title: "Admin Panel",
3533
+ default: "👑 Full admin access granted",
3534
+ },
3535
+ ageRestrictedContent: {
3536
+ type: "string",
3537
+ title: "Age-Restricted Content",
3538
+ default: "🔞 Adult content unlocked",
3539
+ },
3540
+ suspendedNotice: {
3541
+ type: "string",
3542
+ title: "Account Suspended",
3543
+ default: "🚫 Your account is suspended. Contact support.",
3544
+ },
3545
+ },
3546
+ };
3547
+
3548
+ const uiSchema = {
3549
+ "ui:layout": "grid",
3550
+ "ui:layoutOptions": { columns: 2, gap: "md" },
3551
+
3552
+ // Basic features: available to active non-guest users
3553
+ // $and + $not combined
3554
+ "/basicFeatures": {
3555
+ "ui:visibleWhen": {
3556
+ $and: [
3557
+ { $not: { "/userRole": "guest" } },
3558
+ { "/accountStatus": "active" },
3559
+ ],
3560
+ },
3561
+ "ui:widget": "const",
3562
+ },
3563
+
3564
+ // Premium features: premium member OR admin (admins get everything)
3565
+ // $or operator
3566
+ "/premiumFeatures": {
3567
+ "ui:visibleWhen": {
3568
+ $and: [
3569
+ { "/accountStatus": "active" },
3570
+ {
3571
+ $or: [{ "/premiumMember": true }, { "/userRole": "admin" }],
3572
+ },
3573
+ ],
3574
+ },
3575
+ "ui:widget": "const",
3576
+ },
3577
+
3578
+ // Moderator tools: moderator OR admin
3579
+ "/moderatorTools": {
3580
+ "ui:visibleWhen": {
3581
+ $and: [
3582
+ { "/accountStatus": "active" },
3583
+ {
3584
+ $or: [{ "/userRole": "moderator" }, { "/userRole": "admin" }],
3585
+ },
3586
+ ],
3587
+ },
3588
+ "ui:widget": "const",
3589
+ },
3590
+
3591
+ // Admin panel: admin only
3592
+ "/adminPanel": {
3593
+ "ui:visibleWhen": {
3594
+ $and: [{ "/userRole": "admin" }, { "/accountStatus": "active" }],
3595
+ },
3596
+ "ui:widget": "const",
3597
+ },
3598
+
3599
+ // Age-restricted: 18+ and active
3600
+ "/ageRestrictedContent": {
3601
+ "ui:visibleWhen": {
3602
+ $and: [
3603
+ { "/age": { $gte: 18 } },
3604
+ { "/accountStatus": "active" },
3605
+ { $not: { "/userRole": "guest" } },
3606
+ ],
3607
+ },
3608
+ "ui:widget": "const",
3609
+ },
3610
+
3611
+ // Suspended notice: shown when suspended
3612
+ "/suspendedNotice": {
3613
+ "ui:visibleWhen": { "/accountStatus": "suspended" },
3614
+ "ui:widget": "const",
3615
+ },
3616
+ };
3617
+
3618
+ return html`
3619
+ <div class="alert alert-info">
3620
+ <p><strong>Combine conditions with logical operators:</strong></p>
3621
+ <ul>
3622
+ <li><strong>$and</strong>: All conditions must be true</li>
3623
+ <li><strong>$or</strong>: Any condition can be true</li>
3624
+ <li><strong>$not</strong>: Inverts a condition</li>
3625
+ </ul>
3626
+ <p>
3627
+ Try different combinations of Role, Status, Premium, and Age to see
3628
+ which features appear!
3629
+ </p>
3630
+ </div>
3631
+ <pds-form
3632
+ .jsonSchema=${schema}
3633
+ .uiSchema=${uiSchema}
3634
+ @pw:submit=${(e) => toastFormData(e.detail)}
3635
+ ></pds-form>
3636
+ `;
3637
+ },
3638
+ };
3639
+
3640
+ export const ConditionalWithComparison = {
3641
+ parameters: {
3642
+ docs: {
3643
+ description: {
3644
+ story: `Use comparison operators for numeric and value-based conditions.
3645
+
3646
+ | Operator | Description | Example |
3647
+ |----------|-------------|---------|
3648
+ | \`$eq\` | Equals | \`{ "/status": { "$eq": "active" } }\` |
3649
+ | \`$ne\` | Not equals | \`{ "/role": { "$ne": "guest" } }\` |
3650
+ | \`$gt\` | Greater than | \`{ "/age": { "$gt": 18 } }\` |
3651
+ | \`$gte\` | Greater or equal | \`{ "/score": { "$gte": 80 } }\` |
3652
+ | \`$lt\` | Less than | \`{ "/qty": { "$lt": 10 } }\` |
3653
+ | \`$lte\` | Less or equal | \`{ "/price": { "$lte": 100 } }\` |
3654
+ | \`$in\` | In array | \`{ "/tier": { "$in": ["gold", "platinum"] } }\` |
3655
+ | \`$nin\` | Not in array | \`{ "/country": { "$nin": ["XX", "YY"] } }\` |
3656
+ | \`$exists\` | Has value | \`{ "/email": { "$exists": true } }\` |`,
3657
+ },
3658
+ },
3659
+ },
3660
+ render: () => {
3661
+ const schema = {
3662
+ type: "object",
3663
+ title: "Order Validation",
3664
+ properties: {
3665
+ quantity: {
3666
+ type: "integer",
3667
+ title: "Quantity",
3668
+ minimum: 1,
3669
+ default: 5,
3670
+ },
3671
+ lowStockWarning: {
3672
+ type: "string",
3673
+ title: "Low Stock",
3674
+ default: "⚠️ Low quantity - ships within 24h",
3675
+ },
3676
+ bulkOrderNotice: {
3677
+ type: "string",
3678
+ title: "Bulk Order",
3679
+ default: "📦 Bulk order! Contact sales for discount.",
3680
+ },
3681
+ outOfStockError: {
3682
+ type: "string",
3683
+ title: "Out of Stock",
3684
+ default: "❌ Quantity too high - only 100 in stock",
3685
+ },
3686
+
3687
+ country: {
3688
+ type: "string",
3689
+ title: "Shipping Country",
3690
+ oneOf: [
3691
+ { const: "US", title: "United States" },
3692
+ { const: "CA", title: "Canada" },
3693
+ { const: "UK", title: "United Kingdom" },
3694
+ { const: "DE", title: "Germany" },
3695
+ { const: "FR", title: "France" },
3696
+ { const: "AU", title: "Australia" },
3697
+ { const: "JP", title: "Japan" },
3698
+ { const: "OTHER", title: "Other" },
3699
+ ],
3700
+ default: "US",
3701
+ },
3702
+ domesticShipping: {
3703
+ type: "string",
3704
+ title: "Domestic Shipping",
3705
+ default: "🚚 Free domestic shipping (US/CA)",
3706
+ },
3707
+ euShipping: {
3708
+ type: "string",
3709
+ title: "EU Shipping",
3710
+ default: "🇪🇺 EU shipping available - 5-7 days",
3711
+ },
3712
+ internationalShipping: {
3713
+ type: "string",
3714
+ title: "International",
3715
+ default: "✈️ International shipping - 10-14 days",
3716
+ },
3717
+ restrictedCountry: {
3718
+ type: "string",
3719
+ title: "Restricted",
3720
+ default: "🚫 Sorry, we cannot ship to this location",
3721
+ },
3722
+
3723
+ couponCode: {
3724
+ type: "string",
3725
+ title: "Coupon Code (optional)",
3726
+ examples: ["SAVE20"],
3727
+ },
3728
+ couponApplied: {
3729
+ type: "string",
3730
+ title: "Coupon Status",
3731
+ default: "🎟️ Coupon code detected! Will be validated at checkout.",
3732
+ },
3733
+ },
3734
+ };
3735
+
3736
+ const uiSchema = {
3737
+ // $lt: Low quantity warning (1-3)
3738
+ "/lowStockWarning": {
3739
+ "ui:visibleWhen": { "/quantity": { $lte: 3 } },
3740
+ "ui:widget": "const",
3741
+ },
3742
+
3743
+ // $gte: Bulk order notice (50+)
3744
+ "/bulkOrderNotice": {
3745
+ "ui:visibleWhen": { "/quantity": { $gte: 50 } },
3746
+ "ui:widget": "const",
3747
+ },
3748
+
3749
+ // $gt: Out of stock (>100)
3750
+ "/outOfStockError": {
3751
+ "ui:visibleWhen": { "/quantity": { $gt: 100 } },
3752
+ "ui:widget": "const",
3753
+ },
3754
+
3755
+ // $in: Domestic shipping (US, CA)
3756
+ "/domesticShipping": {
3757
+ "ui:visibleWhen": { "/country": { $in: ["US", "CA"] } },
3758
+ "ui:widget": "const",
3759
+ },
3760
+
3761
+ // $in: EU shipping
3762
+ "/euShipping": {
3763
+ "ui:visibleWhen": { "/country": { $in: ["UK", "DE", "FR"] } },
3764
+ "ui:widget": "const",
3765
+ },
3766
+
3767
+ // $in: International (AU, JP)
3768
+ "/internationalShipping": {
3769
+ "ui:visibleWhen": { "/country": { $in: ["AU", "JP"] } },
3770
+ "ui:widget": "const",
3771
+ },
3772
+
3773
+ // $eq: Restricted country
3774
+ "/restrictedCountry": {
3775
+ "ui:visibleWhen": { "/country": { $eq: "OTHER" } },
3776
+ "ui:widget": "const",
3777
+ },
3778
+
3779
+ // $exists: Coupon code entered
3780
+ "/couponApplied": {
3781
+ "ui:visibleWhen": { "/couponCode": { $exists: true } },
3782
+ "ui:widget": "const",
3783
+ },
3784
+ };
3785
+
3786
+ return html`
3787
+ <div class="alert alert-info">
3788
+ <p><strong>Comparison operators in action:</strong></p>
3789
+ <ul>
3790
+ <li>
3791
+ Set <strong>Quantity</strong> to 1-3 → Low stock warning
3792
+ (<code>$lte</code>)
3793
+ </li>
3794
+ <li>
3795
+ Set <strong>Quantity</strong> to 50+ → Bulk order notice
3796
+ (<code>$gte</code>)
3797
+ </li>
3798
+ <li>
3799
+ Set <strong>Quantity</strong> over 100 → Out of stock error
3800
+ (<code>$gt</code>)
3801
+ </li>
3802
+ <li>
3803
+ Select <strong>Country</strong> → Different shipping messages
3804
+ (<code>$in</code>)
3805
+ </li>
3806
+ <li>
3807
+ Enter a <strong>Coupon Code</strong> → Confirmation appears
3808
+ (<code>$exists</code>)
3809
+ </li>
3810
+ </ul>
3811
+ </div>
3812
+ <pds-form
3813
+ .jsonSchema=${schema}
3814
+ .uiSchema=${uiSchema}
3815
+ @pw:submit=${(e) => toastFormData(e.detail)}
3816
+ ></pds-form>
3817
+ `;
3818
+ },
3819
+ };
3820
+
3821
+ export const ConditionalDisabledFields = {
3822
+ parameters: {
3823
+ docs: {
3824
+ description: {
3825
+ story: `Use \`ui:disabledWhen\` to disable fields based on conditions.
3826
+
3827
+ Disabled fields remain visible but cannot be edited. Use this for:
3828
+ - Mutually exclusive options
3829
+ - Fields that become irrelevant based on other choices
3830
+ - Read-only states based on form context
3831
+
3832
+ \`\`\`javascript
3833
+ '/billingAddress': {
3834
+ 'ui:disabledWhen': { '/sameAsShipping': true }
3835
+ }
3836
+ \`\`\``,
3837
+ },
3838
+ },
3839
+ },
3840
+ render: () => {
3841
+ const schema = {
3842
+ type: "object",
3843
+ title: "Checkout Form",
3844
+ properties: {
3845
+ shippingAddress: {
3846
+ type: "string",
3847
+ title: "Shipping Address",
3848
+ examples: ["123 Main St, City, ST 12345"],
3849
+ },
3850
+ sameAsShipping: {
3851
+ type: "boolean",
3852
+ title: "Billing address same as shipping",
3853
+ default: true,
3854
+ },
3855
+ billingAddress: {
3856
+ type: "string",
3857
+ title: "Billing Address",
3858
+ examples: ["456 Oak Ave, Town, ST 67890"],
3859
+ },
3860
+
3861
+ paymentMethod: {
3862
+ type: "string",
3863
+ title: "Payment Method",
3864
+ oneOf: [
3865
+ { const: "credit", title: "Credit Card" },
3866
+ { const: "debit", title: "Debit Card" },
3867
+ { const: "paypal", title: "PayPal" },
3868
+ { const: "crypto", title: "Cryptocurrency" },
3869
+ ],
3870
+ default: "credit",
3871
+ },
3872
+ cardNumber: {
3873
+ type: "string",
3874
+ title: "Card Number",
3875
+ examples: ["4111 1111 1111 1111"],
3876
+ },
3877
+ cardExpiry: {
3878
+ type: "string",
3879
+ title: "Expiry (MM/YY)",
3880
+ examples: ["12/25"],
3881
+ },
3882
+ cardCvv: { type: "string", title: "CVV", examples: ["123"] },
3883
+ paypalEmail: {
3884
+ type: "string",
3885
+ format: "email",
3886
+ title: "PayPal Email",
3887
+ examples: ["you@paypal.com"],
3888
+ },
3889
+ cryptoWallet: {
3890
+ type: "string",
3891
+ title: "Wallet Address",
3892
+ examples: ["0x1234..."],
3893
+ },
3894
+
3895
+ orderLocked: {
3896
+ type: "boolean",
3897
+ title: "🔒 Lock order (prevent changes)",
3898
+ default: false,
3899
+ },
3900
+ notes: {
3901
+ type: "string",
3902
+ title: "Order Notes",
3903
+ examples: ["Special instructions..."],
3904
+ },
3905
+ },
3906
+ };
3907
+
3908
+ const uiSchema = {
3909
+ // Billing address disabled when "same as shipping" is checked
3910
+ "/billingAddress": {
3911
+ "ui:disabledWhen": { "/sameAsShipping": true },
3912
+ "ui:help": 'Uncheck "same as shipping" to edit',
3913
+ },
3914
+
3915
+ // Card fields disabled when not using card payment
3916
+ "/cardNumber": {
3917
+ "ui:disabledWhen": { "/paymentMethod": { $nin: ["credit", "debit"] } },
3918
+ },
3919
+ "/cardExpiry": {
3920
+ "ui:disabledWhen": { "/paymentMethod": { $nin: ["credit", "debit"] } },
3921
+ },
3922
+ "/cardCvv": {
3923
+ "ui:disabledWhen": { "/paymentMethod": { $nin: ["credit", "debit"] } },
3924
+ },
3925
+
3926
+ // PayPal email disabled when not using PayPal
3927
+ "/paypalEmail": {
3928
+ "ui:disabledWhen": { "/paymentMethod": { $ne: "paypal" } },
3929
+ },
3930
+
3931
+ // Crypto wallet disabled when not using crypto
3932
+ "/cryptoWallet": {
3933
+ "ui:disabledWhen": { "/paymentMethod": { $ne: "crypto" } },
3934
+ },
3935
+
3936
+ // Notes disabled when order is locked
3937
+ "/notes": {
3938
+ "ui:disabledWhen": { "/orderLocked": true },
3939
+ "ui:widget": "textarea",
3940
+ "ui:help": "Lock order to prevent changes",
3941
+ },
3942
+ };
3943
+
3944
+ return html`
3945
+ <div class="alert alert-info">
3946
+ <p><strong>Fields become disabled based on conditions:</strong></p>
3947
+ <ul>
3948
+ <li>
3949
+ Check <strong>"Same as shipping"</strong> → Billing address disabled
3950
+ </li>
3951
+ <li>
3952
+ Change <strong>Payment Method</strong> → Only relevant fields stay
3953
+ enabled
3954
+ </li>
3955
+ <li>Check <strong>"Lock order"</strong> → Notes field disabled</li>
3956
+ </ul>
3957
+ <p>Disabled fields show their value but prevent editing.</p>
3958
+ </div>
3959
+ <pds-form
3960
+ .jsonSchema=${schema}
3961
+ .uiSchema=${uiSchema}
3962
+ @pw:submit=${(e) => toastFormData(e.detail)}
3963
+ ></pds-form>
3964
+ `;
3965
+ },
3966
+ };
3967
+
3968
+ // ============================================
3969
+ // Custom Content Injection Stories
3970
+ // ============================================
3971
+
3972
+ export const CustomContentBeforeAfter = {
3973
+ parameters: {
3974
+ docs: {
3975
+ description: {
3976
+ story: `Use \`ui:before\` and \`ui:after\` to inject custom content around fields or fieldsets.
3977
+
3978
+ ### Value Types
3979
+ - **Function**: \`(field) => html\\\`...\\\`\` - full access to render context
3980
+ - **Slot reference**: \`"slot:mySlot"\` - renders a slotted element by name
3981
+
3982
+ ### Field Context
3983
+ The function receives a \`field\` object with: \`{ path, schema, value, label, id, get, set, attrs, host }\``,
3984
+ },
3985
+ },
3986
+ },
3987
+ render: () => {
3988
+ const schema = {
3989
+ type: "object",
3990
+ title: "User Registration",
3991
+ properties: {
3992
+ username: { type: "string", title: "Username", minLength: 3 },
3993
+ email: { type: "string", title: "Email", format: "email" },
3994
+ password: { type: "string", title: "Password", minLength: 8 },
3995
+ confirmPassword: { type: "string", title: "Confirm Password" },
3996
+ acceptTerms: { type: "boolean", title: "I accept the terms" },
3997
+ },
3998
+ };
3999
+
4000
+ const uiSchema = {
4001
+ // Add a header before the username field
4002
+ "/username": {
4003
+ "ui:before": (field) => html`
4004
+ <div class="alert alert-info">
4005
+ <span class="alert-icon">
4006
+ <pds-icon icon="info" size="md"> </pds-icon>
4007
+ </span>
4008
+ <div>
4009
+ <h4 class="alert-title">Account Info</h4>
4010
+ <p>Choose a unique username and secure password.</p>
4011
+ </div>
4012
+ </div>
4013
+ `,
4014
+ },
4015
+
4016
+ // Add validation hint after email
4017
+ "/email": {
4018
+ "ui:after": (field) =>
4019
+ field.value && !field.value.includes("@")
4020
+ ? html`<div class="text-sm text-danger">
4021
+ Please enter a valid email address
4022
+ </div>`
4023
+ : nothing,
4024
+ },
4025
+
4026
+ // Add password strength indicator after password
4027
+ "/password": {
4028
+ "ui:widget": "password",
4029
+ "ui:after": (field) => {
4030
+ if (!field.value) return nothing;
4031
+ const strength =
4032
+ field.value.length < 8
4033
+ ? "Weak"
4034
+ : field.value.length < 12
4035
+ ? "Medium"
4036
+ : "Strong";
4037
+ const color =
4038
+ strength === "Weak"
4039
+ ? "danger"
4040
+ : strength === "Medium"
4041
+ ? "warning"
4042
+ : "success";
4043
+ return html`<div class="text-sm text-${color}">
4044
+ Password strength: ${strength}
4045
+ </div>`;
4046
+ },
4047
+ },
4048
+
4049
+ // Add legal notice before terms checkbox
4050
+ "/acceptTerms": {
4051
+ "ui:before": (field) => html`
4052
+ <hr style="margin: var(--spacing-md) 0;" />
4053
+ <p class="text-sm text-muted">
4054
+ By registering, you agree to our
4055
+ <a href="#terms">Terms of Service</a> and
4056
+ <a href="#privacy">Privacy Policy</a>.
4057
+ </p>
4058
+ `,
4059
+ },
4060
+ };
4061
+
4062
+ return html`
4063
+ <pds-form
4064
+ .jsonSchema=${schema}
4065
+ .uiSchema=${uiSchema}
4066
+ @pw:submit=${(e) => toastFormData(e.detail)}
4067
+ ></pds-form>
4068
+ `;
4069
+ },
4070
+ };
4071
+
4072
+ export const CustomContentRender = {
4073
+ parameters: {
4074
+ docs: {
4075
+ description: {
4076
+ story: `Use \`ui:render\` to completely replace a field's rendering with your own template.
4077
+
4078
+ The render function receives a \`field\` object with: \`{ id, path, label, value, schema, ui, attrs, get, set, host }\`
4079
+
4080
+ This is like using \`defineRenderer()\` but inline in the uiSchema.`,
4081
+ },
4082
+ },
4083
+ },
4084
+ render: () => {
4085
+ const schema = {
4086
+ type: "object",
4087
+ title: "Product Review",
4088
+ properties: {
4089
+ productName: {
4090
+ type: "string",
4091
+ title: "Product",
4092
+ default: "Wireless Headphones",
4093
+ },
4094
+ rating: {
4095
+ type: "integer",
4096
+ title: "Rating",
4097
+ minimum: 1,
4098
+ maximum: 5,
4099
+ default: 4,
4100
+ },
4101
+ review: { type: "string", title: "Review" },
4102
+ recommend: { type: "boolean", title: "Would recommend", default: true },
4103
+ },
4104
+ };
4105
+
4106
+ const uiSchema = {
4107
+ // Custom star rating widget
4108
+ "/rating": {
4109
+ "ui:render": (field) => {
4110
+ const stars = [1, 2, 3, 4, 5];
4111
+ return html`
4112
+ <fieldset data-path=${field.path}>
4113
+ <legend>${field.label}</legend>
4114
+ <div
4115
+ class="flex gap-xs"
4116
+ role="radiogroup"
4117
+ aria-label="${field.label}"
4118
+ >
4119
+ ${stars.map(
4120
+ (star) => html`
4121
+ <button
4122
+ type="button"
4123
+ class="btn btn-sm ${star <= (field.value || 0)
4124
+ ? "btn-primary"
4125
+ : "btn-outline"}"
4126
+ @click=${() => field.set(star)}
4127
+ aria-label="${star} star${star > 1 ? "s" : ""}"
4128
+ aria-pressed=${star <= (field.value || 0)}
4129
+ >
4130
+
4131
+ </button>
4132
+ `
4133
+ )}
4134
+ </div>
4135
+ <input
4136
+ type="hidden"
4137
+ name=${field.path}
4138
+ .value=${String(field.value || "")}
4139
+ />
4140
+ </fieldset>
4141
+ `;
4142
+ },
4143
+ },
4144
+
4145
+ // Custom toggle card for recommendation
4146
+ "/recommend": {
4147
+ "ui:render": (field) => html`
4148
+ <div
4149
+ class="card surface-elevated p-md cursor-pointer flex items-center gap-md"
4150
+ @click=${() => field.set(!field.value)}
4151
+ role="checkbox"
4152
+ aria-checked=${!!field.value}
4153
+ tabindex="0"
4154
+ @keydown=${(e) =>
4155
+ e.key === " " && (e.preventDefault(), field.set(!field.value))}
4156
+ >
4157
+ <span
4158
+ style="cursor: pointer; font-size: 3rem; color: var(--color-${field.value
4159
+ ? "success"
4160
+ : "neutral"}-500)"
4161
+ >${field.value ? "👍🏼" : "👎🏼"}</span
4162
+ >
4163
+ <div>
4164
+ <strong>${field.label}</strong>
4165
+ <p class="text-sm text-muted">
4166
+ ${field.value
4167
+ ? "Yes, I would recommend this!"
4168
+ : "No, I would not recommend this"}
4169
+ </p>
4170
+ </div>
4171
+ <input
4172
+ type="hidden"
4173
+ name=${field.path}
4174
+ .value=${String(!!field.value)}
4175
+ />
4176
+ </div>
4177
+ `,
4178
+ },
4179
+
4180
+ "/review": {
4181
+ "ui:widget": "textarea",
4182
+ "ui:help": "Share your experience with this product",
4183
+ },
4184
+ };
4185
+
4186
+ return html`
4187
+ <div class="alert alert-info">
4188
+ <p><strong>Custom rendered fields:</strong></p>
4189
+ <ul>
4190
+ <li><strong>Rating</strong> uses a custom star button widget</li>
4191
+ <li><strong>Would recommend</strong> uses a custom toggle card</li>
4192
+ </ul>
4193
+ </div>
4194
+ <pds-form
4195
+ .jsonSchema=${schema}
4196
+ .uiSchema=${uiSchema}
4197
+ @pw:submit=${(e) => toastFormData(e.detail)}
4198
+ ></pds-form>
4199
+ `;
4200
+ },
4201
+ };
4202
+
4203
+ export const CustomContentWrapper = {
4204
+ parameters: {
4205
+ docs: {
4206
+ description: {
4207
+ story: `Use \`ui:wrapper\` to customize how a field is wrapped (replacing the default \`<label>\` structure).
4208
+
4209
+ A common use case is adding a **live character counter** to textarea fields.
4210
+
4211
+ The wrapper function receives a \`field\` object with: \`{ control, label, help, id, value, schema, ...context }\``,
4212
+ },
4213
+ },
4214
+ },
4215
+ render: () => {
4216
+ const schema = {
4217
+ type: "object",
4218
+ title: "Author Profile",
4219
+ properties: {
4220
+ name: { type: "string", title: "Display Name", maxLength: 50 },
4221
+ bio: { type: "string", title: "Bio", maxLength: 280 },
4222
+ about: { type: "string", title: "About Me", maxLength: 1000 },
4223
+ },
4224
+ };
4225
+
4226
+ // Wrapper with live character counter
4227
+ const charCountWrapper = (field) => {
4228
+ const maxLength = field.schema.maxLength;
4229
+ const currentLength = (field.value || "").length;
4230
+ const remaining = maxLength - currentLength;
4231
+ const isWarning = remaining <= 20 && remaining > 0;
4232
+ const isOver = remaining <= 0;
4233
+
4234
+ return html`
4235
+ <label for=${field.id}>
4236
+ <div class="flex justify-between items-center">
4237
+ <span data-label>${field.label}</span>
4238
+ <span
4239
+ class="text-sm"
4240
+ style="color: var(${isOver
4241
+ ? "--color-danger-600"
4242
+ : isWarning
4243
+ ? "--color-warning-600"
4244
+ : "--color-text-muted"})"
4245
+ >
4246
+ ${currentLength}/${maxLength}
4247
+ </span>
4248
+ </div>
4249
+ ${field.control} ${field.help}
4250
+ </label>
4251
+ `;
4252
+ };
4253
+
4254
+ const uiSchema = {
4255
+ "/name": { "ui:wrapper": charCountWrapper },
4256
+ "/bio": {
4257
+ "ui:widget": "textarea",
4258
+ "ui:wrapper": charCountWrapper,
4259
+ "ui:help": "A short bio for your profile card",
4260
+ },
4261
+ "/about": {
4262
+ "ui:widget": "textarea",
4263
+ "ui:wrapper": charCountWrapper,
4264
+ "ui:help": "Tell readers more about yourself",
4265
+ },
4266
+ };
4267
+
4268
+ return html`
4269
+ <div class="alert alert-info">
4270
+ <p><strong>Live character counter wrapper:</strong></p>
4271
+ <ul>
4272
+ <li>
4273
+ Counter shows <strong class="text-muted">gray</strong> normally
4274
+ </li>
4275
+ <li>
4276
+ Turns <strong class="text-warning">yellow</strong> when ≤20
4277
+ characters remain
4278
+ </li>
4279
+ <li>
4280
+ Turns <strong class="text-danger">red</strong> when over the limit
4281
+ </li>
4282
+ </ul>
4283
+ <p>Try typing in any field to see the counter update!</p>
4284
+ </div>
4285
+ <pds-form
4286
+ .jsonSchema=${schema}
4287
+ .uiSchema=${uiSchema}
4288
+ @pw:submit=${(e) => toastFormData(e.detail)}
4289
+ ></pds-form>
4290
+ `;
4291
+ },
4292
+ };
4293
+
4294
+ export const CustomContentSlots = {
4295
+ parameters: {
4296
+ docs: {
4297
+ description: {
4298
+ story: `Use slot references (\`"slot:name"\`) in \`ui:before\` or \`ui:after\` to inject pre-defined HTML content.
4299
+
4300
+ This is useful when you want to define content in the HTML rather than in JavaScript.`,
4301
+ },
4302
+ },
4303
+ },
4304
+ render: () => {
4305
+ const schema = {
4306
+ type: "object",
4307
+ title: "Newsletter Signup",
4308
+ properties: {
4309
+ email: { type: "string", title: "Email", format: "email" },
4310
+ frequency: {
4311
+ type: "string",
4312
+ title: "Email Frequency",
4313
+ enum: ["daily", "weekly", "monthly"],
4314
+ enumNames: ["Daily digest", "Weekly roundup", "Monthly newsletter"],
4315
+ default: "weekly",
4316
+ },
4317
+ interests: { type: "string", title: "Interests" },
4318
+ },
4319
+ };
4320
+
4321
+ const uiSchema = {
4322
+ "/email": {
4323
+ "ui:before": "slot:email-header",
4324
+ },
4325
+ "/interests": {
4326
+ "ui:after": "slot:interests-footer",
4327
+ },
4328
+ };
4329
+
4330
+ return html`
4331
+ <pds-form
4332
+ .jsonSchema=${schema}
4333
+ .uiSchema=${uiSchema}
4334
+ @pw:submit=${(e) => toastFormData(e.detail)}
4335
+ >
4336
+ <!-- Slotted content referenced by ui:before/ui:after -->
4337
+ <div
4338
+ slot="email-header"
4339
+ class="alert alert-success"
4340
+ style="margin-bottom: var(--spacing-sm);"
4341
+ >
4342
+ <strong>Join 50,000+ subscribers!</strong>
4343
+ </div>
4344
+
4345
+ <p
4346
+ slot="interests-footer"
4347
+ class="text-sm text-muted"
4348
+ style="margin-top: var(--spacing-sm);"
4349
+ >
4350
+ We'll personalize your newsletter based on your interests.
4351
+ <a href="#privacy">Learn more about how we use your data</a>.
4352
+ </p>
4353
+ </pds-form>
4354
+ `;
4355
+ },
4356
+ };