@cocoar/vue-page-builder 2.20.0-beta.19 → 3.0.0

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 (77) hide show
  1. package/IDP_INTEGRATION.md +350 -355
  2. package/README.md +498 -509
  3. package/dist/{BuilderElementCodeDialog-ByhAkwmS.js → BuilderElementCodeDialog-DXGNzleE.js} +2 -2
  4. package/dist/{BuilderExpressionDialog-DjGebYyv.js → BuilderExpressionDialog-yIfwYgXD.js} +1 -1
  5. package/dist/{BuilderLogicPanel-Cxd04A93.js → BuilderLogicPanel-C9urz5Cd.js} +3 -3
  6. package/dist/{BuilderPageRootCodeDialog-7uXyUGGD.js → BuilderPageRootCodeDialog-CQsg_ozW.js} +2 -2
  7. package/dist/CoarPageBuilder.vue.d.ts +15 -4
  8. package/dist/CoarPageBuilder.vue.d.ts.map +1 -1
  9. package/dist/CoarPageRenderer.vue.d.ts +0 -2
  10. package/dist/CoarPageRenderer.vue.d.ts.map +1 -1
  11. package/dist/PageNode.vue.d.ts.map +1 -1
  12. package/dist/{VisualMarkupInspector-DGjFuyMS.js → VisualMarkupInspector-CmAOc-CH.js} +2 -2
  13. package/dist/builder/BuilderCanvas.vue.d.ts +31 -1
  14. package/dist/builder/BuilderCanvas.vue.d.ts.map +1 -1
  15. package/dist/builder/BuilderCanvasNode.vue.d.ts.map +1 -1
  16. package/dist/builder/BuilderOutline.vue.d.ts.map +1 -1
  17. package/dist/builder/BuilderOutlineNode.vue.d.ts.map +1 -1
  18. package/dist/builder/BuilderPalette.vue.d.ts.map +1 -1
  19. package/dist/builder/BuilderPropsPanel.vue.d.ts.map +1 -1
  20. package/dist/builder/boxSides.d.ts +30 -0
  21. package/dist/builder/boxSides.d.ts.map +1 -0
  22. package/dist/builder/builderContext.d.ts +2 -2
  23. package/dist/builder/builderContext.d.ts.map +1 -1
  24. package/dist/builder/pageCodeAuthoring.d.ts.map +1 -1
  25. package/dist/builder/props/BuilderViewportControl.vue.d.ts +23 -0
  26. package/dist/builder/props/BuilderViewportControl.vue.d.ts.map +1 -0
  27. package/dist/builder/props/BuilderZoomControl.vue.d.ts +24 -0
  28. package/dist/builder/props/BuilderZoomControl.vue.d.ts.map +1 -0
  29. package/dist/builder/props/CompoundLengthProperty.vue.d.ts +20 -0
  30. package/dist/builder/props/CompoundLengthProperty.vue.d.ts.map +1 -0
  31. package/dist/builder/props/StyleProps.vue.d.ts.map +1 -1
  32. package/dist/builder/schemaMigrateV1.d.ts +7 -0
  33. package/dist/builder/schemaMigrateV1.d.ts.map +1 -1
  34. package/dist/builder/{useSchemaValidation.d.ts → useAuthoringFindings.d.ts} +8 -8
  35. package/dist/builder/useAuthoringFindings.d.ts.map +1 -0
  36. package/dist/builder/useCanvasZoom.d.ts +50 -0
  37. package/dist/builder/useCanvasZoom.d.ts.map +1 -0
  38. package/dist/builder/usePageBuilder.d.ts +0 -2
  39. package/dist/builder/usePageBuilder.d.ts.map +1 -1
  40. package/dist/{builderContext-5hKJ8y8r.js → builderContext-B3rX2dxZ.js} +1 -1
  41. package/dist/conditions.d.ts +0 -1
  42. package/dist/conditions.d.ts.map +1 -1
  43. package/dist/documentValidation.d.ts.map +1 -1
  44. package/dist/elements/card/index.d.ts.map +1 -1
  45. package/dist/elements/image/index.d.ts.map +1 -1
  46. package/dist/elements/previewUtils.d.ts.map +1 -1
  47. package/dist/elements/registry.d.ts +158 -4
  48. package/dist/elements/registry.d.ts.map +1 -1
  49. package/dist/elements/repeat/index.d.ts +1 -1
  50. package/dist/elements/repeat/index.d.ts.map +1 -1
  51. package/dist/elements/section/index.d.ts.map +1 -1
  52. package/dist/elements/stack/index.d.ts.map +1 -1
  53. package/dist/elements/visual-markup/index.d.ts.map +1 -1
  54. package/dist/{expressionAuthoring-DHVx3i-G.js → expressionAuthoring-7iAyadA3.js} +1 -1
  55. package/dist/index.css +1 -1
  56. package/dist/index.d.ts +15 -5
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +3787 -4294
  59. package/dist/{pageCode-hhTSp3xO.js → pageCode-echPQdna.js} +57 -49
  60. package/dist/pageCode.d.ts +11 -3
  61. package/dist/pageCode.d.ts.map +1 -1
  62. package/dist/{pageCodeAuthoring-Fg3yqXt3.js → pageCodeAuthoring-C8grEAUf.js} +2 -5
  63. package/dist/runtime/usePageCodeRuntime.d.ts +0 -1
  64. package/dist/runtime/usePageCodeRuntime.d.ts.map +1 -1
  65. package/dist/runtimeBindings.d.ts +0 -1
  66. package/dist/runtimeBindings.d.ts.map +1 -1
  67. package/dist/schema.d.ts +43 -52
  68. package/dist/schema.d.ts.map +1 -1
  69. package/dist/styleMapping.d.ts +3 -0
  70. package/dist/styleMapping.d.ts.map +1 -1
  71. package/dist/{visualDocument-DPb3svLG.js → visualDocument-DKjWBfpW.js} +2 -2
  72. package/package.json +4 -4
  73. package/dist/builder/useSchemaValidation.d.ts.map +0 -1
  74. package/dist/presets/authCustomization.d.ts +0 -33
  75. package/dist/presets/authCustomization.d.ts.map +0 -1
  76. package/dist/stylePresets.d.ts +0 -4
  77. package/dist/stylePresets.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,509 +1,498 @@
