История коммитов

.
docs(agents): add the Twig migration recipe
What the five migrated modules settled: the layout of a migrated module, the
review every output goes through, and the four traps that cost a page each —
the __call() of an Eloquent model, the truthiness of Markup, a layout variable
shadowing the data of the page, and string literals the escaper leaves alone.
.
refactor(online): move the visitor lists to Twig
The four pages share one template each way, so the visitor row and the tabs
become components of the module.

Where a visitor is is a link assembled from the places configuration, so both
formatters behind it return Twig\Markup and the templates print it as it is. The
forum one returns null instead of an empty string: markup is an object, and an
object is truthy however empty it is — a caller could no longer tell "no place"
from "nothing to show".

The IP and browser of a visitor were guarded by rights >= 3 with a second, always
true check for any rights inside; only the outer one survives.
.
fix(docker): send the charset with stylesheets
text/css is not among the default charset_types of nginx, so the stylesheets went
out as "text/css" with no encoding while the pages carried UTF-8. A browser is
then free to read them as latin1, and the breadcrumb divider showed up as ».
.
fix(view): keep layout variables from shadowing page data
A {% set %} in a layout lives in the context its blocks are rendered in, so
"notifications" set for the sidebar counters replaced the list of the
notifications page, and the page died on the first item.

Every variable a layout keeps for itself is now prefixed with layout_, and a test
holds the convention for the layouts still to be written.
.
refactor(notifications): move the notification pages to Twig
Two contracts change with them, and both serve every list page still to come:

- Pagination::render() returns Twig\Markup, so a template prints {{ pagination }}
with no raw filter. Plates keeps echoing it, since Markup is a string when used
as one.
- Notification::getMessageAttribute() returns Markup too: the event templates in
the configuration carry links, and under autoescape they would print as tags.

Whether the navigation is shown is decided by Pagination::hasPages() instead of
comparing the total against the per-page setting of the visitor, which is what
that comparison meant.
.
refactor(language): move the language modal to Twig
A fragment loaded into a modal, so the template names no layout at all — the
Twig counterpart of a Plates template that never called $this->layout().

The list of languages comes from the controller instead of the configuration
array the template used to read, and the checked radio is written as a condition
rather than as an echoed attribute string.
.
refactor(redirect): move the external link page to Twig
Escaping moves to the output: the controller no longer runs htmlspecialchars()
on the referer and the template no longer runs it on the URL, so the value is
escaped once, by the environment.

The host in the warning is still emphasised inside the translated sentence, so
that one string stays markup and says so.

The referer comes off the request instead of $_SERVER, and the page is covered
by the smoke set.
.
feat(view): render the home page with Twig
The first page served by the Twig environment, with the public layout it needs.

- ViewResponse: a controller returns the template and its data, the response
normalizer renders it. The renderer arrives as a closure, so a controller that
still returns a string does not assemble the environment.
- themes/default/templates/layouts/{base,default}.twig and the components of the
chrome, next to the existing .phtml ones.
- The layout no longer reaches for di(), the container or the database: the facts
it prints come from SiteRuntime and DebugPanelRuntime, built only when a
template calls them. Ads and analytics are handed over as Markup.
- The cookie banner is described by the consent module through the twig extension
tag, so the theme only lays it out.
- vite() takes an area instead of a path and resolves the entry point through the
manifests of the theme chain.
- themes/example moves to Twig and keeps documenting how an override is written.
- app.* facts are reachable under their snake_case names; an attribute a guest
does not carry is never read straight off the user model, since Eloquent
answers __call() and Twig then invokes it as a method.
- DEBUG and DEBUG_FOR_ALL are declared dynamic for PHPStan: their value belongs to
the installation. Four baseline entries that rested on that are gone.
.
feat(view): add the Twig engine alongside Plates
Twig 3 is installed and wired, but nothing renders through it yet: no .twig
template ships, so every page is still served by Plates.

- Themes are described by an optional themes/<name>/theme.php: a parent to
inherit from, the Vite entry points and free-form settings. A theme without a
manifest inherits from the default one, so existing themes keep working.
ThemeChainResolver expands a theme into the chain templates and assets are
looked up along.
- TemplatePathRegistry registers the namespaces by convention — @theme, @admin
and one per installed module — so a module needs no Twig configuration at all.
A module with unusual layout can add paths with the johncms.template_paths tag.
- One environment serves the whole of HTTP, with autoescape on, and
strict_variables and auto_reload following DEBUG. The compiled templates live
in data/cache/twig/web, outside the document root.
- Extensions: the app global (facts about the page, the user and the token
resolved only if a template asks), the translation helpers, asset(),
asset_exists(), vite(), avatar(), the value formatters, and plates(), which
lets a Twig page pull in a partial that has not moved yet.
- AssetResolver looks up a file along the theme chain and, unlike the Plates
extension, does not take the page down when it is missing outside DEBUG.
- New commands twig:lint (also part of the verification gate) and twig:compile.
- i18n:scan now walks .twig templates too, through a second scanner working on
the Twig AST — without it the strings of a migrated template would drop out of
the .pot files.
.
refactor(view): prepare the view layer for the Twig migration
Groundwork for running Twig alongside the current Plates engine.

- The admin area is no longer detected by reading $_SERVER['REQUEST_URI'] in
RenderEngineFactory and Assets: CurrentPage::isAdminArea() answers it off the
RequestStack. Render resolves the theme through a resolver instead of holding a
name, so the engine singleton no longer freezes the theme of the first request
handled by the process.
- Johncms\System\View\Theme renamed to Johncms\View\ColorScheme, with
getCurrentScheme()/isDarkScheme(); it is the color scheme of the page, and the
name is needed for the site theme.
- New Johncms\View\RendererInterface with PlatesRenderer and DelegatingRenderer,
which routes a template to its engine by the shape of its name (@ns/file.twig to
Twig, ns::file to Plates). Registered as RendererInterface; the Twig renderer
joins it once it exists.