Context
Operator direction 2026-09-01 on the shipped lock action (#17949): start with disabling, enhance later — and make the content half easy to override per widget, the way reload already is. A downstream product needs "locked" to mean read-only for the pane's content (nested forms disabled, grid cell editing off), which a blanket inert body cannot express: inert also takes away scrolling and text selection, and the primitives own their own read-only semantics.
Live latest-open sweep: checked the latest 20 open issues at 2026-09-01T21:03:06Z, the last 30 A2A messages (all read-states), and the Knowledge Base ticket corpus; no equivalent. #17949 (merged) is the inert default this ticket keeps as the fallback.
The Problem
- The engine has no cascading read-only axis: only
form.field.Text and the Monaco wrapper declare a readOnly config; nothing propagates it, and grid cell editing has no switch. Building that cascade is a large, separate project.
- A pane author already knows what "locked" means for their content — a form container disables its fields, a grid turns cell editing off, a stream keeps scrolling. The engine cannot know, and inert is the honest blunt default when nobody tells it.
- The lock presentation today is engine-owned end to end (
Workspace#syncDockLockItemPresentation): pane.vdom.inert = true with a WeakMap snapshot of prior inert ownership, plus the neo-dock-pane-locked class (cursor: not-allowed + a hairline inset outline, resources/scss/src/dashboard/Container.scss). There is no seam for the pane to take over.
The Architectural Reality
- The delegation precedent is
reload (#17948 / #17966): Workspace#syncDockReloadAction probes typeof pane.dockReload on the live card — or its config's module prototype before the slot materializes — never a resolver call; handleDockReloadAction delegates to pane.dockReload() and otherwise runs the engine fallback.
- Structural protection is separate from content presentation and stays engine-owned:
setItemLocked fails closeItem / detachItem / source moves closed in the reducer, the close action hides, drag sources are suppressed (#17949 layered guards). This ticket touches only the content half.
- The revealed rail pane goes through the same presentation sync (
Workspace#syncDockLockRails → syncDockLockItemPresentation), so the seam must serve both the in-flow card and the reveal overlay's pane.
- The locked-pane cursor rule currently keys on the class; the DOM
inert attribute is the truthful discriminator between the engine default and a delegated pane.
The Fix
- The hook: a pane implementing
dockLock(locked: Boolean): void owns what lock means for its content. syncDockLockItemPresentation probes it exactly like reload (typeof on the live card, else on the config module prototype); when present it calls pane.dockLock(locked) on every transition and writes no inert — the pane restores its own state on dockLock(false). Absent hook → today's inert default, unchanged byte for byte.
- The visual cue splits along the truth:
neo-dock-pane-locked (the hairline frame) stays for both paths; cursor: not-allowed moves onto .neo-dock-pane-locked[inert], so a read-only delegated pane does not show a forbidding cursor over content the user may scroll and select.
- Both surfaces: the in-flow card and the revealed rail pane resolve the hook the same way; unlock reverses both.
- Docs: the
enableDockLockAction config JSDoc on Workspace carries the hook contract (mirroring the enableDockReloadAction wording); one pane example in the dock-lock component fixture.
- Witnesses: unit (
test/playwright/unit/dashboard/DockLockAction.spec.mjs): a pane with dockLock receives true then false, never gains inert; a pane without keeps the inert path and the exact-restore clause. Component (test/playwright/component/dashboard/DockLock.spec.mjs): a fixture pane whose dockLock disables a nested field — the field reads disabled while locked, enabled after unlock, and the pane body stays scrollable/selectable throughout.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
pane.dockLock(locked) (new pane hook) |
Workspace#syncDockLockItemPresentation |
delegated content presentation on lock and unlock; engine writes no inert |
absent → inert default with exact restore (unchanged) |
enableDockLockAction JSDoc |
unit + component witnesses |
.neo-dock-pane-locked[inert] cursor rule |
resources/scss/src/dashboard/Container.scss |
not-allowed only for the engine-inert default |
delegated panes keep the frame cue and a normal cursor |
SCSS comment |
component computed-style check |
Decision Record impact: aligned-with ADR 0029 §2.6 (container contract for embedded product surfaces: the pane stays an ordinary component the host talks to through explicit hooks).
Acceptance Criteria
Out of Scope
- A cascading read-only axis in the component tree, and per-primitive read-only semantics (form containers, grid cell editing, drag sources) — those are what a pane's
dockLock implementation calls into; they are not minted here.
- Changing the structural half of lock or its default-off flag.
- Any consumer-specific read-only implementation.
Avoided Traps
- A generic
readOnly sweep over descendants — it would need every primitive to agree on one config, handle children added later, and still guess wrong on buttons whose handlers mutate; the pane author knows, the engine does not.
- Keeping
inert beside a delegating pane — double ownership of the same DOM state is exactly what the WeakMap snapshot exists to avoid; delegation means the engine writes nothing.
Related
Refs #17949 (lock action) · #17948 / #17966 (reload delegation precedent) · #17836 (epic)
Origin Session ID: 6197a12f-acf2-478a-b05d-4ece32474259
Retrieval Hint: "dock lock content presentation delegate pane hook dockLock inert default reload precedent"
— Mnemosyne (Claude Fable 5.1 · Claude Code) · session 6197a12f-acf2-478a-b05d-4ece32474259
Context
Operator direction 2026-09-01 on the shipped lock action (
#17949): start with disabling, enhance later — and make the content half easy to override per widget, the wayreloadalready is. A downstream product needs "locked" to mean read-only for the pane's content (nested forms disabled, grid cell editing off), which a blanket inert body cannot express: inert also takes away scrolling and text selection, and the primitives own their own read-only semantics.Live latest-open sweep: checked the latest 20 open issues at 2026-09-01T21:03:06Z, the last 30 A2A messages (all read-states), and the Knowledge Base ticket corpus; no equivalent.
#17949(merged) is the inert default this ticket keeps as the fallback.The Problem
form.field.Textand the Monaco wrapper declare areadOnlyconfig; nothing propagates it, and grid cell editing has no switch. Building that cascade is a large, separate project.Workspace#syncDockLockItemPresentation):pane.vdom.inert = truewith a WeakMap snapshot of prior inert ownership, plus theneo-dock-pane-lockedclass (cursor: not-allowed+ a hairline inset outline,resources/scss/src/dashboard/Container.scss). There is no seam for the pane to take over.The Architectural Reality
reload(#17948/#17966):Workspace#syncDockReloadActionprobestypeof pane.dockReloadon the live card — or its config'smoduleprototype before the slot materializes — never a resolver call;handleDockReloadActiondelegates topane.dockReload()and otherwise runs the engine fallback.setItemLockedfailscloseItem/detachItem/ source moves closed in the reducer, the close action hides, drag sources are suppressed (#17949layered guards). This ticket touches only the content half.Workspace#syncDockLockRails→syncDockLockItemPresentation), so the seam must serve both the in-flow card and the reveal overlay's pane.inertattribute is the truthful discriminator between the engine default and a delegated pane.The Fix
dockLock(locked: Boolean): voidowns what lock means for its content.syncDockLockItemPresentationprobes it exactly like reload (typeofon the live card, else on the config module prototype); when present it callspane.dockLock(locked)on every transition and writes noinert— the pane restores its own state ondockLock(false). Absent hook → today's inert default, unchanged byte for byte.neo-dock-pane-locked(the hairline frame) stays for both paths;cursor: not-allowedmoves onto.neo-dock-pane-locked[inert], so a read-only delegated pane does not show a forbidding cursor over content the user may scroll and select.enableDockLockActionconfig JSDoc onWorkspacecarries the hook contract (mirroring theenableDockReloadActionwording); one pane example in the dock-lock component fixture.test/playwright/unit/dashboard/DockLockAction.spec.mjs): a pane withdockLockreceivestruethenfalse, never gainsinert; a pane without keeps the inert path and the exact-restore clause. Component (test/playwright/component/dashboard/DockLock.spec.mjs): a fixture pane whosedockLockdisables a nested field — the field reads disabled while locked, enabled after unlock, and the pane body stays scrollable/selectable throughout.Contract Ledger
pane.dockLock(locked)(new pane hook)Workspace#syncDockLockItemPresentationinertenableDockLockActionJSDoc.neo-dock-pane-locked[inert]cursor ruleresources/scss/src/dashboard/Container.scssnot-allowedonly for the engine-inert defaultDecision Record impact: aligned-with ADR 0029 §2.6 (container contract for embedded product surfaces: the pane stays an ordinary component the host talks to through explicit hooks).
Acceptance Criteria
dockLockis called withtrueon lock andfalseon unlock, on the in-flow card and on the revealed rail pane, and never receives an engineinertwrite.#17949exact-restore witness stays green).closeItem/detachItem/ source moves.cursor: not-allowedapplies only while the pane root carries theinertattribute; a delegated locked pane shows the frame cue with a normal cursor.typeofon the live card or its config module prototype, no resolver call (mirrorssyncDockReloadAction).Out of Scope
dockLockimplementation calls into; they are not minted here.Avoided Traps
readOnlysweep over descendants — it would need every primitive to agree on one config, handle children added later, and still guess wrong on buttons whose handlers mutate; the pane author knows, the engine does not.inertbeside a delegating pane — double ownership of the same DOM state is exactly what the WeakMap snapshot exists to avoid; delegation means the engine writes nothing.Related
Refs #17949 (lock action) · #17948 / #17966 (reload delegation precedent) · #17836 (epic)
Origin Session ID: 6197a12f-acf2-478a-b05d-4ece32474259
Retrieval Hint: "dock lock content presentation delegate pane hook dockLock inert default reload precedent"
— Mnemosyne (Claude Fable 5.1 · Claude Code) · session 6197a12f-acf2-478a-b05d-4ece32474259