Describe the Widgets masonry with owner column spans in the docs

DESIGN "Widgets board" now states what the owner accepted on 2026-10-03
after rejecting the ordered-rows frame: a masonry in the owner's order,
packed the way a Pinterest board is; a card's width as a column span
(1, 2 or 3 columns, or Full width) that replaces the author's `span`
until Reset size; that a board where the owner set no width is exactly
the board before owner widths existed; and the limit he accepted with
it: widths that do not tile leave a gap, the board does not backfill,
and the owner closes it by changing a width or the order. A card whose
height changes makes the masonry pack again, so later cards can move.

CREATING_SKILLS describes `span` as the default column span. The
architecture chapters 01 and 03 and the design-system chapter name
widget_size.js beside masonry.js, the widths controller and the masonry
widths browser lane; their grants are resized to the exact growth, and
the data-layout inventory is regenerated.
This commit is contained in:
Ouroboros 2026-10-03 23:33:37 +03:00
parent 52c26ab353
commit 75839f2448
9 changed files with 60 additions and 47 deletions

View file

@ -1066,12 +1066,13 @@ is the only hook into it.
These geometry keys are valid only for framed `iframe` and `module` renders;
declarative renders remain content-driven and reject them.
Any render kind may add `span`: `1` (the default) or `2`, the card's starting
width on the Widgets board — a third or two thirds of a row (`grid_span` is
read as an alias; a larger number counts as `2`, anything else as `1`). It is
a default only: the owner's width from the card menu or the card's edge wins
for that card until the owner resets it, and the card's height always follows
its content (the board: `docs/DESIGN.md` "Widgets board").
Any render kind may add `span`: `1` (the default) or `2`, the card's default
column span on the Widgets masonry (`grid_span` is read as an alias; a larger
number counts as `2`, anything else as `1`). The board fits it to the columns
it has (a narrow list is one column), and the owner's width from the card
menu or the card's edge (1, 2, 3 columns or full width) wins for that card
until the owner resets it; the card's height always follows its content (the
board: `docs/DESIGN.md` "Widgets board").
For auto-height module CSS, prefer one owner of padding and box geometry between
`body` and `#root`, and choose `border-box` deliberately. Nested percentage

View file

@ -1202,43 +1202,55 @@ controls remain live; the unfinished checkpoint never erases the outcome.
The Widgets page is a board of live cards. The owner arranges the board; each
widget's content belongs to its author.
- **Rows in the owner's order.** Cards stand in rows on a 12-column base, in
the order the owner set with a card's move handle; a card that does not fit
the rest of a row starts the next one. Nothing is measured to place a card,
so a card whose content grows only makes its own row taller and never sends
a neighbour to another column. A new widget joins the end of the order; the
- **A masonry in the owner's order.** Cards are packed into columns the way a
Pinterest board is: in the order the owner set with each card's move handle,
every card goes to the lowest free place its width fits, so cards of
different heights stack without the voids of a row layout. The board decides
how many columns it has from the list's width and from the widths its cards
ask for. A board where the owner has set no width is exactly the board
before owner widths existed. A new widget joins the end of the order; the
order is stored when the owner moves a card, so after a window reload the
widgets added since the last move follow the stored ones alphabetically. A
widget whose skill is off is not drawn; its place in the order (moving the
other cards keeps its slot) and its width stay stored for its return, and
nothing is reserved meanwhile.
- **Gaps belong to the owner.** A short card beside a tall one leaves space
under it (cards align to the top of their row), and widths that do not fill
a row leave the rest of it empty. The owner closes a gap by choosing widths;
the board never repacks cards out of order to hide one.
- **Width is a step, the owner's over the author's.** A card is One third,
Half, Two thirds or Full width of a row. Until the owner picks one, the
author's `span` decides: `1` is a third, `2` two thirds. The owner's choice
holds for that card on every screen of the installation until Reset size
returns the author's default.
- **Width is a column span, the owner's over the author's.** A card spans 1,
2 or 3 columns, or Full width. Until the owner picks one, the author's `span`
decides (`1` one column, `2` two), and the board may widen a lone one-column
card beside wide ones to fill its lane. The board never widens or narrows a
width the owner chose, except that no card is wider than the board: 3
columns on a board of 2 is the whole row. Full width is always the whole
row. Because the column count follows the widths the cards ask for, a
card's width can make the other cards' columns wider or narrower. The
owner's choice holds for that card on every screen of the installation
until Reset size returns the author's default.
- **Widths that do not tile leave gaps; the board does not backfill.** A
card that cannot fit beside the cards above it starts below them: a
3-column card after a 2-column one on a 4-column board leaves the column
beside the first card empty above it. The board never repacks cards out of
the owner's order to hide such a gap; the owner closes it by changing a
width or the order.
- **Height follows content.** A card is as tall as its widget: a table shows
every row, a framed widget its declared or reported frame height
(`docs/CREATING_SKILLS.md`). No card is pinned to a height.
- **A narrow list is one column.** Below 720px of list width — a phone, the
Telegram mini app, a narrow window, a wide side panel — the cards stack at
full width in the same order and widths do not apply; the board returns
when the list is wide again. The list's own width decides, not the viewport.
(`docs/CREATING_SKILLS.md`). No card is pinned to a height. When a card
grows or shrinks, the masonry packs again, so cards after it can move,
even to another column.
- **A narrow list is one column.** When the list is too narrow for two
columns of 280px — a phone, the Telegram mini app, a narrow window — the
cards stack at full width in the same order and widths do not apply; the
board returns when the list is wide again. The list's own width decides,
not the viewport.
- **The card menu works on every surface.** Every card's ⋮ menu carries Size
(the four steps and Reset size) beside a framed card's launch policy, so a
touch screen, a keyboard or the mini app — no hover there, and a vertical
drag collapses it — can size any card; on the stacked column the menu says
that widths apply when the list is wide. On the desktop board a card's right
edge can also be dragged between steps (Escape cancels) or stepped with its
arrow keys.
- **Arranging never restarts a widget.** Reordering, resizing or switching
between board and column changes only where a card is painted; a running
widget keeps its frame and its state. Keyboard focus keeps the card's
original position until the window reloads.
(1, 2, 3 columns, Full width and Reset size) beside a framed card's launch
policy, so a touch screen, a keyboard or the mini app — no hover there, and
a vertical drag collapses it — can size any card; on a narrow list the menu
says that widths apply when the list is wide. On a board of two or more
columns a card's right edge can also be dragged between steps (Escape
cancels) or stepped with its arrow keys.
- **Arranging never restarts a widget.** Reordering, resizing or a change of
the column count changes only where a card is painted; a running widget
keeps its frame and its state. Keyboard focus keeps the card's original
position until the window reloads.
- **One arrangement per installation.** Order and widths are owner UI state
stored on the server, the same for every window and device of the
installation; when two windows arrange at once the last change wins. A

View file

@ -34,7 +34,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
│ ├── modules/onboarding_agents_step.js, onboarding_overlay.js, project_create.js, utils.js ← The first-run "Connect your accounts" step; the framed wizard's sandbox policy, kept in one place because it is a security boundary; the New Project dialog; shared escaping/formatting utilities (§2; §3 Project rooms)
│ ├── modules/review_presentation.js, review_dom_patch.js, harness_presentation.js ← Read-side only: Review Checkpoint grouping, keyed DOM reconciliation, neutral harness identity (`executorIdentityMarkup`, the one markup owner) (§3 Child cards and executor presentation)
│ ├── modules/acceptance_incident_presentation.js ← Local evidence-preparation warning and resolution; no reviewer authority (§6 Task acceptance)
│ └── modules/widgets.js + widget_module.js + widget_frame.js + widget_card.js + widget_reorder.js + widget_chart.js + widget_list.js + widget_grid.js ← Widgets page host (`mountTab`, card registry, declarative renderer); the two framed mounts (extension-route iframe; module `srcdoc` iframe with its CSP/sandbox constants, bridge and disposer); the child-side module bootstrap; card chrome (framed launch policy incl. `retain`, Start/Stop; the width menu); reorder handles and widths (`createWidgetWidths`); chart/table helpers; the list-request seam — `requestWidgetListPayload`/`requestWidgetCards` and the `widgetListRequests()` controller: one `AbortController` and the shared `WIDGET_REQUEST_TIMEOUT_MS` deadline over the list AND preferences reads, sibling abort on failure, disposal abort, typed timeout error for the existing Retry — plus the pure list helpers (change signatures, keyed patch plan); the board that measures nothing and writes only `--widget-*` properties (§3 Skills and Widgets)
│ └── modules/widgets.js + widget_module.js + widget_frame.js + widget_card.js + widget_reorder.js + widget_chart.js + widget_list.js + widget_size.js + masonry.js ← Widgets page host (`mountTab`, card registry, declarative renderer); the two framed mounts (extension-route iframe; module `srcdoc` iframe with its CSP/sandbox constants, bridge and disposer); the child-side module bootstrap; card chrome (framed launch policy incl. `retain`, Start/Stop; the width menu); reorder handles and widths (`createWidgetWidths`); chart/table helpers; the list-request seam — `requestWidgetListPayload`/`requestWidgetCards` and the `widgetListRequests()` controller: one `AbortController` and the shared `WIDGET_REQUEST_TIMEOUT_MS` deadline over the list AND preferences reads, sibling abort on failure, disposal abort, typed timeout error for the existing Retry — plus the pure list helpers (change signatures, keyed patch plan); the masonry that takes the owner's column spans and writes only `--masonry-*` properties (§3 Skills and Widgets)
│
├── supervisor/ ← Background thread inside server.py (§5)
│ ├── active_activity.py ← process-local owner of in-flight native chat actors (`DirectActivityRegistry`): private handles for controls and the writer drain, public snapshots for `/api/state` `active_direct_turns` and WS typing frames; no queue records (§3 Direct turns and the activity block)

View file

@ -93,7 +93,7 @@ Shared primitives keep pages from acquiring competing contracts — frontend wor
- `chat_markdown.js` ← marked+DOMPurify, chat URLs (http(s), mailto, `/api/files/download`), KaTeX (no single `$`), lazy Mermaid, bounded charts, unloaded images (DESIGN §5). `mountChatMarkdown` binds HTML to `.ui-rich-content` on existing hosts; bubble/card owners retain enhancement/teardown and density. `chat.js` binds its column's tables (`bindMarkdownTables`); it disposes both. Vendors: `web/vendor/VENDOR-MANIFEST.md`.
- `log_events.js` ← event classification, shared technical outcome reducers and the one factual task-presentation projection for Chat and Logs: task truth maps only to `Working` / `Done` / `Done with warnings` / `Failed` / `Cancelled`; it owns no actions, incidents or notifications, compact headlines never expose raw reason codes, and a task-authored message is visible in the receiving task's timeline (`task_message_injected`, sender by value) and in the sender's (`task_message_routed`, written or refused with the host reason).
- `effort_evidence.js` ← Logs: requested effort, host carrier, engine preparation, sourced provider observation; missing echoes stay unknown.
- `toast.js`, `widget_grid.js`, `widget_frame.js`, `widget_job.js`, CSS tokens ← notifications, layout, framed-widget bootstrap/lifecycle, bounded widget request/job policy.
- `toast.js`, `masonry.js`, `widget_frame.js`, `widget_job.js`, CSS tokens ← notifications, layout, framed-widget bootstrap/lifecycle, bounded widget request/job policy.
- `result_files.js` ← a settled task card's one Files row: root files plus one folder record per nested tree with its member tooltip, links only from the host's `artifacts`/`artifact_archives`.
- `history_retention.js` ← history-transfer details and storage problems for Chat and Logs; routine transfer does not change task outcome or collapsed-card state.
- `settled_card.js` ← `syncSettledItems`, the settled card's one seam: its Files row and Continue action follow the same detail.
@ -251,7 +251,7 @@ Widgets is a separate page because extension UI is an execution surface, not cat
The list and UI-preferences reads share one `WIDGET_REQUEST_TIMEOUT_MS` authority. Each reconcile owns one `AbortController` and a 25-second deadline covering response headers and body. The failure asymmetry is deliberate: a timeout or a cards-list failure aborts the preferences sibling, leaves the last good cards in place and exposes the existing Retry with an explicit error, while a preferences failure degrades to `null` (last known order kept) and card loading continues. A cards reply without an `ui_tabs` array is refused, never read as an empty list, and a cancelled body read stays a cancellation (`fetchJson` rethrows `AbortError`), so navigating away can never stop a kept-running frame. Navigation, page disposal and pagehide abort owned requests, and only the current render generation may paint or enable controls. Hidden retained frames keep their keyed lifecycle; the list read itself has no durable write side effect.
Three render modes share one capability set (`sandbox="allow-scripts allow-pointer-lock allow-downloads"`, `allow="autoplay; fullscreen; clipboard-write"`, never `allow-same-origin`; author-facing contract: `docs/CREATING_SKILLS.md`). An extension-route iframe (`kind: iframe`) is the skill's own page with no bridge and must declare the `route` it loads; the frozen registration contract still accepts an omitted route, which paints a not-supported card rather than a widget. A declarative widget is rendered by host-owned code from a validated schema. A reviewed module widget runs in an opaque-origin `srcdoc` iframe under a document CSP that `web/modules/widget_module.js` builds from the page origin (`default-src 'none'`, no `connect-src`; scripts only inline, `blob:` and the skill's module prefix, with `'wasm-unsafe-eval'`). Framed declarations may set a bounded `height` (320–8,192 px); a module without one starts at the floor and reports its `#root` content height through the nonce-bound bridge, capped by an optional module-only `max_height`; legacy route iframes stay explicit-height-only because an opaque document cannot be measured, and geometry keys are rejected for declarative renders. Card order (`widget_order`), card widths (`widget_size`) and the per-card launch-policy override (`widget_start_mode`) are host UI state in `/api/ui/preferences`. The board (semantics: DESIGN "Widgets board") is a 12-column CSS grid of ordered rows: `web/modules/widget_grid.js` writes only `--widget-w`/`--widget-order` per card and the list's `data-widget-layout`, which a list-width observer (older WebKitGTK lacks `@container`) sets to one column below 720px. Rows replace measured packing, which moved untouched neighbours whenever one card grew; nothing is measured. `createWidgetWidths` (`web/modules/widget_reorder.js`) writes widths one request at a time; the server merges them by card key (null deletes; last write wins). No reorder or resize moves a node or reloads a frame (disclosed residual: Tab order follows the DOM until a reload). Engineering rules: `docs/DEVELOPMENT.md` "Embedded surfaces declare geometry and refresh semantics", "UI resources carry a disposer", "Declarative widgets".
Three render modes share one capability set (`sandbox="allow-scripts allow-pointer-lock allow-downloads"`, `allow="autoplay; fullscreen; clipboard-write"`, never `allow-same-origin`; author-facing contract: `docs/CREATING_SKILLS.md`). An extension-route iframe (`kind: iframe`) is the skill's own page with no bridge and must declare the `route` it loads; the frozen registration contract still accepts an omitted route, which paints a not-supported card rather than a widget. A declarative widget is rendered by host-owned code from a validated schema. A reviewed module widget runs in an opaque-origin `srcdoc` iframe under a document CSP that `web/modules/widget_module.js` builds from the page origin (`default-src 'none'`, no `connect-src`; scripts only inline, `blob:` and the skill's module prefix, with `'wasm-unsafe-eval'`). Framed declarations may set a bounded `height` (320–8,192 px); a module without one starts at the floor and reports its `#root` content height through the nonce-bound bridge, capped by an optional module-only `max_height`; legacy route iframes stay explicit-height-only because an opaque document cannot be measured, and geometry keys are rejected for declarative renders. Card order, card widths (`widget_size`) and the per-card launch-policy override (`widget_start_mode`) are host UI state in `/api/ui/preferences`; the masonry (`web/modules/masonry.js`) packs cards in that order, each spanning the owner's column count or else the author's `span` (semantics: DESIGN "Widgets board"), and writes only `--masonry-*` custom properties, so a reorder or resize never moves a node or reloads a frame (disclosed residual: Tab order follows the DOM until a reload). `createWidgetWidths` (`web/modules/widget_reorder.js`) writes widths one request at a time; the server merges them by card key (null deletes; last write wins). Engineering rules: `docs/DEVELOPMENT.md` "Embedded surfaces declare geometry and refresh semantics", "UI resources carry a disposer", "Declarative widgets".
Framed cards start under a launch policy — the owner's override over the author's validated `render.start` over the kind default (module and route iframe → `manual`, declarative → `auto`), a pure function in `web/modules/widget_card.js` — with one primary Start/Stop control and a policy menu (Auto / Manual / Keep running). `retain` keeps a framed card mounted while Widgets is hidden and ends it on Stop, when its skill leaves the live list, on a changed `revision` and with the window, never with the server alone. A mounted widget owns its resources through one disposer: a module widget is stopped in order with acknowledgement (`ouro-widget-disposed`, or `WIDGET_DISPOSE_ACK_TIMEOUT_MS` — one second), with one mount in flight per card key so a remount waits for the pending stop; a route iframe disposes synchronously. Poll and WebSocket writers use monotonic progress per job, so an older response cannot rewind a newer event. Chart.js is bundled locally; rendering must not depend on a third-party CDN.