1
- # @cocoar/vue-page-builder
2
-
3
- A generic, headless visual page builder and renderer for Vue 3, built on the
4
- Cocoar Design System. Users drag UI primitives onto a canvas, configure them,
5
- and the result is a plain JSON schema (`PageNode`) that `<CoarPageRenderer>`
6
- turns back into live Cocoar components.
7
-
8
- Everything domain-specific — which actions a button may trigger, where images
9
- come from, which elements are permitted, which element types even exist is
10
- defined by the **consumer application** through a single `PageConfig`, not by
11
- the library. The renderer enforces `allowedElements` as a security boundary:
12
- disallowed nodes are skipped at render time, even in hand-written or tampered
13
- JSON.
14
-
15
- ## Install
16
-
17
- ```bash
18
- pnpm add @cocoar/vue-page-builder @cocoar/vue-ui \
19
- @cocoar/vue-localization @cocoar/vue-script-editor monaco-editor
20
- ```
21
-
22
- The UI, localization, ScriptEditor, Monaco and Vue packages are peers so the
23
- host owns their single application-wide instances. Import the stylesheet once —
24
- it carries the builder chrome **and** the renderer's layout styles:
25
-
26
- ```ts
27
- import '@cocoar/vue-page-builder/styles';
28
- ```
29
-
30
- The Builder uses Monaco in JavaScript and JSON mode. Register the TypeScript /
31
- JavaScript and JSON workers before the first Builder mounts; the complete Vite
32
- and SSR configurations are documented in
33
- [IDP_INTEGRATION.md](./IDP_INTEGRATION.md#register-all-monaco-workers-used-by-the-builder).
34
-
35
- ## Usage
36
-
37
- ```vue
38
- <script setup lang="ts">
39
- import { ref } from 'vue';
40
- import {
41
- CoarPageBuilder,
42
- CoarPageRenderer,
43
- type PageNode,
44
- type PageConfig,
45
- } from '@cocoar/vue-page-builder';
46
-
47
- const schema = ref<PageNode>();
48
-
49
- const config: PageConfig = {
50
- allowedElements: ['stack', 'card', 'heading', 'paragraph', 'text-input', 'button'],
51
- availableActions: [{ id: 'auth:login', label: 'Sign in' }],
52
- };
53
- </script>
54
-
55
- <template>
56
- <!-- Visual editor (needs a bounded height) -->
57
- <CoarPageBuilder v-model="schema" :config="config" style="height: 700px" />
58
-
59
- <!-- Runtime renderer — same config = same boundary -->
60
- <CoarPageRenderer
61
- :schema="schema!"
62
- :config="config"
63
- :actions="{ 'auth:login': (values) => console.log(values) }"
64
- />
65
- </template>
66
- ```
67
-
68
- ## Schema (v5)
69
-
70
- The persisted document is a tree of one uniform node grammar: `type` is an
71
- open registry key (built-in or consumer), everything element-specific lives in
72
- the `props` bag, and the host vocabulary — `id`, `style`, the value-model trio
73
- `name` / `defaultValue` / `validation`, and `children` for containers — stays
74
- at node level. The `page` root carries `schemaVersion: 5`. Version 4 gives every
75
- element a stable, page-wide `name`; value elements use that same name as their
76
- form/DTO property and Element Code uses it as its authoring identity. Version 5
77
- adds builder-only origin metadata for reusable, versioned compositions.
78
-
79
- ```jsonc
80
- {
81
- "id": "3f6c…",
82
- "type": "page",
83
- "schemaVersion": 5,
84
- "style": { "gap": "16px", "padding": "24px" },
85
- "children": [
86
- { "id": "a1b2…", "type": "heading", "props": { "text": "Sign in", "level": 2 } },
87
- {
88
- "id": "c3d4…",
89
- "type": "text-input",
90
- "props": { "label": "Email", "inputType": "email" },
91
- "name": "email",
92
- "validation": { "required": true },
93
- "style": { "size": "fill" }
94
- },
95
- {
96
- "id": "e5f6…",
97
- "type": "button",
98
- "props": { "label": "Sign in", "action": "auth:login", "validates": true }
99
- }
100
- ]
101
- }
102
- ```
103
-
104
- Older documents are normalized transparently on every ingest path: pre-v2 flat
105
- props move into the `props` bag, v3 runtime-composition documents keep their
106
- meaning, and v4 deterministically adds missing element names. Unknown or
107
- unregistered types stay losslessly in the tree (flagged in the builder, skipped
108
- with a one-time warning at runtime).
109
-
110
- ## Reusable compositions
111
-
112
- Any non-page subtree can be stored and reused: a brand panel, header, form
113
- section, footer, product card or a consumer element. Compositions are generic;
114
- there are no auth-specific categories or runtime components.
115
-
116
- The host owns persistence by implementing `PageCompositionRepository`. It may
117
- use an API, IndexedDB or another store, and every method may be sync or async:
118
-
119
- ```ts
120
- import type { PageCompositionRepository } from '@cocoar/vue-page-builder';
121
-
122
- const compositionRepository: PageCompositionRepository = {
123
- list: () => api.get('/page-compositions'),
124
- get: (id, version) => api.get(`/page-compositions/${id}/${version ?? 'latest'}`),
125
- create: (input) => api.post('/page-compositions', input),
126
- publish: (input) => api.post(`/page-compositions/${input.id}/versions`, input),
127
- };
128
- ```
129
-
130
- Pass it only to the Builder:
131
-
132
- ```vue
133
- <CoarPageBuilder
134
- v-model="authoringDocument"
135
- :config="config"
136
- :composition-repository="compositionRepository"
137
- composition-management="consume"
138
- @open-composition="({ id, version }) => openCompositionEditor(id, version)"
139
- />
140
- ```
141
-
142
- `composition-management="consume"` is intended for a host with separate
143
- **Pages** and **Compositions** areas. Page editors can insert an exact pinned
144
- version, update it or detach it, but cannot create or publish definitions. The
145
- host edits a definition as a standalone element subtree (usually inside a
146
- temporary Page root) and calls `create()` / `publish()` itself. The default
147
- `inline` mode additionally exposes create/publish in the Builder tab and is
148
- useful for compact tools or bootstrapping a definition from an existing page.
149
-
150
- Every repository summary is also exposed in the normal element palette under
151
- **Compositions**. Dragging one to a valid tree/canvas drop zone loads its
152
- displayed `latestVersion`, materializes the definition and pins that exact
153
- version. Selecting the linked instance root exposes its name, pinned-version
154
- selector, update, detach and open controls in Properties. `open-composition`
155
- is the host navigation seam: the Builder emits the exact pinned `{ id,
156
- version }`, while the host decides where its independent definition editor
157
- lives. The existing Compositions tab remains the overview for references,
158
- updates and repository problems.
159
-
160
- Instances are fully materialized normal node trees. This gives drafts an
161
- offline-safe snapshot and lets the Builder preserve page-local node ids and
162
- public names across updates. Nested compositions keep a small origin chain so
163
- each linked level can be updated or detached independently. Deleting an instance
164
- from one page never deletes the repository definition or instances on other pages.
165
-
166
- Before persisting a runtime document, remove authoring links deterministically:
167
-
168
- ```ts
169
- import { compilePageCompositions } from '@cocoar/vue-page-builder';
170
-
171
- const runtimeDocument = compilePageCompositions(authoringDocument.value);
172
- ```
173
-
174
- The compiled result has no repository ids, versions or source-node metadata and
175
- requires no composition repository at runtime. Use
176
- `validatePageCompositionReferences(authoringDocument, repository)` before
177
- publication to report missing versions and nested cycles. The package also
178
- exports `createInMemoryPageCompositionRepository()` for tests and local demos.
179
-
180
- ## Action payloads
181
-
182
- Every action-capable element uses the same optional `ActionProps` contract:
183
-
184
- ```ts
185
- interface ActionProps {
186
- action?: string;
187
- actionValues?: Record<string, unknown>;
188
- actionValueField?: string;
189
- actionValue?: unknown;
190
- }
191
- ```
192
-
193
- `actionValues` contains JSON-safe defaults. Every individual entry can be
194
- switched to **fx** or bound through `bindings["actionValues.<key>"]` to a host
195
- context value, customer Page State, form field, named Repeat selection, current
196
- Repeat item/index, or a sandbox expression. `actionValue` /
197
- `actionValueField` remain as the backwards-compatible single-dynamic-value
198
- shape. The handler receives one snapshot with deterministic precedence:
199
- **form values < resolved `actionValues` < dynamic `actionValue`**. Explicit
200
- action arguments therefore win key collisions with form fields.
201
-
202
- ```jsonc
203
- {
204
- "props": {
205
- "action": "auth:consent-allow",
206
- "actionValues": { "approvedScopes": [] }
207
- },
208
- "bindings": {
209
- "actionValues.approvedScopes": {
210
- "source": "selection",
211
- "path": "approvedScopes"
212
- }
213
- }
214
- }
215
- ```
216
-
217
- The builder supplies this editor automatically to built-in buttons/links and
218
- to consumer elements registered with `action: true`. A custom action renderer
219
- should call `usePageElement().triggerElementAction(node.props)` so it follows
220
- the identical merge, validation and async-action path.
221
-
222
- ## Host themes and style presets
223
-
224
- Runtime applications wrap the renderer in the generic `CoarThemeScope` from
225
- `@cocoar/vue-ui`. For authoring, pass the same theme as `previewTheme`; it is
226
- applied only to the preview canvas, never to the builder chrome.
227
-
228
- Host CSS can be exposed as controlled presets through `PageConfig`:
229
-
230
- ```ts
231
- const config: PageConfig = {
232
- stylePresets: [{
233
- id: 'brand-auth-visual',
234
- label: 'Brand Auth Visual',
235
- className: 'brand-auth-visual',
236
- allowedOn: ['page', 'stack', 'card'],
237
- }],
238
- };
239
- ```
240
-
241
- The page document stores only `stylePreset: 'brand-auth-visual'`; it never
242
- stores raw CSS or an arbitrary class. The host must load the matching CSS in
243
- both administration and runtime. Unknown, unsafe, or disallowed presets are an
244
- authoring error and are safely ignored by the renderer.
245
-
246
- ## Decorative visual markup
247
-
248
- The built-in `visual-markup` element is the deliberately narrow escape hatch
249
- for branded, animated decoration that cannot reasonably be expressed through
250
- individual PageBuilder primitives. It renders HTML, inline SVG and local CSS in
251
- an opaque-origin iframe. It is **not** a general custom-code element:
252
-
253
- - the iframe has an empty `sandbox`, `aria-hidden="true"`, `tabindex="-1"` and
254
- `pointer-events: none`;
255
- - JavaScript, forms, links, navigation, network access, parent-DOM access and
256
- interactive controls are rejected or blocked by CSP;
257
- - markup and CSS use strict allowlists and hard size limits;
258
- - invalid visuals are hidden locally; siblings and the page form keep working;
259
- - Builder Preview and Runtime use the same renderer and security policy.
260
-
261
- Functional content must remain native PageBuilder elements. A visual node can
262
- use local `@keyframes`, transforms, media queries, `prefers-reduced-motion`, CSS
263
- custom properties and inline SVG. Its outer PageBuilder `style` controls its
264
- size, including responsive `width`, `height`, `minHeight`, `maxHeight`,
265
- `aspectRatio` and `size`.
266
-
267
- Select the node to edit its HTML and CSS source with the dedicated Monaco
268
- inspectors. These source editors remain available in code-driven authoring
269
- mode; ordinary computed properties still use Element Code and Quick
270
- Properties. Invalid source is reported on the node and in the inspector.
271
-
272
- The host may inject only explicitly approved theme values and font files:
273
-
274
- ```ts
275
- import type { PageConfig } from '@cocoar/vue-page-builder';
276
-
277
- const config: PageConfig = {
278
- allowedElements: ['page', 'row', 'column', 'visual-markup', 'text-input', 'button'],
279
- visualMarkup: {
280
- themeVariables: {
281
- '--coar-accent': '#10b981',
282
- '--visual-surface': '#ffffff',
283
- '--visual-text': '#16202e',
284
- },
285
- fonts: [{
286
- id: 'brand-variable',
287
- family: 'Brand Sans Variable',
288
- source: approvedFontDataUrl, // data:font/...;base64 or a host-created blob URL
289
- format: 'woff2',
290
- weight: '100 900',
291
- style: 'normal',
292
- display: 'swap',
293
- }],
294
- },
295
- };
296
- ```
297
-
298
- Font storage, tenant authorization and URL creation remain host
299
- responsibilities. For deterministic opaque-iframe loading, a `data:font/...`
300
- URL is the simplest option. Do not place secrets in theme values, fonts, markup
301
- or CSS: they become part of the generated `srcdoc`.
302
-
303
- ## Custom elements
304
-
305
- The built-in elements are just pre-registered entries of an open **element
306
- registry** a consumer can register its own element types on the exact same
307
- contract via `config.elements` (or app-wide via `PAGE_ELEMENTS_KEY`). One
308
- registration serves palette, canvas preview, props panel and the runtime
309
- renderer; the value model (defaults, `required`, validation, action payloads)
310
- comes from the host for free. Element renderers wire their field state through
311
- `usePageElement()`.
312
-
313
- ```ts
314
- import { definePageElement, type PageConfig } from '@cocoar/vue-page-builder';
315
- import RatingRenderer from './RatingRenderer.vue';
316
- import RatingInspector from './RatingInspector.vue';
317
-
318
- const ratingElement = definePageElement<{ label: string; max: number }>({
319
- renderer: RatingRenderer, // receives { node }; field wiring via usePageElement()
320
- value: { isEmpty: (v) => !v || Number(v) === 0 }, // participates in the form value model
321
- builder: {
322
- label: { key: 'app.pb.rating', fallback: 'Rating' },
323
- icon: 'star',
324
- defaults: () => ({ label: 'Rating', max: 5 }),
325
- inspector: RatingInspector, // receives { node, patch }
326
- },
327
- });
328
-
329
- const config: PageConfig = {
330
- elements: { 'acme-rating': ratingElement }, // vendor-prefixed key
331
- allowedElements: ['stack', 'heading', 'text-input', 'button', 'acme-rating'],
332
- };
333
- ```
334
-
335
- ## Field contract
336
-
337
- Every element has one page-wide unique `name`, which is its exact Page-Code
338
- key (`elements.pageTitle`). For value elements, the same name is also the form
339
- and DTO property (`elements.username` and `fields.username`); there is no
340
- second field-name or identifier property.
341
-
342
- Pages are usually projections of a DTO — the value-element names and types are
343
- known up front. Declare them as `config.fields` and authors *pick* fields instead
344
- of inventing names: the props panel's Name becomes a select filtered to the
345
- value types each element can edit (`ElementValueSpec.types`; the rating above
346
- declares `types: ['number']` and shows up for number fields), the palette
347
- gains a draggable **Fields** group that drops pre-bound default elements, an
348
- **Element** select switches a bound field to another compatible
349
- representation, and the builder lint flags unknown names, incompatible
350
- bindings and missing required fields. Authoring-only: binding is plain
351
- `node.name`, so persisted schemas stay self-contained and render without the
352
- contract.
353
-
354
- ```ts
355
- const config: PageConfig = {
356
- fields: [
357
- { name: 'username', valueType: 'string', label: 'Username', required: true },
358
- { name: 'password', valueType: 'string', label: 'Password', required: true, defaultElement: 'password-input' },
359
- { name: 'rememberMe', valueType: 'boolean', label: 'Remember me' },
360
- { name: 'age', valueType: 'number', label: 'Age' },
361
- { name: 'dueUntil', valueType: 'date', label: 'Due until' },
362
- ],
363
- };
364
- ```
365
-
366
- ## Page-owned translations
367
-
368
- Human-readable element properties use stable translation keys instead of
369
- embedding one object per language into every node. The root owns the editable
370
- catalogue and Element Code keeps only a data-safe reference:
371
-
372
- ```jsonc
373
- {
374
- "type": "page",
375
- "translations": {
376
- "de": { "page.submit.label": "Anmelden" },
377
- "en": { "page.submit.label": "Sign in" }
378
- }
379
- }
380
- ```
381
-
382
- ```js
383
- element.props.label = i18n.text('page.submit.label', undefined, 'Sign in');
384
- ```
385
-
386
- The Builder's **Translations** tab edits the catalogue, reports missing/unused
387
- keys and shares its language with the preview. Localizability is explicit
388
- element metadata (`valueKind: 'localized-text'`), so layout strings such as
389
- `style.width` never accidentally get translation UI. Monaco completes the
390
- keys present in the page document. At runtime page messages win, then the
391
- host's `@cocoar/vue-localization` store is consulted, followed by the binding
392
- fallback and finally the key itself. The legacy `LocalizedValue` shape remains
393
- readable for existing documents, but new authoring uses translation bindings.
394
-
395
- ## JavaScript property bindings
396
-
397
- Bindable properties can retain a static fallback and opt into a pure
398
- JavaScript expression. In the right-hand Properties panel, a compact `fx`
399
- control in the property's label row switches modes without opening an editor
400
- (struck through = static, accent colour = expression). Its explicit Edit action
401
- opens the shared lazy Monaco dialog. Disabled expressions remain persisted with
402
- `enabled: false`, so switching modes never loses authored code. The optional
403
- Logic overview opens that same dialog and edits the same record:
404
-
405
- ```ts
406
- const submit = {
407
- id: 'submit',
408
- type: 'button',
409
- props: { label: 'Sign in', disabled: false },
410
- bindings: {
411
- disabled: {
412
- source: 'expression',
413
- expression: '!fields.username?.trim() || !fields.password',
414
- },
415
- },
416
- };
417
- ```
418
-
419
- The builder never evaluates this source. A host-owned sandbox session extracts
420
- definitions with `collectPageRuntimeExpressions()`, evaluates them, and passes
421
- the data-only result map to `CoarPageRenderer.expressionValues` (or
422
- `CoarPageBuilder.previewExpressionValues`). Static props remain active during
423
- startup and after runtime failures. Monaco is lazy-loaded in JavaScript mode;
424
- host field/context contracts provide its IntelliSense declarations.
425
-
426
- ## Browser Page Runtime
427
-
428
- The package contains the SES Worker runtime used by Page State, constrained Page
429
- Root Code and per-element code.
430
- Create the host once in the consumer application. It is a capability catalogue,
431
- not shared page state; every `usePageCodeRuntime()` call owns an isolated Worker
432
- session and disposes it with the Vue component.
433
-
434
- ```ts
435
- import {
436
- definePageRuntimeHost,
437
- withRuntimeEndowmentContext,
438
- } from '@cocoar/vue-page-builder';
439
-
440
- export const pageRuntimeHost = definePageRuntimeHost({
441
- endowments: {
442
- api: {
443
- loadOptions: withRuntimeEndowmentContext(
444
- ({ signal, tenantId }, source: unknown) =>
445
- applicationApi.loadOptions(String(source), { tenantId, signal }),
446
- ),
447
- },
448
- },
449
- grants: ({ pageId, definition }) =>
450
- pageId.startsWith('auth:') && definition.id.startsWith('element-action:')
451
- ? ['api']
452
- : [],
453
- });
454
- ```
455
-
456
- ```ts
457
- const runtime = usePageCodeRuntime({
458
- pageId,
459
- tenantId,
460
- schema,
461
- context,
462
- viewport,
463
- runtimeHost: pageRuntimeHost,
464
- });
465
- ```
466
-
467
- Pass `runtime.pageCodeValues` and `runtime.onRuntimeChange` to the renderer and
468
- route unknown action ids through `runtime.runPageAction`. If no host is passed,
469
- the package uses a no-capability host: there is no ambient `fetch`, `window`, DOM
470
- or application API inside tenant code.
471
-
472
- The consuming Vite build emits the SES runtime as a same-origin
473
- `pageScriptRuntime.worker-<hash>.js` module asset. Keep the document CSP free of
474
- `unsafe-eval`; see `IDP_INTEGRATION.md` for the Worker-response CSP requirement.
475
- For Vite development, exclude only the dedicated runtime entry from dependency
476
- pre-bundling:
477
-
478
- ```ts
479
- optimizeDeps: { exclude: ['@cocoar/vue-page-builder/runtime-worker'] }
480
- ```
481
-
482
- `CoarPageBuilder` owns its embedded preview runtime. A selected fixture now
483
- provides context, view state, locale and viewport as one effective preview
484
- contract; the Builder evaluates Page State, Page Root Code and Element Code in
485
- that same isolated session. Pass `previewRuntimeHost` only when preview actions
486
- need the application's explicitly granted capabilities.
487
-
488
- The package also exports four optional integration presets:
489
- `createAuthPageDocument()` and `createAuthPageConfig()` for `login`,
490
- `password-forgot`, `logout` and `consent`. They are example/default documents;
491
- all underlying elements, repeaters, feedback zones and runtime APIs remain
492
- generic.
493
-
494
- See [IDP_INTEGRATION.md](./IDP_INTEGRATION.md) for the complete draft/publish,
495
- host-action and security contract.
496
-
497
- ## Documentation
498
-
499
- Full docs — schema reference, `PageConfig` contract, element registry guide,
500
- security model, and an IDP integration walkthrough — at
501
- [docs.cocoar.dev/cocoar-ui-vue](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/):
502
-
503
- - [Overview & `PageConfig`](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/)
504
- - [`<CoarPageBuilder>`](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/coar-page-builder)
505
- - [`<CoarPageRenderer>`](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/coar-page-renderer)
506
-
507
- ## License
508
-
509
- Apache-2.0
1
+ # @cocoar/vue-page-builder
2
+
3
+ > **Preview.** This package is provisional. It shipped as GA in 2.17 by
4
+ > oversight; 3.0 puts it back under Preview until the authoring model settles.
5
+ > Public API, `PageConfig` and the document schema may still change in a minor
6
+ > release. Documents are safe — every schema change ships a migration that runs
7
+ > on ingest — so pin a version if you depend on the API.
8
+
9
+ A generic, headless visual page builder and renderer for Vue 3, built on the
10
+ Cocoar Design System. Users drag UI primitives onto a canvas, configure them,
11
+ and the result is a plain JSON schema (`PageNode`) that `<CoarPageRenderer>`
12
+ turns back into live Cocoar components.
13
+
14
+ Everything domain-specific — which actions a button may trigger, where images
15
+ come from, which elements are permitted, which element types even exist — is
16
+ defined by the **consumer application** through a single `PageConfig`, not by
17
+ the library. The renderer enforces `allowedElements` as a security boundary:
18
+ disallowed nodes are skipped at render time, even in hand-written or tampered
19
+ JSON.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pnpm add @cocoar/vue-page-builder @cocoar/vue-ui \
25
+ @cocoar/vue-localization @cocoar/vue-script-editor monaco-editor
26
+ ```
27
+
28
+ The UI, localization, ScriptEditor, Monaco and Vue packages are peers so the
29
+ host owns their single application-wide instances. Import the stylesheet once —
30
+ it carries the builder chrome **and** the renderer's layout styles:
31
+
32
+ ```ts
33
+ import '@cocoar/vue-page-builder/styles';
34
+ ```
35
+
36
+ The Builder uses Monaco in JavaScript and JSON mode. Register the TypeScript /
37
+ JavaScript and JSON workers before the first Builder mounts; the complete Vite
38
+ and SSR configurations are documented in
39
+ [IDP_INTEGRATION.md](./IDP_INTEGRATION.md#register-all-monaco-workers-used-by-the-builder).
40
+
41
+ ## Usage
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
+ } from '@cocoar/vue-page-builder';
52
+
53
+ const schema = ref<PageNode>();
54
+
55
+ const config: PageConfig = {
56
+ allowedElements: ['stack', 'card', 'heading', 'paragraph', 'text-input', 'button'],
57
+ availableActions: [{ id: 'auth:login', label: 'Sign in' }],
58
+ };
59
+ </script>
60
+
61
+ <template>
62
+ <!-- Visual editor (needs a bounded height) -->
63
+ <CoarPageBuilder v-model="schema" :config="config" style="height: 700px" />
64
+
65
+ <!-- Runtime renderer — same config = same boundary -->
66
+ <CoarPageRenderer
67
+ :schema="schema!"
68
+ :config="config"
69
+ :actions="{ 'auth:login': (values) => console.log(values) }"
70
+ />
71
+ </template>
72
+ ```
73
+
74
+ ## Schema (v5)
75
+
76
+ The persisted document is a tree of one uniform node grammar: `type` is an
77
+ open registry key (built-in or consumer), everything element-specific lives in
78
+ the `props` bag, and the host vocabulary — `id`, `style`, the value-model trio
79
+ `name` / `defaultValue` / `validation`, and `children` for containers — stays
80
+ at node level. The `page` root carries `schemaVersion: 6`. Version 4 gives every
81
+ element a stable, page-wide `name`; value elements use that same name as their
82
+ form/DTO property and Element Code uses it as its authoring identity. Version 5
83
+ adds builder-only origin metadata for reusable, versioned compositions. Version
84
+ 6 renames the repeat's `props.source` to `props.contextPath`, so `source` means
85
+ one thing everywhere.
86
+
87
+ ```jsonc
88
+ {
89
+ "id": "3f6c…",
90
+ "type": "page",
91
+ "schemaVersion": 6,
92
+ "style": { "gap": "16px", "padding": "24px" },
93
+ "children": [
94
+ { "id": "a1b2…", "type": "heading", "props": { "text": "Sign in", "level": 2 } },
95
+ {
96
+ "id": "c3d4…",
97
+ "type": "text-input",
98
+ "props": { "label": "Email", "inputType": "email" },
99
+ "name": "email",
100
+ "validation": { "required": true },
101
+ "style": { "size": "fill" }
102
+ },
103
+ {
104
+ "id": "e5f6…",
105
+ "type": "button",
106
+ "props": { "label": "Sign in", "action": "auth:login", "validates": true }
107
+ }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ Older documents are normalized transparently on every ingest path: pre-v2 flat
113
+ props move into the `props` bag, v3 runtime-composition documents keep their
114
+ meaning, and v4 deterministically adds missing element names. Unknown or
115
+ unregistered types stay losslessly in the tree (flagged in the builder, skipped
116
+ with a one-time warning at runtime).
117
+
118
+ ## Reusable compositions
119
+
120
+ Any non-page subtree can be stored and reused: a brand panel, header, form
121
+ section, footer, product card or a consumer element. Compositions are generic;
122
+ there are no auth-specific categories or runtime components.
123
+
124
+ The host owns persistence by implementing `PageCompositionRepository`. It may
125
+ use an API, IndexedDB or another store, and every method may be sync or async:
126
+
127
+ ```ts
128
+ import type { PageCompositionRepository } from '@cocoar/vue-page-builder';
129
+
130
+ const compositionRepository: PageCompositionRepository = {
131
+ list: () => api.get('/page-compositions'),
132
+ get: (id, version) => api.get(`/page-compositions/${id}/${version ?? 'latest'}`),
133
+ create: (input) => api.post('/page-compositions', input),
134
+ publish: (input) => api.post(`/page-compositions/${input.id}/versions`, input),
135
+ };
136
+ ```
137
+
138
+ Pass it only to the Builder:
139
+
140
+ ```vue
141
+ <CoarPageBuilder
142
+ v-model="authoringDocument"
143
+ :config="config"
144
+ :composition-repository="compositionRepository"
145
+ composition-management="consume"
146
+ @open-composition="({ id, version }) => openCompositionEditor(id, version)"
147
+ />
148
+ ```
149
+
150
+ `composition-management="consume"` is intended for a host with separate
151
+ **Pages** and **Compositions** areas. Page editors can insert an exact pinned
152
+ version, update it or detach it, but cannot create or publish definitions. The
153
+ host edits a definition as a standalone element subtree (usually inside a
154
+ temporary Page root) and calls `create()` / `publish()` itself. The default
155
+ `inline` mode additionally exposes create/publish in the Builder tab and is
156
+ useful for compact tools or bootstrapping a definition from an existing page.
157
+
158
+ Every repository summary is also exposed in the normal element palette under
159
+ **Compositions**. Dragging one to a valid tree/canvas drop zone loads its
160
+ displayed `latestVersion`, materializes the definition and pins that exact
161
+ version. Selecting the linked instance root exposes its name, pinned-version
162
+ selector, update, detach and open controls in Properties. `open-composition`
163
+ is the host navigation seam: the Builder emits the exact pinned `{ id,
164
+ version }`, while the host decides where its independent definition editor
165
+ lives. The existing Compositions tab remains the overview for references,
166
+ updates and repository problems.
167
+
168
+ Instances are fully materialized normal node trees. This gives drafts an
169
+ offline-safe snapshot and lets the Builder preserve page-local node ids and
170
+ public names across updates. Nested compositions keep a small origin chain so
171
+ each linked level can be updated or detached independently. Deleting an instance
172
+ from one page never deletes the repository definition or instances on other pages.
173
+
174
+ Before persisting a runtime document, remove authoring links deterministically:
175
+
176
+ ```ts
177
+ import { compilePageCompositions } from '@cocoar/vue-page-builder';
178
+
179
+ const runtimeDocument = compilePageCompositions(authoringDocument.value);
180
+ ```
181
+
182
+ The compiled result has no repository ids, versions or source-node metadata and
183
+ requires no composition repository at runtime. Use
184
+ `validatePageCompositionReferences(authoringDocument, repository)` before
185
+ publication to report missing versions and nested cycles. The package also
186
+ exports `createInMemoryPageCompositionRepository()` for tests and local demos.
187
+
188
+ ## Action payloads
189
+
190
+ Every action-capable element uses the same optional `ActionProps` contract:
191
+
192
+ ```ts
193
+ interface ActionProps {
194
+ action?: string;
195
+ actionValues?: Record<string, unknown>;
196
+ actionValueField?: string;
197
+ actionValue?: unknown;
198
+ }
199
+ ```
200
+
201
+ `actionValues` contains JSON-safe defaults. Every individual entry can be
202
+ switched to **fx** or bound through `bindings["actionValues.<key>"]` to a host
203
+ context value, customer Page State, form field, named Repeat selection, current
204
+ Repeat item/index, or a sandbox expression. `actionValue` /
205
+ `actionValueField` remain as the backwards-compatible single-dynamic-value
206
+ shape. The handler receives one snapshot with deterministic precedence:
207
+ **form values < resolved `actionValues` < dynamic `actionValue`**. Explicit
208
+ action arguments therefore win key collisions with form fields.
209
+
210
+ ```jsonc
211
+ {
212
+ "props": {
213
+ "action": "auth:consent-allow",
214
+ "actionValues": { "approvedScopes": [] }
215
+ },
216
+ "bindings": {
217
+ "actionValues.approvedScopes": {
218
+ "source": "selection",
219
+ "path": "approvedScopes"
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ The builder supplies this editor automatically to built-in buttons/links and
226
+ to consumer elements registered with `action: true`. A custom action renderer
227
+ should call `usePageElement().triggerElementAction(node.props)` so it follows
228
+ the identical merge, validation and async-action path.
229
+
230
+ ## Host themes
231
+
232
+ Runtime applications wrap the renderer in the generic `CoarThemeScope` from
233
+ `@cocoar/vue-ui`. For authoring, pass the same theme as `previewTheme`; it is
234
+ applied only to the preview canvas, never to the builder chrome.
235
+
236
+ ## Decorative visual markup
237
+
238
+ The built-in `visual-markup` element is the deliberately narrow escape hatch
239
+ for branded, animated decoration that cannot reasonably be expressed through
240
+ individual PageBuilder primitives. It renders HTML, inline SVG and local CSS in
241
+ an opaque-origin iframe. It is **not** a general custom-code element:
242
+
243
+ - the iframe has an empty `sandbox`, `aria-hidden="true"`, `tabindex="-1"` and
244
+ `pointer-events: none`;
245
+ - JavaScript, forms, links, navigation, network access, parent-DOM access and
246
+ interactive controls are rejected or blocked by CSP;
247
+ - markup and CSS use strict allowlists and hard size limits;
248
+ - invalid visuals are hidden locally; siblings and the page form keep working;
249
+ - Builder Preview and Runtime use the same renderer and security policy.
250
+
251
+ Functional content must remain native PageBuilder elements. A visual node can
252
+ use local `@keyframes`, transforms, media queries, `prefers-reduced-motion`, CSS
253
+ custom properties and inline SVG. Its outer PageBuilder `style` controls its
254
+ size, including responsive `width`, `height`, `minHeight`, `maxHeight`,
255
+ `aspectRatio` and `size`.
256
+
257
+ Select the node to edit its HTML and CSS source with the dedicated Monaco
258
+ inspectors. These source editors remain available in code-driven authoring
259
+ mode; ordinary computed properties still use Element Code and Quick
260
+ Properties. Invalid source is reported on the node and in the inspector.
261
+
262
+ The host may inject only explicitly approved theme values and font files:
263
+
264
+ ```ts
265
+ import type { PageConfig } from '@cocoar/vue-page-builder';
266
+
267
+ const config: PageConfig = {
268
+ allowedElements: ['page', 'row', 'column', 'visual-markup', 'text-input', 'button'],
269
+ visualMarkup: {
270
+ themeVariables: {
271
+ '--coar-accent': '#10b981',
272
+ '--visual-surface': '#ffffff',
273
+ '--visual-text': '#16202e',
274
+ },
275
+ fonts: [{
276
+ id: 'brand-variable',
277
+ family: 'Brand Sans Variable',
278
+ source: approvedFontDataUrl, // data:font/...;base64 or a host-created blob URL
279
+ format: 'woff2',
280
+ weight: '100 900',
281
+ style: 'normal',
282
+ display: 'swap',
283
+ }],
284
+ },
285
+ };
286
+ ```
287
+
288
+ Font storage, tenant authorization and URL creation remain host
289
+ responsibilities. For deterministic opaque-iframe loading, a `data:font/...`
290
+ URL is the simplest option. Do not place secrets in theme values, fonts, markup
291
+ or CSS: they become part of the generated `srcdoc`.
292
+
293
+ ## Custom elements
294
+
295
+ The built-in elements are just pre-registered entries of an open **element
296
+ registry** — a consumer can register its own element types on the exact same
297
+ contract via `config.elementTypes` (or app-wide via `PAGE_ELEMENT_TYPES_KEY`). One
298
+ registration serves palette, canvas preview, props panel and the runtime
299
+ renderer; the value model (defaults, `required`, validation, action payloads)
300
+ comes from the host for free. Element renderers wire their field state through
301
+ `usePageElement()`.
302
+
303
+ ```ts
304
+ import { definePageElement, type PageConfig } from '@cocoar/vue-page-builder';
305
+ import RatingRenderer from './RatingRenderer.vue';
306
+ import RatingInspector from './RatingInspector.vue';
307
+
308
+ const ratingElement = definePageElement<{ label: string; max: number }>({
309
+ renderer: RatingRenderer, // receives { node }; field wiring via usePageElement()
310
+ value: { isEmpty: (v) => !v || Number(v) === 0 }, // participates in the form value model
311
+ builder: {
312
+ label: { key: 'app.pb.rating', fallback: 'Rating' },
313
+ icon: 'star',
314
+ defaults: () => ({ label: 'Rating', max: 5 }),
315
+ inspector: RatingInspector, // receives { node, patch }
316
+ },
317
+ });
318
+
319
+ const config: PageConfig = {
320
+ elements: { 'acme-rating': ratingElement }, // vendor-prefixed key
321
+ allowedElements: ['stack', 'heading', 'text-input', 'button', 'acme-rating'],
322
+ };
323
+ ```
324
+
325
+ ## Field contract
326
+
327
+ Every element has one page-wide unique `name`, which is its exact Page-Code
328
+ key (`elements.pageTitle`). For value elements, the same name is also the form
329
+ and DTO property (`elements.username` and `fields.username`); there is no
330
+ second field-name or identifier property.
331
+
332
+ Pages are usually projections of a DTO — the value-element names and types are
333
+ known up front. Declare them as `config.dataContract` and authors *pick* fields instead
334
+ of inventing names: the props panel's Name becomes a select filtered to the
335
+ value types each element can edit (`ElementValueSpec.types`; the rating above
336
+ declares `types: ['number']` and shows up for number fields), the palette
337
+ gains a draggable **Fields** group that drops pre-bound default elements, an
338
+ **Element** select switches a bound field to another compatible
339
+ representation, and the builder lint flags unknown names, incompatible
340
+ bindings and missing required fields. Authoring-only: binding is plain
341
+ `node.name`, so persisted schemas stay self-contained and render without the
342
+ contract.
343
+
344
+ ```ts
345
+ const config: PageConfig = {
346
+ fields: [
347
+ { name: 'username', valueType: 'string', label: 'Username', required: true },
348
+ { name: 'password', valueType: 'string', label: 'Password', required: true, defaultElement: 'password-input' },
349
+ { name: 'rememberMe', valueType: 'boolean', label: 'Remember me' },
350
+ { name: 'age', valueType: 'number', label: 'Age' },
351
+ { name: 'dueUntil', valueType: 'date', label: 'Due until' },
352
+ ],
353
+ };
354
+ ```
355
+
356
+ ## Page-owned translations
357
+
358
+ Human-readable element properties use stable translation keys instead of
359
+ embedding one object per language into every node. The root owns the editable
360
+ catalogue and Element Code keeps only a data-safe reference:
361
+
362
+ ```jsonc
363
+ {
364
+ "type": "page",
365
+ "translations": {
366
+ "de": { "page.submit.label": "Anmelden" },
367
+ "en": { "page.submit.label": "Sign in" }
368
+ }
369
+ }
370
+ ```
371
+
372
+ ```js
373
+ element.props.label = i18n.text('page.submit.label', undefined, 'Sign in');
374
+ ```
375
+
376
+ The Builder's **Translations** tab edits the catalogue, reports missing/unused
377
+ keys and shares its language with the preview. Localizability is explicit
378
+ element metadata (`valueKind: 'localized-text'`), so layout strings such as
379
+ `style.width` never accidentally get translation UI. Monaco completes the
380
+ keys present in the page document. At runtime page messages win, then the
381
+ host's `@cocoar/vue-localization` store is consulted, followed by the binding
382
+ fallback and finally the key itself. The legacy `LocalizedValue` shape remains
383
+ readable for existing documents, but new authoring uses translation bindings.
384
+
385
+ ## JavaScript property bindings
386
+
387
+ Bindable properties can retain a static fallback and opt into a pure
388
+ JavaScript expression. In the right-hand Properties panel, a compact `fx`
389
+ control in the property's label row switches modes without opening an editor
390
+ (struck through = static, accent colour = expression). Its explicit Edit action
391
+ opens the shared lazy Monaco dialog. Disabled expressions remain persisted with
392
+ `enabled: false`, so switching modes never loses authored code. The optional
393
+ Logic overview opens that same dialog and edits the same record:
394
+
395
+ ```ts
396
+ const submit = {
397
+ id: 'submit',
398
+ type: 'button',
399
+ props: { label: 'Sign in', disabled: false },
400
+ bindings: {
401
+ disabled: {
402
+ source: 'expression',
403
+ expression: '!fields.username?.trim() || !fields.password',
404
+ },
405
+ },
406
+ };
407
+ ```
408
+
409
+ The builder never evaluates this source. A host-owned sandbox session extracts
410
+ definitions with `collectPageRuntimeExpressions()`, evaluates them, and passes
411
+ the data-only result map to `CoarPageRenderer.expressionValues` (or
412
+ `CoarPageBuilder.previewExpressionValues`). Static props remain active during
413
+ startup and after runtime failures. Monaco is lazy-loaded in JavaScript mode;
414
+ host field/context contracts provide its IntelliSense declarations.
415
+
416
+ ## Browser Page Runtime
417
+
418
+ The package contains the SES Worker runtime used by Page State, constrained Page
419
+ Root Code and per-element code.
420
+ Create the host once in the consumer application. It is a capability catalogue,
421
+ not shared page state; every `usePageCodeRuntime()` call owns an isolated Worker
422
+ session and disposes it with the Vue component.
423
+
424
+ ```ts
425
+ import {
426
+ definePageRuntimeHost,
427
+ withRuntimeEndowmentContext,
428
+ } from '@cocoar/vue-page-builder';
429
+
430
+ export const pageRuntimeHost = definePageRuntimeHost({
431
+ endowments: {
432
+ api: {
433
+ loadOptions: withRuntimeEndowmentContext(
434
+ ({ signal, tenantId }, source: unknown) =>
435
+ applicationApi.loadOptions(String(source), { tenantId, signal }),
436
+ ),
437
+ },
438
+ },
439
+ grants: ({ pageId, definition }) =>
440
+ pageId.startsWith('auth:') && definition.id.startsWith('element-action:')
441
+ ? ['api']
442
+ : [],
443
+ });
444
+ ```
445
+
446
+ ```ts
447
+ const runtime = usePageCodeRuntime({
448
+ pageId,
449
+ tenantId,
450
+ schema,
451
+ context,
452
+ viewport,
453
+ runtimeHost: pageRuntimeHost,
454
+ });
455
+ ```
456
+
457
+ Pass `runtime.pageCodeValues` and `runtime.onRuntimeChange` to the renderer and
458
+ route unknown action ids through `runtime.runPageAction`. If no host is passed,
459
+ the package uses a no-capability host: there is no ambient `fetch`, `window`, DOM
460
+ or application API inside tenant code.
461
+
462
+ The consuming Vite build emits the SES runtime as a same-origin
463
+ `pageScriptRuntime.worker-<hash>.js` module asset. Keep the document CSP free of
464
+ `unsafe-eval`; see `IDP_INTEGRATION.md` for the Worker-response CSP requirement.
465
+ For Vite development, exclude only the dedicated runtime entry from dependency
466
+ pre-bundling:
467
+
468
+ ```ts
469
+ optimizeDeps: { exclude: ['@cocoar/vue-page-builder/runtime-worker'] }
470
+ ```
471
+
472
+ `CoarPageBuilder` owns its embedded preview runtime. The host supplies its
473
+ inputs — `previewContext`, `previewInitialValues`, `previewLocale` and the
474
+ Builder evaluates Page State, Page Root Code and Element Code in one isolated
475
+ session against exactly those. Pass `previewRuntimeHost` only when preview actions
476
+ need the application's explicitly granted capabilities.
477
+
478
+ The package ships no auth-specific configuration or documents. An IDP owns its
479
+ own `PageConfig` and starting documents for `login`, `password-forgot`, `logout`
480
+ and `consent`; every element, repeater, feedback zone and runtime API stays
481
+ generic.
482
+
483
+ See [IDP_INTEGRATION.md](./IDP_INTEGRATION.md) for the complete draft/publish,
484
+ host-action and security contract.
485
+
486
+ ## Documentation
487
+
488
+ Full docs schema reference, `PageConfig` contract, element registry guide,
489
+ security model, and an IDP integration walkthrough — at
490
+ [docs.cocoar.dev/cocoar-ui-vue](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/):
491
+
492
+ - [Overview & `PageConfig`](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/)
493
+ - [`<CoarPageBuilder>`](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/coar-page-builder)
494
+ - [`<CoarPageRenderer>`](https://docs.cocoar.dev/cocoar-ui-vue/components/page-builder/coar-page-renderer)
495
+
496
+ ## License
497
+
498
+ Apache-2.0