diff --git a/packages/core/src/editor/index.ts b/packages/core/src/editor/index.ts index e95e24b69..f0c5063c9 100644 --- a/packages/core/src/editor/index.ts +++ b/packages/core/src/editor/index.ts @@ -18,8 +18,8 @@ export type { PageSnapshot } from './history/snapshot' export { graphFromPageSnapshot } from './history/snapshot-graph' export type { ClipboardPayload, ClipboardSnapshot } from './clipboard/copy' export { resolvePasteTarget } from './clipboard/paste-target' -export { hasBehaviour, playIslandRoots } from './play/islands' -export { layerPath, resolvePlayState, type InstanceState } from './play/states' +export { playIslandRoots } from './play/islands' +export { resolvePlayState, type InstanceState } from './play/states' export type { PlayState } from './play/actions' export type { Editor } from './create' export type { VariableTokenFields } from './variables' diff --git a/packages/core/src/editor/play/islands.ts b/packages/core/src/editor/play/islands.ts index 43cabaa31..c31269504 100644 --- a/packages/core/src/editor/play/islands.ts +++ b/packages/core/src/editor/play/islands.ts @@ -1,17 +1,4 @@ -import { - behaviourOwner, - readBehaviour, - type SceneGraph, - type SceneNode -} from '@open-pencil/scene-graph' - -/** Whether an instance's main component, or its set, has a behaviour. */ -export function hasBehaviour(graph: SceneGraph, node: SceneNode): boolean { - if (node.type !== 'INSTANCE' || !node.componentId) return false - const component = graph.getNode(node.componentId) - const owner = component && behaviourOwner(graph, component) - return !!owner && !!readBehaviour(owner) -} +import { hasBehaviour, type SceneGraph, type SceneNode } from '@open-pencil/scene-graph' function containsControl(graph: SceneGraph, node: SceneNode): boolean { if (hasBehaviour(graph, node)) return true diff --git a/packages/core/src/editor/play/states.ts b/packages/core/src/editor/play/states.ts index 2e7193278..95ff7ff35 100644 --- a/packages/core/src/editor/play/states.ts +++ b/packages/core/src/editor/play/states.ts @@ -1,6 +1,7 @@ import { applyComponentPropertyValue, componentPropertyDefinitions, + findLayerByPath, SceneGraph, type SceneNode } from '@open-pencil/scene-graph' @@ -22,37 +23,6 @@ export interface InstanceState { reveal?: string[] } -/** - * A layer's path below a root: the names of the layers down to it, with the position among - * same-named siblings when names repeat. It stays the same when a variant switch rebuilds an - * instance's layers, so it identifies controls and keeps their DOM in place. - */ -export function layerPath(graph: SceneGraph, rootId: string, nodeId: string): string { - const segments: string[] = [] - let current = graph.getNode(nodeId) - while (current && current.id !== rootId) { - const parent = current.parentId ? graph.getNode(current.parentId) : undefined - const { id, name } = current - const twins = parent ? graph.getChildren(parent.id).filter((child) => child.name === name) : [] - const index = twins.findIndex((child) => child.id === id) - segments.unshift(twins.length > 1 ? `${name}#${index}` : name) - current = parent - } - return segments.join('/') -} - -function findByPath(graph: SceneGraph, rootId: string, path: string): SceneNode | undefined { - let current = graph.getNode(rootId) - for (const segment of path ? path.split('/') : []) { - if (!current) return undefined - const hash = segment.lastIndexOf('#') - const name = hash === -1 ? segment : segment.slice(0, hash) - const twins = graph.getChildren(current.id).filter((child) => child.name === name) - current = twins.at(hash === -1 ? 0 : Number(segment.slice(hash + 1))) - } - return current -} - /** The root's layers and every component they show, so instances can switch variant. */ function closure(source: SceneGraph, rootId: string): Set { const ids = new Set() @@ -158,7 +128,7 @@ function applyState( reflow(graph, instance.id) } for (const path of state.reveal ?? []) { - const layer = findByPath(graph, rootId, path) + const layer = findLayerByPath(graph, rootId, path) if (layer && !layer.visible) { graph.updateNode(layer.id, { visible: true }) reflow(graph, layer.id) @@ -179,7 +149,7 @@ export function resolvePlayState( const graph = copyGraph(source, closure(source, rootId)) const ordered = [...states].sort(([a], [b]) => a.split('/').length - b.split('/').length) for (const [path, state] of ordered) { - const instance = findByPath(graph, rootId, path) + const instance = findLayerByPath(graph, rootId, path) if (instance?.type === 'INSTANCE') applyState(graph, rootId, instance, state) } computeAllLayouts(graph, rootId) diff --git a/packages/core/tests/editor/play/states.test.ts b/packages/core/tests/editor/play/states.test.ts index 6afd3aa77..774b5e87d 100644 --- a/packages/core/tests/editor/play/states.test.ts +++ b/packages/core/tests/editor/play/states.test.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'bun:test' +import { createEditor, playIslandRoots, resolvePlayState } from '@open-pencil/core/editor' import { - createEditor, + emptyBehaviour, + instanceMainComponent, layerPath, - playIslandRoots, - resolvePlayState -} from '@open-pencil/core/editor' -import { emptyBehaviour, instanceMainComponent, SceneGraph } from '@open-pencil/scene-graph' + SceneGraph +} from '@open-pencil/scene-graph' /** A Switch set (State × Interaction) and a card frame holding an Off instance with a label. */ function switchCard() { diff --git a/packages/docs/development/behaviours-and-preview.md b/packages/docs/development/behaviours-and-preview.md index 82e35ce3d..846c3218e 100644 --- a/packages/docs/development/behaviours-and-preview.md +++ b/packages/docs/development/behaviours-and-preview.md @@ -36,7 +36,7 @@ Preview is a per-pane mode (`EditorViewState.play`). It runs controls as real co 2. **Shadow DOM.** Each island renders into its own shadow root: app CSS does not reach in, island CSS does not leak out, fonts already loaded by the app apply, and the island moves with the canvas in the same frame. Wheel gestures over an island are passed to the canvas. 3. **State to design.** An island keeps each control's state (values, choice, number, typed text, hover, pressed, focus). Core's `resolvePlayState` copies the island's layers into a private graph and shows each instance in its state: the matching variant, boolean properties, and layers a control reveals, such as tab panels. The document is never changed. 4. **Design to DOM.** The private graph is projected to DOM through `@open-pencil/dom-css`, the same projection HTML export uses, so island fidelity and export fidelity improve together. -5. **DOM to Reka.** Each projected layer has a role: a control's root, a part, a group's item, a tab trigger or panel, or a text input. Roles wrap the layer in its Reka UI primitive with `asChild`, so Reka renders through the designed element. Typing, focus, keyboard navigation, and accessibility are the browser's and Reka's. +5. **DOM to Reka.** Each projected layer has a role: a control's root, a part, a group's item, a tab trigger or panel, or a text input. `behaviourControls` and `controlRoles` in `@open-pencil/dom-css` (`packages/dom-css/src/behaviours/`) assign them, so code export reads the same roles. Roles wrap the layer in its Reka UI primitive with `asChild`, so Reka renders through the designed element. Typing, focus, keyboard navigation, and accessibility are the browser's and Reka's. Controls and DOM keys use layer paths (names below the island root), not node ids: a variant switch rebuilds an instance's layers with new ids, and a stable path keeps the DOM in place, including a focused input. diff --git a/packages/vue/src/canvas/islands/model.ts b/packages/dom-css/src/behaviours/controls.ts similarity index 91% rename from packages/vue/src/canvas/islands/model.ts rename to packages/dom-css/src/behaviours/controls.ts index 6514f30f1..d48faf8a5 100644 --- a/packages/vue/src/canvas/islands/model.ts +++ b/packages/dom-css/src/behaviours/controls.ts @@ -1,12 +1,13 @@ -import { hasBehaviour, layerPath } from '@open-pencil/core/editor' import { behaviourContract, behaviourOwner, behaviourProperties, booleanBinding, findComponentPropertyTargets, + hasBehaviour, instanceMainComponent, instanceSlotFrames, + layerPath, partBinding, readBehaviour, slotPropertyId, @@ -36,23 +37,23 @@ export interface StatesModel { designedDisabled: boolean } -/** An instance with a behaviour inside an island, as the island runs it. */ +/** An instance with a behaviour below a root layer: its values, states, and bound layers. */ export interface ControlModel { - /** The instance's layer path below the island root. */ + /** The instance's layer path below the root. */ path: string kind: Behaviour['kind'] behaviour: Behaviour booleans: Record states: StatesModel | null - /** Each bound part's slot frame, by part id, as a layer path below the island root. */ + /** Each bound part's slot frame, by part id, as a layer path below the root. */ parts: Record - /** Each bound text value's text layer, by value id, as a layer path below the island root. */ + /** Each bound text value's text layer, by value id, as a layer path below the root. */ texts: Record /** The children of the items slot that are controls themselves, in order. */ items: ControlModel[] /** * Layers the control decides to show, which the design may hide: tab panels, or a - * collapsible's content, as layer paths below the island root. + * collapsible's content, as layer paths below the root. */ reveal: string[] /** For tabs, each panel's layer path, in order. */ @@ -173,10 +174,11 @@ function controlModel(graph: SceneGraph, rootId: string, instance: SceneNode): C } /** - * The controls of an island, by layer path, outermost first. Items of a group are listed both - * in the group's `items` and on their own, since each keeps its own state. + * The controls below a root layer, by layer path, outermost first. Items of a group are listed + * both in the group's `items` and on their own, since each keeps its own state. Preview runs + * them as live islands; export generates components from them. */ -export function islandControls(graph: SceneGraph, rootId: string): Map { +export function behaviourControls(graph: SceneGraph, rootId: string): Map { const controls = new Map() const visit = (node: SceneNode) => { if (hasBehaviour(graph, node)) { diff --git a/packages/dom-css/src/behaviours/index.ts b/packages/dom-css/src/behaviours/index.ts new file mode 100644 index 000000000..d81bc418e --- /dev/null +++ b/packages/dom-css/src/behaviours/index.ts @@ -0,0 +1,8 @@ +export { + behaviourControls, + booleanOf, + type BooleanModel, + type ControlModel, + type StatesModel +} from './controls' +export { controlRoles, groupOf, type ControlRole } from './roles' diff --git a/packages/vue/src/canvas/islands/roles.ts b/packages/dom-css/src/behaviours/roles.ts similarity index 83% rename from packages/vue/src/canvas/islands/roles.ts rename to packages/dom-css/src/behaviours/roles.ts index 41b5e406d..5160a6b44 100644 --- a/packages/vue/src/canvas/islands/roles.ts +++ b/packages/dom-css/src/behaviours/roles.ts @@ -1,7 +1,7 @@ -import type { ControlModel } from './model' +import type { ControlModel } from './controls' -/** What a layer of an island is to the controls in it. */ -export type IslandRole = +/** What a layer below a root is to the controls in it. */ +export type ControlRole = | { type: 'root'; control: ControlModel } | { type: 'part'; control: ControlModel; part: string } | { type: 'item'; group: ControlModel; control: ControlModel; index: number } @@ -14,8 +14,10 @@ export type IslandRole = * and panels, and the text layers that become inputs. Items of a group take the item role in * place of their root role, since the group drives them. */ -export function islandRoles(controls: ReadonlyMap): Map { - const roles = new Map() +export function controlRoles( + controls: ReadonlyMap +): Map { + const roles = new Map() for (const control of controls.values()) { if (!roles.has(control.path)) roles.set(control.path, { type: 'root', control }) for (const [part, path] of Object.entries(control.parts)) @@ -34,7 +36,7 @@ export function islandRoles(controls: ReadonlyMap): Map, + roles: ReadonlyMap, control: ControlModel ): ControlModel | undefined { const role = roles.get(control.path) diff --git a/packages/dom-css/src/export/index.ts b/packages/dom-css/src/export/index.ts index 92f3552f4..0b6b101c3 100644 --- a/packages/dom-css/src/export/index.ts +++ b/packages/dom-css/src/export/index.ts @@ -9,6 +9,7 @@ export { } from './tailwind-jsx' export { serializeHTML } from './html' export * from '../tokens' +export * from '../behaviours' export type { ExportHTMLBundle, ExportHTMLBundleOptions, diff --git a/packages/dom-css/tests/behaviours/controls.test.ts b/packages/dom-css/tests/behaviours/controls.test.ts new file mode 100644 index 000000000..97d8b3c31 --- /dev/null +++ b/packages/dom-css/tests/behaviours/controls.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from 'bun:test' + +import { behaviourControls, booleanOf, controlRoles } from '#dom-css/behaviours' + +import { emptyBehaviour, SceneGraph, withBehaviour } from '@open-pencil/scene-graph' + +/** A Switch set (State) and a card frame holding an On instance named Wifi. */ +function switchCard() { + const graph = new SceneGraph() + const pageId = graph.getPages()[0].id + const set = graph.createNode('COMPONENT_SET', pageId, { + name: 'Switch', + componentPropertyDefinitions: [ + { + id: 'state', + name: 'State', + type: 'VARIANT', + defaultValue: 'Off', + variantOptions: ['Off', 'On'] + } + ] + }) + graph.createNode('COMPONENT', set.id, { + name: 'State=Off', + componentPropertyValues: { State: 'Off' } + }) + const on = graph.createNode('COMPONENT', set.id, { + name: 'State=On', + componentPropertyValues: { State: 'On' } + }) + graph.updateNode(set.id, { + pluginData: withBehaviour(set, { + ...emptyBehaviour('switch'), + booleans: { value: { propertyId: 'state', on: 'On', off: 'Off' } } + }) + }) + const card = graph.createNode('FRAME', pageId, { name: 'Card' }) + const instance = graph.createInstance(on.id, card.id) + if (!instance) throw new Error('No instance') + graph.updateNode(instance.id, { name: 'Wifi' }) + return { graph, card } +} + +describe('behaviour controls', () => { + test('finds the controls below a root by layer path with the values their design shows', () => { + const { graph, card } = switchCard() + const controls = behaviourControls(graph, card.id) + expect([...controls.keys()]).toEqual(['Wifi']) + const control = controls.get('Wifi') + expect(control?.kind).toBe('switch') + expect(control && booleanOf(control, 'value')?.designed).toBe(true) + expect(control && booleanOf(control, 'open')).toBeUndefined() + }) + + test('gives a control root its role by layer path', () => { + const { graph, card } = switchCard() + const roles = controlRoles(behaviourControls(graph, card.id)) + expect(roles.get('Wifi')?.type).toBe('root') + expect(roles.size).toBe(1) + }) +}) diff --git a/packages/scene-graph/src/behaviours/layers.ts b/packages/scene-graph/src/behaviours/layers.ts new file mode 100644 index 000000000..e0eb3be7e --- /dev/null +++ b/packages/scene-graph/src/behaviours/layers.ts @@ -0,0 +1,51 @@ +import type { SceneGraph } from '../index' +import type { SceneNode } from '../types' +import { behaviourOwner, readBehaviour } from './model' + +/** Whether an instance's main component, or its set, has a behaviour. */ +export function hasBehaviour(graph: SceneGraph, node: SceneNode): boolean { + if (node.type !== 'INSTANCE' || !node.componentId) return false + const component = graph.getNode(node.componentId) + const owner = component && behaviourOwner(graph, component) + return !!owner && !!readBehaviour(owner) +} + +/** A name as a path segment: `/` separates segments and `#` the twin index, so both are escaped. */ +const pathName = (name: string) => name.replace(/[%/#]/g, (char) => encodeURIComponent(char)) + +/** + * A layer's path below a root: the names of the layers down to it, with the position among + * same-named siblings when names repeat. It stays the same when a variant switch rebuilds an + * instance's layers, so it identifies controls and keeps their DOM in place. + */ +export function layerPath(graph: SceneGraph, rootId: string, nodeId: string): string { + const segments: string[] = [] + // `closest` stops on a parent cycle in bad data, where a hand-written walk would not. + graph.closest(nodeId, (node) => { + if (node.id === rootId) return true + const twins = node.parentId + ? graph.getChildren(node.parentId).filter((child) => child.name === node.name) + : [] + const index = twins.findIndex((child) => child.id === node.id) + segments.unshift(twins.length > 1 ? `${pathName(node.name)}#${index}` : pathName(node.name)) + return false + }) + return segments.join('/') +} + +/** The layer at `path` below a root, the inverse of `layerPath`. */ +export function findLayerByPath( + graph: SceneGraph, + rootId: string, + path: string +): SceneNode | undefined { + let current = graph.getNode(rootId) + for (const segment of path ? path.split('/') : []) { + if (!current) return undefined + const hash = segment.lastIndexOf('#') + const name = hash === -1 ? segment : segment.slice(0, hash) + const twins = graph.getChildren(current.id).filter((child) => pathName(child.name) === name) + current = twins.at(hash === -1 ? 0 : Number(segment.slice(hash + 1))) + } + return current +} diff --git a/packages/scene-graph/src/index.ts b/packages/scene-graph/src/index.ts index 299224cf1..0bf172fcb 100644 --- a/packages/scene-graph/src/index.ts +++ b/packages/scene-graph/src/index.ts @@ -20,6 +20,7 @@ export * from './slots/content' export * from './slots/authoring' export * from './slots/limits' export * from './behaviours/kinds' +export * from './behaviours/layers' export * from './behaviours/model' export * from './behaviours/spec' export * from './copy' diff --git a/packages/scene-graph/tests/behaviours/layers.test.ts b/packages/scene-graph/tests/behaviours/layers.test.ts new file mode 100644 index 000000000..47727bc23 --- /dev/null +++ b/packages/scene-graph/tests/behaviours/layers.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'bun:test' + +import { findLayerByPath, layerPath, SceneGraph } from '@open-pencil/scene-graph' + +describe('layer paths', () => { + test('round-trip names that contain the path separators', () => { + const graph = new SceneGraph() + const root = graph.createNode('FRAME', graph.getPages()[0].id, { name: 'Card' }) + const group = graph.createNode('FRAME', root.id, { name: 'Row/1' }) + for (const name of ['Wifi#1', '50%', 'Label', 'Label']) { + const node = graph.createNode('FRAME', group.id, { name }) + const path = layerPath(graph, root.id, node.id) + expect(findLayerByPath(graph, root.id, path)?.id).toBe(node.id) + } + expect(layerPath(graph, root.id, group.id)).toBe('Row%2F1') + }) + + test('stops on a parent cycle in bad data', () => { + const graph = new SceneGraph() + const pageId = graph.getPages()[0].id + const root = graph.createNode('FRAME', pageId, { name: 'Card' }) + const a = graph.createNode('FRAME', pageId, { name: 'A' }) + const b = graph.createNode('FRAME', a.id, { name: 'B' }) + // Imported data can point parents at each other; the walk must still end. + const loose = graph.getNode(a.id) + if (loose) loose.parentId = b.id + expect(typeof layerPath(graph, root.id, b.id)).toBe('string') + }) +}) diff --git a/packages/vue/AGENTS.md b/packages/vue/AGENTS.md index a9c1053aa..90f651c02 100644 --- a/packages/vue/AGENTS.md +++ b/packages/vue/AGENTS.md @@ -18,6 +18,7 @@ The app's `DesignPanel` retains at most one selection-property subtree through ` ## Preview islands - Preview runs controls as real components, never as canvas simulations: `packages/vue/src/canvas/islands/` renders each top-level layer that holds an instance with a behaviour as DOM in a shadow root over the canvas, projected through `@open-pencil/dom-css`, and mounts each behaviour's Reka UI primitives on its layers. Typing, focus, and keyboard handling belong to the browser and Reka. +- Islands only map layers to Reka components. Which layer is a control, part, item, trigger, panel, or input comes from `behaviourControls` and `controlRoles` in `@open-pencil/dom-css`, which code export shares; never derive roles in Vue (`packages/dom-css/tests/behaviours/controls.test.ts`). - The concept, the pipeline from state to DOM, and how to add a kind are in `packages/docs/development/behaviours-and-preview.md`. - Variants draw control states: Core's `resolvePlayState` shows instances in a state on a private graph, and controls are keyed by layer path so a variant switch keeps their DOM, including a focused input. The document is never changed (`packages/core/tests/editor/play/states.test.ts`, `tests/e2e/components/behaviours.spec.ts`). diff --git a/packages/vue/src/canvas/islands/Island.vue b/packages/vue/src/canvas/islands/Island.vue index 5b8a6f6b5..37d6b2836 100644 --- a/packages/vue/src/canvas/islands/Island.vue +++ b/packages/vue/src/canvas/islands/Island.vue @@ -2,13 +2,15 @@ import { computed, shallowRef, useTemplateRef, watch } from 'vue' import { resolvePlayState } from '@open-pencil/core/editor' -import { sceneNodeToDesignDocument } from '@open-pencil/dom-css/export' +import { + behaviourControls, + controlRoles, + sceneNodeToDesignDocument +} from '@open-pencil/dom-css/export' import { useEditor } from '#vue/editor/context' -import { islandControls } from './model' import { renderNode } from './render' -import { islandRoles } from './roles' import { useShadowContainer } from './shadow' import { createIslandState, instanceStates } from './state' @@ -30,9 +32,9 @@ const container = useShadowContainer(useTemplateRef('host'), ISLAND const controls = computed(() => { void editor.state.sceneVersion - return islandControls(editor.graph, rootId) + return behaviourControls(editor.graph, rootId) }) -const roles = computed(() => islandRoles(controls.value)) +const roles = computed(() => controlRoles(controls.value)) const state = shallowRef(createIslandState(controls.value)) watch( () => [rootId, revision, [...controls.value.keys()].join('\n')], diff --git a/packages/vue/src/canvas/islands/controls/fields.ts b/packages/vue/src/canvas/islands/controls/fields.ts index 2e87bde12..3109552db 100644 --- a/packages/vue/src/canvas/islands/controls/fields.ts +++ b/packages/vue/src/canvas/islands/controls/fields.ts @@ -6,11 +6,10 @@ import { } from 'reka-ui' import { h, type VNode } from 'vue' +import { booleanOf, type ControlModel, type ControlRole } from '@open-pencil/dom-css/export' import { numberSettings } from '@open-pencil/scene-graph' -import { booleanOf, type ControlModel } from '#vue/canvas/islands/model' import type { ElementOverride, IslandRenderContext } from '#vue/canvas/islands/render' -import type { IslandRole } from '#vue/canvas/islands/roles' import { disabled, interaction, stateOf } from './shared' @@ -53,7 +52,7 @@ export function fieldRoot(island: IslandRenderContext, control: ControlModel, ba /** The text layer of a field, as the input it is, or a number field's stepper part. */ export function fieldRole( island: IslandRenderContext, - role: IslandRole, + role: ControlRole, base: Base ): VNode | undefined { if (role.type === 'part' && role.control.kind === 'numberField') { diff --git a/packages/vue/src/canvas/islands/controls/index.ts b/packages/vue/src/canvas/islands/controls/index.ts index aa992f71b..502709999 100644 --- a/packages/vue/src/canvas/islands/controls/index.ts +++ b/packages/vue/src/canvas/islands/controls/index.ts @@ -28,12 +28,10 @@ import { } from 'reka-ui' import { h, type Component, type VNode } from 'vue' -import type { DesignElement } from '@open-pencil/dom-css/export' +import { groupOf, type ControlRole, type DesignElement } from '@open-pencil/dom-css/export' import { numberSettings } from '@open-pencil/scene-graph' import type { ElementOverride, IslandRenderContext } from '#vue/canvas/islands/render' -import type { IslandRole } from '#vue/canvas/islands/roles' -import { groupOf } from '#vue/canvas/islands/roles' import { fieldRole, fieldRoot } from './fields' import { chooseItem, disabled, interaction, stateOf, type RoleContext } from './shared' @@ -45,7 +43,7 @@ function asChild(component: Component, props: Record, child: () const ON_OFF: Record = { switch: SwitchRoot, toggle: Toggle } -function root({ island, role, base }: RoleContext>): VNode { +function root({ island, role, base }: RoleContext>): VNode { const { control } = role const state = stateOf(island, control) const off = disabled(island, control) @@ -170,7 +168,7 @@ function groupRoot( } /** An item of a group: a radio, a toggle, or a collapsible inside an accordion. */ -function item({ island, role, base }: RoleContext>): VNode { +function item({ island, role, base }: RoleContext>): VNode { const state = stateOf(island, role.control) const value = String(role.index) const pointer = interaction(state) @@ -187,7 +185,7 @@ const SLIDER_PARTS: Record>): VNode { +function part({ island, role, base }: RoleContext>): VNode { const { control } = role const sliderPart = control.kind === 'slider' ? SLIDER_PARTS[role.part] : undefined if (sliderPart) return asChild(sliderPart.component, {}, () => base({ omit: sliderPart.omit })) @@ -209,7 +207,7 @@ function part({ island, role, base }: RoleContext, + role: Extract, base: RoleContext['base'] ): VNode { const inAccordion = groupOf(island.roles, role.control)?.kind === 'accordion' @@ -227,7 +225,7 @@ function disclosurePart( /** Render a layer with the role it has for the controls of its island. */ export function wrapRole( island: IslandRenderContext, - role: IslandRole, + role: ControlRole, element: DesignElement, base: (override?: ElementOverride) => VNode ): VNode { diff --git a/packages/vue/src/canvas/islands/controls/shared.ts b/packages/vue/src/canvas/islands/controls/shared.ts index 8e3464fdc..a1636eeff 100644 --- a/packages/vue/src/canvas/islands/controls/shared.ts +++ b/packages/vue/src/canvas/islands/controls/shared.ts @@ -1,14 +1,12 @@ import type { VNode } from 'vue' -import type { DesignElement } from '@open-pencil/dom-css/export' +import type { ControlModel, ControlRole, DesignElement } from '@open-pencil/dom-css/export' -import type { ControlModel } from '#vue/canvas/islands/model' import type { ElementOverride, IslandRenderContext } from '#vue/canvas/islands/render' -import type { IslandRole } from '#vue/canvas/islands/roles' import { isDisabled, type ControlState } from '#vue/canvas/islands/state' /** What a role's wrapper gets: the island, the layer, and how to render it as designed. */ -export interface RoleContext { +export interface RoleContext { island: IslandRenderContext role: R element: DesignElement diff --git a/packages/vue/src/canvas/islands/render.ts b/packages/vue/src/canvas/islands/render.ts index 8b8a3bf02..2c0f9eacb 100644 --- a/packages/vue/src/canvas/islands/render.ts +++ b/packages/vue/src/canvas/islands/render.ts @@ -1,12 +1,14 @@ import { h, type VNode } from 'vue' -import { layerPath } from '@open-pencil/core/editor' -import type { DesignElement, DesignNode } from '@open-pencil/dom-css/export' -import type { SceneGraph } from '@open-pencil/scene-graph' +import type { + ControlModel, + ControlRole, + DesignElement, + DesignNode +} from '@open-pencil/dom-css/export' +import { layerPath, type SceneGraph } from '@open-pencil/scene-graph' import { wrapRole } from './controls' -import type { ControlModel } from './model' -import type { IslandRole } from './roles' import type { IslandState } from './state' export interface IslandRenderContext { @@ -14,7 +16,7 @@ export interface IslandRenderContext { graph: SceneGraph rootId: string controls: ReadonlyMap - roles: ReadonlyMap + roles: ReadonlyMap state: IslandState } diff --git a/packages/vue/src/canvas/islands/state.ts b/packages/vue/src/canvas/islands/state.ts index 89d69a17c..9e1ac40de 100644 --- a/packages/vue/src/canvas/islands/state.ts +++ b/packages/vue/src/canvas/islands/state.ts @@ -1,10 +1,9 @@ import { reactive } from 'vue' import type { InstanceState } from '@open-pencil/core/editor' +import { booleanOf, type ControlModel } from '@open-pencil/dom-css/export' import { numberSettings, type InteractionState } from '@open-pencil/scene-graph' -import { booleanOf, type ControlModel } from './model' - /** What one control of an island holds while it runs. */ export interface ControlState { booleans: Partial>