LearnNewsExamplesServices
Frontmatter
title>-
authorneo-opus-grace
stateClosed
createdAt1:43 AM
updatedAt1:58 AM
closedAt1:58 AM
mergedAt
branchesagent/14654-autohide-rail-affordanceagent/14660-autohide-reveal-dismiss
urlhttps://github.com/neomjs/neo/pull/14907
contentTrust
projected
quarantined0
signals[]
Closed
neo-opus-grace
neo-opus-grace commented on 1:43 AM

Resolves #14660

Ships line D2 — the reveal/dismiss interaction contract — on top of the D1 rail (stacked on agent/14654-autohide-rail-affordance, PR #14906; rebase onto dev + retarget follows its merge). The timing brain is DockRevealStateMachine, a pure, timer-injectable class with the full states/transitions table in JSDoc and named design constants (DWELL_MS, DISMISS_GRACE_MS; the 200/160ms slide durations stay in CSS where animation belongs). DockRail upgrades click from the D1 interim (direct restore) to the contract default — click = focused transient reveal, re-click dismisses — with hover-reveal strictly behind the workspace opt-in (autoHideRevealOnHover, dwell-gated, never steals focus), threaded through the adapter from projection options. DockRevealOverlay is the presentation host: edge-anchored, sized from the last committed split extent (new pure DockLayoutAdapter.resolveRevealExtent()) else a configurable fraction, translating DOM reality into semantic intents the machine consumes. The persist path is the overlay pin: setItemPinned(true) through the same reducer contract as every dock affordance — dismissal in every form emits nothing.

Evidence: L2 (unit contract specs — 190/190 dashboard suite green, incl. 13 machine specs on injected fake timers) → L2 required (close-target ACs are spec-checkable: state table, focus-hold, executor-routed pin, never-persist). Residual: none — live hover/focus DOM verification rides the Demo-A showcase + the reserved tour e2e (#14591).

AC map

  • State machine documented; grace constants namedDockRevealStateMachine JSDoc carries the states table and the transitions table; DWELL_MS / DISMISS_GRACE_MS are named statics, workspace-overridable via rail configs (revealDwellMs, revealDismissGraceMs).
  • Focused pane never auto-dismisses → focus-hold specs at BOTH altitudes: machine (focus-hold: a focused reveal never auto-dismisses) and rail↔overlay binding (bindRevealOverlay … focus-hold holds).
  • Pin escape = executor op, no parallel pathonRevealPinRequestedcommitOperation({operation: 'setItemPinned', pinned: true}) → callback / DockZoneModel.applyOperation(); the model clears autoHidden itself (landed guard). Specs cover commit, policy block, and stale-projection executor rejection.
  • Reveal state never persists → dedicated spec: a full reveal/dismiss cycle leaves the document byte-identical and fires zero document-change notifications.
  • Cross-family review → requested after the stack rebases onto dev (per stacked-PR review semantics).

Deltas from ticket

  • ADR precedence applied to the ticket's hover framing: the ticket headlines "hover-reveal", but its own cited authority (the docking design record's interaction table) mandates click-reveal as DEFAULT with hover as an a11y-gated workspace opt-in. Both paths shipped; the default is click. The Demo-A S3 hover choreography runs on the opt-in config (flagged to the screenplay owner via A2A at intake).
  • D1's disabled-tab treatment migrates to the overlay pin control: composing D1+D2 revealed that a pinnable: false item with a disabled tab would become unreachable content — item loss. Reveal is policy-free; only PIN is policy-gated. The D1 tab-disable code and specs are superseded in this stack.
  • Pointer-away grace applies to unfocused reveals only — focused reveals dismiss on focus-leave/Escape/outside-click per the contract; this is the focus-hold rule, made explicit in the state table.
  • The pane-side collapse control (tuck a visible pane to its rail) is out of scope here — it lives with the pin-controls line (D4), not the rail's interaction layer.
  • Pane hosting is a documented slot contract (<overlay id>__pane-slot): the consuming workspace mounts/clears the revealed pane, same seam as resolveComponentRef — keeps the overlay pane-blind at this altitude.

Test Evidence

npm run test-unit -- test/playwright/unit/dashboard/
190 passed (30.9s)

New: DockRevealStateMachine.spec.mjs (13 — dwell, grace, focus-hold, retarget, fail-close, destroy, all on injected fake timers), DockRevealOverlay.spec.mjs (5 — visibility incl. grace window, sizing, pin policy mirror, intent translation). Rewritten: DockRail.spec.mjs (10 — click-reveal, never-persist, pin escape, policy, hover opt-in, overlay binding, fail-close). Extended: DockLayoutAdapter.spec.mjs (resolveRevealExtent cases + hover opt-in threading).

Post-Merge Validation

  • Rebase onto dev + retarget after PR #14906 merges; full CI on the merge candidate before cross-family approval.
  • Demo-A scene-3 choreography drives reveal/dismiss live (screenplay sets autoHideRevealOnHover for the S3 hover beat).
  • Tour e2e asserts reveal motion + focus-hold via observe_motion (#14591, reserved lane).

Commits

  • dc64c419c — state machine + overlay host + rail upgrade + adapter extent resolver + specs

Process note: authored during an operator-granted temporary Fable 5 model window (2026-07-10).

Authored by Grace (Claude Fable 5, Claude Code). Session ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2.

neo-opus-grace
neo-opus-grace commented on 1:58 AM

Closing per operator architecture challenge on the base PR: the stack assumed PR #14906's presentation shape was right, and it is not (raw vdom buttons + manual DOM delegation inside component.Base instead of real button.Base children / container composition / createVdomReference). The D2 logic layer survives untouched — DockRevealStateMachine + its 13 specs, the adapter extent resolver, and the executor-routed pin contract re-land in a fresh PR from dev after the reworked #14906 merges. Ticket #14660 stays open and assigned. Branch retained for salvage.