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:
<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
Problem
Studio currently wraps every rendered Runtime node in a Studio-owned
Pressableand also clones the rendered Runtime component to inject selectiononPressandstyleprops.The cloned props change authored component semantics. For example, injecting
onPressinto a ZORAPaneland a nestedCardcauses both Surface components to become interactive buttons, producing invalid HTML:Selection chrome is also currently dependent on whether an arbitrary rendered component forwards an injected
styleprop 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/contractsankhorage/templatesStudio alone owns selection instrumentation. Runtime's existing generic
wrapNodehook 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:
Preserve any existing unrelated
bun.lockmodification:git add <paths>onlyProduction implementation
The only production implementation change in this phase must be in:
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:
onPressstyleinjectioncloneElementandisValidElementuseStateanduseZoraThemeusage when no longer neededPressable; do not replace it withTouchableOpacityaccessibilityRole,accessibilityLabel,accessibilityState, hover handlers, focus handlers, selected styles, or focusability to the boundaryevent.stopPropagation()on the boundary selection handler so the deepest nested Runtime boundary wins over ancestorsdisableActions: !previewModewrapNode: previewMode ? undefined : wrapStudioRuntimeNodeDo not use
pointerEvents="none"on authored child components as a workaround.Exact boundary evidence
Document and verify the exact remaining Studio boundary:
Do not assume that omitting accessibility props automatically prevents a role, focusability, or
tabindex; verify the rendered result.Decisive behavior check
DisclosureSectionis a decisive architecture test.An outer bubbling
stopPropagation()may prevent ancestor selection, but it cannot retroactively suppress an authored childonPressthat has already executed.If selecting a
DisclosureSectionin Edit mode still toggles the authored disclosure state, the focused outer-boundary approach is NO-GO.Do not compensate with:
pointerEvents="none"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 > CardButton > TextPanelsurfaceDisclosureSectionAssert:
Panel > Cardcontains no nestedbutton buttonButton > Textcontains exactly the authored buttonrole="button", notabindex, and no authored-component accessibility or focus semanticsCardselects theCard, not thePanelPanelsurface selects thePanelDisclosureSectiondoes not toggle authored disclosure behaviorwrapNode: undefinedonPressor selection-style injectionIf
DisclosureSectionstill 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:
Button > TextPanel > CardSectionHeaderDisclosureSectionMeasure 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
iOS
Phase 1 outcome
Return exactly one outcome:
GO
Web, Android, and iOS validation pass:
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:
DisclosureSectiontoggles during Edit selectionCommit 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:
ankhorage/studioIf 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 > Cardrenders without nested buttons in Studio Edit modeButton > Textpreserves the authored button as the only interactive semantic elementCardselects theCard, not itsPanelparentPanelsurface selects thePanelDisclosureSectiondo not execute for an Edit-mode selection gestureonPress, visual styles, roles, labels, focusability, or accessibility state into authored componentsSectionHeader,Panel,Card,Button, andTextankhorage/studioPR policy
Refs #156for scoped prerequisite PRsFixes #156only when all final acceptance criteria are implemented and validated