diff --git a/openspec/changes/openbuilt-templates-marketplace/.openspec.yaml b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/.openspec.yaml
similarity index 100%
rename from openspec/changes/openbuilt-templates-marketplace/.openspec.yaml
rename to openspec/changes/archive/2026-05-12-openbuilt-page-editor/.openspec.yaml
diff --git a/openspec/changes/archive/2026-05-12-openbuilt-page-editor/design.md b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/design.md
new file mode 100644
index 00000000..1bc868f2
--- /dev/null
+++ b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/design.md
@@ -0,0 +1,432 @@
+## Context
+
+OpenBuilt spec #1 (`bootstrap-openbuilt`) shipped a JSON textarea as
+the only path to author a virtual app's manifest. The textarea
+proves the runtime contract (load → validate → save → render via a
+nested `CnAppRoot`) but is unusable for citizen developers — the
+canonical `@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json`
+(v1.4.0) is a 1500+ line OpenAPI document with a closed 9-page-type
+enum, seven `$defs` for the recurring sub-shapes
+(`column` / `action` / `widgetDef` / `layoutItem` / `formField` /
+`sidebarSection` / `sidebarTab`), and per-type config sub-shapes
+that vary substantially (an `index` page's `config` looks nothing
+like a `chat` page's).
+
+This spec replaces the textarea-as-only-editor with a tabbed view
+whose default tab is a visual designer. The textarea persists as the
+"Raw JSON" fallback tab for integrators and for the corner cases the
+visual designer cannot express (yet). Everything ships in the
+existing `openbuilt` Nextcloud app's frontend — no new backend code,
+no new schemas, no new register namespaces. Manifest CRUD continues
+to flow through OR REST per ADR-022.
+
+The chain dependency on `nextcloud-vue-in-memory-manifest` (chain
+spec #2) shapes the design of the live-preview pane: that spec adds
+the `useAppManifest(appId, manifestObject)` in-memory overload the
+preview pane mounts against. Spec #2 is parallel to this one in the
+9-spec chain; the design assumes it MAY not be shipped when this
+editor lands, and provides a degraded "save & reload" preview
+fallback.
+
+## Goals / Non-Goals
+
+**Goals**
+
+- Replace the spec #1 textarea-as-only-editor with a visual designer
+ that authors every shape declared in the canonical manifest
+ schema's closed 9-page-type enum.
+- Keep the textarea reachable as a "Raw JSON" fallback tab so power
+ users and integrators are not regressed.
+- Validate-as-you-type via `validateManifest`, surfacing errors both
+ in a side panel and as inline marks on the offending field.
+- Provide a live-preview pane when the in-memory manifest loader from
+ chain spec #2 is available; gracefully degrade to "save & reload"
+ when it is not.
+- Stay strictly inside the canonical schema. The editor MUST NOT
+ emit shapes outside the schema's enum/closed-set boundaries, and
+ it MUST round-trip externally authored manifests losslessly.
+- Stay strictly inside ADR-022 — no per-app REST wrappers, no
+ controller additions; the designer reads/writes Application
+ objects via OR REST.
+
+**Non-Goals (deferred to chain or out of scope)**
+
+- Versioning / draft / publish UX (chain spec #6 `openbuilt-versioning`).
+- Per-built-app permission management surface (chain spec #7
+ `openbuilt-rbac`).
+- Starter-template gallery (chain spec #8
+ `openbuilt-templates-marketplace`).
+- Real-app export of the manifest to a `src/manifest.json` file in a
+ target Nextcloud-app repo (chain spec #9
+ `openbuilt-export-to-real-app`).
+- Editing the underlying schemas a page binds to (that is the
+ `openbuilt-schema-editor` spec #4 — this spec only *picks* from
+ the registers/schemas OR already exposes).
+- Real-time multi-user collaborative editing (see Open Questions).
+- Undo / redo within a single editing session (see Open Questions).
+- A `customComponents` registry-management surface — the
+ custom-page sub-editor picks from whatever registry the running
+ `CnAppRoot` exposes, but it does not let the user *create* new
+ registry entries.
+
+## Decisions
+
+### Decision 1 — One Vue component per page type (vs polymorphic sub-editor)
+
+The canonical schema declares nine page types and their `config`
+sub-shapes diverge sharply (an `index` page's `config` shape is
+register/schema/columns/actions; a `chat` page's is
+`conversationSource`/`postUrl`/`schema`; a `dashboard` page's is
+`widgets`/`layout`). Reviewing these shapes side-by-side, **one
+component per type is the right unit of decomposition**: the schema
+itself partitions cleanly by type, and any "polymorphic" sub-editor
+ends up being nine `v-if` branches stitched together inside one
+file — the same component count without the file boundaries.
+
+The decomposition is exactly:
+
+| Page type | Component |
+|-------------|----------------------------|
+| `index` | `IndexPageEditor.vue` |
+| `detail` | `DetailPageEditor.vue` |
+| `dashboard` | `DashboardPageEditor.vue` |
+| `logs` | `LogsPageEditor.vue` |
+| `settings` | `SettingsPageEditor.vue` |
+| `chat` | `ChatPageEditor.vue` |
+| `files` | `FilesPageEditor.vue` |
+| `form` | `FormPageEditor.vue` |
+| `custom` | `CustomPageEditor.vue` |
+
+Each sub-editor receives `v-model="page.config"` (the in-flight
+config block for the page) and emits `update:modelValue` events with
+the new shape. Shared `$def` sub-shapes (columns / actions / widgets
+/ layout items / form fields / sidebar sections / sidebar tabs) live
+in a separate `src/components/page-editor/fields/` directory and
+mount inside whichever sub-editors need them — `ColumnBuilder.vue`
+mounts inside `IndexPageEditor.vue` and `LogsPageEditor.vue`,
+`FormFieldBuilder.vue` mounts inside `FormPageEditor.vue` and
+`SettingsPageEditor.vue` (the settings page's
+`sections[].fields[]` reuses the same `formField` `$def`), and so
+on.
+
+**Alternatives considered**
+
+- *One polymorphic `PageConfigEditor.vue` with type-keyed branches*.
+ Rejected. The branches are large enough that a single file
+ becomes hard to navigate, code reviews lose their per-type focus,
+ and adding a tenth page type (the canonical schema is extensible
+ via the `customComponents` registry, but new built-in types would
+ land via a future schema bump) means surgery in a hot file rather
+ than a new file.
+- *Code-generate the sub-editors from the JSON schema*. Tempting,
+ but the canonical schema's `pages[].config` description block is
+ a free-text discriminator (one giant description string covering
+ all nine types) rather than a typed `oneOf` — code-generation
+ would need a separate machine-readable mapping table that is
+ itself bespoke. Defer to a future spec if the schema is
+ refactored into a clean `oneOf`.
+
+### Decision 2 — Drag-drop library reuse over hand-rolled
+
+The menu-tree editor and the page-list editor both need
+drag-reorder. `@nextcloud/vue` re-exports `vue-draggable` /
+`vuedraggable` as part of its component set (used by
+`NcAppNavigation` internally). The decision is: **reuse whatever
+`@conduction/nextcloud-vue` and `@nextcloud/vue` pull in
+transitively** before adding a direct dependency.
+
+The apply step's first task is `npm ls vuedraggable` to determine
+the current dependency state. If it is present transitively, the
+editor imports from `vuedraggable` directly (the transitive copy
+serves both the library wrappers and our direct use). If it is
+absent, we add it as a direct devDep — the package is small (~10kb
+minified) and stable. We do **not** hand-roll drag-drop: every
+hand-rolled DnD implementation in our codebase to date has accreted
+edge cases (touch support, autoscroll, keyboard a11y).
+
+For the menu tree, the two-level nesting constraint (top-level +
+children, no third level) is enforced at the editor layer, not at
+the drag-drop library layer — `vuedraggable` happily supports
+arbitrary depth, but the canonical schema's `menu[].children[]`
+shape has no further `children[]`, so we cap at depth two by
+refusing to render a third-level drop zone.
+
+**Alternatives considered**
+
+- *Hand-rolled HTML5 DnD wrappers*. Rejected per the accretion
+ argument above. We get a11y, keyboard handling, and autoscroll
+ for free from `vuedraggable`.
+- *`@nextcloud/vue`'s `NcAppNavigation` drag wrappers*. Rejected for
+ the menu/page editor surface: those wrappers are designed for
+ navigation entries with specific NC styling expectations; using
+ them for a builder canvas fights their assumptions. Reuse the
+ underlying `vuedraggable` directly instead.
+
+### Decision 3 — Live preview depends on chain spec #2; degrade gracefully
+
+The right-hand pane is a **sandboxed `CnAppRoot`** mount that
+renders the in-flight (unsaved) manifest live as the user edits.
+This is the user-facing payoff of the visual editor — without it,
+the designer feels like editing forms, not building an app.
+
+The sandbox requires the in-memory `useAppManifest(appId,
+manifestObject)` overload that **chain spec #2
+(`nextcloud-vue-in-memory-manifest`) ships in the `@conduction/nextcloud-vue`
+library repo**. Spec #2 runs parallel to this spec in the 9-spec
+chain (per ADR-032 and the `chain:` block in proposal.md), so this
+spec MAY ship before spec #2.
+
+**Behaviour when spec #2 is available:**
+The right-hand pane mounts ``. Each manifest
+edit re-renders the preview after the debounced validator pass
+clears (no flickering on invalid in-flight states). The sandbox
+`appId = openbuilt-preview-{slug}` so it does not collide with the
+production-mounted `openbuilt-{slug}`.
+
+**Behaviour when spec #2 is unavailable:**
+`useLivePreview.js` feature-detects the overload by inspecting
+`useAppManifest.length` (arity: spec #2 adds a second positional
+parameter). When the overload is missing, the right-hand pane
+collapses to a button that (a) saves the current manifest via the
+spec-1 REST path and (b) opens `/builder/:slug` in a new browser
+tab. An i18n note
+(`openbuilt.page-designer.preview.unavailable`) explains the
+limitation. The fallback is intentionally one composable deep so
+the rewrite when spec #2 lands is small.
+
+**Alternatives considered**
+
+- *Block this spec on spec #2 landing first*. Rejected. Sequential
+ chains pay coordination costs without yielding architectural
+ benefit; spec #5 can ship usable without spec #2 because the
+ designer itself works without preview — preview is a productivity
+ enhancement, not a correctness requirement.
+- *Mount the sandbox via the per-slug manifest endpoint workaround
+ spec #1 uses*. Rejected: that workaround requires a saved
+ manifest in OR, which defeats the entire purpose of a live
+ preview (rendering UNSAVED edits). The in-memory overload is the
+ only viable path for live preview.
+
+### Decision 4 — Validation surface: side panel plus inline marks, debounced 300ms
+
+The designer runs `validateManifest` against the in-flight manifest
+on every edit, debounced to at most once every 300ms. Errors
+surface twice: in the right-hand error-list side panel (a
+collapsible band when the live-preview pane occupies the right
+column instead), and as inline marks on the specific editor field
+whose JSON path matches the error path.
+
+Path mapping leans on `validateManifest`'s structured error output —
+each error carries a JSON Pointer (`/pages/1/config/columns/0`)
+that the editor's path-to-field map resolves to the offending Vue
+component (the page-list editor's second row, the index sub-editor's
+first column row). The map lives in
+`useManifestValidator.js` as a registered set of path-prefix →
+field-component-ref entries; sub-editors register their fields on
+mount and unregister on unmount.
+
+300ms was chosen because (a) `validateManifest` on a typical
+1-2KB manifest completes in ~5-20ms in dev measurements — fast
+enough that "live" is the right framing — and (b) anything tighter
+than 300ms surfaces transient errors in the middle of multi-character
+edits (e.g. typing "submitMethod" briefly flags "submitMetho" as
+invalid). 300ms is also the Vue community default for input
+debouncing.
+
+The validator MUST NOT block the editor: the run happens
+asynchronously and the UI stays responsive. The composable
+internally uses a worker-free `setTimeout` debouncer; if profiling
+reveals validator cost on a large manifest (>10KB) it MAY be moved
+to a Web Worker in a follow-on spec, but for the v1 surface area
+(~1-5KB manifests) the synchronous path is adequate.
+
+**Alternatives considered**
+
+- *Validate only on save*. Rejected: defers feedback to the moment
+ the user thinks they are done, which is the worst time. The
+ whole point of the visual designer is real-time validity
+ feedback.
+- *Validate on blur*. Rejected: half of the editor's surface is
+ drag-drop / select / pick, which don't have a meaningful blur
+ event; "as you type" + debounce covers both keyboard and
+ drag-driven edits uniformly.
+
+### Decision 5 — Custom-page handling defers customComponents registry management
+
+The canonical schema's `type: custom` page renders against a key in
+the consuming app's `customComponents` registry. The registry is
+**not** OpenBuilt's concern in this spec — registry composition is
+how a builder configures *what* custom components a virtual app has
+access to, and configuring that registry needs its own surface
+(deferred to a follow-on spec; the candidates are a new
+`openbuilt-custom-components-registry` capability or folding it into
+the marketplace spec #8).
+
+For this spec, `CustomPageEditor.vue` reads the registry **only at
+runtime** from the sandboxed `CnAppRoot`'s injected `customComponents`
+prop (which today is whatever the OpenBuilt host app passes —
+typically an empty `{}` until a future spec wires in component
+discovery). When the live-preview pane is active, the picker is a
+dropdown over the registry's keys. When the preview pane is
+unavailable, the picker degrades to free text with a warning that
+the value cannot be validated until preview is enabled.
+
+The free-form `config` block for a custom page is an embedded
+`` JSON editor (a single-purpose mini-textarea) because
+the canonical schema explicitly allows custom pages' `config` to be
+"any shape the custom component expects" — we cannot structure-edit
+something we don't know the shape of. This is the one part of the
+designer that mirrors the Raw JSON tab.
+
+**Alternatives considered**
+
+- *Ship registry management in this spec*. Rejected. Registry
+ composition is a sibling concern to manifest authoring, not a
+ child concern — folding it in would balloon the spec scope and
+ blur the line between "what does my app look like" (this spec)
+ and "what custom components does my app have access to" (a
+ future spec).
+- *Hide the custom page type entirely from the page-type picker
+ until a registry exists*. Rejected. The closed enum is closed —
+ hiding a type would either lie about the canonical schema or
+ break externally authored manifests that already use `type:
+ custom`. Better to keep the type visible with a degraded picker.
+
+### Declarative-vs-imperative call-out (ADR-031)
+
+The Page Designer's output **is** the manifest blob. The manifest
+itself is the canonical declarative artefact for the OpenBuilt
+ecosystem (it declares pages, menus, routes, sidebars, widgets,
+form fields — *what* the app is, not *how* it runs). This spec
+introduces **no service class**: no `PageBuilderService`, no
+`ManifestComposerService`, no `PageTypeRegistry`. The
+per-page-type sub-editors are dumb-form components that
+read/write the matching `pages[].config` sub-shape — they have no
+internal state machine, no derived behaviour, nothing that would
+trigger ADR-031's anti-pattern test.
+
+The closest the editor comes to "logic" is `useManifestValidator.js`
+(debounced wrapper around the library's `validateManifest`) and
+`useLivePreview.js` (feature-detect + sandbox mount). Both are
+composables-of-glue, not domain logic. Neither encodes a state
+machine, aggregation, calculation, or notification. The save path
+is a single PUT to OR's REST endpoint, mediated by the existing
+spec-1 Pinia store action — no new service.
+
+If during apply a future iteration finds itself reaching for a
+"build the manifest from scratch" or "auto-fix validation errors"
+behaviour, that behaviour SHALL land as a declarative manifest
+**template** or **transform** declared as data, not as a PHP/JS
+service class — and SHALL be deferred to a separate spec for
+review under ADR-031 in isolation.
+
+## Risks / Trade-offs
+
+- **Risk** — *Editor drifts from the canonical schema as v1.5.x →
+ v1.6.x lands in `@conduction/nextcloud-vue`.* → Mitigation: each
+ sub-editor's allowed input shapes are checked against
+ `validateManifest` on every keystroke — a schema bump that
+ invalidates an editor field surfaces immediately as a validation
+ error, prompting an editor patch. The editor pins to the schema
+ version shipped by the `@conduction/nextcloud-vue` version listed
+ in `package.json`; on a library bump, run the
+ `hello-world`/decidesk reference manifests through the editor's
+ round-trip test (load → re-serialise → diff) and patch any
+ divergence.
+- **Risk** — *Round-trip-losslessness regressions on externally
+ authored manifests.* → Mitigation: every sub-editor MUST keep
+ unknown fields it does not understand (the canonical schema
+ declares `additionalProperties: true` on the outer `config` block
+ for per-type scalars and consumer-app extension keys). The editor
+ stores the unmodified Application object in the Pinia store and
+ surgical-merges its UI-controlled fields back on save, rather
+ than re-serialising the whole shape from UI state. Tested by
+ the `manifest-roundtrip.spec.js` Vitest suite (load decidesk's
+ `src/manifest.json` → mount editor → re-serialise → assert
+ bytewise equivalence ignoring whitespace).
+- **Risk** — *Live-preview pane re-mounts thrash on rapid edits.* →
+ Mitigation: the preview's `:key` binds to a content hash, not a
+ timestamp; identical-content edits do not re-mount. The
+ debounced validator gates the preview-mount path so invalid
+ in-flight states never trigger a sandbox render.
+- **Risk** — *Custom-page free-form JSON editor regresses into a
+ second textarea.* → Acceptable trade-off for v1: the canonical
+ schema allows any shape for custom `config`, and we don't know
+ the consuming component's expectations. When `customComponents`
+ registry management ships (deferred spec), the registry can
+ declare each component's expected `config` shape and the editor
+ can structure-edit that block too.
+- **Trade-off** — *Nine sub-editor files vs one polymorphic file.*
+ See Decision 1. Nine files is the right unit.
+- **Trade-off** — *Live preview depends on chain spec #2.* See
+ Decision 3. The fallback path keeps this spec independently
+ shippable.
+
+## Migration Plan
+
+This spec ships a frontend-only change inside the existing
+`openbuilt` Nextcloud app. There is no database migration, no
+schema change, no API change. Deployment steps:
+
+1. Land the change on a feature branch from `development`.
+2. CI runs the existing Newman suite (unchanged — no API surface
+ change) plus the new Vitest suite for the designer components
+ and a Playwright test that walks the seeded `hello-world`
+ Application through the visual designer end-to-end.
+3. Merge into `development`. On next deploy, the Application
+ editor view opens with the Design tab as default; existing
+ manifests load and render in the designer; the Raw JSON tab
+ surfaces the unchanged spec-1 textarea.
+4. **Rollback** — front-end rollback only; redeploy the previous
+ build. Application objects in OR are unchanged by this spec, so
+ there is no data to revert.
+5. **Chain coordination** — when chain spec #2 lands in
+ `@conduction/nextcloud-vue`, bump the library version in
+ `package.json` and verify the live-preview pane activates. No
+ editor code change should be required because the feature
+ detection is at runtime.
+
+## Open Questions
+
+- **OQ-1 — Undo/redo within a single editing session.** The visual
+ designer is a many-knob surface; users will mis-click. Do we
+ ship a built-in undo/redo stack (typically a `useUndoable.js`
+ composable over the Pinia store) in this spec, or defer to a
+ follow-on? *Provisional decision*: **defer**. The user can
+ always switch to the Raw JSON tab and fix mistakes there, and
+ the save-after-validate flow prevents persisting broken
+ manifests. Adding undo here doubles the scope of the spec for a
+ productivity feature that doesn't affect correctness. File a
+ follow-on issue.
+- **OQ-2 — Real-time multi-user editing.** Two users editing the
+ same Application's manifest concurrently could clobber each
+ other's edits on save. Do we add optimistic concurrency control
+ (an ETag header on the PUT) or accept last-write-wins for v1?
+ *Provisional decision*: **defer to chain spec #6 (versioning)**.
+ Versioning will introduce explicit version snapshots which
+ naturally surface concurrency conflicts as "your edit is based
+ on version N but the current is N+1". Until versioning ships,
+ the textarea-from-spec-1 has the same last-write-wins behaviour
+ and no user has hit it; the visual designer does not change the
+ risk profile.
+- **OQ-3 — Embedded i18n key picker vs free-text label binding.**
+ Every editor field that binds an i18n key (menu label, page
+ title, button label) currently accepts a free-text string the
+ user must type. Should the editor pick from the consuming app's
+ registered i18n key set (read at runtime from `t()` / `n()` /
+ the i18n catalogue)? *Provisional decision*: **defer to a
+ follow-on**. For v1 the editor accepts free text and surfaces a
+ warning when the saved key does not resolve in the running
+ catalogue; a registry-backed picker is a fit-and-finish
+ improvement that needs its own design pass on how to expose the
+ catalogue to the editor without coupling OpenBuilt to every
+ consuming app's i18n loader.
+- **OQ-4 — Default page type on "Add page".** When the user clicks
+ "Add page", do we pre-select a page type (e.g. `index`) or
+ force a pick before any other field is shown? *Provisional
+ decision*: **force a pick** — the per-type sub-editor mounts as
+ soon as the type is chosen and the page row's other fields make
+ sense only in the context of the type. Forcing the pick keeps
+ the flow consistent and avoids the "create-then-discard" pattern
+ if the user actually wanted a different type.
diff --git a/openspec/changes/archive/2026-05-12-openbuilt-page-editor/proposal.md b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/proposal.md
new file mode 100644
index 00000000..edaed5af
--- /dev/null
+++ b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/proposal.md
@@ -0,0 +1,151 @@
+---
+kind: code
+depends_on: [bootstrap-openbuilt]
+chain:
+ - bootstrap-openbuilt
+ - openbuilt-page-editor # THIS spec
+---
+
+## Why
+
+Spec #1 (`bootstrap-openbuilt`) shipped a textarea-based JSON manifest
+editor as the integrator-only entry point for authoring a virtual
+app. That textarea proves the runtime contract but is unusable for
+the citizen-developer audience OpenBuilt actually targets: hand-typing
+a manifest means knowing the canonical
+`@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json` (v1.4.0)
+by heart, including its closed 9-type page enum, the per-type config
+sub-shapes, the menu/permission/route grammar, and the `$ref`'d
+`column` / `action` / `widgetDef` / `layoutItem` / `formField` /
+`sidebarSection` / `sidebarTab` sub-shapes. A visual designer is the
+missing piece between "we can store and render a manifest" and "a
+non-technical user can build one".
+
+This is spec #5 of the 9-spec OpenBuilt chain. It is **purely a
+frontend code change** inside the existing `openbuilt` Nextcloud app —
+no new schemas, no new backend controllers, no new register
+namespaces. The editor reads/writes the same `Application.manifest`
+JSON blob the textarea already reads/writes (via OR REST per ADR-022),
+just through a graphical UI instead of a raw text area.
+
+## What Changes
+
+- **NEW** `src/views/PageDesigner.vue` — top-level visual manifest
+ designer view, routed at `/applications/:slug/design`. Split into
+ three panes: a left **page list + menu tree editor**, a centre
+ **per-page-type config sub-editor** (one component per page type),
+ and an optional right **live preview pane** (mounts a sandboxed
+ `CnAppRoot` against the in-flight manifest when the in-memory
+ manifest loader from chain spec #2 is available; otherwise falls
+ back to a "save & reload" preview).
+- **NEW** per-page-type sub-editor components under
+ `src/components/page-editor/` — one Vue component for each of the
+ nine canonical page types declared by ADR-024:
+ - `IndexPageEditor.vue` — register + schema picker, column
+ selector (with `@self.*` metadata options), action declarations,
+ sidebar/card-component config.
+ - `DetailPageEditor.vue` — sidebar tab config (open-enum tab
+ builder), route param schema, sidebar widget list.
+ - `DashboardPageEditor.vue` — widget list + grid layout editor.
+ - `LogsPageEditor.vue` — register/schema or `source` picker +
+ columns.
+ - `SettingsPageEditor.vue` — section list builder (fields vs
+ component vs widgets exactly-one-of).
+ - `ChatPageEditor.vue` — `conversationSource` or `postUrl` picker
+ + optional `schema`.
+ - `FilesPageEditor.vue` — folder picker + allowed-types selector.
+ - `FormPageEditor.vue` — field list (reuses the `formField`
+ builder), validation rules, `submitHandler` vs `submitEndpoint`
+ exactly-one-of, method/mode pickers.
+ - `CustomPageEditor.vue` — `customComponents` registry-name picker
+ (read from the running `CnAppRoot`'s injected registry) + free-form
+ slot/config JSON for shapes the registry component expects.
+- **NEW** `src/components/page-editor/MenuTreeEditor.vue` — drag-reorder
+ menu builder with two-level nesting (top-level entries + their
+ `children[]`), `target: main | settings` picker, i18n-key label
+ binding, `permission` and `action` field support.
+- **NEW** `src/components/page-editor/PageListEditor.vue` —
+ page-list view with add/remove, drag-reorder, uniqueness
+ enforcement on `id`, and route-pattern validation.
+- **NEW** shared field-builder components under
+ `src/components/page-editor/fields/` (`ColumnBuilder.vue`,
+ `ActionBuilder.vue`, `WidgetBuilder.vue`, `LayoutItemBuilder.vue`,
+ `FormFieldBuilder.vue`, `SidebarSectionBuilder.vue`,
+ `SidebarTabBuilder.vue`) that match the canonical `$defs` and are
+ reused across the per-page-type sub-editors.
+- **NEW** `src/composables/useManifestValidator.js` — debounced
+ wrapper around `validateManifest` from
+ `@conduction/nextcloud-vue` that surfaces errors in the side-panel
+ error list and decorates each editor pane with inline marks
+ pointing at the offending requirement.
+- **NEW** `src/composables/useLivePreview.js` — feature-detects the
+ in-memory `useAppManifest` overload shipped by chain spec #2 and
+ either mounts a sandboxed `CnAppRoot` (preview pane active) or
+ falls back to a "save & reload" link that opens
+ `/builder/:slug` in a new tab.
+- **MODIFIED** `src/views/ApplicationEditor.vue` (from spec #1) —
+ the existing textarea becomes one of two tabs (the **"Raw JSON"**
+ fallback tab); the new **"Design"** tab embeds `PageDesigner.vue`
+ and is the default. The Application save path (PUT to OR REST) is
+ unchanged.
+- **MODIFIED** `src/router/index.js` (from spec #1) — adds a
+ `/applications/:slug/design` route alias that opens the editor
+ pre-focused on the Design tab.
+- **NEW** i18n strings under `l10n/en.json` + `l10n/nl.json` for
+ every editor pane label, button, validation message, and empty
+ state (per the workspace i18n requirement).
+
+### Capabilities
+
+#### New Capabilities
+
+- `openbuilt-page-designer`: The visual manifest / page designer that
+ produces output validating against
+ `@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json`.
+ Owns the menu-tree editor, page-list editor, per-page-type
+ sub-editors for all nine canonical types, the shared field
+ builders for the seven `$defs`, the live-preview pane (when the
+ in-memory manifest loader is available), and the validator
+ surface. Reads/writes the `Application.manifest` blob via OR REST —
+ no new backend.
+
+#### Modified Capabilities
+
+- `openbuilt-runtime`: The editor swap. The Application edit view
+ registered by spec #1 (`ApplicationEditor.vue` — single-textarea)
+ is reshaped into a tabbed editor with a "Design" tab (the new
+ `PageDesigner.vue`) as default and a "Raw JSON" tab (the existing
+ textarea) as the integrator fallback. The runtime contract
+ (manifest endpoint, nested `CnAppRoot` mount, seeded `hello-world`)
+ is unchanged.
+
+## Impact
+
+- **New code** — `src/views/PageDesigner.vue` (~300 LOC),
+ `src/components/page-editor/*.vue` (9 page-type sub-editors plus
+ menu/page/field builders, ~150-250 LOC each),
+ `src/composables/useManifestValidator.js` + `useLivePreview.js`,
+ i18n strings in `l10n/en.json` + `l10n/nl.json`.
+- **Modified code** — `src/views/ApplicationEditor.vue` (tabbed shell
+ around the existing textarea + new designer), `src/router/index.js`
+ (design route alias).
+- **External dependency** — `@conduction/nextcloud-vue` (already
+ installed) for `validateManifest` re-export and — when chain spec
+ #2 ships — the in-memory `useAppManifest` overload. The editor
+ feature-detects the overload and degrades to "save & reload"
+ preview when it is absent, so this spec does **not** block on spec
+ #2.
+- **Drag-and-drop dependency** — uses `vuedraggable` if already
+ pulled in transitively by `@nextcloud/vue` / `@conduction/nextcloud-vue`;
+ otherwise adds it as a direct devDep. Decided during apply via the
+ `npm ls vuedraggable` check.
+- **No backend changes** — manifest CRUD continues to flow through
+ OR REST per ADR-022. No new PHP, no new routes, no migration.
+- **No breaking changes** — the existing textarea path is preserved as
+ the "Raw JSON" tab; users with externally authored manifests keep
+ their workflow.
+- **Foundational ADRs honoured** — ADR-024 (manifest contract / closed
+ 9-type enum), ADR-022 (no per-app REST wrappers), ADR-031 (no
+ service classes — editor output is the declarative manifest blob
+ itself; see `design.md` for the explicit declarative-vs-imperative
+ call-out).
diff --git a/openspec/changes/archive/2026-05-12-openbuilt-page-editor/specs/openbuilt-page-designer/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/specs/openbuilt-page-designer/spec.md
new file mode 100644
index 00000000..4f8f7dc4
--- /dev/null
+++ b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/specs/openbuilt-page-designer/spec.md
@@ -0,0 +1,338 @@
+## ADDED Requirements
+
+### Requirement: REQ-OBPD-001 Menu tree editor with two-level nesting
+
+The system SHALL provide a `MenuTreeEditor.vue` component that
+authors the manifest's `menu[]` array. The editor SHALL support
+drag-reordering of top-level entries and their `children[]`, with a
+**maximum nesting depth of two** (a child MUST NOT itself declare
+`children[]`). Each entry SHALL surface form fields for `id`,
+`label` (bound as an i18n key, not a literal), `icon`, `route`,
+`order`, `permission`, `target` (closed enum: `main` | `settings`,
+default `main`), `href`, and `action` (closed enum: `user-settings`,
+optional). The editor MUST enforce the canonical schema rule that
+`action`, when set, makes `route` and `href` ignored, by surfacing a
+disabled-with-tooltip state on those fields.
+
+#### Scenario: Drag-reorder updates the manifest in order
+
+- **WHEN** the editor lists three menu entries and the user drags the
+ third entry to the first position
+- **THEN** the in-flight manifest's `menu[]` array reflects the new
+ ordering with `order` integers re-assigned monotonically (0, 1, 2)
+- **AND** the change appears in the next validator pass
+
+#### Scenario: Child nesting blocks third-level adds
+
+- **WHEN** the user attempts to add a child entry inside a
+ `menu[].children[].children[]` slot
+- **THEN** the editor refuses the action with the i18n message
+ `openbuilt.page-designer.menu.error.nesting-depth`
+- **AND** the manifest remains unchanged
+
+#### Scenario: Action field disables route and href
+
+- **WHEN** the user sets `action` on a menu entry to `user-settings`
+- **THEN** the editor disables the `route` and `href` input fields
+- **AND** displays an inline note explaining the canonical rule
+- **AND** clears any pre-existing values from `route` and `href` in the
+ manifest output
+
+### Requirement: REQ-OBPD-002 Page list editor with uniqueness and route-pattern validation
+
+The system SHALL provide a `PageListEditor.vue` component that
+authors the manifest's `pages[]` array. The editor SHALL support
+adding, removing, and drag-reordering pages, and MUST enforce the
+following invariants before allowing a save:
+
+- Every `pages[].id` SHALL be unique within the manifest.
+- Every `pages[].route` SHALL match the vue-router pattern grammar
+ (`/`, `/:param`, `/segment/:param`, `/:catchAll(.*)?`).
+- Adding a page SHALL prompt for the page `type` from the canonical
+ closed enum (`index` | `detail` | `dashboard` | `logs` | `settings`
+ | `chat` | `files` | `form` | `custom`) before any other field is
+ shown, so the correct per-type sub-editor mounts immediately.
+
+#### Scenario: Duplicate id blocks save with a marked error
+
+- **WHEN** two pages in the list share the same `id` value
+- **THEN** both rows display an inline error mark
+- **AND** the manifest validator surfaces the duplication in the
+ side-panel error list
+- **AND** the parent editor's Save button is disabled
+
+#### Scenario: Page-type pick mounts the matching sub-editor
+
+- **WHEN** the user clicks "Add page" and selects `type: form`
+- **THEN** the centre pane mounts `FormPageEditor.vue`
+- **AND** the new page is appended to `pages[]` with `type: 'form'`
+ and a placeholder `id` + `route` the user is prompted to fill in
+
+### Requirement: REQ-OBPD-003 Per-page-type config sub-editor for each of the nine canonical types
+
+The system SHALL ship one Vue sub-editor component per canonical page
+type declared in the
+`@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json`
+v1.4.0+ page enum: `IndexPageEditor.vue`, `DetailPageEditor.vue`,
+`DashboardPageEditor.vue`, `LogsPageEditor.vue`,
+`SettingsPageEditor.vue`, `ChatPageEditor.vue`,
+`FilesPageEditor.vue`, `FormPageEditor.vue`, `CustomPageEditor.vue`.
+Each sub-editor SHALL author **only** the `pages[].config` block
+appropriate to its type and MUST NOT emit keys outside that type's
+config sub-shape declared in the canonical schema's `pages[].config`
+description block. The page-list editor SHALL mount the sub-editor
+whose name matches the selected page's `type` field.
+
+#### Scenario: Switching page type swaps the sub-editor
+
+- **WHEN** the user edits an existing `type: index` page and changes
+ its type to `dashboard`
+- **THEN** the centre pane unmounts `IndexPageEditor.vue` and mounts
+ `DashboardPageEditor.vue`
+- **AND** the previous `config` block is replaced by the
+ dashboard-shaped default (`{ widgets: [], layout: [] }`)
+- **AND** the side-panel validator confirms the new shape against the
+ canonical schema
+
+### Requirement: REQ-OBPD-004 Index-page sub-editor: register, schema, columns, actions
+
+`IndexPageEditor.vue` SHALL author the index-type `config` block per
+the canonical schema. It SHALL expose:
+
+- A **register picker** populated from the user's accessible
+ OpenRegister registers (via OR REST).
+- A **schema picker** filtered to the selected register, also via OR
+ REST.
+- A **column selector** that lists every property of the selected
+ schema, plus the `@self.*` metadata virtual columns (`@self.uuid`,
+ `@self.created`, `@self.updated`, `@self.owner`,
+ `@self.organisation`, `@self.locked`). Columns SHALL be addable in
+ either string shorthand or as a typed `column` object (the editor
+ MUST round-trip both forms losslessly).
+- An **action declarations** list reusing `ActionBuilder.vue` for
+ each `actions[]` entry.
+- Optional **sidebar** + **cardComponent** sub-blocks matching the
+ canonical schema's index `sidebar` and `cardComponent` shapes.
+
+#### Scenario: Column picker offers @self.* metadata fields
+
+- **WHEN** the user opens the column-selector dropdown for an index
+ page bound to `register: openbuilt, schema: hello-message`
+- **THEN** the dropdown lists each `hello-message` property AND the
+ six `@self.*` metadata entries
+- **AND** selecting `@self.created` adds the column to `columns[]` in
+ string shorthand `"@self.created"`
+
+### Requirement: REQ-OBPD-005 Detail-page sub-editor: sidebar tabs and route param schema
+
+`DetailPageEditor.vue` SHALL author the detail-type `config` block. It
+SHALL expose:
+
+- **Register + schema picker** mirroring the index sub-editor.
+- A **route-param schema** that auto-derives expected `$route.params`
+ from the parent page's `route` string (e.g. `/messages/:id` →
+ `{ id: }`) and surfaces a warning if
+ the route has no `:param` segment.
+- A **sidebar config** block supporting both the legacy boolean shape
+ (`sidebar: true|false`) and the v1.2.0+ object shape
+ (`{ enabled, show, columnGroups[], facets, showMetadata, search }`).
+- A **sidebarProps.tabs** list reusing `SidebarTabBuilder.vue` for each
+ open-enum tab definition (`{ id, label, icon?, widgets?, component?,
+ order? }`).
+
+#### Scenario: Tab list overrides the built-in sidebar tabs
+
+- **WHEN** the user adds three tabs (`overview`, `audit`, `relations`)
+ via `SidebarTabBuilder.vue`
+- **THEN** the manifest's `config.sidebarProps.tabs` contains exactly
+ those three entries in the authored order
+- **AND** the validator confirms each tab declares exactly one of
+ `widgets[]` OR `component`
+
+### Requirement: REQ-OBPD-006 Form-page sub-editor with exactly-one-of submit handling
+
+`FormPageEditor.vue` SHALL author the form-type `config` block. It
+SHALL expose:
+
+- A **field list** built from `FormFieldBuilder.vue` matching the
+ canonical `formField` `$def`, including validation rules
+ (`required`, `pattern`, `min`, `max`, `enum`).
+- An **exactly-one-of** picker for `submitHandler` (a
+ customComponents registry-name string) **OR** `submitEndpoint`
+ (a URL string with `:paramName` segments). The editor MUST refuse
+ to allow both to be set simultaneously.
+- A **submitMethod** picker (closed enum: `POST` | `PUT` | `PATCH`;
+ default `POST`).
+- A **mode** picker (closed enum: `edit` | `create` | `public`;
+ default `public`).
+- Optional **submitLabel**, **successMessage**, **initialValue**
+ inputs.
+
+#### Scenario: Setting submitHandler clears submitEndpoint
+
+- **WHEN** the user enters a `submitEndpoint` and then types a value
+ into `submitHandler`
+- **THEN** the editor clears `submitEndpoint` from the manifest
+- **AND** the picker visibly reflects the active branch as
+ `submitHandler`
+- **AND** the validator passes
+
+#### Scenario: Invalid submitMethod blocks save
+
+- **WHEN** the user attempts (via raw JSON tab) to set
+ `submitMethod: 'DELETE'` on a form page
+- **THEN** the validator marks the page with an error referencing the
+ closed enum
+- **AND** the parent editor's Save button is disabled
+
+### Requirement: REQ-OBPD-007 Custom-page sub-editor reads the customComponents registry
+
+`CustomPageEditor.vue` SHALL surface a **component-name picker**
+populated from the consuming app's `customComponents` registry —
+specifically, the keys of the `customComponents` prop passed to the
+sandboxed `CnAppRoot` mounted by the live-preview pane (REQ-OBPD-008).
+When the live-preview pane is unavailable (chain spec #2 not yet
+shipped), the picker SHALL accept a free-text string and surface a
+warning that the value cannot be validated until preview is enabled.
+The sub-editor SHALL also expose a free-form JSON editor for the
+`config` sub-shape, because the canonical schema explicitly allows
+`type: custom` configs to be "any shape the custom component
+expects".
+
+#### Scenario: Registry-backed picker lists known components
+
+- **WHEN** the live-preview pane is active and the registry exposes
+ three custom-component keys
+- **THEN** the picker dropdown lists exactly those three keys
+- **AND** selecting one writes the chosen key to `pages[].component`
+- **AND** the free-form JSON editor opens with an empty `config: {}`
+
+#### Scenario: Free-text fallback when preview is unavailable
+
+- **WHEN** the live-preview pane is unavailable (in-memory manifest
+ loader from chain spec #2 not detected)
+- **THEN** the picker renders as a free-text input
+- **AND** an i18n warning explains the validation-deferral
+- **AND** the value writes through to `pages[].component` unchanged
+
+### Requirement: REQ-OBPD-008 Live-preview pane mounts a sandboxed CnAppRoot when available
+
+The Page Designer SHALL provide an optional right-hand pane that
+mounts a **sandboxed** `CnAppRoot` instance configured from the
+in-flight (unsaved) manifest, so the user sees their edits render
+live without saving. The pane SHALL be considered available **only
+when** the in-memory `useAppManifest(appId, manifestObject)`
+overload from chain spec #2 (`nextcloud-vue-in-memory-manifest`) is
+detected at runtime. When the overload is absent, the pane SHALL
+collapse to a "save & reload" affordance that opens
+`/builder/:slug` in a new browser tab against the last saved
+manifest, with an inline i18n note explaining the limitation.
+
+The sandboxed `CnAppRoot` SHALL:
+
+- Use a unique `appId` of `openbuilt-preview-{slug}` so its state
+ does not collide with the production-mounted virtual app.
+- Receive the manifest as an in-memory object (no fetch).
+- Re-mount via a `:key` bound to the manifest's content hash, so any
+ manifest edit re-renders the preview cleanly.
+
+#### Scenario: Preview pane renders the in-flight manifest
+
+- **WHEN** chain spec #2's in-memory manifest loader is available
+- **AND** the user edits a page's `title` field
+- **THEN** the right-hand pane re-renders the preview with the new
+ title visible
+- **AND** no PUT request is sent to OR
+
+#### Scenario: Fallback when in-memory loader is unavailable
+
+- **WHEN** chain spec #2's in-memory manifest loader is NOT detected
+- **THEN** the right-hand pane displays a "Save & open preview" button
+- **AND** an i18n note (`openbuilt.page-designer.preview.unavailable`)
+ explains the limitation
+- **AND** clicking the button saves the manifest and opens
+ `/builder/:slug` in a new tab
+
+### Requirement: REQ-OBPD-009 Save flow PUTs the manifest via OpenRegister REST
+
+The Page Designer's Save action SHALL serialise the in-flight
+manifest, validate it via
+`@conduction/nextcloud-vue`'s `validateManifest` export, and PUT the
+updated `Application` object via OpenRegister's existing REST API at
+`/index.php/apps/openregister/api/objects/openbuilt/application/{uuid}`
+— the same path the spec #1 textarea editor already uses. The
+designer MUST NOT introduce a new openbuilt-side controller for
+manifest writes (ADR-022).
+
+#### Scenario: Save persists via OR REST
+
+- **WHEN** the user clicks Save with a valid manifest
+- **THEN** the editor sends a PUT to OR's
+ `/api/objects/openbuilt/application/{uuid}` endpoint with the full
+ Application body and the updated `manifest` field
+- **AND** the response is `200`
+- **AND** the editor's "dirty" indicator clears
+
+#### Scenario: Save is blocked when validator reports errors
+
+- **WHEN** the validator has at least one open error in the
+ side-panel error list
+- **THEN** the Save button is disabled
+- **AND** clicking the (disabled) button surfaces a tooltip
+ enumerating the blocking error count
+
+### Requirement: REQ-OBPD-010 Raw JSON fallback tab preserves the spec-1 textarea
+
+The Application edit view SHALL retain the textarea-based JSON
+manifest editor shipped by spec #1 (`bootstrap-openbuilt`) as a
+secondary tab labelled "Raw JSON". The Design tab (the new
+`PageDesigner.vue`) SHALL be the default tab on view load. The two
+tabs SHALL share the same in-flight manifest state, so edits made in
+one tab are visible in the other when the user switches tabs without
+saving.
+
+#### Scenario: Switching tabs preserves unsaved edits
+
+- **WHEN** the user edits a page title in the Design tab and switches
+ to the Raw JSON tab without saving
+- **THEN** the textarea content reflects the unsaved page title
+- **AND** the dirty indicator persists across the tab switch
+
+#### Scenario: Raw JSON edits flow back to the designer
+
+- **WHEN** the user edits the JSON directly in the Raw JSON tab and
+ switches back to the Design tab
+- **THEN** the designer re-parses the JSON and re-renders the
+ relevant panes
+- **AND** if the JSON is invalid, the designer surfaces a parse error
+ in its side-panel error list and disables the Design tab inputs
+ until the JSON is valid again
+
+### Requirement: REQ-OBPD-011 Debounced validator surface decorates editor panes inline
+
+The system SHALL provide `useManifestValidator.js`, a composable
+that wraps `validateManifest` from `@conduction/nextcloud-vue` and
+re-runs the validator at most every 300ms of editor-state change.
+Each validation error SHALL be surfaced **twice**: in the right-hand
+error-list side panel (or a collapsible band when the live preview
+pane occupies the right column), and as an **inline mark** on the
+specific editor field whose JSON path matches the error path. The
+composable MUST NOT block the editor on validation — the UI stays
+responsive and the validator output catches up asynchronously.
+
+#### Scenario: Error path maps to inline field mark
+
+- **WHEN** the validator reports an error at JSON path
+ `pages[1].config.columns[0]`
+- **THEN** the page-list editor highlights the second page row
+- **AND** the index sub-editor's column-selector first row shows an
+ inline error mark
+- **AND** the side-panel error list contains the same error with a
+ click-to-focus link to the column row
+
+#### Scenario: Validator debounce coalesces rapid edits
+
+- **WHEN** the user types eight characters into a field within 200ms
+- **THEN** the validator runs at most once during that window
+- **AND** the editor remains responsive (no input lag attributable to
+ validation)
diff --git a/openspec/changes/archive/2026-05-12-openbuilt-page-editor/specs/openbuilt-runtime/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/specs/openbuilt-runtime/spec.md
new file mode 100644
index 00000000..8a8ae640
--- /dev/null
+++ b/openspec/changes/archive/2026-05-12-openbuilt-page-editor/specs/openbuilt-runtime/spec.md
@@ -0,0 +1,56 @@
+## MODIFIED Requirements
+
+### Requirement: REQ-OBR-005 Textarea manifest editor saves to the Application object
+
+The OpenBuilt shell SHALL render a **tabbed Application editor** for
+the `manifest` field of an `Application` object, composed of two
+sibling tabs sharing one in-flight manifest state:
+
+1. **"Design"** (default tab) — mounts the visual `PageDesigner.vue`
+ shipped by the `openbuilt-page-designer` capability. The designer
+ authors the manifest through structured per-page-type sub-editors
+ and a menu-tree editor; see the `openbuilt-page-designer`
+ capability spec for its full requirements.
+2. **"Raw JSON"** — the original JSON `