LearnNewsExamplesServices
Frontmatter
id18044
titleLock content presentation delegates to the pane, like reload
stateClosed
labels
enhancementaicore
assigneesneo-fable
createdAtSep 1, 2026, 11:06 PM
updatedAtSep 2, 2026, 1:25 AM
githubUrlhttps://github.com/neomjs/neo/issues/18044
authorneo-fable
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 1:25 AM

Lock content presentation delegates to the pane, like reload

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementaicore
neo-fable
neo-fable commented on Sep 1, 2026, 11:06 PM

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

  1. 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.
  2. 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.
  3. Both surfaces: the in-flow card and the revealed rail pane resolve the hook the same way; unlock reverses both.
  4. Docs: the enableDockLockAction config JSDoc on Workspace carries the hook contract (mirroring the enableDockReloadAction wording); one pane example in the dock-lock component fixture.
  5. 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

  • A pane implementing dockLock is called with true on lock and false on unlock, on the in-flow card and on the revealed rail pane, and never receives an engine inert write.
  • A pane without the hook keeps today's behavior exactly: inert while locked, prior inert ownership/value restored on unlock (the #17949 exact-restore witness stays green).
  • Structural protections are unaffected by the hook: close hidden, drag sources suppressed, reducer rejections on closeItem / detachItem / source moves.
  • cursor: not-allowed applies only while the pane root carries the inert attribute; a delegated locked pane shows the frame cue with a normal cursor.
  • The probe is pure: typeof on the live card or its config module prototype, no resolver call (mirrors syncDockReloadAction).
  • The Workspace JSDoc documents the hook next to the reload delegation contract.

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