Skip to content

CMS authoring: partial editor, Blocks, content-region layouts, theme-fork + Modules taxonomy - #103

Merged
WebTigers merged 14 commits into
mainfrom
feature/get-tiger-page-builder-partials
Aug 7, 2026
Merged

CMS authoring: partial editor, Blocks, content-region layouts, theme-fork + Modules taxonomy#103
WebTigers merged 14 commits into
mainfrom
feature/get-tiger-page-builder-partials

Conversation

@WebTigers

Copy link
Copy Markdown
Owner

CMS authoring overhaul, plus a marketing page, a skin, and two platform fixes. 11 commits, ~50 files.

Highlights

CMS builder + composition

  • Partial editor — a partial edits inside its layout's chrome (the page builder inverted): layout_key association + a [Layout ▾] picker, auto slot-detection (a partial the layout names edits in its own slot; any other edits in the [content] area), save writes back only the partial.
  • Blocks — the copy-in twin of the reference-placed Partial widget: a type=block fragment, a My Blocks palette (drop = an editable copy), and Save as Block (create-from-selection, live palette add).
  • Content-region layouts — a layout is now a template that renders inside the theme shell's <main> (not a self-contained document), so a CMS user never touches the shell plumbing. PUMA ships Full Width · Sidebar Left · Sidebar Right · Two Sidebars (+ a Sidebar partial) as forkable tiger:layout files.
  • Builder hardening: no dark-mode FOUC, saveDesign meta-encode fixes, strip locked chrome from the saved project.

Theme → CMS fork workflow

  • Tiger_Theme gains layouts()/partials()/template()/forkables() (fingerprint-cached); forkTheme forks a theme's tiger:layout/tiger:partial files into editable rows, gated to the active theme. The Theme Templates tab is a server-side DataTable (a theme may ship 800+).
  • AUTHORING.md — the page-building workflow of record (shell vs content-region seam; human + agent share the /api door).

Modules screen

  • Retain source taxonomy: modules keep their type/category after install (Discovery reads the manifest; install persists it — migration 0042; resolves stored → manifest → default; routed default is now plugin). The 17 first-party modules ship a module.json declaring their taxonomy.
  • The Modules list is a client-side DataTable with a Type column + a taxonomy type filter (Apps / Themes / Plugins / Developer …).

Fixes

  • acl.ini activation-gate — an inactive module's acl.ini no longer loads (it was leaking, e.g. apidocs).
  • JSON columns → LONGTEXT (migration 0041) — DB stores text; the app owns JSON validity (MariaDB's json_valid rejects depth ≥ 32, which broke the builder's deep project blob).

Prod / theme

  • The Get Tiger marketing page (file-based) + the Bengal PUMA skin + named content-width classes.

Migrations

0041_json_to_longtext, 0042_module_taxonomy — both additive.

Verification

Every change was exercised on tiger-dev (a real vendored install): builder flows, theme forks, the multi-column layouts end-to-end (single document, no nested <html>), the module-taxonomy distribution, and the acl.ini gate.

🤖 Generated with Claude Code

WebTigers and others added 14 commits August 5, 2026 08:41
- /get-tiger: shipped marketing page (IndexController::getTigerAction + view
  + routes.ini alias) — the four ways to run Tiger, vibe-stack comparison,
  portability, in the /vibe family.
- Bengal skin: warm near-black ground + burnt tiger-orange, big bold headings;
  registered in the skin-switcher catalog.
- default.css: named content-width classes (.container-narrow/-menu/-wide/
  -full-bleed) for all skins; .container-menu matches the navbar width.

CMS visual builder (design.phtml + tiger.page-builder.js):
- Load the real front-end stylesheets into the GrapesJS canvas (was unstyled).
- Canvas light/dark toggle; body background now tracks the theme token.
- Non-editable, locked header/footer chrome preview (Elementor-style),
  stripped from the saved body.
- Partials/composition primitive: [partial name="x"] shortcode (recursive,
  cycle/depth-guarded) + a GrapesJS Partial widget (live preview, exports the
  shortcode); designAction feeds the named partials.
