Официальный сайт SLAED CMS
Журнал изменений
The side and top keys a module carries have decided nothing since the modules moved off their table: the two globals the foot reads by were never assigned, so every page rendered all four block positions. The reading is restored, and the grid stops holding a column it was no longer given.
Core changes:
- The module block setting reaches the renderer (index.php):
$blocks and $blocks_c are assigned from $mconf beside active and view
- setFoot() reads both by global and skips the sides a module switched off
- They were declared in the global list of setFoot() and assigned in no file of the tree
- A null global compares equal to the empty string, so the first branch of all four won always
The line lost in the migration read the columns blocks and blocks_c of the modules table
- The keys are now side and top of $conf['modules'][$name], the values unchanged: 0 both, 1 first, 2 second, 3 neither
- The four conditions of setFoot() are the ones 6.2 shipped and needed no edit
- The home branch stays as it is, which is what 6.2 did: a random module never decided the blocks of the front page
- The content grid follows the columns that exist (theme.css):
#container declares one column, and a side track only under :has() of the rail it belongs to
- Left alone, right alone, both, neither - four rules, each naming its own template
- The empty right track cost the content 400px of width and 24px of gutter on every page without blocks
- #leftbar had no track at all: with left blocks the content would have landed in the sidebar column
The three rules sit inside @media (width > 900px) rather than at the top level
- :has(#sidebar) carries the specificity of an id, which would outrank the narrow-screen reset
- The two queries are complementary, so the stacked layout below 900px is untouched
- The narrow-screen rule drops its grid-template-columns: it repeated the base, and 1fr is minmax(auto, 1fr)
Benefits:
- A setting the panel has been saving for years does what its label says
- A module without blocks reads across the full measure instead of against a 424px margin of nothing
- The layout answers the markup, so a left rail needs no second rule the day a left block is written
Technical notes:
- Behaviour: every module whose side or top is not 0 changes appearance, not only the one that was edited
- API: no token added or moved; both rules read --sl-layout-sidebar and --sl-layout-gutter as before
- Compatibility: :has() is already load-bearing in this theme; without it the page degrades to one column
- Gates: ui-audit clean per theme and global, counters identical to HEAD, ui:gates 193 green, phpunit 915 green
- Not run: the screenshot pair, for want of SLAED_UI_USER and SLAED_UI_PASS on the stand
The stand for index.php?name=main is rebuilt from the band studies it used to hold into twenty three whole-page
designs, every one of them carrying the same content in the real site chrome. The implementation section stops
showing theme banners and shows what it is meant to show: the seventy one client sites of the project catalogue.
Core changes:
- Real implementation screenshots (uploads/screens/):
Seventy one pairs restored from the reference site, thumbnails and full pages
- The live carousel on the presentation page fills again: it reads uploads/screens/thumb at request time
- Sizes are 260 wide by 166 to 455 tall for a thumbnail, 865 to 1024 wide for a full page
The stand reads twelve of them from one list in demo.js, never from its own markup
- data-demo-sites builds the cards, data-demo-sites-skip offsets, data-demo-sites-natural keeps own height
- Twelve entries at one address cannot drift apart across twenty three files
- Two laws about pictures (demo/assets/demo.css):
A screenshot is never wider than its 260 and never taller than its own height
- An even row is made by cropping the tall ones, never by stretching the short ones
- The column is sized to the picture, not the picture to the column
A theme photograph is 1920x440, so a band may not exceed 440 * width / 1920
d-band-artpaints the picture at its own pixels: full width to 1920, then centred- This is what made the previous glass variant look cheap, and it is now impossible to repeat
- The glass direction, developed into four (13, 19, 20, 21):
- 13 rebuilt: the band is exactly the height of its photograph and the panel overhangs its lower edge
19 liquid glass: progressive blur in three masked layers, so the photograph dissolves instead of ending
- One layer cannot do it - a masked backdrop-filter fades its opacity, not its radius
- 20 client showcase: a real client site at full size in a browser frame, four of them on a rail
21 layers: a stuck scene where the four pillars arrive as sheets in perspective
- Driven by a named view-timeline on the tall block, read by its four descendants
- Two designs outside the glass (22, 23):
- 22 anti-grid: no shadow, no blur, no radius, no gradient - hard rule, monospace, deliberately broken columns
- 23 wall of work: a masonry wall where every screenshot keeps its own height, built with columns
- The stand itself (demo/):
- Eighteen band studies and fourteen footer studies replaced by twenty three page designs
- assets/demo.js carries the manifest, the site chrome, the control rail and five shared behaviours
- assets/demo.css carries one section rhythm (d-*) so a variant is judged by what it does differently
- A variant adds nothing to the theme: its own CSS is one <style> block under v-* names
Benefits:
- The presentation page can be chosen from real alternatives rather than described in words
- The implementation catalogue argues with the sites built on the system instead of with stock banners
- No design on the stand can ship an upscaled picture, which is the one flaw that cannot be styled away
- Every variant is the same content in the same chrome, so the comparison is of design and nothing else
Technical notes:
- Storage: uploads/screens gains 142 files, 28 MB, the same set the production site serves
- API: nothing in templates, core, modules or plugins is touched; the stand does not ship
- Compatibility: scroll-driven variants degrade through @supports to static, readable sections
- Gates: ui-audit clean per theme and global, demo/ is outside its scope; PHP suites not run, no PHP changed
The two photo bands of the front page gain a slow beam and a veil the dark mode alone puts on, the lower menu is laid out by icons at every width and closes the page on the same 50px edge the top bar opens it with, and every season and slider photograph now ships as WebP at a third of the weight.
Core changes:
- Light beam on both photo bands (base.css, theme.css):
Four tokens carry the whole effect, all declared through light-dark() in the API block
- --sl-band-bg is the veil: transparent in light, half-strength ink in dark
- --sl-beam-width, --sl-beam-dur and --sl-beam-bg are the shaft, its pass and its light
The darkening lives in the alpha of the colour, not in an opacity layer
- The opacity ladder carries no even half, and an alpha inside a colour is part of the colour
The beam travels upright at an even rate: eight seconds, three and a half of travel, four and a half at rest
- It starts its own width left of the band and ends a window past it, which holds at any width
- No literal outside the API block: calc() off the width token, 100vw, and the named layer tokens
- Season photographs cover their band again (theme.css):
The ten season rules set the background with longhands instead of the
backgroundshorthand- The shorthand reset background-size, and with it the
coverthe base rules declare - On a narrow screen the band grew taller than the photo, which then hung as a strip
- The shorthand reset background-size, and with it the
- #head-content restates its own position, because its base rule centres on 50% 0
- Season and slider art in WebP (images/, theme.css, index.php, main-slider.html):
- Seventeen season files and four slider files replaced, 668 KB down to 248 KB
- Every reference follows: seventeen url() in the theme, the LCP preload builder, the slider partial
- ViewBridgeSmokeTest asserts the slider paths by name and moves with them
- Footer menu as an icon grid (site-footer.html, base.css, theme.css, ui-contract.php):
Each destination carries its icon in the markup, never in an nth-child rule
- A rule keyed on position drifts from the labels the first time a section is added
- Three shapes of one idea: one row above 1200, four columns to 768, two columns below
- --sl-fmenu-min-height is one thumb target and one row height, because it is one decision
--sl-fmenu-height reads --sl-topbar-height, so the page closes on the edge it opens with
- Both are floors rather than fixed heights: below the wide step both blocks grow with content
- The bar separator survives only in the single row, where a line break cannot strand it
- Component names
beam,fmenudeclared in the contract - Prototype stands (demo/):
- demo/ gains the hybrid of beam and season particles, and its beam variant matches what shipped
demo/footer/ is a new stand: fourteen treatments of the lower menu, each one <style> block
- A variant renders inside an iframe of a chosen width, because the theme answers the viewport
Benefits:
- The dark mode stops asking white text to stand on a photograph shot for daylight
- A phone no longer meets a hanging separator, a 23px tap target or a stranded advertising line
- Four fewer JPEG kilobytes on every page view, and a third off the theme image payload
- Both bands and the footer are described by tokens, so the next theme repaints them from one block
Technical notes:
- Storage: seventeen season JPEG and four slider JPEG deleted; their WebP counterparts keep the pixel sizes
- API: --sl-sep-height stays declared and read, because a frozen API may gain a name and never lose one
- Compatibility: WebP needs no fallback at the theme baseline (Chrome 123, Safari 17.5, Firefox 120)
- Gates: ui-audit clean per theme and global, phpunit 915 tests green, ratchet re-stored
The settings window posted a row and the browser closed the dialog on the navigation that followed, so a second answer needed the window opened again by hand. It now names itself before the form leaves and comes back on the next load. Alongside it, demo/ carries sixteen prototype treatments of #head-content and #demo-line for the dark mode, where the seasonal photo keeps its full brightness while the page around it turns over.
Core changes:
- Settings window (core/admin.php):
One row of the window submits through requestSubmit() with submit() as the fallback
- requestSubmit() fires the submit event, which is what marks the window as open
- submit() stays for a browser that does not carry it
- The comments over getAdminSettingsRow() say the same thing in fewer words
- Window reopening (plugins/system/slaed.js):
A form inside a dialog marks that window in sessionStorage before it navigates
- Only a window carrying an id is marked, because only such a window can be found again
- A form driven by htmx swaps in place and is skipped, or it would open a window later
- setWindowBack() reads the mark once on the next load, clears it and opens that window
- Locale switch note (core/security.php):
- The comment over setLangChoice() is shortened; no behaviour change
- Band treatments (demo/):
Sixteen self-contained pages: the live band markup, the real theme CSS, one style block each
- The dial darkens the photo in the dark mode alone; every effect is independent of it
- Motion runs on transforms, so the compositor carries it instead of the painter
- The photo moves as its own layer through background-image: inherit, so the five season rules stay in one place
- assets/demo.js holds the manifest, the page chrome and the control rail; index.html is the gallery
- README.md records how to view them and what the migration into the theme will require
Benefits:
- An administrator changes two settings in a row without reopening the window between them
- The candidate treatments can be compared side by side before any of them touches the theme
- Nothing in demo/ is read by the site: they are prototypes, outside the theme and its gates
Technical notes:
- sessionStorage carries one key, slaed-window:reopen, written before a navigation and cleared on read
- demo/ holds literal CSS values by design, which is why it sits outside templates/
- No schema, URL or API contract changes
The panel settings window landed in 7e864723 with three rows built three ways, one of them posting without a token; the locale switch answered to any GET that carried newlang. This commit closes both, collapses the rows onto one builder, and follows the RSS feed the same way: declared where a reader looks for it, stable where it was random.
Core changes:
- Settings window (core/admin.php, templates/admin/fragments/settings-row.html):
getAdminSettingsRow() takes the axis and returns the row
- getAdminLanguagePick() and getAdminEditorPick() collapse into it
- the panel stops drawing a mode row of its own, so templates/admin/fragments/
mode-switch.html and its fork entry in tools/ui-contract.php are gone
- the sl-mode class moves onto settings-row.html behind an is_mode flag, which
keeps --sl-mode-width and --sl-mode-height read and the frozen API intact
- hidden fields settle on fragments/hidden.html for all three rows
the sidebar editor block leaves getAdminInfo(); the window is the only place the editor is offered, and the function loses three globals and gains a return type
- CSRF on the editor and the locale (admin/index.php, core/security.php, index.php):
- updateAdminEditor() refuses a request without checkAdminPost('changeeditor')
setLang() only reads the cookie now; setLangChoice() does the writing
- a token check cannot live in setLang(): it runs before the session, the database
and the parser exist, and every admin POST came back as the login page
- both op=newlang handlers call it and redirect, so the POST turns back into a GET
scope newlang joins the token allowlist of checkDynamicMark(), because the site block sits on cacheable pages and needs getPageToken()
- Language producers (blocks/languages.php, templates/lite/fragments/lang-switch.html):
- the flags became submit buttons of one form, the select became a POST of the same
- the geo redirect writes the cookie directly instead of handing itself ?newlang=
getAdminLanguageLinks() is gone: the panel renders with the admin theme, whose layout has no admin_langs, so the strip was computed on every load and discarded
- RSS feed (core/user.php, core/system.php):
- the site declares its own feed with <link rel="alternate"> in the head
- <atom:link rel="self"> and dc:creator replace an antispam@antispam.com placeholder
- ?go=rss without a name answers with the first module instead of a random one
- Route handler names (admin/index.php, core/system.php, modules/account/index.php):
every handler carries a verb prefix now: updateAdminEditor, setAdminThemeMode, setAdminLanguage, deleteAdminSession, getAdminLoginForm, checkAdminLogin, addAdminAccount, and login_report becomes addLoginReport
- the op strings and token scopes they answer to are untouched
- Login journal (core/system.php):
addLoginReport() reads the flag before the kind becomes a string
- a non-empty string is true either way, so log_u was unreachable and log_a alone
decided both journals, which wrote user passwords the operator had switched off
- Removals and site header (docs/, demo/, templates/lite/partials/site-header.html):
- docs/SETTINGS-FIXES-2026.md and demo/ are deleted, no code referenced either
the RSS button leaves the header, browsers have not rendered a feed since 2018; the remaining tones shift back onto a contiguous position % 6 cycle
--sl-wordmark-img-width 145px -> 150px and a quoting fix in ThemeCreationTest.php ride along, both unrelated to the rest and included on request
Benefits:
- One builder and one fragment for a row instead of three constructions
- Two handlers that changed state without a token now refuse one that lacks it
- A locale change no longer happens because a prefetcher followed a link
- Readers find the feed without being handed its address
Technical notes:
Breaking: log_u now means what it says. With log_a on and log_u off, user logins stop being written to log_user.log, and the attempted passwords with them
- Breaking: RSS <author> becomes <dc:creator>; a parser reading <author> loses the name
- Breaking: ?newlang= as a GET no longer switches the locale anywhere
- The site language switch returns to the referring page rather than to the home page
- No storage, schema or route changed: every op string and token scope is as it was
Seven batches found by reading the colour mode rail and the panel settings window back, not by running into a failure. One of them closes a hole that predates the work; the rest are consolidation, a contrast measurement and two decisions that were left open. The file follows the plan documents already in docs/: a status line, no line numbers, and every reference by the name it is searched for.
Core changes:
- Follow-up plan (docs/SETTINGS-FIXES-2026.md):
what already holds, so the next reader does not re-derive it and does not undo it
- the rail places its knob with :has(), and no state attribute is written from PHP
- --sl-mode-width and --sl-mode-height stay declared because a frozen API may not lose a name
- the measured contrast the groove gradient was chosen for
batch 1: changeeditor() writes the session and the administrator row from a POST that carries no token
- the mode handler beside it is guarded, this one never was, and the window gave it a second caller
- the language row is left as an open question, because newlang answers to a GET link as well
batches 2 to 6: one builder for the three window rows, the alias the frozen tokens were given, the editor offered in two places, the panel losing two settings without JavaScript, two category tones thin under a white icon
- batch 7: the toolbar clip and the missing screenshot pair
- what is deliberately not done, so the next pass does not start there
Benefits:
- the security finding is written down with its scope and its verification instead of living in a chat
- the two undecided points are named as decisions rather than left to be rediscovered
- the measured figures travel with the plan, so a repaint knows what it has to beat
Technical notes:
- no code changes, documentation only
the plan is a working document: it is deleted by the batch that finishes it, and anything durable moves to the permanent references under docs/
The site header stops cycling through the modes one press at a time and stands all three open on a rail; the panel stops spending its toolbar row on six flags and a toggle, and gathers the colour mode, the content editor and the language into one window opened from a single item. Both spend handlers that already existed: no new storage, no new op, and the editor keeps writing the administrator row it always wrote.
Core changes:
- Colour mode control (core/system.php, templates/lite/fragments/mode-switch.html):
getThemeModeSwitch() returns the three modes with the one in force marked, instead of the next one to step to
- the order is light, auto, dark, so the answer that follows the system sits between the two it chooses from
- the knob rides over the cells and the active cell hands its icon to it, so nothing is drawn twice
- the rail places its knob with :has() on the active cell, so no state attribute is written from PHP
- Settings window of the panel (core/admin.php, templates/admin/fragments/window-settings.html):
getAdminSettingsWindow() builds one window from three rows, each posting to the handler that already owns it
- the mode writes its cookie, the editor writes _admins.editor through changeeditor(), the language writes the session
- Editor::getSelect() is reused as it stands, so the sidebar block and the window stay in step
- getLanguageList() is the one list the flag strip of the plain panel and the language row both read
- the toolbar keeps one item for the window and rules the row where the kind of destination changes
- Theme components (templates//assets/css/.css, tools/ui-contract.php):
- .sl-mode-rail, .sl-mode-cell and .sl-mode-knob replace the single .sl-mode-but of the site
- .sl-set-row and .sl-set-item carry the window rows and the toolbar item; the component set is declared in the contract
- a toolbar item may now hold a button as well as a link, which is what lets the window open from the menu row
- the social row takes its hover ground from the category palette instead of eight hand-picked colours
Benefits:
- any mode is one press away on the site, where three presses were needed to come back to the system answer
- the panel toolbar returns to one line: 40px against the 70px the flags and the toggle wrapped it to
- one place for the three answers an administrator changes from any screen, with room for a fourth
- eight colour rules fall to one rotation the tree already spends on categories
Technical notes:
- no schema change and no new op: every row posts to an existing handler
- fragments/mode-switch.html diverges by theme on purpose, with the reason recorded in tools/ui-contract.php
- --sl-mode-width and --sl-mode-height stay declared in both themes, as a frozen API may not lose a name
- demo/ carries the stand the variants were chosen on; it links the shipped CSS and JS and ships no theme of its own
A hook that is present but never runs is worse than no hook: it reads as covered. On any clone other than the one it was written in, this one was present and silent for three unrelated reasons, and on a machine without composer it would have refused every commit without saying why.
Core changes:
- The hook is enabled per machine, not per repository (package.json):
core.hooksPathlives in.git/config, which neither a clone nor a pull carries- git will not let a repository enable its own hooks, which is a security property
and not an oversight, so something has to set it on each machine
postinstallsets it duringnpm install, andnpm run ui:hookssets it by hand- the postinstall falls through with a message outside a git checkout instead of
failing the install
- The hook file was committed without its executable bit (tools/hooks/pre-commit):
core.fileModeis false in this checkout, sochmod +xnever reached the index and the file went in as 100644Windows does not care - git runs hooks through sh either way - but git on Linux and macOS skips a non-executable hook without a word
git update-index --chmod=+xputs it in as 100755- The hook assumed two tools that a fresh machine does not have (tools/hooks/pre-commit):
phphas to be on the PATH of the shell git runs hooks invendor/is gitignored, so phpunit is absent untilcomposer installboth are checked before anything else and each stops with its own reason and the remedy, rather than failing on a confusing error or stepping aside quietly
verified in all four directions: no php stops with the PATH message, no vendor stops with the composer message,
SLAED_SKIP_GATES=1passes silently, and a normal tree prints[gates] ok- Documentation (CONTRIBUTING.md, docs/TESTS.md):
- both said "enable it once per clone", which reads as something the clone does
both now say it is per machine, name
core.hooksPathand.git/configas the reason a clone and a pull leave it behind, and record what the hook refuses to run without
Benefits:
- the hook either runs or says why it did not, on any machine
npm installis enough to arm it after a clone
Technical notes:
this commit carries no file the gates watch, so the hook exits early on it by design; the fast set was run by hand instead and is green
a gate that steps aside when its tools are missing is the failure this repository has now closed five times, which is why both new checks refuse rather than skip
The etalon plan is finished, so its prose moves into the reference documentation and the plan file goes. What replaces the checklist nobody would remember is a pre-commit hook for the cheap gates and a two-word pair for the visual one. Four gates that read zero while checking nothing are closed, and the committed screenshot baseline is dropped because it cannot be stable on a stand whose own content moves.
Core changes:
- Gates that no longer answer when their input vanishes:
tools/ui-contrast.jsoncould be deleted and the contrast count still read zero- getContract() folds the registry in only
if (is_file(...)), so a missing file
- getContract() folds the registry in only
left nothing to check and ThemeContractTest stayed green
- ThemeContractTest now asserts the file exists and carries pairs for every theme
node tools/ui-shots.mjs --checkwrote a missing baseline image instead of failing- a check whose baseline is absent re-baselined itself against the tree it was
asked to judge and exited zero; it reports MISSING and exits non-zero now
php tools/ui-audit.php --markuphad no limit beside the countmarkup.limitis 0, so the scan fails on the next hardcoded class, style or tag
the screenshot rig captured a degraded set without credentials
--beforeand--afterrefuse to start without SLAED_UI_USER and SLAED_UI_PASS,
because a run without them skips every state that needs a session
- Automation (tools/hooks/pre-commit, package.json):
the pre-commit hook runs the audit, the markup scan and the theme, template and parser tests on every commit carrying a file able to move a count
- about twenty seconds, and nothing at all on a documentation commit
- enable once per clone with
npm run ui:hooks;SLAED_SKIP_GATES=1skips one - when a commit touches theme CSS or a canon template it prints the two visual
commands, because no count can see a moved pixel
npm run ui:beforeandnpm run ui:afterare the visual gateui:afterempties the page and template caches, compares against the capture
`ui:before` took, regenerates the contrast registry when a base.css moved, reads
the counts and prints one verdict
- verified in both directions: no change gives PASS, a padding shift on a class the
page really carries gives eight DIFFs and FAIL
- The screenshot baseline is no longer committed (tools/ui-baseline, .gitignore):
measured: a check against the committed set went red hours after capture on a tree whose frontend was byte-identical - 100% size changed at sm, md and lg on the front page and the forum list, 1.6% to 3.9% even at xl
the page height follows the content of the stand, and a mask cannot save a box whose height is what moves, so the reference is captured per change into a temp directory
168 images and about 80 MB leave the working tree; the path is gitignored so a stray capture cannot put them back
- A scratch theme stopped leaking its compiled templates (tests/Support/theme_scratch.php):
deleteScratchTheme() removed
templates/scratch-*and leftstorage/cache/templates/scratch-*behind, so every phpunit run and every--newthemeleaked about half a megabyte under a name nothing can read again- Documentation (docs/, root):
docs/THEME-ETALON-2026.md is deleted; its contract, its gate table and its measured findings move into docs/TEMPLATES.md as three reference sections, without the batch narrative and without any superseded figure
- docs/PARSER.md records that the parser owns no markup and needs a global $tpl
docs/ARCHITECTURE.md records that $tpl is created before $prs because the parser depends on it, not by coincidence
- docs/EDITORS.md records that every driver mounts through fragments/editor-mount.html
CONTRIBUTING.md, UPGRADING.md and docs/TEMPLATES.md stop describing HTML leaving PHP as work in progress; it is done and a gate holds it
- five files that pointed at the deleted plan now point at docs/TEMPLATES.md
Benefits:
- the cheap gates need nothing remembered: they run on commit or refuse it
- a gate whose input is missing now fails instead of reporting a clean zero
- the working tree drops from about 145 MB to 65 MB
Technical notes:
.gitis unchanged in size: the baseline blobs stay in history and any image can be recovered withgit show b115eaa0:tools/ui-baseline/<name>.png- the hook needs
core.hooksPathpointed attools/hooks, whichnpm run ui:hooksdoes the full 168-image walk has not been run under the new
--before/--afterflags; the cycle was verified with--only=voting, and the contrast branch ofui:afterhas not executed because no palette moved in that test- no breaking change: 915 tests pass, phpstan is clean, every ratcheted count holds
A theme cannot restyle what PHP hardcodes, so the closing batch of the etalon plan moves every class attribute, inline style and HTML tag out of PHP and into the theme layer. The scan that measures it falls from 143 occurrences in 21 files to zero and gains a limit, so the next one fails the build instead of printing a number.
Core changes:
- Extended statistics panel (admin/modules/statistic.php):
The inline <style> block it printed painted with --line, --bg, --soft, --muted, --blue2, --green, --purple and --orange
- None of those eight names exists in either theme, so every border, background,
shadow and accent in the panel resolved to nothing and it had never been styled
- No count could see it: the audit reads CSS files and this was a string in PHP
The markup is partials/statistic-extended.html and the CSS is 24 rules in theme.css
- Every value lands on a ladder; the four accents are primary, success, warning
and accent; five component tokens are declared under statx
- These eight images are the only ones of 168 that moved, and they moved on purpose
- Monitor traffic chart (admin/modules/monitor.php):
The 900x285 SVG becomes partials/monitor-chart.html taking four series, six hover slots and two axis lists
Four attribute kinds are dropped as dead
- stroke and filter on lines and areas are SVG presentation attributes, which lose
to any CSS rule, and .sl-chart-line-up { stroke: ... } had been winning already
- .sl-chart-tip .muted set fill: var(--sl-text), which .sl-chart-tip text two rules
above already set, so the rule and the class are both gone
- Markdown and BBCode renderer (core/classes/parser.php):
55 tags, 7 inline styles and 4 classes move into four new fragments plus the existing list, list-item, code-highlight, blockquote, div and parser-image
A fragment can own a regular expression's replacement string once it renders the placeholder
- parser-inline.html rendered with content_html set to the literal $1 gives the
template a whole element and PHP a replacement string
- A pair that wraps text the parser has not finished reading renders the same whole
element with \x01 as its content and is split there, so a theme never spells half
a tag
The blockquote and attachment fallbacks are gone: the parser renders through the theme or not at all
- One decision, one address, across eighteen smaller files:
class strings inside form_attr become semantic flags in form-add.html and form-wrap.html
style="display:none" becomes the hidden attribute, which the reset gives !important so nothing a component sets can put back what the page took off screen; the tab script writes .hidden instead of style.display
- Eight sites that joined text with a tag now call getTplLines() over fragments/lines.html
add_voting() built class="sl-field sl-form-control" into select_attr, where select.html had already written class="sl-select..."; an HTML parser keeps the first attribute of a name, so the string had never reached a pixel and is deleted with its parameter
- The captcha directory guard copies storage/index.html instead of building a document
- Gates (tools/ui-contract.php, tests/Unit/ParserFixturesTest.php):
markup.limit is 0, so php tools/ui-audit.php --markup exits non-zero on the next hardcoded class, style or tag
ParserFixturesTest asserts the exact HTML of 86 cases and ran with no engine, so every expected string was what PHP concatenated; it builds a Template now, because the bytes it asserts are a theme's answer
tools/ui-contrast.json is regenerated beside the manifest that feeds it: 438 pairs, 118 admin and 320 lite, contrast at zero in both
Benefits:
A theme can restyle every element the CMS emits, which is the goal of the plan expressed as a measurement: markup 0, and every count either theme controls at zero
- The statistics panel is readable for the first time, in both light and dark
- Three gates that were reporting stale answers now report current ones
Technical notes:
Six fragments are byte-identical in both themes: lines, editor-mount, parser-block, parser-inline, parser-link, parser-table; two partials are the panel's alone
- The parser now hard-depends on the theme layer; there is no plain-HTML fallback left
The parser disk cache already carries getTheme() and the class mtime in its key, so theme-owned markup cannot be served across themes or across this change
Masking an element for the screenshot gate removes it from the contrast gate, because the collector skips visibility:hidden and display:none; the registry shrank from 460 pairs to 438 for that reason and not from a regression, which the retired span.sl-but.sl-hidden still sitting in the committed file independently proves
No breaking change: 86 byte-exact parser fixtures pass unchanged, and 160 of 168 baseline images are identical to the pristine tree