Официальный сайт SLAED CMS
Журнал изменений
Take the file manager out of the editor plugin and make it the one way the whole system asks for a file. A form row no longer carries a bare file field: it carries a button that opens the same window an editor opens, and what was picked comes back as a chip. The storage of already uploaded files becomes reachable from every place a file is asked for instead of only from inside an editor.
Core changes:
- Upload places (core/system.php):
getUploadPlaceRule() resolves one rule for one place, named with a dot
- the grammar ^[a-z0-9_]+\.[a-z0-9_]+$ lives here and in no caller
- three branches: <mod>.attach, files.dist, users.avatar
- answers every field the routes read, not only the limits
- the upload right is two settings ANDed: upload && add, upload && addquest
- users.avatar is a member place: guestupload and guestfiles are hard zero
mod is the module a place is moderated as, never the first segment: users.avatar answers account, and no caller splits the place string
ops names which of the four routes a place permits; a field place permits editorFiles alone, so a form place can never reach an upload or a deletion
- getUploadFileArea() replaces getEditorFileArea() and is built from the rule
- getUploadTakenFile() resolves one client path for both form handlers
- getUploadPlaceView() answers the module, the rights and the extension list
- Place routing (index.php, plugins/editors/toastui/driver.php):
- the go=4 entry guard reads place, raw: filterVar() empties a string with a dot
- getEditorRouteRule() gates on ops beside its three existing guards
every endpoint URL carries place instead of mod, built server side, so both ends of the contract migrate in one step and no shim is needed
- The window becomes the theme's (core/helpers.php, templates/{lite,admin}):
- getFileManagerWindow() builds it from the place rule, in either mode
getFileManagerField() builds the row: the button, the chip and the three hidden carriers the submit takes to the handler
- partials/editor-toastui-files.html becomes partials/file-manager.html
the window rules move from assets/editors/toastui/skin.css into theme.css under the new root sl-fm-win; the administrative browser owns the bare sl-fm-* names, so the window stays scoped rather than unscoped
- the shot panel rules move with them: getWindowShot() is already system-wide
- fragments/file-input.html is deleted; fragments/file-field.html replaces it
- The runtime leaves the plugin (plugins/system/filemanager.js):
editor-upload.js becomes plugins/system/filemanager.js and is delivered by getFileManagerWindow() once per request, so a page with no editor behaves
- addField(id, node, opt) is the second entry: a box instead of an editor
the eleven draw templates the runtime needs move to file-manager-templates.html; the four emoji ones stay with the driver
SlaedToastUi keeps tags, emoji and the word lists; editor-tags.js calls the new namespace explicitly and no alias is created
- The two callers (modules/files/index.php, modules/account/index.php):
both read the three outcomes in a fixed defensive order and resolve a stored path through getUploadTakenFile(), which refuses it unless the owner matches
the catalogue passes getEditorFileOwner() and never the integer user id, which used to turn every guest token into zero and match one guest against another
the avatar keeps plan 2's arbitration: a preset beats anything the window produced, and an avatar failure does not roll back the profile
- Tooling and tests:
tools/upload-route-check.php walks the new place routes; its merge deletion is verified rather than assumed, and the master secret is re-read per call while the last non-empty value is held
- tools/ui-contract.php follows the rules into the theme
Benefits:
One door: adding a file is one gesture everywhere, and stored files are reachable from every place a file is asked for
One rule per place instead of a rule assembled by hand in each module, so a limit and a right are answered in one file and read the same before and after
The server refuses what the interface does not draw: an interface that draws no button is not a guard, and the ops gate is enforced in one place
- Guest uploads are isolated per session instead of sharing the owner segment 0
Technical notes:
Breaking: the editor endpoints take place=<mod>.<slot> and no longer accept mod=<mod>; a dot cannot travel through filterVar(), which is why the parameter is new rather than widened. Both ends are built server side and migrate together
Breaking: _FILE_USER, _AVATAR_USER and _EDITOR_ADD are removed from lang. The first two lost their callers with the rows they captioned; the third was a scoped duplicate of the global _ADD, identical in all six locales
Breaking: templates/{lite,admin}/fragments/file-input.html is deleted. A theme or module calling it must move to file-field.html through getFileManagerField()
sl-toastui-upload survives as a second class on the window: it cures the fixed button height the vendor root imposes and belongs where the vendor is
data-editor is deliberately not renamed: it is read by five producers and a template-only rename would break the editor silently
Files stored before this change keep the owner segment 0 and are listed to nobody; they are not migrated, which is the correct failure direction
The first two plans of the queue are finished and each deleted itself with its last batch, but the third still named both by path. A reader following the reading order would open the plan, go looking for its two prerequisites and find neither.
Core changes:
- Queue section (docs/UPLOAD-FIELD-2026.md):
- the two finished plans are named without a path, and said to be deleted
what outlived each of them is named instead
- plan 1: the Form Row Contract of docs/TEMPLATES.md and getFieldIds()
- plan 2: the Settings Page Contract of the same file
- the two live defects plan 2 left open, and where each is now recorded
- the handover section below is flagged as the only surviving copy of what plan 2 hands over
Benefits:
- The reading order resolves; every docs path this plan names now exists
- A reader is not left guessing whether a missing prerequisite means missing work
Technical notes:
- Documentation only; no batch, no code and no behaviour is changed
The dead docs/FILE-MANAGER-CONCEPT-2026.md citation is left alone: the plan already knows it is dead and batch 3 is told to replace it
The crawl fails on this stand before any edit, and the record of that was deleted with the plan that measured it. A future reader would meet a red gate and look for the cause in their own work.
Core changes:
- Label crawl (docs/TEMPLATES.md):
the failure is coverage, not violations: 366 stored against 365 rendered
- three route variants no longer answer, two of them record-bound
- proved by stashing a whole batch and running the same audit on the committed tree
- and a warning not to --store over it, which would adopt a regression as the floor
Benefits:
- A red gate that is not yours is recognisable in a minute instead of an hour
Technical notes:
- Documentation only; the loss itself is untouched and still wants an investigation
The account settings plan was deleted with its last batch, and it carried two live defects and one rig property that nothing else in the tree records. Each is written where its subject lives rather than in a register nobody reads.
Core changes:
- Editor defect (docs/EDITORS.md):
a value carrying <br> comes back from a driver without them, measured to the byte
- block fell 1475 to 1387, exactly 22 tags; sig fell 213 to 201, exactly 3
- the settings form rewrites both columns on every save, so any save costs a member their line breaks
- both wrong guesses already spent are recorded, so the next reader does not spend them again
- Settings page defect (docs/TEMPLATES.md):
a validation stop rebuilds every field from the stored row and discards the typed input
- measured with a broken token: a typed occ came back as the stored value
- Contrast registry blind spot (docs/TEMPLATES.md):
a masked or initially hidden element is never sighted, so it has no measured contrast at all
- time is masked for the shots and holds no pair on any page of the tree
Benefits:
- A user-facing data loss stays findable after the document that found it is gone
- The next reader of the contrast registry knows what a zero does not cover
Technical notes:
- Documentation only; no code, no schema and no behaviour change
- Neither defect is fixed here; both want a batch of their own
A sixteen part plan for the content subsystem that is meant to serve several material types at once and retire the same-shaped modules one by one, without growing a second CMS inside SLAED. Status is design; no implementation has started, and nothing in the tree reads any of it yet.
Core changes:
- Plan set (docs/node/):
- README names the reading order and the status
goals, naming, database, code structure and the public core API
- types, fields and settings; routing; rendering
- integrations with SLAED, type extensions, security and performance
- data migration from the modules being replaced, testing, roadmap
- a decision journal, so a settled question is not reopened by the next reader
Benefits:
- The design is arguable before it is expensive, while it is still only prose
- A decision journal keeps the reasoning with the plan instead of in a session
Technical notes:
- Documentation only; no code, no schema, no route and no configuration change
- Written in Russian, as the working language of these plans
The account settings stop being four tabs hiding four endpoints and become one page of sections and tiles: PHP hands over nested data, the template owns every tag and class, and the state of the account is readable before a single field is touched. The password keeps a form of its own so a mistyped old password cannot be answered with "saved" about half the page.
Core changes:
- Page assembly (modules/account/index.php):
edithome() builds sections, tiles, lines and fields instead of markup
- a section that renders nothing is never appended, so the rail counts what exists
- a validation stop carries its section name and the message lands beside its cause
- the shared form opens before the first section marked inform and closes after the last
getProfileFillRate(), getAccountLamps() and getAccountLog() name the three things the page computes
- the fill rate counts six plain controls, and the two it leaves out are left out for a reason each
- the log is assembled from columns that already carry a time and adds no table and no column
- a provider sign-in not later than its linking is the linking, and is not printed twice
- Page template (templates/lite/partials/account-settings.html):
one partial renders sections, tiles, lamps, rail, log and save bar
- PHP names a width number and a tone number; the template maps both to classes
- the tile is the fold container, so rows fold on the tile and never on the window
- Page behaviours (plugins/system/slaed.js):
- data-sl-spy marks the current section, data-sl-meter recomputes completeness as you type
data-sl-dirty raises the save bar on the first change and reverts on discard
- the hidden state is armed by the script, so a page without JavaScript keeps a usable bar
- Theme (templates/lite/assets/css/theme.css, base.css):
- the sl-opt-* vocabulary, three component tokens, and sl-is-ok and sl-is-info beside sl-is-warn
- the yes/no switch label takes the body colour, clearing a 4.17 contrast pair the crawler found
- Dead code removed (templates/lite/fragments/table-row.html, table.html):
the is_avatar_link branch and the is_avatar_grid flag lost their last caller with the preset gallery
- the gallery is now one radio group of 127 options inside the shared form, carrying one token
Benefits:
- One save button for one page, instead of four forms pretending to be one
- The page carries 3 CSRF tokens where it carried 128
- Every caption id comes from getFieldIds(), and no label is emitted without a control
Technical notes:
- The saveavatar route is gone; the avatar arbitration lives inside savehome()
- An avatar failure does not roll back the profile: the profile is one UPDATE, the avatar a second
- Fifteen language constants added across all six locales
- The settings page contract now lives in docs/TEMPLATES.md and the work plan is deleted
A member who had once saved a site theme was served an admin panel built from partials no site theme carries, so the login form did not render at all and the log filled with a template-not-found warning. The name reached the cookie from a default the code wrote in as if it were a decision.
Core changes:
- Theme resolution (core/system.php):
getTheme() ignores the member's theme once ADMIN_FILE is defined
- the panel forces its own theme at bootstrap and the account cookie may not take it back
- the constant is read only where it is set before the bootstrap, which is the panel entry point
- index.php declares the same constant for its admin endpoints, but long after this answer is cached
- Cookie writes (modules/account/index.php):
savehome() and savepass() write the theme the member chose and never the site default
- an empty slot means no preference, which every reader already resolves for itself
- a name written in now means a decision, which is what the database column already held
Benefits:
- The panel is reachable again for every member who ever picked a theme
- The cookie and the users.theme column stop disagreeing about what empty means
Technical notes:
- No schema change; the column already stored an honest empty string
- Backward compatible: a cookie carrying the old default resolves to the same theme
The account settings page stopped being four tabs hiding four independent POST endpoints and became one form saved by one button, with the password form and the OAuth unlink buttons standing beside it as siblings. On the way the password handler lost a CSRF gate that checked nothing, stopped mailing the new password in clear text and stopped logging the member out of an account whose password had just changed.
Core changes:
- Password handling (modules/account/index.php):
savepass() gates on the token check it already performed
- checkSiteToken() wrote into $stop and the function never read it, so a forged
token changed the password
The UPDATE became the only gate: nothing happens before it succeeds
- the mail was queued and the result of the write was never read, so a failed
write still announced a successful change
The session survives the change
- the account cookie carries the password hash, so the new hash is written into
it with the same six-element shape savehome() uses, and the member returns to
the settings page instead of the login form
The mail no longer carries the password
- _PASSESEND lost its fourth argument and every locale renumbered its
positional placeholders
- The three password fields became type="password" with autocomplete
- One form and one handler (modules/account/index.php):
edithome() builds one form-add carrying the profile, the avatar and the presets
- getNaviTabs() left the function: the password form and the unlink buttons live
inside tab panels, and wrapping the tabs in a form would nest one form in
another, which the parser drops silently
The preset gallery stopped being 127 forms and became one radio field
- the page carries one token instead of 128, and every thumbnail is lazy
savehome() took over the avatar with three explicit branches
- a preset, a file that was offered at all, or neither
- neither is UPLOAD_ERR_NO_FILE alone, so an ordinary save with no file attached
no longer tells the member a file was not selected
- a rejected upload does not roll back the profile
- saveavatar() and its route are gone
- Each $stop entry carries the section it belongs to
- Radio group helper (core/helpers.php):
getTplRadioGroup() forwards label_html, which fragments/radio.html already supported, so an option can be labelled by an image
- Upload adapter contract (tests/Unit/UploadIntegrationTest.php):
- The account adapter is savehome(), the handler that now publishes
Benefits:
- A forged token can no longer change a password
- A password is no longer sent by mail in clear text
- Changing the password no longer ends the session
- One POST and one token instead of four endpoints and 128 tokens
- 127 lazy thumbnails instead of 127 eager ones inside 127 forms
Technical notes:
- The screenshot manifest gained the settings route, which it never covered
- The avatar is written by its own UPDATE, never folded into the profile write
- _PASSTEXT no longer promises a dropped session; rewritten in all six locales
- No schema change, no route change apart from the removed op=saveavatar
- Breaking change: op=saveavatar is gone; the avatar now travels with op=savehome
The field standard tied captions to controls but left four kinds of row unnamed: a radio group, an editor and a hint carried no accessible name at all, and the value row was a label labelling nothing. One helper now owns the three ids a row needs, every group, editor and hint is tied to what belongs to it, and a row folds on the box it stands in rather than on the window.
Core changes:
- Field identity (core/helpers.php):
getFieldIds() answers the control, caption and hint ids of one row
- The id is taken and never derived from the field name: this tree writes ids by hand and the mapping is no rule
- Only a row whose field has no labelable control mints one, from the seed the caller names and a per-request counter
- getTplRadioGroup(), getTplTextarea(), getTplModuleSelect() and getTplRefreshTimeSelect() carry a caption id and a hint id down
- getTplAddFieldRows() gives its dynamic controls an id and the row a for
- Row markup (templates/*/fragments/div-row.html, label.html, form-field-row.html):
hint_html, hint_id and label_id are row keys; the hint leaves the label element and keeps its cell
- A caption that swallowed the hint read the explanation out as the name of the field
- label-hint.html is gone and its 98 call sites carry caption and hint apart
- lite renders a span where a row has no for, instead of a label labelling nothing
- Radio groups (core/helpers.php, block-content.html, partials/div.html):
- Every .sl-radio-group carries role="group" and aria-labelledby to its own caption
- The switch variant and the checkbox lists built through the div partial take the same two attributes
- Editors (core/classes/editor.php, plugins/editors/*/driver.php):
Editor::getNameData() settles the name once, so four drivers cannot answer it four ways
- A caption is pointed at; a row without one is named by its own text
- plain writes the name onto its textarea; toastui and ckeditor onto the mount, and their JS moves it to the element holding role="textbox"
- TinyMCE takes aria-label only: its editable body is a second document and an IDREF does not cross that boundary
- EditorPlain::getWidget() uses the declared fragment keys instead of a hand-built attribute string
- Hints (core/admin.php, admin/modules/.php, modules//admin/index.php):
- aria-describedby ties every hint to the control, or to the group, of its own row
- catacess() and getMailRow() learned to take an id, so the rows they build can be pointed at
- Read-only value row (templates/*/fragments/field-value.html, assets/css/theme.css):
- Own vocabulary: .sl-value-row, .sl-value-label, .sl-value-text, and the caption is a span
- The panel row gains the fold it never had; .sl-form-* now means the editable row and nothing else
- Container fold (templates/*/assets/css/theme.css):
- A grid nested in another row's field cell folds unconditionally: measured 496 to 816px at the four rig widths, it never has room for two columns
- .sl-oauth-form and .sl-pmf-reply declare a container, so a card in a minmax(280px, 1fr) column and a composer in a split pane fold while the window is wide
- The crawl and its baseline (tools/label-audit.mjs, tools/label-audit-baseline.json):
- npm run ui:label walks the panel, the site as a member and the site as a guest, and asks of every rendered document what no count in a file can ask
- 417 baseline entries fell to 20: what remains is 15 duplicate ids, four codemirror editors and one for that reaches no labelable element, each recorded on purpose
Benefits:
- A screen reader announces the question a radio group answers, the caption an editor carries and the sentence that explains a field, none of which it could before
- One owner for the three ids of a row, so a caption, a group and a hint cannot disagree about the name that joins them
- The panel value row folds on a phone instead of holding a 160px caption column down to it
- A form row in a narrow box reads the box, so the OAuth card stops drawing two columns in 342px
Technical notes:
- fragments/label-hint.html is removed. A theme package outside this repository that renders it must move to the hint_html row key
- getMailRow() in admin/modules/config.php takes the id triple as its fifth argument; catacess() and getTplModuleSelect() gained optional id and description arguments and stay compatible with every existing call
- --sl-form-label-width keeps its name although the rule it serves was renamed: the admin API block is frozen and a distributed theme reads that name
- No count reported by tools/ui-audit.php moved; tools/ui-contrast.json was regenerated from the final tree
- docs/FORM-FIELDS-2026.md is deleted, and what outlives it is in docs/TEMPLATES.md under Form Row Contract and Label crawl
The stand carried one series, the presentation page. It now carries three: the account settings section and the file upload form join it, each drawn as whole pages in the real site shell rather than as descriptions of an idea.
Core changes:
- The gallery (demo/index.html, demo/assets/demo.js):
- The stand presents its variants as series instead of one flat list
DEMO_SERIES holds the three of them, and a file finds its own neighbours and its own gallery section through it
The panel arrows walk inside a series and wrap, so a series is a subject of its own and not a continuation of the one beside it
- The settings series (demo/set-01-deck.html through demo/set-10-bridge.html):
- Ten variants of index.php?name=account&op=edithome
- The upload series (demo/up-01-drop.html through demo/up-06-inline.html):
- Six variants of index.php?name=files&op=add
- The shared rhythm (demo/assets/demo.css, demo/README.md):
- The stand stylesheet gains what the two new series ask of it
The readme describes all three series, the numbering of each and the file names they follow
Benefits:
- Two more decisions can be compared as whole pages under the real theme
- Numbering restarts per series, so a variant is named by what it answers
Technical notes:
- The stand is not part of a release and lives only in the development tree
Every variant keeps whatever it needs beyond the theme in a single style block inside its own file, and adds nothing to the lite theme
The content inside a series is identical to the letter, so a comparison reads the composition and never the text