Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-pandas-drag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@ankhorage/studio': minor
---

Restore contextual Insert and Delete authoring, add placement-backed canvas drag and drop through
the cross-platform adapter with deterministic session cleanup, and remove obsolete DnD wrappers
and directional tree mutation APIs.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# @ankhorage/studio

![license: MIT](././paradox/badges/license.svg) ![npm: v0.13.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
![license: MIT](././paradox/badges/license.svg) ![npm: v0.14.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)

Standalone Studio authoring package for Ankhorage apps.

Expand All @@ -29,7 +29,6 @@ Manage Studio development and projects through the Ankh CLI.
- [Architecture overview](././paradox/diagrams/architecture-overview.mmd)
- [Module relationships](././paradox/diagrams/module-relationships.mmd)
- [Export graph](././paradox/diagrams/export-graph.mmd)
- [addNodeToTree sequence](././paradox/diagrams/sequences/add-node-to-tree.mmd)
- [appendStudioEventBinding sequence](././paradox/diagrams/sequences/append-studio-event-binding.mmd)
- [assessStudioBindingCompatibility sequence](././paradox/diagrams/sequences/assess-studio-binding-compatibility.mmd)
- [buildInsertCatalogEntries sequence](././paradox/diagrams/sequences/build-insert-catalog-entries.mmd)
Expand All @@ -40,6 +39,7 @@ Manage Studio development and projects through the Ankh CLI.
- [findNodeById sequence](././paradox/diagrams/sequences/find-node-by-id.mmd)
- [removeStudioEventBinding sequence](././paradox/diagrams/sequences/remove-studio-event-binding.mmd)
- [removeStudioPropBinding sequence](././paradox/diagrams/sequences/remove-studio-prop-binding.mmd)
- [resolveInsertPlacement sequence](././paradox/diagrams/sequences/resolve-insert-placement.mmd)
- [resolveStudioInstancePropertyGroups sequence](././paradox/diagrams/sequences/resolve-studio-instance-property-groups.mmd)
- [resolveStudioSchemaValueMeta sequence](././paradox/diagrams/sequences/resolve-studio-schema-value-meta.mmd)
- [updateNodeInTree sequence](././paradox/diagrams/sequences/update-node-in-tree.mmd)
Expand Down
16 changes: 13 additions & 3 deletions docs/studio-authoring-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This package now owns the package-neutral Studio authoring/model helpers used by
- component metadata contract shape used by authoring helpers
- empty screen starter template
- Studio ID generation helper
- UI tree helpers for cloning, finding, updating, removing, adding, and moving nodes
- UI tree helpers for cloning, finding, updating, removing, inserting, and placement-backed moving
- node placement validation/resolution
- insert catalog entry construction and placement resolution
- catalog entry node creation
Expand All @@ -19,7 +19,13 @@ This package now owns the package-neutral Studio authoring/model helpers used by

The package intentionally does not import Zora or any concrete component registry. Host packages pass component metadata through `StudioComponentMetaRegistry` when resolving placement, building insert catalog entries, or creating nodes.

This keeps `@ankhorage/studio` as the authoring-model owner while letting `ankhorage4` and future hosts decide which component registry powers the Studio catalog.
This keeps `@ankhorage/studio` as the authoring-model owner while letting the first-party Studio app
and generated app hosts inject the same component registry that powers their Runtime rendering.

Placement mutation is intentionally singular. `insertNodeAtPlacement`,
`resolveMoveNodePlacement`, and `moveNodeToPlacement` are the canonical tree APIs. The older
directional `addNodeToTree`, `moveNodeInTree`, and `moveStudioManifestNode` APIs are removed, as are
the `@ankhorage/studio/dnd*` wrapper subpaths.

## Deliberately not moved here

Expand All @@ -32,11 +38,15 @@ This slice still does not move product shell or platform code. In particular, th
- panels
- React Native components
- Expo Router code
- DnD provider/runtime implementation
- package-neutral DnD provider/runtime implementation
- Supabase/storage implementation
- generated-app runtime composition code
- template catalog ownership

The local generated Studio runtime may compose the shared cross-platform DnD adapter directly for
its app-facing canvas UI. That integration does not change ownership of the package-neutral tree
and placement model.

## Dependency rule

The authoring model may depend on `@ankhorage/contracts` for manifest and `UiNode` types. It must not import React, React Native, Expo, DnD, Supabase, Zora implementation modules, generated-app runtime composition code, or host-app implementation modules.
34 changes: 31 additions & 3 deletions docs/studio-canvas-drag-model.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Studio canvas interaction model

`@ankhorage/studio/canvasDragModel` provides package-neutral helpers for Studio canvas node movement payloads and canvas placement slot selection.
`@ankhorage/studio/canvasDragModel` provides package-neutral helpers for Studio canvas node
movement payloads, placement slots, measured drop-zone geometry, and drag-preview text.

## Import

```ts
import {
createStudioCanvasDragPayload,
isStudioCanvasDragPayload,
resolveCanvasDragSession,
resolveCanvasDragPreviewText,
resolveCanvasDropZoneRect,
resolveCanvasDropZoneSlots,
} from '@ankhorage/studio/canvasDragModel';
```
Expand All @@ -18,7 +22,31 @@ import {
- runtime guard for canvas movement payloads
- valid canvas placement-zone detection
- resolving before, inside, and after placement slots
- deriving drop-zone rectangles from measured Runtime node bounds
- selecting concise authored text for the measured-bounds drag preview
- resolving whether an active drag still belongs to the current edit selection and manifest tree

## Host-owned concerns
## Runtime integration

Hosts still own the gesture implementation, React rendering, hover/focus state, styles, and concrete move execution.
The generated local Studio host composes these helpers with `DropProvider`, `Draggable`,
`Draggable.Handle`, and `Droppable` imported directly from
`@ankhorage/react-native-reanimated-dnd-web`. It renders a 48-point handle only for the selected
non-root node, keeps the rest of the canvas on the stationary-selection path, and shows a
measured-bounds ghost while dragging.

Every rendered Runtime node becomes a measured target while a drag is active. The screen surface
supplies a synthetic root target on native, where the normal selection recorder intentionally does
not wrap the root. More specific targets are registered before broad ancestor zones so nested
placements win overlapping collision checks.

The UI adapter owns transient drag and hover state and resets it after cancellation or commit. Each
reset also advances the Draggable session identity, remounting the web adapter after it reports a
drop so committed translation and `DROPPED` state cannot leak into the next interaction. The
selected node ID is part of that identity, so changing selection also creates a fresh adapter
instance.

An active drag is exposed to Runtime measurement and the overlay only while Edit mode is active,
the active node is still selected, and the node still exists in the current root. Selection loss,
tree removal, and Preview transitions synchronously suppress drag geometry and clear transient
state. A valid drop invokes the context's placement mutation exactly once. The package-neutral
model remains free of gestures, React lifecycle, concrete ZORA metadata, and manifest persistence.
15 changes: 10 additions & 5 deletions docs/studio-canvas-drop-zones.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Studio canvas drop zones

`@ankhorage/studio/canvasDropZones` provides package-neutral helpers for resolving valid canvas drop zones around a target node.
`@ankhorage/studio/canvasDropZones` provides package-neutral helpers for resolving canvas drop zones
around a rendered target node.

## Import

Expand All @@ -11,10 +12,14 @@ import { resolveCanvasDropZones, getValidCanvasDropZones } from '@ankhorage/stud
## Owned here

- resolving `before`, `inside`, and `after` drop zones
- rejecting self-drops
- validating placements through Studio component metadata
- rejecting root, self, descendant, and no-op movement
- validating placements through caller-provided Studio component metadata
- preserving canonical placement failure codes and messages for invalid-zone feedback
- filtering valid drop zones

## Host-owned concerns
## Runtime integration

Hosts still own pointer handling, drag/drop gestures, visual affordances, dialogs, and React lifecycle wiring.
The package's generated Studio runtime owns pointer handling, direct adapter composition, visual
affordances, and React lifecycle wiring. Other hosts may render the same package-neutral zone
resolutions with their platform UI, but must execute successful drops through the canonical
`NodePlacement` mutation instead of maintaining a second tree-moving algorithm.
21 changes: 18 additions & 3 deletions docs/studio-insert-modal-model.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Studio insert modal model

`@ankhorage/studio/insertModalModel` provides package-neutral helpers for preparing insert catalog entries for insert modal UIs.
`@ankhorage/studio/insertModalModel` provides package-neutral helpers for preparing insert catalog
entries for contextual Insert UI.

## Import

Expand All @@ -19,6 +20,20 @@ import {
- resolving node display labels from aliases and component metadata
- formatting placement hints for enabled catalog entries

## Host-owned concerns
## Studio integration

Hosts still own rendering modals, search input state, selected category UI, concrete component metadata such as Zora metadata, and insert execution.
The first-party Studio shell renders the contextual Insert surface with ordinary ZORA `Modal`,
`Input`, `ListSection`, and `ListRow` components. The app bar says **Add child** when the selected
node accepts a direct child and **Insert** when placement falls back to an ancestor or sibling.
Search and category state are transient; enabled rows show their resolved placement hint, while
disabled rows retain the catalog's exact explanation.

`StudioProvider` receives concrete component metadata from the generated app and exposes the
package-neutral catalog, insert, delete, and placement-backed move capabilities through the Studio
context. Insertion creates nodes from the catalog blueprint and defaults, persists through the
canonical manifest placement mutation, and selects the inserted node. Deletion uses a ZORA
confirmation dialog, rejects the screen root, removes the complete subtree and its bindings, and
selects the deleted node's former parent.

There is no parallel Toolbox or Studio-only component registry. Hosts still own which concrete
metadata registry they inject and where the contextual app-bar augmentation is mounted.
9 changes: 9 additions & 0 deletions docs/studio-manifest-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ Host packages still own lifecycle and concrete integration inputs:

Where a mutation needs component placement rules, callers pass `StudioComponentMetaRegistry` instead of this package importing Zora.

Insert and move wrappers delegate to the same canonical tree placement functions exported from the
root authoring model. `resolveMoveNodePlacement` validates the requested parent, reference, index,
component compatibility, root/self/descendant safety, and same-parent index adjustment before a
move is applied. This keeps Insert UI and canvas DnD on one `NodePlacement` contract rather than
directional or gesture-specific mutation APIs.

Deleting a node removes the full subtree. Data and event bindings keyed by, or owned by, any node
in that subtree are removed in the same immutable manifest mutation.

## Import path

Use the manifest-state subpath when consuming these helpers:
Expand Down
11 changes: 11 additions & 0 deletions docs/studio-public-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ to integrate the Studio package boundary:
- Studio command/event contracts
- the type-only context value shape

`StudioContextValue` now carries the caller-injected `componentMeta` registry and concrete
`insertFromCatalogEntry`, `moveNodeToPlacement`, and `deleteNode` capabilities. `StudioProvider`
requires that registry so the generated Studio app uses the same component rules as its Runtime.
`resolveMoveNodePlacement` is public for validating and normalizing a placement before mutation;
`invalid-reference` identifies incoherent sibling references.

The obsolete `addNodeToTree`, `moveNodeInTree`, directional `moveStudioManifestNode`, and
`StudioContextValue.moveNode` APIs have been removed. The `@ankhorage/studio/dnd`,
`@ankhorage/studio/dnd/primitives`, and `@ankhorage/studio/dnd/state` exports are also removed;
app-facing canvas code imports the maintained cross-platform adapter directly.

## Deliberately not moved here

This slice does not move product UI or app-host implementation code. In particular, this package
Expand Down
20 changes: 18 additions & 2 deletions docs/studio-runtime-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Generated apps compose Studio selection through the normal Runtime renderer conf
selection. Edit mode supplies `interactionPolicy="passive"` only to components that explicitly
support the canonical policy; Preview restores `enabled`.

The same root selection surface owns canvas drag composition in Edit mode. A selected non-root node
gets one 48-point `Draggable.Handle`; the selected component itself remains on the stationary tap
path. The handle and every drop target use `@ankhorage/react-native-reanimated-dnd-web` directly,
so native and web share the adapter contract without `@ankhorage/studio/dnd*` wrapper APIs. Preview
mounts no drag overlay and clears an active drag if mode changes.

Starting a drag activates public geometry measurement for every rendered Runtime node. Web uses
DOM descendant bounds and native uses `measureInWindow`; the root surface contributes its own
bounds when native root instrumentation is intentionally absent. Before, inside, and after zones
are resolved through caller-injected component metadata and the canonical `NodePlacement` rules.
Invalid zones remain visible with their structured reason. A measured-bounds ghost includes the
component label and the first useful authored text property. Cancellation and commit both release
drag geometry, hover state, and the ghost.

Stationary native Runtime selection remains supported and was validated on Android: stationary
Edit taps select the deepest intended node once, movement and scrolling cancel unintended
selection, passive interaction preserves nested scrolling and authored state, and Preview restores
Expand Down Expand Up @@ -41,7 +55,8 @@ refresh it without a layout-changing wrapper or private React Native/Fabric APIs
On web, Runtime-node registration stores a lazy `getResizeTargets()` callback. Descendants are not
traversed and are not observed merely because a node rendered. Inactive registration changes also
avoid a complete scan of the Runtime-node registry. Only the selected node and visible unsupported
nodes activate resize targets. Selection or Edit/Preview changes compute the complete desired
nodes activate resize targets while idle; an active canvas drag temporarily activates every
rendered Runtime node. Selection, drag, or Edit/Preview changes compute the complete desired
target set, observe newly required elements, and unobserve elements no longer required. Because the
set is deduplicated before diffing, a shared descendant is observed once and remains observed while
any active measurement still requires it.
Expand All @@ -51,7 +66,8 @@ selection surface. Web selected chrome and unsupported overlays both use `pointe
never intercept authored input. Scroll, viewport resize, responsive layout, and authored-root
`onLayout` refresh applicable geometry. Changing or clearing selection immediately removes the
previous web outline. Preview releases authoring ResizeObserver targets and renders neither
selected nor unsupported chrome. Unmount disconnects the observer and clears desired-target
selected chrome, unsupported chrome, nor canvas drag affordances. Unmount disconnects the observer
and clears desired-target
ownership; navigation and active measurement removal cancel pending work once no indicator remains.

The generated Studio shell synchronizes the current app pathname into `StudioProvider`. Studio
Expand Down
19 changes: 0 additions & 19 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,4 @@ export default createConfig({
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
files: ['src/**/*.ts', 'scripts/**/*.ts'],
overrides: [
{
files: ['src/dnd/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'react-native-reanimated-dnd',
message:
"Forbidden in Studio. Use '@ankhorage/react-native-reanimated-dnd-web' directly instead.",
},
],
},
],
},
},
],
});
3 changes: 0 additions & 3 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ export default createKnipConfig({
'src/core/StudioContext.ts',
'src/core/StudioProvider.ts',
'src/core/studioPackageBoundary.ts',
'src/dnd/index.ts',
'src/dnd/primitives.ts',
'src/dnd/state.ts',
'src/runtime/index.ts',
'src/runtime/actionSuppression.ts',
'src/runtime/appExtensionRegistry.ts',
Expand Down
12 changes: 0 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,6 @@
"types": "./dist/cli/index.d.ts",
"import": "./dist/cli/index.js"
},
"./dnd": {
"types": "./dist/dnd/index.d.ts",
"import": "./dist/dnd/index.js"
},
"./dnd/primitives": {
"types": "./dist/dnd/primitives.d.ts",
"import": "./dist/dnd/primitives.js"
},
"./dnd/state": {
"types": "./dist/dnd/state.d.ts",
"import": "./dist/dnd/state.js"
},
"./runtime": {
"types": "./dist/runtime/index.d.ts",
"import": "./dist/runtime/index.js"
Expand Down
6 changes: 3 additions & 3 deletions paradox/badges/npm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading