@cocoar/vue-ui 3.1.0 → 3.2.0-beta.6

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 (179) hide show
  1. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  3. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  5. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  6. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  7. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  9. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  10. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  11. package/dist/components/data-list/index.d.ts +12 -0
  12. package/dist/components/data-list/index.d.ts.map +1 -0
  13. package/dist/components/data-list/internal/compare.d.ts +14 -0
  14. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  15. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  17. package/dist/components/data-list/internal/search.d.ts +16 -0
  18. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  19. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  21. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  23. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  25. package/dist/components/data-list/types.d.ts +185 -0
  26. package/dist/components/data-list/types.d.ts.map +1 -0
  27. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  28. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  29. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  30. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  31. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  32. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  33. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  34. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  36. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  38. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  40. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  42. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  44. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  45. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  46. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  47. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  48. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  50. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  52. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  54. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  56. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  58. package/dist/composables/dragRegistry.d.ts +12 -0
  59. package/dist/composables/dragRegistry.d.ts.map +1 -1
  60. package/dist/composables/index.d.ts +2 -2
  61. package/dist/composables/index.d.ts.map +1 -1
  62. package/dist/composables/useDragDrop.d.ts +62 -6
  63. package/dist/composables/useDragDrop.d.ts.map +1 -1
  64. package/dist/composables/useVirtualList.d.ts +21 -1
  65. package/dist/composables/useVirtualList.d.ts.map +1 -1
  66. package/dist/index.css +1 -1
  67. package/dist/index.d.ts +4 -2
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4508 -2039
  70. package/package.json +5 -3
  71. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  72. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  73. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  74. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  75. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  76. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +353 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1645 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +475 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  86. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  87. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  89. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  90. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  91. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  99. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  100. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  101. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  102. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  103. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  104. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  105. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  106. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  110. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  111. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  112. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  116. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  117. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  118. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  119. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  120. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  121. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  122. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  127. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  128. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  129. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  130. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  131. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  132. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  133. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  134. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  140. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  141. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  142. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  143. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  144. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  145. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  146. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  147. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  148. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  149. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  150. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  151. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  152. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  153. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  154. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  155. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  156. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  157. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  158. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  159. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  160. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  161. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  162. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  163. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  164. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  165. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  166. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  167. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  171. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  172. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  173. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  174. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  175. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  176. package/skills/cocoar-vue-ui/references/guide/getting-started.md +119 -0
  177. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  179. package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
