# What's new — headless core, three modules, mirrored form outcomes The browser client now ships as three ES5, dependency-free scripts with a strict load order — and **core is headless**: ```html ``` **Load order is a contract.** `statum-core.js` must be first: it owns the slot protocol (state store, persistence, directives, SSE/SharedWorker realtime, entrypoint/action requests) and creates `window.statum`. The canonical order is core → document → markdown. The former fourth module, `statum-forms.js`, was folded into `statum-document.js` (and its resource now 404s). ## Core is headless — usable without any DOM `statum-core.js` no longer touches the DOM at all. Everything visual — the render walk, the attribute registry, boot visibility, `stm-guard` links, error/success classes, form bindings — lives in `statum-document.js`, which installs itself into core through internal hooks. Core alone boots, fetches the entrypoint and applies directives to state, so it works as a state layer for non-document templating frameworks (or plain JS): ```html ``` Endpoint config still arrives from the page too: `statum-document.js` reads the `stm-entrypoint` / `stm-slots` / `stm-channel` / `stm-worker` `` attributes into `statum.config` during its load, before core's init runs. Cherry-picking still works (core first, always): a headless page loads only core; a page without markdown loads core+document. Legacy pages that still say `` keep working: `statum-mkpstm` expands that reference at build time into the three tags above (one build warning per page names the expansion; explicit module tags are deduplicated). The single-file artifact itself no longer exists. Depth on all of this: `model.md` (wire protocol, directives, every attribute) and `getting-started.md` (tutorial + full JS API). ## stm-prefill — state into controls, without clobbering edits `stm-prefill` writes a state expression into form controls as **properties**, not attributes, every render cycle: ```html ``` The control matrix: - **Text-like inputs and `textarea`** — writes `.value` (`null`/`undefined` become `''`; `undefined` leaves the element untouched). - **Select** — single: writes `.value`; `multiple`: sets each option's `.selected` from an array of string values. - **Checkbox** — writes `.checked`, coerced truthy/falsy. - **Radio** — per-element: give every member of the group the same expression and the one whose `value` matches is checked. ```html ``` - **File inputs are skipped**: assigning `.value` from a script is forbidden by the platform, so there is nothing sensible to write. **Dirty tracking is the point.** The moment a user types in or changes a control (capture-phase `input`/`change` listeners), it is *dirty* and stm-prefill never overwrites it again — the edit outranks the state snapshot. A form `reset` (a reset button, `statum.document.reset`, or the server's `form-reset` directive — all funnel through the same `reset` event) un-dirties the form's controls so state can re-apply. Because dirtiness is per-element (a `WeakSet`), a form inside a keyed loop instance keeps its dirty state across re-renders: the instance owns its nodes. ## Mirrored form outcomes: stm-clear-on-success, stm-clear-on-error, stm-on-success A form whose own `stm-action` settles can clear itself on either outcome: ```html
…
…
``` Both wait for the action's promise and read its outcome flag: success resets a `stm-clear-on-success` form, a real failure (HTTP error or `error` directive) resets a `stm-clear-on-error` form, and neither touches any other form on the page. **A declined `stm-confirm` never clears** — decline is cancellation, not error. The reset also un-dirties the controls, so the next render cycle re-applies `stm-prefill` against the fresh state. Action elements gained the success mirror of `stm-on-error`: ```html ``` The two are mirrored: a success applies the success classes and clears the error classes; a failure does the reverse; a new attempt clears both at its start. The programmatic form API lives on `statum.document` now (`statum.forms` is gone — the module merge made it redundant): ```js statum.document.reset(formOrElement); // reset the form owning the element statum.document.markDirty(el); // force an element dirty (prefill skips it) statum.document.isDirty(el); // query dirtiness ``` ## statum.markdown — the renderer as an API The CommonMark engine (unchanged, just moved) is callable directly, on the same code path the `stm-markdown` binding uses: ```js statum.markdown.render(src); // → safe HTML string statum.markdown.renderTo(el, src); // replaces el's content ``` Escape-first posture unchanged: raw source HTML is neutralised and `javascript:`/`data:` URLs never survive as hrefs. ## statum.registerAttribute — write your own bindings Module authors extend the render walk without touching core: ```js statum.registerAttribute('stm-foo', function (el, scope) { // called once per element per render cycle when el carries stm-foo }, { after: 'stm-html' }); // or before: 'stm-class.' ``` A trailing dot makes the name a **prefix matcher** (`'stm-class.'` matches `stm-class.hidden`); the handler then iterates the element's attributes itself. `after`/`before` name another registered attribute and insert relative to it — order matters (`stm-prefill` runs before `stm-class.*`; markdown renders before `stm-h-demotion` demotes). A constraint naming an unregistered attribute warns and keeps registration order; handlers get no per-handler try/catch — a throwing handler aborts that render cycle, exactly like the built-ins. The registry is provided by `statum-document.js` (it *is* the walk's machinery). Loading a registering module against core alone is safe: core queues the registration until document loads. ## The reenter directive A response can ask the client to re-fetch the page's entrypoint for the current URL and apply its directives in place — no page reload: ```json { "type": "reenter" } ``` Use it after an action that invalidates page-level state. `reenter` runs after `form-reset`/`form-submit` and before `navigate`/`post` (navigation still wins if both appear), and the refresh is awaited — the originating action's promise (and so `stm-clear-on-success` and `statum.act`) resolves only after the re-entered state has been applied. A failed refresh is logged and swallowed. ## Upcoming A dedicated binding module (beyond document's built-ins) is planned as a fourth script tag; when it lands, the expansion set and this file change again.