- Fix: Menu/Partial widgets are hover-selectable (pointer-events moved off the
  component root onto an inner preview wrapper).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A GrapesJS project can carry lone surrogates / invalid UTF-8 (JS strings) that
make a strict json_encode return false — an empty string that fails the meta
column's json_valid CHECK (SQLSTATE 23000 / err 4025). Encode with
JSON_INVALID_UTF8_SUBSTITUTE (then '{}' as a last resort) so saving a builder
page never writes invalid JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real cause of the saveDesign 4025 error: the injected header/footer chrome
preview deeply nests the GrapesJS project blob, and MariaDB's json_valid (the
page.meta CHECK) rejects JSON nested >= 32 levels — PHP encodes it fine, MariaDB
refuses it. (The earlier UTF-8 hardening was defensive, not the cause.) The chrome
is now removed from ALL saved data (body/css/project) on save and re-injected for
continued editing; suppressUpdate keeps that churn from marking the doc dirty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dity

Migration 0041: page.meta, page_version.meta, media.variants, media.scan_meta
JSON -> LONGTEXT, plus a sweep that drops every json_valid CHECK in the schema.
MariaDB's JSON type carries an implicit CHECK(json_valid) that rejects JSON
nested >= 32 levels (PHP encodes it fine) — it broke the CMS builder storing its
deep GrapesJS project in page.meta. LONGTEXT holds any depth; the app already
json_encode/decodes these values. One-way (no down to JSON). AGENTS.md documents
the convention: new tables use LONGTEXT, never the JSON type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GrapesJS canvas iframe painted light (data-bs-theme default) before the theme
was applied on load, flashing to dark. Init tbMode from the builder's own
data-bs-theme (not 'light') so the first frame:load applies the right mode, and
hide the .gjs-frame until JS marks it ready (theme applied + chrome placed, via
rAF), then fade it in — with a 2.5s safety reveal so it can't stay blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A layout is now 'just a partial': a visually-built layout body places the page
inside itself via [content], and composes header/footer from [partial]. The
renderer already passes the page body to a layout as $context['content']; the
phtml path used <?= $content ?>, this gives html/builder layouts the same slot.
Empty (harmless) outside a layout render. Completes the composition primitive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Partial editor — a partial now edits INSIDE its layout's chrome (the page
builder inverted: layout = locked context, partial = the one editable region).
Reuses the whole locked-chrome / canvas-CSS / light-dark stack.
- Association via the reused layout_key column (no migration) + a [Layout ▾]
  picker that persists the choice; default = the theme header/footer.
- Auto slot-detection: a layout that NAMES the partial ([partial name=key])
  edits it in its own slot (header/footer); any other partial edits in the
  [content] area (hero/CTA). _splitLayoutForPartial cuts the rendered layout
  on a marker into locked chrome-before / chrome-after.
- Save writes back only the partial body (chrome stripped).

Blocks — the copy-in twin of the reference-placed Partial widget.
- New type=block fragment primitive (model/form/list/datatable filter).
- 'My Blocks' builder palette: dropping a Block inlines its editable HTML into
  the page (a detached copy), reusing the theme-components insert path.
- Save as Block: persist the selected element as a reusable Block + live-add it
  to the palette (Cms_Service_Page::saveBlock, create-from-selection).
- Block masters are editable in the builder (fragment chrome, 'Block' badge).

Partials and Blocks are reachable from the content list (Design button now
covers page/partial/block; Type filter gains Block).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The activation gate strips a deactivated module from the dispatch map (so its
controllers/services are unreachable), and the nav/usermenu/header/fields/
schedule registries all skip inactive modules — but Tiger_Acl_Acl::_aclIniPaths
globbed EVERY module dir, so an inactive module's acl.ini still registered its
roles/resources/rules into the ACL graph. Gate it on Tiger_Model_Module::
inactiveSlugs(), mirroring Tiger_Admin_Nav; fail-safe (DB not ready -> load all).
Real leak: the inactive 'apidocs' module ships an acl.ini that was loading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AUTHORING.md — the page-building workflow of record: every start is a page-store
row, seeded from blank / a core BS5 starter / a theme; human + agent share the
/api door; the shell-vs-body-skeleton seam.