View file

@ -9,7 +9,7 @@ This chapter owns the engineering rules that preserve the visual and interaction
- **Typography on a migrated surface** names a `--type-*` size token AND a named foreground token in every text declaration: a rule with a size and no colour is the exact defect that made secondary text inherit near-white primary ink. `tests/test_web_typography_static.py` keeps the class closed on the migrated families/regions; extending that guard and migrating its subject are the same commit (DESIGN §8 names the boundary).
- **The variable contract** is checked in BOTH directions across the whole stylesheet by the same test file: every `var(--x)` must resolve — an undeclared one silently renders its hardcoded fallback, which becomes the real value nobody can find — and every `:root` token must have a reader, because a token that resolves nowhere is what makes surfaces reach for literals. Each document resolves against the sheets it actually loads, and neither page may shadow a shared palette name. Fix a dangling name by pointing it at an existing token, not by declaring a new one.
- **Appearance is client-local.** Keep the choice and radiogroup in `theme.js`, outside Settings' `s-` input collector. Theme-dependent images use whole-image tokens (`--select-arrow`), and selected warning controls use the semantic foreground/background/border triple rather than a raw hue. Mounted charts update in place; Mermaid retains source before any async work and rejects stale epochs. Every theme subscription has an owner and disposer; `onTheme` is opt-in, resolved only, author-styled. Request persistent desktop storage at every `webview.start`; verify a rebuilt launcher through quit/relaunch, not a server restart. Storage/iframe boundaries and mechanisms live in ARCHITECTURE §3. Regressions: `web/tests/theme.test.js`, `theme_palette.test.js`, `chat_markdown_theme.test.js`, `evolution_theme.test.js`, `tests/test_appearance_static.py`, and the `ui_browser` `tests/test_theme_browser.py`.
- **Layout and controls.** Top-level pages use a fixed `renderPageHeader` outside an independently scrolling body; page icons come from `web/modules/page_icons.js`; primary actions, Refresh included, live in the `renderPageHeader({ actionsHtml })` slot. Tab strips are one design-system control (`renderTabStrip` + `bindTabStrip` in `page_header.js`, `.app-tab-strip`/`.app-tab`, the `--pill-*` tokens): the binder owns selected state, ARIA, roving focus and strip-only reveal, callbacks own loading and panels, programmatic `select()` never calls them, and the binder and its resize observer are disposed with the owning page. The navigation column's height must not grow with the number of items in a collection inside it; a variable-length collection owns a bounded window with its own scroll (`--nav-projects-list-max-height`). Scroll bodies share `.scroll-fade-y` — except a dense list whose rows are shorter than the 32px edge, where the fade would cover a whole row — and `scroll_fade.js::bindScrollFade` returns the disposer for its observer and listener. The Widgets board follows DESIGN "Widgets board": `web/modules/widget_grid.js::applyWidgetGrid` lays cards out in ordered CSS Grid rows, writes only `--widget-*` custom properties and never measures a card; the gaps rows leave are accepted and closed by the owner's widths, because measured packing moved untouched neighbours. Never move `<article>` nodes to reorder or resize, because a moved `<iframe>` reloads; a card's label and menu carry no fact about its neighbours (`web/tests/widget_card.test.js`). Widget order, widths (`widget_size`) and the per-card start-mode override (`widget_start_mode`, values from `extension_ui_validation.WIDGET_START_MODES`) persist through `/api/ui/preferences` + `data/state/ui_preferences.json`, never in extension manifests. A new visual dimension becomes a CSS variable first and is consumed by shared classes; new inline `style=""` markup and `.style.<property>` assignments are review debt, except a dynamic measured value updating a narrowly named custom property when that is the real runtime data flow.
- **Layout and controls.** Top-level pages use a fixed `renderPageHeader` outside an independently scrolling body; page icons come from `web/modules/page_icons.js`; primary actions, Refresh included, live in the `renderPageHeader({ actionsHtml })` slot. Tab strips are one design-system control (`renderTabStrip` + `bindTabStrip` in `page_header.js`, `.app-tab-strip`/`.app-tab`, the `--pill-*` tokens): the binder owns selected state, ARIA, roving focus and strip-only reveal, callbacks own loading and panels, programmatic `select()` never calls them, and the binder and its resize observer are disposed with the owning page. The navigation column's height must not grow with the number of items in a collection inside it; a variable-length collection owns a bounded window with its own scroll (`--nav-projects-list-max-height`). Scroll bodies share `.scroll-fade-y` — except a dense list whose rows are shorter than the 32px edge, where the fade would cover a whole row — and `scroll_fade.js::bindScrollFade` returns the disposer for its observer and listener. Masonry packing uses `web/modules/masonry.js::applyMasonry` (CSS Grid row packing leaves gaps under shorter cards): it packs in the page's key order, takes the owner's column spans by key (DESIGN "Widgets board") and writes only `--masonry-*` custom properties; never move `<article>` nodes to reorder or resize, because a moved `<iframe>` reloads; a card's menu carries no fact about its neighbours (`web/tests/widget_card.test.js`). Widget order, widths (`widget_size`) and the per-card start-mode override (`widget_start_mode`, values from `extension_ui_validation.WIDGET_START_MODES`) persist through `/api/ui/preferences` + `data/state/ui_preferences.json`, never in extension manifests. A new visual dimension becomes a CSS variable first and is consumed by shared classes; new inline `style=""` markup and `.style.<property>` assignments are review debt, except a dynamic measured value updating a narrowly named custom property when that is the real runtime data flow.
- **Containment.** A control never widens its column. Horizontal overflow lives in the wrapper that owns the wide content and declares `overflow-x: auto` (code block, `.md-table-wrap`, tab strip, Costs table cells) — never in a page scroll body, whose `overflow-y: auto` alone already makes `overflow-x` compute to `auto`. The shared `select.ui-control` recipe therefore clips its own value (`overflow: hidden`): WebKit computes `overflow: visible` on a native select, so an unclipped option label becomes scrollable overflow of the page scroller. A grid track holding controls takes a minimum that yields to its container — `minmax(0, …)` or `repeat(auto-fit, minmax(min(100%, Npx), 1fr))`; a fixed px minimum rescued only by a viewport media query is review debt, because the viewport does not know how wide the content column is. The global webkit scrollbar recipe sizes both axes. Enforced by `tests/test_web_typography_static.py::test_select_control_clips_its_value` and its `::test_webkit_scrollbar_recipe_covers_both_axes` neighbour, `tests/test_ui_settings_overflow_browser.py` (WebKit, the native-select clip) and `tests/test_ui_settings_grid_tracks_browser.py` (Chromium, yielding tracks). Two gaps stay open: the wizard document loads `ui.css` without `style.css` and keeps native scrollbars, and an element setting the standard `scrollbar-width`/`scrollbar-color` opts out of the webkit recipe on Blink.
- **One semantic button variant expresses one action role**: neutral Settings and onboarding controls use the existing `.btn.btn-default`; a one-action result row uses the named `.settings-action-row` contract (status first, action docked right); notifications use the shared toast host. Working, warning, error and destructive states keep one meaning across Chat, Logs, Settings and Skills. Enforced by `web/tests/settings_action_row.test.js` (the shared `.btn-default` role in both shells, the `.settings-action-row` contract and shared busy/status semantics); the toast-host and cross-page state-meaning clauses are review-only (CHECKLISTS item 30).
- **One owner intent has one control, built in one module** (semantics: DESIGN
@ -96,7 +96,7 @@ A module handler that calls `OuroborosWidget.openExternal(url)` or `window.open(
`web/modules/widgets.js` is the host for reviewed widget declarations — forms/actions, text/data/media, tabs/charts, async jobs, files, map/calendar/kanban, and composition through `group`, `metric` and `callout` (the author-facing schema, depth/node limits and composition rules: `docs/CREATING_SKILLS.md`; the render modes, frame CSP and bridge: ARCHITECTURE §3 "Widgets page" and "Module widget bridge"). The host-side rules: nested interactive components use stable identity and one disposer, and `subscription.render` is transitively passive. Data updates patch the existing component/field nodes at the mount's identity seam, preserving selection, composition, native popup state and password input; passwords stay only in their mounted control, never in the retained form-value snapshot. Forms and actions own visible pending/result/error feedback by component id — an optional status component or a sibling's shared data target is not that action's result. Preserve the existing job identity, bounded-retry and disposal contracts; long-running actions use a durable job id and resumable status polling. Escape text and attributes for their actual HTML contexts, constrain media to extension routes or safe data URLs, and keep charts accessible through a semantic table. Rare `kind: "module"` UI runs only in the sandboxed opaque-origin iframe — never load skill JavaScript into the SPA origin. A failed list read exposes contextual Retry through the keyed list reconciliation, preserving unchanged frames and the owner's Stop choices; do not turn Retry into a global refresh/remount. Every timer, listener, observer, stream, abort controller, chart and mounted widget has a paired disposer.
Enforcement: `tests/test_widgets_ui_static.py` at commit tier; in the release-tier `ui_browser` lane `tests/test_widgets_ui_browser.py` (geometry, job retry), `tests/test_widgets_ui_browser_lifecycle.py` (launch policy, ordered stop, `retain`, the streaming bridge), `tests/test_widgets_ui_browser_patch.py` (keyed patch of a running card, reconnect reconcile), `tests/test_widgets_ui_browser_grid.py` (the board: rows, widths, the stacked column) and `tests/test_widgets_ui_browser_capabilities.py` (the frame CSP, sandbox and permissions boundary on Chromium and WebKit) — run all five before a release that touched Widgets. `tests/test_widgets_ui_browser_identity.py` additionally pins retained interactive nodes, composition/password lifetime, local action feedback and non-destructive list Retry through real declarative consumers.
Enforcement: `tests/test_widgets_ui_static.py` at commit tier; in the release-tier `ui_browser` lane `tests/test_widgets_ui_browser.py` (geometry, job retry), `tests/test_widgets_ui_browser_lifecycle.py` (launch policy, ordered stop, `retain`, the streaming bridge), `tests/test_widgets_ui_browser_patch.py` (keyed patch of a running card, reconnect reconcile), `tests/test_widgets_ui_browser_widths.py` (the masonry board, owner widths, the narrow stack) and `tests/test_widgets_ui_browser_capabilities.py` (the frame CSP, sandbox and permissions boundary on Chromium and WebKit) — run all five before a release that touched Widgets. `tests/test_widgets_ui_browser_identity.py` additionally pins retained interactive nodes, composition/password lifetime, local action feedback and non-destructive list Retry through real declarative consumers.
### Optional author controls

View file

@ -2,7 +2,7 @@
Machine extraction of the `docs/ARCHITECTURE.md` "Data layout (`~/Ouroboros/`)" tree — the durable-file orientation carrier (this tree's counterpart of the reference PERSISTENCE_OWNERS derivation checklist) — regenerated by `python scripts/regenerate_inventories.py`. Do not edit. Every entry is probed against reality: repo entries must exist as tracked paths; data-plane entries must appear as a literal in the runtime sources that construct them. A durable file renamed or removed in code while its tree row survives = red (`tests/test_generated_inventories.py`).
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 641-734; UTF-8 SHA-256 `50632a1512c5e7ba8ee334216d3fe41b16eb5898dd413e93c6ce3ba977aef214`.
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 641-734; UTF-8 SHA-256 `3cf2258bafcbb9a11d6343037958ba135f9b647822c0097011262fca59d003b8`.
- entries: **83** (code-ref: 76, repo-dir: 6, repo-path: 1)

View file

@ -1,2 +1,2 @@
96
The module map names widget_grid.js (the board) in place of masonry.js, every card's width menu and the width controller, and the preferences row names widget_size.
126
The module map names widget_size.js (the owner's width steps) beside masonry.js, every card's width menu and the widths controller, and that the masonry takes the owner's column spans; the preferences row names widget_size.

View file

@ -1,2 +1,2 @@
500
Replace the masonry sentence with the board's mechanism and rationale: the widget_size key, custom-property-only rows on a 12-column grid, the list-width observer instead of @container, why rows replace measured packing, and the merge-by-key width write; DESIGN owns the board semantics.
296
The Widgets paragraph keeps the masonry and adds the widget_size key, that each card spans the owner's column count or else the author's span (DESIGN "Widgets board"), that a resize never moves a node either, and the merge-by-key width write.

View file

@ -1,2 +1,2 @@
344
Replace the masonry rule and its "CSS Grid row packing leaves gaps" motivation with the board rule and the honest motivation (gaps are accepted and owned by the owner's widths), the card-label scale rule, the widget_size preference, and the board browser lane in the Widgets enforcement list.
279
The masonry rule keeps its motivation and adds that it takes the owner's column spans by key, the card-menu scale rule, the widget_size preference, and the masonry widths browser lane in the Widgets enforcement list.