feat!: composition builder — JSON PressTree replaces the dynamic-zone mechanism - #13
Conversation
…youts - Introduced `preset-organism.columns`, allowing editors to create 2-4 column layouts with rich text, images, and buttons. - Added `preset-molecule.column` as a nested component within `preset-organism.columns`, supporting rich text, optional images, and buttons. - Implemented deep population in `buildBodyPopulate` to ensure nested components are correctly populated. - Updated serializers and generators to handle the new two-level nesting without requiring code changes. - Created a new `Columns` renderer that manages layout and rendering of columns based on specified ratios and gaps. - Enhanced theme styles to accommodate the new columns layout and button styles. - Added comprehensive tests for the new columns functionality, ensuring correct rendering and behavior.
…mechanism) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… group Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…; retire nav-item/column/columns Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JSON custom field; Dynamic Zones and dz-populate removed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion with tree contract version Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w enum values Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…refs in composition trees Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted coverage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iants Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pplicability, palette groups) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…slots modes, registry-driven forms) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ves the wire Task 13 of the composition-builder refactor: rewrites the text atoms (paragraph/list/quote) to render curated plain-text strings instead of the Strapi blocks AST, moves the button atom onto the shared PressLink/PressLinkData resolver, deletes the in-house blocks-AST renderer, and rewrites types/base.ts so PageBody/Page describe the PressTree wire shape from @ogs-tech/press-shared. Every PresetAtom*/PresetOrganism* type drops __component/id (they are tree `data` objects now, not DZ rows). Also fixes heading.test.ts and map-page.test.ts fixtures to match the new type contracts (PresetAtomHeading, PageBody) — both are direct, mechanical fallout of this task's own produced types, not part of the Task 14/17 migration debt. The full pnpm typecheck still reports errors in sections/hero.tsx, cta.tsx, columns.tsx, columns.test.ts (and their downstream block-renderer.test.tsx runtime failure) — expected, deferred to Tasks 14/17 per the brief. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…a PressLink Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tree renderer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…solve-slots) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… blockKey retired Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t main children; cell/stack CSS Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ressPageRef, no DZ unions) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on; seed-shape regression guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ime dep (workspace: allowed to co-published targets) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he tarball A `files` allowlist makes npm stop honoring .npmignore, so the vitest suite shipped in the published tarball. Match press-web's proven pattern (no `files` field; .npmignore alone) — the CLI publish-readiness guard documents this trap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…geset + CLAUDE.md architecture rewrite Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…not Map.entries()
GET /api/press/schema 500'd at runtime ("registry.entries is not a function"):
Strapi's components registry is not a Map — it exposes keys()/get()/getAll(), no
entries(). The `as Map<string, any>` cast let `registry.entries()` typecheck AND
pass a Map-based unit fake, so the bug only surfaced on boot (type-sync). Iterate
with keys()+get(); type the registry structurally so a future .entries() fails to
compile; reshape the test fake to the real registry API so this class can't regress.
Verified end-to-end: schema 200 (tree.version=1), /api/pages/home hydrates the
hero image assetId -> url/dims.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ost React The builder custom field crashed the Strapi admin with "Cannot read properties of null (reading 'useState')": @strapi/sdk-plugin's vite build externalizes only the plugin's dependencies + peerDependencies, and cms declared neither react nor react-dom, so the admin chunk BUNDLED the workspace-hoisted React 18 — a second React instance beside Strapi 5.48's admin React 19 → null hooks dispatcher. Declaring them as peerDependencies externalizes them; the rebuilt chunk now imports react (not a bundled copy). Added a guard test since nothing in build/test/typecheck catches this — only opening the admin does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root pnpm.overrides pinned React 18, while Strapi 5.48's admin runs React 19 — a split that (with the plugin bundling its own React) crashed the admin. Pin react/react-dom to Strapi's 19.2.7 across the workspace so the Next host, engine packages, and Strapi admin all share ONE React (single 19.2.7 resolved). Bump press-web dev/@types to 19; regenerate the CLI scaffold's react pin to ^19.2.7. React 19 fallout was minimal: two `JSX.Element` refs needed `import type { JSX }` (@types/react 19 dropped the global JSX namespace). Full gate green under React 19 (web 260, cms 68, cli 29, shared 11; all typechecks; pack:check). CLAUDE.md documents the pin + the plugin react-externalization contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rebuilt the `plugin::press-cms.builder` custom field UI using `@strapi/design-system` and `@strapi/icons` for a cohesive admin experience. - Introduced collapsible cards for blocks, rows, and columns, improving readability and navigation. - Updated the scaffold seed in `create-press` for a simplified demo showcasing essential components and grid layout. - Added `palette-labels` utility for mapping component UIDs to human-readable labels and icons. - Configured Vitest to handle ESM/CJS compatibility and deduplicate React instances for testing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p ratio ops Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ total badge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ayoutRow drops ratio Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the free-column-spans MAX_COLUMNS 4->12 change; the module docstring still said '1-4 columns'. Comment-only, no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🧱 Increment pushed: Free Column Spans (
|
| Layer | Change |
|---|---|
@ogs-tech/press-shared |
PRESS_TREE_VERSION = 2; Span = number + ColumnSpan; ColumnNode.span; RowNode.ratio/Ratio/MAX_ROW_COLUMNS removed; validator sanitizes malformed spans (default { base: 12 } + warn), no wire column cap |
@ogs-tech/press-web |
spanFor(column) is now a passthrough (column.span as Responsive<Span>); RATIO_SPANS/Ratio re-export gone; primitives + theme.css untouched |
@ogs-tech/press-cms admin |
Builder: per-column base/md/lg span selects + read-only 12-track preview bar + Base/md/lg tier toggle + per-tier total badge (danger at >12); MAX_COLUMNS 4 → 12; setColumnSpan/effectiveSpan |
@ogs-tech/press-cms server |
preset-layout.row descriptor drops ratio; /api/press/schema serves tree.version: 2 (no code change — reads the constant) |
@ogs-tech/create-press |
Seed emits v2 spanned columns; generated PresetLayoutRow drops ratio? |
Quality gate (all green)
- Build:
pnpm build6/6 turbo tasks incl. playground Next build + SSG - Tests: 374 passing — shared 15 · cli 28 · web 259 · cms 72
- Typechecks: shared / web / cli + cms
test:ts:back+ cmstest:ts:frontall clean
(note:pnpm -r typecheckskips cms andstrapi-plugin buildexits 0 even on admin type errors — the real admin gate istest:ts:front, run explicitly)
Whole-branch review (this increment): Ready to merge — Yes
0 Critical, 0 Important. Independently verified: the spanFor cast against the real <Column> Responsive<Span> prop, the builder-only column cap (validator has only < 1), and that sanitizeSpan upholds the strict-write/tolerant-read split (malformed span → warning, never nulls the tree). Remaining findings are Minor/cosmetic; one stale comment already fixed in c3f333d.
⚠️ Reviewer / merge precondition — manual smoke test
No data migration (pre-release, authorized): a v1-seeded DB reads back empty until reseeded. Before merging, reset + reboot the dogfood:
rm -f apps/playground/packages/cms/.tmp/data.db && pnpm devConfirm: schema.tree.version = 2; home renders the spanned demo row (image | paragraph, 50/50 desktop / stacked mobile); admin Composition field shows the span selects + 12-track preview + tier toggle + badge.
Changeset
shared / web / cms → major; create-press → patch.
Composition builder — a JSON
PressTreereplaces the dynamic-zone mechanism engine-widePer-page layout root (header/footer/children slots), rows with 1–4 ratio-bound columns, full recursion, adopter
custom-*blocks anywhere. Stored via oneplugin::press-cms.builderJSON custom field; a structural tree-editor drives it in the admin.BREAKING (wire + palette + API), no data migration (pre-release). 25 commits, executed task-by-task from
docs/superpowers/plans/2026-07-20-composition-builder.md.Highlights
page.body(fullPressTree) andsite-setting.pageDefaults({header,footer}slots) are JSON custom fields; the page-body + chrome Dynamic Zones anddz-populateare gone.@ogs-tech/press-sharedis now a published runtime dependency: it ships the tree types + the sanitizingvalidatePressTree/validateNodeArrayvalidators (cms bundles into dist; web depends + host transpiles). Strict-write (cms lifecycle rejects errors OR warnings) / tolerant-read (web sanitizes).preset-molecule.link(label/page/url/newTab) referenced by button/hero/cta/navbar;nav-item/molecule.column/organism.columnsretired. Text atoms store curated plain text (content: text) — the Strapi blocks AST leaves the wire.preset-layout.{container,row,column}descriptors drive the builder's layout forms.{assetId}→ url/dims, page{documentId}→ slug — resolved server-side (rename-safe internal links, no populate depth to fight), batched one query pair per request.TreeRenderer(<header>/<main>/<footer>shell) replacesBlockRenderer;PressLink/resolveLinkis the one link resolver (dangerous-protocol-safe); prose rail rescoped to direct<main>children;PageBodygenerates asPressTree;PressPageRefadded;HeaderBlocks/FooterBlocksunions gone. Block-override contractcomponents={{ 'preset-organism.hero': MyHero }}unchanged.Automated verification (all green)
pnpm build6/6 incl. the playground Next build (SSG 3/3)pnpm -r test— web 260, cms 67, cli 29, shared 11test:ts:back/test:ts:front)pnpm pack:checkexit 0 — no*.test.tsin any tarballPlan gaps caught & fixed during execution
hydratePageDocsto one query pair (was a plan-mandated N+1 on the unpaginated/pagesroute) + addedserve-hydrated.test.ts.press-shareddevDeps→deps in web (Decision 3 gap); host transpiles it.footer.tsxinline-typed afterResolvedChromeFooterdeletion (Task 14 migrated navbar but not footer).publish-readinessmade target-aware (press-shared now published;workspace:allowed to co-published targets).filesallowlist from press-shared so.npmignorekeeps tests out of the tarball (surfaced bypack:check).Page.bodyofftype==='customField'alone (serialize strips the id) — production-shape guard test added.Manual review checklist (not covered by automated tests — please verify on
pnpm dev)/api/press/schemahastree:{version:1};/api/pages/homebody.version===1, heroimagecarries a hydratedurl, navbarpageDefaults.headeritem haspage:{documentId,slug:"home"}.localhost:3000renders navbar / hero+image / prose atoms at ~72ch / the recursive 50-50 row / cta / callout / footer.pageDefaultsslots editor; emptying header → inheriting pages render bare.verticalAligncenter/end, nomain[data-press-stack]↔ organismmargin-blockdouble-gap.Known cosmetic note
The playground regen changed the cms Strapi
uuid(the plan expected it preserved) — harmless dev instance id; can restore on request.🤖 Generated with Claude Code