Theme fork generalized beyond pages — Tiger_Theme gains layouts()/partials()/
template($kind,$slug)/forkables() (fingerprint-cached) so a theme's tiger:layout
and tiger:partial files fork into editable type=layout/partial rows;
Cms_Service_Page::forkTheme takes a kind + bakes the origin theme's stylesheet
links into a forked page's head (self-loads its CSS via the theme symlink).

Only the ACTIVE theme surfaces (Tiger_Theme::active) — an installed-but-inactive
theme has no asset symlink, so its content can't render and must not be forkable;
forkTheme refuses a non-active theme.

The 'Theme Templates' tab is now a server-side DataTable (Cms_Service_Page::
themeTemplates) with a Theme column, since a theme may ship hundreds (Porto ~830);
'customized' is matched by origin (theme|kind|slug) in the fork's meta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A module's taxonomy (type + category) now travels with it and is retained after
install, so the installed list agrees with what Add Module showed.

- Discovery reads type + category from module.json; an untyped routed module
  defaults to 'plugin' (the WordPress model), theme->theme, snippets->code.
- Install captures the source taxonomy: module.type/category columns (migration
  0042); Tiger_Module_Installer records the listing/manifest values; the Modules
  admin resolves stored -> live manifest -> default.
- The 17 first-party modules ship a module.json declaring their taxonomy (cms/blog
  app:content, agent app:ai, media plugin:media, code/schedule developer:tools, …)
  — self-describing, matching the registry vocabulary.
- The Modules screen is now a client-side DataTable (sort/page/search) with a Type
  column and a taxonomy type filter (All | Apps | Themes | Plugins | Developer …),
  its interactions rewired as delegated handlers to survive redraws.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 'layout' is now a content-region template that renders INSIDE the theme shell's
<main>, not a whole page — so a CMS user composing a layout never touches the
shell's plumbing (SEO/analytics/consent/assets/scripts), and a layout is
structurally incapable of carrying injection code.

- PageController::viewAction retires the self-contained 'layout_key owns the whole
  document' path: every CMS page renders through the theme shell, and its layout
  composes the content region within it (verified end-to-end: single document, no
  nested <html>). Safe — 0 live pages used the old behavior. Dropped two now-dead
  helpers (_injectBefore/_consentBanner).
- Header/footer stay chrome the SHELL renders (they need the theme view scope —
  themeAssets, nav helper, auth placeholders — a CMS render can't supply); their
  content stays editable via the partial editor, just not re-placed per layout.
- PUMA ships the starter set as forkable tiger:layout files: Full Width, Sidebar
  Left, Sidebar Right, Two Sidebars (+ a Sidebar partial) — asides/columns via
  [content] + [partial] slots, zero plumbing.
- AUTHORING.md §1 updated to nail the shell-vs-content-region split.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…avior

- Tiger_Theme::active() derived its key from the boot-time THEME constant, which
  ignores a runtime Tiger_ThemeDir override (an admin preview, or a test's temp
  theme) — so forkTheme recorded the wrong source_key. Derive it from the manifest
  (else the dir basename minus a theme- prefix) instead. (Caught by the forkTheme
  integration test.)
- DiscoveryTest: a routed module's default type is now 'plugin', not 'module'.
- CmsControllerTest: the Theme Templates tab is a DataTable now — assert themeCount,
  not the removed themeName / inline template list.
- CoreControllerActionsTest: the self-contained-layout path is retired — rewrite to
  assert the content-region model (body wrapped in its layout, rendered through the
  shell; head_html/body_scripts flow to the shell slots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…view test

The [content] shortcode isn't registered in the reduced integration bootstrap, so
the html layout left it literal. phtml pulls the body via the renderer context var,
independent of shortcode registration (matches the prior test's approach).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature release: in-context partial editor + Blocks, content-region layouts +
PUMA starter set, theme-fork workflow + Theme-Templates DataTable, Modules
taxonomy retention + Type-column DataTable, and the acl.ini activation-gate fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@WebTigers
WebTigers merged commit 1dce8d4 into main Aug 7, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant