Skip to content

Implement non-invasive Studio canvas selection with node hit-testing and overlay chrome #156

Description

@artiphishle

Problem

Studio currently wraps every rendered Runtime node in a Studio-owned Pressable and also clones the rendered Runtime component to inject selection onPress and style props.

The cloned props change authored component semantics. For example, injecting onPress into a ZORA Panel and a nested Card causes both Surface components to become interactive buttons, producing invalid HTML:

Panel
└─ Card
<button>
  <button>...</button>
</button>

Selection chrome is also currently dependent on whether an arbitrary rendered component forwards an injected style prop to its visual root.

The outer Studio selection boundary is not yet proven to be the cause of these regressions. This issue therefore starts with a focused Studio-only boundary pass before introducing a broader overlay, registry, or canvas-renderer architecture.

Repository boundary

All production investigation and implementation for this issue must remain inside ankhorage/studio.

Do not modify or introduce Studio-specific contracts in:

  • @ankhorage/runtime
  • @ankhorage/zora
  • @ankhorage/surface
  • @ankhorage/contracts
  • ankhorage/templates
  • any other repository

Studio alone owns selection instrumentation. Runtime's existing generic wrapNode hook may be consumed as-is, but Runtime and the standalone UI packages must remain unaware of Studio selection, measurement, hit-testing, and chrome.

Phase 1: focused outer-boundary pass

Prove or reject the existing Studio-owned outer selection boundary as a safe Edit-mode interaction layer after removing invasive cloned Runtime component props.

This phase is a scoped prerequisite for the complete non-invasive selection architecture. It does not implement the final registry, centralized hit-testing controller, or separate chrome overlay.

Branch and workspace hygiene

Use branch:

issue-156/focused-selection-boundary

Preserve any existing unrelated bun.lock modification:

  • do not edit it
  • do not include it in any commit
  • use explicit git add <paths> only

Production implementation

The only production implementation change in this phase must be in:

src/host/layout/templates/rootLayout.ts

Tests, disposable probe code, validation evidence, documentation, and a patch changeset may also be added or updated inside ankhorage/studio.

Update the generated Studio selection helpers as follows:

  • remove rendered Runtime component cloning for selection
  • remove cloned selection onPress
  • remove cloned selected, hovered, and focused style injection
  • remove the selection use of cloneElement and isValidElement
  • remove selection-only useState and useZoraTheme usage when no longer needed
  • keep selection exclusively on the outer Studio-owned boundary in Edit mode
  • keep the outer boundary as Pressable; do not replace it with TouchableOpacity
  • do not add accessibilityRole, accessibilityLabel, accessibilityState, hover handlers, focus handlers, selected styles, or focusability to the boundary
  • add only non-semantic Studio test markers when required for rendered-DOM verification
  • keep event.stopPropagation() on the boundary selection handler so the deepest nested Runtime boundary wins over ancestors
  • keep disableActions: !previewMode
  • use wrapNode: previewMode ? undefined : wrapStudioRuntimeNode
  • document in tests that switching between Edit and Preview may remount authored Runtime nodes

Do not use pointerEvents="none" on authored child components as a workaround.

Exact boundary evidence

Document and verify the exact remaining Studio boundary:

  • component type
  • props
  • styles
  • pointer-event behavior
  • actual Expo web DOM output
  • actual Android/iOS native hierarchy where executable

Do not assume that omitting accessibility props automatically prevents a role, focusability, or tabindex; verify the rendered result.

Decisive behavior check

DisclosureSection is a decisive architecture test.

An outer bubbling stopPropagation() may prevent ancestor selection, but it cannot retroactively suppress an authored child onPress that has already executed.

If selecting a DisclosureSection in Edit mode still toggles the authored disclosure state, the focused outer-boundary approach is NO-GO.

Do not compensate with:

  • pointerEvents="none"
  • authored prop mutation
  • injected handlers or styles
  • Runtime, ZORA, Surface, Contracts, or template changes
  • private React Native or Fabric internals

Web validation

Validation must execute an actual generated Expo web app in Chrome. Generated-source string assertions alone are insufficient.

Cover a generated Studio screen containing:

  • Panel > Card
  • Button > Text
  • exposed Panel surface
  • DisclosureSection
  • an Edit/Preview toggle path

Assert:

  • Panel > Card contains no nested button button
  • Button > Text contains exactly the authored button
  • the Studio boundary adds no role="button", no tabindex, and no authored-component accessibility or focus semantics
  • clicking a nested Card selects the Card, not the Panel
  • clicking exposed Panel surface selects the Panel
  • an Edit-mode selection click on DisclosureSection does not toggle authored disclosure behavior
  • Preview uses wrapNode: undefined
  • Preview contains no Studio boundary markers or Studio hit-selection behavior
  • Preview restores authored disclosure interaction
  • generated source contains no clone-based selection onPress or selection-style injection

If DisclosureSection still toggles in Edit mode after clone removal, return NO-GO rather than hiding the failure.

Native feasibility validation

Create a disposable native probe outside the repository using the exact proposed Studio boundary shape from generated Edit mode and a Preview baseline with wrapNode: undefined.

Compare baseline Preview geometry against instrumented Edit geometry for:

  • Stack
  • Inline/row
  • Grid
  • Text
  • Button > Text
  • Panel > Card
  • SectionHeader
  • DisclosureSection
  • percentage dimensions
  • flex values
  • absolute positioning
  • nested scroll containers
  • nested Runtime-node boundaries

Measure the authored rendered fixtures, not the boundary as a chrome anchor. The question in this phase is whether the boundary changes authored layout or tap-selection semantics.

Any fixture refs used for geometry comparison are probe-only test instrumentation. They are not a production host-ref contract and must not lead to Runtime, ZORA, or Surface changes.

Android

  • execute on an emulator or device
  • record geometry deltas and nested tap-selection results
  • confirm that the boundary does not appear as an extra accessible button or focus target where inspectable

iOS

  • attempt the same execution
  • if simulator or device execution is unavailable, report the exact local blocker
  • do not infer iOS correctness from TypeScript types, Android, or web behavior
  • do not claim full cross-platform validation without executed iOS evidence

Phase 1 outcome

Return exactly one outcome:

GO

Web, Android, and iOS validation pass:

  • no semantic or layout regression
  • deepest nested selection works
  • exposed parent fallback works
  • authored Edit-mode actions are suppressed for the selection gesture
  • Preview restores authored behavior

Commit implementation and evidence, then open a focused prerequisite PR using Refs #156.

Do not claim that #156 is fully fixed unless every final acceptance criterion below is also implemented.

PROVISIONAL GO / iOS BLOCKED

Web and Android pass, but iOS execution is unavailable for a documented environmental reason.

A prerequisite PR may be opened using Refs #156, but it must prominently state that iOS validation remains outstanding. Do not claim cross-platform completion or that #156 is fixed.

NO-GO

Return NO-GO when any of the following occurs:

  • DisclosureSection toggles during Edit selection
  • the boundary changes authored layout or semantics
  • deepest-node selection fails
  • parent-surface fallback fails
  • an executed platform fails validation

Commit evidence only. Do not open an implementation PR and do not claim #156 is fixed.

Final target architecture

If the focused boundary receives GO, continue the remaining #156 work inside Studio only:

  • render consistent selection chrome without injecting styles into authored components
  • keep Preview completely uninstrumented
  • preserve deepest-node selection, parent fallback, scroll, and DnD behavior
  • keep all selection-specific implementation in ankhorage/studio

If the focused boundary receives NO-GO, investigate a Studio-owned Edit-mode canvas renderer as the next alternative. Do not move Studio concerns into Runtime, ZORA, Surface, Contracts, or templates.

Final acceptance criteria

  • Panel > Card renders without nested buttons in Studio Edit mode
  • Button > Text preserves the authored button as the only interactive semantic element
  • clicking a nested Card selects the Card, not its Panel parent
  • clicking exposed Panel surface selects the Panel
  • authored internal actions such as DisclosureSection do not execute for an Edit-mode selection gesture
  • selection does not inject onPress, visual styles, roles, labels, focusability, or accessibility state into authored components
  • selection chrome is rendered consistently for components including SectionHeader, Panel, Card, Button, and Text
  • chrome remains aligned after scroll, viewport resize, responsive reflow, disclosure expansion, and route changes
  • DnD gestures do not produce unintended selection taps
  • Preview uses no Studio selection wrapper, markers, handlers, hit-testing, or overlay and restores authored interactions
  • web regression tests execute real DOM and assert valid interactive nesting
  • executed native validation covers layout safety, deepest-node selection, parent fallback, and Preview behavior
  • no production changes are required outside ankhorage/studio

PR policy

  • do not merge any PR automatically
  • use Refs #156 for scoped prerequisite PRs
  • use Fixes #156 only when all final acceptance criteria are implemented and validated

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions