Status: Draft
Updated: 2026-06-05 Owner: Core
Purpose: Draft for the CMS system theme, admin UI design language, shared layout primitives, and reusable interface components.
- Defines the visual and interaction foundation for setup, login, admin, editor, operational workflows, and system pages.
- Covers design tokens, layouts, reusable components, accessibility, translations, responsive behavior, and theme boundaries.
- Depends on core architecture, setup automation, error handling, and the public theme engine.
- Keeps the CMS usable as a product instead of only a backend architecture.
The CMS needs native system templates that behave like immutable virtual packages with frontend-theme, backend-theme, and system-template scopes. Frontend templates render public project content, user flows, and error pages; backend templates render setup, admin, editor, package management, and operations. Root templates provide shared wrappers and macros. The native package also contains the first public fallback templates, so content remains renderable without an installed package.
The system UI should be quiet, dense, predictable, and work-focused. It should favor clear navigation, readable forms, stable table layouts, explicit status indicators, and low-friction repeated workflows over decorative marketing surfaces. The goal is to make content and configuration work safe, fast, and understandable.
The first design system should be small but real. It should define enough shared primitives that every feature does not invent its own UI, while avoiding a broad custom framework. Twig components, Symfony UX, Stimulus controllers, Tailwind utilities, forms, translations, and AssetMapper should remain the default toolchain.
- Keep frontend, backend, and root template areas scope-aware, even while all native fallbacks live in the project.
- Keep
templates/base.html.twigas the global wrapper. The template root should otherwise contain only shared directories such astemplates/macros/**; area-specific templates belong undertemplates/frontend/**ortemplates/backend/**. - Use logical Twig namespaces in native templates:
@frontend/...,@backend/..., and@root/.... - Use
templates/frontend/frontend.html.twigas the frontend base layout andtemplates/frontend/layouts/**for optional frontend variants. - Use
templates/backend/admin.html.twig,templates/backend/editor.html.twig, andtemplates/backend/setup.html.twigas backend area layouts andtemplates/backend/layouts/**for optional backend variants. - Reserve
templates/backend/admin/**,templates/backend/editor/**,templates/backend/setup/**, andtemplates/backend/operations/**for backend-scoped UI. - Use
templates/frontend/content/entity.html.twigas the generic public content fallback renderer. - Keep user-route templates frontend-scoped for the first release, for example under
templates/frontend/user/**, even though the public routes are/user/...; keep the controller/resolver boundary small enough that these templates can later move or become capability-controlled overrides. - Place operation UI under
templates/backend/operations/**; the shared action-log overlay should live there with small partials for entries, summaries, and progress states. - Place error pages under
templates/frontend/error-pages/**, includingdefault.html.twigand lightweight standalone candidates for429and503. - Place context-specific partials under
templates/frontend/partials/**,templates/backend/partials/**, or narrower area directories. Do not place generic page partials at template root. - Keep partials intentionally small and override-friendly: layout fragments, brand/navigation fragments, typography headers, feedback states, form labels/help/errors/actions, action buttons/toolbars, and individual field types should be separate templates where practical.
- Place reusable Twig components under the matching Twig namespace component directory:
templates/components/**forroot:*,templates/frontend/components/**forfrontend:*,templates/backend/components/**forbackend:*, and provider/package component templates below namespace-awarecomponents/**paths so active package template ordering can override or extend them through the existing Twig loader. - Place reusable Twig macros/functions under namespaced macro files and aggregate provider macro namespaces rather than treating them as replaceable override files.
- Provide a base admin layout with sidebar or top navigation, content area, page header, action area, status area, and flash/error rendering.
- Provide a setup/login layout for unauthenticated system workflows.
- Resolve native backend areas through explicit Symfony routes for
/setup,/admin, and/editorbefore feature pages are added. - Keep setup DB-free: setup availability must be decidable without Doctrine/DBAL because database configuration is created during setup.
- Lock setup after successful installation through a loaded
APP_SETUP_COMPLETEDenvironment value that setup writes into Composer's dumped.env.local.phparray. - Protect backend areas with ACL access levels: setup stays public until locked, editor requires level
3+, and admin requires level8+. - Use the shared navigation builder for backend menus only while it remains simple: backend items may target Symfony routes, carry minimum access metadata, and expose active plus active-ancestor state to templates.
- Let packages contribute backend pages through static view injections on the
adminandeditorsurfaces. Core backend views keep priority over injected package paths, and setup is not a package-facing surface because it locks after installation. - Define design tokens for spacing, typography, color, borders, focus states, elevation, status colors, and density.
- Use existing local font assets and icon assets where appropriate.
- Keep unDraw illustration assets compatible with theme colors by preserving
var(--primary-svg-color)fills and mapping that variable tovar(--color-primary)in the surrounding system UI where inline rendering is used. - Prefer Symfony Forms and Twig components for repeatable form layouts.
- Provide a small renderer-neutral form-definition and submission layer for generated forms before committing to feature-specific form builders. The same layer should drive Admin Settings and package settings now, and later support public forms such as contact forms.
- Keep optional provider fields, including captcha, represented as field types that render through stable provider namespaces such as
@provider/captcha/**instead of hard-coding one provider into native forms. - Provide reusable UI primitives:
- Buttons and icon buttons.
- Form rows, field errors, help text, and validation summaries.
- Tables, filters, bulk-action bars, pagination, and empty states.
- Tabs, segmented controls, toggles, checkboxes, radios, selects, and menus.
- Cards for repeated items only, not as default page sections.
- Modals, confirmation dialogs, drawers, toasts, progress indicators, badges, and alerts.
- Diff/review panels for imports, destructive actions, and generated changes.
- Action-log panels for long-running or high-impact operations.
- Provide command palette, contextual help drawer, notification/toast, and background-job status primitives only after the base admin shell exists.
- Keep user-facing strings translated through deterministic translation keys.
- Keep translation catalogues modular:
messagesis reserved for the structured message layer, while UI copy belongs in narrower domains such asui,admin,editor,setup,operations,demo, or future feature slices. - Provide responsive behavior for desktop and tablet first, with safe mobile fallback for urgent administration.
- Meet baseline accessibility expectations: semantic landmarks, keyboard navigation, visible focus, color contrast, labels, error association, and reduced-motion safety.
- Keep JavaScript progressive and feature-specific through Stimulus controllers. Alpine and ApexCharts are not baseline dependencies; prefer Symfony UX, native Stimulus controllers, and the project live-polling helper before adding another frontend runtime.
- Do not let frontend-theme packages override backend templates.
- Do not let packages override
@rootshared templates unless they declaresystem-template. - Allow active packages to contribute admin navigation entries, dashboard widgets, form sections, or action buttons only through documented extension points.
- Keep system UI assets under
assets/styles/**andassets/js/**with context-specific subdirectories such asfrontend,backend,admin,editor, andsetup; keep JavaScript Stimulus-first and treat Alpine as an optional later dependency, not an assumed baseline. - Keep global design tokens under
assets/styles/tokens/**and shared primitives underassets/styles/system/**. Frontend-only styles belong underassets/styles/frontend/**; backend/admin/editor/setup styles belong underassets/styles/backend/**. This mirrors Twig namespace ownership and keeps future package styles from leaking across shell boundaries. - Treat
assets/frontend/**andassets/backend/**inside packages as area-specific package asset roots. Package assets outside those folders are shared/global and enter the extension registry only when the package also declares a global runtime scope such asmodule,captcha-provider,editor-provider, orsystem-template. - Remember that Tailwind currently aggregates all native and package CSS into one
app.css. Asset buckets protect rebuild order and ownership, but CSS isolation still depends on area root selectors. Native root selectors should move toward branding-neutralsystem-frontendandsystem-backendnames, while package selectors stay under their package slug and surface scope; stricter namespace validation can be added later if package CSS leakage becomes a practical risk. - Provide temporary shell preview routes under
/demo,/demo/backend, and/demo/typographythrough the optional demo module while the product UI is still forming. These routes are development scaffolding for checking the complete native template set and Markdown typography profiles with placeholder content and can be removed with the package before production readiness. The module declares its routes as a configurable static route set, so the stable parent path defaults to/demobut can move through package settings if the site owner wants that path for content. Demo-only translation keys live in the demo module language files and are aggregated only when the package is active.
- Render representative system pages with
php bin/console render:route /<route>once routes exist. - Run
.codex/compare_translations.phpafter UI copy changes. - Test form error rendering, flash messages, empty states, pagination, and destructive confirmations.
- Test keyboard navigation and focus states for core controls.
- Test responsive layout behavior for narrow and wide viewports.
- Run
php bin/console tailwind:buildafter CSS asset changes; keepasset-map:compileproduction-only. - Use browser screenshots for major UI changes once routes exist.
- Decision recorded: The CMS native templates behave like immutable virtual
frontend-theme,backend-theme, andsystem-templatepackages. - Decision recorded: The canonical Twig namespaces are
@frontend,@backend, and@root. - Decision recorded: Root templates are restricted to
base.html.twigand shared macro/helper directories; area templates live underfrontend/**orbackend/**. - Decision recorded: The native package also includes the fallback public theme so the CMS can render without an installed frontend package.
- Decision recorded: The first system UI should be functional, dense, and work-focused, with reusable primitives but no broad custom UI framework.
- Decision recorded: Backend templates are not overridden by frontend-only packages.
- Decision recorded: User-route templates such as login, registration, profile, password reset, password change, API keys, and invitation acceptance remain native frontend-scoped templates in early releases and should be structured so they can later move or become capability-controlled without broad controller rewrites. The login route uses the native frontend template and is also reused by anonymous
401responses. Registration remains config-gated and is linked from the login page only when enabled. - Decision recorded: Operation templates, including the action-log overlay, live under
templates/backend/operations/**. - Decision recorded: Error-page templates live under
templates/frontend/error-pages/**;429and503may use lightweight standalone templates. - Decision recorded: Packages may contribute system UI elements only through documented extension points.
- Decision recorded: Long-running or high-impact operations should have a reusable action-log panel component in addition to simple progress indicators.
- Decision recorded: UI alerts use root-scoped Twig components and a stable targeted Mercure contract. Alert producers should publish explicit UI alerts to private user or session topics instead of broadcasting arbitrary structured message-layer entries.
- Decision recorded: Alerts are managed through a lightweight notification center with badge counts,
auto/hidden/persistentmodes, quiet text actions, sessionStorage restoration when available, and close semantics that remove active alerts rather than merely hiding the panel. - Decision recorded: Live JSON polling belongs in a reusable Stimulus/helper layer for
/api/live/**endpoints. Operation forms should default to notification-center runner alerts with optional ActionLog overlay details, and provider packages can reuse the polling helper for later dynamic mechanisms such as captcha seed refreshes. - Decision recorded: Scoped Twig components should mirror the existing Twig namespaces (
root:*,frontend:*,backend:*, and provider/package namespaces) so themes and active packages can override or extend reusable UI primitives through the same template path order as partials. - Decision recorded: Global tokens and shared UI primitives are native system assets. Frontend and backend theme packages should use their matching area asset roots for area-specific styles, while root/shared package assets stay in the global extension bucket only for packages that declare a global runtime scope.
- Decision recorded: Tailwind builds a single stylesheet for now. Native area CSS and package CSS should therefore use owner/surface root selectors for practical isolation when a rule is not intentionally global.
- Decision recorded: Temporary
/demo,/demo/backend, and/demo/typographyroutes may exist before production readiness through the demo module so the native shells, partials, Markdown profiles, status colors, forms, empty states, and operation panels have real render targets without loose Core demo routes. Demo copy belongs to the demo module package language files. - Decision recorded: Packages may declare package-setting-backed configurable static route sets when a feature needs a stable route parent but should not permanently reserve a hard-coded public path. Static content and system views still win over injected package routes during route resolution.
- Decision recorded: Native translation sources are split into language-grouped files so UI areas stay small and scoped while Symfony consumes a generated default-domain
messages.*.yamlruntime catalogue. - Decision recorded: External SVG files used through CSS
background-imagecannot reliably inherit CSS custom properties from the host element. Dynamic theme coloring for unDraw assets should therefore use an allowlisted inline SVG renderer or Twig component when real theme-color adaptation is required. Mask-based coloring is acceptable only for single-color icon-like use cases; build-time or cached server-side SVG variants remain fallback options for later public/theme delivery needs. - Decision recorded: Backend routing starts with explicit Symfony routes for setup, admin, and editor. Static package view injections can add admin/editor route targets and menu entries, while core backend views keep priority over matching injected paths.
- Decision recorded: Backend navigation reuses the shared navigation builder for parent/child trees, depth slicing, route URL generation, permission filtering, active state, and active-ancestor state as long as it stays modular and understandable.
- Decision recorded: Setup completion is written to Composer's dumped
.env.local.phparray and checked through the already-loaded environment value, so/setupcan remain available before database configuration exists and lock itself after successful installation without Doctrine/DBAL. - Decision recorded: Before setup completion, main public requests should redirect to
/setupthrough an early DB-free request subscriber so a fresh installation cannot fall through into content, navigation, config, or package queries that require migrated tables. - Decision recorded: Generated forms use a neutral
App\Formdefinition/submission layer rather than an admin-only helper, so Admin Settings, package settings, and later public forms can share field definitions, validation attributes, typed value casting, translated validation feedback, and provider-backed captcha fields. - Decision recorded: The shared form builder should remain one central renderer-neutral form layer, but it should align as closely as practical with Symfony Forms and Validator so it does not become a parallel framework.
- Decision recorded: Twig helpers should be split by helper family as they stabilize, for example navigation, package, settings form, diagnostics, and markup/Markdown helpers instead of one broad extension.
- Decision recorded: Markdown profiles are intentional. The Design profile may allow richer HTML/attributes only for trusted/admin/package content and can later be assigned through ACL or field policy.
- Decision recorded: Dynamic view injection failures should be visible to administrators or debug contexts while normal production visitors are protected from raw failures. The first implementation keeps rendering graceful and records failed dynamic injection rendering through the message layer with bounded context.
- Decision recorded: Admin diagnostics may offer detailed exports only in strongly redacted form. Raw secrets, complete environment dumps, cookies, and request payloads are not valid diagnostic output just because the Admin Panel is protected.
- Decision recorded: Technical API, Twig helper, form, and CSS naming should be stabilized during refactors where practical.
Studio/studiois branding, not the default selector or technical API namespace. Native system styles should usesystem-*,system-frontend-*,system-backend-*, orsystem-{provider-scope}-*; package styles should use{package-slug}-*,{package-slug}-frontend-*,{package-slug}-backend-*, or{package-slug}-{provider-scope}-*. - Decision recorded: Runtime rebranding should stay concentrated in
.manifest, branding assets, branding tokens, and later optional branding packages. Hardcoded user-facing or inspectablestudio-*identifiers should be treated as migration debt unless they are deliberately retained product branding. - Open: Decide the first navigation model, dashboard information architecture, and exact component inventory during the first UI implementation slice.
- Open: Decide whether command palette and contextual help ship in the first admin release or remain second-slice enhancements.