@@ -0,0 +1,573 @@
1
+ <!-- Generated from apps/docs/components/page-builder/index.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # Page Builder (Preview)
4
+
5
+ > **Warning: Preview**
6
+ >
7
+ > Page Builder is **provisional**. It shipped as GA in 2.17 — that was an
8
+ > oversight, and 3.0 corrects it: the package is back under the Preview badge
9
+ > until the authoring model settles. Public API, `PageConfig` and the document
10
+ > schema may still change in a minor release, and 3.0 itself removed four config
11
+ > concepts and renamed several more.
12
+ >
13
+ > Documents are safe across those changes: every schema change ships a migration
14
+ > that runs on ingest. Pin a version if you depend on the API, and read the
15
+ > [Authoring contract](./authoring-contract.md) for what is still open.
16
+
17
+ `@cocoar/vue-page-builder` is a generic, headless visual page composition framework. Users drag UI primitives onto a canvas, configure them, and the result is a portable JSON schema that a companion renderer turns back into live Cocoar components.
18
+
19
+ Everything domain-specific — what actions a button can trigger, where images come from, which elements are permitted, and even **which element types exist** — is defined by the **consumer application**, not the library. Built-in elements are pre-registered definitions on an open [element registry](./custom-elements.md); consumer apps register their own element types on the same contract.
20
+
21
+ ## Two components
22
+
23
+ | Component | Purpose | Docs |
24
+ |-----------|---------|------|
25
+ | `<CoarPageBuilder>` | Visual editor — 3-panel layout, drag-and-drop, props panel | [→ CoarPageBuilder](./coar-page-builder.md) |
26
+ | `<CoarPageRenderer>` | Runtime renderer — schema → live Cocoar components | [→ CoarPageRenderer](./coar-page-renderer.md) |
27
+
28
+ Both share the same `PageConfig`. The builder uses it as UI affordances; the renderer uses it as the security boundary.
29
+
30
+ ## Quick start
31
+
32
+ Import the stylesheet once (it carries the builder chrome **and** the renderer's
33
+ layout styles — without it, stacks lose their flex layout):
34
+
35
+ ```ts
36
+ import '@cocoar/vue-page-builder/styles';
37
+ ```
38
+
39
+ > **Info: Peer dependencies**
40
+ >
41
+ > `@cocoar/vue-page-builder` declares `@cocoar/vue-ui` **and** `@cocoar/vue-localization` as peer dependencies. All builder chrome and the renderer's validation messages resolve through `@cocoar/vue-localization` (keys under `coar.pageBuilder.*`), with built-in English fallbacks — English-only apps need no i18n setup.
42
+
43
+ ```vue
44
+ <script setup lang="ts">
45
+ import { ref } from 'vue';
46
+ import {
47
+ CoarPageBuilder,
48
+ CoarPageRenderer,
49
+ type PageNode,
50
+ type PageConfig,
51
+ type ActionValues,
52
+ } from '@cocoar/vue-page-builder';
53
+
54
+ const schema = ref<PageNode>({
55
+ id: 'root',
56
+ type: 'page',
57
+ schemaVersion: 6,
58
+ style: { gap: '16px', padding: '24px' },
59
+ children: [],
60
+ });
61
+
62
+ const config: PageConfig = {
63
+ allowedElements: ['stack', 'card', 'heading', 'paragraph',
64
+ 'text-input', 'checkbox', 'button', 'link', 'image'],
65
+ availableActions: [
66
+ { id: 'auth:login', label: 'Sign in' },
67
+ { id: 'auth:forgot', label: 'Forgot password' },
68
+ ],
69
+ };
70
+
71
+ const actions: Record<string, (v: ActionValues) => void> = {
72
+ 'auth:login': (values) => api.login(values),
73
+ 'auth:forgot': () => router.push('/forgot'),
74
+ };
75
+ </script>
76
+
77
+ <template>
78
+ <!-- Visual editor — user builds the page -->
79
+ <CoarPageBuilder v-model="schema" :config="config" style="height: 700px" />
80
+
81
+ <!-- Renderer — plays back the schema at runtime -->
82
+ <CoarPageRenderer
83
+ :schema="schema"
84
+ :config="config"
85
+ :actions="actions"
86
+ :asset-resolver="(id) => `/tenant/${tenantId}/assets/${encodeURIComponent(id)}`"
87
+ />
88
+ </template>
89
+ ```
90
+
91
+ The **same `config` is passed to both** — the builder uses it to filter UI affordances; the renderer uses it as the security boundary at render time. When `config.assetResolver` is set, the renderer falls back to it automatically, so the `:asset-resolver` prop is only needed as an override.
92
+
93
+ ## Architecture
94
+
95
+ ```
96
+ Consumer app
97
+
98
+ ├── <CoarPageBuilder v-model="schema" :config /> ← visual editor
99
+
100
+ └── <CoarPageRenderer :schema :config ← runtime renderer
101
+ :actions :asset-resolver /> maps JSON nodes → Cocoar components
102
+ wires action IDs → real handler functions
103
+ ```
104
+
105
+ The JSON schema is the single artifact that flows between builder and renderer. It is plain JSON with no library dependency — any renderer (including a custom one) can interpret it.
106
+
107
+ ## PageConfig — the consumer contract
108
+
109
+ Everything tenant-facing or domain-specific is declared here. Pass the **same value** to both the builder and the renderer.
110
+
111
+ ```ts
112
+ interface PageConfig {
113
+ /**
114
+ * Element types permitted to appear in the tree — built-in types and
115
+ * consumer-registered keys alike. Omit to allow every type.
116
+ * `page` (the root marker) is always implicitly allowed.
117
+ */
118
+ allowedElements?: (ElementType | (string & {}))[]
119
+
120
+ /**
121
+ * Consumer-registered element types, merged ADDITIVELY over the built-in
122
+ * set (shadowing a built-in key warns in DEV). One registration serves
123
+ * palette, canvas, inspector and runtime. App-wide defaults can be
124
+ * provided under PAGE_ELEMENT_TYPES_KEY instead; this field wins when both
125
+ * are present. See the Custom elements guide.
126
+ */
127
+ elements?: PageElementRegistry
128
+
129
+ /**
130
+ * The data contract behind the page (DTO fields). When present, the
131
+ * builder's Field section offers these instead of a free-text name —
132
+ * filtered per element to the compatible value types — the palette
133
+ * gains a draggable Fields group, and the builder lint flags unknown
134
+ * names, incompatible bindings and missing required fields.
135
+ * See the Field contract section below.
136
+ */
137
+ fields?: PageFieldSpec[]
138
+
139
+ /** Allow-listed host context for property bindings, conditions and repeaters. */
140
+ contextFields?: PageContextField[]
141
+ // interface PageContextField {
142
+ // path: string
143
+ // type: PageContextValueType
144
+ // itemFields?: PageContextItemField[]
145
+ // /** Closed set of values — the condition editor offers them as a dropdown
146
+ // * instead of a free-text box. This is how a host view state, tier or
147
+ // * status becomes authorable without a second mechanism for it. */
148
+ // values?: string[]
149
+ // }
150
+
151
+ /** Locales offered by the builder for LocalizedValue props. */
152
+ locales?: { id: string; label: string }[]
153
+ defaultLocale?: string
154
+
155
+ documentLimits?: { maxNodes?: number; maxDepth?: number }
156
+
157
+ /**
158
+ * Allow binding names outside `fields`. Defaults to false — with a
159
+ * contract, authors pick from it.
160
+ */
161
+ allowCustomFields?: boolean
162
+
163
+ /**
164
+ * Hide free value-producing elements from the library and the Inputs
165
+ * entries of the outline's add-child menu — exactly what the field
166
+ * contract replaces. Containers and content/action elements stay available.
167
+ * Pure authoring UI;
168
+ * `allowedElements` remains the boundary for what may be used at all.
169
+ */
170
+ hideElementPicker?: boolean
171
+
172
+ /**
173
+ * Action IDs that registry elements with `action: true` may reference. When provided, the
174
+ * builder's Action input becomes a dropdown of these labeled choices
175
+ * instead of free text. The renderer's `actions` map is the actual
176
+ * security boundary — `availableActions` is a UX affordance.
177
+ */
178
+ availableActions?: { id: string; label: string }[]
179
+
180
+ /**
181
+ * Resolves an assetId to a URL. Used by the builder for thumbnails
182
+ * (canvas preview, props panel, Preview tab) and by the runtime
183
+ * renderer for `<img src>`. Same contract as the renderer's
184
+ * `:asset-resolver` prop — the renderer falls back to this when
185
+ * that prop is absent, so passing the same config to both is enough.
186
+ */
187
+ assetResolver?: (id: string) => string
188
+
189
+ /**
190
+ * Opens the consumer's own asset picker UI and resolves to the chosen
191
+ * `assetId`, or `null` if the user cancelled. The library does NOT
192
+ * ship a picker — the IDP owns the entire picker UX (browse, upload,
193
+ * search, delete, categorisation, …). When omitted, the image element
194
+ * falls back to a free-text Asset ID input.
195
+ */
196
+ pickAsset?: (currentId?: string) => Promise<string | null>
197
+
198
+ /**
199
+ * Resolves an options-source id to the option list of a choice input
200
+ * (select / multi-select / radio-group) — the async sibling of
201
+ * `assetResolver`, for API-backed lists (countries, users, …). A node
202
+ * opts in via its `optionsSourceId` prop; static `options` stay the
203
+ * default and the fallback when this callback is absent.
204
+ */
205
+ optionsSource?: (sourceId: string) => Promise<OptionItem[]>
206
+ }
207
+ ```
208
+
209
+ ### `allowedElements`
210
+
211
+ Takes built-in types and consumer-registered keys alike. Enforced at **both** layers:
212
+ - *Builder*: hidden from the palette and add-child menu; tenants can't insert disallowed types. Nodes of a disallowed type already present in the schema get a validation **error** and a "skipped at runtime" treatment on the canvas; nodes of an *unregistered* type get a **warning** (they stay in the tree losslessly), so authors learn about both before saving.
213
+ - *Renderer*: disallowed nodes are skipped at render time (with one `console.warn` per type) even if they appear in hand-written or tampered JSON. This is the security boundary.
214
+
215
+ The gate also applies to the renderer's **value model**, not just rendering: disallowed subtrees contribute no default values and their fields never block validation — an invisible `required` field can't permanently veto a validating button.
216
+
217
+ ```ts
218
+ allowedElements: [
219
+ 'stack', 'card', 'section', 'divider',
220
+ 'heading', 'paragraph',
221
+ 'text-input', 'checkbox', 'button', 'link', 'image',
222
+ 'acme-rating', // a consumer-registered key from config.elementTypes
223
+ ],
224
+ ```
225
+
226
+ Drop element types the tenant shouldn't be able to use. `page` is implicitly always allowed.
227
+
228
+ ### `elements` — custom element types
229
+
230
+ The element set is open: one `definePageElement()` definition registers a component defined entirely in **your app** as a first-class element — it appears in the palette, canvas, inspector, preview and value model exactly like a built-in. Registrations merge **additively** over the built-ins; keys are lowercase kebab-case (`^[a-z][a-z0-9-]*$`) and a vendor prefix is recommended:
231
+
232
+ ```ts
233
+ import { definePageElement, type PageConfig } from '@cocoar/vue-page-builder';
234
+ import RatingRenderer from './RatingRenderer.vue';
235
+
236
+ const config: PageConfig = {
237
+ elements: {
238
+ 'acme-rating': definePageElement({
239
+ renderer: RatingRenderer, // receives { node }
240
+ value: { isEmpty: (v) => !v || Number(v) === 0 }, // participates in the value model
241
+ }),
242
+ },
243
+ allowedElements: ['stack', 'heading', 'text-input', 'button', 'acme-rating'],
244
+ };
245
+ ```
246
+
247
+ Unregistered types degrade **losslessly**: kept in the tree and in the JSON tab, flagged in the builder, skipped at runtime with one console warning per type, and excluded from the value model so they can never block a submit. The full contract — builder half (palette label, canvas preview, inspector, lint), `usePageElement()` renderer context, app-wide registration via `PAGE_ELEMENT_TYPES_KEY` — is covered in the [Custom elements guide](./custom-elements.md).
248
+
249
+ ### Field contract
250
+
251
+ In practice a page is rarely a free-form document — it usually **projects a DTO**: the login request, the profile record, the ticket form. The field names and their types are known up front, and authors should *pick* from them instead of inventing names the backend then has to guess at. `config.dataContract` declares that contract:
252
+
253
+ ```ts
254
+ const config: PageConfig = {
255
+ fields: [
256
+ { name: 'username', valueType: 'string', label: 'Username', required: true },
257
+ { name: 'password', valueType: 'string', label: 'Password', required: true, defaultElement: 'password-input' },
258
+ { name: 'rememberMe', valueType: 'boolean', label: 'Remember me' },
259
+ { name: 'age', valueType: 'number', label: 'Age' },
260
+ { name: 'dueUntil', valueType: 'date', label: 'Due until' },
261
+ ],
262
+ };
263
+ ```
264
+
265
+ Each `PageFieldSpec` is `{ name, valueType, label?, required?, defaultElement? }`: `name` is the `ActionValues` key (the DTO property), `valueType` decides which elements can edit the field, `label` is carried onto the element on binding, `required` sets `validation.required` on binding and keeps a root-level warning alive while the field is missing from the page, and `defaultElement` picks the element the field-first flow creates.
266
+
267
+ #### Value types and compatibility
268
+
269
+ Compatibility is an exact token match between the field's `valueType` and the element definition's `ElementValueSpec.types`. `PageValueType` is an **open** token union — the built-in tokens below plus any consumer token (`'geo'`, `'money'`, …):
270
+
271
+ | `valueType` | Compatible built-in elements |
272
+ |-------------|------------------------------|
273
+ | `string` | `text-input`, `password-input`, `select`, `radio-group`, `otp-input` |
274
+ | `boolean` | `checkbox`, `switch` |
275
+ | `number` | `number-input` |
276
+ | `string[]` | `multi-select` |
277
+ | `date` | `date-input` |
278
+ | `datetime` | `datetime-input` |
279
+
280
+ Consumer elements participate through the same declaration: a rating element whose definition says `value: { types: ['number'] }` becomes a representation for `number` fields — compatibility is registry-driven, not a central table. A value spec **without** `types` is unconstrained (compatible with every field), so consumer elements that don't declare are never falsely blocked. See [Custom elements](./custom-elements.md#_3-value-model-participation).
281
+
282
+ #### Two authoring flows
283
+
284
+ **Element-first** — drop any element, then bind it. With a contract, the Field section's *Field name* control becomes a select over the **compatible** fields only (a `text-input` never offers `rememberMe`). Binding takes the contract label along — never overwriting a label the author already edited — and sets `validation.required` for contract-required fields. Clearing the select unbinds; a bound name outside the contract stays visible as `(custom)`.
285
+
286
+ **Field-first** — with a contract the palette gains a third group, **Fields**: one draggable card per contract field, with a type icon, the contract label and a `*` for required fields; a card greys out once its name is bound anywhere on the page. Dropping a card creates the field's default element — `field.defaultElement` when registered, else the first compatible value element in registry order — **pre-bound**: name set, contract label carried into the props bag (when the element has a `label` prop at all), `validation.required` applied.
287
+
288
+ #### Representation switch
289
+
290
+ Same field, different element: the Field section gains an **Element** select listing the representations that can edit the bound field's value type (unbound: any type the current element declares) — filtered to placeable, allow-listed elements and hidden when fewer than two remain. Switching converts the node **in place**: it keeps `id` (selection follows), `name`, `defaultValue`, `validation`, `style` and the label, while the rest of the props bag restarts from the target element's defaults. One undoable step — username as `text-input` ⇄ `password-input` ⇄ `select` ⇄ `otp-input`, one click each.
291
+
292
+ #### Contract lint
293
+
294
+ Three rules join [builder-side validation](./coar-page-builder.md#builder-side-validation):
295
+
296
+ - a bound name **outside the contract** is an *error* — unless `allowCustomFields` is set,
297
+ - a **type-incompatible** binding (say, a `checkbox` bound to a `string` field) is an *error*,
298
+ - a **required contract field missing** from the page is a *warning* on the root node.
299
+
300
+ #### Strict by default
301
+
302
+ `allowCustomFields` defaults to `false`: with a contract, binding is select-only, and freshly dropped value elements start **unbound** instead of minting a `field_*` name the lint would immediately flag — the author picks a contract field. Setting `allowCustomFields: true` relaxes all of it: the Field section adds a free-text *Custom name* input, fresh elements mint names again, and the unknown-name lint rule stands down.
303
+
304
+ #### `allowedElements` governs everything
305
+
306
+ The allow-list composes with the contract at every seam: a field's default element (field-first drop) and the representation switcher only ever offer **allowed** elements. Drop `password-input` from `allowedElements` and a `string` field can no longer be represented as a password input — the field's `defaultElement` falls back to the first compatible *allowed* element, and a field with no allowed representation greys out in the palette.
307
+
308
+ #### Fields-only authoring
309
+
310
+ Set `hideElementPicker: true` to remove free value-producing elements from the right-hand **Elements** library and from the outline's **Inputs** add-child group — exactly the entries the field contract replaces. Fields then come exclusively from dragging contract cards. **Containers** and content/action elements (headings, notes, buttons, links, images) stay available because every form needs structure and chrome. Classification is registry-derived from the value spec, so consumer elements sort themselves. This is pure authoring UI — combine it with `allowedElements` when the *rendering* boundary should shrink too.
311
+
312
+ #### Typed field lists (opt-in)
313
+
314
+ For a **static** DTO, `defineFields<TDto>()` checks the field list at compile time — names must be DTO properties, value types must fit the property types (string properties admit the `date`/`datetime` tokens, since dates travel as ISO strings):
315
+
316
+ ```ts
317
+ interface LoginDto { username: string; password: string; rememberMe: boolean }
318
+
319
+ fields: defineFields<LoginDto>([
320
+ { name: 'username', valueType: 'string', required: true },
321
+ { name: 'rememberMe', valueType: 'boolean' },
322
+ // { name: 'usernme', valueType: 'string' }, // ✗ compile error — not a DTO property
323
+ // { name: 'rememberMe', valueType: 'string' } // ✗ compile error — boolean property
324
+ ])
325
+ ```
326
+
327
+ It is pure opt-in sugar with zero runtime cost: the result is a plain `PageFieldSpec[]`, so dynamically grown DTOs keep working — either skip the helper entirely, or mix: `[...defineFields<LoginDto>([...]), ...dynamicExtraFields]`.
328
+
329
+ #### Authoring-only by design
330
+
331
+ The contract constrains **authoring only**. Binding is plain `node.name` — persisted schemas stay self-contained, render without the contract, and a document authored under one contract remains a valid document everywhere. The renderer never consults `fields`; `allowedElements` remains the security boundary.
332
+
333
+ ### `availableActions`
334
+
335
+ When provided, the Action ID input in Button/Link props becomes a dropdown. Stored action IDs that aren't in the list are surfaced as `auth:something (not configured)` so orphans don't silently disappear.
336
+
337
+ ```ts
338
+ availableActions: [
339
+ { id: 'auth:login', label: 'Sign in' },
340
+ { id: 'auth:register', label: 'Create account' },
341
+ { id: 'auth:forgot-password', label: 'Forgot password' },
342
+ { id: 'auth:sso-google', label: 'Sign in with Google' },
343
+ { id: 'auth:sso-microsoft', label: 'Sign in with Microsoft' },
344
+ ],
345
+ ```
346
+
347
+ The runtime `actions` map on the renderer is the real boundary — it only invokes handlers that exist there. `availableActions` is purely a UX affordance.
348
+
349
+ ### `assetResolver` + `pickAsset`
350
+
351
+ The library does **not** ship an asset picker. You build your own — a modal, a drawer, a sidebar, however you want — and wire it in via two simple callbacks.
352
+
353
+ ```ts
354
+ const config: PageConfig = {
355
+ // ...
356
+ /** Resolves an asset id to a URL. The builder uses this for thumbnails;
357
+ the renderer falls back to it when its :asset-resolver prop is absent. */
358
+ assetResolver: (id) => `https://cdn.example.com/t/${tenantId}/${encodeURIComponent(id)}`,
359
+
360
+ /** Opens YOUR picker and resolves to the chosen id (or null on cancel). */
361
+ async pickAsset(currentId) {
362
+ const result = await myAssetModal.open({ initial: currentId });
363
+ return result ?? null;
364
+ },
365
+ };
366
+ ```
367
+
368
+ > **Warning: Validate the asset id**
369
+ >
370
+ > `assetResolver` receives whatever `assetId` string sits in the schema — including hand-edited JSON. Encode it (`encodeURIComponent`) or allowlist it (e.g. `/^[A-Za-z0-9_-]+$/`) before splicing it into a URL, or a crafted id like `../other-tenant/logo` walks out of your tenant prefix.
371
+
372
+ The image element's props panel renders:
373
+ - a **thumbnail** using `assetResolver(node.assetId)`
374
+ - a **Choose…/Change…** button that calls `pickAsset(currentId)` and patches the returned id onto the schema
375
+ - a **Clear** button when an id is set
376
+
377
+ When `pickAsset` is omitted, the image element falls back to a free-text Asset ID input — useful for development or scripted authoring.
378
+
379
+ #### What your picker needs to do
380
+
381
+ The full contract is just `(currentId?: string) => Promise<string | null>`. Inside, you do whatever fits your stack:
382
+
383
+ - list assets from your API
384
+ - handle uploads (sign URL, POST file, etc.)
385
+ - search, filter, paginate
386
+ - delete
387
+ - categorise by tag/folder
388
+ - show metadata, dimensions, file size
389
+
390
+ Return the chosen asset's id, or `null` if the user cancelled. The library doesn't care about anything else.
391
+
392
+ Example skeleton using Cocoar's `useDialog`:
393
+
394
+ ```ts
395
+ import { useDialog } from '@cocoar/vue-ui';
396
+ import MyAssetPicker from './MyAssetPicker.vue';
397
+
398
+ const dialog = useDialog();
399
+
400
+ const config: PageConfig = {
401
+ // ...
402
+ assetResolver: (id) => assetUrlMap.value.get(id) ?? '',
403
+ async pickAsset(currentId) {
404
+ const { result } = dialog.open<string>(
405
+ MyAssetPicker,
406
+ { title: 'Choose image', size: 'l' },
407
+ { initial: currentId },
408
+ );
409
+ return (await result) ?? null;
410
+ },
411
+ };
412
+ ```
413
+
414
+ A complete reference implementation lives at `apps/playground/src/components/PlaygroundAssetPicker.vue` — copy it as a starting point.
415
+
416
+ ## Security Model
417
+
418
+ **Allowed elements** — `config.allowedElements` is enforced at both layers (builder hides and flags; renderer skips, with one `console.warn` per type). The renderer is the hard boundary — even tampered JSON cannot smuggle in disallowed types, and disallowed subtrees are excluded from the value model too (no defaults, no validation veto).
419
+
420
+ **Actions** — every registry element that declares `action: true` stores the shared optional `ActionProps` contract; built-in buttons and links use it too. The builder supplies one Action + JSON key/value editor with an `fx` switch per value, and the renderer only invokes handlers from the consumer-provided `actions` map — action ids are inert strings. Handler payload precedence is form values < resolved per-key `actionValues` < the legacy bound `actionValue`; only JSON-safe explicit values cross the boundary. Per-key bindings may read controlled context, customer Page State, form fields, named Repeat selections, or the current Repeat item/index. When `config.availableActions` is set, the builder also constrains the Action input to a labeled dropdown. One qualification to "nothing executable lives in the schema": `validation.pattern` is a tenant-authored regular expression that *is* evaluated at render time. It is compiled safely — an invalid pattern becomes an inert rule with a single `console.warn` — and anchored to match the full string, like the HTML `pattern` attribute.
421
+
422
+ **Images** — `image` nodes store an `assetId` reference, never a raw URL. The renderer calls `assetResolver(id)` at render time. The "tenants cannot reference external domains" guarantee is therefore exactly as strong as **your** `assetResolver` — validate or encode the id before building a URL (see the warning above). Uploads happen entirely inside the consumer-built picker (whatever `pickAsset` opens) — that's where you validate file type, scan for malware, and enforce per-tenant size quotas.
423
+
424
+ ## Complete IDP integration walkthrough
425
+
426
+ Here's how a tenant-customisable login flow fits together end-to-end.
427
+
428
+ ### 1. Define the tenant config
429
+
430
+ In a shared file your admin app and your login app both import:
431
+
432
+ ```ts
433
+ // tenants/loginConfig.ts
434
+ import type { PageConfig } from '@cocoar/vue-page-builder';
435
+
436
+ export function buildLoginConfig(tenantId: string): PageConfig {
437
+ return {
438
+ allowedElements: [
439
+ 'stack', 'card', 'divider',
440
+ 'heading', 'paragraph',
441
+ 'text-input', 'checkbox', 'button', 'link', 'image',
442
+ ],
443
+ availableActions: [
444
+ { id: 'auth:login', label: 'Sign in' },
445
+ { id: 'auth:sso-google', label: 'Sign in with Google' },
446
+ { id: 'auth:sso-microsoft', label: 'Sign in with Microsoft' },
447
+ { id: 'auth:forgot-password', label: 'Forgot password' },
448
+ { id: 'auth:register', label: 'Create account' },
449
+ { id: 'nav:login', label: 'Go to login' },
450
+ ],
451
+ // Allowlist the id — it's tenant-authored schema data, not trusted input.
452
+ assetResolver: (id) =>
453
+ /^[A-Za-z0-9_-]+$/.test(id) ? `https://cdn.example.com/t/${tenantId}/${id}` : '',
454
+ async pickAsset(currentId) {
455
+ // Open your own asset picker — the library does not ship one.
456
+ // Inside MyAssetPickerModal you'd call your /api/tenants/${tenantId}/assets
457
+ // endpoint for the list, POST for uploads, etc.
458
+ return await openMyAssetPickerModal({ tenantId, initial: currentId });
459
+ },
460
+ };
461
+ }
462
+ ```
463
+
464
+ ### 2. Admin page — the builder
465
+
466
+ ```vue
467
+ <script setup lang="ts">
468
+ import { ref, onMounted } from 'vue';
469
+ import { CoarPageBuilder, type PageNode } from '@cocoar/vue-page-builder';
470
+ import { buildLoginConfig } from '@/tenants/loginConfig';
471
+
472
+ const props = defineProps<{ tenantId: string }>();
473
+ const schema = ref<PageNode>({
474
+ id: 'root', type: 'page', style: { gap: '16px', padding: '24px' }, children: [],
475
+ });
476
+ const config = buildLoginConfig(props.tenantId);
477
+ const saving = ref(false);
478
+
479
+ onMounted(async () => {
480
+ const res = await fetch(`/api/tenants/${props.tenantId}/login-schema`);
481
+ if (res.ok) schema.value = await res.json();
482
+ });
483
+
484
+ async function save() {
485
+ saving.value = true;
486
+ try {
487
+ await fetch(`/api/tenants/${props.tenantId}/login-schema`, {
488
+ method: 'PUT',
489
+ headers: { 'Content-Type': 'application/json' },
490
+ body: JSON.stringify(schema.value),
491
+ });
492
+ } finally {
493
+ saving.value = false;
494
+ }
495
+ }
496
+ </script>
497
+
498
+ <template>
499
+ <div class="tenant-login-editor">
500
+ <header>
501
+ <h1>Login page editor</h1>
502
+ <button @click="save" :disabled="saving">
503
+ {{ saving ? 'Saving…' : 'Save' }}
504
+ </button>
505
+ </header>
506
+ <CoarPageBuilder v-model="schema" :config="config" style="height: 80vh" />
507
+ </div>
508
+ </template>
509
+ ```
510
+
511
+ ### 3. Runtime — the login page itself
512
+
513
+ ```vue
514
+ <script setup lang="ts">
515
+ import { ref, onMounted } from 'vue';
516
+ import { CoarPageRenderer, type PageNode, type ActionValues } from '@cocoar/vue-page-builder';
517
+ import { buildLoginConfig } from '@/tenants/loginConfig';
518
+
519
+ const props = defineProps<{ tenantId: string }>();
520
+ const schema = ref<PageNode | null>(null);
521
+ const config = buildLoginConfig(props.tenantId);
522
+
523
+ onMounted(async () => {
524
+ const res = await fetch(`/api/tenants/${props.tenantId}/login-schema`);
525
+ schema.value = await res.json();
526
+ });
527
+
528
+ const actions: Record<string, (v: ActionValues) => void> = {
529
+ 'auth:login': (v) => authService.login(v),
530
+ 'auth:forgot-password': () => router.push('/forgot'),
531
+ 'auth:sso-google': () => authService.startSso('google'),
532
+ 'auth:sso-microsoft': () => authService.startSso('microsoft'),
533
+ 'auth:register': (v) => authService.register(v),
534
+ 'nav:login': () => router.push('/login'),
535
+ };
536
+ </script>
537
+
538
+ <template>
539
+ <!-- No :asset-resolver needed — the renderer falls back to config.assetResolver. -->
540
+ <CoarPageRenderer
541
+ v-if="schema"
542
+ :schema="schema"
543
+ :config="config"
544
+ :actions="actions"
545
+ />
546
+ </template>
547
+ ```
548
+
549
+ ### Notes for the IDP wiring
550
+
551
+ - **Schema migration** — the builder normalises schemas at **every entry point**: the initial `v-model` value, external `v-model` replacement, and the JSON tab's Apply. Legacy `column`/`row` containers migrate to `stack`, v1 flat documents get their `props` bags, non-`page` roots get wrapped in a `page`, missing or duplicate node ids are repaired, missing `children` arrays / `props` bags and out-of-range heading levels are healed. The runtime renderer additionally runs both migrations on the fly, so old saved schemas keep rendering even without a round-trip through the builder. To run the same migration server-side before persisting, use the exported helpers: `normalizePageSchema(value)` returns `{ schema, issues, changed }`; `migrateLegacyTypes`, `migrateV1PropsBag` and `KNOWN_ELEMENT_TYPES` are exported alongside it.
552
+ - **`schemaVersion`** — new roots are stamped with `schemaVersion: 6`. Version 4 keeps the v2 props-bag and v3 runtime-composition grammar and adds a stable page-wide `name` to every element for Element Code (the same name is the form/DTO key for value elements). Version 5 adds builder-only origin metadata for reusable, versioned compositions. Version 6 renames the repeat's `props.source` to `props.contextPath`, so `source` means one thing everywhere. Older documents are normalized deterministically. Persist the version as-is.
553
+ - **JSON Apply is gated by severity** — structural **errors** (non-object nodes — data would be dropped) reject the Apply with a message; nothing broken reaches your `v-model`. **Warnings** (healed or lossless findings — including *unknown element types*, which stay in the tree losslessly) apply anyway and are surfaced inline, so documents using newer or unregistered element types remain editable.
554
+ - **Validation** — builder validation flags authoring mistakes but never blocks saving: a button/link without an action, or with an action id outside `availableActions`, or an *unregistered* element type, is a *warning*; duplicate field names, missing image asset ids, invalid `validation.pattern`, and disallowed element types are *errors*. If you need hard guarantees, validate server-side before persisting (e.g., reject if any image node has an empty `props.assetId`). At runtime, a `validates: true` button stays **clickable** while the form is invalid — clicking it marks every field touched and reveals all errors instead of running the action; it only disables while an async `onValidate` is in flight. Cross-field or server-side checks (e.g., "email domain not allowed for this tenant") go through the renderer's `:on-validate` prop: it runs at submit time after the declarative rules pass, may return a `Promise` of `{ fieldName: errorMessage }`, a non-empty result blocks the action, and editing a field clears its server error. See [CoarPageRenderer](./coar-page-renderer.md).
555
+ - **CSP** — image URLs come from `assetResolver`, so your CDN domain needs to be in `img-src`. Action IDs and labels are inert strings; the one tenant-authored value evaluated at render time is `validation.pattern`, which is compiled safely and anchored (see [Security Model](#security-model)).
556
+ - **Full-screen / centering** — the renderer fills and measures its host width. To center content on a full-height screen, set the `page` node's `minHeight: '100dvh'` + `justify: 'center'` + `align: 'center'`. See [Sizing and alignment](./coar-page-renderer.md#sizing-and-alignment).
557
+ - **Per-tenant theming** — the renderer uses the Cocoar Design System tokens; override CSS variables on a wrapping container for tenant brand colors.
558
+
559
+ ## Implementation Roadmap
560
+
561
+ | Phase | Scope | Status |
562
+ |-------|-------|--------|
563
+ | **1 — Foundation** | `schema.ts` types · `CoarPageRenderer` · playground demo | ✅ Done |
564
+ | **2 — Builder shell** | Canvas + palette · Outline · Props panel · DnD · Undo/redo · JSON tab | ✅ Done |
565
+ | **3 — Config + safety** | `page` root · `stack` (direction toggle) · `:config.allowedElements` · `:config.availableActions` | ✅ Done |
566
+ | **4 — Asset callbacks + polish** | `:config.pickAsset` + `:config.assetResolver` · builder validation · responsive preview | ✅ Done |
567
+ | **5 — Layout & sizing** | Flex model — `justify` / `align` / `alignSelf` / `size` (fit · fill · fixed) / `minHeight`; guided Style-panel controls; Editor matches Preview | ✅ Done |
568
+ | **GA hardening** | Correctness & data-safety fixes (schema normalization at every entry point, gated JSON Apply, `crypto.randomUUID` ids, `schemaVersion` stamp, safe `pattern` compile) · pointer-events DnD (mouse + touch/pen long-press, outline drag-to-reorder) · validation UX (clickable validating buttons, submit-time async `onValidate`) · outline ARIA tree + scoped keyboard shortcuts · duplicate / select-options / default-value editors · i18n (`coar.pageBuilder.*` via `@cocoar/vue-localization`) | ✅ Done |
569
+ | **Element registry** | Unified props-bag wire format (introduced in v2, current `schemaVersion: 6`, transparent older-document normalization) · open [consumer-registered element types](./custom-elements.md) (`config.elementTypes`, `definePageElement`, `usePageElement`) · lossless degradation of unregistered types · severity-gated JSON Apply · renderer `initialValues` | ✅ Done |
570
+ | **Submit lifecycle & dynamics** | Async actions (`isSubmitting`, spinner, reentry guards) · [form-level error channel](./coar-page-renderer.md#async-actions-the-form-level-error-channel) (`_form`, banner, `#form-error` slot) · [Enter-to-submit](./coar-page-renderer.md#enter-to-submit) · built-in email format check · host form API (`update:values`, `values` / `isDirty` / `reset`) · [`visibleWhen`](./coar-page-renderer.md#conditional-visibility-visiblewhen) conditional visibility · [`optionsSource`](./coar-page-renderer.md#dynamic-options-optionssource) dynamic option lists | ✅ Done |
571
+ | **5b — Style editor (visual)** | Spacing sliders + colour pickers (rolls into the tenant theming track) | Planned |
572
+ | **Runtime composition v4** | Mobile-first responsive overrides · safe context/state/item bindings · Page State and per-element code · key-based localization · generic repeaters and selected-key outputs · feedback zones · document-limit fallback validation | ✅ Done |
573
+ | **5+ — Schema versioning** | Formal multi-step migration framework beyond the current deterministic normalization to v5 | Planned |