feat(editor): user-saved layouts — save any subtree, re-insert it exactly#33
Merged
Conversation
…ctly Layouts in the module inserter now have two groups: the user's saved layouts above the seeded built-in presets. Right-clicking a layer in the DOM panel or canvas offers "Save as layout…", which snapshots the clicked element + its whole subtree — props, breakpoint overrides, and every referenced style rule — for exact re-insertion later. Engine: capture/restore is the clipboard's snapshot machinery, extracted into a shared module (@site/store/subtreeSnapshot) so paste and layout insertion cannot drift: fresh node ids, scoped classes cloned with remapped scope.nodeId, framework classes re-matched by name, regular classes reused-or-reimported. Persistence: a fourth locked system table `layouts` (kind 'layout') in both migration baselines, loaded onto SiteDocument.layouts and saved through the same incremental roster protocol as pages/components (PUT /admin/api/cms/layouts, per-layout dirty tracking). Existing local dev databases need a re-migrate (drop .tmp/dev.db) — pre-release. Editor: layoutsSlice (save/insert/rename/delete, all undoable), naming via LayoutNameDialog with inline duplicate-name errors, inserter items with wireframe previews and inline disabled reasons for snapshot-borne hazards (content outlets follow the outlet module's placement rules; VC dependency cycles are blocked in component mode). Saved-layout items get a right-click Rename…/Delete menu; refs to since-deleted Visual Components are stripped at insertion time. Splits forced by the 700-line module ceiling: uiSlice's save-dirty accumulator moved to saveTrackingSlice; the saved-layout manage menu into SavedLayoutManageMenu; saved-layout validation into persistence/validateLayouts (shared error/sanitize helpers in validationShared). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the layouts system-table seed out of the 001 baseline into a new 017_layouts_system_table migration so live installs upgrade in place — no database re-creation. PG swaps the data_tables kind CHECK and seeds the table; SQLite rebuilds data_tables with the widened CHECK. Because data_rows.table_id is ON DELETE RESTRICT (fires immediately even under defer_foreign_keys, and renames always rewrite child FK clauses since SQLite 3.25), the runner gains Migration.disableForeignKeys: it toggles PRAGMA foreign_keys around that migration's transaction and verifies foreign_key_check before re-enabling enforcement. Covered by a new populated-DB upgrade test (migration017LayoutsUpgrade.test.ts). Hardening found by a post-merge audit: - import.ts SYSTEM_TABLE_IDS now includes 'layouts' (an import into a not-yet-migrated DB can no longer create layouts as an unprotected custom table) - plugin cms.content.tables.create rejects the reserved 'layouts' slug - data-tables-system-flag architecture gate extended to all four system tables; import/export roundtrip now exercises a layouts row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rade test
Packs (pack/site.json) can now carry `layouts` alongside visualComponents/
pages/classes. definePack({ layouts: [...] }) auto-namespaces ids under the
plugin id (createdAt optional); parsePluginPack validates each entry with
the tolerant SavedLayout parser plus a strict tree-coherence check and the
same ownership rule as classes (ids must be plugin-namespaced, so re-syncs
replace the plugin's own rows and never collide with user layouts).
applyPluginPackToSite records replaced layout ids; the install handler
upserts them as data_rows (table_id 'layouts') and reports them in the
install summary, toast counts, and audit metadata.
Also removes the migration-017 upgrade test per review feedback (the
migration itself is unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s in the inserter
Pack layouts are now authored the way authors already write markup:
definePack({ layouts: [{ id, name, html, css? }] }). compilePackLayout
runs the host's HTML-import pipeline at plugin build time — <style>
blocks are harvested, scripts stripped, elements mapped to base.*
modules, CSS parsed into style rules with deterministic ids
(<layoutId>/<className>) so re-inserting after a pack update reuses the
site's class instead of importing a duplicate. Multi-root markup is
wrapped in a container; class names without a matching rule are dropped.
The wire format (pack/site.json) keeps carrying compiled SavedLayout
objects, so the host install path is unchanged and needs no DOM. The CLI
installs a happy-dom window + the base module registry before evaluating
the author's config (packCompileEnvironment, mirroring the server's
richtext sanitizer setup). Layout ids now require the <pluginId>/ form.
The inserter's Layouts section now groups: the user's Saved layouts,
one group per plugin labelled with the plugin's display name, then
Built-in presets. pluginRuntime gains a plugin-name registry fed by the
editor plugin loader for every installed plugin; composeLayoutsSection
in the inserter model owns the ordering + labels and is unit-tested.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ad8c9c7 to
385559f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Layouts in the module inserter, three groups: the user's saved layouts, per-plugin groups (labelled with the plugin's display name), and the seeded built-in presets. Users create layouts by right-clicking any layer (DOM panel or canvas) → Save as layout…, which snapshots the element + its whole subtree (props, breakpoint overrides, every referenced style rule) for exact re-insertion. Plugins ship layouts in packs, authored as clean HTML.
How
One snapshot engine, two front doors. Capture/restore was extracted from the clipboard slice into
@site/store/subtreeSnapshot, and both paste and layout insertion run through it — a saved layout inserts exactly like pasting the original selection: fresh node ids, scoped classes cloned with remappedscope.nodeId, framework classes re-matched by name, regular classes reused-or-reimported.Persistence. A fourth locked system table
layouts(kindlayout) joins posts/pages/components. Saved layouts load ontoSiteDocument.layoutsand save through the same incremental roster protocol as pages/components (PUT /admin/api/cms/layouts, per-layout dirty tracking from Mutative patches). Strict write validation enforces unique ids/names against the merged post-save roster; the tolerant load path drops corrupt rows and sanitizes richtext props.Migration — in-place upgrade, no DB re-creation. Seeded by a new
017_layouts_system_tablemigration (001 baseline untouched). PG swaps thedata_tableskind CHECK and seeds; SQLite rebuildsdata_tableswith the widened CHECK. Becausedata_rows.table_idisON DELETE RESTRICT(fires immediately even underdefer_foreign_keys, and SQLite ≥3.25 renames always rewrite child FK clauses), the migration runner gainsMigration.disableForeignKeys— it togglesPRAGMA foreign_keysaround that one migration and verifiesforeign_key_checkbefore re-enabling enforcement.Editor.
layoutsSlice:saveNodeAsLayout/insertLayout/renameLayout/deleteLayout— all undoable site mutations.LayoutNameDialogwith inline duplicate-name errors.wireFromTreepreviews and inline disabled reasons: snapshots carrying abase.outletfollow the outlet module's placement rules; in VC mode, snapshots whose component refs would create a dependency cycle are disabled. Refs to since-deleted VCs are stripped at insertion.composeLayoutsSection: Saved → per-plugin groups (display names from a newpluginRuntimename registry fed by the editor plugin loader) → Built-in. Labels render only when more than one group is present.Plugin packs — HTML-authored layouts.
definePack({ layouts: [{ id, name, html, css? }] }): authors write the markup they already have.compilePackLayoutruns the host's HTML-import pipeline at plugin build time (instatic-plugin buildinstalls a happy-dom window + the base module registry first):<style>blocks harvested, scripts stripped, elements mapped tobase.*modules, CSS parsed into style rules with deterministic ids (<layoutId>/<className>) so re-inserting after a pack update reuses the site's class instead of importing a duplicate. Multi-root markup is wrapped in a container. The wire format (pack/site.json) carries compiledSavedLayoutobjects, so the host install path needs no DOM. Ids must be<pluginId>/-namespaced (validated at install) — re-syncs replace the plugin's own rows and never collide with user layouts; the prefix is also what the inserter groups on. Install summary, toast counts, and audit metadata report layouts; samevisualComponents.registergate as the rest of the pack.Module-size ceiling splits (700-line gate): uiSlice's save-dirty accumulator →
saveTrackingSlice; the manage menu →SavedLayoutManageMenu; layout validation →persistence/validateLayoutswith shared helpers invalidationShared.Integration hardening (post-implementation audit): import's
SYSTEM_TABLE_IDSand the plugincms.content.tables.createreserved-slug guard both know aboutlayouts; export/import carries layout rows (round-trip-tested); the system-flag architecture gate checks all four tables.Notes
LAYOUT_PRESETS, still HTML-only; CSS for built-ins is a later task as discussed).@mediablocks compile to conditions the snapshot format doesn't carry yet.docs/architecture.md,docs/editor.md("Saved layouts" section incl. plugin grouping),docs/features/content-storage.md,docs/features/content-workspace.md,docs/features/plugin-system.md(pack layouts + ID rules),docs/reference/database-dialects.md(RESTRICT-parent rebuild pattern).Testing
layoutsSlice.test.ts(save/insert round-trip, scoped-class remap, outlet guard, dangling-VC-ref healing, rename/delete),layoutsWriteValidation.test.ts,schemas.test.ts,layoutFromRow.test.ts, pack layout cases inpluginPack.test.ts(namespacing, malformed/incoherent rejection, replace-by-id), HTML compile cases inbuilders.test.ts(class linking,<style>harvesting, multi-root wrap, deterministic ids, empty-HTML rejection),composeLayoutsSectionordering/label tests.bun test(5284 pass),bun run build,bun run lintall green.🤖 Generated with Claude Code