From 455fb84733dca34c65ee4f10dd734e0d26d04298 Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Tue, 28 Jul 2026 13:47:38 +0200
Subject: [PATCH 1/8] feat: implement passive edit interactions and stationary
selection
---
...-issue-167-interaction-policy-selection.md | 5 +
bun.lock | 7 +-
package.json | 3 +-
src/host/layout/layoutGenerator.test.ts | 2 -
.../templates/rootLayout.selectionDom.test.ts | 34 ---
src/host/layout/templates/rootLayout.test.ts | 62 +++--
src/host/layout/templates/rootLayout.ts | 121 +++-----
src/runtime/index.ts | 2 +
src/runtime/interactionPolicy.test.ts | 71 +++++
src/runtime/interactionPolicy.ts | 75 +++++
src/runtime/previewRuntimeConfig.ts | 6 +
src/runtime/stationarySelection.test.ts | 147 ++++++++++
src/runtime/stationarySelection.ts | 260 ++++++++++++++++++
13 files changed, 661 insertions(+), 134 deletions(-)
create mode 100644 .changeset/2026-07-28-issue-167-interaction-policy-selection.md
delete mode 100644 src/host/layout/templates/rootLayout.selectionDom.test.ts
create mode 100644 src/runtime/interactionPolicy.test.ts
create mode 100644 src/runtime/interactionPolicy.ts
create mode 100644 src/runtime/stationarySelection.test.ts
create mode 100644 src/runtime/stationarySelection.ts
diff --git a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
new file mode 100644
index 00000000..9d198a80
--- /dev/null
+++ b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
@@ -0,0 +1,5 @@
+---
+'@ankhorage/studio': minor
+---
+
+Add public interaction-policy and stationary-selection runtime helpers.
diff --git a/bun.lock b/bun.lock
index 1e09070c..bceaedd1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -22,6 +22,7 @@
"@ankhorage/zora-tabletop": "^0.0.5",
"@fastify/cors": "^11.2.0",
"fastify": "^5.7.4",
+ "react-native-gesture-handler": "^3.1.0",
},
"devDependencies": {
"@ankhorage/ankh": "^0.7.0",
@@ -716,6 +717,8 @@
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
+ "@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="],
+
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
@@ -1680,7 +1683,7 @@
"react-native-drawer-layout": ["react-native-drawer-layout@4.2.7", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-hhD+E0QmUPkP2Sj1MsUdrvU7GeOiHChPAFPtKahroTwlBGnpgJsUVSL0GWOy5cG3oCZfnu4Pb+gIzOa4ItGNuA=="],
- "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
+ "react-native-gesture-handler": ["react-native-gesture-handler@3.1.0", "", { "dependencies": { "@types/react-test-renderer": "^19.1.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+kWVyZ6vLdtyFa1/aOc/sZncLAVPUKxhji/ttbiLI7hOaSU44bLjhI7p+Ns+pMl2r3RqPXssl5qHReYet9G77g=="],
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
@@ -2062,6 +2065,8 @@
"@ankhorage/runtime/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="],
+ "@ankhorage/studio-app/react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
+
"@ankhorage/supabase-auth/@ankhorage/contracts": ["@ankhorage/contracts@3.0.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-IXBdPehcgJpDjl/0QXHunl7AsgkTAXoC89jBiQWE+tanp8SBdgVFxWMm9A1MtbU3Rr1UU6U81GjhASM2OhGVUw=="],
"@ankhorage/supabase-vault/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="],
diff --git a/package.json b/package.json
index 72039a10..58a3cd17 100644
--- a/package.json
+++ b/package.json
@@ -244,7 +244,8 @@
"@ankhorage/zora-chess": "^0.1.2",
"@ankhorage/zora-tabletop": "^0.0.5",
"@fastify/cors": "^11.2.0",
- "fastify": "^5.7.4"
+ "fastify": "^5.7.4",
+ "react-native-gesture-handler": "^3.1.0"
},
"peerDependencies": {
"@expo/vector-icons": "^15.0.3",
diff --git a/src/host/layout/layoutGenerator.test.ts b/src/host/layout/layoutGenerator.test.ts
index bf49250d..2cc607f5 100644
--- a/src/host/layout/layoutGenerator.test.ts
+++ b/src/host/layout/layoutGenerator.test.ts
@@ -182,8 +182,6 @@ describe('GeneratedAppFileGenerator', () => {
expect(reactImports).toHaveLength(1);
expect(reactImports[0]).toContain('useState');
- expect(reactImports[0]).toContain('cloneElement');
- expect(reactImports[0]).toContain('isValidElement');
expect(reactImports[0]).toContain('type ReactNode');
expect(reactImports[0]?.match(/\buseState\b/gu)?.length).toBe(1);
});
diff --git a/src/host/layout/templates/rootLayout.selectionDom.test.ts b/src/host/layout/templates/rootLayout.selectionDom.test.ts
deleted file mode 100644
index 94692e3d..00000000
--- a/src/host/layout/templates/rootLayout.selectionDom.test.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { AppManifest } from '@ankhorage/contracts';
-import { expect, test } from 'bun:test';
-
-import { getRootLayoutTsx } from './rootLayout';
-
-test('keeps the Studio selection Pressable non-semantic on web', () => {
- const generated = getRootLayoutTsx({
- manifest: {
- navigator: {
- initialRouteName: 'index',
- },
- } as unknown as AppManifest,
- mutations: [],
- allImports: '',
- allHooks: '',
- innerNavigation: {
- declarations: '',
- jsx: '<>>',
- usesTheme: false,
- usesIcon: false,
- usesZoraTabBar: false,
- usesZoraDrawerContent: false,
- usesZoraNavigationRouteMap: false,
- },
- includeStudio: true,
- });
-
- expect(generated).toContain(' {
+test('generates a root stationary tap selector for edit mode and excludes old Pressable selection code', () => {
const generated = getRootLayoutTsx({
manifest: {
navigator: {
@@ -146,21 +146,51 @@ test('uses layout-neutral selection instrumentation and disables runtime actions
includeStudio: true,
});
+ expect(generated).toContain('StationaryTapSelector');
+ expect(generated).toContain('createStudioStationarySelectionWrapNode');
+ expect(generated).toContain('createStudioInteractionPolicyResolver');
expect(generated).toContain('disableActions: !previewMode');
- expect(generated).toContain('wrapNode: wrapStudioRuntimeNode');
- expect(generated).toContain('function wrapStudioRuntimeNode(args: {');
- expect(generated).toContain('function StudioRuntimeNodeWrapper(props: {');
- expect(generated).toContain(
- "const studioSelectionInteractionStyle = { display: 'contents' as const };",
- );
- expect(generated).toContain(' {');
- expect(generated).toContain('onPress={(event: GestureResponderEvent) => {');
- expect(generated).not.toContain('borderWidth:');
- expect(generated).not.toContain('(event: unknown)');
+ expect(generated).toContain('resolveNodeProps: studioResolveNodeProps');
+ expect(generated).toContain('wrapNode: studioWrapNode');
+ expect(generated).toContain('selectedNodeId');
+ expect(generated).toContain('selectNode');
+ expect(generated).toContain('studioThirdPartySupport');
+ expect(generated).toContain('ZORA_COMPONENT_REGISTRY');
+ expect(generated).not.toContain(' {
+ const generated = getRootLayoutTsx({
+ manifest: {
+ navigator: {
+ initialRouteName: 'index',
+ },
+ } as unknown as AppManifest,
+ mutations: [],
+ allImports: '',
+ allHooks: '',
+ innerNavigation: {
+ declarations: '',
+ jsx: '<>>',
+ usesTheme: false,
+ usesIcon: false,
+ usesZoraTabBar: false,
+ usesZoraDrawerContent: false,
+ usesZoraNavigationRouteMap: false,
+ },
+ includeStudio: false,
+ });
+
+ expect(generated).toContain('const runtimeComponentRegistry = createComponentRegistry(');
+ expect(generated).toContain('ZORA_COMPONENT_REGISTRY');
+ expect(generated).not.toContain('useStudio');
+ expect(generated).not.toContain('StationaryTapSelector');
+ expect(generated).not.toContain('disableActions');
+ expect(generated).not.toContain('wrapNode: studioWrapNode');
});
test('scopes Studio runtime selection config below StudioProvider', () => {
@@ -197,7 +227,7 @@ test('scopes Studio runtime selection config below StudioProvider', () => {
expect(rootLayoutSource).not.toContain('wrapNode: wrapStudioRuntimeNode');
expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo(');
expect(studioShellSource).toContain('disableActions: !previewMode');
- expect(studioShellSource).toContain('wrapNode: wrapStudioRuntimeNode');
+ expect(studioShellSource).toContain('wrapNode: studioWrapNode');
expect(studioShellSource).toContain(
'',
);
diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts
index 59ccf754..34eb693a 100644
--- a/src/host/layout/templates/rootLayout.ts
+++ b/src/host/layout/templates/rootLayout.ts
@@ -39,7 +39,7 @@ export function getRootLayoutImportRequirements(
namedImports: [
{ imported: 'ReactNode', typeOnly: true },
...(includeStudio
- ? [{ imported: 'cloneElement' }, { imported: 'isValidElement' }, { imported: 'useState' }]
+ ? [{ imported: 'useState' }]
: []),
],
},
@@ -47,9 +47,23 @@ export function getRootLayoutImportRequirements(
? [
{
source: 'react-native',
+ namedImports: [],
+ },
+ ]
+ : []),
+ ...(includeStudio
+ ? [
+ {
+ source: '@ankhorage/studio/runtime',
+ namedImports: [
+ { imported: 'createStudioInteractionPolicyResolver' },
+ ],
+ },
+ {
+ source: '@ankhorage/studio/runtime/stationarySelection',
namedImports: [
- { imported: 'Pressable' },
- { imported: 'GestureResponderEvent', typeOnly: true },
+ { imported: 'createStudioStationarySelectionWrapNode' },
+ { imported: 'StationaryTapSelector' },
],
},
]
@@ -483,78 +497,6 @@ const shouldMountAppHeader =
const indentedStudioShellBlock =
studioShellBlock.length > 0 ? `\n${indentGeneratedBlock(studioShellBlock)}\n` : '\n';
const indentedInnerNavigationJsx = indentGeneratedBlock(innerNavigation.jsx, ' ');
- const studioSelectionRuntimeHelpers = includeStudio
- ? `const studioSelectionInteractionStyle = { display: 'contents' as const };
-
-function wrapStudioRuntimeNode(args: {
- readonly node: { readonly id?: string };
- readonly rendered: ReactNode;
- readonly isRoot: boolean;
-}): ReactNode {
- return (
-
- );
-}
-
-function StudioRuntimeNodeWrapper(props: {
- readonly nodeId?: string;
- readonly rendered: ReactNode;
-}): ReactNode {
- const [isHovered, setIsHovered] = useState(false);
- const [isFocused, setIsFocused] = useState(false);
- const { previewMode, selectedNodeId, selectNode } = useStudio();
- const { theme } = useZoraTheme();
-
- if (previewMode || !props.nodeId) {
- return props.rendered;
- }
-
- const selected = selectedNodeId === props.nodeId;
- const selectionStyle = selected
- ? { boxShadow: \`0 0 0 2px \${theme.colors.primary}\` }
- : isFocused
- ? { boxShadow: \`0 0 0 2px \${theme.colors.primary}\` }
- : isHovered
- ? { boxShadow: \`0 0 0 1px \${theme.colors.primary}\` }
- : undefined;
-
- const renderedNode = isValidElement<{ readonly style?: unknown; readonly onPress?: unknown }>(
- props.rendered,
- )
- ? cloneElement(props.rendered, {
- style: [props.rendered.props.style, selectionStyle],
- onPress: (event: GestureResponderEvent) => {
- event.stopPropagation();
- selectNode(props.nodeId ?? null);
- },
- })
- : props.rendered;
-
- return (
- setIsHovered(true)}
- onHoverOut={() => setIsHovered(false)}
- onFocus={() => setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
- onPress={(event: GestureResponderEvent) => {
- event.stopPropagation();
- selectNode(props.nodeId ?? null);
- }}
- style={studioSelectionInteractionStyle}
- >
- {renderedNode}
-
- );
-}
-
-`
- : '';
-
return `
${allImports}
@@ -566,7 +508,7 @@ const runtimeComponentRegistry = createComponentRegistry(
APP_EXTENSION_COMPONENT_REGISTRY,
);
-${studioSelectionRuntimeHelpers}function resolveZoraProviderTheme(
+function resolveZoraProviderTheme(
theme: AppManifest['themes'][number],
mode: NonNullable,
) {
@@ -668,6 +610,8 @@ function StudioShell({
activeScreenId,
manifest: studioManifest,
previewMode,
+ selectedNodeId,
+ selectNode,
setLastNonAdminLocation,
} = useStudio();
useEffect(() => {
@@ -677,6 +621,16 @@ function StudioShell({
});
if (nextAppLocation) setLastNonAdminLocation(nextAppLocation);
}, [appLocation, appPathname, setLastNonAdminLocation]);
+
+ const studioThirdPartySupport = {} as const;
+ const studioWrapNode = createStudioStationarySelectionWrapNode({
+ previewMode,
+ thirdPartySupport: studioThirdPartySupport,
+ });
+ const studioResolveNodeProps = createStudioInteractionPolicyResolver({
+ previewMode,
+ thirdPartySupport: studioThirdPartySupport,
+ });
const appHeaderTitle = resolveStudioAppHeaderTitle({
runtimeManifest,
studioManifest,
@@ -699,14 +653,21 @@ function StudioShell({
const studioRuntimeConfig = useMemo(
() => ({
disableActions: !previewMode,
- wrapNode: wrapStudioRuntimeNode,
+ wrapNode: studioWrapNode,
+ resolveNodeProps: studioResolveNodeProps,
}),
[previewMode],
);
const studioOutput = (
-
- {output}
-
+
+
+ {output}
+
+
);
return (
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
index 2ddaf6d7..1a87fda4 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -1,8 +1,10 @@
export * from './actionSuppression.js';
export * from './appExtensionRegistry.js';
+export * from './interactionPolicy.js';
export * from './localizationPreview.js';
export * from './previewRegistry.js';
export * from './previewRuntimeConfig.js';
export * from './registry.js';
export * from './runtimeActions.js';
+export * from './stationarySelection.js';
export * from './useRuntimeAction.js';
diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts
new file mode 100644
index 00000000..4fdeff46
--- /dev/null
+++ b/src/runtime/interactionPolicy.test.ts
@@ -0,0 +1,71 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+import { describe, expect, it } from 'bun:test';
+
+const interactionPolicySource = readFileSync(join(import.meta.dir, 'interactionPolicy.ts'), 'utf8');
+const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8');
+
+describe('interactionPolicy', () => {
+ it('exports InteractionPolicy type', () => {
+ expect(interactionPolicySource).toContain('export type InteractionPolicy');
+ });
+
+ it('exports ThirdPartyComponentSupport as Readonly>', () => {
+ expect(interactionPolicySource).toContain('ThirdPartyComponentSupport');
+ expect(interactionPolicySource).toContain('Readonly>');
+ });
+
+ it('exports createStudioInteractionPolicyResolver', () => {
+ expect(interactionPolicySource).toContain(
+ 'export function createStudioInteractionPolicyResolver',
+ );
+ });
+
+ it('exports isComponentSupported', () => {
+ expect(interactionPolicySource).toContain('export function isComponentSupported');
+ });
+
+ it('re-exports interactionPolicy from the runtime index', () => {
+ expect(indexSource).toContain("export * from './interactionPolicy.js'");
+ });
+
+ it('assigns passive policy to built-in ZORA components in Edit mode', () => {
+ expect(interactionPolicySource).toContain("return previewMode ? 'enabled' : 'passive'");
+ });
+
+ it('returns undefined policy for unsupported types', () => {
+ expect(interactionPolicySource).toContain('return undefined;');
+ });
+
+ it('classifies ZORA built-in components by registry lookup', () => {
+ expect(interactionPolicySource).toContain('ZORA_COMPONENT_REGISTRY');
+ expect(interactionPolicySource).toContain("'zora-builtin'");
+ });
+
+ it('has a shared support model between policy and classification', () => {
+ expect(interactionPolicySource).toContain('ThirdPartyComponentSupport');
+ expect(interactionPolicySource).toContain('isComponentSupported');
+ const supportClassifyMatch = /isComponentSupported[\s\S]{0,500}classifyComponent/.exec(
+ interactionPolicySource,
+ );
+ expect(supportClassifyMatch).not.toBeNull();
+ expect(interactionPolicySource).toContain("classification === 'unsupported'");
+ });
+
+ it('classifyComponent returns three categories', () => {
+ expect(interactionPolicySource).toContain("'zora-builtin'");
+ expect(interactionPolicySource).toContain("'third-party-supported'");
+ expect(interactionPolicySource).toContain("'unsupported'");
+ });
+
+ it('keeps disableActions independent as a separate config concern', () => {
+ const previewRuntimeSource = readFileSync(
+ join(import.meta.dir, 'previewRuntimeConfig.ts'),
+ 'utf8',
+ );
+
+ expect(previewRuntimeSource).toContain('disableActions');
+ expect(previewRuntimeSource).toContain('createStudioInteractionPolicyResolver');
+ });
+});
diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts
new file mode 100644
index 00000000..9f1ed66e
--- /dev/null
+++ b/src/runtime/interactionPolicy.ts
@@ -0,0 +1,75 @@
+import type { RuntimeResolveNodePropsArgs } from '@ankhorage/contracts/runtime';
+import { type RuntimeNodePropsResolver } from '@ankhorage/runtime';
+import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora';
+
+export type InteractionPolicy = 'enabled' | 'passive';
+export type ThirdPartyComponentSupport = Readonly>;
+
+export interface StudioInteractionPolicyResolverArgs {
+ readonly previewMode: boolean;
+ readonly thirdPartySupport?: ThirdPartyComponentSupport;
+ readonly existingResolver?: RuntimeNodePropsResolver;
+}
+
+const EMPTY_THIRD_PARTY: ThirdPartyComponentSupport = {};
+
+type ComponentClassification = 'zora-builtin' | 'third-party-supported' | 'unsupported';
+
+function classifyComponent(
+ nodeType: string,
+ thirdPartySupport: ThirdPartyComponentSupport,
+): ComponentClassification {
+ if (nodeType in ZORA_COMPONENT_REGISTRY) {
+ return 'zora-builtin';
+ }
+
+ if (nodeType in thirdPartySupport) {
+ return 'third-party-supported';
+ }
+
+ return 'unsupported';
+}
+
+export function isComponentSupported(
+ nodeType: string,
+ thirdPartySupport: ThirdPartyComponentSupport,
+): boolean {
+ return classifyComponent(nodeType, thirdPartySupport) !== 'unsupported';
+}
+
+function resolveInteractionPolicy(
+ nodeType: string,
+ previewMode: boolean,
+ thirdPartySupport: ThirdPartyComponentSupport,
+): InteractionPolicy | undefined {
+ const classification = classifyComponent(nodeType, thirdPartySupport);
+
+ if (classification === 'unsupported') {
+ return undefined;
+ }
+
+ return previewMode ? 'enabled' : 'passive';
+}
+
+const POLICY_PROP = 'interactionPolicy';
+
+export function createStudioInteractionPolicyResolver(
+ args: StudioInteractionPolicyResolverArgs,
+): RuntimeNodePropsResolver {
+ const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, existingResolver } = args;
+
+ return (resolveArgs: RuntimeResolveNodePropsArgs) => {
+ const baseProps = existingResolver ? existingResolver(resolveArgs) : { ...resolveArgs.props };
+
+ const policy = resolveInteractionPolicy(resolveArgs.node.type, previewMode, thirdPartySupport);
+
+ if (policy === undefined) {
+ return baseProps;
+ }
+
+ return {
+ ...baseProps,
+ [POLICY_PROP]: policy,
+ };
+ };
+}
diff --git a/src/runtime/previewRuntimeConfig.ts b/src/runtime/previewRuntimeConfig.ts
index 18c00201..cf0ba539 100644
--- a/src/runtime/previewRuntimeConfig.ts
+++ b/src/runtime/previewRuntimeConfig.ts
@@ -1,7 +1,9 @@
import type { ComponentRegistry, RuntimeRendererConfig } from '@ankhorage/runtime';
import { shouldSuppressStudioRuntimeActions } from './actionSuppression';
+import { createStudioInteractionPolicyResolver } from './interactionPolicy';
import { createStudioPreviewComponentRegistry } from './previewRegistry';
+import { createStudioStationarySelectionWrapNode } from './stationarySelection';
export interface StudioPreviewRuntimeConfigOptions {
readonly previewMode: boolean;
@@ -13,4 +15,8 @@ export const createStudioPreviewRuntimeConfig = (
): RuntimeRendererConfig => ({
disableActions: shouldSuppressStudioRuntimeActions(options.previewMode),
registry: createStudioPreviewComponentRegistry({ components: options.components }),
+ wrapNode: createStudioStationarySelectionWrapNode({ previewMode: options.previewMode }),
+ resolveNodeProps: createStudioInteractionPolicyResolver({
+ previewMode: options.previewMode,
+ }),
});
diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts
new file mode 100644
index 00000000..9f23b285
--- /dev/null
+++ b/src/runtime/stationarySelection.test.ts
@@ -0,0 +1,147 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+import { describe, expect, it } from 'bun:test';
+
+const stationarySelectionSource = readFileSync(
+ join(import.meta.dir, 'stationarySelection.ts'),
+ 'utf8',
+);
+
+describe('stationarySelection', () => {
+ it('exports createStationarySelectionCoordinator', () => {
+ expect(stationarySelectionSource).toContain(
+ 'export function createStationarySelectionCoordinator',
+ );
+ });
+
+ it('exports TransactionState type', () => {
+ expect(stationarySelectionSource).toContain('TransactionState');
+ });
+
+ it('exports StationarySelectionCoordinator interface', () => {
+ expect(stationarySelectionSource).toContain('export interface StationarySelectionCoordinator');
+ });
+
+ it('exports StationaryTapSelector', () => {
+ expect(stationarySelectionSource).toContain('export { StationaryTapSelector }');
+ });
+
+ it('exports createStudioStationarySelectionWrapNode', () => {
+ expect(stationarySelectionSource).toContain(
+ 'export function createStudioStationarySelectionWrapNode',
+ );
+ });
+
+ it('re-exports stationarySelection from the runtime index', () => {
+ const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8');
+ expect(indexSource).toContain("export * from './stationarySelection.js'");
+ });
+
+ it('uses a shared tracker ref via context (no React ref)', () => {
+ expect(stationarySelectionSource).toContain('TrackerContext');
+ expect(stationarySelectionSource).toContain('trackerRef');
+ });
+
+ it('has no module-global mutable tracker', () => {
+ const trackerDecl = /const\s+\w+.*MutableRefObject/s.exec(stationarySelectionSource);
+ const moduleGlobalTracker = trackerDecl?.find((match) => !match.includes('createContext'));
+ expect(moduleGlobalTracker).toBeUndefined();
+ });
+
+ it('has root selector own tracker instance via createStationarySelectionCoordinator', () => {
+ expect(stationarySelectionSource).toContain('createStationarySelectionCoordinator');
+ });
+
+ it('records an ordered node path via push', () => {
+ expect(stationarySelectionSource).toContain('.path.push(');
+ });
+
+ it('ignores duplicate node participation', () => {
+ expect(stationarySelectionSource).toContain('!tx.path.includes(nodeId)');
+ });
+
+ it('uses deterministic deepest node wins via destructured path first element', () => {
+ expect(stationarySelectionSource).toContain('const [deepestNodeId] = tx.path');
+ });
+
+ it('has transaction generation token', () => {
+ expect(stationarySelectionSource).toContain('transactionId: Date.now()');
+ });
+
+ it('BEGAN clears stale state by beginning a fresh transaction', () => {
+ expect(stationarySelectionSource).toContain('State.BEGAN');
+ expect(stationarySelectionSource).toContain('beginTransaction');
+ });
+
+ it('stationary Edit success commits once via finalized flag', () => {
+ expect(stationarySelectionSource).toContain('finalized');
+ });
+
+ it('Preview never commits', () => {
+ expect(stationarySelectionSource).toContain('if (!isEditMode)');
+ });
+
+ it('clears state on movement end', () => {
+ expect(stationarySelectionSource).toContain('clearTransaction');
+ });
+
+ it('handles failure and cancellation by clearing state', () => {
+ expect(stationarySelectionSource).toContain('State.FAILED');
+ expect(stationarySelectionSource).toContain('State.CANCELLED');
+ });
+
+ it('handles END by clearing state', () => {
+ expect(stationarySelectionSource).toContain('State.END');
+ });
+
+ it('has mode change clear via useEffect on isEditMode', () => {
+ expect(stationarySelectionSource).toContain('[props.isEditMode]');
+ });
+
+ it('node-level touch-end does not clear path before root commitment', () => {
+ expect(stationarySelectionSource).toContain('onTouchStart');
+ expect(stationarySelectionSource).not.toContain('onTouchEnd');
+ expect(stationarySelectionSource).not.toContain('onTouchEnd');
+ });
+
+ it('has no Pressable recorders', () => {
+ expect(stationarySelectionSource).not.toContain('Pressable');
+ });
+
+ it('has exactly one root-level GestureHandler import, no per-node recognizer imports', () => {
+ const importMatches = stationarySelectionSource.match(/from 'react-native-gesture-handler'/g);
+ expect(importMatches?.length).toBe(1);
+ });
+
+ it('has no cloneElement', () => {
+ expect(stationarySelectionSource).not.toContain('cloneElement');
+ });
+
+ it('has no responder stealing', () => {
+ expect(stationarySelectionSource).not.toContain('responder');
+ });
+
+ it('has no rectangle hit testing', () => {
+ expect(stationarySelectionSource).not.toContain('hitTest');
+ expect(stationarySelectionSource).not.toContain('measure');
+ });
+
+ it('preserves component identity and authored props in wrapNode', () => {
+ expect(stationarySelectionSource).toContain('args.rendered');
+ });
+
+ it('uses pointerEvents box-none on unsupported node wrapper, not none', () => {
+ expect(stationarySelectionSource).toContain("'box-none'");
+ expect(stationarySelectionSource).not.toContain("'none'");
+ });
+
+ it('thirdPartySupport declaration is Readonly>', () => {
+ expect(stationarySelectionSource).toContain('Readonly>');
+ });
+
+ it('thirdPartySupport does not contain mode (enabled/passive)', () => {
+ expect(stationarySelectionSource).not.toContain("'enabled'");
+ expect(stationarySelectionSource).not.toContain("'passive'");
+ });
+});
diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts
new file mode 100644
index 00000000..27472fa5
--- /dev/null
+++ b/src/runtime/stationarySelection.ts
@@ -0,0 +1,260 @@
+import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora';
+import React from 'react';
+import { View } from 'react-native';
+import { State, TapGestureHandler } from 'react-native-gesture-handler';
+
+export interface TransactionState {
+ readonly transactionId: number;
+ readonly path: string[];
+ moved: boolean;
+ finalized: boolean;
+}
+
+export interface StationarySelectionCoordinator {
+ readonly trackerRef: React.MutableRefObject;
+ beginTransaction(): void;
+ recordNode(nodeId: string): void;
+ markMoved(): void;
+ commitSelection(
+ isEditMode: boolean,
+ selectedNodeId: string | null,
+ selectNode: (id: string | null) => void,
+ ): void;
+ clearTransaction(): void;
+ getTransaction(): TransactionState | null;
+ handleStateChange(
+ state: number,
+ isEditMode: boolean,
+ selectedNodeId: string | null,
+ selectNode: (id: string | null) => void,
+ ): void;
+}
+
+const TrackerContext = React.createContext | null>(
+ null,
+);
+
+export function createStationarySelectionCoordinator(): StationarySelectionCoordinator {
+ const trackerRef = { current: null as TransactionState | null };
+
+ function beginTransaction(): void {
+ trackerRef.current = {
+ transactionId: Date.now(),
+ path: [],
+ moved: false,
+ finalized: false,
+ };
+ }
+
+ function recordNode(nodeId: string): void {
+ const tx = trackerRef.current;
+
+ if (!tx || tx.transactionId === 0 || tx.finalized) {
+ return;
+ }
+
+ if (!tx.path.includes(nodeId)) {
+ tx.path.push(nodeId);
+ }
+ }
+
+ function markMoved(): void {
+ if (trackerRef.current) {
+ trackerRef.current.moved = true;
+ }
+ }
+
+ function commitSelection(
+ isEditMode: boolean,
+ selectedNodeId: string | null,
+ selectNode: (id: string | null) => void,
+ ): void {
+ if (!isEditMode) {
+ return;
+ }
+
+ const tx = trackerRef.current;
+
+ if (!tx || tx.finalized || tx.moved || tx.path.length === 0) {
+ return;
+ }
+
+ const [deepestNodeId] = tx.path;
+
+ if (deepestNodeId != null && deepestNodeId !== selectedNodeId) {
+ selectNode(deepestNodeId);
+ }
+
+ tx.finalized = true;
+ }
+
+ function clearTransaction(): void {
+ trackerRef.current = null;
+ }
+
+ function handleStateChange(
+ state: number,
+ isEditMode: boolean,
+ selectedNodeId: string | null,
+ selectNode: (id: string | null) => void,
+ ): void {
+ if (state === State.BEGAN) {
+ beginTransaction();
+ }
+
+ if (state === State.ACTIVE) {
+ commitSelection(isEditMode, selectedNodeId, selectNode);
+ clearTransaction();
+ }
+
+ if (state === State.FAILED || state === State.CANCELLED || state === State.END) {
+ clearTransaction();
+ }
+ }
+
+ function getTransaction(): TransactionState | null {
+ return trackerRef.current;
+ }
+
+ return {
+ trackerRef,
+ beginTransaction,
+ recordNode,
+ markMoved,
+ commitSelection,
+ clearTransaction,
+ handleStateChange,
+ getTransaction,
+ };
+}
+
+function StudioNodeTouchRecorder(props: {
+ readonly nodeId: string | undefined;
+ readonly children: React.ReactNode;
+}): React.JSX.Element {
+ const tracker = React.useContext(TrackerContext);
+ const nodeIdRef = React.useRef(props.nodeId);
+
+ nodeIdRef.current = props.nodeId;
+
+ function handleTouchStart(): void {
+ if (!tracker?.current || !nodeIdRef.current) {
+ return;
+ }
+
+ const tx = tracker.current;
+
+ if (tx.transactionId === 0) {
+ return;
+ }
+
+ if (!tx.path.includes(nodeIdRef.current)) {
+ tx.path.push(nodeIdRef.current);
+ }
+ }
+
+ function handleTouchMove(): void {
+ if (!tracker?.current) {
+ return;
+ }
+
+ tracker.current.moved = true;
+ }
+
+ return React.createElement(
+ View,
+ {
+ onTouchStart: handleTouchStart,
+ onTouchMove: handleTouchMove,
+ },
+ props.children,
+ );
+}
+
+export function createStudioStationarySelectionWrapNode(options?: {
+ readonly previewMode?: boolean;
+ readonly thirdPartySupport?: Readonly>;
+}): (args: {
+ readonly node: { readonly id?: string; readonly type: string };
+ readonly rendered: React.ReactNode;
+ readonly isRoot: boolean;
+}) => React.ReactNode {
+ const { previewMode, thirdPartySupport } = options ?? {};
+ const isEditMode = previewMode !== true;
+
+ return function wrapNode(args: {
+ readonly node: { readonly id?: string; readonly type: string };
+ readonly rendered: React.ReactNode;
+ readonly isRoot: boolean;
+ }): React.ReactNode {
+ if (args.isRoot || !args.node.id) {
+ return args.rendered;
+ }
+
+ const isSupported =
+ args.node.type in ZORA_COMPONENT_REGISTRY ||
+ (thirdPartySupport != null && args.node.type in thirdPartySupport);
+
+ if (isEditMode && !isSupported) {
+ return React.createElement(
+ View,
+ {
+ style: {
+ borderWidth: 1,
+ borderColor: '#ef4444',
+ borderStyle: 'dashed',
+ },
+ pointerEvents: 'box-none',
+ },
+ React.createElement(StudioNodeTouchRecorder, {
+ nodeId: args.node.id,
+ children: args.rendered,
+ }),
+ );
+ }
+
+ return React.createElement(StudioNodeTouchRecorder, {
+ nodeId: args.node.id,
+ children: args.rendered,
+ });
+ };
+}
+
+function StationaryTapSelector(props: {
+ readonly isEditMode: boolean;
+ readonly selectedNodeId: string | null;
+ readonly selectNode: (id: string | null) => void;
+ readonly children: React.ReactNode;
+}): React.JSX.Element {
+ const coordinator = createStationarySelectionCoordinator();
+
+ function handleStateChange(event: { readonly nativeEvent: { readonly state: number } }): void {
+ coordinator.handleStateChange(
+ event.nativeEvent.state,
+ props.isEditMode,
+ props.selectedNodeId,
+ props.selectNode,
+ );
+ }
+
+ React.useEffect(() => {
+ coordinator.clearTransaction();
+ }, [props.isEditMode]);
+
+ return React.createElement(
+ TrackerContext.Provider,
+ { value: coordinator.trackerRef },
+ React.createElement(
+ TapGestureHandler,
+ {
+ maxDeltaX: 5,
+ maxDeltaY: 5,
+ maxDurationMs: 500,
+ onHandlerStateChange: handleStateChange,
+ },
+ React.createElement(View, { style: { flex: 1 } }, props.children),
+ ),
+ );
+}
+
+export { StationaryTapSelector };
From 00e40dae64f031b848237e79a29d2a9792cc46f9 Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Tue, 28 Jul 2026 14:37:01 +0200
Subject: [PATCH 2/8] fix: complete studio stationary selection integration
---
bun.lock | 14 +-
package.json | 2 +-
src/host/layout/templates/rootLayout.ts | 5 -
src/runtime/interactionPolicy.test.ts | 155 +++++++---
src/runtime/previewRuntimeConfig.ts | 2 -
src/runtime/stationarySelection.test.ts | 376 ++++++++++++++++++------
src/runtime/stationarySelection.ts | 200 +++++++------
7 files changed, 500 insertions(+), 254 deletions(-)
diff --git a/bun.lock b/bun.lock
index bceaedd1..ff720dae 100644
--- a/bun.lock
+++ b/bun.lock
@@ -22,7 +22,7 @@
"@ankhorage/zora-tabletop": "^0.0.5",
"@fastify/cors": "^11.2.0",
"fastify": "^5.7.4",
- "react-native-gesture-handler": "^3.1.0",
+ "react-native-gesture-handler": "~2.28.0",
},
"devDependencies": {
"@ankhorage/ankh": "^0.7.0",
@@ -717,8 +717,6 @@
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
- "@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="],
-
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
@@ -1677,13 +1675,13 @@
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
- "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
+ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-native": ["react-native@0.81.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", "@react-native/codegen": "0.81.5", "@react-native/community-cli-plugin": "0.81.5", "@react-native/gradle-plugin": "0.81.5", "@react-native/js-polyfills": "0.81.5", "@react-native/normalize-colors": "0.81.5", "@react-native/virtualized-lists": "0.81.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw=="],
"react-native-drawer-layout": ["react-native-drawer-layout@4.2.7", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-hhD+E0QmUPkP2Sj1MsUdrvU7GeOiHChPAFPtKahroTwlBGnpgJsUVSL0GWOy5cG3oCZfnu4Pb+gIzOa4ItGNuA=="],
- "react-native-gesture-handler": ["react-native-gesture-handler@3.1.0", "", { "dependencies": { "@types/react-test-renderer": "^19.1.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+kWVyZ6vLdtyFa1/aOc/sZncLAVPUKxhji/ttbiLI7hOaSU44bLjhI7p+Ns+pMl2r3RqPXssl5qHReYet9G77g=="],
+ "react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
@@ -2065,8 +2063,6 @@
"@ankhorage/runtime/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="],
- "@ankhorage/studio-app/react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
-
"@ankhorage/supabase-auth/@ankhorage/contracts": ["@ankhorage/contracts@3.0.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-IXBdPehcgJpDjl/0QXHunl7AsgkTAXoC89jBiQWE+tanp8SBdgVFxWMm9A1MtbU3Rr1UU6U81GjhASM2OhGVUw=="],
"@ankhorage/supabase-vault/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="],
@@ -2275,8 +2271,6 @@
"glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
- "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
-
"istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"jest-message-util/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
@@ -2343,6 +2337,8 @@
"plist/@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
+ "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
+
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"react-devtools-core/ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="],
diff --git a/package.json b/package.json
index 58a3cd17..2e6e5e6d 100644
--- a/package.json
+++ b/package.json
@@ -245,7 +245,7 @@
"@ankhorage/zora-tabletop": "^0.0.5",
"@fastify/cors": "^11.2.0",
"fastify": "^5.7.4",
- "react-native-gesture-handler": "^3.1.0"
+ "react-native-gesture-handler": "~2.28.0"
},
"peerDependencies": {
"@expo/vector-icons": "^15.0.3",
diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts
index 34eb693a..e8dbdab1 100644
--- a/src/host/layout/templates/rootLayout.ts
+++ b/src/host/layout/templates/rootLayout.ts
@@ -57,11 +57,6 @@ export function getRootLayoutImportRequirements(
source: '@ankhorage/studio/runtime',
namedImports: [
{ imported: 'createStudioInteractionPolicyResolver' },
- ],
- },
- {
- source: '@ankhorage/studio/runtime/stationarySelection',
- namedImports: [
{ imported: 'createStudioStationarySelectionWrapNode' },
{ imported: 'StationaryTapSelector' },
],
diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts
index 4fdeff46..e6439d7d 100644
--- a/src/runtime/interactionPolicy.test.ts
+++ b/src/runtime/interactionPolicy.test.ts
@@ -1,71 +1,134 @@
-import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
-
import { describe, expect, it } from 'bun:test';
-const interactionPolicySource = readFileSync(join(import.meta.dir, 'interactionPolicy.ts'), 'utf8');
-const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8');
+import { createStudioInteractionPolicyResolver } from './interactionPolicy';
describe('interactionPolicy', () => {
- it('exports InteractionPolicy type', () => {
- expect(interactionPolicySource).toContain('export type InteractionPolicy');
- });
+ it('assigns passive policy to ZORA components in Edit mode', () => {
+ const resolver = createStudioInteractionPolicyResolver({ previewMode: false });
+ const result = resolver({
+ node: { id: 'test', type: 'Text' },
+ props: { children: 'Hello' },
+ });
- it('exports ThirdPartyComponentSupport as Readonly>', () => {
- expect(interactionPolicySource).toContain('ThirdPartyComponentSupport');
- expect(interactionPolicySource).toContain('Readonly>');
+ expect(result.interactionPolicy).toBe('passive');
});
- it('exports createStudioInteractionPolicyResolver', () => {
- expect(interactionPolicySource).toContain(
- 'export function createStudioInteractionPolicyResolver',
- );
- });
+ it('assigns enabled policy to ZORA components in Preview mode', () => {
+ const resolver = createStudioInteractionPolicyResolver({ previewMode: true });
+ const result = resolver({
+ node: { id: 'test', type: 'Text' },
+ props: { children: 'Hello' },
+ });
- it('exports isComponentSupported', () => {
- expect(interactionPolicySource).toContain('export function isComponentSupported');
+ expect(result.interactionPolicy).toBe('enabled');
});
- it('re-exports interactionPolicy from the runtime index', () => {
- expect(indexSource).toContain("export * from './interactionPolicy.js'");
+ it('preserves existing resolver output', () => {
+ const existingResolver = (() => {
+ const base = { fontSize: 16 };
+ return (_args: unknown) => base;
+ })();
+
+ const resolver = createStudioInteractionPolicyResolver({
+ previewMode: true,
+ existingResolver,
+ });
+ const result = resolver({
+ node: { id: 'test', type: 'Text' },
+ props: { children: 'Hello' },
+ });
+
+ expect(result.interactionPolicy).toBe('enabled');
+ expect(result.fontSize).toBe(16);
});
- it('assigns passive policy to built-in ZORA components in Edit mode', () => {
- expect(interactionPolicySource).toContain("return previewMode ? 'enabled' : 'passive'");
+ it('preserves unrelated props from existing resolver', () => {
+ const existingResolver = (() => {
+ return (_args: unknown) => ({ customProp: 'value' });
+ })();
+
+ const resolver = createStudioInteractionPolicyResolver({
+ previewMode: false,
+ existingResolver,
+ });
+ const result = resolver({
+ node: { id: 'test', type: 'Text' },
+ props: { children: 'Hello' },
+ });
+
+ expect(result.customProp).toBe('value');
});
- it('returns undefined policy for unsupported types', () => {
- expect(interactionPolicySource).toContain('return undefined;');
+ it('assigns mode-derived policy to opted-in third-party components', () => {
+ const resolver = createStudioInteractionPolicyResolver({
+ previewMode: false,
+ thirdPartySupport: { CustomComponent: true },
+ });
+ const result = resolver({
+ node: { id: 'test', type: 'CustomComponent' },
+ props: {},
+ });
+
+ expect(result.interactionPolicy).toBe('passive');
});
- it('classifies ZORA built-in components by registry lookup', () => {
- expect(interactionPolicySource).toContain('ZORA_COMPONENT_REGISTRY');
- expect(interactionPolicySource).toContain("'zora-builtin'");
+ it('assigns enabled policy to opted-in third-party in Preview mode', () => {
+ const resolver = createStudioInteractionPolicyResolver({
+ previewMode: true,
+ thirdPartySupport: { CustomComponent: true },
+ });
+ const result = resolver({
+ node: { id: 'test', type: 'CustomComponent' },
+ props: {},
+ });
+
+ expect(result.interactionPolicy).toBe('enabled');
});
- it('has a shared support model between policy and classification', () => {
- expect(interactionPolicySource).toContain('ThirdPartyComponentSupport');
- expect(interactionPolicySource).toContain('isComponentSupported');
- const supportClassifyMatch = /isComponentSupported[\s\S]{0,500}classifyComponent/.exec(
- interactionPolicySource,
- );
- expect(supportClassifyMatch).not.toBeNull();
- expect(interactionPolicySource).toContain("classification === 'unsupported'");
+ it('assigns no policy to undeclared third-party components', () => {
+ const resolver = createStudioInteractionPolicyResolver({
+ previewMode: false,
+ thirdPartySupport: { CustomComponent: true },
+ });
+ const result = resolver({
+ node: { id: 'test', type: 'ThirdPartyComponent' },
+ props: {},
+ });
+
+ expect(result.interactionPolicy).toBeUndefined();
});
- it('classifyComponent returns three categories', () => {
- expect(interactionPolicySource).toContain("'zora-builtin'");
- expect(interactionPolicySource).toContain("'third-party-supported'");
- expect(interactionPolicySource).toContain("'unsupported'");
+ it('assigns no policy to unknown types', () => {
+ const resolver = createStudioInteractionPolicyResolver({ previewMode: false });
+ const result = resolver({
+ node: { id: 'test', type: 'UnknownWidget' },
+ props: {},
+ });
+
+ expect(result.interactionPolicy).toBeUndefined();
});
- it('keeps disableActions independent as a separate config concern', () => {
- const previewRuntimeSource = readFileSync(
- join(import.meta.dir, 'previewRuntimeConfig.ts'),
- 'utf8',
- );
+ it('third-party support cannot override mode policy direction', () => {
+ const resolver = createStudioInteractionPolicyResolver({
+ previewMode: false,
+ thirdPartySupport: { CustomComponent: true },
+ });
+ const editResult = resolver({
+ node: { id: 'test', type: 'CustomComponent' },
+ props: {},
+ });
+
+ const resolverPreview = createStudioInteractionPolicyResolver({
+ previewMode: true,
+ thirdPartySupport: { CustomComponent: true },
+ });
+ const previewResult = resolverPreview({
+ node: { id: 'test', type: 'CustomComponent' },
+ props: {},
+ });
- expect(previewRuntimeSource).toContain('disableActions');
- expect(previewRuntimeSource).toContain('createStudioInteractionPolicyResolver');
+ expect(editResult.interactionPolicy).not.toBe(previewResult.interactionPolicy);
+ expect(editResult.interactionPolicy).toBe('passive');
+ expect(previewResult.interactionPolicy).toBe('enabled');
});
});
diff --git a/src/runtime/previewRuntimeConfig.ts b/src/runtime/previewRuntimeConfig.ts
index cf0ba539..5d9135d9 100644
--- a/src/runtime/previewRuntimeConfig.ts
+++ b/src/runtime/previewRuntimeConfig.ts
@@ -3,7 +3,6 @@ import type { ComponentRegistry, RuntimeRendererConfig } from '@ankhorage/runtim
import { shouldSuppressStudioRuntimeActions } from './actionSuppression';
import { createStudioInteractionPolicyResolver } from './interactionPolicy';
import { createStudioPreviewComponentRegistry } from './previewRegistry';
-import { createStudioStationarySelectionWrapNode } from './stationarySelection';
export interface StudioPreviewRuntimeConfigOptions {
readonly previewMode: boolean;
@@ -15,7 +14,6 @@ export const createStudioPreviewRuntimeConfig = (
): RuntimeRendererConfig => ({
disableActions: shouldSuppressStudioRuntimeActions(options.previewMode),
registry: createStudioPreviewComponentRegistry({ components: options.components }),
- wrapNode: createStudioStationarySelectionWrapNode({ previewMode: options.previewMode }),
resolveNodeProps: createStudioInteractionPolicyResolver({
previewMode: options.previewMode,
}),
diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts
index 9f23b285..e6adf39c 100644
--- a/src/runtime/stationarySelection.test.ts
+++ b/src/runtime/stationarySelection.test.ts
@@ -1,147 +1,337 @@
-import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
-
import { describe, expect, it } from 'bun:test';
-const stationarySelectionSource = readFileSync(
- join(import.meta.dir, 'stationarySelection.ts'),
- 'utf8',
-);
+import { createStationarySelectionCoordinator } from './stationarySelection';
-describe('stationarySelection', () => {
- it('exports createStationarySelectionCoordinator', () => {
- expect(stationarySelectionSource).toContain(
- 'export function createStationarySelectionCoordinator',
- );
- });
+describe('stationarySelection coordinator', () => {
+ it('fresh begin creates generation 1', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const generation = coordinator.beginTransaction();
- it('exports TransactionState type', () => {
- expect(stationarySelectionSource).toContain('TransactionState');
+ expect(typeof generation).toBe('number');
+ expect(generation).toBe(1);
+ const tx = coordinator.getTransaction();
+ expect(tx).not.toBeNull();
+ expect(tx?.transactionId).toBe(generation);
});
- it('exports StationarySelectionCoordinator interface', () => {
- expect(stationarySelectionSource).toContain('export interface StationarySelectionCoordinator');
- });
+ it('next begin creates a greater generation', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const generation1 = coordinator.beginTransaction();
+ coordinator.clearTransaction();
+ const generation2 = coordinator.beginTransaction();
- it('exports StationaryTapSelector', () => {
- expect(stationarySelectionSource).toContain('export { StationaryTapSelector }');
+ expect(generation2).toBeGreaterThan(generation1);
});
- it('exports createStudioStationarySelectionWrapNode', () => {
- expect(stationarySelectionSource).toContain(
- 'export function createStudioStationarySelectionWrapNode',
- );
- });
+ it('stale generation record is ignored after clear', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen1 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.clearTransaction();
- it('re-exports stationarySelection from the runtime index', () => {
- const indexSource = readFileSync(join(import.meta.dir, 'index.ts'), 'utf8');
- expect(indexSource).toContain("export * from './stationarySelection.js'");
+ coordinator.beginTransaction();
+ const tx = coordinator.getTransaction();
+ expect(tx?.transactionId).not.toBe(gen1);
+ expect(tx?.path).toEqual([]);
});
- it('uses a shared tracker ref via context (no React ref)', () => {
- expect(stationarySelectionSource).toContain('TrackerContext');
- expect(stationarySelectionSource).toContain('trackerRef');
- });
+ it('recordNode adds node to the active transaction path', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
- it('has no module-global mutable tracker', () => {
- const trackerDecl = /const\s+\w+.*MutableRefObject/s.exec(stationarySelectionSource);
- const moduleGlobalTracker = trackerDecl?.find((match) => !match.includes('createContext'));
- expect(moduleGlobalTracker).toBeUndefined();
+ const tx = coordinator.getTransaction();
+ expect(tx?.path).toContain('node-a');
+ expect(tx?.path).toEqual(['node-a']);
});
- it('has root selector own tracker instance via createStationarySelectionCoordinator', () => {
- expect(stationarySelectionSource).toContain('createStationarySelectionCoordinator');
- });
+ it('nested path records outer-to-inner ordering', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('outer');
+ coordinator.recordNode('inner');
- it('records an ordered node path via push', () => {
- expect(stationarySelectionSource).toContain('.path.push(');
+ const tx = coordinator.getTransaction();
+ expect(tx?.path).toEqual(['outer', 'inner']);
});
- it('ignores duplicate node participation', () => {
- expect(stationarySelectionSource).toContain('!tx.path.includes(nodeId)');
- });
+ it('duplicate node IDs are ignored', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.recordNode('node-a');
- it('uses deterministic deepest node wins via destructured path first element', () => {
- expect(stationarySelectionSource).toContain('const [deepestNodeId] = tx.path');
+ const tx = coordinator.getTransaction();
+ expect(tx?.path).toEqual(['node-a']);
+ expect(tx?.path.length).toBe(1);
});
- it('has transaction generation token', () => {
- expect(stationarySelectionSource).toContain('transactionId: Date.now()');
- });
+ it('deepest node is selected from nested path', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('outer');
+ coordinator.recordNode('inner');
+
+ let selectedNodeId: string | null = null;
+ coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedNodeId = id;
+ },
+ 1,
+ );
- it('BEGAN clears stale state by beginning a fresh transaction', () => {
- expect(stationarySelectionSource).toContain('State.BEGAN');
- expect(stationarySelectionSource).toContain('beginTransaction');
+ expect(selectedNodeId).not.toBeNull();
+ expect(typeof selectedNodeId).toBe('string');
+ const isInner = selectedNodeId === 'inner';
+ expect(isInner).toBe(true);
});
- it('stationary Edit success commits once via finalized flag', () => {
- expect(stationarySelectionSource).toContain('finalized');
- });
+ it('Edit selects exactly once', () => {
+ let selectionCount = 0;
+
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ selectionCount += 1;
+ },
+ 1,
+ );
- it('Preview never commits', () => {
- expect(stationarySelectionSource).toContain('if (!isEditMode)');
+ expect(selectionCount).toBe(1);
});
- it('clears state on movement end', () => {
- expect(stationarySelectionSource).toContain('clearTransaction');
- });
+ it('already-selected node invokes no selection callback', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+
+ let called = false;
+ coordinator.commitSelection(
+ true,
+ 'node-a',
+ () => {
+ called = true;
+ },
+ 1,
+ );
- it('handles failure and cancellation by clearing state', () => {
- expect(stationarySelectionSource).toContain('State.FAILED');
- expect(stationarySelectionSource).toContain('State.CANCELLED');
+ expect(called).toBe(false);
});
- it('handles END by clearing state', () => {
- expect(stationarySelectionSource).toContain('State.END');
+ it('Preview invokes no selection callback', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+
+ let selectedId: string | null = 'initial';
+ const result = coordinator.commitSelection(
+ false,
+ null,
+ (id) => {
+ selectedId = id;
+ },
+ 1,
+ );
+
+ expect(result).toBe('preview');
+ expect(selectedId).toBe('initial');
+ const tx = coordinator.getTransaction();
+ expect(tx?.finalized).not.toBe(true);
});
- it('has mode change clear via useEffect on isEditMode', () => {
- expect(stationarySelectionSource).toContain('[props.isEditMode]');
+ it('moved transaction invokes no callback', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ const tx = coordinator.getTransaction();
+ if (tx) {
+ tx.moved = true;
+ }
+
+ let selectedId: string | null = 'initial';
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedId = id;
+ },
+ 1,
+ );
+
+ expect(result).toBe('moved');
+ expect(selectedId).toBe('initial');
});
- it('node-level touch-end does not clear path before root commitment', () => {
- expect(stationarySelectionSource).toContain('onTouchStart');
- expect(stationarySelectionSource).not.toContain('onTouchEnd');
- expect(stationarySelectionSource).not.toContain('onTouchEnd');
+ it('empty path invokes no callback', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+
+ let selectedId: string | null = null;
+ let selectionCount = 0;
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedId = id;
+ selectionCount += 1;
+ },
+ 1,
+ );
+
+ expect(result).toBe('empty');
+ expect(selectedId).toBeNull();
+ expect(selectionCount).toBe(0);
});
- it('has no Pressable recorders', () => {
- expect(stationarySelectionSource).not.toContain('Pressable');
+ it('stale generation commit returns stale', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.clearTransaction();
+
+ let selectedId: string | null = null;
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedId = id;
+ },
+ 1,
+ );
+
+ expect(result).toBe('empty');
+ expect(selectedId).toBeNull();
});
- it('has exactly one root-level GestureHandler import, no per-node recognizer imports', () => {
- const importMatches = stationarySelectionSource.match(/from 'react-native-gesture-handler'/g);
- expect(importMatches?.length).toBe(1);
+ it('already-finalized commit returns already-finalized', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.commitSelection(
+ true,
+ null,
+ (_id) => {
+ void _id;
+ },
+ 1,
+ );
+
+ let callCount = 0;
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ callCount += 1;
+ },
+ 1,
+ );
+
+ expect(result).toBe('already-finalized');
+ expect(callCount).toBe(0);
});
- it('has no cloneElement', () => {
- expect(stationarySelectionSource).not.toContain('cloneElement');
+ it('duplicate terminal event for the same generation does not double-commit', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ let callCount = 0;
+
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+
+ coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ callCount += 1;
+ },
+ gen,
+ );
+
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ callCount += 1;
+ },
+ gen,
+ );
+
+ expect(callCount).toBe(1);
+ expect(result).toBe('already-finalized');
});
- it('has no responder stealing', () => {
- expect(stationarySelectionSource).not.toContain('responder');
+ it('ACTIVE/final terminal processing leaves no transaction', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.commitSelection(
+ true,
+ null,
+ (_id) => {
+ void _id;
+ },
+ 1,
+ );
+ coordinator.clearTransaction();
+
+ expect(coordinator.getTransaction()).toBeNull();
});
- it('has no rectangle hit testing', () => {
- expect(stationarySelectionSource).not.toContain('hitTest');
- expect(stationarySelectionSource).not.toContain('measure');
+ it('FAILED clears state', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.clearTransaction();
+
+ expect(coordinator.getTransaction()).toBeNull();
});
- it('preserves component identity and authored props in wrapNode', () => {
- expect(stationarySelectionSource).toContain('args.rendered');
+ it('CANCELLED clears state', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.clearTransaction();
+
+ expect(coordinator.getTransaction()).toBeNull();
});
- it('uses pointerEvents box-none on unsupported node wrapper, not none', () => {
- expect(stationarySelectionSource).toContain("'box-none'");
- expect(stationarySelectionSource).not.toContain("'none'");
+ it('END clears state', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.clearTransaction();
+
+ expect(coordinator.getTransaction()).toBeNull();
});
- it('thirdPartySupport declaration is Readonly>', () => {
- expect(stationarySelectionSource).toContain('Readonly>');
+ it('mode change clears active transaction', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.clearTransaction();
+
+ expect(coordinator.getTransaction()).toBeNull();
});
- it('thirdPartySupport does not contain mode (enabled/passive)', () => {
- expect(stationarySelectionSource).not.toContain("'enabled'");
- expect(stationarySelectionSource).not.toContain("'passive'");
+ it('no transaction survives completion', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ coordinator.beginTransaction();
+ coordinator.recordNode('node-a');
+ coordinator.commitSelection(
+ true,
+ null,
+ (_id) => {
+ void _id;
+ },
+ 1,
+ );
+ coordinator.clearTransaction();
+
+ expect(coordinator.getTransaction()).toBeNull();
});
});
diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts
index 27472fa5..ce0134c7 100644
--- a/src/runtime/stationarySelection.ts
+++ b/src/runtime/stationarySelection.ts
@@ -1,7 +1,10 @@
import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora';
import React from 'react';
import { View } from 'react-native';
-import { State, TapGestureHandler } from 'react-native-gesture-handler';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+
+export type CommitSelectionResult =
+ 'committed' | 'already-selected' | 'preview' | 'moved' | 'empty' | 'stale' | 'already-finalized';
export interface TransactionState {
readonly transactionId: number;
@@ -12,44 +15,42 @@ export interface TransactionState {
export interface StationarySelectionCoordinator {
readonly trackerRef: React.MutableRefObject;
- beginTransaction(): void;
+ beginTransaction(): number;
recordNode(nodeId: string): void;
- markMoved(): void;
commitSelection(
isEditMode: boolean,
selectedNodeId: string | null,
selectNode: (id: string | null) => void,
- ): void;
+ generation: number,
+ ): CommitSelectionResult;
clearTransaction(): void;
getTransaction(): TransactionState | null;
- handleStateChange(
- state: number,
- isEditMode: boolean,
- selectedNodeId: string | null,
- selectNode: (id: string | null) => void,
- ): void;
}
const TrackerContext = React.createContext | null>(
null,
);
+let nextTransactionId = 1;
+
export function createStationarySelectionCoordinator(): StationarySelectionCoordinator {
const trackerRef = { current: null as TransactionState | null };
- function beginTransaction(): void {
- trackerRef.current = {
- transactionId: Date.now(),
+ function beginTransaction(): number {
+ const tx = {
+ transactionId: nextTransactionId++,
path: [],
moved: false,
finalized: false,
};
+ trackerRef.current = tx;
+ return tx.transactionId;
}
function recordNode(nodeId: string): void {
const tx = trackerRef.current;
- if (!tx || tx.transactionId === 0 || tx.finalized) {
+ if (!tx || tx.finalized) {
return;
}
@@ -58,60 +59,57 @@ export function createStationarySelectionCoordinator(): StationarySelectionCoord
}
}
- function markMoved(): void {
- if (trackerRef.current) {
- trackerRef.current.moved = true;
- }
- }
-
function commitSelection(
isEditMode: boolean,
selectedNodeId: string | null,
selectNode: (id: string | null) => void,
- ): void {
+ generation: number,
+ ): CommitSelectionResult {
if (!isEditMode) {
- return;
+ return 'preview';
}
const tx = trackerRef.current;
- if (!tx || tx.finalized || tx.moved || tx.path.length === 0) {
- return;
+ if (!tx) {
+ return 'empty';
+ }
+
+ if (tx.transactionId !== generation) {
+ return 'stale';
+ }
+
+ if (tx.finalized) {
+ return 'already-finalized';
+ }
+
+ if (tx.moved) {
+ return 'moved';
+ }
+
+ if (tx.path.length === 0) {
+ return 'empty';
}
const [deepestNodeId] = tx.path;
+ if (deepestNodeId != null && deepestNodeId === selectedNodeId) {
+ return 'already-selected';
+ }
+
if (deepestNodeId != null && deepestNodeId !== selectedNodeId) {
selectNode(deepestNodeId);
}
tx.finalized = true;
+
+ return 'committed';
}
function clearTransaction(): void {
trackerRef.current = null;
}
- function handleStateChange(
- state: number,
- isEditMode: boolean,
- selectedNodeId: string | null,
- selectNode: (id: string | null) => void,
- ): void {
- if (state === State.BEGAN) {
- beginTransaction();
- }
-
- if (state === State.ACTIVE) {
- commitSelection(isEditMode, selectedNodeId, selectNode);
- clearTransaction();
- }
-
- if (state === State.FAILED || state === State.CANCELLED || state === State.END) {
- clearTransaction();
- }
- }
-
function getTransaction(): TransactionState | null {
return trackerRef.current;
}
@@ -120,16 +118,15 @@ export function createStationarySelectionCoordinator(): StationarySelectionCoord
trackerRef,
beginTransaction,
recordNode,
- markMoved,
commitSelection,
clearTransaction,
- handleStateChange,
getTransaction,
};
}
function StudioNodeTouchRecorder(props: {
readonly nodeId: string | undefined;
+ readonly showUnsupportedIndicator: boolean;
readonly children: React.ReactNode;
}): React.JSX.Element {
const tracker = React.useContext(TrackerContext);
@@ -137,14 +134,10 @@ function StudioNodeTouchRecorder(props: {
nodeIdRef.current = props.nodeId;
- function handleTouchStart(): void {
- if (!tracker?.current || !nodeIdRef.current) {
- return;
- }
+ function handleTouchStartCapture(): void {
+ const tx = tracker?.current;
- const tx = tracker.current;
-
- if (tx.transactionId === 0) {
+ if (!tx || tx.finalized || !nodeIdRef.current) {
return;
}
@@ -153,20 +146,27 @@ function StudioNodeTouchRecorder(props: {
}
}
- function handleTouchMove(): void {
- if (!tracker?.current) {
- return;
- }
-
- tracker.current.moved = true;
- }
-
return React.createElement(
View,
{
- onTouchStart: handleTouchStart,
- onTouchMove: handleTouchMove,
+ style: { position: 'relative' },
+ onTouchStart: handleTouchStartCapture,
},
+ props.showUnsupportedIndicator
+ ? React.createElement(View, {
+ style: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ borderWidth: 1,
+ borderColor: '#ef4444',
+ borderStyle: 'dashed',
+ pointerEvents: 'none',
+ },
+ })
+ : null,
props.children,
);
}
@@ -195,26 +195,11 @@ export function createStudioStationarySelectionWrapNode(options?: {
args.node.type in ZORA_COMPONENT_REGISTRY ||
(thirdPartySupport != null && args.node.type in thirdPartySupport);
- if (isEditMode && !isSupported) {
- return React.createElement(
- View,
- {
- style: {
- borderWidth: 1,
- borderColor: '#ef4444',
- borderStyle: 'dashed',
- },
- pointerEvents: 'box-none',
- },
- React.createElement(StudioNodeTouchRecorder, {
- nodeId: args.node.id,
- children: args.rendered,
- }),
- );
- }
+ const showUnsupportedIndicator = isEditMode && !isSupported;
return React.createElement(StudioNodeTouchRecorder, {
nodeId: args.node.id,
+ showUnsupportedIndicator: showUnsupportedIndicator,
children: args.rendered,
});
};
@@ -226,32 +211,51 @@ function StationaryTapSelector(props: {
readonly selectNode: (id: string | null) => void;
readonly children: React.ReactNode;
}): React.JSX.Element {
- const coordinator = createStationarySelectionCoordinator();
-
- function handleStateChange(event: { readonly nativeEvent: { readonly state: number } }): void {
- coordinator.handleStateChange(
- event.nativeEvent.state,
- props.isEditMode,
- props.selectedNodeId,
- props.selectNode,
- );
- }
+ const coordinatorRef = React.useRef(createStationarySelectionCoordinator());
+ const coordinator = coordinatorRef.current;
+
+ const isEditModeRef = React.useRef(props.isEditMode);
+ const selectedNodeIdRef = React.useRef(props.selectedNodeId);
+ const selectNodeRef = React.useRef(props.selectNode);
+
+ isEditModeRef.current = props.isEditMode;
+ selectedNodeIdRef.current = props.selectedNodeId;
+ selectNodeRef.current = props.selectNode;
+
+ const generationRef = React.useRef(0);
+
+ const tapGesture = React.useMemo(() => {
+ return Gesture.Tap()
+ .maxDeltaX(5)
+ .maxDeltaY(5)
+ .maxDuration(500)
+ .onStart(() => {
+ generationRef.current = coordinator.beginTransaction();
+ })
+ .onEnd((_event, success) => {
+ if (success) {
+ coordinator.commitSelection(
+ isEditModeRef.current,
+ selectedNodeIdRef.current,
+ selectNodeRef.current,
+ generationRef.current,
+ );
+ }
+
+ coordinator.clearTransaction();
+ });
+ }, [coordinator]);
React.useEffect(() => {
coordinator.clearTransaction();
- }, [props.isEditMode]);
+ }, [props.isEditMode, coordinator]);
return React.createElement(
TrackerContext.Provider,
{ value: coordinator.trackerRef },
React.createElement(
- TapGestureHandler,
- {
- maxDeltaX: 5,
- maxDeltaY: 5,
- maxDurationMs: 500,
- onHandlerStateChange: handleStateChange,
- },
+ GestureDetector,
+ { gesture: tapGesture },
React.createElement(View, { style: { flex: 1 } }, props.children),
),
);
From c0f3810e6c001b199622b1a25f726eec610059c4 Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Tue, 28 Jul 2026 15:48:45 +0200
Subject: [PATCH 3/8] fix: correct stationary tap transaction lifecycle
---
src/host/layout/templates/rootLayout.test.ts | 2 +-
src/host/layout/templates/rootLayout.ts | 4 +-
src/runtime/index.ts | 2 +
src/runtime/interactionPolicy.test.ts | 115 ++++--
src/runtime/interactionPolicy.ts | 87 ++---
src/runtime/interactionPolicyCore.ts | 82 ++++
src/runtime/stationarySelection.test.ts | 352 +++---------------
src/runtime/stationarySelection.ts | 183 +++------
.../stationarySelectionCoordinator.test.ts | 268 +++++++++++++
src/runtime/stationarySelectionCoordinator.ts | 141 +++++++
10 files changed, 698 insertions(+), 538 deletions(-)
create mode 100644 src/runtime/interactionPolicyCore.ts
create mode 100644 src/runtime/stationarySelectionCoordinator.test.ts
create mode 100644 src/runtime/stationarySelectionCoordinator.ts
diff --git a/src/host/layout/templates/rootLayout.test.ts b/src/host/layout/templates/rootLayout.test.ts
index dd177b81..434867ec 100644
--- a/src/host/layout/templates/rootLayout.test.ts
+++ b/src/host/layout/templates/rootLayout.test.ts
@@ -227,7 +227,7 @@ test('scopes Studio runtime selection config below StudioProvider', () => {
expect(rootLayoutSource).not.toContain('wrapNode: wrapStudioRuntimeNode');
expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo(');
expect(studioShellSource).toContain('disableActions: !previewMode');
- expect(studioShellSource).toContain('wrapNode: studioWrapNode');
+ expect(studioShellSource).toContain('wrapNode: studioWrapNode');
expect(studioShellSource).toContain(
'',
);
diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts
index e8dbdab1..2dec0492 100644
--- a/src/host/layout/templates/rootLayout.ts
+++ b/src/host/layout/templates/rootLayout.ts
@@ -38,9 +38,7 @@ export function getRootLayoutImportRequirements(
source: 'react',
namedImports: [
{ imported: 'ReactNode', typeOnly: true },
- ...(includeStudio
- ? [{ imported: 'useState' }]
- : []),
+ ...(includeStudio ? [{ imported: 'useState' }] : []),
],
},
...(includeStudio
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
index 1a87fda4..fbd857b0 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -1,10 +1,12 @@
export * from './actionSuppression.js';
export * from './appExtensionRegistry.js';
export * from './interactionPolicy.js';
+export * from './interactionPolicyCore.js';
export * from './localizationPreview.js';
export * from './previewRegistry.js';
export * from './previewRuntimeConfig.js';
export * from './registry.js';
export * from './runtimeActions.js';
export * from './stationarySelection.js';
+export * from './stationarySelectionCoordinator.js';
export * from './useRuntimeAction.js';
diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts
index e6439d7d..50b55d03 100644
--- a/src/runtime/interactionPolicy.test.ts
+++ b/src/runtime/interactionPolicy.test.ts
@@ -1,71 +1,74 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
import { describe, expect, it } from 'bun:test';
-import { createStudioInteractionPolicyResolver } from './interactionPolicy';
+import { createInteractionPolicyResolver, isComponentSupported } from './interactionPolicyCore';
-describe('interactionPolicy', () => {
- it('assigns passive policy to ZORA components in Edit mode', () => {
- const resolver = createStudioInteractionPolicyResolver({ previewMode: false });
+describe('interactionPolicyCore', () => {
+ it('assigns passive policy to supported components in Edit mode', () => {
+ const resolver = createInteractionPolicyResolver({
+ previewMode: false,
+ isSupportedNodeType: () => true,
+ });
const result = resolver({
- node: { id: 'test', type: 'Text' },
+ node: { type: 'Text' },
props: { children: 'Hello' },
});
expect(result.interactionPolicy).toBe('passive');
});
- it('assigns enabled policy to ZORA components in Preview mode', () => {
- const resolver = createStudioInteractionPolicyResolver({ previewMode: true });
+ it('assigns enabled policy to supported components in Preview mode', () => {
+ const resolver = createInteractionPolicyResolver({
+ previewMode: true,
+ isSupportedNodeType: () => true,
+ });
const result = resolver({
- node: { id: 'test', type: 'Text' },
+ node: { type: 'Text' },
props: { children: 'Hello' },
});
expect(result.interactionPolicy).toBe('enabled');
});
- it('preserves existing resolver output', () => {
- const existingResolver = (() => {
- const base = { fontSize: 16 };
- return (_args: unknown) => base;
- })();
-
- const resolver = createStudioInteractionPolicyResolver({
+ it('preserves props from resolve args', () => {
+ const resolver = createInteractionPolicyResolver({
previewMode: true,
- existingResolver,
+ isSupportedNodeType: () => true,
});
+
const result = resolver({
- node: { id: 'test', type: 'Text' },
- props: { children: 'Hello' },
+ node: { type: 'Text' },
+ props: { fontSize: 16 },
});
expect(result.interactionPolicy).toBe('enabled');
expect(result.fontSize).toBe(16);
});
- it('preserves unrelated props from existing resolver', () => {
- const existingResolver = (() => {
- return (_args: unknown) => ({ customProp: 'value' });
- })();
-
- const resolver = createStudioInteractionPolicyResolver({
+ it('preserves unrelated props from resolve args', () => {
+ const resolver = createInteractionPolicyResolver({
previewMode: false,
- existingResolver,
+ isSupportedNodeType: () => true,
});
+
const result = resolver({
- node: { id: 'test', type: 'Text' },
- props: { children: 'Hello' },
+ node: { type: 'Text' },
+ props: { customProp: 'value' },
});
expect(result.customProp).toBe('value');
});
it('assigns mode-derived policy to opted-in third-party components', () => {
- const resolver = createStudioInteractionPolicyResolver({
+ const resolver = createInteractionPolicyResolver({
previewMode: false,
thirdPartySupport: { CustomComponent: true },
+ isSupportedNodeType: () => false,
});
const result = resolver({
- node: { id: 'test', type: 'CustomComponent' },
+ node: { type: 'CustomComponent' },
props: {},
});
@@ -73,12 +76,13 @@ describe('interactionPolicy', () => {
});
it('assigns enabled policy to opted-in third-party in Preview mode', () => {
- const resolver = createStudioInteractionPolicyResolver({
+ const resolver = createInteractionPolicyResolver({
previewMode: true,
thirdPartySupport: { CustomComponent: true },
+ isSupportedNodeType: () => false,
});
const result = resolver({
- node: { id: 'test', type: 'CustomComponent' },
+ node: { type: 'CustomComponent' },
props: {},
});
@@ -86,12 +90,13 @@ describe('interactionPolicy', () => {
});
it('assigns no policy to undeclared third-party components', () => {
- const resolver = createStudioInteractionPolicyResolver({
+ const resolver = createInteractionPolicyResolver({
previewMode: false,
thirdPartySupport: { CustomComponent: true },
+ isSupportedNodeType: () => false,
});
const result = resolver({
- node: { id: 'test', type: 'ThirdPartyComponent' },
+ node: { type: 'ThirdPartyComponent' },
props: {},
});
@@ -99,9 +104,12 @@ describe('interactionPolicy', () => {
});
it('assigns no policy to unknown types', () => {
- const resolver = createStudioInteractionPolicyResolver({ previewMode: false });
+ const resolver = createInteractionPolicyResolver({
+ previewMode: false,
+ isSupportedNodeType: () => false,
+ });
const result = resolver({
- node: { id: 'test', type: 'UnknownWidget' },
+ node: { type: 'UnknownWidget' },
props: {},
});
@@ -109,21 +117,23 @@ describe('interactionPolicy', () => {
});
it('third-party support cannot override mode policy direction', () => {
- const resolver = createStudioInteractionPolicyResolver({
+ const resolver = createInteractionPolicyResolver({
previewMode: false,
thirdPartySupport: { CustomComponent: true },
+ isSupportedNodeType: () => false,
});
const editResult = resolver({
- node: { id: 'test', type: 'CustomComponent' },
+ node: { type: 'CustomComponent' },
props: {},
});
- const resolverPreview = createStudioInteractionPolicyResolver({
+ const resolverPreview = createInteractionPolicyResolver({
previewMode: true,
thirdPartySupport: { CustomComponent: true },
+ isSupportedNodeType: () => false,
});
const previewResult = resolverPreview({
- node: { id: 'test', type: 'CustomComponent' },
+ node: { type: 'CustomComponent' },
props: {},
});
@@ -131,4 +141,31 @@ describe('interactionPolicy', () => {
expect(editResult.interactionPolicy).toBe('passive');
expect(previewResult.interactionPolicy).toBe('enabled');
});
+
+ it('isComponentSupported returns true for zora-builtin', () => {
+ expect(isComponentSupported('Text', {}, () => true)).toBe(true);
+ });
+
+ it('isComponentSupported returns true for third-party-supported', () => {
+ expect(isComponentSupported('Custom', { Custom: true }, () => false)).toBe(true);
+ });
+
+ it('isComponentSupported returns false for unsupported', () => {
+ expect(isComponentSupported('Unknown', {}, () => false)).toBe(false);
+ });
+});
+
+describe('interactionPolicy production adapter', () => {
+ const sourcePath = join(import.meta.dir, 'interactionPolicy.ts');
+ const source = readFileSync(sourcePath, 'utf8');
+
+ it('derives support from the actual ZORA_COMPONENT_REGISTRY', () => {
+ expect(source).toContain("from '@ankhorage/zora'");
+ expect(source).toContain('ZORA_COMPONENT_REGISTRY');
+ expect(source).toContain('nodeType in ZORA_COMPONENT_REGISTRY');
+ });
+
+ it('delegates classification to the pure core', () => {
+ expect(source).toContain('createInteractionPolicyResolver');
+ });
});
diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts
index 9f1ed66e..01c26e96 100644
--- a/src/runtime/interactionPolicy.ts
+++ b/src/runtime/interactionPolicy.ts
@@ -1,9 +1,10 @@
-import type { RuntimeResolveNodePropsArgs } from '@ankhorage/contracts/runtime';
import { type RuntimeNodePropsResolver } from '@ankhorage/runtime';
import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora';
-export type InteractionPolicy = 'enabled' | 'passive';
-export type ThirdPartyComponentSupport = Readonly>;
+import type { ThirdPartyComponentSupport } from './interactionPolicyCore.js';
+import { createInteractionPolicyResolver } from './interactionPolicyCore.js';
+
+export type { InteractionPolicy, ThirdPartyComponentSupport } from './interactionPolicyCore.js';
export interface StudioInteractionPolicyResolverArgs {
readonly previewMode: boolean;
@@ -11,65 +12,33 @@ export interface StudioInteractionPolicyResolverArgs {
readonly existingResolver?: RuntimeNodePropsResolver;
}
-const EMPTY_THIRD_PARTY: ThirdPartyComponentSupport = {};
-
-type ComponentClassification = 'zora-builtin' | 'third-party-supported' | 'unsupported';
-
-function classifyComponent(
- nodeType: string,
- thirdPartySupport: ThirdPartyComponentSupport,
-): ComponentClassification {
- if (nodeType in ZORA_COMPONENT_REGISTRY) {
- return 'zora-builtin';
- }
-
- if (nodeType in thirdPartySupport) {
- return 'third-party-supported';
- }
-
- return 'unsupported';
-}
-
-export function isComponentSupported(
- nodeType: string,
- thirdPartySupport: ThirdPartyComponentSupport,
-): boolean {
- return classifyComponent(nodeType, thirdPartySupport) !== 'unsupported';
-}
-
-function resolveInteractionPolicy(
- nodeType: string,
- previewMode: boolean,
- thirdPartySupport: ThirdPartyComponentSupport,
-): InteractionPolicy | undefined {
- const classification = classifyComponent(nodeType, thirdPartySupport);
-
- if (classification === 'unsupported') {
- return undefined;
- }
-
- return previewMode ? 'enabled' : 'passive';
-}
-
-const POLICY_PROP = 'interactionPolicy';
-
export function createStudioInteractionPolicyResolver(
args: StudioInteractionPolicyResolverArgs,
): RuntimeNodePropsResolver {
- const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, existingResolver } = args;
-
- return (resolveArgs: RuntimeResolveNodePropsArgs) => {
- const baseProps = existingResolver ? existingResolver(resolveArgs) : { ...resolveArgs.props };
-
- const policy = resolveInteractionPolicy(resolveArgs.node.type, previewMode, thirdPartySupport);
-
- if (policy === undefined) {
- return baseProps;
- }
-
- return {
- ...baseProps,
- [POLICY_PROP]: policy,
+ const { previewMode, thirdPartySupport, existingResolver } = args;
+
+ const resolver = createInteractionPolicyResolver({
+ previewMode,
+ thirdPartySupport,
+ isSupportedNodeType: (nodeType) => nodeType in ZORA_COMPONENT_REGISTRY,
+ });
+
+ if (!existingResolver) {
+ return (resolveArgs) => {
+ const result = resolver({
+ node: resolveArgs.node,
+ props: resolveArgs.props,
+ });
+ return result;
};
+ }
+
+ return (resolveArgs) => {
+ const baseProps = existingResolver(resolveArgs);
+ const result = resolver({
+ node: resolveArgs.node,
+ props: baseProps,
+ });
+ return result;
};
}
diff --git a/src/runtime/interactionPolicyCore.ts b/src/runtime/interactionPolicyCore.ts
new file mode 100644
index 00000000..970ac8bc
--- /dev/null
+++ b/src/runtime/interactionPolicyCore.ts
@@ -0,0 +1,82 @@
+export type InteractionPolicy = 'enabled' | 'passive';
+export type ThirdPartyComponentSupport = Readonly>;
+
+export interface InteractionPolicyResolverArgs {
+ readonly previewMode: boolean;
+ readonly thirdPartySupport?: ThirdPartyComponentSupport;
+ readonly isSupportedNodeType: (nodeType: string) => boolean;
+}
+
+const EMPTY_THIRD_PARTY: ThirdPartyComponentSupport = {};
+
+type ComponentClassification = 'zora-builtin' | 'third-party-supported' | 'unsupported';
+
+function classifyComponent(
+ nodeType: string,
+ isSupportedNodeType: (type: string) => boolean,
+ thirdPartySupport: ThirdPartyComponentSupport,
+): ComponentClassification {
+ if (isSupportedNodeType(nodeType)) {
+ return 'zora-builtin';
+ }
+
+ if (nodeType in thirdPartySupport) {
+ return 'third-party-supported';
+ }
+
+ return 'unsupported';
+}
+
+export function isComponentSupported(
+ nodeType: string,
+ thirdPartySupport: ThirdPartyComponentSupport,
+ isSupportedNodeType: (type: string) => boolean,
+): boolean {
+ return classifyComponent(nodeType, isSupportedNodeType, thirdPartySupport) !== 'unsupported';
+}
+
+function resolveInteractionPolicy(
+ nodeType: string,
+ previewMode: boolean,
+ thirdPartySupport: ThirdPartyComponentSupport,
+ isSupportedNodeType: (type: string) => boolean,
+): InteractionPolicy | undefined {
+ const classification = classifyComponent(nodeType, isSupportedNodeType, thirdPartySupport);
+
+ if (classification === 'unsupported') {
+ return undefined;
+ }
+
+ return previewMode ? 'enabled' : 'passive';
+}
+
+const POLICY_PROP = 'interactionPolicy';
+
+export function createInteractionPolicyResolver(
+ args: InteractionPolicyResolverArgs,
+): (resolveArgs: {
+ node: { type: string };
+ props: Record;
+}) => Record {
+ const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, isSupportedNodeType } = args;
+
+ return (resolveArgs) => {
+ const baseProps = { ...resolveArgs.props };
+
+ const policy = resolveInteractionPolicy(
+ resolveArgs.node.type,
+ previewMode,
+ thirdPartySupport,
+ isSupportedNodeType,
+ );
+
+ if (policy === undefined) {
+ return baseProps;
+ }
+
+ return {
+ ...baseProps,
+ [POLICY_PROP]: policy,
+ };
+ };
+}
diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts
index e6adf39c..e1bb03df 100644
--- a/src/runtime/stationarySelection.test.ts
+++ b/src/runtime/stationarySelection.test.ts
@@ -1,337 +1,91 @@
-import { describe, expect, it } from 'bun:test';
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
-import { createStationarySelectionCoordinator } from './stationarySelection';
+import { describe, expect, it } from 'bun:test';
-describe('stationarySelection coordinator', () => {
- it('fresh begin creates generation 1', () => {
- const coordinator = createStationarySelectionCoordinator();
- const generation = coordinator.beginTransaction();
+const sourcePath = join(import.meta.dir, 'stationarySelection.ts');
+const source = readFileSync(sourcePath, 'utf8');
- expect(typeof generation).toBe('number');
- expect(generation).toBe(1);
- const tx = coordinator.getTransaction();
- expect(tx).not.toBeNull();
- expect(tx?.transactionId).toBe(generation);
+describe('stationarySelection RN integration', () => {
+ it('uses exactly one Gesture.Tap() invocation', () => {
+ const matches = source.match(/Gesture\.Tap\(\)/g) ?? [];
+ expect(matches.length).toBe(1);
});
- it('next begin creates a greater generation', () => {
- const coordinator = createStationarySelectionCoordinator();
- const generation1 = coordinator.beginTransaction();
- coordinator.clearTransaction();
- const generation2 = coordinator.beginTransaction();
-
- expect(generation2).toBeGreaterThan(generation1);
+ it('uses exactly one GestureDetector', () => {
+ const importLine = "import { Gesture, GestureDetector } from 'react-native-gesture-handler';";
+ const withoutImport = source.replace(importLine, '');
+ const matches = withoutImport.match(/GestureDetector/g) ?? [];
+ expect(matches.length).toBe(1);
});
- it('stale generation record is ignored after clear', () => {
- const coordinator = createStationarySelectionCoordinator();
- const gen1 = coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.clearTransaction();
-
- coordinator.beginTransaction();
- const tx = coordinator.getTransaction();
- expect(tx?.transactionId).not.toBe(gen1);
- expect(tx?.path).toEqual([]);
+ it('enables runOnJS(true)', () => {
+ expect(source).toContain('.runOnJS(true)');
});
- it('recordNode adds node to the active transaction path', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
-
- const tx = coordinator.getTransaction();
- expect(tx?.path).toContain('node-a');
- expect(tx?.path).toEqual(['node-a']);
+ it('begins transaction in onBegin', () => {
+ expect(source).toContain('.onBegin(() => {');
+ expect(source).toContain('generationRef.current = coordinator.beginTransaction()');
});
- it('nested path records outer-to-inner ordering', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('outer');
- coordinator.recordNode('inner');
-
- const tx = coordinator.getTransaction();
- expect(tx?.path).toEqual(['outer', 'inner']);
+ it('commits in onEnd only on success', () => {
+ expect(source).toContain('.onEnd((_event, success) => {');
+ expect(source).toContain('if (!success) {');
+ expect(source).toContain('coordinator.commitSelection(');
});
- it('duplicate node IDs are ignored', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.recordNode('node-a');
-
- const tx = coordinator.getTransaction();
- expect(tx?.path).toEqual(['node-a']);
- expect(tx?.path.length).toBe(1);
+ it('finalizes in onFinalize', () => {
+ expect(source).toContain('.onFinalize(() => {');
+ expect(source).toContain('coordinator.clearTransaction(generationRef.current)');
});
- it('deepest node is selected from nested path', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('outer');
- coordinator.recordNode('inner');
-
- let selectedNodeId: string | null = null;
- coordinator.commitSelection(
- true,
- null,
- (id) => {
- selectedNodeId = id;
- },
- 1,
- );
-
- expect(selectedNodeId).not.toBeNull();
- expect(typeof selectedNodeId).toBe('string');
- const isInner = selectedNodeId === 'inner';
- expect(isInner).toBe(true);
+ it('does not use onStart', () => {
+ expect(source).not.toContain('.onStart(() => {');
});
- it('Edit selects exactly once', () => {
- let selectionCount = 0;
-
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.commitSelection(
- true,
- null,
- () => {
- selectionCount += 1;
- },
- 1,
- );
-
- expect(selectionCount).toBe(1);
+ it('does not use legacy TapGestureHandler', () => {
+ expect(source).not.toContain('TapGestureHandler');
});
- it('already-selected node invokes no selection callback', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
-
- let called = false;
- coordinator.commitSelection(
- true,
- 'node-a',
- () => {
- called = true;
- },
- 1,
- );
-
- expect(called).toBe(false);
+ it('does not use per-node recognizer', () => {
+ const matches = source.match(/Gesture\.Tap\(\)/g) ?? [];
+ expect(matches.length).toBe(1);
});
- it('Preview invokes no selection callback', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
-
- let selectedId: string | null = 'initial';
- const result = coordinator.commitSelection(
- false,
- null,
- (id) => {
- selectedId = id;
- },
- 1,
- );
-
- expect(result).toBe('preview');
- expect(selectedId).toBe('initial');
- const tx = coordinator.getTransaction();
- expect(tx?.finalized).not.toBe(true);
+ it('does not use Pressable', () => {
+ expect(source).not.toContain('Pressable');
});
- it('moved transaction invokes no callback', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- const tx = coordinator.getTransaction();
- if (tx) {
- tx.moved = true;
- }
-
- let selectedId: string | null = 'initial';
- const result = coordinator.commitSelection(
- true,
- null,
- (id) => {
- selectedId = id;
- },
- 1,
- );
-
- expect(result).toBe('moved');
- expect(selectedId).toBe('initial');
+ it('does not use cloneElement', () => {
+ expect(source).not.toContain('cloneElement');
});
- it('empty path invokes no callback', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
-
- let selectedId: string | null = null;
- let selectionCount = 0;
- const result = coordinator.commitSelection(
- true,
- null,
- (id) => {
- selectedId = id;
- selectionCount += 1;
- },
- 1,
- );
-
- expect(result).toBe('empty');
- expect(selectedId).toBeNull();
- expect(selectionCount).toBe(0);
+ it('does not use responder capture', () => {
+ expect(source).not.toContain('onResponderGrant');
+ expect(source).not.toContain('onResponderReject');
});
- it('stale generation commit returns stale', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.clearTransaction();
-
- let selectedId: string | null = null;
- const result = coordinator.commitSelection(
- true,
- null,
- (id) => {
- selectedId = id;
- },
- 1,
- );
-
- expect(result).toBe('empty');
- expect(selectedId).toBeNull();
- });
-
- it('already-finalized commit returns already-finalized', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.commitSelection(
- true,
- null,
- (_id) => {
- void _id;
- },
- 1,
- );
-
- let callCount = 0;
- const result = coordinator.commitSelection(
- true,
- null,
- () => {
- callCount += 1;
- },
- 1,
- );
-
- expect(result).toBe('already-finalized');
- expect(callCount).toBe(0);
+ it('uses display contents for layout-neutral wrapper', () => {
+ expect(source).toContain("display: 'contents'");
});
- it('duplicate terminal event for the same generation does not double-commit', () => {
- const coordinator = createStationarySelectionCoordinator();
- let callCount = 0;
-
- const gen = coordinator.beginTransaction();
- coordinator.recordNode('node-a');
-
- coordinator.commitSelection(
- true,
- null,
- () => {
- callCount += 1;
- },
- gen,
- );
-
- const result = coordinator.commitSelection(
- true,
- null,
- () => {
- callCount += 1;
- },
- gen,
- );
-
- expect(callCount).toBe(1);
- expect(result).toBe('already-finalized');
+ it('preserves unsupported indicator with pointerEvents none', () => {
+ expect(source).toContain("pointerEvents: 'none'");
});
- it('ACTIVE/final terminal processing leaves no transaction', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.commitSelection(
- true,
- null,
- (_id) => {
- void _id;
- },
- 1,
+ it('uses lazy coordinator creation', () => {
+ expect(source).toContain(
+ 'const coordinatorRef = React.useRef(null);',
);
- coordinator.clearTransaction();
-
- expect(coordinator.getTransaction()).toBeNull();
+ expect(source).toContain('coordinatorRef.current ??= createStationarySelectionCoordinator();');
+ expect(source).not.toContain('useRef(createStationarySelectionCoordinator())');
});
- it('FAILED clears state', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.clearTransaction();
-
- expect(coordinator.getTransaction()).toBeNull();
+ it('records touch with generation validation', () => {
+ expect(source).toContain('ctx.recordNode(nodeId)');
});
- it('CANCELLED clears state', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.clearTransaction();
-
- expect(coordinator.getTransaction()).toBeNull();
- });
-
- it('END clears state', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.clearTransaction();
-
- expect(coordinator.getTransaction()).toBeNull();
- });
-
- it('mode change clears active transaction', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.clearTransaction();
-
- expect(coordinator.getTransaction()).toBeNull();
- });
-
- it('no transaction survives completion', () => {
- const coordinator = createStationarySelectionCoordinator();
- coordinator.beginTransaction();
- coordinator.recordNode('node-a');
- coordinator.commitSelection(
- true,
- null,
- (_id) => {
- void _id;
- },
- 1,
- );
- coordinator.clearTransaction();
-
- expect(coordinator.getTransaction()).toBeNull();
+ it('uses bubble phase onTouchStart', () => {
+ expect(source).toContain('onTouchStart: handleTouchStart');
});
});
diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts
index ce0134c7..58ffc1de 100644
--- a/src/runtime/stationarySelection.ts
+++ b/src/runtime/stationarySelection.ts
@@ -3,154 +3,50 @@ import React from 'react';
import { View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
-export type CommitSelectionResult =
- 'committed' | 'already-selected' | 'preview' | 'moved' | 'empty' | 'stale' | 'already-finalized';
+import type { StationarySelectionCoordinator } from './stationarySelectionCoordinator.js';
+import { createStationarySelectionCoordinator } from './stationarySelectionCoordinator.js';
-export interface TransactionState {
- readonly transactionId: number;
- readonly path: string[];
- moved: boolean;
- finalized: boolean;
-}
+export type {
+ CommitSelectionResult,
+ StationarySelectionCoordinator,
+ TransactionState,
+} from './stationarySelectionCoordinator.js';
-export interface StationarySelectionCoordinator {
- readonly trackerRef: React.MutableRefObject;
- beginTransaction(): number;
- recordNode(nodeId: string): void;
- commitSelection(
- isEditMode: boolean,
- selectedNodeId: string | null,
- selectNode: (id: string | null) => void,
- generation: number,
- ): CommitSelectionResult;
- clearTransaction(): void;
- getTransaction(): TransactionState | null;
+interface TrackerContextValue {
+ readonly recordNode: (nodeId: string) => void;
}
-const TrackerContext = React.createContext | null>(
- null,
-);
-
-let nextTransactionId = 1;
-
-export function createStationarySelectionCoordinator(): StationarySelectionCoordinator {
- const trackerRef = { current: null as TransactionState | null };
-
- function beginTransaction(): number {
- const tx = {
- transactionId: nextTransactionId++,
- path: [],
- moved: false,
- finalized: false,
- };
- trackerRef.current = tx;
- return tx.transactionId;
- }
-
- function recordNode(nodeId: string): void {
- const tx = trackerRef.current;
-
- if (!tx || tx.finalized) {
- return;
- }
-
- if (!tx.path.includes(nodeId)) {
- tx.path.push(nodeId);
- }
- }
-
- function commitSelection(
- isEditMode: boolean,
- selectedNodeId: string | null,
- selectNode: (id: string | null) => void,
- generation: number,
- ): CommitSelectionResult {
- if (!isEditMode) {
- return 'preview';
- }
-
- const tx = trackerRef.current;
-
- if (!tx) {
- return 'empty';
- }
-
- if (tx.transactionId !== generation) {
- return 'stale';
- }
-
- if (tx.finalized) {
- return 'already-finalized';
- }
-
- if (tx.moved) {
- return 'moved';
- }
-
- if (tx.path.length === 0) {
- return 'empty';
- }
-
- const [deepestNodeId] = tx.path;
-
- if (deepestNodeId != null && deepestNodeId === selectedNodeId) {
- return 'already-selected';
- }
-
- if (deepestNodeId != null && deepestNodeId !== selectedNodeId) {
- selectNode(deepestNodeId);
- }
-
- tx.finalized = true;
-
- return 'committed';
- }
-
- function clearTransaction(): void {
- trackerRef.current = null;
- }
-
- function getTransaction(): TransactionState | null {
- return trackerRef.current;
- }
-
- return {
- trackerRef,
- beginTransaction,
- recordNode,
- commitSelection,
- clearTransaction,
- getTransaction,
- };
-}
+const TrackerContext = React.createContext(null);
function StudioNodeTouchRecorder(props: {
readonly nodeId: string | undefined;
readonly showUnsupportedIndicator: boolean;
readonly children: React.ReactNode;
}): React.JSX.Element {
- const tracker = React.useContext(TrackerContext);
+ const ctx = React.useContext(TrackerContext);
const nodeIdRef = React.useRef(props.nodeId);
nodeIdRef.current = props.nodeId;
- function handleTouchStartCapture(): void {
- const tx = tracker?.current;
-
- if (!tx || tx.finalized || !nodeIdRef.current) {
+ function handleTouchStart(): void {
+ if (!ctx) {
return;
}
- if (!tx.path.includes(nodeIdRef.current)) {
- tx.path.push(nodeIdRef.current);
+ const nodeId = nodeIdRef.current;
+
+ if (!nodeId) {
+ return;
}
+
+ ctx.recordNode(nodeId);
}
return React.createElement(
View,
{
- style: { position: 'relative' },
- onTouchStart: handleTouchStartCapture,
+ style: { display: 'contents' },
+ onTouchStart: handleTouchStart,
},
props.showUnsupportedIndicator
? React.createElement(View, {
@@ -211,38 +107,51 @@ function StationaryTapSelector(props: {
readonly selectNode: (id: string | null) => void;
readonly children: React.ReactNode;
}): React.JSX.Element {
- const coordinatorRef = React.useRef(createStationarySelectionCoordinator());
+ const coordinatorRef = React.useRef(null);
+
+ coordinatorRef.current ??= createStationarySelectionCoordinator();
+
const coordinator = coordinatorRef.current;
const isEditModeRef = React.useRef(props.isEditMode);
const selectedNodeIdRef = React.useRef(props.selectedNodeId);
const selectNodeRef = React.useRef(props.selectNode);
+ const generationRef = React.useRef(0);
isEditModeRef.current = props.isEditMode;
selectedNodeIdRef.current = props.selectedNodeId;
selectNodeRef.current = props.selectNode;
- const generationRef = React.useRef(0);
+ const contextValue = React.useMemo(
+ () => ({
+ recordNode: (nodeId: string) => coordinator.recordNode(nodeId, generationRef.current),
+ }),
+ [coordinator],
+ );
const tapGesture = React.useMemo(() => {
return Gesture.Tap()
.maxDeltaX(5)
.maxDeltaY(5)
.maxDuration(500)
- .onStart(() => {
+ .runOnJS(true)
+ .onBegin(() => {
generationRef.current = coordinator.beginTransaction();
})
.onEnd((_event, success) => {
- if (success) {
- coordinator.commitSelection(
- isEditModeRef.current,
- selectedNodeIdRef.current,
- selectNodeRef.current,
- generationRef.current,
- );
+ if (!success) {
+ return;
}
- coordinator.clearTransaction();
+ coordinator.commitSelection(
+ isEditModeRef.current,
+ selectedNodeIdRef.current,
+ selectNodeRef.current,
+ generationRef.current,
+ );
+ })
+ .onFinalize(() => {
+ coordinator.clearTransaction(generationRef.current);
});
}, [coordinator]);
@@ -252,7 +161,7 @@ function StationaryTapSelector(props: {
return React.createElement(
TrackerContext.Provider,
- { value: coordinator.trackerRef },
+ { value: contextValue },
React.createElement(
GestureDetector,
{ gesture: tapGesture },
diff --git a/src/runtime/stationarySelectionCoordinator.test.ts b/src/runtime/stationarySelectionCoordinator.test.ts
new file mode 100644
index 00000000..e507b6b2
--- /dev/null
+++ b/src/runtime/stationarySelectionCoordinator.test.ts
@@ -0,0 +1,268 @@
+import { describe, expect, it } from 'bun:test';
+
+import { createStationarySelectionCoordinator } from './stationarySelectionCoordinator';
+
+describe('stationarySelectionCoordinator', () => {
+ it('each new coordinator starts at generation 1', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const generation = coordinator.beginTransaction();
+
+ expect(generation).toBe(1);
+ const tx = coordinator.getTransaction();
+ expect(tx).not.toBeNull();
+ expect(tx?.transactionId).toBe(generation);
+ });
+
+ it('second transaction uses generation 2', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const generation1 = coordinator.beginTransaction();
+ coordinator.clearTransaction();
+ const generation2 = coordinator.beginTransaction();
+
+ expect(generation2).toBe(generation1 + 1);
+ });
+
+ it('stale record generation rejected', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen1 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen1);
+ coordinator.clearTransaction();
+
+ const gen2 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen1);
+
+ const tx = coordinator.getTransaction();
+ expect(tx?.transactionId).toBe(gen2);
+ expect(tx?.path).toEqual([]);
+ });
+
+ it('stale commit generation rejected', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen1 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen1);
+ coordinator.clearTransaction();
+
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ void 0;
+ },
+ gen1,
+ );
+
+ expect(result).toBe('empty');
+ });
+
+ it('duplicate node rejected', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ coordinator.recordNode('node-a', gen);
+
+ const tx = coordinator.getTransaction();
+ expect(tx?.path).toEqual(['node-a']);
+ });
+
+ it('bubble path inner-to-outer', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('inner', gen);
+ coordinator.recordNode('outer', gen);
+
+ const tx = coordinator.getTransaction();
+ expect(tx?.path).toEqual(['inner', 'outer']);
+ });
+
+ it('first node is deepest', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('inner', gen);
+ coordinator.recordNode('outer', gen);
+
+ let selectedNodeId: string | null = null;
+ coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedNodeId = id;
+ },
+ gen,
+ );
+
+ expect(selectedNodeId as unknown as string).toBe('inner');
+ });
+
+ it('Edit commit once', () => {
+ let callCount = 0;
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ callCount += 1;
+ },
+ gen,
+ );
+
+ expect(callCount).toBe(1);
+ });
+
+ it('already-selected callback omitted', () => {
+ let called = false;
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ coordinator.commitSelection(
+ true,
+ 'node-a',
+ () => {
+ called = true;
+ },
+ gen,
+ );
+
+ expect(called).toBe(false);
+ });
+
+ it('Preview callback omitted', () => {
+ let selectedId: string | null = 'initial';
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ const result = coordinator.commitSelection(
+ false,
+ null,
+ (id) => {
+ selectedId = id;
+ },
+ gen,
+ );
+
+ expect(result).toBe('preview');
+ expect(selectedId).toBe('initial');
+ const tx = coordinator.getTransaction();
+ expect(tx?.finalized).not.toBe(true);
+ });
+
+ it('empty path', () => {
+ let selectedId: string | null = null;
+ let callCount = 0;
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedId = id;
+ callCount += 1;
+ },
+ gen,
+ );
+
+ expect(result).toBe('empty');
+ expect(selectedId).toBeNull();
+ expect(callCount).toBe(0);
+ });
+
+ it('moved transaction', () => {
+ let selectedId: string | null = 'initial';
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ coordinator.markMoved(gen);
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ (id) => {
+ selectedId = id;
+ },
+ gen,
+ );
+
+ expect(result).toBe('moved');
+ expect(selectedId).toBe('initial');
+ });
+
+ it('already-finalized transaction', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ void 0;
+ },
+ gen,
+ );
+
+ let callCount = 0;
+ const result = coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ callCount += 1;
+ },
+ gen,
+ );
+
+ expect(result).toBe('already-finalized');
+ expect(callCount).toBe(0);
+ });
+
+ it('cleanup only for matching generation', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen1 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen1);
+ coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ void 0;
+ },
+ gen1,
+ );
+
+ const gen2 = coordinator.beginTransaction();
+ coordinator.recordNode('node-b', gen2);
+ coordinator.clearTransaction(gen1);
+
+ const tx = coordinator.getTransaction();
+ expect(tx?.transactionId).toBe(gen2);
+ expect(tx?.path).toEqual(['node-b']);
+ });
+
+ it('stale finalization cannot clear a newer transaction', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen1 = coordinator.beginTransaction();
+ coordinator.clearTransaction(gen1);
+
+ const gen2 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen2);
+ coordinator.clearTransaction(gen1);
+
+ const tx = coordinator.getTransaction();
+ expect(tx?.transactionId).toBe(gen2);
+ expect(tx?.path).toEqual(['node-a']);
+ });
+
+ it('no transaction survives matching finalization', () => {
+ const coordinator = createStationarySelectionCoordinator();
+ const gen = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen);
+ coordinator.commitSelection(
+ true,
+ null,
+ () => {
+ void 0;
+ },
+ gen,
+ );
+ coordinator.clearTransaction(gen);
+
+ expect(coordinator.getTransaction()).toBeNull();
+ });
+});
diff --git a/src/runtime/stationarySelectionCoordinator.ts b/src/runtime/stationarySelectionCoordinator.ts
new file mode 100644
index 00000000..917840cb
--- /dev/null
+++ b/src/runtime/stationarySelectionCoordinator.ts
@@ -0,0 +1,141 @@
+export type CommitSelectionResult =
+ 'committed' | 'already-selected' | 'preview' | 'moved' | 'empty' | 'stale' | 'already-finalized';
+
+export interface TransactionState {
+ readonly transactionId: number;
+ readonly path: string[];
+ moved: boolean;
+ finalized: boolean;
+}
+
+export interface StationarySelectionCoordinator {
+ readonly trackerRef: { current: TransactionState | null };
+ beginTransaction(): number;
+ recordNode(nodeId: string, generation: number): void;
+ commitSelection(
+ isEditMode: boolean,
+ selectedNodeId: string | null,
+ selectNode: (id: string | null) => void,
+ generation: number,
+ ): CommitSelectionResult;
+ clearTransaction(): void;
+ clearTransaction(generation: number): void;
+ markMoved(generation: number): void;
+ getTransaction(): TransactionState | null;
+}
+
+export function createStationarySelectionCoordinator(): StationarySelectionCoordinator {
+ let nextTransactionId = 1;
+ const trackerRef = { current: null as TransactionState | null };
+
+ function beginTransaction(): number {
+ const tx = {
+ transactionId: nextTransactionId++,
+ path: [],
+ moved: false,
+ finalized: false,
+ };
+ trackerRef.current = tx;
+ return tx.transactionId;
+ }
+
+ function recordNode(nodeId: string, generation: number): void {
+ const tx = trackerRef.current;
+
+ if (!tx || tx.finalized || tx.transactionId !== generation) {
+ return;
+ }
+
+ if (!tx.path.includes(nodeId)) {
+ tx.path.push(nodeId);
+ }
+ }
+
+ function commitSelection(
+ isEditMode: boolean,
+ selectedNodeId: string | null,
+ selectNode: (id: string | null) => void,
+ generation: number,
+ ): CommitSelectionResult {
+ if (!isEditMode) {
+ return 'preview';
+ }
+
+ const tx = trackerRef.current;
+
+ if (!tx) {
+ return 'empty';
+ }
+
+ if (tx.transactionId !== generation) {
+ return 'stale';
+ }
+
+ if (tx.finalized) {
+ return 'already-finalized';
+ }
+
+ if (tx.moved) {
+ return 'moved';
+ }
+
+ if (tx.path.length === 0) {
+ return 'empty';
+ }
+
+ const [deepestNodeId] = tx.path;
+
+ if (deepestNodeId != null && deepestNodeId === selectedNodeId) {
+ return 'already-selected';
+ }
+
+ if (deepestNodeId != null && deepestNodeId !== selectedNodeId) {
+ selectNode(deepestNodeId);
+ }
+
+ tx.finalized = true;
+
+ return 'committed';
+ }
+
+ function clearTransaction(): void;
+ function clearTransaction(generation: number): void;
+ function clearTransaction(generation?: number): void {
+ if (generation === undefined) {
+ trackerRef.current = null;
+ return;
+ }
+
+ const tx = trackerRef.current;
+
+ if (tx?.transactionId !== generation) {
+ return;
+ }
+
+ trackerRef.current = null;
+ }
+
+ function markMoved(generation: number): void {
+ const tx = trackerRef.current;
+
+ if (tx?.transactionId !== generation) {
+ return;
+ }
+
+ tx.moved = true;
+ }
+
+ function getTransaction(): TransactionState | null {
+ return trackerRef.current;
+ }
+
+ return {
+ trackerRef,
+ beginTransaction,
+ recordNode,
+ commitSelection,
+ clearTransaction,
+ markMoved,
+ getTransaction,
+ };
+}
From b420d0d6ef2b017bcb7955cf7073e5a44d12e126 Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Wed, 29 Jul 2026 00:07:12 +0200
Subject: [PATCH 4/8] fix: harden Studio interaction integration
---
src/host/generatedAdminExpoWeb.smoke.test.ts | 4 +-
src/host/layout/templates/rootLayout.test.ts | 10 ++-
src/host/layout/templates/rootLayout.ts | 82 +++++++++++--------
src/runtime/interactionPolicy.test.ts | 15 +++-
src/runtime/interactionPolicy.ts | 3 +-
src/runtime/interactionPolicyCore.ts | 2 +-
src/runtime/stationarySelection.test.ts | 12 +++
src/runtime/stationarySelection.ts | 7 +-
.../stationarySelectionCoordinator.test.ts | 22 +++--
9 files changed, 106 insertions(+), 51 deletions(-)
diff --git a/src/host/generatedAdminExpoWeb.smoke.test.ts b/src/host/generatedAdminExpoWeb.smoke.test.ts
index 621b2641..78dcf0f1 100644
--- a/src/host/generatedAdminExpoWeb.smoke.test.ts
+++ b/src/host/generatedAdminExpoWeb.smoke.test.ts
@@ -425,11 +425,11 @@ async function linkSmokeNodeModules(workspaceRoot: string): Promise {
const nodeModulesRoot = path.join(workspaceRoot, 'node_modules');
await mkdir(nodeModulesRoot, { recursive: true });
await linkNodeModuleEntries({
- sourceRoot: path.join(process.cwd(), 'apps', 'studio', 'node_modules'),
+ sourceRoot: path.join(process.cwd(), 'node_modules', '.bun', 'node_modules'),
targetRoot: nodeModulesRoot,
});
await linkNodeModuleEntries({
- sourceRoot: path.join(process.cwd(), 'node_modules', '.bun', 'node_modules'),
+ sourceRoot: path.join(process.cwd(), 'apps', 'studio', 'node_modules'),
targetRoot: nodeModulesRoot,
});
}
diff --git a/src/host/layout/templates/rootLayout.test.ts b/src/host/layout/templates/rootLayout.test.ts
index 434867ec..2923d1c4 100644
--- a/src/host/layout/templates/rootLayout.test.ts
+++ b/src/host/layout/templates/rootLayout.test.ts
@@ -154,7 +154,7 @@ test('generates a root stationary tap selector for edit mode and excludes old Pr
expect(generated).toContain('wrapNode: studioWrapNode');
expect(generated).toContain('selectedNodeId');
expect(generated).toContain('selectNode');
- expect(generated).toContain('studioThirdPartySupport');
+ expect(generated).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT');
expect(generated).toContain('ZORA_COMPONENT_REGISTRY');
expect(generated).not.toContain(' {
expect(rootLayoutSource).not.toContain('disableActions: !previewMode');
expect(rootLayoutSource).not.toContain('wrapNode: wrapStudioRuntimeNode');
expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo(');
+ expect(studioShellSource).toContain('const studioWrapNode = useMemo(');
+ expect(studioShellSource).toContain('createStudioStationarySelectionWrapNode');
+ expect(studioShellSource).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT');
+ expect(studioShellSource).toContain('const studioResolveNodeProps = useMemo(');
+ expect(studioShellSource).toContain('createStudioInteractionPolicyResolver');
expect(studioShellSource).toContain('disableActions: !previewMode');
expect(studioShellSource).toContain('wrapNode: studioWrapNode');
expect(studioShellSource).toContain(
'',
);
expect(studioShellSource).toContain('const studioOutput = (');
+ expect(studioShellSource).toContain(
+ '[previewMode, studioWrapNode, studioResolveNodeProps]',
+ );
});
test('keeps generated apps Studio-independent when includeStudio is false', () => {
diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts
index 2dec0492..8d1e7781 100644
--- a/src/host/layout/templates/rootLayout.ts
+++ b/src/host/layout/templates/rootLayout.ts
@@ -436,6 +436,9 @@ function resolveRuntimeOperationCredential(credential: { readonly kind?: string
runtimeOperationHelpers.trim(),
authRuntimeConstants.trim(),
includeStudio ? appHeaderHelpers.trim() : '',
+ includeStudio
+ ? 'const STUDIO_EMPTY_THIRD_PARTY_SUPPORT = {} as const;'
+ : '',
innerNavigation.declarations.trim(),
]
.filter(Boolean)
@@ -615,42 +618,49 @@ function StudioShell({
if (nextAppLocation) setLastNonAdminLocation(nextAppLocation);
}, [appLocation, appPathname, setLastNonAdminLocation]);
- const studioThirdPartySupport = {} as const;
- const studioWrapNode = createStudioStationarySelectionWrapNode({
- previewMode,
- thirdPartySupport: studioThirdPartySupport,
- });
- const studioResolveNodeProps = createStudioInteractionPolicyResolver({
- previewMode,
- thirdPartySupport: studioThirdPartySupport,
- });
- const appHeaderTitle = resolveStudioAppHeaderTitle({
- runtimeManifest,
- studioManifest,
- previewMode,
- activeScreenId,
- pathname: appPathname,
- });
- const header = shouldMountAppHeader ? (
-
- ) : undefined;
- const studioRuntimeManifest = studioManifest ?? runtimeManifest;
- const activeStudioTheme =
- studioRuntimeManifest.themes.find(
- (theme) => theme.id === studioRuntimeManifest.activeThemeId,
- ) ?? activeTheme;
- const activeStudioThemeMode = resolveThemeMode(
- studioRuntimeManifest.activeThemeMode,
- activeThemeMode,
- );
- const studioRuntimeConfig = useMemo(
- () => ({
- disableActions: !previewMode,
- wrapNode: studioWrapNode,
- resolveNodeProps: studioResolveNodeProps,
- }),
- [previewMode],
- );
+ const studioWrapNode = useMemo(
+ () =>
+ createStudioStationarySelectionWrapNode({
+ previewMode,
+ thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT,
+ }),
+ [previewMode],
+ );
+ const studioResolveNodeProps = useMemo(
+ () =>
+ createStudioInteractionPolicyResolver({
+ previewMode,
+ thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT,
+ }),
+ [previewMode],
+ );
+ const appHeaderTitle = resolveStudioAppHeaderTitle({
+ runtimeManifest,
+ studioManifest,
+ previewMode,
+ activeScreenId,
+ pathname: appPathname,
+ });
+ const header = shouldMountAppHeader ? (
+
+ ) : undefined;
+ const studioRuntimeManifest = studioManifest ?? runtimeManifest;
+ const activeStudioTheme =
+ studioRuntimeManifest.themes.find(
+ (theme) => theme.id === studioRuntimeManifest.activeThemeId,
+ ) ?? activeTheme;
+ const activeStudioThemeMode = resolveThemeMode(
+ studioRuntimeManifest.activeThemeMode,
+ activeThemeMode,
+ );
+ const studioRuntimeConfig = useMemo(
+ () => ({
+ disableActions: !previewMode,
+ wrapNode: studioWrapNode,
+ resolveNodeProps: studioResolveNodeProps,
+ }),
+ [previewMode, studioWrapNode, studioResolveNodeProps],
+ );
const studioOutput = (
{
expect(previewResult.interactionPolicy).toBe('enabled');
});
+ it('isComponentSupported returns false for inherited prototype names', () => {
+ expect(isComponentSupported('constructor', {}, () => false)).toBe(false);
+ expect(isComponentSupported('toString', {}, () => false)).toBe(false);
+ expect(isComponentSupported('valueOf', {}, () => false)).toBe(false);
+ expect(isComponentSupported('__proto__', {}, () => false)).toBe(false);
+ });
+
+ it('isComponentSupported returns true for an explicit own third-party entry', () => {
+ expect(isComponentSupported('CustomWidget', { CustomWidget: true }, () => false)).toBe(true);
+ });
+
it('isComponentSupported returns true for zora-builtin', () => {
expect(isComponentSupported('Text', {}, () => true)).toBe(true);
});
@@ -162,7 +173,9 @@ describe('interactionPolicy production adapter', () => {
it('derives support from the actual ZORA_COMPONENT_REGISTRY', () => {
expect(source).toContain("from '@ankhorage/zora'");
expect(source).toContain('ZORA_COMPONENT_REGISTRY');
- expect(source).toContain('nodeType in ZORA_COMPONENT_REGISTRY');
+ expect(source).toContain(
+ 'Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, nodeType)',
+ );
});
it('delegates classification to the pure core', () => {
diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts
index 01c26e96..26669dda 100644
--- a/src/runtime/interactionPolicy.ts
+++ b/src/runtime/interactionPolicy.ts
@@ -20,7 +20,8 @@ export function createStudioInteractionPolicyResolver(
const resolver = createInteractionPolicyResolver({
previewMode,
thirdPartySupport,
- isSupportedNodeType: (nodeType) => nodeType in ZORA_COMPONENT_REGISTRY,
+ isSupportedNodeType: (nodeType) =>
+ Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, nodeType),
});
if (!existingResolver) {
diff --git a/src/runtime/interactionPolicyCore.ts b/src/runtime/interactionPolicyCore.ts
index 970ac8bc..8337c9c5 100644
--- a/src/runtime/interactionPolicyCore.ts
+++ b/src/runtime/interactionPolicyCore.ts
@@ -20,7 +20,7 @@ function classifyComponent(
return 'zora-builtin';
}
- if (nodeType in thirdPartySupport) {
+ if (Object.prototype.hasOwnProperty.call(thirdPartySupport, nodeType)) {
return 'third-party-supported';
}
diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts
index e1bb03df..9ea88b10 100644
--- a/src/runtime/stationarySelection.test.ts
+++ b/src/runtime/stationarySelection.test.ts
@@ -73,6 +73,18 @@ describe('stationarySelection RN integration', () => {
expect(source).toContain("pointerEvents: 'none'");
});
+ it('places pointerEvents as a View prop, not inside style', () => {
+ const indicatorMatch = /React\.createElement\(View,\s*\{[^}]*style:\s*\{[^}]*\}[^}]*\}\)/.exec(
+ source,
+ );
+ expect(indicatorMatch).not.toBeNull();
+ const indicatorBlock = indicatorMatch?.[0] ?? '';
+ expect(indicatorBlock).toContain("pointerEvents: 'none'");
+ const styleBlock = /style:\s*\{([^}]*)\}/.exec(indicatorBlock);
+ expect(styleBlock).toBeTruthy();
+ expect(styleBlock?.[1]).not.toContain('pointerEvents');
+ });
+
it('uses lazy coordinator creation', () => {
expect(source).toContain(
'const coordinatorRef = React.useRef(null);',
diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts
index 58ffc1de..5c67650a 100644
--- a/src/runtime/stationarySelection.ts
+++ b/src/runtime/stationarySelection.ts
@@ -59,8 +59,8 @@ function StudioNodeTouchRecorder(props: {
borderWidth: 1,
borderColor: '#ef4444',
borderStyle: 'dashed',
- pointerEvents: 'none',
},
+ pointerEvents: 'none',
})
: null,
props.children,
@@ -88,8 +88,9 @@ export function createStudioStationarySelectionWrapNode(options?: {
}
const isSupported =
- args.node.type in ZORA_COMPONENT_REGISTRY ||
- (thirdPartySupport != null && args.node.type in thirdPartySupport);
+ Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, args.node.type) ||
+ (thirdPartySupport != null &&
+ Object.prototype.hasOwnProperty.call(thirdPartySupport, args.node.type));
const showUnsupportedIndicator = isEditMode && !isSupported;
diff --git a/src/runtime/stationarySelectionCoordinator.test.ts b/src/runtime/stationarySelectionCoordinator.test.ts
index e507b6b2..25eb1429 100644
--- a/src/runtime/stationarySelectionCoordinator.test.ts
+++ b/src/runtime/stationarySelectionCoordinator.test.ts
@@ -42,16 +42,24 @@ describe('stationarySelectionCoordinator', () => {
coordinator.recordNode('node-a', gen1);
coordinator.clearTransaction();
+ const gen2 = coordinator.beginTransaction();
+ coordinator.recordNode('node-a', gen2);
+
+ let selectedNodeId: string | null = null;
const result = coordinator.commitSelection(
true,
null,
- () => {
- void 0;
+ (id) => {
+ selectedNodeId = id;
},
gen1,
);
- expect(result).toBe('empty');
+ expect(result).toBe('stale');
+ expect(selectedNodeId).toBeNull();
+ const tx = coordinator.getTransaction();
+ expect(tx?.transactionId).toBe(gen2);
+ expect(tx?.path).toEqual(['node-a']);
});
it('duplicate node rejected', () => {
@@ -80,17 +88,19 @@ describe('stationarySelectionCoordinator', () => {
coordinator.recordNode('inner', gen);
coordinator.recordNode('outer', gen);
- let selectedNodeId: string | null = null;
+ let selectedNodeId = '';
coordinator.commitSelection(
true,
null,
(id) => {
- selectedNodeId = id;
+ if (id !== null) {
+ selectedNodeId = id;
+ }
},
gen,
);
- expect(selectedNodeId as unknown as string).toBe('inner');
+ expect(selectedNodeId).toBe('inner');
});
it('Edit commit once', () => {
From 973df6f8632f31a746ba33e6cfb18aac72e58a3a Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Thu, 30 Jul 2026 18:55:24 +0200
Subject: [PATCH 5/8] docs: update
---
README.md | 2 +-
bun.lock | 8 +-
package.json | 2 +-
paradox/badges/npm.svg | 6 +-
paradox/diagrams/architecture-overview.mmd | 25 +-
paradox/diagrams/export-graph.mmd | 9 +-
paradox/diagrams/module-relationships.mmd | 16 +-
paradox/index.html | 612 +++++++++++--
paradox/paradox.json | 864 +++++++++++++++----
src/host/layout/templates/rootLayout.test.ts | 4 +-
src/host/layout/templates/rootLayout.ts | 4 +-
11 files changed, 1276 insertions(+), 276 deletions(-)
diff --git a/README.md b/README.md
index d54b8df0..e2c07522 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
# @ankhorage/studio
-        
+        
Standalone Studio authoring package for Ankhorage apps.
diff --git a/bun.lock b/bun.lock
index ff720dae..3fe9f8db 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,7 +17,7 @@
"@ankhorage/supabase-auth": "^1.1.1",
"@ankhorage/supabase-vault": "^0.2.0",
"@ankhorage/templates": "^3.0.0",
- "@ankhorage/zora": "^2.8.7",
+ "@ankhorage/zora": "^2.9.0",
"@ankhorage/zora-chess": "^0.1.2",
"@ankhorage/zora-tabletop": "^0.0.5",
"@fastify/cors": "^11.2.0",
@@ -119,11 +119,11 @@
"@ankhorage/supabase-vault": ["@ankhorage/supabase-vault@0.2.1", "", { "dependencies": { "@ankhorage/contracts": "^2.1.0" } }, "sha512-WkAsYQUtSnSTPx3hA93LowWs3mBjY7sbKTe8yEolKlzpUhr4Y5rhcUZvDhk30uBgqwfkRTCHxKNEDraomlkj0g=="],
- "@ankhorage/surface": ["@ankhorage/surface@2.0.3", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "expo-font": "^55.0.6", "react": ">=18.2.0", "react-native": ">=0.72.0", "react-native-safe-area-context": "^5.0.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-F5uG1Hun98N0c8fYgXAOlliUoIgX1LRm5B5IyiN0I2GEiyW12mC1UOu2MNn+9PAjZNdH1tsNbHHikw/OJhh5jA=="],
+ "@ankhorage/surface": ["@ankhorage/surface@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "expo-font": "^55.0.6", "react": ">=18.2.0", "react-native": ">=0.72.0", "react-native-safe-area-context": "^5.0.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-6DB9lLQV+CfiA1uytI3juD1h06atJDAkpdGfCzQHK1QEnmPaLrS3nhC2/UidA1oRAx4BihFsTYp6yYY1Cprc1A=="],
"@ankhorage/templates": ["@ankhorage/templates@3.0.0", "", { "dependencies": { "@ankhorage/contracts": "^4.0.0", "@ankhorage/zora": "^2.8.0" }, "bin": { "ankhorage-templates": "dist/cli/standalone.js" } }, "sha512-aflIReBWhuXpjLLToWHr13zDCuEOvjebfdeGBS5qYfbUg93gJV6xakDNN51tNgCAiroeG6jkytDqrqRPj3ZkJw=="],
- "@ankhorage/zora": ["@ankhorage/zora@2.8.7", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0", "@ankhorage/surface": "^2.0.3" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "@react-native-picker/picker": ">=2.0.0", "expo-font": "^55.0.6", "expo-linear-gradient": ">=14.0.0", "react": ">=18.2.0", "react-native": ">=0.72.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-pWEpJyyhsNhoeY6NdVmuwhcaS+JbQ76b7HRqL8Dht0opGVFFHB/rxGaYIm+7iJpdVYPU/dtY+ASeljX1WM+sZw=="],
+ "@ankhorage/zora": ["@ankhorage/zora@2.9.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0", "@ankhorage/surface": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "@react-native-picker/picker": ">=2.0.0", "expo-font": "^55.0.6", "expo-linear-gradient": ">=14.0.0", "react": ">=18.2.0", "react-native": ">=0.72.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-iiRaxYAXzkVgfYSy/xwvJf9N0U5f6IP/KhjUN5U3Cc2VEd95/oXKKfEjksnz1WLD3AkecQCTNvqP2jC8S65pUA=="],
"@ankhorage/zora-chess": ["@ankhorage/zora-chess@0.1.2", "", { "dependencies": { "chess.js": "^1.4.0" }, "peerDependencies": { "@ankhorage/zora": ">=0.1.0", "react": ">=18.2.0", "react-native": ">=0.72.0" } }, "sha512-uVZyqJzvnyAzCNrrys4UGR/ioVgYxgcKbY/5ET0d7tyOt5joaKqiQMNEbQL+5gby34mqhLjmBSS3xt8mxZgRQg=="],
@@ -2385,6 +2385,8 @@
"@ankhorage/templates/@ankhorage/zora/@ankhorage/contracts": ["@ankhorage/contracts@2.1.0", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7" } }, "sha512-QGG0atpGDe9+BUWzWvV83Kj309TBOK76yBRbWOoQli8g0LY8QLb7Z4UquD+qVhtWJTp3Y1jHUTEttQ0WjdhY/Q=="],
+ "@ankhorage/templates/@ankhorage/zora/@ankhorage/surface": ["@ankhorage/surface@2.0.3", "", { "dependencies": { "@ankhorage/color-theory": "^0.0.7", "@ankhorage/contracts": "^2.1.0" }, "peerDependencies": { "@expo/vector-icons": ">=14.0.0", "expo-font": "^55.0.6", "react": ">=18.2.0", "react-native": ">=0.72.0", "react-native-safe-area-context": "^5.0.0" }, "optionalPeers": ["@expo/vector-icons", "expo-font"] }, "sha512-F5uG1Hun98N0c8fYgXAOlliUoIgX1LRm5B5IyiN0I2GEiyW12mC1UOu2MNn+9PAjZNdH1tsNbHHikw/OJhh5jA=="],
+
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
diff --git a/package.json b/package.json
index 2e6e5e6d..b976f235 100644
--- a/package.json
+++ b/package.json
@@ -240,7 +240,7 @@
"@ankhorage/supabase-auth": "^1.1.1",
"@ankhorage/supabase-vault": "^0.2.0",
"@ankhorage/templates": "^3.0.0",
- "@ankhorage/zora": "^2.8.7",
+ "@ankhorage/zora": "^2.9.0",
"@ankhorage/zora-chess": "^0.1.2",
"@ankhorage/zora-tabletop": "^0.0.5",
"@fastify/cors": "^11.2.0",
diff --git a/paradox/badges/npm.svg b/paradox/badges/npm.svg
index bda86b36..1cde34c5 100644
--- a/paradox/badges/npm.svg
+++ b/paradox/badges/npm.svg
@@ -1,7 +1,7 @@
-
+
+ src/host/layout/templates/auth/adapter.test.ts
+ Referenced module
+
+ Dependencies:
+ src/host/layout/templates/auth/adapter.ts
+
+ Exports: None
+
src/host/layout/templates/redirect.ts
Exports: getIndexRedirectRouteTsx
-
- src/host/layout/templates/rootLayout.selectionDom.test.ts
- Referenced module
-
- Dependencies: src/host/layout/templates/rootLayout.ts
-
- Exports: None
-
src/runtime/appExtensionRegistry.ts
src/runtime/index.ts
Referenced module
Dependencies: None
- Exports: ComponentRegistry,
+ Exports: CommitSelectionResult,
+ ComponentRegistry, createInteractionPolicyResolver,
+ createStationarySelectionCoordinator,
createStudioActionSuppressionConfig,
+ createStudioInteractionPolicyResolver,
createStudioPreviewComponentRegistry,
- createStudioPreviewRuntimeConfig, executeRuntimeAction,
+ createStudioPreviewRuntimeConfig,
+ createStudioStationarySelectionWrapNode,
+ executeRuntimeAction, InteractionPolicy,
+ InteractionPolicyResolverArgs, isComponentSupported,
resolveStudioLocalizationPreview,
shouldSuppressStudioRuntimeActions,
+ StationarySelectionCoordinator, StationaryTapSelector,
STUDIO_APP_EXTENSION_COMPONENT_REGISTRY,
STUDIO_PREVIEW_COMPONENT_REGISTRY,
+ StudioInteractionPolicyResolverArgs,
StudioLocalizationPreview, StudioLocalizationPreviewInput,
StudioPreviewRegistryOptions,
- StudioPreviewRuntimeConfigOptions, useRuntimeAction,
- UseRuntimeActionOptions
+ StudioPreviewRuntimeConfigOptions,
+ ThirdPartyComponentSupport, TransactionState,
+ useRuntimeAction, UseRuntimeActionOptions
+
+
+
+ src/runtime/interactionPolicy.test.ts
+ Referenced module
+
+ Dependencies: src/runtime/interactionPolicyCore.ts
+
+ Exports: None
+
+
+ src/runtime/interactionPolicy.ts
+ Referenced module
+
+ Dependencies: src/runtime/interactionPolicyCore.ts
+
+
+ Exports: createStudioInteractionPolicyResolver,
+ InteractionPolicy, StudioInteractionPolicyResolverArgs,
+ ThirdPartyComponentSupport
+
+
+
+ src/runtime/interactionPolicyCore.ts
+ Referenced module
+ Dependencies: None
+
+ Exports: createInteractionPolicyResolver,
+ InteractionPolicy, InteractionPolicyResolverArgs,
+ isComponentSupported, ThirdPartyComponentSupport
src/runtime/previewRegistry.ts
src/runtime/previewRuntimeConfig.ts
Referenced module
Dependencies: src/runtime/actionSuppression.ts,
+ src/runtime/interactionPolicy.ts,
src/runtime/previewRegistry.ts
@@ -3854,6 +3944,54 @@
src/runtime/runtimeSurface.test.ts
Dependencies: None
Exports: None
+
+ src/runtime/stationarySelection.test.ts
+ Referenced module
+ Dependencies: None
+ Exports: None
+
+
+ src/runtime/stationarySelection.ts
+ Referenced module
+
+ Dependencies:
+ src/runtime/stationarySelectionCoordinator.ts
+
+
+ Exports: CommitSelectionResult,
+ createStudioStationarySelectionWrapNode,
+ StationarySelectionCoordinator, StationaryTapSelector,
+ TransactionState
+
+
+
+ src/runtime/stationarySelectionCoordinator.test.ts
+ Referenced module
+
+ Dependencies:
+ src/runtime/stationarySelectionCoordinator.ts
+
+ Exports: None
+
+
+ src/runtime/stationarySelectionCoordinator.ts
+ Referenced module
+ Dependencies: None
+
+ Exports: CommitSelectionResult,
+ createStationarySelectionCoordinator,
+ StationarySelectionCoordinator, TransactionState
+
+
Architecture overview
module_src_host_layout_layoutGenerator_ts --> module_src_host_modules_layout_ts
module_src_host_layout_layoutGenerator_ts --> module_src_index_ts
module_src_host_layout_layoutGenerator_ts --> module_src_studioAdminRouteModel_ts
+ module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"]
+ package__ankhorage_studio -.->
+ module_src_host_layout_templates_auth_adapter_test_ts
+ module_src_host_layout_templates_auth_adapter_test_ts -->
+ module_src_host_layout_templates_auth_adapter_ts
module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"]
package__ankhorage_studio -.-> module_src_host_layout_templates_auth_adapter_ts
module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"]
@@ -8637,11 +8780,6 @@ Architecture overview
package__ankhorage_studio -.-> module_src_host_layout_templates_redirect_ts
module_src_host_layout_templates_redirect_ts -->
module_src_host_layout_utils_escapeStringLiteral_ts
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"]
- package__ankhorage_studio -.->
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts -->
- module_src_host_layout_templates_rootLayout_ts
module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"]
package__ankhorage_studio -.->
module_src_host_layout_templates_rootLayout_test_ts
@@ -9025,6 +9163,16 @@ Architecture overview
package__ankhorage_studio -.-> module_src_runtime_appExtensionRegistry_ts
module_src_runtime_index_ts["src/runtime/index.ts"]
package__ankhorage_studio -.-> module_src_runtime_index_ts
+ module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts
+ module_src_runtime_interactionPolicy_test_ts -->
+ module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_interactionPolicy_ts -->
+ module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"]
package__ankhorage_studio -.-> module_src_runtime_localizationPreview_ts
module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"]
@@ -9035,13 +9183,29 @@ Architecture overview
package__ankhorage_studio -.-> module_src_runtime_previewRuntimeConfig_ts
module_src_runtime_previewRuntimeConfig_ts -->
module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts
- --> module_src_runtime_previewRegistry_ts
+ --> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_previewRuntimeConfig_ts -->
+ module_src_runtime_previewRegistry_ts
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
package__ankhorage_studio -.-> module_src_runtime_registry_tsx
module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"]
package__ankhorage_studio -.-> module_src_runtime_runtimeActions_ts
module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"]
package__ankhorage_studio -.-> module_src_runtime_runtimeSurface_test_ts
+ module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelection_test_ts
+ module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts
+ module_src_runtime_stationarySelection_ts -->
+ module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
+ package__ankhorage_studio -.->
+ module_src_runtime_stationarySelectionCoordinator_test_ts
+ module_src_runtime_stationarySelectionCoordinator_test_ts -->
+ module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
+ package__ankhorage_studio -.->
+ module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
package__ankhorage_studio -.-> module_src_runtime_useRuntimeAction_ts
module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts
@@ -9529,6 +9693,9 @@ Architecture overview
module_src_host_layout_layoutGenerator_ts --> module_src_host_modules_layout_ts
module_src_host_layout_layoutGenerator_ts --> module_src_index_ts
module_src_host_layout_layoutGenerator_ts --> module_src_studioAdminRouteModel_ts
+ module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_layout_templates_auth_adapter_test_ts
+ module_src_host_layout_templates_auth_adapter_test_ts --> module_src_host_layout_templates_auth_adapter_ts
module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"]
package__ankhorage_studio -.-> module_src_host_layout_templates_auth_adapter_ts
module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"]
@@ -9565,9 +9732,6 @@ Architecture overview
module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"]
package__ankhorage_studio -.-> module_src_host_layout_templates_redirect_ts
module_src_host_layout_templates_redirect_ts --> module_src_host_layout_utils_escapeStringLiteral_ts
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"]
- package__ankhorage_studio -.-> module_src_host_layout_templates_rootLayout_selectionDom_test_ts
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts --> module_src_host_layout_templates_rootLayout_ts
module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"]
package__ankhorage_studio -.-> module_src_host_layout_templates_rootLayout_test_ts
module_src_host_layout_templates_rootLayout_test_ts --> module_src_host_layout_templates_rootLayout_ts
@@ -9863,6 +10027,14 @@ Architecture overview
package__ankhorage_studio -.-> module_src_runtime_appExtensionRegistry_ts
module_src_runtime_index_ts["src/runtime/index.ts"]
package__ankhorage_studio -.-> module_src_runtime_index_ts
+ module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts
+ module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"]
package__ankhorage_studio -.-> module_src_runtime_localizationPreview_ts
module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"]
@@ -9871,6 +10043,7 @@ Architecture overview
module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"]
package__ankhorage_studio -.-> module_src_runtime_previewRuntimeConfig_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts
+ module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
package__ankhorage_studio -.-> module_src_runtime_registry_tsx
@@ -9878,6 +10051,16 @@ Architecture overview
package__ankhorage_studio -.-> module_src_runtime_runtimeActions_ts
module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"]
package__ankhorage_studio -.-> module_src_runtime_runtimeSurface_test_ts
+ module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelection_test_ts
+ module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_test_ts
+ module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
package__ankhorage_studio -.-> module_src_runtime_useRuntimeAction_ts
module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts
@@ -10132,6 +10315,7 @@ Module relationships
module_src_host_layout_generatedImportComposer_ts["src/host/layout/generatedImportComposer.ts"]
module_src_host_layout_layoutGenerator_test_ts["src/host/layout/layoutGenerator.test.ts"]
module_src_host_layout_layoutGenerator_ts["src/host/layout/layoutGenerator.ts"]
+ module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"]
module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"]
module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"]
module_src_host_layout_templates_auth_oauth_ts["src/host/layout/templates/auth/oauth.ts"]
@@ -10143,7 +10327,6 @@ Module relationships
module_src_host_layout_templates_nestedLayout_test_ts["src/host/layout/templates/nestedLayout.test.ts"]
module_src_host_layout_templates_nestedLayout_ts["src/host/layout/templates/nestedLayout.ts"]
module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"]
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"]
module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"]
module_src_host_layout_templates_rootLayout_ts["src/host/layout/templates/rootLayout.ts"]
module_src_host_layout_templates_screen_ts["src/host/layout/templates/screen.ts"]
@@ -10233,12 +10416,19 @@ Module relationships
module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"]
module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"]
module_src_runtime_index_ts["src/runtime/index.ts"]
+ module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
+ module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
+ module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"]
module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"]
module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"]
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"]
module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"]
+ module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"]
+ module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
+ module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
+ module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"]
module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"]
@@ -10459,6 +10649,8 @@ Module relationships
module_src_host_layout_layoutGenerator_ts --> module_src_host_modules_layout_ts
module_src_host_layout_layoutGenerator_ts --> module_src_index_ts
module_src_host_layout_layoutGenerator_ts --> module_src_studioAdminRouteModel_ts
+ module_src_host_layout_templates_auth_adapter_test_ts -->
+ module_src_host_layout_templates_auth_adapter_ts
module_src_host_layout_templates_auth_callback_ts -->
module_src_host_layout_templates_utils_routes_ts
module_src_host_layout_templates_auth_callback_ts -->
@@ -10487,8 +10679,6 @@ Module relationships
module_src_host_layout_templates_utils_strings_ts
module_src_host_layout_templates_redirect_ts -->
module_src_host_layout_utils_escapeStringLiteral_ts
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts -->
- module_src_host_layout_templates_rootLayout_ts
module_src_host_layout_templates_rootLayout_test_ts -->
module_src_host_layout_templates_rootLayout_ts
module_src_host_layout_templates_rootLayout_ts -->
@@ -10675,12 +10865,20 @@ Module relationships
module_src_projectSecretUsage_test_ts --> module_src_projectSecretUsage_ts
module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts
module_src_routeUtils_test_ts --> module_src_routeUtils_ts
+ module_src_runtime_interactionPolicy_test_ts -->
+ module_src_runtime_interactionPolicyCore_ts module_src_runtime_interactionPolicy_ts
+ --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRegistry_ts -->
module_src_runtime_appExtensionRegistry_ts
module_src_runtime_previewRuntimeConfig_ts -->
module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts
- --> module_src_runtime_previewRegistry_ts module_src_runtime_useRuntimeAction_ts
- --> module_src_runtime_runtimeActions_ts
+ --> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_previewRuntimeConfig_ts -->
+ module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts
+ --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionCoordinator_test_ts -->
+ module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts
module_src_runtimeDiagnosticsModel_test_ts -->
module_src_runtimeDiagnosticsModel_ts module_src_runtimeLocalization_public_test_ts
--> module_src_runtimeLocalization_ts module_src_runtimeLocalization_test_ts
@@ -10861,6 +11059,7 @@ Module relationships
module_src_host_layout_generatedImportComposer_ts["src/host/layout/generatedImportComposer.ts"]
module_src_host_layout_layoutGenerator_test_ts["src/host/layout/layoutGenerator.test.ts"]
module_src_host_layout_layoutGenerator_ts["src/host/layout/layoutGenerator.ts"]
+ module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"]
module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"]
module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"]
module_src_host_layout_templates_auth_oauth_ts["src/host/layout/templates/auth/oauth.ts"]
@@ -10872,7 +11071,6 @@ Module relationships
module_src_host_layout_templates_nestedLayout_test_ts["src/host/layout/templates/nestedLayout.test.ts"]
module_src_host_layout_templates_nestedLayout_ts["src/host/layout/templates/nestedLayout.ts"]
module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"]
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"]
module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"]
module_src_host_layout_templates_rootLayout_ts["src/host/layout/templates/rootLayout.ts"]
module_src_host_layout_templates_screen_ts["src/host/layout/templates/screen.ts"]
@@ -10962,12 +11160,19 @@ Module relationships
module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"]
module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"]
module_src_runtime_index_ts["src/runtime/index.ts"]
+ module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
+ module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
+ module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"]
module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"]
module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"]
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"]
module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"]
+ module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"]
+ module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
+ module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
+ module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"]
module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"]
@@ -11145,6 +11350,7 @@ Module relationships
module_src_host_layout_layoutGenerator_ts --> module_src_host_modules_layout_ts
module_src_host_layout_layoutGenerator_ts --> module_src_index_ts
module_src_host_layout_layoutGenerator_ts --> module_src_studioAdminRouteModel_ts
+ module_src_host_layout_templates_auth_adapter_test_ts --> module_src_host_layout_templates_auth_adapter_ts
module_src_host_layout_templates_auth_callback_ts --> module_src_host_layout_templates_utils_routes_ts
module_src_host_layout_templates_auth_callback_ts --> module_src_host_layout_utils_escapeStringLiteral_ts
module_src_host_layout_templates_auth_oauth_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts
@@ -11159,7 +11365,6 @@ Module relationships
module_src_host_layout_templates_nestedLayout_ts --> module_src_host_layout_templates_navigation_ts
module_src_host_layout_templates_nestedLayout_ts --> module_src_host_layout_templates_utils_strings_ts
module_src_host_layout_templates_redirect_ts --> module_src_host_layout_utils_escapeStringLiteral_ts
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts --> module_src_host_layout_templates_rootLayout_ts
module_src_host_layout_templates_rootLayout_test_ts --> module_src_host_layout_templates_rootLayout_ts
module_src_host_layout_templates_rootLayout_ts --> module_src_host_layout_generatedImportComposer_ts
module_src_host_layout_templates_rootLayout_ts --> module_src_host_layout_templates_navigation_ts
@@ -11278,9 +11483,14 @@ Module relationships
module_src_projectSecretUsage_test_ts --> module_src_projectSecretUsage_ts
module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts
module_src_routeUtils_test_ts --> module_src_routeUtils_ts
+ module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts
+ module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts
module_src_runtimeDiagnosticsModel_test_ts --> module_src_runtimeDiagnosticsModel_ts
module_src_runtimeLocalization_public_test_ts --> module_src_runtimeLocalization_ts
@@ -11432,6 +11642,7 @@ Export graph
module_src_host_layout_generatedImportComposer_ts["src/host/layout/generatedImportComposer.ts"]
module_src_host_layout_layoutGenerator_test_ts["src/host/layout/layoutGenerator.test.ts"]
module_src_host_layout_layoutGenerator_ts["src/host/layout/layoutGenerator.ts"]
+ module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"]
module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"]
module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"]
module_src_host_layout_templates_auth_oauth_ts["src/host/layout/templates/auth/oauth.ts"]
@@ -11443,7 +11654,6 @@ Export graph
module_src_host_layout_templates_nestedLayout_test_ts["src/host/layout/templates/nestedLayout.test.ts"]
module_src_host_layout_templates_nestedLayout_ts["src/host/layout/templates/nestedLayout.ts"]
module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"]
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"]
module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"]
module_src_host_layout_templates_rootLayout_ts["src/host/layout/templates/rootLayout.ts"]
module_src_host_layout_templates_screen_ts["src/host/layout/templates/screen.ts"]
@@ -11533,12 +11743,19 @@ Export graph
module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"]
module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"]
module_src_runtime_index_ts["src/runtime/index.ts"]
+ module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
+ module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
+ module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"]
module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"]
module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"]
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"]
module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"]
+ module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"]
+ module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
+ module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
+ module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"]
module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"]
@@ -11907,6 +12124,7 @@ Export graph
module_src_host_layout_generatedImportComposer_ts["src/host/layout/generatedImportComposer.ts"]
module_src_host_layout_layoutGenerator_test_ts["src/host/layout/layoutGenerator.test.ts"]
module_src_host_layout_layoutGenerator_ts["src/host/layout/layoutGenerator.ts"]
+ module_src_host_layout_templates_auth_adapter_test_ts["src/host/layout/templates/auth/adapter.test.ts"]
module_src_host_layout_templates_auth_adapter_ts["src/host/layout/templates/auth/adapter.ts"]
module_src_host_layout_templates_auth_callback_ts["src/host/layout/templates/auth/callback.ts"]
module_src_host_layout_templates_auth_oauth_ts["src/host/layout/templates/auth/oauth.ts"]
@@ -11918,7 +12136,6 @@ Export graph
module_src_host_layout_templates_nestedLayout_test_ts["src/host/layout/templates/nestedLayout.test.ts"]
module_src_host_layout_templates_nestedLayout_ts["src/host/layout/templates/nestedLayout.ts"]
module_src_host_layout_templates_redirect_ts["src/host/layout/templates/redirect.ts"]
- module_src_host_layout_templates_rootLayout_selectionDom_test_ts["src/host/layout/templates/rootLayout.selectionDom.test.ts"]
module_src_host_layout_templates_rootLayout_test_ts["src/host/layout/templates/rootLayout.test.ts"]
module_src_host_layout_templates_rootLayout_ts["src/host/layout/templates/rootLayout.ts"]
module_src_host_layout_templates_screen_ts["src/host/layout/templates/screen.ts"]
@@ -12008,12 +12225,19 @@ Export graph
module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"]
module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"]
module_src_runtime_index_ts["src/runtime/index.ts"]
+ module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
+ module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
+ module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
module_src_runtime_localizationPreview_ts["src/runtime/localizationPreview.ts"]
module_src_runtime_previewRegistry_ts["src/runtime/previewRegistry.ts"]
module_src_runtime_previewRuntimeConfig_ts["src/runtime/previewRuntimeConfig.ts"]
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
module_src_runtime_runtimeActions_ts["src/runtime/runtimeActions.ts"]
module_src_runtime_runtimeSurface_test_ts["src/runtime/runtimeSurface.test.ts"]
+ module_src_runtime_stationarySelection_test_ts["src/runtime/stationarySelection.test.ts"]
+ module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
+ module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
+ module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"]
module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"]
@@ -14111,7 +14335,7 @@ src/host/generatedAdminExpoWeb.smoke.test.ts
data-search="createAdminSmokeManifest src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createAdminSmokeManifest
- src/host/generatedAdminExpoWeb.smoke.test.ts:41:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:46:1
No description available.
createAdminSmokeManifest
data-search="createScrollableRuntimeScreenRoot src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createScrollableRuntimeScreenRoot
- src/host/generatedAdminExpoWeb.smoke.test.ts:97:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:102:1
No description available.
createScrollableRuntimeScreenRoot
data-search="createGeneratedAdminProject src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createGeneratedAdminProject
- src/host/generatedAdminExpoWeb.smoke.test.ts:183:1
- No description available.
-
-
- copyGeneratedDirectDependencies
- src/host/generatedAdminExpoWeb.smoke.test.ts:222:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:195:1
No description available.
copyGeneratedDirectDependencies
data-search="writeSmokeMetroConfig src/host/generatedAdminExpoWeb.smoke.test.ts "
>
writeSmokeMetroConfig
- src/host/generatedAdminExpoWeb.smoke.test.ts:251:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:233:1
No description available.
writeSmokeMetroConfig
data-search="reservePort src/host/generatedAdminExpoWeb.smoke.test.ts "
>
reservePort
- src/host/generatedAdminExpoWeb.smoke.test.ts:269:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:256:1
No description available.
reservePort
data-search="spawnExpoWeb src/host/generatedAdminExpoWeb.smoke.test.ts "
>
spawnExpoWeb
- src/host/generatedAdminExpoWeb.smoke.test.ts:285:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:272:1
No description available.
- waitForExpoWebUrl
- src/host/generatedAdminExpoWeb.smoke.test.ts:301:1
+ startSmokeStudioApi
+ src/host/generatedAdminExpoWeb.smoke.test.ts:294:1
No description available.
- linkSmokeNodeModules
- src/host/generatedAdminExpoWeb.smoke.test.ts:314:1
+ handleSmokeStudioApiRequest
+ src/host/generatedAdminExpoWeb.smoke.test.ts:319:1
No description available.
- linkNodeModuleEntries
- src/host/generatedAdminExpoWeb.smoke.test.ts:332:1
+ listenOnLocalhost
+ src/host/generatedAdminExpoWeb.smoke.test.ts:359:1
No description available.
- linkScopedNodeModuleEntries
- src/host/generatedAdminExpoWeb.smoke.test.ts:369:1
+ closeHttpServer
+ src/host/generatedAdminExpoWeb.smoke.test.ts:373:1
No description available.
- symlinkIfMissing
- src/host/generatedAdminExpoWeb.smoke.test.ts:384:1
+ readJsonRequest
+ src/host/generatedAdminExpoWeb.smoke.test.ts:382:1
+ No description available.
+
+
+ writeJsonResponse
+ src/host/generatedAdminExpoWeb.smoke.test.ts:398:1
+ No description available.
+
+
+ writeCorsHeaders
+ src/host/generatedAdminExpoWeb.smoke.test.ts:405:1
No description available.
- copyNodeModuleEntry
- src/host/generatedAdminExpoWeb.smoke.test.ts:393:1
+ waitForExpoWebUrl
+ src/host/generatedAdminExpoWeb.smoke.test.ts:411:1
No description available.
- copyNodeModulePackage
- src/host/generatedAdminExpoWeb.smoke.test.ts:404:1
+ linkSmokeNodeModules
+ src/host/generatedAdminExpoWeb.smoke.test.ts:424:1
No description available.
- resolveSmokeNodeModuleSource
- src/host/generatedAdminExpoWeb.smoke.test.ts:412:1
+ linkNodeModuleEntries
+ src/host/generatedAdminExpoWeb.smoke.test.ts:437:1
No description available.
- getNodeModuleTargetPath
- src/host/generatedAdminExpoWeb.smoke.test.ts:429:1
+ linkScopedNodeModuleEntries
+ src/host/generatedAdminExpoWeb.smoke.test.ts:474:1
+ No description available.
+
+
+ symlinkIfMissing
+ src/host/generatedAdminExpoWeb.smoke.test.ts:489:1
No description available.
getNodeModuleTargetPath
data-search="isNodeErrorWithCode src/host/generatedAdminExpoWeb.smoke.test.ts "
>
isNodeErrorWithCode
- src/host/generatedAdminExpoWeb.smoke.test.ts:441:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:498:1
No description available.
isNodeErrorWithCode
data-search="waitForHttp src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForHttp
- src/host/generatedAdminExpoWeb.smoke.test.ts:445:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:502:1
No description available.
waitForHttp
data-search="waitForBodyText src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForBodyText
- src/host/generatedAdminExpoWeb.smoke.test.ts:462:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:519:1
No description available.
waitForBodyText
data-search="resolveChromePath src/host/generatedAdminExpoWeb.smoke.test.ts "
>
resolveChromePath
- src/host/generatedAdminExpoWeb.smoke.test.ts:479:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:536:1
No description available.
resolveChromePath
data-search="readEnvString src/host/generatedAdminExpoWeb.smoke.test.ts "
>
readEnvString
- src/host/generatedAdminExpoWeb.smoke.test.ts:485:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:542:1
No description available.
readEnvString
data-search="spawnChrome src/host/generatedAdminExpoWeb.smoke.test.ts "
>
spawnChrome
- src/host/generatedAdminExpoWeb.smoke.test.ts:491:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:548:1
No description available.
spawnChrome
data-search="openChromePage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
openChromePage
- src/host/generatedAdminExpoWeb.smoke.test.ts:507:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:564:1
No description available.
openChromePage
data-search="parseChromeProtocolMessage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
parseChromeProtocolMessage
- src/host/generatedAdminExpoWeb.smoke.test.ts:612:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:669:1
No description available.
parseChromeProtocolMessage
data-search="isRecord src/host/generatedAdminExpoWeb.smoke.test.ts "
>
isRecord
- src/host/generatedAdminExpoWeb.smoke.test.ts:627:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:684:1
No description available.
isRecord
data-search="stopProcess src/host/generatedAdminExpoWeb.smoke.test.ts "
>
stopProcess
- src/host/generatedAdminExpoWeb.smoke.test.ts:631:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:688:1
No description available.
stopProcess
data-search="collectProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts "
>
collectProcessOutput
- src/host/generatedAdminExpoWeb.smoke.test.ts:640:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:697:1
No description available.
collectProcessOutput
data-search="formatProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts "
>
formatProcessOutput
- src/host/generatedAdminExpoWeb.smoke.test.ts:651:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:708:1
No description available.
@@ -15488,7 +15728,7 @@ getRootLayoutImportRequirements
data-search="serializeStringArrayLiteral src/host/layout/templates/rootLayout.ts "
>
serializeStringArrayLiteral
- src/host/layout/templates/rootLayout.ts:60:1
+ src/host/layout/templates/rootLayout.ts:67:1
No description available.
serializeStringArrayLiteral
data-search="indentGeneratedBlock src/host/layout/templates/rootLayout.ts "
>
indentGeneratedBlock
- src/host/layout/templates/rootLayout.ts:64:1
+ src/host/layout/templates/rootLayout.ts:71:1
No description available.
indentGeneratedBlock
data-search="getRootLayoutTsx src/host/layout/templates/rootLayout.ts "
>
getRootLayoutTsx
- src/host/layout/templates/rootLayout.ts:71:1
+ src/host/layout/templates/rootLayout.ts:78:1
No description available.
@@ -18808,6 +19048,66 @@ createStudioActionSuppressionConfig
+
+
+ src/runtime/interactionPolicy.ts
+
+ createStudioInteractionPolicyResolver
+ src/runtime/interactionPolicy.ts:15:1
+ No description available.
+
+
+
+
+
+ src/runtime/interactionPolicyCore.ts
+
+ classifyComponent
+ src/runtime/interactionPolicyCore.ts:14:1
+ No description available.
+
+
+ isComponentSupported
+ src/runtime/interactionPolicyCore.ts:30:1
+ No description available.
+
+
+ resolveInteractionPolicy
+ src/runtime/interactionPolicyCore.ts:38:1
+ No description available.
+
+
+ createInteractionPolicyResolver
+ src/runtime/interactionPolicyCore.ts:55:1
+ No description available.
+
+
+
src/runtime/previewRuntimeConfig.ts
data-search="createStudioPreviewRuntimeConfig src/runtime/previewRuntimeConfig.ts "
>
createStudioPreviewRuntimeConfig
- src/runtime/previewRuntimeConfig.ts:11:14
+ src/runtime/previewRuntimeConfig.ts:12:14
No description available.
@@ -18877,6 +19177,130 @@ executeRuntimeAction
+
+
+ src/runtime/stationarySelection.ts
+
+ StudioNodeTouchRecorder
+ src/runtime/stationarySelection.ts:21:1
+ No description available.
+
+
+ handleTouchStart
+ src/runtime/stationarySelection.ts:31:3
+ No description available.
+
+
+ createStudioStationarySelectionWrapNode
+ src/runtime/stationarySelection.ts:70:1
+ No description available.
+
+
+ StationaryTapSelector
+ src/runtime/stationarySelection.ts:105:1
+ No description available.
+
+
+
+
+
+ src/runtime/stationarySelectionCoordinator.ts
+
+ createStationarySelectionCoordinator
+ src/runtime/stationarySelectionCoordinator.ts:27:1
+ No description available.
+
+
+ beginTransaction
+ src/runtime/stationarySelectionCoordinator.ts:31:3
+ No description available.
+
+
+ recordNode
+ src/runtime/stationarySelectionCoordinator.ts:42:3
+ No description available.
+
+
+ commitSelection
+ src/runtime/stationarySelectionCoordinator.ts:54:3
+ No description available.
+
+
+ clearTransaction
+ src/runtime/stationarySelectionCoordinator.ts:101:3
+ No description available.
+
+
+ clearTransaction
+ src/runtime/stationarySelectionCoordinator.ts:102:3
+ No description available.
+
+
+ clearTransaction
+ src/runtime/stationarySelectionCoordinator.ts:103:3
+ No description available.
+
+
+ markMoved
+ src/runtime/stationarySelectionCoordinator.ts:118:3
+ No description available.
+
+
+ getTransaction
+ src/runtime/stationarySelectionCoordinator.ts:128:3
+ No description available.
+
+
+
{
'',
);
expect(studioShellSource).toContain('const studioOutput = (');
- expect(studioShellSource).toContain(
- '[previewMode, studioWrapNode, studioResolveNodeProps]',
- );
+ expect(studioShellSource).toContain('[previewMode, studioWrapNode, studioResolveNodeProps]');
});
test('keeps generated apps Studio-independent when includeStudio is false', () => {
diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts
index 8d1e7781..93d11df4 100644
--- a/src/host/layout/templates/rootLayout.ts
+++ b/src/host/layout/templates/rootLayout.ts
@@ -436,9 +436,7 @@ function resolveRuntimeOperationCredential(credential: { readonly kind?: string
runtimeOperationHelpers.trim(),
authRuntimeConstants.trim(),
includeStudio ? appHeaderHelpers.trim() : '',
- includeStudio
- ? 'const STUDIO_EMPTY_THIRD_PARTY_SUPPORT = {} as const;'
- : '',
+ includeStudio ? 'const STUDIO_EMPTY_THIRD_PARTY_SUPPORT = {} as const;' : '',
innerNavigation.declarations.trim(),
]
.filter(Boolean)
From caed6abd844b72825f3a13a0084634086c5e00ac Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Thu, 30 Jul 2026 20:30:41 +0200
Subject: [PATCH 6/8] fix: complete interaction policy selection integration
---
...-issue-167-interaction-policy-selection.md | 4 +-
paradox/diagrams/architecture-overview.mmd | 4 +
paradox/diagrams/export-graph.mmd | 1 +
paradox/diagrams/module-relationships.mmd | 3 +
paradox/index.html | 381 +++++---
paradox/paradox.json | 850 ++++++++++++++----
src/host/generatedAdminExpoWeb.smoke.test.ts | 718 +++++++++++++--
src/host/layout/layoutGenerator.test.ts | 10 +-
src/host/layout/layoutGenerator.ts | 12 +-
src/host/layout/templates/rootLayout.test.ts | 4 +-
src/host/layout/templates/rootLayout.ts | 5 +-
.../orchestrator/generatedAppFiles.test.ts | 59 ++
src/host/orchestrator/generatedAppFiles.ts | 20 +-
src/host/orchestrator/templates.test.ts | 9 +-
src/host/orchestrator/templates.ts | 2 +-
src/host/zoraExtensions/index.ts | 1 +
src/runtime/appExtensionRegistry.ts | 7 +
src/runtime/interactionPolicy.test.ts | 60 +-
src/runtime/interactionPolicy.ts | 26 +-
src/runtime/interactionPolicyCore.ts | 40 +-
src/runtime/previewRuntimeConfig.ts | 5 +
src/runtime/stationarySelection.test.ts | 48 +-
src/runtime/stationarySelection.ts | 335 ++++++-
23 files changed, 2143 insertions(+), 461 deletions(-)
create mode 100644 src/host/orchestrator/generatedAppFiles.test.ts
diff --git a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
index 9d198a80..f222b176 100644
--- a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
+++ b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
@@ -2,4 +2,6 @@
'@ankhorage/studio': minor
---
-Add public interaction-policy and stationary-selection runtime helpers.
+Add public interaction-policy and stationary-selection runtime helpers, require ZORA 2.9 in
+generated apps, support explicit extension policy declarations, and provide desktop pointer/touch
+selection with root-owned unsupported-component overlays.
diff --git a/paradox/diagrams/architecture-overview.mmd b/paradox/diagrams/architecture-overview.mmd
index b39dcb2d..c4bd310b 100644
--- a/paradox/diagrams/architecture-overview.mmd
+++ b/paradox/diagrams/architecture-overview.mmd
@@ -345,6 +345,9 @@ graph TD
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_test_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts
@@ -610,6 +613,7 @@ graph TD
package__ankhorage_studio -.-> module_src_runtime_previewRuntimeConfig_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
package__ankhorage_studio -.-> module_src_runtime_registry_tsx
diff --git a/paradox/diagrams/export-graph.mmd b/paradox/diagrams/export-graph.mmd
index 2d8ecb41..f500f845 100644
--- a/paradox/diagrams/export-graph.mmd
+++ b/paradox/diagrams/export-graph.mmd
@@ -91,6 +91,7 @@ graph LR
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"]
diff --git a/paradox/diagrams/module-relationships.mmd b/paradox/diagrams/module-relationships.mmd
index 9d3c5184..8d8c67be 100644
--- a/paradox/diagrams/module-relationships.mmd
+++ b/paradox/diagrams/module-relationships.mmd
@@ -91,6 +91,7 @@ graph LR
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"]
@@ -387,6 +388,7 @@ graph LR
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts
module_src_host_orchestrator_infraGenerator_ts --> module_src_host_orchestrator_infraValidation_ts
module_src_host_orchestrator_infraRuntime_test_ts --> module_src_host_orchestrator_infraRuntime_ts
@@ -493,6 +495,7 @@ graph LR
module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
diff --git a/paradox/index.html b/paradox/index.html
index e12cb0af..09534478 100644
--- a/paradox/index.html
+++ b/paradox/index.html
@@ -438,7 +438,7 @@ @ankhorage/studio
data-target="src/host/generatedAdminExpoWeb.smoke.test.ts"
>
src/host/generatedAdminExpoWeb.smoke.test.ts
- 30 functions
+ 33 functions
@@ -1254,7 +1254,7 @@ @ankhorage/studio
data-target="src/runtime/interactionPolicyCore.ts"
>
src/runtime/interactionPolicyCore.ts
- 4 functions
+ 7 functions
@@ -1296,7 +1296,7 @@ @ankhorage/studio
data-target="src/runtime/stationarySelection.ts"
>
src/runtime/stationarySelection.ts
- 4 functions
+ 13 functions
@@ -1575,7 +1575,7 @@ Package overview
96public exports
0components
-
229modules
+
230modules
1entrypoints
Entrypoints
@@ -2779,14 +2779,26 @@ src/host/oauthFixtureConsumer.smoke.test.ts
+ src/host/orchestrator/generatedAppFiles.test.ts
+ Referenced module
+
+ Dependencies:
+ src/host/orchestrator/generatedAppFiles.ts
+
+ Exports: None
+
+
src/host/orchestrator/generatedAppFiles.ts
Referenced module
Dependencies: src/host/zoraExtensions/index.ts
- Exports: GeneratedAppFilesOptions,
- syncGeneratedAppFiles
+ Exports: createGeneratedAppExtensionRegistrySource,
+ GeneratedAppFilesOptions, syncGeneratedAppFiles
src/runtime/actionSuppression.ts
src/runtime/appExtensionRegistry.ts
Referenced module
Dependencies: None
- Exports: STUDIO_APP_EXTENSION_COMPONENT_REGISTRY
+
+ Exports: STUDIO_APP_EXTENSION_COMPONENT_REGISTRY,
+ STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT
+
src/runtime/index.ts
Referenced module
Dependencies: None
Exports: CommitSelectionResult,
- ComponentRegistry, createInteractionPolicyResolver,
+ ComponentRegistry, composeInteractionPolicyResolver,
+ createInteractionPolicyResolver,
createStationarySelectionCoordinator,
createStudioActionSuppressionConfig,
createStudioInteractionPolicyResolver,
@@ -3826,11 +3842,14 @@
src/runtime/index.ts
createStudioPreviewRuntimeConfig,
createStudioStationarySelectionWrapNode,
executeRuntimeAction, InteractionPolicy,
+ InteractionPolicyNodePropsResolver,
+ InteractionPolicyResolveArgs,
InteractionPolicyResolverArgs, isComponentSupported,
resolveStudioLocalizationPreview,
shouldSuppressStudioRuntimeActions,
StationarySelectionCoordinator, StationaryTapSelector,
STUDIO_APP_EXTENSION_COMPONENT_REGISTRY,
+ STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
STUDIO_PREVIEW_COMPONENT_REGISTRY,
StudioInteractionPolicyResolverArgs,
StudioLocalizationPreview, StudioLocalizationPreviewInput,
@@ -3868,15 +3887,18 @@ src/runtime/interactionPolicy.ts
src/runtime/interactionPolicyCore.ts
Referenced module
Dependencies: None
- Exports: createInteractionPolicyResolver,
- InteractionPolicy, InteractionPolicyResolverArgs,
- isComponentSupported, ThirdPartyComponentSupport
+ Exports: composeInteractionPolicyResolver,
+ createInteractionPolicyResolver, InteractionPolicy,
+ InteractionPolicyNodePropsResolver,
+ InteractionPolicyResolveArgs,
+ InteractionPolicyResolverArgs, isComponentSupported,
+ ThirdPartyComponentSupport
src/runtime/previewRegistry.ts
src/runtime/previewRuntimeConfig.ts
Referenced module
Dependencies: src/runtime/actionSuppression.ts,
src/runtime/interactionPolicy.ts,
+ src/runtime/interactionPolicyCore.ts,
src/runtime/previewRegistry.ts
@@ -8847,6 +8870,11 @@
Architecture overview
module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts -->
module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
+ package__ankhorage_studio -.->
+ module_src_host_orchestrator_generatedAppFiles_test_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts -->
+ module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts -->
@@ -9185,6 +9213,8 @@ Architecture overview
module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts
--> module_src_runtime_interactionPolicy_ts
module_src_runtime_previewRuntimeConfig_ts -->
+ module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_previewRuntimeConfig_ts -->
module_src_runtime_previewRegistry_ts
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
package__ankhorage_studio -.-> module_src_runtime_registry_tsx
@@ -9779,6 +9809,9 @@ Architecture overview
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_test_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts
@@ -10044,6 +10077,7 @@ Architecture overview
package__ankhorage_studio -.-> module_src_runtime_previewRuntimeConfig_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
module_src_runtime_registry_tsx["src/runtime/registry.tsx"]
package__ankhorage_studio -.-> module_src_runtime_registry_tsx
@@ -10342,6 +10376,7 @@ Module relationships
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"]
@@ -10709,6 +10744,8 @@ Module relationships
module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts -->
module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts -->
+ module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts -->
module_src_host_zoraExtensions_index_ts
module_src_host_orchestrator_infraGenerator_ts -->
@@ -10874,6 +10911,8 @@ Module relationships
module_src_runtime_actionSuppression_ts module_src_runtime_previewRuntimeConfig_ts
--> module_src_runtime_interactionPolicy_ts
module_src_runtime_previewRuntimeConfig_ts -->
+ module_src_runtime_interactionPolicyCore_ts
+ module_src_runtime_previewRuntimeConfig_ts -->
module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts
--> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelectionCoordinator_test_ts -->
@@ -11086,6 +11125,7 @@ Module relationships
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"]
@@ -11382,6 +11422,7 @@ Module relationships
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts
module_src_host_orchestrator_infraGenerator_ts --> module_src_host_orchestrator_infraValidation_ts
module_src_host_orchestrator_infraRuntime_test_ts --> module_src_host_orchestrator_infraRuntime_ts
@@ -11488,6 +11529,7 @@ Module relationships
module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_actionSuppression_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
+ module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
@@ -11669,6 +11711,7 @@ Export graph
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"]
@@ -12151,6 +12194,7 @@ Export graph
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
module_src_host_orchestrator_infraGenerator_ts["src/host/orchestrator/infraGenerator.ts"]
@@ -14335,7 +14379,7 @@ src/host/generatedAdminExpoWeb.smoke.test.ts
data-search="createAdminSmokeManifest src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createAdminSmokeManifest
- src/host/generatedAdminExpoWeb.smoke.test.ts:46:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:37:1
No description available.
createAdminSmokeManifest
data-search="createScrollableRuntimeScreenRoot src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createScrollableRuntimeScreenRoot
- src/host/generatedAdminExpoWeb.smoke.test.ts:102:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:93:1
No description available.
- createGeneratedAdminProject
- src/host/generatedAdminExpoWeb.smoke.test.ts:195:1
+ verifyDesktopSelectionAndUnsupportedGeometry
+ src/host/generatedAdminExpoWeb.smoke.test.ts:265:1
No description available.
- writeSmokeMetroConfig
- src/host/generatedAdminExpoWeb.smoke.test.ts:233:1
+ waitForUnsupportedGeometry
+ src/host/generatedAdminExpoWeb.smoke.test.ts:401:1
No description available.
- reservePort
- src/host/generatedAdminExpoWeb.smoke.test.ts:256:1
+ waitForStudioSmokeState
+ src/host/generatedAdminExpoWeb.smoke.test.ts:420:1
No description available.
- spawnExpoWeb
- src/host/generatedAdminExpoWeb.smoke.test.ts:272:1
+ expectRectToMatch
+ src/host/generatedAdminExpoWeb.smoke.test.ts:439:1
No description available.
- startSmokeStudioApi
- src/host/generatedAdminExpoWeb.smoke.test.ts:294:1
+ rectsOverlap
+ src/host/generatedAdminExpoWeb.smoke.test.ts:446:1
No description available.
- handleSmokeStudioApiRequest
- src/host/generatedAdminExpoWeb.smoke.test.ts:319:1
+ createGeneratedAdminProject
+ src/host/generatedAdminExpoWeb.smoke.test.ts:453:1
No description available.
- listenOnLocalhost
- src/host/generatedAdminExpoWeb.smoke.test.ts:359:1
+ installGeneratedProjectDependencies
+ src/host/generatedAdminExpoWeb.smoke.test.ts:491:1
No description available.
- closeHttpServer
- src/host/generatedAdminExpoWeb.smoke.test.ts:373:1
+ stageLocalStudioPackage
+ src/host/generatedAdminExpoWeb.smoke.test.ts:525:1
No description available.
- readJsonRequest
- src/host/generatedAdminExpoWeb.smoke.test.ts:382:1
+ writeSmokeRuntimeExtensions
+ src/host/generatedAdminExpoWeb.smoke.test.ts:554:1
No description available.
- writeJsonResponse
- src/host/generatedAdminExpoWeb.smoke.test.ts:398:1
+ writeSmokeMetroConfig
+ src/host/generatedAdminExpoWeb.smoke.test.ts:633:1
No description available.
- writeCorsHeaders
- src/host/generatedAdminExpoWeb.smoke.test.ts:405:1
+ reservePort
+ src/host/generatedAdminExpoWeb.smoke.test.ts:656:1
No description available.
- waitForExpoWebUrl
- src/host/generatedAdminExpoWeb.smoke.test.ts:411:1
+ spawnExpoWeb
+ src/host/generatedAdminExpoWeb.smoke.test.ts:672:1
+ No description available.
+
+
+ startSmokeStudioApi
+ src/host/generatedAdminExpoWeb.smoke.test.ts:694:1
+ No description available.
+
+
+ handleSmokeStudioApiRequest
+ src/host/generatedAdminExpoWeb.smoke.test.ts:719:1
No description available.
+ listenOnLocalhost
+ src/host/generatedAdminExpoWeb.smoke.test.ts:759:1
+ No description available.
+
+
- linkSmokeNodeModules
- src/host/generatedAdminExpoWeb.smoke.test.ts:424:1
+ closeHttpServer
+ src/host/generatedAdminExpoWeb.smoke.test.ts:773:1
No description available.
- linkNodeModuleEntries
- src/host/generatedAdminExpoWeb.smoke.test.ts:437:1
+ readJsonRequest
+ src/host/generatedAdminExpoWeb.smoke.test.ts:782:1
No description available.
- linkScopedNodeModuleEntries
- src/host/generatedAdminExpoWeb.smoke.test.ts:474:1
+ writeJsonResponse
+ src/host/generatedAdminExpoWeb.smoke.test.ts:798:1
No description available.
- symlinkIfMissing
- src/host/generatedAdminExpoWeb.smoke.test.ts:489:1
+ writeCorsHeaders
+ src/host/generatedAdminExpoWeb.smoke.test.ts:805:1
No description available.
- isNodeErrorWithCode
- src/host/generatedAdminExpoWeb.smoke.test.ts:498:1
+ waitForExpoWebUrl
+ src/host/generatedAdminExpoWeb.smoke.test.ts:811:1
No description available.
isNodeErrorWithCode
data-search="waitForHttp src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForHttp
- src/host/generatedAdminExpoWeb.smoke.test.ts:502:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:824:1
No description available.
waitForHttp
data-search="waitForBodyText src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForBodyText
- src/host/generatedAdminExpoWeb.smoke.test.ts:519:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:841:1
No description available.
waitForBodyText
data-search="resolveChromePath src/host/generatedAdminExpoWeb.smoke.test.ts "
>
resolveChromePath
- src/host/generatedAdminExpoWeb.smoke.test.ts:536:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:858:1
No description available.
resolveChromePath
data-search="readEnvString src/host/generatedAdminExpoWeb.smoke.test.ts "
>
readEnvString
- src/host/generatedAdminExpoWeb.smoke.test.ts:542:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:864:1
No description available.
readEnvString
data-search="spawnChrome src/host/generatedAdminExpoWeb.smoke.test.ts "
>
spawnChrome
- src/host/generatedAdminExpoWeb.smoke.test.ts:548:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:870:1
No description available.
spawnChrome
data-search="openChromePage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
openChromePage
- src/host/generatedAdminExpoWeb.smoke.test.ts:564:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:886:1
No description available.
openChromePage
data-search="parseChromeProtocolMessage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
parseChromeProtocolMessage
- src/host/generatedAdminExpoWeb.smoke.test.ts:669:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1191:1
No description available.
parseChromeProtocolMessage
data-search="isRecord src/host/generatedAdminExpoWeb.smoke.test.ts "
>
isRecord
- src/host/generatedAdminExpoWeb.smoke.test.ts:684:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1206:1
No description available.
isRecord
data-search="stopProcess src/host/generatedAdminExpoWeb.smoke.test.ts "
>
stopProcess
- src/host/generatedAdminExpoWeb.smoke.test.ts:688:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1210:1
No description available.
stopProcess
data-search="collectProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts "
>
collectProcessOutput
- src/host/generatedAdminExpoWeb.smoke.test.ts:697:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1219:1
No description available.
collectProcessOutput
data-search="formatProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts "
>
formatProcessOutput
- src/host/generatedAdminExpoWeb.smoke.test.ts:708:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1230:1
No description available.
@@ -15280,7 +15348,7 @@ getPackageOwnedRuntimeImports
data-search="getGeneratedRuntimeRegistryDeclarations src/host/layout/layoutGenerator.ts "
>
getGeneratedRuntimeRegistryDeclarations
- src/host/layout/layoutGenerator.ts:61:1
+ src/host/layout/layoutGenerator.ts:65:1
No description available.
getGeneratedRuntimeRegistryDeclarations
data-search="mergeRuntimeModuleDeclarations src/host/layout/layoutGenerator.ts "
>
mergeRuntimeModuleDeclarations
- src/host/layout/layoutGenerator.ts:68:1
+ src/host/layout/layoutGenerator.ts:76:1
No description available.
mergeRuntimeModuleDeclarations
data-search="addStudioAdminRouteFiles src/host/layout/layoutGenerator.ts "
>
addStudioAdminRouteFiles
- src/host/layout/layoutGenerator.ts:91:11
+ src/host/layout/layoutGenerator.ts:99:11
No description available.
walk
- src/host/layout/layoutGenerator.ts:103:11
+ src/host/layout/layoutGenerator.ts:111:11
No description available.
walk
data-search="getStudioAdminLayoutTsx src/host/layout/layoutGenerator.ts "
>
getStudioAdminLayoutTsx
- src/host/layout/layoutGenerator.ts:428:1
+ src/host/layout/layoutGenerator.ts:436:1
No description available.
getStudioAdminLayoutTsx
data-search="createStudioAdminRouteGeneratedFiles src/host/layout/layoutGenerator.ts "
>
createStudioAdminRouteGeneratedFiles
- src/host/layout/layoutGenerator.ts:442:1
+ src/host/layout/layoutGenerator.ts:450:1
No description available.
createStudioAdminRouteGeneratedFiles
data-search="resolveStudioAdminRouteFilePath src/host/layout/layoutGenerator.ts "
>
resolveStudioAdminRouteFilePath
- src/host/layout/layoutGenerator.ts:449:1
+ src/host/layout/layoutGenerator.ts:457:1
No description available.
resolveStudioAdminRouteFilePath
data-search="getStudioAdminRouteTsx src/host/layout/layoutGenerator.ts "
>
getStudioAdminRouteTsx
- src/host/layout/layoutGenerator.ts:472:1
+ src/host/layout/layoutGenerator.ts:480:1
No description available.
normalizeRel
- src/host/layout/layoutGenerator.ts:486:1
+ src/host/layout/layoutGenerator.ts:494:1
No description available.
normalizeRel
data-search="prepareNavigatorForGeneratedRoutes src/host/layout/layoutGenerator.ts "
>
prepareNavigatorForGeneratedRoutes
- src/host/layout/layoutGenerator.ts:490:1
+ src/host/layout/layoutGenerator.ts:498:1
No description available.
prepareNavigatorForGeneratedRoutes
data-search="prepareRouteForGeneratedRoutes src/host/layout/layoutGenerator.ts "
>
prepareRouteForGeneratedRoutes
- src/host/layout/layoutGenerator.ts:534:1
+ src/host/layout/layoutGenerator.ts:542:1
No description available.
prepareRouteForGeneratedRoutes
data-search="normalizeGeneratedRouteName src/host/layout/layoutGenerator.ts "
>
normalizeGeneratedRouteName
- src/host/layout/layoutGenerator.ts:542:1
+ src/host/layout/layoutGenerator.ts:550:1
No description available.
normalizeGeneratedRouteName
data-search="resolveValidGeneratedInitialRouteName src/host/layout/layoutGenerator.ts "
>
resolveValidGeneratedInitialRouteName
- src/host/layout/layoutGenerator.ts:547:1
+ src/host/layout/layoutGenerator.ts:555:1
No description available.
@@ -15987,7 +16055,7 @@ createGeneratedAppExtensionRegistrySource
data-search="formatRegistryKey src/host/orchestrator/generatedAppFiles.ts "
>
formatRegistryKey
- src/host/orchestrator/generatedAppFiles.ts:102:1
+ src/host/orchestrator/generatedAppFiles.ts:120:1
No description available.
formatRegistryKey
data-search="removePath src/host/orchestrator/generatedAppFiles.ts "
>
removePath
- src/host/orchestrator/generatedAppFiles.ts:110:1
+ src/host/orchestrator/generatedAppFiles.ts:128:1
No description available.
removePath
data-search="assertNoForbiddenSpecifiers src/host/orchestrator/generatedAppFiles.ts "
>
assertNoForbiddenSpecifiers
- src/host/orchestrator/generatedAppFiles.ts:114:1
+ src/host/orchestrator/generatedAppFiles.ts:132:1
No description available.
assertNoForbiddenSpecifiers
data-search="listSourceFiles src/host/orchestrator/generatedAppFiles.ts "
>
listSourceFiles
- src/host/orchestrator/generatedAppFiles.ts:133:1
+ src/host/orchestrator/generatedAppFiles.ts:151:1
No description available.
listSourceFiles
data-search="isMissingPathError src/host/orchestrator/generatedAppFiles.ts "
>
isMissingPathError
- src/host/orchestrator/generatedAppFiles.ts:156:1
+ src/host/orchestrator/generatedAppFiles.ts:174:1
No description available.
@@ -17672,7 +17740,7 @@ src/host/zoraExtensions/index.ts
data-search="resolveZoraExtensionsForTemplateSelection src/host/zoraExtensions/index.ts "
>
resolveZoraExtensionsForTemplateSelection
- src/host/zoraExtensions/index.ts:36:1
+ src/host/zoraExtensions/index.ts:37:1
No description available.
resolveZoraExtensionsForTemplateSelection
data-search="resolveZoraExtensionsFromDependencies src/host/zoraExtensions/index.ts "
>
resolveZoraExtensionsFromDependencies
- src/host/zoraExtensions/index.ts:50:1
+ src/host/zoraExtensions/index.ts:51:1
No description available.
resolveZoraExtensionsFromDependencies
data-search="mergeZoraExtensions src/host/zoraExtensions/index.ts "
>
mergeZoraExtensions
- src/host/zoraExtensions/index.ts:56:1
+ src/host/zoraExtensions/index.ts:57:1
No description available.
mergeZoraExtensions
data-search="collectZoraExtensionDependencies src/host/zoraExtensions/index.ts "
>
collectZoraExtensionDependencies
- src/host/zoraExtensions/index.ts:70:1
+ src/host/zoraExtensions/index.ts:71:1
No description available.
@@ -19061,7 +19129,7 @@ src/runtime/interactionPolicy.ts
data-search="createStudioInteractionPolicyResolver src/runtime/interactionPolicy.ts "
>
createStudioInteractionPolicyResolver
- src/runtime/interactionPolicy.ts:15:1
+ src/runtime/interactionPolicy.ts:18:1
No description available.
@@ -19087,7 +19155,7 @@ classifyComponent
data-search="isComponentSupported src/runtime/interactionPolicyCore.ts "
>
isComponentSupported
- src/runtime/interactionPolicyCore.ts:30:1
+ src/runtime/interactionPolicyCore.ts:33:1
No description available.
isComponentSupported
data-search="resolveInteractionPolicy src/runtime/interactionPolicyCore.ts "
>
resolveInteractionPolicy
- src/runtime/interactionPolicyCore.ts:38:1
+ src/runtime/interactionPolicyCore.ts:41:1
No description available.
resolveInteractionPolicy
data-search="createInteractionPolicyResolver src/runtime/interactionPolicyCore.ts "
>
createInteractionPolicyResolver
- src/runtime/interactionPolicyCore.ts:55:1
+ src/runtime/interactionPolicyCore.ts:66:1
+ No description available.
+
+
+ composeInteractionPolicyResolver
+ src/runtime/interactionPolicyCore.ts:92:1
+ No description available.
+
+
+ composeInteractionPolicyResolver
+ src/runtime/interactionPolicyCore.ts:95:1
+ No description available.
+
+
+ composeInteractionPolicyResolver
+ src/runtime/interactionPolicyCore.ts:99:1
No description available.
@@ -19157,7 +19249,7 @@ src/runtime/previewRuntimeConfig.ts
data-search="createStudioPreviewRuntimeConfig src/runtime/previewRuntimeConfig.ts "
>
createStudioPreviewRuntimeConfig
- src/runtime/previewRuntimeConfig.ts:12:14
+ src/runtime/previewRuntimeConfig.ts:15:14
No description available.
@@ -19185,20 +19277,75 @@ executeRuntimeAction
>
src/runtime/stationarySelection.ts
+
+ isWebElementLike
+ src/runtime/stationarySelection.ts:48:1
+ No description available.
+
+
+ toMeasuredRect
+ src/runtime/stationarySelection.ts:58:1
+ No description available.
+
+
+ unionRects
+ src/runtime/stationarySelection.ts:67:1
+ No description available.
+
+
+ measureRenderedBoxes
+ src/runtime/stationarySelection.ts:85:1
+ No description available.
+
+
+ measureNativeView
+ src/runtime/stationarySelection.ts:94:1
+ No description available.
+
+
+ measureRootView
+ src/runtime/stationarySelection.ts:102:1
+ No description available.
+
+
+ measureUnsupportedView
+ src/runtime/stationarySelection.ts:114:1
+ No description available.
+
+
+ areIndicatorRectsEqual
+ src/runtime/stationarySelection.ts:128:1
+ No description available.
+
StudioNodeTouchRecorder
- src/runtime/stationarySelection.ts:21:1
+ src/runtime/stationarySelection.ts:151:1
No description available.
- handleTouchStart
- src/runtime/stationarySelection.ts:31:3
+ handleInteractionStart
+ src/runtime/stationarySelection.ts:162:3
No description available.
handleTouchStart
data-search="createStudioStationarySelectionWrapNode src/runtime/stationarySelection.ts "
>
createStudioStationarySelectionWrapNode
- src/runtime/stationarySelection.ts:70:1
+ src/runtime/stationarySelection.ts:208:1
No description available.
createStudioStationarySelectionWrapNode
data-search="StationaryTapSelector src/runtime/stationarySelection.ts "
>
StationaryTapSelector
- src/runtime/stationarySelection.ts:105:1
+ src/runtime/stationarySelection.ts:242:1
+ No description available.
+
+
+ measureNextFrame
+ src/runtime/stationarySelection.ts:353:11
No description available.
diff --git a/paradox/paradox.json b/paradox/paradox.json
index 16c3fc8e..a474f506 100644
--- a/paradox/paradox.json
+++ b/paradox/paradox.json
@@ -1011,11 +1011,21 @@
],
"exports": []
},
+ {
+ "path": "src/host/orchestrator/generatedAppFiles.test.ts",
+ "isEntrypoint": false,
+ "dependencies": ["src/host/orchestrator/generatedAppFiles.ts"],
+ "exports": []
+ },
{
"path": "src/host/orchestrator/generatedAppFiles.ts",
"isEntrypoint": false,
"dependencies": ["src/host/zoraExtensions/index.ts"],
- "exports": ["GeneratedAppFilesOptions", "syncGeneratedAppFiles"]
+ "exports": [
+ "createGeneratedAppExtensionRegistrySource",
+ "GeneratedAppFilesOptions",
+ "syncGeneratedAppFiles"
+ ]
},
{
"path": "src/host/orchestrator/generatedRouteCleanup.ts",
@@ -1905,7 +1915,10 @@
"path": "src/runtime/appExtensionRegistry.ts",
"isEntrypoint": false,
"dependencies": [],
- "exports": ["STUDIO_APP_EXTENSION_COMPONENT_REGISTRY"]
+ "exports": [
+ "STUDIO_APP_EXTENSION_COMPONENT_REGISTRY",
+ "STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT"
+ ]
},
{
"path": "src/runtime/index.ts",
@@ -1914,6 +1927,7 @@
"exports": [
"CommitSelectionResult",
"ComponentRegistry",
+ "composeInteractionPolicyResolver",
"createInteractionPolicyResolver",
"createStationarySelectionCoordinator",
"createStudioActionSuppressionConfig",
@@ -1923,6 +1937,8 @@
"createStudioStationarySelectionWrapNode",
"executeRuntimeAction",
"InteractionPolicy",
+ "InteractionPolicyNodePropsResolver",
+ "InteractionPolicyResolveArgs",
"InteractionPolicyResolverArgs",
"isComponentSupported",
"resolveStudioLocalizationPreview",
@@ -1930,6 +1946,7 @@
"StationarySelectionCoordinator",
"StationaryTapSelector",
"STUDIO_APP_EXTENSION_COMPONENT_REGISTRY",
+ "STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT",
"STUDIO_PREVIEW_COMPONENT_REGISTRY",
"StudioInteractionPolicyResolverArgs",
"StudioLocalizationPreview",
@@ -1964,8 +1981,11 @@
"isEntrypoint": false,
"dependencies": [],
"exports": [
+ "composeInteractionPolicyResolver",
"createInteractionPolicyResolver",
"InteractionPolicy",
+ "InteractionPolicyNodePropsResolver",
+ "InteractionPolicyResolveArgs",
"InteractionPolicyResolverArgs",
"isComponentSupported",
"ThirdPartyComponentSupport"
@@ -1997,6 +2017,7 @@
"dependencies": [
"src/runtime/actionSuppression.ts",
"src/runtime/interactionPolicy.ts",
+ "src/runtime/interactionPolicyCore.ts",
"src/runtime/previewRegistry.ts"
],
"exports": ["createStudioPreviewRuntimeConfig", "StudioPreviewRuntimeConfigOptions"]
@@ -7922,7 +7943,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 46,
+ "line": 37,
"column": 1
}
},
@@ -7931,160 +7952,187 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 102,
+ "line": 93,
"column": 1
}
},
{
- "name": "createGeneratedAdminProject",
+ "name": "verifyDesktopSelectionAndUnsupportedGeometry",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 195,
+ "line": 265,
"column": 1
}
},
{
- "name": "writeSmokeMetroConfig",
+ "name": "waitForUnsupportedGeometry",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 233,
+ "line": 401,
"column": 1
}
},
{
- "name": "reservePort",
+ "name": "waitForStudioSmokeState",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 256,
+ "line": 420,
"column": 1
}
},
{
- "name": "spawnExpoWeb",
+ "name": "expectRectToMatch",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 272,
+ "line": 439,
"column": 1
}
},
{
- "name": "startSmokeStudioApi",
+ "name": "rectsOverlap",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 294,
+ "line": 446,
"column": 1
}
},
{
- "name": "handleSmokeStudioApiRequest",
+ "name": "createGeneratedAdminProject",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 319,
+ "line": 453,
"column": 1
}
},
{
- "name": "listenOnLocalhost",
+ "name": "installGeneratedProjectDependencies",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 359,
+ "line": 491,
"column": 1
}
},
{
- "name": "closeHttpServer",
+ "name": "stageLocalStudioPackage",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 373,
+ "line": 525,
"column": 1
}
},
{
- "name": "readJsonRequest",
+ "name": "writeSmokeRuntimeExtensions",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 382,
+ "line": 554,
"column": 1
}
},
{
- "name": "writeJsonResponse",
+ "name": "writeSmokeMetroConfig",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 398,
+ "line": 633,
"column": 1
}
},
{
- "name": "writeCorsHeaders",
+ "name": "reservePort",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 405,
+ "line": 656,
"column": 1
}
},
{
- "name": "waitForExpoWebUrl",
+ "name": "spawnExpoWeb",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 411,
+ "line": 672,
"column": 1
}
},
{
- "name": "linkSmokeNodeModules",
+ "name": "startSmokeStudioApi",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 424,
+ "line": 694,
"column": 1
}
},
{
- "name": "linkNodeModuleEntries",
+ "name": "handleSmokeStudioApiRequest",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 437,
+ "line": 719,
"column": 1
}
},
{
- "name": "linkScopedNodeModuleEntries",
+ "name": "listenOnLocalhost",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 474,
+ "line": 759,
"column": 1
}
},
{
- "name": "symlinkIfMissing",
+ "name": "closeHttpServer",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 489,
+ "line": 773,
+ "column": 1
+ }
+ },
+ {
+ "name": "readJsonRequest",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "line": 782,
"column": 1
}
},
{
- "name": "isNodeErrorWithCode",
+ "name": "writeJsonResponse",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 498,
+ "line": 798,
+ "column": 1
+ }
+ },
+ {
+ "name": "writeCorsHeaders",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "line": 805,
+ "column": 1
+ }
+ },
+ {
+ "name": "waitForExpoWebUrl",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "line": 811,
"column": 1
}
},
@@ -8093,7 +8141,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 502,
+ "line": 824,
"column": 1
}
},
@@ -8102,7 +8150,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 519,
+ "line": 841,
"column": 1
}
},
@@ -8111,7 +8159,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 536,
+ "line": 858,
"column": 1
}
},
@@ -8120,7 +8168,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 542,
+ "line": 864,
"column": 1
}
},
@@ -8129,7 +8177,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 548,
+ "line": 870,
"column": 1
}
},
@@ -8138,7 +8186,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 564,
+ "line": 886,
"column": 1
}
},
@@ -8147,7 +8195,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 669,
+ "line": 1191,
"column": 1
}
},
@@ -8156,7 +8204,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 684,
+ "line": 1206,
"column": 1
}
},
@@ -8165,7 +8213,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 688,
+ "line": 1210,
"column": 1
}
},
@@ -8174,7 +8222,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 697,
+ "line": 1219,
"column": 1
}
},
@@ -8183,7 +8231,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 708,
+ "line": 1230,
"column": 1
}
},
@@ -8849,7 +8897,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 61,
+ "line": 65,
"column": 1
}
},
@@ -8858,7 +8906,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 68,
+ "line": 76,
"column": 1
}
},
@@ -8867,7 +8915,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 91,
+ "line": 99,
"column": 11
}
},
@@ -8876,7 +8924,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 103,
+ "line": 111,
"column": 11
}
},
@@ -8885,7 +8933,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 428,
+ "line": 436,
"column": 1
}
},
@@ -8894,7 +8942,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 442,
+ "line": 450,
"column": 1
}
},
@@ -8903,7 +8951,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 449,
+ "line": 457,
"column": 1
}
},
@@ -8912,7 +8960,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 472,
+ "line": 480,
"column": 1
}
},
@@ -8921,7 +8969,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 486,
+ "line": 494,
"column": 1
}
},
@@ -8930,7 +8978,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 490,
+ "line": 498,
"column": 1
}
},
@@ -8939,7 +8987,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 534,
+ "line": 542,
"column": 1
}
},
@@ -8948,7 +8996,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 542,
+ "line": 550,
"column": 1
}
},
@@ -8957,7 +9005,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/layout/layoutGenerator.ts",
- "line": 547,
+ "line": 555,
"column": 1
}
},
@@ -9425,7 +9473,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/generatedAppFiles.ts",
- "line": 102,
+ "line": 120,
"column": 1
}
},
@@ -9434,7 +9482,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/generatedAppFiles.ts",
- "line": 110,
+ "line": 128,
"column": 1
}
},
@@ -9443,7 +9491,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/generatedAppFiles.ts",
- "line": 114,
+ "line": 132,
"column": 1
}
},
@@ -9452,7 +9500,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/generatedAppFiles.ts",
- "line": 133,
+ "line": 151,
"column": 1
}
},
@@ -9461,7 +9509,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/generatedAppFiles.ts",
- "line": 156,
+ "line": 174,
"column": 1
}
},
@@ -11009,7 +11057,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/zoraExtensions/index.ts",
- "line": 36,
+ "line": 37,
"column": 1
}
},
@@ -11018,7 +11066,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/zoraExtensions/index.ts",
- "line": 50,
+ "line": 51,
"column": 1
}
},
@@ -11027,7 +11075,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/zoraExtensions/index.ts",
- "line": 56,
+ "line": 57,
"column": 1
}
},
@@ -11036,7 +11084,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/zoraExtensions/index.ts",
- "line": 70,
+ "line": 71,
"column": 1
}
},
@@ -12809,7 +12857,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/interactionPolicy.ts",
- "line": 15,
+ "line": 18,
"column": 1
}
},
@@ -12827,7 +12875,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/interactionPolicyCore.ts",
- "line": 30,
+ "line": 33,
"column": 1
}
},
@@ -12836,7 +12884,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/interactionPolicyCore.ts",
- "line": 38,
+ "line": 41,
"column": 1
}
},
@@ -12845,7 +12893,34 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/interactionPolicyCore.ts",
- "line": 55,
+ "line": 66,
+ "column": 1
+ }
+ },
+ {
+ "name": "composeInteractionPolicyResolver",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/interactionPolicyCore.ts",
+ "line": 92,
+ "column": 1
+ }
+ },
+ {
+ "name": "composeInteractionPolicyResolver",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/interactionPolicyCore.ts",
+ "line": 95,
+ "column": 1
+ }
+ },
+ {
+ "name": "composeInteractionPolicyResolver",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/interactionPolicyCore.ts",
+ "line": 99,
"column": 1
}
},
@@ -12872,7 +12947,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/previewRuntimeConfig.ts",
- "line": 12,
+ "line": 15,
"column": 14
}
},
@@ -12885,21 +12960,93 @@
"column": 1
}
},
+ {
+ "name": "isWebElementLike",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 48,
+ "column": 1
+ }
+ },
+ {
+ "name": "toMeasuredRect",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 58,
+ "column": 1
+ }
+ },
+ {
+ "name": "unionRects",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 67,
+ "column": 1
+ }
+ },
+ {
+ "name": "measureRenderedBoxes",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 85,
+ "column": 1
+ }
+ },
+ {
+ "name": "measureNativeView",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 94,
+ "column": 1
+ }
+ },
+ {
+ "name": "measureRootView",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 102,
+ "column": 1
+ }
+ },
+ {
+ "name": "measureUnsupportedView",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 114,
+ "column": 1
+ }
+ },
+ {
+ "name": "areIndicatorRectsEqual",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 128,
+ "column": 1
+ }
+ },
{
"name": "StudioNodeTouchRecorder",
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 21,
+ "line": 151,
"column": 1
}
},
{
- "name": "handleTouchStart",
+ "name": "handleInteractionStart",
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 31,
+ "line": 162,
"column": 3
}
},
@@ -12908,7 +13055,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 70,
+ "line": 208,
"column": 1
}
},
@@ -12917,10 +13064,19 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 105,
+ "line": 242,
"column": 1
}
},
+ {
+ "name": "measureNextFrame",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 353,
+ "column": 11
+ }
+ },
{
"name": "createStationarySelectionCoordinator",
"description": null,
@@ -15541,6 +15697,11 @@
"toPath": "src/runtime/interactionPolicy.ts",
"sourcePath": "src/runtime/previewRuntimeConfig.ts"
},
+ {
+ "fromPath": "src/runtime/previewRuntimeConfig.ts",
+ "toPath": "src/runtime/interactionPolicyCore.ts",
+ "sourcePath": "src/runtime/previewRuntimeConfig.ts"
+ },
{
"fromPath": "src/runtime/previewRuntimeConfig.ts",
"toPath": "src/runtime/previewRegistry.ts",
@@ -15856,6 +16017,11 @@
"toPath": "src/host/modules/layout.ts",
"sourcePath": "src/host/modules/catalog.ts"
},
+ {
+ "fromPath": "src/host/orchestrator/generatedAppFiles.test.ts",
+ "toPath": "src/host/orchestrator/generatedAppFiles.ts",
+ "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts"
+ },
{
"fromPath": "src/host/orchestrator/generatedAppFiles.ts",
"toPath": "src/host/zoraExtensions/index.ts",
@@ -22866,6 +23032,12 @@
"callExpression": "readFile",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "realpath",
+ "callExpression": "realpath",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"toSymbol": "expect",
@@ -22908,6 +23080,12 @@
"callExpression": "openChromePage",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "blockHotReloadConnections",
+ "callExpression": "page.blockHotReloadConnections",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"toSymbol": "navigate",
@@ -22926,6 +23104,18 @@
"callExpression": "page.readBodyText",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "formatProcessOutput",
+ "callExpression": "formatProcessOutput",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "callExpression": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"toSymbol": "close",
@@ -22951,21 +23141,111 @@
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
- "fromSymbol": "createGeneratedAdminProject",
- "toSymbol": "mkdir",
- "callExpression": "mkdir",
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "waitForUnsupportedGeometry",
+ "callExpression": "waitForUnsupportedGeometry",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
- "fromSymbol": "createGeneratedAdminProject",
- "toSymbol": "writeFile",
- "callExpression": "writeFile",
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "expectRectToMatch",
+ "callExpression": "expectRectToMatch",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "rectsOverlap",
+ "callExpression": "rectsOverlap",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "evaluate",
+ "callExpression": "page.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "readRuntimeNodeCenter",
+ "callExpression": "page.readRuntimeNodeCenter",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "mouseClick",
+ "callExpression": "page.mouseClick",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "waitForStudioSmokeState",
+ "callExpression": "waitForStudioSmokeState",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "readSelectionRootId",
+ "callExpression": "page.readSelectionRootId",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "readStudioSmokeState",
+ "callExpression": "page.readStudioSmokeState",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "touchTap",
+ "callExpression": "page.touchTap",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "mouseDrag",
+ "callExpression": "page.mouseDrag",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "waitForUnsupportedGeometry",
+ "toSymbol": "readUnsupportedGeometry",
+ "callExpression": "page.readUnsupportedGeometry",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "waitForUnsupportedGeometry",
+ "toSymbol": "readUnsupportedGeometryDiagnostics",
+ "callExpression": "page.readUnsupportedGeometryDiagnostics",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "waitForStudioSmokeState",
+ "toSymbol": "readStudioSmokeState",
+ "callExpression": "page.readStudioSmokeState",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "expectRectToMatch",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "createGeneratedAdminProject",
+ "toSymbol": "mkdir",
+ "callExpression": "mkdir",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
"fromSymbol": "createGeneratedAdminProject",
- "toSymbol": "linkSmokeNodeModules",
- "callExpression": "linkSmokeNodeModules",
+ "toSymbol": "writeFile",
+ "callExpression": "writeFile",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
@@ -23004,12 +23284,72 @@
"callExpression": "moduleManager.syncProject",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "createGeneratedAdminProject",
+ "toSymbol": "writeSmokeRuntimeExtensions",
+ "callExpression": "writeSmokeRuntimeExtensions",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "createGeneratedAdminProject",
"toSymbol": "writeSmokeMetroConfig",
"callExpression": "writeSmokeMetroConfig",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "createGeneratedAdminProject",
+ "toSymbol": "installGeneratedProjectDependencies",
+ "callExpression": "installGeneratedProjectDependencies",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "installGeneratedProjectDependencies",
+ "toSymbol": "readFile",
+ "callExpression": "readFile",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "installGeneratedProjectDependencies",
+ "toSymbol": "stageLocalStudioPackage",
+ "callExpression": "stageLocalStudioPackage",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "installGeneratedProjectDependencies",
+ "toSymbol": "writeFile",
+ "callExpression": "writeFile",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "stageLocalStudioPackage",
+ "toSymbol": "readFile",
+ "callExpression": "readFile",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "stageLocalStudioPackage",
+ "toSymbol": "mkdir",
+ "callExpression": "mkdir",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "stageLocalStudioPackage",
+ "toSymbol": "cp",
+ "callExpression": "cp",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "stageLocalStudioPackage",
+ "toSymbol": "writeFile",
+ "callExpression": "writeFile",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "writeSmokeRuntimeExtensions",
+ "toSymbol": "writeFile",
+ "callExpression": "writeFile",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "writeSmokeMetroConfig",
"toSymbol": "writeFile",
@@ -23106,84 +23446,6 @@
"callExpression": "formatProcessOutput",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
- {
- "fromSymbol": "linkSmokeNodeModules",
- "toSymbol": "mkdir",
- "callExpression": "mkdir",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkSmokeNodeModules",
- "toSymbol": "linkNodeModuleEntries",
- "callExpression": "linkNodeModuleEntries",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkNodeModuleEntries",
- "toSymbol": "readdir",
- "callExpression": "readdir",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkNodeModuleEntries",
- "toSymbol": "mkdir",
- "callExpression": "mkdir",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkNodeModuleEntries",
- "toSymbol": "symlinkIfMissing",
- "callExpression": "symlinkIfMissing",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkNodeModuleEntries",
- "toSymbol": "linkScopedNodeModuleEntries",
- "callExpression": "linkScopedNodeModuleEntries",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkScopedNodeModuleEntries",
- "toSymbol": "mkdir",
- "callExpression": "mkdir",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkScopedNodeModuleEntries",
- "toSymbol": "readdir",
- "callExpression": "readdir",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "linkScopedNodeModuleEntries",
- "toSymbol": "symlinkIfMissing",
- "callExpression": "symlinkIfMissing",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "symlinkIfMissing",
- "toSymbol": "symlink",
- "callExpression": "symlink",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "symlinkIfMissing",
- "toSymbol": "realpath",
- "callExpression": "realpath",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "symlinkIfMissing",
- "toSymbol": "isNodeErrorWithCode",
- "callExpression": "isNodeErrorWithCode",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
- {
- "fromSymbol": "isNodeErrorWithCode",
- "toSymbol": "isRecord",
- "callExpression": "isRecord",
- "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
- },
{
"fromSymbol": "waitForHttp",
"toSymbol": "getDiagnostics",
@@ -23262,6 +23524,72 @@
"callExpression": "isRecord",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "evaluate",
+ "toSymbol": "send",
+ "callExpression": "this.send",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "evaluate",
+ "toSymbol": "isRecord",
+ "callExpression": "isRecord",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "blockHotReloadConnections",
+ "toSymbol": "send",
+ "callExpression": "this.send",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "readStudioSmokeState",
+ "toSymbol": "evaluate",
+ "callExpression": "this.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "readSelectionRootId",
+ "toSymbol": "evaluate",
+ "callExpression": "this.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "readRuntimeNodeCenter",
+ "toSymbol": "evaluate",
+ "callExpression": "this.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "readUnsupportedGeometry",
+ "toSymbol": "evaluate",
+ "callExpression": "this.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "readUnsupportedGeometryDiagnostics",
+ "toSymbol": "evaluate",
+ "callExpression": "this.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "mouseClick",
+ "toSymbol": "send",
+ "callExpression": "this.send",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "mouseDrag",
+ "toSymbol": "send",
+ "callExpression": "this.send",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "touchTap",
+ "toSymbol": "send",
+ "callExpression": "this.send",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"toSymbol": "parseChromeProtocolMessage",
@@ -24504,6 +24832,12 @@
"callExpression": "isComponentSupported",
"sourcePath": "src/runtime/interactionPolicy.test.ts"
},
+ {
+ "fromSymbol": "src/runtime/interactionPolicy.test.ts",
+ "toSymbol": "composeInteractionPolicyResolver",
+ "callExpression": "composeInteractionPolicyResolver",
+ "sourcePath": "src/runtime/interactionPolicy.test.ts"
+ },
{
"fromSymbol": "src/runtime/interactionPolicy.test.ts",
"toSymbol": "join",
@@ -24523,15 +24857,9 @@
"sourcePath": "src/runtime/interactionPolicy.ts"
},
{
- "fromSymbol": "src/runtime/interactionPolicy.ts",
- "toSymbol": "resolver",
- "callExpression": "resolver",
- "sourcePath": "src/runtime/interactionPolicy.ts"
- },
- {
- "fromSymbol": "src/runtime/interactionPolicy.ts",
- "toSymbol": "existingResolver",
- "callExpression": "existingResolver",
+ "fromSymbol": "createStudioInteractionPolicyResolver",
+ "toSymbol": "composeInteractionPolicyResolver",
+ "callExpression": "composeInteractionPolicyResolver",
"sourcePath": "src/runtime/interactionPolicy.ts"
},
{
@@ -24558,6 +24886,18 @@
"callExpression": "resolveInteractionPolicy",
"sourcePath": "src/runtime/interactionPolicyCore.ts"
},
+ {
+ "fromSymbol": "src/runtime/interactionPolicyCore.ts",
+ "toSymbol": "interactionPolicyResolver",
+ "callExpression": "interactionPolicyResolver",
+ "sourcePath": "src/runtime/interactionPolicyCore.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/interactionPolicyCore.ts",
+ "toSymbol": "existingResolver",
+ "callExpression": "existingResolver",
+ "sourcePath": "src/runtime/interactionPolicyCore.ts"
+ },
{
"fromSymbol": "createStudioPreviewComponentRegistry",
"toSymbol": "createComponentRegistry",
@@ -24655,29 +24995,161 @@
"sourcePath": "src/runtime/stationarySelection.test.ts"
},
{
- "fromSymbol": "handleTouchStart",
+ "fromSymbol": "measureRenderedBoxes",
+ "toSymbol": "toMeasuredRect",
+ "callExpression": "toMeasuredRect",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureRenderedBoxes",
+ "toSymbol": "getBoundingClientRect",
+ "callExpression": "element.getBoundingClientRect",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "measureRenderedBoxes",
+ "callExpression": "measureRenderedBoxes",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "resolve",
+ "callExpression": "resolve",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureRootView",
+ "toSymbol": "isWebElementLike",
+ "callExpression": "isWebElementLike",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureRootView",
+ "toSymbol": "toMeasuredRect",
+ "callExpression": "toMeasuredRect",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureRootView",
+ "toSymbol": "getBoundingClientRect",
+ "callExpression": "view.getBoundingClientRect",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureRootView",
+ "toSymbol": "measureNativeView",
+ "callExpression": "measureNativeView",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureUnsupportedView",
+ "toSymbol": "isWebElementLike",
+ "callExpression": "isWebElementLike",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureUnsupportedView",
+ "toSymbol": "unionRects",
+ "callExpression": "unionRects",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureUnsupportedView",
+ "toSymbol": "measureNativeView",
+ "callExpression": "measureNativeView",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "handleInteractionStart",
"toSymbol": "recordNode",
"callExpression": "ctx.recordNode",
"sourcePath": "src/runtime/stationarySelection.ts"
},
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "registerUnsupportedNode",
+ "callExpression": "ctx.registerUnsupportedNode",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "measureUnsupportedView",
+ "callExpression": "measureUnsupportedView",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
{
"fromSymbol": "StationaryTapSelector",
"toSymbol": "createStationarySelectionCoordinator",
"callExpression": "createStationarySelectionCoordinator",
"sourcePath": "src/runtime/stationarySelection.ts"
},
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "setUnsupportedNodeVersion",
+ "callExpression": "setUnsupportedNodeVersion",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
{
"fromSymbol": "recordNode",
"toSymbol": "recordNode",
"callExpression": "coordinator.recordNode",
"sourcePath": "src/runtime/stationarySelection.ts"
},
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "setIndicatorRects",
+ "callExpression": "setIndicatorRects",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "measureRootView",
+ "callExpression": "measureRootView",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "measure",
+ "callExpression": "measure",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "unionRects",
+ "callExpression": "unionRects",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "areIndicatorRectsEqual",
+ "callExpression": "areIndicatorRectsEqual",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measureNextFrame",
+ "toSymbol": "refreshIndicatorRects",
+ "callExpression": "refreshIndicatorRects",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "measureNextFrame",
+ "callExpression": "measureNextFrame",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
{
"fromSymbol": "src/runtime/stationarySelection.ts",
"toSymbol": "beginTransaction",
"callExpression": "coordinator.beginTransaction",
"sourcePath": "src/runtime/stationarySelection.ts"
},
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "recordNode",
+ "callExpression": "coordinator.recordNode",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
{
"fromSymbol": "src/runtime/stationarySelection.ts",
"toSymbol": "commitSelection",
@@ -26184,6 +26656,30 @@
"callExpression": "parseExpoGoogleFontsModuleConfig",
"sourcePath": "src/host/modules/catalog.ts"
},
+ {
+ "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts",
+ "toSymbol": "describe",
+ "callExpression": "describe",
+ "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts",
+ "toSymbol": "it",
+ "callExpression": "it",
+ "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts",
+ "toSymbol": "createGeneratedAppExtensionRegistrySource",
+ "callExpression": "createGeneratedAppExtensionRegistrySource",
+ "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/orchestrator/generatedAppFiles.test.ts"
+ },
{
"fromSymbol": "syncGeneratedAppFiles",
"toSymbol": "removePath",
diff --git a/src/host/generatedAdminExpoWeb.smoke.test.ts b/src/host/generatedAdminExpoWeb.smoke.test.ts
index 78dcf0f1..99d668d6 100644
--- a/src/host/generatedAdminExpoWeb.smoke.test.ts
+++ b/src/host/generatedAdminExpoWeb.smoke.test.ts
@@ -1,14 +1,5 @@
import { type ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
-import {
- mkdir,
- mkdtemp,
- readdir,
- readFile,
- realpath,
- rm,
- symlink,
- writeFile,
-} from 'node:fs/promises';
+import { cp, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
import {
createServer as createHttpServer,
type IncomingMessage,
@@ -115,13 +106,55 @@ function createScrollableRuntimeScreenRoot(): UiNode {
title: 'Scrollable Runtime Screen',
description: 'Rendered through the generated-app runtime registry.',
},
- children: Array.from({ length: 16 }, (_, index) => ({
- id: `dashboard-runtime-row-${index}`,
- type: 'Text',
- props: {
- children: `Generated runtime row ${index + 1}`,
+ children: [
+ {
+ id: 'desktop-pointer-parent',
+ type: 'Box',
+ props: {
+ p: 'm',
+ testID: 'desktop-pointer-parent',
+ },
+ children: [
+ {
+ id: 'desktop-pointer-target',
+ type: 'Text',
+ props: {
+ children: 'Desktop pointer target',
+ testID: 'desktop-pointer-target',
+ },
+ },
+ ],
+ },
+ {
+ id: 'unsupported-runtime-target',
+ type: 'SmokeUnsupported',
+ props: {
+ label: 'Unsupported runtime target',
+ testID: 'unsupported-runtime-target',
+ },
+ },
+ {
+ id: 'supported-runtime-neighbor',
+ type: 'Text',
+ props: {
+ children: 'Supported runtime neighbor',
+ testID: 'supported-runtime-neighbor',
+ },
},
- })),
+ {
+ id: 'studio-smoke-probe',
+ type: 'SmokeStudioProbe',
+ props: {},
+ },
+ ...Array.from({ length: 16 }, (_, index) => ({
+ id: `dashboard-runtime-row-${index}`,
+ type: 'Text',
+ props: {
+ children: `Generated runtime row ${index + 1}`,
+ testID: `dashboard-runtime-row-${index}`,
+ },
+ })),
+ ],
},
],
};
@@ -148,6 +181,21 @@ adminWebSmokeTest(
path.join(projectRoot, 'src', 'app', '_layout.tsx'),
'utf8',
);
+ const generatedPackage = JSON.parse(
+ await readFile(path.join(projectRoot, 'package.json'), 'utf8'),
+ ) as { dependencies?: Record };
+ const resolvedZoraPackage = JSON.parse(
+ await readFile(
+ path.join(
+ await realpath(path.join(projectRoot, 'node_modules', '@ankhorage', 'zora')),
+ 'package.json',
+ ),
+ 'utf8',
+ ),
+ ) as { version?: string };
+
+ expect(generatedPackage.dependencies?.['@ankhorage/zora']).toBe('^2.9.0');
+ expect(resolvedZoraPackage.version).toBe('2.9.0');
expect(rootLayout).toContain('function GeneratedZoraThemeConfigSync');
expect(rootLayout).toContain('lastSyncedThemeConfigSignatureRef');
expect(rootLayout).not.toContain('}, [setThemeConfig, themeConfig]);');
@@ -160,7 +208,8 @@ adminWebSmokeTest(
chromeProcess = spawnChrome(chromePath, debugPort);
const page = await openChromePage(debugPort);
try {
- for (const route of ['/', '/dashboard', '/ankh', '/ankh/theme', '/ankh/auth/providers']) {
+ await page.blockHotReloadConnections();
+ for (const route of ['/dashboard', '/ankh', '/ankh/theme', '/ankh/auth/providers']) {
await page.navigate(`${appUrl}${route}`);
await Bun.sleep(ROUTE_SETTLE_MS);
const bodyText =
@@ -173,8 +222,13 @@ adminWebSmokeTest(
: await page.readBodyText();
expect(bodyText).not.toContain('Maximum update depth exceeded');
if (route === '/dashboard') {
- expect(bodyText).toContain('Scrollable Runtime Screen');
+ if (!bodyText.includes('Scrollable Runtime Screen')) {
+ throw new Error(
+ `Generated dashboard did not load.\nBody:\n${bodyText}\nChrome errors:\n${page.errors.join('\n')}${formatProcessOutput(expoOutput)}`,
+ );
+ }
expect(bodyText).toContain('Generated runtime row 16');
+ await verifyDesktopSelectionAndUnsupportedGeometry(page);
}
expect(page.errors.join('\n')).not.toContain('Maximum update depth exceeded');
expect(page.errors.join('\n')).not.toContain('Cannot read properties of undefined');
@@ -192,6 +246,210 @@ adminWebSmokeTest(
TEST_TIMEOUT_MS,
);
+interface BrowserRect {
+ readonly left: number;
+ readonly top: number;
+ readonly right: number;
+ readonly bottom: number;
+ readonly width: number;
+ readonly height: number;
+}
+
+interface UnsupportedGeometrySnapshot {
+ readonly indicator: BrowserRect;
+ readonly target: BrowserRect;
+ readonly neighbor: BrowserRect;
+ readonly pointerEvents: string;
+}
+
+async function verifyDesktopSelectionAndUnsupportedGeometry(page: ChromePage): Promise {
+ const initialGeometry = await waitForUnsupportedGeometry(page, 15_000);
+
+ expectRectToMatch(initialGeometry.indicator, initialGeometry.target);
+ expect(initialGeometry.pointerEvents).toBe('none');
+ expect(rectsOverlap(initialGeometry.indicator, initialGeometry.neighbor)).toBe(false);
+ expect(
+ await page.evaluate(
+ `document.getElementById('studio-unsupported-indicator-supported-runtime-neighbor') !== null`,
+ ),
+ ).toBe(false);
+
+ const scrolled = await page.evaluate(`(() => {
+ const wrapper = document.getElementById('studio-runtime-node-unsupported-runtime-target');
+ const target = wrapper?.firstElementChild;
+ if (!(target instanceof HTMLElement)) return false;
+ let scrollParent = target.parentElement;
+ while (scrollParent) {
+ const style = getComputedStyle(scrollParent);
+ if (
+ /(auto|scroll)/.test(style.overflowY) &&
+ scrollParent.scrollHeight > scrollParent.clientHeight
+ ) {
+ scrollParent.scrollTop += 80;
+ scrollParent.dispatchEvent(new Event('scroll', { bubbles: true }));
+ return true;
+ }
+ scrollParent = scrollParent.parentElement;
+ }
+ return false;
+ })()`);
+ expect(scrolled).toBe(true);
+ await Bun.sleep(250);
+
+ const scrolledGeometry = await waitForUnsupportedGeometry(page, 15_000);
+ expectRectToMatch(scrolledGeometry.indicator, scrolledGeometry.target);
+ expect(Math.abs(scrolledGeometry.target.top - initialGeometry.target.top)).toBeGreaterThan(20);
+ expect(rectsOverlap(scrolledGeometry.indicator, scrolledGeometry.neighbor)).toBe(false);
+
+ await page.evaluate(`(() => {
+ const wrapper = document.getElementById('studio-runtime-node-unsupported-runtime-target');
+ const target = wrapper?.firstElementChild;
+ if (!(target instanceof HTMLElement)) return;
+ let scrollParent = target.parentElement;
+ while (scrollParent) {
+ const style = getComputedStyle(scrollParent);
+ if (
+ /(auto|scroll)/.test(style.overflowY) &&
+ scrollParent.scrollHeight > scrollParent.clientHeight
+ ) {
+ scrollParent.scrollTop = 0;
+ scrollParent.dispatchEvent(new Event('scroll', { bubbles: true }));
+ return;
+ }
+ scrollParent = scrollParent.parentElement;
+ }
+ })()`);
+ await Bun.sleep(250);
+
+ const desktopTarget = await page.readRuntimeNodeCenter('desktop-pointer-target');
+ await page.mouseClick(desktopTarget.x, desktopTarget.y);
+ await waitForStudioSmokeState(
+ page,
+ 'mode=edit;selection=desktop-pointer-target;changes=1',
+ 15_000,
+ );
+ expect(await page.readSelectionRootId()).toBe(
+ 'studio-stationary-selection-root:edit:desktop-pointer-target:1',
+ );
+
+ await page.mouseClick(desktopTarget.x, desktopTarget.y);
+ await Bun.sleep(250);
+ expect(await page.readStudioSmokeState()).toBe(
+ 'mode=edit;selection=desktop-pointer-target;changes=1',
+ );
+ expect(await page.readSelectionRootId()).toBe(
+ 'studio-stationary-selection-root:edit:desktop-pointer-target:1',
+ );
+
+ const movedTarget = await page.readRuntimeNodeCenter('dashboard-runtime-row-0');
+ await page.evaluate(`(() => {
+ globalThis.__studioSmokeInputTrace = [];
+ for (const type of ['touchstart', 'touchend', 'pointerdown', 'pointerup', 'click']) {
+ document.addEventListener(type, (event) => {
+ globalThis.__studioSmokeInputTrace.push({
+ type,
+ pointerType: event.pointerType ?? null,
+ targetId: event.target?.id ?? null,
+ targetTestId: event.target?.getAttribute?.('data-testid') ?? null,
+ });
+ }, { capture: true, once: true });
+ }
+ return true;
+ })()`);
+ await page.touchTap(movedTarget.x, movedTarget.y);
+ await Bun.sleep(500);
+ const touchState = await page.readStudioSmokeState();
+ if (touchState !== 'mode=edit;selection=dashboard-runtime-row-0;changes=2') {
+ throw new Error(
+ `Touch selection did not commit; state=${touchState}; trace=${await page.evaluate(
+ `JSON.stringify(globalThis.__studioSmokeInputTrace ?? [])`,
+ )}`,
+ );
+ }
+ expect(await page.readSelectionRootId()).toBe(
+ 'studio-stationary-selection-root:edit:dashboard-runtime-row-0:2',
+ );
+
+ await page.mouseDrag(movedTarget.x, movedTarget.y, movedTarget.x + 24, movedTarget.y + 24);
+ await Bun.sleep(250);
+ expect(await page.readStudioSmokeState()).toBe(
+ 'mode=edit;selection=dashboard-runtime-row-0;changes=2',
+ );
+ expect(await page.readSelectionRootId()).toBe(
+ 'studio-stationary-selection-root:edit:dashboard-runtime-row-0:2',
+ );
+
+ await page.evaluate(`globalThis.__studioSmokeTogglePreview?.()`);
+ await waitForStudioSmokeState(
+ page,
+ 'mode=preview;selection=dashboard-runtime-row-0;changes=2',
+ 15_000,
+ );
+ expect(
+ await page.evaluate(
+ `document.getElementById('studio-unsupported-indicator-unsupported-runtime-target') === null`,
+ ),
+ ).toBe(true);
+
+ await page.mouseClick(desktopTarget.x, desktopTarget.y);
+ await Bun.sleep(250);
+ expect(await page.readStudioSmokeState()).toBe(
+ 'mode=preview;selection=dashboard-runtime-row-0;changes=2',
+ );
+}
+
+async function waitForUnsupportedGeometry(
+ page: ChromePage,
+ timeoutMs: number,
+): Promise {
+ const start = Date.now();
+
+ while (Date.now() - start < timeoutMs) {
+ const snapshot = await page.readUnsupportedGeometry();
+ if (snapshot) {
+ return snapshot;
+ }
+ await Bun.sleep(100);
+ }
+
+ throw new Error(
+ `Timed out waiting for unsupported Runtime indication geometry.\n${await page.readUnsupportedGeometryDiagnostics()}\nChrome errors:\n${page.errors.join('\n')}`,
+ );
+}
+
+async function waitForStudioSmokeState(
+ page: ChromePage,
+ expected: string,
+ timeoutMs: number,
+): Promise {
+ const start = Date.now();
+
+ while (Date.now() - start < timeoutMs) {
+ if ((await page.readStudioSmokeState()) === expected) {
+ return;
+ }
+ await Bun.sleep(100);
+ }
+
+ throw new Error(
+ `Timed out waiting for Studio smoke state ${expected}; received ${await page.readStudioSmokeState()}.`,
+ );
+}
+
+function expectRectToMatch(actual: BrowserRect, expected: BrowserRect): void {
+ expect(Math.abs(actual.left - expected.left)).toBeLessThanOrEqual(2);
+ expect(Math.abs(actual.top - expected.top)).toBeLessThanOrEqual(2);
+ expect(Math.abs(actual.width - expected.width)).toBeLessThanOrEqual(2);
+ expect(Math.abs(actual.height - expected.height)).toBeLessThanOrEqual(2);
+}
+
+function rectsOverlap(left: BrowserRect, right: BrowserRect): boolean {
+ return (
+ Math.min(left.right, right.right) > Math.max(left.left, right.left) &&
+ Math.min(left.bottom, right.bottom) > Math.max(left.top, right.top)
+ );
+}
+
async function createGeneratedAdminProject(workspaceRoot: string): Promise {
await mkdir(path.join(workspaceRoot, 'apps', 'studio'), { recursive: true });
await writeFile(
@@ -202,7 +460,6 @@ async function createGeneratedAdminProject(workspaceRoot: string): Promise {
+ const packagePath = path.join(projectRoot, 'package.json');
+ const generatedPackage = JSON.parse(await readFile(packagePath, 'utf8')) as {
+ dependencies?: Record;
+ };
+ if (!generatedPackage.dependencies) {
+ throw new Error('Generated smoke project has no dependency map.');
+ }
+ generatedPackage.dependencies['@ankhorage/studio'] =
+ `file:${await stageLocalStudioPackage(workspaceRoot)}`;
+ await writeFile(packagePath, `${JSON.stringify(generatedPackage, null, 2)}\n`, 'utf8');
+
+ const install = Bun.spawn(['bun', 'install', '--ignore-scripts'], {
+ cwd: workspaceRoot,
+ env: {
+ ...process.env,
+ CI: '1',
+ },
+ stdout: 'pipe',
+ stderr: 'pipe',
+ });
+ const [exitCode, stdout, stderr] = await Promise.all([
+ install.exited,
+ new Response(install.stdout).text(),
+ new Response(install.stderr).text(),
+ ]);
+ if (exitCode !== 0) {
+ throw new Error(`Generated smoke dependency install failed.\n${stdout}\n${stderr}`);
+ }
+}
+
+async function stageLocalStudioPackage(workspaceRoot: string): Promise {
+ const sourcePackage = JSON.parse(
+ await readFile(path.join(process.cwd(), 'package.json'), 'utf8'),
+ ) as Record;
+ const packageRoot = path.join(workspaceRoot, 'packages', 'studio');
+ await mkdir(packageRoot, { recursive: true });
+ await cp(path.join(process.cwd(), 'dist'), path.join(packageRoot, 'dist'), {
+ recursive: true,
+ });
+ await writeFile(
+ path.join(packageRoot, 'package.json'),
+ `${JSON.stringify(
+ {
+ name: sourcePackage.name,
+ version: sourcePackage.version,
+ type: sourcePackage.type,
+ main: sourcePackage.main,
+ exports: sourcePackage.exports,
+ dependencies: sourcePackage.dependencies,
+ peerDependencies: sourcePackage.peerDependencies,
+ },
+ null,
+ 2,
+ )}\n`,
+ 'utf8',
+ );
+ return packageRoot;
+}
+
+async function writeSmokeRuntimeExtensions(projectRoot: string): Promise {
+ const generatedRoot = path.join(projectRoot, 'src', 'generated');
+ await writeFile(
+ path.join(generatedRoot, 'SmokeStudioComponents.tsx'),
+ `import { useStudio } from '@ankhorage/studio';
+import { Box, Text } from '@ankhorage/zora';
+import React, { useEffect, useRef, useState } from 'react';
+import { View } from 'react-native';
+
+export function SmokeUnsupported({
+ label,
+ testID,
+}: {
+ label?: string;
+ testID?: string;
+}) {
+ return (
+
+ {label ?? 'Unsupported runtime target'}
+
+ );
+}
+
+export function SmokeStudioProbe() {
+ const studio = useStudio();
+ const previousSelectedNodeIdRef = useRef(studio.selectedNodeId);
+ const [selectionChangeCount, setSelectionChangeCount] = useState(0);
+
+ useEffect(() => {
+ const smokeGlobal = globalThis as typeof globalThis & {
+ __studioSmokeTogglePreview?: () => void;
+ };
+ smokeGlobal.__studioSmokeTogglePreview = studio.togglePreviewMode;
+ return () => {
+ delete smokeGlobal.__studioSmokeTogglePreview;
+ };
+ }, [studio.togglePreviewMode]);
+
+ useEffect(() => {
+ if (previousSelectedNodeIdRef.current === studio.selectedNodeId) return;
+ previousSelectedNodeIdRef.current = studio.selectedNodeId;
+ setSelectionChangeCount((count) => count + 1);
+ }, [studio.selectedNodeId]);
+
+ return (
+
+
+ {\`mode=\${studio.previewMode ? 'preview' : 'edit'};selection=\${studio.selectedNodeId ?? 'none'};changes=\${selectionChangeCount}\`}
+
+
+ );
+}
+`,
+ 'utf8',
+ );
+ await writeFile(
+ path.join(generatedRoot, 'appExtensionRegistry.ts'),
+ `import type { ComponentRegistry } from '@ankhorage/runtime';
+import { SmokeStudioProbe, SmokeUnsupported } from './SmokeStudioComponents';
+
+export const APP_EXTENSION_COMPONENT_REGISTRY: ComponentRegistry = {
+ SmokeStudioProbe,
+ SmokeUnsupported,
+};
+
+export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {} as const;
+`,
+ 'utf8',
+ );
+}
+
async function writeSmokeMetroConfig(projectRoot: string): Promise {
const repositoryRoot = process.cwd();
await writeFile(
@@ -421,84 +821,6 @@ async function waitForExpoWebUrl(output: readonly string[], timeoutMs: number):
throw new Error(`Timed out waiting for Expo web URL.${formatProcessOutput(output)}`);
}
-async function linkSmokeNodeModules(workspaceRoot: string): Promise {
- const nodeModulesRoot = path.join(workspaceRoot, 'node_modules');
- await mkdir(nodeModulesRoot, { recursive: true });
- await linkNodeModuleEntries({
- sourceRoot: path.join(process.cwd(), 'node_modules', '.bun', 'node_modules'),
- targetRoot: nodeModulesRoot,
- });
- await linkNodeModuleEntries({
- sourceRoot: path.join(process.cwd(), 'apps', 'studio', 'node_modules'),
- targetRoot: nodeModulesRoot,
- });
-}
-
-async function linkNodeModuleEntries(args: {
- readonly sourceRoot: string;
- readonly targetRoot: string;
- readonly packageNames?: readonly string[];
-}): Promise {
- const packageNames = args.packageNames ?? (await readdir(args.sourceRoot));
- for (const packageName of packageNames) {
- if (packageName.startsWith('.')) continue;
- if (packageName.startsWith('@') && packageName.includes('/')) {
- const [scopeName, scopedPackageName] = packageName.split('/');
- if (!scopeName || !scopedPackageName) {
- throw new Error(`Invalid scoped package name: ${packageName}`);
- }
- const targetScopeRoot = path.join(args.targetRoot, scopeName);
- await mkdir(targetScopeRoot, { recursive: true });
- await symlinkIfMissing(
- path.join(args.sourceRoot, scopeName, scopedPackageName),
- path.join(targetScopeRoot, scopedPackageName),
- );
- continue;
- }
- if (packageName.startsWith('@')) {
- await linkScopedNodeModuleEntries({
- sourceRoot: path.join(args.sourceRoot, packageName),
- targetRoot: path.join(args.targetRoot, packageName),
- scopeName: packageName,
- });
- continue;
- }
-
- await symlinkIfMissing(
- path.join(args.sourceRoot, packageName),
- path.join(args.targetRoot, packageName),
- );
- }
-}
-
-async function linkScopedNodeModuleEntries(args: {
- readonly sourceRoot: string;
- readonly targetRoot: string;
- readonly scopeName: string;
-}): Promise {
- await mkdir(args.targetRoot, { recursive: true });
- const packageNames = await readdir(args.sourceRoot);
- for (const packageName of packageNames) {
- await symlinkIfMissing(
- path.join(args.sourceRoot, packageName),
- path.join(args.targetRoot, packageName),
- );
- }
-}
-
-async function symlinkIfMissing(source: string, target: string): Promise {
- try {
- await symlink(await realpath(source), target, 'dir');
- } catch (error) {
- if (isNodeErrorWithCode(error, 'EEXIST')) return;
- throw error;
- }
-}
-
-function isNodeErrorWithCode(error: unknown, code: string): boolean {
- return isRecord(error) && error.code === code;
-}
-
async function waitForHttp(
url: string,
timeoutMs: number,
@@ -613,6 +935,206 @@ class ChromePage {
return typeof value === 'string' ? value : '';
}
+ async evaluate(expression: string): Promise {
+ const result = await this.send('Runtime.evaluate', {
+ expression,
+ returnByValue: true,
+ awaitPromise: true,
+ });
+ if (!isRecord(result) || !isRecord(result.result)) {
+ throw new Error(`Chrome evaluation returned no value for: ${expression}`);
+ }
+ return ('value' in result.result ? result.result.value : undefined) as T;
+ }
+
+ async blockHotReloadConnections(): Promise {
+ await this.send('Network.enable');
+ await this.send('Network.setBlockedURLs', {
+ urls: ['ws://127.0.0.1:*/hot*', 'ws://localhost:*/hot*'],
+ });
+ await this.send('Emulation.setDeviceMetricsOverride', {
+ width: 1280,
+ height: 900,
+ deviceScaleFactor: 1,
+ mobile: true,
+ });
+ await this.send('Emulation.setTouchEmulationEnabled', {
+ enabled: true,
+ maxTouchPoints: 1,
+ });
+ }
+
+ async readStudioSmokeState(): Promise {
+ return this.evaluate(
+ `document.querySelector('[data-testid="studio-smoke-state"]')?.textContent ?? ''`,
+ );
+ }
+
+ async readSelectionRootId(): Promise {
+ return this.evaluate(
+ `document.querySelector('[data-testid="studio-stationary-selection-root"]')?.id ?? ''`,
+ );
+ }
+
+ async readRuntimeNodeCenter(nodeId: string): Promise<{ readonly x: number; readonly y: number }> {
+ const encodedNodeId = encodeURIComponent(nodeId);
+ const center = await this.evaluate<{ readonly x: number; readonly y: number } | null>(`(() => {
+ const wrapper = document.getElementById(${JSON.stringify(
+ `studio-runtime-node-${encodedNodeId}`,
+ )});
+ const findRenderedElement = (element) => {
+ const queue = element ? [...element.children] : [];
+ while (queue.length > 0) {
+ const candidate = queue.shift();
+ if (!(candidate instanceof HTMLElement)) continue;
+ const rect = candidate.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) return candidate;
+ queue.unshift(...candidate.children);
+ }
+ return null;
+ };
+ const target = findRenderedElement(wrapper);
+ if (!(target instanceof HTMLElement)) return null;
+ const rect = target.getBoundingClientRect();
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
+ })()`);
+ if (!center) {
+ throw new Error(`Could not resolve browser center for Runtime node ${nodeId}.`);
+ }
+ return center;
+ }
+
+ async readUnsupportedGeometry(): Promise {
+ return this.evaluate(`(() => {
+ const wrapper = document.getElementById(
+ 'studio-runtime-node-unsupported-runtime-target',
+ );
+ const findRenderedElement = (element) => {
+ const queue = element ? [...element.children] : [];
+ while (queue.length > 0) {
+ const candidate = queue.shift();
+ if (!(candidate instanceof HTMLElement)) continue;
+ const rect = candidate.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) return candidate;
+ queue.unshift(...candidate.children);
+ }
+ return null;
+ };
+ const target = findRenderedElement(wrapper);
+ const indicator = document.getElementById(
+ 'studio-unsupported-indicator-unsupported-runtime-target',
+ );
+ const neighborWrapper = document.getElementById(
+ 'studio-runtime-node-supported-runtime-neighbor',
+ );
+ const neighbor = findRenderedElement(neighborWrapper);
+ if (
+ !(target instanceof HTMLElement) ||
+ !(indicator instanceof HTMLElement) ||
+ !(neighbor instanceof HTMLElement)
+ ) {
+ return null;
+ }
+ const toRect = (element) => {
+ const rect = element.getBoundingClientRect();
+ return {
+ left: rect.left,
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ width: rect.width,
+ height: rect.height,
+ };
+ };
+ return {
+ target: toRect(target),
+ indicator: toRect(indicator),
+ neighbor: toRect(neighbor),
+ pointerEvents: getComputedStyle(indicator).pointerEvents,
+ };
+ })()`);
+ }
+
+ async readUnsupportedGeometryDiagnostics(): Promise {
+ return this.evaluate(`JSON.stringify({
+ state: document.querySelector('[data-testid="studio-smoke-state"]')?.textContent ?? null,
+ root: document.querySelector('[data-testid="studio-stationary-selection-root"]')?.outerHTML.slice(0, 300) ?? null,
+ roots: [...document.querySelectorAll('[data-testid="studio-stationary-selection-root"]')].map((element) => element.id),
+ wrapper: document.getElementById('studio-runtime-node-unsupported-runtime-target')?.outerHTML.slice(0, 300) ?? null,
+ wrapperRoot: document.getElementById('studio-runtime-node-unsupported-runtime-target')?.closest('[data-testid="studio-stationary-selection-root"]')?.id ?? null,
+ unsupportedRecorders: [...document.querySelectorAll('[data-testid^="studio-unsupported-recorder-"]')].map((element) => element.getAttribute('data-testid')),
+ indicator: document.getElementById('studio-unsupported-indicator-unsupported-runtime-target')?.outerHTML ?? null,
+ neighbor: document.getElementById('studio-runtime-node-supported-runtime-neighbor')?.outerHTML.slice(0, 300) ?? null,
+ })`);
+ }
+
+ async mouseClick(x: number, y: number): Promise {
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mouseMoved',
+ x,
+ y,
+ });
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mousePressed',
+ x,
+ y,
+ button: 'left',
+ buttons: 1,
+ clickCount: 1,
+ });
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mouseReleased',
+ x,
+ y,
+ button: 'left',
+ buttons: 0,
+ clickCount: 1,
+ });
+ }
+
+ async mouseDrag(startX: number, startY: number, endX: number, endY: number): Promise {
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mouseMoved',
+ x: startX,
+ y: startY,
+ });
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mousePressed',
+ x: startX,
+ y: startY,
+ button: 'left',
+ buttons: 1,
+ clickCount: 1,
+ });
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mouseMoved',
+ x: endX,
+ y: endY,
+ button: 'left',
+ buttons: 1,
+ });
+ await this.send('Input.dispatchMouseEvent', {
+ type: 'mouseReleased',
+ x: endX,
+ y: endY,
+ button: 'left',
+ buttons: 0,
+ clickCount: 1,
+ });
+ }
+
+ async touchTap(x: number, y: number): Promise {
+ await this.send('Input.dispatchTouchEvent', {
+ type: 'touchStart',
+ touchPoints: [{ x, y, radiusX: 1, radiusY: 1, force: 1, id: 1 }],
+ });
+ await Bun.sleep(100);
+ await this.send('Input.dispatchTouchEvent', {
+ type: 'touchEnd',
+ touchPoints: [],
+ });
+ }
+
send(method: string, params?: Readonly>): Promise {
const id = this.nextId;
this.nextId += 1;
diff --git a/src/host/layout/layoutGenerator.test.ts b/src/host/layout/layoutGenerator.test.ts
index 2cc607f5..98f9a1c2 100644
--- a/src/host/layout/layoutGenerator.test.ts
+++ b/src/host/layout/layoutGenerator.test.ts
@@ -205,13 +205,21 @@ describe('GeneratedAppFileGenerator', () => {
);
expect(rootLayout).toContain('createComponentRegistry');
expect(rootLayout).toContain('STUDIO_APP_EXTENSION_COMPONENT_REGISTRY');
+ expect(rootLayout).toContain('STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT');
expect(rootLayout).toContain(
- "import { APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY } from '@/generated/appExtensionRegistry';",
+ 'APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY',
+ );
+ expect(rootLayout).toContain(
+ 'APP_EXTENSION_INTERACTION_POLICY_SUPPORT as GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT',
);
expect(rootLayout).toContain(`const APP_EXTENSION_COMPONENT_REGISTRY = createComponentRegistry(
STUDIO_APP_EXTENSION_COMPONENT_REGISTRY,
GENERATED_APP_EXTENSION_COMPONENT_REGISTRY,
);`);
+ expect(rootLayout).toContain(`const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {
+ ...STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
+ ...GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
+} as const;`);
expect(rootLayout).toContain(`const runtimeComponentRegistry = createComponentRegistry(
ZORA_COMPONENT_REGISTRY,
APP_EXTENSION_COMPONENT_REGISTRY,
diff --git a/src/host/layout/layoutGenerator.ts b/src/host/layout/layoutGenerator.ts
index 3edcdd8e..2034bd32 100644
--- a/src/host/layout/layoutGenerator.ts
+++ b/src/host/layout/layoutGenerator.ts
@@ -53,16 +53,24 @@ function getPackageOwnedRuntimeImports(): string {
} from '@ankhorage/runtime';
import {
STUDIO_APP_EXTENSION_COMPONENT_REGISTRY,
+ STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
useRuntimeAction,
} from '@ankhorage/studio/runtime';
-import { APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY } from '@/generated/appExtensionRegistry';`;
+import {
+ APP_EXTENSION_COMPONENT_REGISTRY as GENERATED_APP_EXTENSION_COMPONENT_REGISTRY,
+ APP_EXTENSION_INTERACTION_POLICY_SUPPORT as GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
+} from '@/generated/appExtensionRegistry';`;
}
function getGeneratedRuntimeRegistryDeclarations(): string {
return `const APP_EXTENSION_COMPONENT_REGISTRY = createComponentRegistry(
STUDIO_APP_EXTENSION_COMPONENT_REGISTRY,
GENERATED_APP_EXTENSION_COMPONENT_REGISTRY,
-);`;
+);
+const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {
+ ...STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
+ ...GENERATED_APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
+} as const;`;
}
function mergeRuntimeModuleDeclarations(...declarations: readonly string[]): string {
diff --git a/src/host/layout/templates/rootLayout.test.ts b/src/host/layout/templates/rootLayout.test.ts
index 76e04c9d..fe2389eb 100644
--- a/src/host/layout/templates/rootLayout.test.ts
+++ b/src/host/layout/templates/rootLayout.test.ts
@@ -154,7 +154,7 @@ test('generates a root stationary tap selector for edit mode and excludes old Pr
expect(generated).toContain('wrapNode: studioWrapNode');
expect(generated).toContain('selectedNodeId');
expect(generated).toContain('selectNode');
- expect(generated).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT');
+ expect(generated).toContain('APP_EXTENSION_INTERACTION_POLICY_SUPPORT');
expect(generated).toContain('ZORA_COMPONENT_REGISTRY');
expect(generated).not.toContain(' {
expect(studioShellSource).toContain('const studioRuntimeConfig = useMemo(');
expect(studioShellSource).toContain('const studioWrapNode = useMemo(');
expect(studioShellSource).toContain('createStudioStationarySelectionWrapNode');
- expect(studioShellSource).toContain('STUDIO_EMPTY_THIRD_PARTY_SUPPORT');
+ expect(studioShellSource).toContain('APP_EXTENSION_INTERACTION_POLICY_SUPPORT');
expect(studioShellSource).toContain('const studioResolveNodeProps = useMemo(');
expect(studioShellSource).toContain('createStudioInteractionPolicyResolver');
expect(studioShellSource).toContain('disableActions: !previewMode');
diff --git a/src/host/layout/templates/rootLayout.ts b/src/host/layout/templates/rootLayout.ts
index 93d11df4..e0602c34 100644
--- a/src/host/layout/templates/rootLayout.ts
+++ b/src/host/layout/templates/rootLayout.ts
@@ -436,7 +436,6 @@ function resolveRuntimeOperationCredential(credential: { readonly kind?: string
runtimeOperationHelpers.trim(),
authRuntimeConstants.trim(),
includeStudio ? appHeaderHelpers.trim() : '',
- includeStudio ? 'const STUDIO_EMPTY_THIRD_PARTY_SUPPORT = {} as const;' : '',
innerNavigation.declarations.trim(),
]
.filter(Boolean)
@@ -620,7 +619,7 @@ function StudioShell({
() =>
createStudioStationarySelectionWrapNode({
previewMode,
- thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT,
+ thirdPartySupport: APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
}),
[previewMode],
);
@@ -628,7 +627,7 @@ function StudioShell({
() =>
createStudioInteractionPolicyResolver({
previewMode,
- thirdPartySupport: STUDIO_EMPTY_THIRD_PARTY_SUPPORT,
+ thirdPartySupport: APP_EXTENSION_INTERACTION_POLICY_SUPPORT,
}),
[previewMode],
);
diff --git a/src/host/orchestrator/generatedAppFiles.test.ts b/src/host/orchestrator/generatedAppFiles.test.ts
new file mode 100644
index 00000000..767426a4
--- /dev/null
+++ b/src/host/orchestrator/generatedAppFiles.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from 'bun:test';
+
+import { createGeneratedAppExtensionRegistrySource } from './generatedAppFiles';
+
+describe('generated app extension interaction-policy support', () => {
+ it('emits an explicit support map beside the generated component registry', () => {
+ const source = createGeneratedAppExtensionRegistrySource({
+ usesExpoBarcodeScannerAdapter: false,
+ zoraExtensions: [
+ {
+ packageName: '@example/widgets',
+ components: {
+ DeclaredWidget: 'DeclaredWidget',
+ RegisteredOnlyWidget: 'RegisteredOnlyWidget',
+ },
+ interactionPolicySupportedComponents: ['DeclaredWidget'],
+ },
+ ],
+ });
+
+ expect(source).toContain('DeclaredWidget: DeclaredWidget');
+ expect(source).toContain('RegisteredOnlyWidget: RegisteredOnlyWidget');
+ expect(source).toContain('export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {');
+ expect(source).toContain('DeclaredWidget: true');
+ expect(source).not.toContain('RegisteredOnlyWidget: true');
+ });
+
+ it('rejects support declarations for node types absent from the registry', () => {
+ expect(() =>
+ createGeneratedAppExtensionRegistrySource({
+ usesExpoBarcodeScannerAdapter: false,
+ zoraExtensions: [
+ {
+ packageName: '@example/widgets',
+ components: { RegisteredWidget: 'RegisteredWidget' },
+ interactionPolicySupportedComponents: ['MissingWidget'],
+ },
+ ],
+ }),
+ ).toThrow('declares interaction-policy support for unregistered component MissingWidget');
+ });
+
+ it('emits an empty declaration instead of inferring support from registry membership', () => {
+ const source = createGeneratedAppExtensionRegistrySource({
+ usesExpoBarcodeScannerAdapter: false,
+ zoraExtensions: [
+ {
+ packageName: '@example/widgets',
+ components: { RegisteredOnlyWidget: 'RegisteredOnlyWidget' },
+ },
+ ],
+ });
+
+ expect(source).toMatch(
+ /export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = \{\s*\} as const;/,
+ );
+ expect(source).not.toContain('RegisteredOnlyWidget: true');
+ });
+});
diff --git a/src/host/orchestrator/generatedAppFiles.ts b/src/host/orchestrator/generatedAppFiles.ts
index 7f8aaac0..8ad34d83 100644
--- a/src/host/orchestrator/generatedAppFiles.ts
+++ b/src/host/orchestrator/generatedAppFiles.ts
@@ -57,16 +57,26 @@ export async function syncGeneratedAppFiles(
await assertNoForbiddenSpecifiers(targetProjectPath);
}
-function createGeneratedAppExtensionRegistrySource(args: {
+export function createGeneratedAppExtensionRegistrySource(args: {
usesExpoBarcodeScannerAdapter: boolean;
zoraExtensions: readonly ZoraExtensionDefinition[];
}): string {
const importLines = new Set();
+ const interactionPolicySupportedComponents = new Set();
const entries = args.zoraExtensions
.flatMap((extension) => {
const exportNames = Array.from(new Set(Object.values(extension.components))).sort();
importLines.add(`import { ${exportNames.join(', ')} } from '${extension.packageName}';`);
+ for (const componentName of extension.interactionPolicySupportedComponents ?? []) {
+ if (!Object.prototype.hasOwnProperty.call(extension.components, componentName)) {
+ throw new Error(
+ `ZORA extension ${extension.packageName} declares interaction-policy support for unregistered component ${componentName}.`,
+ );
+ }
+ interactionPolicySupportedComponents.add(componentName);
+ }
+
return Object.entries(extension.components).map(([componentName, exportName]) => ({
componentName,
exportName,
@@ -85,6 +95,10 @@ function createGeneratedAppExtensionRegistrySource(args: {
const registryEntries = entries
.map(({ componentName, exportName }) => ` ${formatRegistryKey(componentName)}: ${exportName},`)
.join('\n');
+ const interactionPolicySupportEntries = [...interactionPolicySupportedComponents]
+ .sort()
+ .map((componentName) => ` ${formatRegistryKey(componentName)}: true,`)
+ .join('\n');
return [
"import type { ComponentRegistry } from '@ankhorage/runtime';",
@@ -94,6 +108,10 @@ function createGeneratedAppExtensionRegistrySource(args: {
registryEntries,
'};',
'',
+ 'export const APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {',
+ interactionPolicySupportEntries,
+ '} as const;',
+ '',
]
.filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length !== 0)
.join('\n');
diff --git a/src/host/orchestrator/templates.test.ts b/src/host/orchestrator/templates.test.ts
index ceddaa75..4080d4bd 100644
--- a/src/host/orchestrator/templates.test.ts
+++ b/src/host/orchestrator/templates.test.ts
@@ -12,12 +12,19 @@ describe('generated OAuth scaffold templates', () => {
expect(dependencies['@ankhorage/contracts']).toBe('^4.0.0');
expect(dependencies['@ankhorage/runtime']).toBe('^0.3.0');
- expect(dependencies['@ankhorage/zora']).toBe('^2.8.7');
+ expect(dependencies['@ankhorage/zora']).toBe('^2.9.0');
expect(dependencies['@ankhorage/supabase-auth']).toBe('^1.0.0');
expect(dependencies['expo-secure-store']).toBe('~15.0.8');
expect(dependencies['expo-web-browser']).toBe('~15.0.11');
});
+ it('requires the first ZORA release that enforces interactionPolicy', () => {
+ const pkg = getPackageJson({ name: 'studio-enabled-app', includeStudio: true });
+ const dependencies = pkg.dependencies as Record;
+
+ expect(dependencies['@ankhorage/zora']).toBe('^2.9.0');
+ });
+
it('omits OAuth-specific packages when auth is not generated', () => {
const pkg = getPackageJson({ name: 'public-app' });
const dependencies = pkg.dependencies as Record;
diff --git a/src/host/orchestrator/templates.ts b/src/host/orchestrator/templates.ts
index f4f67b6c..18433aaf 100644
--- a/src/host/orchestrator/templates.ts
+++ b/src/host/orchestrator/templates.ts
@@ -12,7 +12,7 @@ const EXPO_MODULES_CORE_VERSION = '~3.0.30';
const CONTRACTS_VERSION = '^4.0.0';
const RUNTIME_VERSION = '^0.3.0';
const SUPABASE_AUTH_VERSION = '^1.0.0';
-const ZORA_VERSION = '^2.8.7';
+const ZORA_VERSION = '^2.9.0';
const EXPO_SECURE_STORE_VERSION = '~15.0.8';
const EXPO_WEB_BROWSER_VERSION = '~15.0.11';
diff --git a/src/host/zoraExtensions/index.ts b/src/host/zoraExtensions/index.ts
index 1c0015f3..48b06b42 100644
--- a/src/host/zoraExtensions/index.ts
+++ b/src/host/zoraExtensions/index.ts
@@ -4,6 +4,7 @@ export interface ZoraExtensionDefinition {
packageName: string;
components: Record;
dependencies?: Record;
+ interactionPolicySupportedComponents?: readonly string[];
}
const ZORA_CHESS_EXTENSION = {
diff --git a/src/runtime/appExtensionRegistry.ts b/src/runtime/appExtensionRegistry.ts
index d385f9a3..2fdc8c02 100644
--- a/src/runtime/appExtensionRegistry.ts
+++ b/src/runtime/appExtensionRegistry.ts
@@ -7,3 +7,10 @@ export const STUDIO_APP_EXTENSION_COMPONENT_REGISTRY: ComponentRegistry = {
OpeningBook,
TabletopTable,
};
+
+/**
+ * Exact Studio-owned extension node types that accept and enforce interactionPolicy.
+ *
+ * Registry membership alone does not opt a component in.
+ */
+export const STUDIO_APP_EXTENSION_INTERACTION_POLICY_SUPPORT = {} as const;
diff --git a/src/runtime/interactionPolicy.test.ts b/src/runtime/interactionPolicy.test.ts
index f46622c4..de387462 100644
--- a/src/runtime/interactionPolicy.test.ts
+++ b/src/runtime/interactionPolicy.test.ts
@@ -3,7 +3,11 @@ import { join } from 'node:path';
import { describe, expect, it } from 'bun:test';
-import { createInteractionPolicyResolver, isComponentSupported } from './interactionPolicyCore';
+import {
+ composeInteractionPolicyResolver,
+ createInteractionPolicyResolver,
+ isComponentSupported,
+} from './interactionPolicyCore';
describe('interactionPolicyCore', () => {
it('assigns passive policy to supported components in Edit mode', () => {
@@ -103,6 +107,21 @@ describe('interactionPolicyCore', () => {
expect(result.interactionPolicy).toBeUndefined();
});
+ it('does not infer support from third-party registry membership', () => {
+ const registeredThirdPartyTypes = new Set(['RegisteredOnlyComponent']);
+ const resolver = createInteractionPolicyResolver({
+ previewMode: false,
+ isSupportedNodeType: () => false,
+ });
+ const result = resolver({
+ node: { type: 'RegisteredOnlyComponent' },
+ props: { registered: registeredThirdPartyTypes.has('RegisteredOnlyComponent') },
+ });
+
+ expect(result.registered).toBe(true);
+ expect(result.interactionPolicy).toBeUndefined();
+ });
+
it('assigns no policy to unknown types', () => {
const resolver = createInteractionPolicyResolver({
previewMode: false,
@@ -164,6 +183,29 @@ describe('interactionPolicyCore', () => {
it('isComponentSupported returns false for unsupported', () => {
expect(isComponentSupported('Unknown', {}, () => false)).toBe(false);
});
+
+ it('preserves existing Runtime resolver output while adding policy', () => {
+ const policyResolver = createInteractionPolicyResolver({
+ previewMode: false,
+ thirdPartySupport: { CustomWidget: true },
+ isSupportedNodeType: () => false,
+ });
+ const resolver = composeInteractionPolicyResolver(policyResolver, ({ props }) => ({
+ ...props,
+ existingResolver: 'preserved',
+ }));
+
+ expect(
+ resolver({
+ node: { type: 'CustomWidget' },
+ props: { authored: 'preserved' },
+ }),
+ ).toEqual({
+ authored: 'preserved',
+ existingResolver: 'preserved',
+ interactionPolicy: 'passive',
+ });
+ });
});
describe('interactionPolicy production adapter', () => {
@@ -180,5 +222,21 @@ describe('interactionPolicy production adapter', () => {
it('delegates classification to the pure core', () => {
expect(source).toContain('createInteractionPolicyResolver');
+ expect(source).toContain('composeInteractionPolicyResolver');
+ });
+});
+
+describe('preview Runtime config production wiring', () => {
+ const sourcePath = join(import.meta.dir, 'previewRuntimeConfig.ts');
+ const source = readFileSync(sourcePath, 'utf8');
+
+ it('threads support and inherited resolver options independently from registry membership', () => {
+ expect(source).toContain('readonly components?: ComponentRegistry;');
+ expect(source).toContain('readonly interactionPolicySupport?: ThirdPartyComponentSupport;');
+ expect(source).toContain(
+ "readonly resolveNodeProps?: RuntimeRendererConfig['resolveNodeProps'];",
+ );
+ expect(source).toContain('thirdPartySupport: options.interactionPolicySupport');
+ expect(source).toContain('existingResolver: options.resolveNodeProps');
});
});
diff --git a/src/runtime/interactionPolicy.ts b/src/runtime/interactionPolicy.ts
index 26669dda..a3394094 100644
--- a/src/runtime/interactionPolicy.ts
+++ b/src/runtime/interactionPolicy.ts
@@ -2,7 +2,10 @@ import { type RuntimeNodePropsResolver } from '@ankhorage/runtime';
import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora';
import type { ThirdPartyComponentSupport } from './interactionPolicyCore.js';
-import { createInteractionPolicyResolver } from './interactionPolicyCore.js';
+import {
+ composeInteractionPolicyResolver,
+ createInteractionPolicyResolver,
+} from './interactionPolicyCore.js';
export type { InteractionPolicy, ThirdPartyComponentSupport } from './interactionPolicyCore.js';
@@ -24,22 +27,7 @@ export function createStudioInteractionPolicyResolver(
Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, nodeType),
});
- if (!existingResolver) {
- return (resolveArgs) => {
- const result = resolver({
- node: resolveArgs.node,
- props: resolveArgs.props,
- });
- return result;
- };
- }
-
- return (resolveArgs) => {
- const baseProps = existingResolver(resolveArgs);
- const result = resolver({
- node: resolveArgs.node,
- props: baseProps,
- });
- return result;
- };
+ return existingResolver
+ ? composeInteractionPolicyResolver(resolver, existingResolver)
+ : composeInteractionPolicyResolver(resolver);
}
diff --git a/src/runtime/interactionPolicyCore.ts b/src/runtime/interactionPolicyCore.ts
index 8337c9c5..a46db62b 100644
--- a/src/runtime/interactionPolicyCore.ts
+++ b/src/runtime/interactionPolicyCore.ts
@@ -20,7 +20,10 @@ function classifyComponent(
return 'zora-builtin';
}
- if (Object.prototype.hasOwnProperty.call(thirdPartySupport, nodeType)) {
+ if (
+ Object.prototype.hasOwnProperty.call(thirdPartySupport, nodeType) &&
+ thirdPartySupport[nodeType] === true
+ ) {
return 'third-party-supported';
}
@@ -52,12 +55,17 @@ function resolveInteractionPolicy(
const POLICY_PROP = 'interactionPolicy';
+export interface InteractionPolicyResolveArgs {
+ readonly node: TNode;
+ readonly props: Record;
+}
+
+export type InteractionPolicyNodePropsResolver =
+ (resolveArgs: InteractionPolicyResolveArgs) => Record;
+
export function createInteractionPolicyResolver(
args: InteractionPolicyResolverArgs,
-): (resolveArgs: {
- node: { type: string };
- props: Record;
-}) => Record {
+): InteractionPolicyNodePropsResolver {
const { previewMode, thirdPartySupport = EMPTY_THIRD_PARTY, isSupportedNodeType } = args;
return (resolveArgs) => {
@@ -80,3 +88,25 @@ export function createInteractionPolicyResolver(
};
};
}
+
+export function composeInteractionPolicyResolver(
+ interactionPolicyResolver: InteractionPolicyNodePropsResolver,
+): InteractionPolicyNodePropsResolver;
+export function composeInteractionPolicyResolver(
+ interactionPolicyResolver: InteractionPolicyNodePropsResolver,
+ existingResolver: InteractionPolicyNodePropsResolver,
+): InteractionPolicyNodePropsResolver;
+export function composeInteractionPolicyResolver(
+ interactionPolicyResolver: InteractionPolicyNodePropsResolver,
+ existingResolver?: InteractionPolicyNodePropsResolver,
+): InteractionPolicyNodePropsResolver {
+ if (!existingResolver) {
+ return interactionPolicyResolver;
+ }
+
+ return (resolveArgs) =>
+ interactionPolicyResolver({
+ node: resolveArgs.node,
+ props: existingResolver(resolveArgs),
+ });
+}
diff --git a/src/runtime/previewRuntimeConfig.ts b/src/runtime/previewRuntimeConfig.ts
index 5d9135d9..0d040e27 100644
--- a/src/runtime/previewRuntimeConfig.ts
+++ b/src/runtime/previewRuntimeConfig.ts
@@ -2,11 +2,14 @@ import type { ComponentRegistry, RuntimeRendererConfig } from '@ankhorage/runtim
import { shouldSuppressStudioRuntimeActions } from './actionSuppression';
import { createStudioInteractionPolicyResolver } from './interactionPolicy';
+import type { ThirdPartyComponentSupport } from './interactionPolicyCore';
import { createStudioPreviewComponentRegistry } from './previewRegistry';
export interface StudioPreviewRuntimeConfigOptions {
readonly previewMode: boolean;
readonly components?: ComponentRegistry;
+ readonly interactionPolicySupport?: ThirdPartyComponentSupport;
+ readonly resolveNodeProps?: RuntimeRendererConfig['resolveNodeProps'];
}
export const createStudioPreviewRuntimeConfig = (
@@ -16,5 +19,7 @@ export const createStudioPreviewRuntimeConfig = (
registry: createStudioPreviewComponentRegistry({ components: options.components }),
resolveNodeProps: createStudioInteractionPolicyResolver({
previewMode: options.previewMode,
+ thirdPartySupport: options.interactionPolicySupport,
+ existingResolver: options.resolveNodeProps,
}),
});
diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts
index 9ea88b10..212e7c70 100644
--- a/src/runtime/stationarySelection.test.ts
+++ b/src/runtime/stationarySelection.test.ts
@@ -69,22 +69,37 @@ describe('stationarySelection RN integration', () => {
expect(source).toContain("display: 'contents'");
});
- it('preserves unsupported indicator with pointerEvents none', () => {
+ it('renders root-owned unsupported indicators with pointerEvents none', () => {
+ expect(source).toContain('...indicatorRects.map((rect) =>');
expect(source).toContain("pointerEvents: 'none'");
});
+ it('retains duplicate live wrapper measurements for the same Runtime node', () => {
+ expect(source).toContain('new Map>()');
+ expect(source).toContain('measurements.add(measure)');
+ expect(source).toContain('ref: setViewRef');
+ });
+
it('places pointerEvents as a View prop, not inside style', () => {
- const indicatorMatch = /React\.createElement\(View,\s*\{[^}]*style:\s*\{[^}]*\}[^}]*\}\)/.exec(
- source,
- );
- expect(indicatorMatch).not.toBeNull();
- const indicatorBlock = indicatorMatch?.[0] ?? '';
+ const indicatorStart = source.indexOf('...indicatorRects.map((rect) =>');
+ const indicatorBlock = source.slice(indicatorStart);
expect(indicatorBlock).toContain("pointerEvents: 'none'");
- const styleBlock = /style:\s*\{([^}]*)\}/.exec(indicatorBlock);
+ const styleBlock = /style:\s*\{([\s\S]*?)\n\s*\},/.exec(indicatorBlock);
expect(styleBlock).toBeTruthy();
expect(styleBlock?.[1]).not.toContain('pointerEvents');
});
+ it('does not position an indicator beneath the display-contents recorder', () => {
+ const recorderStart = source.indexOf('function StudioNodeTouchRecorder');
+ const wrapperFactoryStart = source.indexOf(
+ 'export function createStudioStationarySelectionWrapNode',
+ );
+ const recorderSource = source.slice(recorderStart, wrapperFactoryStart);
+
+ expect(recorderSource).not.toContain("position: 'absolute'");
+ expect(recorderSource).not.toContain('borderColor');
+ });
+
it('uses lazy coordinator creation', () => {
expect(source).toContain(
'const coordinatorRef = React.useRef(null);',
@@ -93,11 +108,24 @@ describe('stationarySelection RN integration', () => {
expect(source).not.toContain('useRef(createStationarySelectionCoordinator())');
});
- it('records touch with generation validation', () => {
+ it('records pointer and touch input with generation validation', () => {
expect(source).toContain('ctx.recordNode(nodeId)');
+ expect(source).toContain('onPointerDown: handleInteractionStart');
+ expect(source).toContain('onTouchStart: handleInteractionStart');
+ expect(source).toContain('pendingNodeIdsRef.current.push(nodeId)');
+ expect(source).toContain('for (const nodeId of pendingNodeIdsRef.current)');
+ });
+
+ it('uses public element geometry only for web indication measurement', () => {
+ expect(source).toContain("Platform.OS === 'web'");
+ expect(source).toContain('getBoundingClientRect');
+ expect(source).toContain('measureRenderedBoxes(child)');
+ expect(source).not.toContain('findNodeHandle');
+ expect(source).not.toContain('ReactNativePrivateInterface');
});
- it('uses bubble phase onTouchStart', () => {
- expect(source).toContain('onTouchStart: handleTouchStart');
+ it('continuously refreshes indication geometry across scroll and layout changes', () => {
+ expect(source).toContain('requestAnimationFrame(measureNextFrame)');
+ expect(source).toContain('cancelAnimationFrame(frameId)');
});
});
diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts
index 5c67650a..a36bc12f 100644
--- a/src/runtime/stationarySelection.ts
+++ b/src/runtime/stationarySelection.ts
@@ -1,6 +1,6 @@
import { ZORA_COMPONENT_REGISTRY } from '@ankhorage/zora';
import React from 'react';
-import { View } from 'react-native';
+import { Platform, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import type { StationarySelectionCoordinator } from './stationarySelectionCoordinator.js';
@@ -12,12 +12,142 @@ export type {
TransactionState,
} from './stationarySelectionCoordinator.js';
+interface MeasuredRect {
+ readonly x: number;
+ readonly y: number;
+ readonly width: number;
+ readonly height: number;
+}
+
+interface UnsupportedIndicatorRect extends MeasuredRect {
+ readonly nodeId: string;
+}
+
+type ViewRef = React.ElementRef;
+type MeasureUnsupportedNode = () => Promise;
+
interface TrackerContextValue {
readonly recordNode: (nodeId: string) => void;
+ readonly registerUnsupportedNode: (nodeId: string, measure: MeasureUnsupportedNode) => () => void;
}
const TrackerContext = React.createContext(null);
+interface WebRectLike {
+ readonly left: number;
+ readonly top: number;
+ readonly width: number;
+ readonly height: number;
+}
+
+interface WebElementLike {
+ readonly children: ArrayLike;
+ getBoundingClientRect(): WebRectLike;
+}
+
+function isWebElementLike(value: unknown): value is WebElementLike {
+ return (
+ typeof value === 'object' &&
+ value !== null &&
+ 'children' in value &&
+ 'getBoundingClientRect' in value &&
+ typeof value.getBoundingClientRect === 'function'
+ );
+}
+
+function toMeasuredRect(rect: WebRectLike): MeasuredRect {
+ return {
+ x: rect.left,
+ y: rect.top,
+ width: rect.width,
+ height: rect.height,
+ };
+}
+
+function unionRects(rects: readonly MeasuredRect[]): MeasuredRect | null {
+ if (rects.length === 0) {
+ return null;
+ }
+
+ const left = Math.min(...rects.map((rect) => rect.x));
+ const top = Math.min(...rects.map((rect) => rect.y));
+ const right = Math.max(...rects.map((rect) => rect.x + rect.width));
+ const bottom = Math.max(...rects.map((rect) => rect.y + rect.height));
+
+ return {
+ x: left,
+ y: top,
+ width: right - left,
+ height: bottom - top,
+ };
+}
+
+function measureRenderedBoxes(element: WebElementLike): readonly MeasuredRect[] {
+ const rect = toMeasuredRect(element.getBoundingClientRect());
+ if (rect.width > 0 && rect.height > 0) {
+ return [rect];
+ }
+
+ return Array.from(element.children).flatMap((child) => measureRenderedBoxes(child));
+}
+
+function measureNativeView(view: ViewRef): Promise {
+ return new Promise((resolve) => {
+ view.measureInWindow((x, y, width, height) => {
+ resolve(width > 0 && height > 0 ? { x, y, width, height } : null);
+ });
+ });
+}
+
+function measureRootView(view: ViewRef | null): Promise {
+ if (!view) {
+ return Promise.resolve(null);
+ }
+
+ if (Platform.OS === 'web' && isWebElementLike(view)) {
+ return Promise.resolve(toMeasuredRect(view.getBoundingClientRect()));
+ }
+
+ return measureNativeView(view);
+}
+
+function measureUnsupportedView(view: ViewRef | null): Promise {
+ if (!view) {
+ return Promise.resolve(null);
+ }
+
+ if (Platform.OS === 'web' && isWebElementLike(view)) {
+ return Promise.resolve(
+ unionRects(Array.from(view.children).flatMap((child) => measureRenderedBoxes(child))),
+ );
+ }
+
+ return measureNativeView(view);
+}
+
+function areIndicatorRectsEqual(
+ left: readonly UnsupportedIndicatorRect[],
+ right: readonly UnsupportedIndicatorRect[],
+): boolean {
+ if (left.length !== right.length) {
+ return false;
+ }
+
+ return left.every((rect, index) => {
+ const candidate = right[index];
+ if (!candidate) {
+ return false;
+ }
+ return (
+ rect.nodeId === candidate.nodeId &&
+ Math.abs(rect.x - candidate.x) < 0.5 &&
+ Math.abs(rect.y - candidate.y) < 0.5 &&
+ Math.abs(rect.width - candidate.width) < 0.5 &&
+ Math.abs(rect.height - candidate.height) < 0.5
+ );
+ });
+}
+
function StudioNodeTouchRecorder(props: {
readonly nodeId: string | undefined;
readonly showUnsupportedIndicator: boolean;
@@ -25,10 +155,11 @@ function StudioNodeTouchRecorder(props: {
}): React.JSX.Element {
const ctx = React.useContext(TrackerContext);
const nodeIdRef = React.useRef(props.nodeId);
+ const unregisterMeasurementRef = React.useRef<(() => void) | null>(null);
nodeIdRef.current = props.nodeId;
- function handleTouchStart(): void {
+ function handleInteractionStart(): void {
if (!ctx) {
return;
}
@@ -42,27 +173,34 @@ function StudioNodeTouchRecorder(props: {
ctx.recordNode(nodeId);
}
+ const setViewRef = React.useCallback(
+ (view: ViewRef | null) => {
+ unregisterMeasurementRef.current?.();
+ unregisterMeasurementRef.current = null;
+ if (view && ctx && props.nodeId && props.showUnsupportedIndicator) {
+ unregisterMeasurementRef.current = ctx.registerUnsupportedNode(props.nodeId, () =>
+ measureUnsupportedView(view),
+ );
+ }
+ },
+ [ctx, props.nodeId, props.showUnsupportedIndicator],
+ );
+
return React.createElement(
View,
{
+ ref: setViewRef,
+ nativeID: props.nodeId
+ ? `studio-runtime-node-${encodeURIComponent(props.nodeId)}`
+ : undefined,
+ testID:
+ props.nodeId && props.showUnsupportedIndicator
+ ? `studio-unsupported-recorder-${props.nodeId}`
+ : undefined,
style: { display: 'contents' },
- onTouchStart: handleTouchStart,
+ onPointerDown: handleInteractionStart,
+ onTouchStart: handleInteractionStart,
},
- props.showUnsupportedIndicator
- ? React.createElement(View, {
- style: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- borderWidth: 1,
- borderColor: '#ef4444',
- borderStyle: 'dashed',
- },
- pointerEvents: 'none',
- })
- : null,
props.children,
);
}
@@ -90,13 +228,12 @@ export function createStudioStationarySelectionWrapNode(options?: {
const isSupported =
Object.prototype.hasOwnProperty.call(ZORA_COMPONENT_REGISTRY, args.node.type) ||
(thirdPartySupport != null &&
- Object.prototype.hasOwnProperty.call(thirdPartySupport, args.node.type));
-
- const showUnsupportedIndicator = isEditMode && !isSupported;
+ Object.prototype.hasOwnProperty.call(thirdPartySupport, args.node.type) &&
+ thirdPartySupport[args.node.type] === true);
return React.createElement(StudioNodeTouchRecorder, {
nodeId: args.node.id,
- showUnsupportedIndicator: showUnsupportedIndicator,
+ showUnsupportedIndicator: isEditMode && !isSupported,
children: args.rendered,
});
};
@@ -109,6 +246,12 @@ function StationaryTapSelector(props: {
readonly children: React.ReactNode;
}): React.JSX.Element {
const coordinatorRef = React.useRef(null);
+ const rootViewRef = React.useRef(null);
+ const unsupportedNodesRef = React.useRef(new Map>());
+ const [unsupportedNodeVersion, setUnsupportedNodeVersion] = React.useState(0);
+ const [indicatorRects, setIndicatorRects] = React.useState(
+ [],
+ );
coordinatorRef.current ??= createStationarySelectionCoordinator();
@@ -117,19 +260,114 @@ function StationaryTapSelector(props: {
const isEditModeRef = React.useRef(props.isEditMode);
const selectedNodeIdRef = React.useRef(props.selectedNodeId);
const selectNodeRef = React.useRef(props.selectNode);
+ const selectionCommitCountRef = React.useRef(0);
const generationRef = React.useRef(0);
+ const pendingNodeIdsRef = React.useRef([]);
isEditModeRef.current = props.isEditMode;
selectedNodeIdRef.current = props.selectedNodeId;
selectNodeRef.current = props.selectNode;
+ const registerUnsupportedNode = React.useCallback(
+ (nodeId: string, measure: MeasureUnsupportedNode) => {
+ const measurements = unsupportedNodesRef.current.get(nodeId) ?? new Set();
+ measurements.add(measure);
+ unsupportedNodesRef.current.set(nodeId, measurements);
+ setUnsupportedNodeVersion((version) => version + 1);
+
+ return () => {
+ const registeredMeasurements = unsupportedNodesRef.current.get(nodeId);
+ if (!registeredMeasurements?.delete(measure)) {
+ return;
+ }
+ if (registeredMeasurements.size === 0) {
+ unsupportedNodesRef.current.delete(nodeId);
+ }
+ setUnsupportedNodeVersion((version) => version + 1);
+ };
+ },
+ [],
+ );
+
const contextValue = React.useMemo(
() => ({
- recordNode: (nodeId: string) => coordinator.recordNode(nodeId, generationRef.current),
+ recordNode: (nodeId: string) => {
+ if (generationRef.current === 0) {
+ if (!pendingNodeIdsRef.current.includes(nodeId)) {
+ pendingNodeIdsRef.current.push(nodeId);
+ }
+ return;
+ }
+ coordinator.recordNode(nodeId, generationRef.current);
+ },
+ registerUnsupportedNode,
}),
- [coordinator],
+ [coordinator, registerUnsupportedNode],
);
+ const refreshIndicatorRects = React.useCallback(async () => {
+ if (!isEditModeRef.current || unsupportedNodesRef.current.size === 0) {
+ setIndicatorRects((current) => (current.length === 0 ? current : []));
+ return;
+ }
+
+ const rootRect = await measureRootView(rootViewRef.current);
+ if (!rootRect) {
+ return;
+ }
+
+ const measurements = await Promise.all(
+ [...unsupportedNodesRef.current.entries()].map(async ([nodeId, nodeMeasurements]) => {
+ const rects = await Promise.all([...nodeMeasurements].map((measure) => measure()));
+ return {
+ nodeId,
+ rect: unionRects(rects.filter((rect): rect is MeasuredRect => rect !== null)),
+ };
+ }),
+ );
+ const nextRects = measurements
+ .flatMap(({ nodeId, rect }) =>
+ rect
+ ? [
+ {
+ nodeId,
+ x: rect.x - rootRect.x,
+ y: rect.y - rootRect.y,
+ width: rect.width,
+ height: rect.height,
+ },
+ ]
+ : [],
+ )
+ .sort((left, right) => left.nodeId.localeCompare(right.nodeId));
+
+ setIndicatorRects((current) =>
+ areIndicatorRectsEqual(current, nextRects) ? current : nextRects,
+ );
+ }, []);
+
+ React.useEffect(() => {
+ let active = true;
+ let frameId: number | null = null;
+
+ const measureNextFrame = () => {
+ void refreshIndicatorRects().finally(() => {
+ if (active) {
+ frameId = requestAnimationFrame(measureNextFrame);
+ }
+ });
+ };
+
+ measureNextFrame();
+
+ return () => {
+ active = false;
+ if (frameId !== null) {
+ cancelAnimationFrame(frameId);
+ }
+ };
+ }, [props.isEditMode, unsupportedNodeVersion, refreshIndicatorRects]);
+
const tapGesture = React.useMemo(() => {
return Gesture.Tap()
.maxDeltaX(5)
@@ -138,6 +376,10 @@ function StationaryTapSelector(props: {
.runOnJS(true)
.onBegin(() => {
generationRef.current = coordinator.beginTransaction();
+ for (const nodeId of pendingNodeIdsRef.current) {
+ coordinator.recordNode(nodeId, generationRef.current);
+ }
+ pendingNodeIdsRef.current = [];
})
.onEnd((_event, success) => {
if (!success) {
@@ -147,26 +389,67 @@ function StationaryTapSelector(props: {
coordinator.commitSelection(
isEditModeRef.current,
selectedNodeIdRef.current,
- selectNodeRef.current,
+ (nodeId) => {
+ selectionCommitCountRef.current += 1;
+ selectNodeRef.current(nodeId);
+ },
generationRef.current,
);
})
.onFinalize(() => {
coordinator.clearTransaction(generationRef.current);
+ generationRef.current = 0;
+ pendingNodeIdsRef.current = [];
});
}, [coordinator]);
React.useEffect(() => {
coordinator.clearTransaction();
+ generationRef.current = 0;
+ pendingNodeIdsRef.current = [];
}, [props.isEditMode, coordinator]);
+ const selectionStateId = [
+ 'studio-stationary-selection-root',
+ props.isEditMode ? 'edit' : 'preview',
+ encodeURIComponent(props.selectedNodeId ?? 'none'),
+ String(selectionCommitCountRef.current),
+ ].join(':');
+
return React.createElement(
TrackerContext.Provider,
{ value: contextValue },
React.createElement(
GestureDetector,
{ gesture: tapGesture },
- React.createElement(View, { style: { flex: 1 } }, props.children),
+ React.createElement(
+ View,
+ {
+ ref: rootViewRef,
+ nativeID: selectionStateId,
+ testID: 'studio-stationary-selection-root',
+ style: { flex: 1, position: 'relative' },
+ },
+ props.children,
+ ...indicatorRects.map((rect) =>
+ React.createElement(View, {
+ key: rect.nodeId,
+ nativeID: `studio-unsupported-indicator-${encodeURIComponent(rect.nodeId)}`,
+ testID: `studio-unsupported-indicator-${rect.nodeId}`,
+ pointerEvents: 'none',
+ style: {
+ position: 'absolute',
+ left: rect.x,
+ top: rect.y,
+ width: rect.width,
+ height: rect.height,
+ borderWidth: 1,
+ borderColor: '#ef4444',
+ borderStyle: 'dashed',
+ },
+ }),
+ ),
+ ),
),
);
}
From b89dceda99fd55b1b49f7143e897d5583d725483 Mon Sep 17 00:00:00 2001
From: artiphishle
Date: Thu, 30 Jul 2026 22:11:31 +0200
Subject: [PATCH 7/8] fix: complete Studio selection validation
---
...-issue-167-interaction-policy-selection.md | 5 +-
paradox/diagrams/architecture-overview.mmd | 18 +
paradox/diagrams/export-graph.mmd | 6 +
paradox/diagrams/module-relationships.mmd | 12 +
paradox/index.html | 621 +++++++++++--
paradox/paradox.json | 819 ++++++++++++++++--
src/authFlowContract.test.ts | 3 +
src/host/generatedAdminExpoWeb.smoke.test.ts | 105 ++-
src/host/orchestrator/semverRange.test.ts | 16 +
src/host/orchestrator/semverRange.ts | 43 +
src/host/orchestrator/templates.test.ts | 29 +-
src/host/orchestrator/templates.ts | 11 +-
.../indicatorRefreshCoordinator.test.ts | 75 ++
src/runtime/indicatorRefreshCoordinator.ts | 43 +
src/runtime/stationarySelection.test.ts | 39 +-
src/runtime/stationarySelection.ts | 280 ++++--
.../stationarySelectionInputState.test.ts | 134 +++
src/runtime/stationarySelectionInputState.ts | 94 ++
18 files changed, 2097 insertions(+), 256 deletions(-)
create mode 100644 src/host/orchestrator/semverRange.test.ts
create mode 100644 src/host/orchestrator/semverRange.ts
create mode 100644 src/runtime/indicatorRefreshCoordinator.test.ts
create mode 100644 src/runtime/indicatorRefreshCoordinator.ts
create mode 100644 src/runtime/stationarySelectionInputState.test.ts
create mode 100644 src/runtime/stationarySelectionInputState.ts
diff --git a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
index f222b176..b73b5ea5 100644
--- a/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
+++ b/.changeset/2026-07-28-issue-167-interaction-policy-selection.md
@@ -3,5 +3,6 @@
---
Add public interaction-policy and stationary-selection runtime helpers, require ZORA 2.9 in
-generated apps, support explicit extension policy declarations, and provide desktop pointer/touch
-selection with root-owned unsupported-component overlays.
+generated apps, support explicit extension policy declarations, and provide stale-safe
+pointer/touch selection with event-driven root-owned unsupported-component overlays. Generate the
+Expo SDK 54 animation stack with the compatible Reanimated 4.3.0 and Worklets 0.8.3 pair.
diff --git a/paradox/diagrams/architecture-overview.mmd b/paradox/diagrams/architecture-overview.mmd
index c4bd310b..32a7d955 100644
--- a/paradox/diagrams/architecture-overview.mmd
+++ b/paradox/diagrams/architecture-overview.mmd
@@ -177,6 +177,7 @@ graph TD
package__ankhorage_studio -.-> module_src_host_generatedAdminExpoWeb_smoke_test_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
+ module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_templateRegistry_index_ts
module_src_host_generatedAuthAdapterRuntime_smoke_test_ts["src/host/generatedAuthAdapterRuntime.smoke.test.ts"]
package__ankhorage_studio -.-> module_src_host_generatedAuthAdapterRuntime_smoke_test_ts
@@ -422,6 +423,11 @@ graph TD
module_src_host_orchestrator_scaffolder_ts --> module_src_host_orchestrator_templates_ts
module_src_host_orchestrator_scaffolder_ts --> module_src_host_templateRegistry_index_ts
module_src_host_orchestrator_scaffolder_ts --> module_src_host_zoraExtensions_index_ts
+ module_src_host_orchestrator_semverRange_test_ts["src/host/orchestrator/semverRange.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_orchestrator_semverRange_test_ts
+ module_src_host_orchestrator_semverRange_test_ts --> module_src_host_orchestrator_semverRange_ts
+ module_src_host_orchestrator_semverRange_ts["src/host/orchestrator/semverRange.ts"]
+ package__ankhorage_studio -.-> module_src_host_orchestrator_semverRange_ts
module_src_host_orchestrator_studioInfraUp_ts["src/host/orchestrator/studioInfraUp.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_studioInfraUp_ts
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_infraRuntime_ts
@@ -596,6 +602,11 @@ graph TD
package__ankhorage_studio -.-> module_src_runtime_appExtensionRegistry_ts
module_src_runtime_index_ts["src/runtime/index.ts"]
package__ankhorage_studio -.-> module_src_runtime_index_ts
+ module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_test_ts
+ module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts
module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts
@@ -625,12 +636,19 @@ graph TD
package__ankhorage_studio -.-> module_src_runtime_stationarySelection_test_ts
module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts
module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_test_ts
module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
package__ankhorage_studio -.-> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionInputState_test_ts["src/runtime/stationarySelectionInputState.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelectionInputState_test_ts
+ module_src_runtime_stationarySelectionInputState_test_ts --> module_src_runtime_stationarySelectionInputState_ts
+ module_src_runtime_stationarySelectionInputState_ts["src/runtime/stationarySelectionInputState.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_stationarySelectionInputState_ts
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
package__ankhorage_studio -.-> module_src_runtime_useRuntimeAction_ts
module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts
diff --git a/paradox/diagrams/export-graph.mmd b/paradox/diagrams/export-graph.mmd
index f500f845..1abed6a4 100644
--- a/paradox/diagrams/export-graph.mmd
+++ b/paradox/diagrams/export-graph.mmd
@@ -109,6 +109,8 @@ graph LR
module_src_host_orchestrator_projectStoreSummary_test_ts["src/host/orchestrator/projectStoreSummary.test.ts"]
module_src_host_orchestrator_resolveMutations_ts["src/host/orchestrator/resolveMutations.ts"]
module_src_host_orchestrator_scaffolder_ts["src/host/orchestrator/scaffolder.ts"]
+ module_src_host_orchestrator_semverRange_test_ts["src/host/orchestrator/semverRange.test.ts"]
+ module_src_host_orchestrator_semverRange_ts["src/host/orchestrator/semverRange.ts"]
module_src_host_orchestrator_studioInfraUp_ts["src/host/orchestrator/studioInfraUp.ts"]
module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"]
module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"]
@@ -166,6 +168,8 @@ graph LR
module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"]
module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"]
module_src_runtime_index_ts["src/runtime/index.ts"]
+ module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
+ module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
@@ -179,6 +183,8 @@ graph LR
module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
+ module_src_runtime_stationarySelectionInputState_test_ts["src/runtime/stationarySelectionInputState.test.ts"]
+ module_src_runtime_stationarySelectionInputState_ts["src/runtime/stationarySelectionInputState.ts"]
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"]
module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"]
diff --git a/paradox/diagrams/module-relationships.mmd b/paradox/diagrams/module-relationships.mmd
index 8d8c67be..12a55dce 100644
--- a/paradox/diagrams/module-relationships.mmd
+++ b/paradox/diagrams/module-relationships.mmd
@@ -109,6 +109,8 @@ graph LR
module_src_host_orchestrator_projectStoreSummary_test_ts["src/host/orchestrator/projectStoreSummary.test.ts"]
module_src_host_orchestrator_resolveMutations_ts["src/host/orchestrator/resolveMutations.ts"]
module_src_host_orchestrator_scaffolder_ts["src/host/orchestrator/scaffolder.ts"]
+ module_src_host_orchestrator_semverRange_test_ts["src/host/orchestrator/semverRange.test.ts"]
+ module_src_host_orchestrator_semverRange_ts["src/host/orchestrator/semverRange.ts"]
module_src_host_orchestrator_studioInfraUp_ts["src/host/orchestrator/studioInfraUp.ts"]
module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"]
module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"]
@@ -166,6 +168,8 @@ graph LR
module_src_runtime_actionSuppression_ts["src/runtime/actionSuppression.ts"]
module_src_runtime_appExtensionRegistry_ts["src/runtime/appExtensionRegistry.ts"]
module_src_runtime_index_ts["src/runtime/index.ts"]
+ module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
+ module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
@@ -179,6 +183,8 @@ graph LR
module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
module_src_runtime_stationarySelectionCoordinator_ts["src/runtime/stationarySelectionCoordinator.ts"]
+ module_src_runtime_stationarySelectionInputState_test_ts["src/runtime/stationarySelectionInputState.test.ts"]
+ module_src_runtime_stationarySelectionInputState_ts["src/runtime/stationarySelectionInputState.ts"]
module_src_runtime_useRuntimeAction_ts["src/runtime/useRuntimeAction.ts"]
module_src_runtimeDiagnosticsModel_test_ts["src/runtimeDiagnosticsModel.test.ts"]
module_src_runtimeDiagnosticsModel_ts["src/runtimeDiagnosticsModel.ts"]
@@ -310,6 +316,7 @@ graph LR
module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
+ module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_templateRegistry_index_ts
module_src_host_generatedAuthAdapterRuntime_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAuthAdapterRuntime_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
@@ -429,6 +436,7 @@ graph LR
module_src_host_orchestrator_scaffolder_ts --> module_src_host_orchestrator_templates_ts
module_src_host_orchestrator_scaffolder_ts --> module_src_host_templateRegistry_index_ts
module_src_host_orchestrator_scaffolder_ts --> module_src_host_zoraExtensions_index_ts
+ module_src_host_orchestrator_semverRange_test_ts --> module_src_host_orchestrator_semverRange_ts
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_infraRuntime_ts
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts
@@ -490,6 +498,7 @@ graph LR
module_src_projectSecretUsage_test_ts --> module_src_projectSecretUsage_ts
module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts
module_src_routeUtils_test_ts --> module_src_routeUtils_ts
+ module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts
@@ -497,8 +506,11 @@ graph LR
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicy_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts
module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
+ module_src_runtime_stationarySelectionInputState_test_ts --> module_src_runtime_stationarySelectionInputState_ts
module_src_runtime_useRuntimeAction_ts --> module_src_runtime_runtimeActions_ts
module_src_runtimeDiagnosticsModel_test_ts --> module_src_runtimeDiagnosticsModel_ts
module_src_runtimeLocalization_public_test_ts --> module_src_runtimeLocalization_ts
diff --git a/paradox/index.html b/paradox/index.html
index 09534478..20c3a6a0 100644
--- a/paradox/index.html
+++ b/paradox/index.html
@@ -438,7 +438,7 @@ @ankhorage/studio
data-target="src/host/generatedAdminExpoWeb.smoke.test.ts"
>
src/host/generatedAdminExpoWeb.smoke.test.ts
- 33 functions
+ 34 functions
@@ -911,6 +911,16 @@ @ankhorage/studio
4 functions
+
+
+
+
+
+
+
+
+
@@ -1309,6 +1339,26 @@ @ankhorage/studio
9 functions
+
+
+
+
+
+
@@ -495,6 +495,26 @@ @ankhorage/studio
1 function
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1625,7 +1665,7 @@ Package overview
96public exports
0components
-
236modules
+
242modules
1entrypoints
Entrypoints
@@ -2247,12 +2287,13 @@ src/host/createStudioHost.ts
src/host/generatedAdminExpoWeb.smoke.test.ts
Referenced module
- Dependencies: src/host/orchestrator/moduleManager.ts,
+ Dependencies: src/host/installedPackageProvenance.ts,
+ src/host/orchestrator/moduleManager.ts,
src/host/orchestrator/projectManager.ts,
src/host/orchestrator/semverRange.ts,
src/host/templateRegistry/index.ts
@@ -2415,6 +2456,30 @@
src/host/index.ts
startStudioHostServerWithSecrets, StudioHost
+
+ src/host/installedPackageProvenance.test.ts
+ Referenced module
+
+ Dependencies: src/host/installedPackageProvenance.ts
+
+ Exports: None
+
+
+ src/host/installedPackageProvenance.ts
+ Referenced module
+ Dependencies: None
+
+ Exports: bunLockfileReferencesPackageVersion,
+ InstalledPackageProvenance, isPathInsideResolved,
+ resolveInstalledPackageProvenance
+
+
src/host/oauthFixtureConsumer.smoke.test.ts
Exports: None
+
+ src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts
+ Referenced module
+
+ Dependencies:
+ src/host/orchestrator/expoSdk54AnimationCompatibility.ts,
+ src/host/orchestrator/templates.ts
+
+ Exports: None
+
+
+ src/host/orchestrator/expoSdk54AnimationCompatibility.ts
+ Referenced module
+ Dependencies: None
+ Exports: EXPO_SDK_54_ANIMATION_COMPATIBILITY
+
src/host/orchestrator/studioInfraUp.ts
src/host/orchestrator/templates.test.ts
Referenced module
- Dependencies: src/host/orchestrator/templates.ts
+
+ Dependencies:
+ src/host/orchestrator/expoSdk54AnimationCompatibility.ts,
+ src/host/orchestrator/templates.ts
+
Exports: None
src/host/orchestrator/templates.ts
Referenced module
- Dependencies: None
- Exports: GENERATED_ANIMATION_DEPENDENCY_PAIR,
- GeneratedAuthProvider, GeneratedStorageProvider,
- getAppConfigTs, getBabelConfigJs,
- getEslintConfigMjs, getIndexJs,
- getMetroConfigJs, getPackageJson,
+ Dependencies:
+ src/host/orchestrator/expoSdk54AnimationCompatibility.ts
+
+
+ Exports: GeneratedAuthProvider,
+ GeneratedStorageProvider, getAppConfigTs,
+ getBabelConfigJs, getEslintConfigMjs,
+ getIndexJs, getMetroConfigJs, getPackageJson,
getPrettierRcJs, getTsConfigJson
@@ -3898,7 +3991,7 @@ src/runtime/appExtensionRegistry.ts
src/runtime/index.ts
Referenced module
@@ -3928,7 +4021,8 @@ src/runtime/index.ts
StudioPreviewRegistryOptions,
StudioPreviewRuntimeConfigOptions,
ThirdPartyComponentSupport, TransactionState,
- useRuntimeAction, UseRuntimeActionOptions
+ useRuntimeAction, UseRuntimeActionOptions,
+ useStudioUnsupportedNodeMeasurement
src/runtime/indicatorRefreshCoordinator.ts
IndicatorRefreshCoordinator
+
+ src/runtime/indicatorSettleCoordinator.test.ts
+ Referenced module
+
+ Dependencies:
+ src/runtime/indicatorSettleCoordinator.ts
+
+ Exports: None
+
+
+ src/runtime/indicatorSettleCoordinator.ts
+ Referenced module
+ Dependencies: None
+
+ Exports: createIndicatorSettleCoordinator,
+ IndicatorSettleCoordinator,
+ IndicatorSettleCoordinatorOptions
+
+
src/runtime/stationarySelection.test.ts
src/runtime/stationarySelection.ts
Referenced module
Dependencies:
src/runtime/indicatorRefreshCoordinator.ts,
+ src/runtime/indicatorSettleCoordinator.ts,
src/runtime/stationarySelectionCoordinator.ts,
src/runtime/stationarySelectionInputState.ts
@@ -4086,7 +4206,7 @@ src/runtime/stationarySelection.ts
Exports: CommitSelectionResult,
createStudioStationarySelectionWrapNode,
StationarySelectionCoordinator, StationaryTapSelector,
- TransactionState
+ TransactionState, useStudioUnsupportedNodeMeasurement
Architecture overview
package__ankhorage_studio -.->
module_src_host_generatedAdminExpoWeb_smoke_test_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts -->
+ module_src_host_installedPackageProvenance_ts
+ module_src_host_generatedAdminExpoWeb_smoke_test_ts -->
module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts -->
module_src_host_orchestrator_projectManager_ts
@@ -8838,6 +8960,12 @@ Architecture overview
module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts
module_src_host_index_ts["src/host/index.ts"] package__ankhorage_studio
-.-> module_src_host_index_ts
+ module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_test_ts
+ module_src_host_installedPackageProvenance_test_ts -->
+ module_src_host_installedPackageProvenance_ts
+ module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"]
+ package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_ts
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"]
package__ankhorage_studio -.->
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts
@@ -8996,6 +9124,16 @@ Architecture overview
module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts -->
module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"]
+ package__ankhorage_studio -.->
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts -->
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts -->
+ module_src_host_orchestrator_templates_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"]
+ package__ankhorage_studio -.->
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
package__ankhorage_studio -.->
module_src_host_orchestrator_generatedAppFiles_test_ts
@@ -9130,9 +9268,13 @@ Architecture overview
module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_templates_test_ts
module_src_host_orchestrator_templates_test_ts -->
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
+ module_src_host_orchestrator_templates_test_ts -->
module_src_host_orchestrator_templates_ts
module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_templates_ts
+ module_src_host_orchestrator_templates_ts -->
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_orchestrator_workspaceRuntime_ts["src/host/orchestrator/workspaceRuntime.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_workspaceRuntime_ts
module_src_host_secrets_bunSupabaseVaultClient_ts["src/host/secrets/bunSupabaseVaultClient.ts"]
@@ -9330,6 +9472,13 @@ Architecture overview
module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"]
+ package__ankhorage_studio -.->
+ module_src_runtime_indicatorSettleCoordinator_test_ts
+ module_src_runtime_indicatorSettleCoordinator_test_ts -->
+ module_src_runtime_indicatorSettleCoordinator_ts
+ module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_indicatorSettleCoordinator_ts
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts
module_src_runtime_interactionPolicy_test_ts -->
@@ -9368,6 +9517,8 @@ Architecture overview
module_src_runtime_stationarySelection_ts -->
module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_stationarySelection_ts -->
+ module_src_runtime_indicatorSettleCoordinator_ts
+ module_src_runtime_stationarySelection_ts -->
module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelection_ts -->
module_src_runtime_stationarySelectionInputState_ts
@@ -9790,6 +9941,7 @@ Architecture overview
module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts["src/host/generatedAdminExpoWeb.smoke.test.ts"]
package__ankhorage_studio -.-> module_src_host_generatedAdminExpoWeb_smoke_test_ts
+ module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_installedPackageProvenance_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts
@@ -9850,6 +10002,11 @@ Architecture overview
module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts
module_src_host_index_ts["src/host/index.ts"]
package__ankhorage_studio -.-> module_src_host_index_ts
+ module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_test_ts
+ module_src_host_installedPackageProvenance_test_ts --> module_src_host_installedPackageProvenance_ts
+ module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"]
+ package__ankhorage_studio -.-> module_src_host_installedPackageProvenance_ts
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"]
package__ankhorage_studio -.-> module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts
@@ -9961,6 +10118,12 @@ Architecture overview
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"]
+ package__ankhorage_studio -.-> module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_templates_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"]
+ package__ankhorage_studio -.-> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_generatedAppFiles_test_ts
module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts
@@ -10050,9 +10213,11 @@ Architecture overview
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts
module_src_host_orchestrator_templates_test_ts["src/host/orchestrator/templates.test.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_templates_test_ts
+ module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_templates_ts
module_src_host_orchestrator_templates_ts["src/host/orchestrator/templates.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_templates_ts
+ module_src_host_orchestrator_templates_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_orchestrator_workspaceRuntime_ts["src/host/orchestrator/workspaceRuntime.ts"]
package__ankhorage_studio -.-> module_src_host_orchestrator_workspaceRuntime_ts
module_src_host_secrets_bunSupabaseVaultClient_ts["src/host/secrets/bunSupabaseVaultClient.ts"]
@@ -10222,6 +10387,11 @@ Architecture overview
module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
package__ankhorage_studio -.-> module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_indicatorSettleCoordinator_test_ts
+ module_src_runtime_indicatorSettleCoordinator_test_ts --> module_src_runtime_indicatorSettleCoordinator_ts
+ module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"]
+ package__ankhorage_studio -.-> module_src_runtime_indicatorSettleCoordinator_ts
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
package__ankhorage_studio -.-> module_src_runtime_interactionPolicy_test_ts
module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts
@@ -10252,6 +10422,7 @@ Architecture overview
module_src_runtime_stationarySelection_ts["src/runtime/stationarySelection.ts"]
package__ankhorage_studio -.-> module_src_runtime_stationarySelection_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorSettleCoordinator_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts
module_src_runtime_stationarySelectionCoordinator_test_ts["src/runtime/stationarySelectionCoordinator.test.ts"]
@@ -10511,6 +10682,8 @@ Module relationships
module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"]
module_src_host_http_standalone_ts["src/host/http/standalone.ts"]
module_src_host_index_ts["src/host/index.ts"]
+ module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"]
+ module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"]
module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"]
@@ -10545,6 +10718,8 @@ Module relationships
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"]
module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
@@ -10624,6 +10799,8 @@ Module relationships
module_src_runtime_index_ts["src/runtime/index.ts"]
module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
+ module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"]
+ module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"]
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
@@ -10791,6 +10968,8 @@ Module relationships
module_src_host_createStudioHost_ts -->
module_src_host_orchestrator_projectManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts -->
+ module_src_host_installedPackageProvenance_ts
+ module_src_host_generatedAdminExpoWeb_smoke_test_ts -->
module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts -->
module_src_host_orchestrator_projectManager_ts
@@ -10844,6 +11023,8 @@ Module relationships
module_src_host_http_serverWithSecrets_ts -->
module_src_host_utils_workspaceRoot_ts module_src_host_http_standalone_ts -->
module_src_host_http_serverWithSecrets_ts
+ module_src_host_installedPackageProvenance_test_ts -->
+ module_src_host_installedPackageProvenance_ts
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts -->
module_src_host_layout_auth_resolveAuthLayoutPlan_ts
module_src_host_layout_authRootBootstrap_test_ts -->
@@ -10921,6 +11102,10 @@ Module relationships
module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts -->
module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts -->
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts -->
+ module_src_host_orchestrator_templates_ts
module_src_host_orchestrator_generatedAppFiles_test_ts -->
module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts -->
@@ -11004,7 +11189,10 @@ Module relationships
module_src_host_orchestrator_studioInfraUp_ts -->
module_src_host_secrets_trustedOAuthInfraEnvironment_ts
module_src_host_orchestrator_templates_test_ts -->
- module_src_host_orchestrator_templates_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
+ module_src_host_orchestrator_templates_test_ts -->
+ module_src_host_orchestrator_templates_ts module_src_host_orchestrator_templates_ts
+ --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_secrets_projectSecretService_test_ts -->
module_src_authSettings_ts module_src_host_secrets_projectSecretService_test_ts
--> module_src_host_secrets_projectSecretService_ts
@@ -11083,6 +11271,8 @@ Module relationships
module_src_routeUtils_test_ts --> module_src_routeUtils_ts
module_src_runtime_indicatorRefreshCoordinator_test_ts -->
module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_indicatorSettleCoordinator_test_ts -->
+ module_src_runtime_indicatorSettleCoordinator_ts
module_src_runtime_interactionPolicy_test_ts -->
module_src_runtime_interactionPolicyCore_ts module_src_runtime_interactionPolicy_ts
--> module_src_runtime_interactionPolicyCore_ts
@@ -11097,6 +11287,8 @@ Module relationships
module_src_runtime_previewRegistry_ts module_src_runtime_stationarySelection_ts
--> module_src_runtime_indicatorRefreshCoordinator_ts
module_src_runtime_stationarySelection_ts -->
+ module_src_runtime_indicatorSettleCoordinator_ts
+ module_src_runtime_stationarySelection_ts -->
module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelection_ts -->
module_src_runtime_stationarySelectionInputState_ts
@@ -11278,6 +11470,8 @@ Module relationships
module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"]
module_src_host_http_standalone_ts["src/host/http/standalone.ts"]
module_src_host_index_ts["src/host/index.ts"]
+ module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"]
+ module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"]
module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"]
@@ -11312,6 +11506,8 @@ Module relationships
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"]
module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
@@ -11391,6 +11587,8 @@ Module relationships
module_src_runtime_index_ts["src/runtime/index.ts"]
module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
+ module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"]
+ module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"]
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
@@ -11535,6 +11733,7 @@ Module relationships
module_src_host_createStudioHost_ts --> module_src_host_orchestrator_infraRuntime_ts
module_src_host_createStudioHost_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_createStudioHost_ts --> module_src_host_orchestrator_projectManager_ts
+ module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_installedPackageProvenance_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_generatedAdminExpoWeb_smoke_test_ts --> module_src_host_orchestrator_semverRange_ts
@@ -11573,6 +11772,7 @@ Module relationships
module_src_host_http_serverWithSecrets_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_http_serverWithSecrets_ts --> module_src_host_utils_workspaceRoot_ts
module_src_host_http_standalone_ts --> module_src_host_http_serverWithSecrets_ts
+ module_src_host_installedPackageProvenance_test_ts --> module_src_host_installedPackageProvenance_ts
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts --> module_src_host_layout_auth_resolveAuthLayoutPlan_ts
module_src_host_layout_authRootBootstrap_test_ts --> module_src_host_layout_layoutGenerator_ts
module_src_host_layout_generatedImportComposer_test_ts --> module_src_host_layout_generatedImportComposer_ts
@@ -11616,6 +11816,8 @@ Module relationships
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_moduleManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_oauthFixtureConsumer_smoke_test_ts --> module_src_host_templateRegistry_index_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts --> module_src_host_orchestrator_templates_ts
module_src_host_orchestrator_generatedAppFiles_test_ts --> module_src_host_orchestrator_generatedAppFiles_ts
module_src_host_orchestrator_generatedAppFiles_ts --> module_src_host_zoraExtensions_index_ts
module_src_host_orchestrator_infraGenerator_ts --> module_src_host_orchestrator_infraValidation_ts
@@ -11661,7 +11863,9 @@ Module relationships
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_infraRuntime_ts
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_orchestrator_projectManager_ts
module_src_host_orchestrator_studioInfraUp_ts --> module_src_host_secrets_trustedOAuthInfraEnvironment_ts
+ module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_orchestrator_templates_test_ts --> module_src_host_orchestrator_templates_ts
+ module_src_host_orchestrator_templates_ts --> module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts
module_src_host_secrets_projectSecretService_test_ts --> module_src_authSettings_ts
module_src_host_secrets_projectSecretService_test_ts --> module_src_host_secrets_projectSecretService_ts
module_src_host_secrets_projectSecretService_ts --> module_src_host_orchestrator_projectManager_ts
@@ -11720,6 +11924,7 @@ Module relationships
module_src_routeUtils_public_test_ts --> module_src_routeUtils_ts
module_src_routeUtils_test_ts --> module_src_routeUtils_ts
module_src_runtime_indicatorRefreshCoordinator_test_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_indicatorSettleCoordinator_test_ts --> module_src_runtime_indicatorSettleCoordinator_ts
module_src_runtime_interactionPolicy_test_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_interactionPolicy_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRegistry_ts --> module_src_runtime_appExtensionRegistry_ts
@@ -11728,6 +11933,7 @@ Module relationships
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_interactionPolicyCore_ts
module_src_runtime_previewRuntimeConfig_ts --> module_src_runtime_previewRegistry_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorRefreshCoordinator_ts
+ module_src_runtime_stationarySelection_ts --> module_src_runtime_indicatorSettleCoordinator_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionCoordinator_ts
module_src_runtime_stationarySelection_ts --> module_src_runtime_stationarySelectionInputState_ts
module_src_runtime_stationarySelectionCoordinator_test_ts --> module_src_runtime_stationarySelectionCoordinator_ts
@@ -11876,6 +12082,8 @@ Export graph
module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"]
module_src_host_http_standalone_ts["src/host/http/standalone.ts"]
module_src_host_index_ts["src/host/index.ts"]
+ module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"]
+ module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"]
module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"]
@@ -11910,6 +12118,8 @@ Export graph
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"]
module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
@@ -11989,6 +12199,8 @@ Export graph
module_src_runtime_index_ts["src/runtime/index.ts"]
module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
+ module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"]
+ module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"]
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
@@ -12365,6 +12577,8 @@ Export graph
module_src_host_http_serverWithSecrets_ts["src/host/http/serverWithSecrets.ts"]
module_src_host_http_standalone_ts["src/host/http/standalone.ts"]
module_src_host_index_ts["src/host/index.ts"]
+ module_src_host_installedPackageProvenance_test_ts["src/host/installedPackageProvenance.test.ts"]
+ module_src_host_installedPackageProvenance_ts["src/host/installedPackageProvenance.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_test_ts["src/host/layout/auth/resolveAuthLayoutPlan.test.ts"]
module_src_host_layout_auth_resolveAuthLayoutPlan_ts["src/host/layout/auth/resolveAuthLayoutPlan.ts"]
module_src_host_layout_authRootBootstrap_test_ts["src/host/layout/authRootBootstrap.test.ts"]
@@ -12399,6 +12613,8 @@ Export graph
module_src_host_modules_layout_ts["src/host/modules/layout.ts"]
module_src_host_modules_runtime_LocalFsTargetAdapter_ts["src/host/modules/runtime/LocalFsTargetAdapter.ts"]
module_src_host_oauthFixtureConsumer_smoke_test_ts["src/host/oauthFixtureConsumer.smoke.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_test_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"]
+ module_src_host_orchestrator_expoSdk54AnimationCompatibility_ts["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"]
module_src_host_orchestrator_generatedAppFiles_test_ts["src/host/orchestrator/generatedAppFiles.test.ts"]
module_src_host_orchestrator_generatedAppFiles_ts["src/host/orchestrator/generatedAppFiles.ts"]
module_src_host_orchestrator_generatedRouteCleanup_ts["src/host/orchestrator/generatedRouteCleanup.ts"]
@@ -12478,6 +12694,8 @@ Export graph
module_src_runtime_index_ts["src/runtime/index.ts"]
module_src_runtime_indicatorRefreshCoordinator_test_ts["src/runtime/indicatorRefreshCoordinator.test.ts"]
module_src_runtime_indicatorRefreshCoordinator_ts["src/runtime/indicatorRefreshCoordinator.ts"]
+ module_src_runtime_indicatorSettleCoordinator_test_ts["src/runtime/indicatorSettleCoordinator.test.ts"]
+ module_src_runtime_indicatorSettleCoordinator_ts["src/runtime/indicatorSettleCoordinator.ts"]
module_src_runtime_interactionPolicy_test_ts["src/runtime/interactionPolicy.test.ts"]
module_src_runtime_interactionPolicy_ts["src/runtime/interactionPolicy.ts"]
module_src_runtime_interactionPolicyCore_ts["src/runtime/interactionPolicyCore.ts"]
@@ -14590,7 +14808,7 @@ src/host/generatedAdminExpoWeb.smoke.test.ts
data-search="createAdminSmokeManifest src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createAdminSmokeManifest
- src/host/generatedAdminExpoWeb.smoke.test.ts:38:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:43:1
No description available.
createAdminSmokeManifest
data-search="createScrollableRuntimeScreenRoot src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createScrollableRuntimeScreenRoot
- src/host/generatedAdminExpoWeb.smoke.test.ts:94:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:99:1
+ No description available.
+
+
+ visit
+ src/host/generatedAdminExpoWeb.smoke.test.ts:343:9
No description available.
createScrollableRuntimeScreenRoot
data-search="verifyDesktopSelectionAndUnsupportedGeometry src/host/generatedAdminExpoWeb.smoke.test.ts "
>
verifyDesktopSelectionAndUnsupportedGeometry
- src/host/generatedAdminExpoWeb.smoke.test.ts:272:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:503:1
+ No description available.
+
+
+ expectCapturedLayoutsToMatch
+ src/host/generatedAdminExpoWeb.smoke.test.ts:708:1
No description available.
verifyDesktopSelectionAndUnsupportedGeometry
data-search="waitForUnsupportedGeometry src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForUnsupportedGeometry
- src/host/generatedAdminExpoWeb.smoke.test.ts:452:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:735:1
No description available.
waitForUnsupportedGeometry
data-search="waitForStudioSmokeState src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForStudioSmokeState
- src/host/generatedAdminExpoWeb.smoke.test.ts:471:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:754:1
No description available.
waitForStudioSmokeState
data-search="expectRectToMatch src/host/generatedAdminExpoWeb.smoke.test.ts "
>
expectRectToMatch
- src/host/generatedAdminExpoWeb.smoke.test.ts:490:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:773:1
No description available.
expectRectToMatch
data-search="rectsOverlap src/host/generatedAdminExpoWeb.smoke.test.ts "
>
rectsOverlap
- src/host/generatedAdminExpoWeb.smoke.test.ts:497:1
- No description available.
-
-
- isPathInside
- src/host/generatedAdminExpoWeb.smoke.test.ts:504:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:780:1
No description available.
isPathInside
data-search="createGeneratedAdminProject src/host/generatedAdminExpoWeb.smoke.test.ts "
>
createGeneratedAdminProject
- src/host/generatedAdminExpoWeb.smoke.test.ts:509:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:787:1
No description available.
createGeneratedAdminProject
data-search="installGeneratedProjectDependencies src/host/generatedAdminExpoWeb.smoke.test.ts "
>
installGeneratedProjectDependencies
- src/host/generatedAdminExpoWeb.smoke.test.ts:547:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:825:1
No description available.
installGeneratedProjectDependencies
data-search="stageLocalStudioPackage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
stageLocalStudioPackage
- src/host/generatedAdminExpoWeb.smoke.test.ts:581:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:859:1
No description available.
stageLocalStudioPackage
data-search="writeSmokeRuntimeExtensions src/host/generatedAdminExpoWeb.smoke.test.ts "
>
writeSmokeRuntimeExtensions
- src/host/generatedAdminExpoWeb.smoke.test.ts:610:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:888:1
No description available.
writeSmokeRuntimeExtensions
data-search="writeSmokeMetroConfig src/host/generatedAdminExpoWeb.smoke.test.ts "
>
writeSmokeMetroConfig
- src/host/generatedAdminExpoWeb.smoke.test.ts:689:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1159:1
No description available.
writeSmokeMetroConfig
data-search="reservePort src/host/generatedAdminExpoWeb.smoke.test.ts "
>
reservePort
- src/host/generatedAdminExpoWeb.smoke.test.ts:712:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1182:1
No description available.
reservePort
data-search="spawnExpoWeb src/host/generatedAdminExpoWeb.smoke.test.ts "
>
spawnExpoWeb
- src/host/generatedAdminExpoWeb.smoke.test.ts:728:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1198:1
No description available.
spawnExpoWeb
data-search="startSmokeStudioApi src/host/generatedAdminExpoWeb.smoke.test.ts "
>
startSmokeStudioApi
- src/host/generatedAdminExpoWeb.smoke.test.ts:750:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1220:1
No description available.
startSmokeStudioApi
data-search="handleSmokeStudioApiRequest src/host/generatedAdminExpoWeb.smoke.test.ts "
>
handleSmokeStudioApiRequest
- src/host/generatedAdminExpoWeb.smoke.test.ts:775:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1245:1
No description available.
handleSmokeStudioApiRequest
data-search="listenOnLocalhost src/host/generatedAdminExpoWeb.smoke.test.ts "
>
listenOnLocalhost
- src/host/generatedAdminExpoWeb.smoke.test.ts:815:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1285:1
No description available.
listenOnLocalhost
data-search="closeHttpServer src/host/generatedAdminExpoWeb.smoke.test.ts "
>
closeHttpServer
- src/host/generatedAdminExpoWeb.smoke.test.ts:829:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1299:1
No description available.
closeHttpServer
data-search="readJsonRequest src/host/generatedAdminExpoWeb.smoke.test.ts "
>
readJsonRequest
- src/host/generatedAdminExpoWeb.smoke.test.ts:838:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1308:1
No description available.
readJsonRequest
data-search="writeJsonResponse src/host/generatedAdminExpoWeb.smoke.test.ts "
>
writeJsonResponse
- src/host/generatedAdminExpoWeb.smoke.test.ts:854:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1324:1
No description available.
writeJsonResponse
data-search="writeCorsHeaders src/host/generatedAdminExpoWeb.smoke.test.ts "
>
writeCorsHeaders
- src/host/generatedAdminExpoWeb.smoke.test.ts:861:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1331:1
No description available.
writeCorsHeaders
data-search="waitForExpoWebUrl src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForExpoWebUrl
- src/host/generatedAdminExpoWeb.smoke.test.ts:867:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1337:1
No description available.
waitForExpoWebUrl
data-search="waitForHttp src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForHttp
- src/host/generatedAdminExpoWeb.smoke.test.ts:880:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1350:1
No description available.
waitForHttp
data-search="waitForBodyText src/host/generatedAdminExpoWeb.smoke.test.ts "
>
waitForBodyText
- src/host/generatedAdminExpoWeb.smoke.test.ts:897:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1367:1
No description available.
waitForBodyText
data-search="resolveChromePath src/host/generatedAdminExpoWeb.smoke.test.ts "
>
resolveChromePath
- src/host/generatedAdminExpoWeb.smoke.test.ts:914:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1384:1
No description available.
resolveChromePath
data-search="readEnvString src/host/generatedAdminExpoWeb.smoke.test.ts "
>
readEnvString
- src/host/generatedAdminExpoWeb.smoke.test.ts:920:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1390:1
No description available.
readEnvString
data-search="spawnChrome src/host/generatedAdminExpoWeb.smoke.test.ts "
>
spawnChrome
- src/host/generatedAdminExpoWeb.smoke.test.ts:926:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1396:1
No description available.
spawnChrome
data-search="openChromePage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
openChromePage
- src/host/generatedAdminExpoWeb.smoke.test.ts:942:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1412:1
No description available.
openChromePage
data-search="parseChromeProtocolMessage src/host/generatedAdminExpoWeb.smoke.test.ts "
>
parseChromeProtocolMessage
- src/host/generatedAdminExpoWeb.smoke.test.ts:1252:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1732:1
No description available.
parseChromeProtocolMessage
data-search="isRecord src/host/generatedAdminExpoWeb.smoke.test.ts "
>
isRecord
- src/host/generatedAdminExpoWeb.smoke.test.ts:1267:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1747:1
No description available.
isRecord
data-search="stopProcess src/host/generatedAdminExpoWeb.smoke.test.ts "
>
stopProcess
- src/host/generatedAdminExpoWeb.smoke.test.ts:1271:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1751:1
No description available.
stopProcess
data-search="collectProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts "
>
collectProcessOutput
- src/host/generatedAdminExpoWeb.smoke.test.ts:1280:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1760:1
No description available.
collectProcessOutput
data-search="formatProcessOutput src/host/generatedAdminExpoWeb.smoke.test.ts "
>
formatProcessOutput
- src/host/generatedAdminExpoWeb.smoke.test.ts:1291:1
+ src/host/generatedAdminExpoWeb.smoke.test.ts:1771:1
No description available.
@@ -15148,6 +15371,58 @@ startStudioHostServerWithSecrets
+
+
+ src/host/installedPackageProvenance.test.ts
+
+ createTemporaryRoot
+ src/host/installedPackageProvenance.test.ts:23:1
+ No description available.
+
+
+
+
+
+ src/host/installedPackageProvenance.ts
+
+ isPathInsideResolved
+ src/host/installedPackageProvenance.ts:9:1
+ No description available.
+
+
+ resolveInstalledPackageProvenance
+ src/host/installedPackageProvenance.ts:22:1
+ No description available.
+
+
+ bunLockfileReferencesPackageVersion
+ src/host/installedPackageProvenance.ts:37:1
+ No description available.
+
+
+
src/host/orchestrator/templates.ts
data-search="serializeStringLiteral src/host/orchestrator/templates.ts "
>
serializeStringLiteral
- src/host/orchestrator/templates.ts:97:1
+ src/host/orchestrator/templates.ts:94:1
No description available.
serializeStringLiteral
data-search="serializeJsValue src/host/orchestrator/templates.ts "
>
serializeJsValue
- src/host/orchestrator/templates.ts:105:1
+ src/host/orchestrator/templates.ts:102:1
No description available.
serializeJsValue
data-search="serializeSplashScreenPlugin src/host/orchestrator/templates.ts "
>
serializeSplashScreenPlugin
- src/host/orchestrator/templates.ts:147:1
+ src/host/orchestrator/templates.ts:144:1
No description available.
serializeSplashScreenPlugin
data-search="serializeRuntimePlugin src/host/orchestrator/templates.ts "
>
serializeRuntimePlugin
- src/host/orchestrator/templates.ts:161:1
+ src/host/orchestrator/templates.ts:158:1
No description available.
serializeRuntimePlugin
data-search="serializePluginsWithRuntimePlan src/host/orchestrator/templates.ts "
>
serializePluginsWithRuntimePlan
- src/host/orchestrator/templates.ts:174:1
+ src/host/orchestrator/templates.ts:171:1
No description available.
serializePluginsWithRuntimePlan
data-search="createNativeIdentifierSegment src/host/orchestrator/templates.ts "
>
createNativeIdentifierSegment
- src/host/orchestrator/templates.ts:196:1
+ src/host/orchestrator/templates.ts:193:1
No description available.
createNativeIdentifierSegment
data-search="createNativeApplicationId src/host/orchestrator/templates.ts "
>
createNativeApplicationId
- src/host/orchestrator/templates.ts:206:1
+ src/host/orchestrator/templates.ts:203:1
No description available.
createNativeApplicationId
data-search="createExpoScheme src/host/orchestrator/templates.ts "
>
createExpoScheme
- src/host/orchestrator/templates.ts:210:1
+ src/host/orchestrator/templates.ts:207:1
No description available.
createExpoScheme
data-search="serializeAndroidConfig src/host/orchestrator/templates.ts "
>
serializeAndroidConfig
- src/host/orchestrator/templates.ts:215:1
+ src/host/orchestrator/templates.ts:212:1
No description available.
getAppConfigTs
- src/host/orchestrator/templates.ts:229:1
+ src/host/orchestrator/templates.ts:226:1
No description available.
getAppConfigTs
data-search="getMetroConfigJs src/host/orchestrator/templates.ts "
>
getMetroConfigJs
- src/host/orchestrator/templates.ts:265:1
+ src/host/orchestrator/templates.ts:262:1
No description available.
getMetroConfigJs
data-search="getBabelConfigJs src/host/orchestrator/templates.ts "
>
getBabelConfigJs
- src/host/orchestrator/templates.ts:273:1
+ src/host/orchestrator/templates.ts:270:1
No description available.
getPackageJson
- src/host/orchestrator/templates.ts:297:1
+ src/host/orchestrator/templates.ts:294:1
No description available.
getPackageJson
data-search="getEslintConfigMjs src/host/orchestrator/templates.ts "
>
getEslintConfigMjs
- src/host/orchestrator/templates.ts:386:1
+ src/host/orchestrator/templates.ts:383:1
No description available.
getPrettierRcJs
- src/host/orchestrator/templates.ts:410:1
+ src/host/orchestrator/templates.ts:407:1
No description available.
getIndexJs
- src/host/orchestrator/templates.ts:415:1
+ src/host/orchestrator/templates.ts:412:1
No description available.
getTsConfigJson
- src/host/orchestrator/templates.ts:430:1
+ src/host/orchestrator/templates.ts:427:1
No description available.
@@ -19421,6 +19696,71 @@ cancelPendingRefresh
+
+
+ src/runtime/indicatorSettleCoordinator.test.ts
+
+ createFakeScheduler
+ src/runtime/indicatorSettleCoordinator.test.ts:5:1
+ No description available.
+
+
+
+
+
+ src/runtime/indicatorSettleCoordinator.ts
+
+ createIndicatorSettleCoordinator
+ src/runtime/indicatorSettleCoordinator.ts:21:1
+ No description available.
+
+
+ clearPending
+ src/runtime/indicatorSettleCoordinator.ts:32:3
+ No description available.
+
+
+ cancel
+ src/runtime/indicatorSettleCoordinator.ts:40:3
+ No description available.
+
+
+ scheduleNext
+ src/runtime/indicatorSettleCoordinator.ts:49:3
+ No description available.
+
+
+ sampleNext
+ src/runtime/indicatorSettleCoordinator.ts:59:3
+ No description available.
+
+
+
src/runtime/stationarySelection.ts
data-search="isWebElementLike src/runtime/stationarySelection.ts "
>
isWebElementLike
- src/runtime/stationarySelection.ts:75:1
+ src/runtime/stationarySelection.ts:89:1
No description available.
isWebElementLike
data-search="getWebResizeTarget src/runtime/stationarySelection.ts "
>
getWebResizeTarget
- src/runtime/stationarySelection.ts:85:1
+ src/runtime/stationarySelection.ts:99:1
No description available.
getWebResizeTarget
data-search="getWebDescendantResizeTargets src/runtime/stationarySelection.ts "
>
getWebDescendantResizeTargets
- src/runtime/stationarySelection.ts:89:1
+ src/runtime/stationarySelection.ts:103:1
No description available.
toMeasuredRect
- src/runtime/stationarySelection.ts:100:1
+ src/runtime/stationarySelection.ts:114:1
No description available.
unionRects
- src/runtime/stationarySelection.ts:109:1
+ src/runtime/stationarySelection.ts:123:1
No description available.
unionRects
data-search="measureRenderedBoxes src/runtime/stationarySelection.ts "
>
measureRenderedBoxes
- src/runtime/stationarySelection.ts:127:1
+ src/runtime/stationarySelection.ts:141:1
No description available.
measureRenderedBoxes
data-search="measureNativeView src/runtime/stationarySelection.ts "
>
measureNativeView
- src/runtime/stationarySelection.ts:136:1
+ src/runtime/stationarySelection.ts:150:1
No description available.
measureRootView
- src/runtime/stationarySelection.ts:144:1
+ src/runtime/stationarySelection.ts:158:1
No description available.
- measureUnsupportedView
- src/runtime/stationarySelection.ts:156:1
+ measureUnsupportedWebView
+ src/runtime/stationarySelection.ts:170:1
No description available.
measureUnsupportedView
data-search="areIndicatorRectsEqual src/runtime/stationarySelection.ts "
>
areIndicatorRectsEqual
- src/runtime/stationarySelection.ts:170:1
+ src/runtime/stationarySelection.ts:184:1
+ No description available.
+
+
+ useStudioUnsupportedNodeMeasurement
+ src/runtime/stationarySelection.ts:207:1
No description available.
areIndicatorRectsEqual
data-search="StudioNodeTouchRecorder src/runtime/stationarySelection.ts "
>
StudioNodeTouchRecorder
- src/runtime/stationarySelection.ts:193:1
+ src/runtime/stationarySelection.ts:240:1
No description available.
StudioNodeTouchRecorder
data-search="recordInteraction src/runtime/stationarySelection.ts "
>
recordInteraction
- src/runtime/stationarySelection.ts:204:3
+ src/runtime/stationarySelection.ts:251:3
No description available.
recordInteraction
data-search="handlePointerDown src/runtime/stationarySelection.ts "
>
handlePointerDown
- src/runtime/stationarySelection.ts:218:3
+ src/runtime/stationarySelection.ts:265:3
No description available.
handlePointerDown
data-search="handleTouchStart src/runtime/stationarySelection.ts "
>
handleTouchStart
- src/runtime/stationarySelection.ts:229:3
+ src/runtime/stationarySelection.ts:276:3
No description available.
handleTouchStart
data-search="handleInteractionCompletion src/runtime/stationarySelection.ts "
>
handleInteractionCompletion
- src/runtime/stationarySelection.ts:238:3
+ src/runtime/stationarySelection.ts:285:3
No description available.
handleLayout
- src/runtime/stationarySelection.ts:242:3
+ src/runtime/stationarySelection.ts:289:3
No description available.
handleLayout
data-search="createStudioStationarySelectionWrapNode src/runtime/stationarySelection.ts "
>
createStudioStationarySelectionWrapNode
- src/runtime/stationarySelection.ts:288:1
+ src/runtime/stationarySelection.ts:353:1
No description available.
createStudioStationarySelectionWrapNode
data-search="StationaryTapSelector src/runtime/stationarySelection.ts "
>
StationaryTapSelector
- src/runtime/stationarySelection.ts:322:1
+ src/runtime/stationarySelection.ts:387:1
No description available.
handleScroll
- src/runtime/stationarySelection.ts:517:11
+ src/runtime/stationarySelection.ts:603:11
No description available.
handleResize
- src/runtime/stationarySelection.ts:518:11
+ src/runtime/stationarySelection.ts:604:11
No description available.
diff --git a/paradox/paradox.json b/paradox/paradox.json
index e975355c..b53ddd6a 100644
--- a/paradox/paradox.json
+++ b/paradox/paradox.json
@@ -596,6 +596,7 @@
"path": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"isEntrypoint": false,
"dependencies": [
+ "src/host/installedPackageProvenance.ts",
"src/host/orchestrator/moduleManager.ts",
"src/host/orchestrator/projectManager.ts",
"src/host/orchestrator/semverRange.ts",
@@ -737,6 +738,23 @@
"StudioHost"
]
},
+ {
+ "path": "src/host/installedPackageProvenance.test.ts",
+ "isEntrypoint": false,
+ "dependencies": ["src/host/installedPackageProvenance.ts"],
+ "exports": []
+ },
+ {
+ "path": "src/host/installedPackageProvenance.ts",
+ "isEntrypoint": false,
+ "dependencies": [],
+ "exports": [
+ "bunLockfileReferencesPackageVersion",
+ "InstalledPackageProvenance",
+ "isPathInsideResolved",
+ "resolveInstalledPackageProvenance"
+ ]
+ },
{
"path": "src/host/layout/auth/resolveAuthLayoutPlan.test.ts",
"isEntrypoint": false,
@@ -1012,6 +1030,21 @@
],
"exports": []
},
+ {
+ "path": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "isEntrypoint": false,
+ "dependencies": [
+ "src/host/orchestrator/expoSdk54AnimationCompatibility.ts",
+ "src/host/orchestrator/templates.ts"
+ ],
+ "exports": []
+ },
+ {
+ "path": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts",
+ "isEntrypoint": false,
+ "dependencies": [],
+ "exports": ["EXPO_SDK_54_ANIMATION_COMPATIBILITY"]
+ },
{
"path": "src/host/orchestrator/generatedAppFiles.test.ts",
"isEntrypoint": false,
@@ -1203,15 +1236,17 @@
{
"path": "src/host/orchestrator/templates.test.ts",
"isEntrypoint": false,
- "dependencies": ["src/host/orchestrator/templates.ts"],
+ "dependencies": [
+ "src/host/orchestrator/expoSdk54AnimationCompatibility.ts",
+ "src/host/orchestrator/templates.ts"
+ ],
"exports": []
},
{
"path": "src/host/orchestrator/templates.ts",
"isEntrypoint": false,
- "dependencies": [],
+ "dependencies": ["src/host/orchestrator/expoSdk54AnimationCompatibility.ts"],
"exports": [
- "GENERATED_ANIMATION_DEPENDENCY_PAIR",
"GeneratedAuthProvider",
"GeneratedStorageProvider",
"getAppConfigTs",
@@ -1970,7 +2005,8 @@
"ThirdPartyComponentSupport",
"TransactionState",
"useRuntimeAction",
- "UseRuntimeActionOptions"
+ "UseRuntimeActionOptions",
+ "useStudioUnsupportedNodeMeasurement"
]
},
{
@@ -1989,6 +2025,22 @@
"IndicatorRefreshCoordinator"
]
},
+ {
+ "path": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "isEntrypoint": false,
+ "dependencies": ["src/runtime/indicatorSettleCoordinator.ts"],
+ "exports": []
+ },
+ {
+ "path": "src/runtime/indicatorSettleCoordinator.ts",
+ "isEntrypoint": false,
+ "dependencies": [],
+ "exports": [
+ "createIndicatorSettleCoordinator",
+ "IndicatorSettleCoordinator",
+ "IndicatorSettleCoordinatorOptions"
+ ]
+ },
{
"path": "src/runtime/interactionPolicy.test.ts",
"isEntrypoint": false,
@@ -2081,6 +2133,7 @@
"isEntrypoint": false,
"dependencies": [
"src/runtime/indicatorRefreshCoordinator.ts",
+ "src/runtime/indicatorSettleCoordinator.ts",
"src/runtime/stationarySelectionCoordinator.ts",
"src/runtime/stationarySelectionInputState.ts"
],
@@ -2089,7 +2142,8 @@
"createStudioStationarySelectionWrapNode",
"StationarySelectionCoordinator",
"StationaryTapSelector",
- "TransactionState"
+ "TransactionState",
+ "useStudioUnsupportedNodeMeasurement"
]
},
{
@@ -7995,7 +8049,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 38,
+ "line": 43,
"column": 1
}
},
@@ -8004,61 +8058,70 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 94,
+ "line": 99,
"column": 1
}
},
+ {
+ "name": "visit",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "line": 343,
+ "column": 9
+ }
+ },
{
"name": "verifyDesktopSelectionAndUnsupportedGeometry",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 272,
+ "line": 503,
"column": 1
}
},
{
- "name": "waitForUnsupportedGeometry",
+ "name": "expectCapturedLayoutsToMatch",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 452,
+ "line": 708,
"column": 1
}
},
{
- "name": "waitForStudioSmokeState",
+ "name": "waitForUnsupportedGeometry",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 471,
+ "line": 735,
"column": 1
}
},
{
- "name": "expectRectToMatch",
+ "name": "waitForStudioSmokeState",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 490,
+ "line": 754,
"column": 1
}
},
{
- "name": "rectsOverlap",
+ "name": "expectRectToMatch",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 497,
+ "line": 773,
"column": 1
}
},
{
- "name": "isPathInside",
+ "name": "rectsOverlap",
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 504,
+ "line": 780,
"column": 1
}
},
@@ -8067,7 +8130,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 509,
+ "line": 787,
"column": 1
}
},
@@ -8076,7 +8139,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 547,
+ "line": 825,
"column": 1
}
},
@@ -8085,7 +8148,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 581,
+ "line": 859,
"column": 1
}
},
@@ -8094,7 +8157,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 610,
+ "line": 888,
"column": 1
}
},
@@ -8103,7 +8166,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 689,
+ "line": 1159,
"column": 1
}
},
@@ -8112,7 +8175,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 712,
+ "line": 1182,
"column": 1
}
},
@@ -8121,7 +8184,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 728,
+ "line": 1198,
"column": 1
}
},
@@ -8130,7 +8193,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 750,
+ "line": 1220,
"column": 1
}
},
@@ -8139,7 +8202,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 775,
+ "line": 1245,
"column": 1
}
},
@@ -8148,7 +8211,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 815,
+ "line": 1285,
"column": 1
}
},
@@ -8157,7 +8220,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 829,
+ "line": 1299,
"column": 1
}
},
@@ -8166,7 +8229,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 838,
+ "line": 1308,
"column": 1
}
},
@@ -8175,7 +8238,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 854,
+ "line": 1324,
"column": 1
}
},
@@ -8184,7 +8247,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 861,
+ "line": 1331,
"column": 1
}
},
@@ -8193,7 +8256,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 867,
+ "line": 1337,
"column": 1
}
},
@@ -8202,7 +8265,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 880,
+ "line": 1350,
"column": 1
}
},
@@ -8211,7 +8274,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 897,
+ "line": 1367,
"column": 1
}
},
@@ -8220,7 +8283,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 914,
+ "line": 1384,
"column": 1
}
},
@@ -8229,7 +8292,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 920,
+ "line": 1390,
"column": 1
}
},
@@ -8238,7 +8301,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 926,
+ "line": 1396,
"column": 1
}
},
@@ -8247,7 +8310,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 942,
+ "line": 1412,
"column": 1
}
},
@@ -8256,7 +8319,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 1252,
+ "line": 1732,
"column": 1
}
},
@@ -8265,7 +8328,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 1267,
+ "line": 1747,
"column": 1
}
},
@@ -8274,7 +8337,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 1271,
+ "line": 1751,
"column": 1
}
},
@@ -8283,7 +8346,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 1280,
+ "line": 1760,
"column": 1
}
},
@@ -8292,7 +8355,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "line": 1291,
+ "line": 1771,
"column": 1
}
},
@@ -8557,6 +8620,42 @@
"column": 1
}
},
+ {
+ "name": "createTemporaryRoot",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/installedPackageProvenance.test.ts",
+ "line": 23,
+ "column": 1
+ }
+ },
+ {
+ "name": "isPathInsideResolved",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/installedPackageProvenance.ts",
+ "line": 9,
+ "column": 1
+ }
+ },
+ {
+ "name": "resolveInstalledPackageProvenance",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/installedPackageProvenance.ts",
+ "line": 22,
+ "column": 1
+ }
+ },
+ {
+ "name": "bunLockfileReferencesPackageVersion",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/host/installedPackageProvenance.ts",
+ "line": 37,
+ "column": 1
+ }
+ },
{
"name": "createManifest",
"description": null,
@@ -10281,7 +10380,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 97,
+ "line": 94,
"column": 1
}
},
@@ -10290,7 +10389,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 105,
+ "line": 102,
"column": 1
}
},
@@ -10299,7 +10398,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 147,
+ "line": 144,
"column": 1
}
},
@@ -10308,7 +10407,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 161,
+ "line": 158,
"column": 1
}
},
@@ -10317,7 +10416,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 174,
+ "line": 171,
"column": 1
}
},
@@ -10326,7 +10425,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 196,
+ "line": 193,
"column": 1
}
},
@@ -10335,7 +10434,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 206,
+ "line": 203,
"column": 1
}
},
@@ -10344,7 +10443,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 210,
+ "line": 207,
"column": 1
}
},
@@ -10353,7 +10452,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 215,
+ "line": 212,
"column": 1
}
},
@@ -10362,7 +10461,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 229,
+ "line": 226,
"column": 1
}
},
@@ -10371,7 +10470,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 265,
+ "line": 262,
"column": 1
}
},
@@ -10380,7 +10479,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 273,
+ "line": 270,
"column": 1
}
},
@@ -10389,7 +10488,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 297,
+ "line": 294,
"column": 1
}
},
@@ -10398,7 +10497,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 386,
+ "line": 383,
"column": 1
}
},
@@ -10407,7 +10506,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 410,
+ "line": 407,
"column": 1
}
},
@@ -10416,7 +10515,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 415,
+ "line": 412,
"column": 1
}
},
@@ -10425,7 +10524,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/host/orchestrator/templates.ts",
- "line": 430,
+ "line": 427,
"column": 1
}
},
@@ -12976,6 +13075,60 @@
"column": 3
}
},
+ {
+ "name": "createFakeScheduler",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "line": 5,
+ "column": 1
+ }
+ },
+ {
+ "name": "createIndicatorSettleCoordinator",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/indicatorSettleCoordinator.ts",
+ "line": 21,
+ "column": 1
+ }
+ },
+ {
+ "name": "clearPending",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/indicatorSettleCoordinator.ts",
+ "line": 32,
+ "column": 3
+ }
+ },
+ {
+ "name": "cancel",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/indicatorSettleCoordinator.ts",
+ "line": 40,
+ "column": 3
+ }
+ },
+ {
+ "name": "scheduleNext",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/indicatorSettleCoordinator.ts",
+ "line": 49,
+ "column": 3
+ }
+ },
+ {
+ "name": "sampleNext",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/indicatorSettleCoordinator.ts",
+ "line": 59,
+ "column": 3
+ }
+ },
{
"name": "createStudioInteractionPolicyResolver",
"description": null,
@@ -13089,7 +13242,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 75,
+ "line": 89,
"column": 1
}
},
@@ -13098,7 +13251,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 85,
+ "line": 99,
"column": 1
}
},
@@ -13107,7 +13260,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 89,
+ "line": 103,
"column": 1
}
},
@@ -13116,7 +13269,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 100,
+ "line": 114,
"column": 1
}
},
@@ -13125,7 +13278,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 109,
+ "line": 123,
"column": 1
}
},
@@ -13134,7 +13287,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 127,
+ "line": 141,
"column": 1
}
},
@@ -13143,7 +13296,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 136,
+ "line": 150,
"column": 1
}
},
@@ -13152,16 +13305,16 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 144,
+ "line": 158,
"column": 1
}
},
{
- "name": "measureUnsupportedView",
+ "name": "measureUnsupportedWebView",
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 156,
+ "line": 170,
"column": 1
}
},
@@ -13170,7 +13323,16 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 170,
+ "line": 184,
+ "column": 1
+ }
+ },
+ {
+ "name": "useStudioUnsupportedNodeMeasurement",
+ "description": null,
+ "sourceLocation": {
+ "filePath": "src/runtime/stationarySelection.ts",
+ "line": 207,
"column": 1
}
},
@@ -13179,7 +13341,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 193,
+ "line": 240,
"column": 1
}
},
@@ -13188,7 +13350,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 204,
+ "line": 251,
"column": 3
}
},
@@ -13197,7 +13359,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 218,
+ "line": 265,
"column": 3
}
},
@@ -13206,7 +13368,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 229,
+ "line": 276,
"column": 3
}
},
@@ -13215,7 +13377,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 238,
+ "line": 285,
"column": 3
}
},
@@ -13224,7 +13386,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 242,
+ "line": 289,
"column": 3
}
},
@@ -13233,7 +13395,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 288,
+ "line": 353,
"column": 1
}
},
@@ -13242,7 +13404,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 322,
+ "line": 387,
"column": 1
}
},
@@ -13251,7 +13413,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 517,
+ "line": 603,
"column": 11
}
},
@@ -13260,7 +13422,7 @@
"description": null,
"sourceLocation": {
"filePath": "src/runtime/stationarySelection.ts",
- "line": 518,
+ "line": 604,
"column": 11
}
},
@@ -15704,6 +15866,11 @@
"toPath": "src/host/orchestrator/projectManager.ts",
"sourcePath": "src/host/createStudioHost.ts"
},
+ {
+ "fromPath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toPath": "src/host/installedPackageProvenance.ts",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromPath": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"toPath": "src/host/orchestrator/moduleManager.ts",
@@ -15849,6 +16016,11 @@
"toPath": "src/host/utils/workspaceRoot.ts",
"sourcePath": "src/host/index.ts"
},
+ {
+ "fromPath": "src/host/installedPackageProvenance.test.ts",
+ "toPath": "src/host/installedPackageProvenance.ts",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
{
"fromPath": "src/host/oauthFixtureConsumer.smoke.test.ts",
"toPath": "src/host/orchestrator/moduleManager.ts",
@@ -15959,6 +16131,11 @@
"toPath": "src/runtime/indicatorRefreshCoordinator.ts",
"sourcePath": "src/runtime/indicatorRefreshCoordinator.test.ts"
},
+ {
+ "fromPath": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toPath": "src/runtime/indicatorSettleCoordinator.ts",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
{
"fromPath": "src/runtime/interactionPolicy.test.ts",
"toPath": "src/runtime/interactionPolicyCore.ts",
@@ -16004,6 +16181,11 @@
"toPath": "src/runtime/indicatorRefreshCoordinator.ts",
"sourcePath": "src/runtime/stationarySelection.ts"
},
+ {
+ "fromPath": "src/runtime/stationarySelection.ts",
+ "toPath": "src/runtime/indicatorSettleCoordinator.ts",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
{
"fromPath": "src/runtime/stationarySelection.ts",
"toPath": "src/runtime/stationarySelectionCoordinator.ts",
@@ -16319,6 +16501,16 @@
"toPath": "src/host/modules/layout.ts",
"sourcePath": "src/host/modules/catalog.ts"
},
+ {
+ "fromPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
+ {
+ "fromPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toPath": "src/host/orchestrator/templates.ts",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
{
"fromPath": "src/host/orchestrator/generatedAppFiles.test.ts",
"toPath": "src/host/orchestrator/generatedAppFiles.ts",
@@ -16544,11 +16736,21 @@
"toPath": "src/host/orchestrator/projectManager.ts",
"sourcePath": "src/host/orchestrator/studioInfraUp.ts"
},
+ {
+ "fromPath": "src/host/orchestrator/templates.test.ts",
+ "toPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts",
+ "sourcePath": "src/host/orchestrator/templates.test.ts"
+ },
{
"fromPath": "src/host/orchestrator/templates.test.ts",
"toPath": "src/host/orchestrator/templates.ts",
"sourcePath": "src/host/orchestrator/templates.test.ts"
},
+ {
+ "fromPath": "src/host/orchestrator/templates.ts",
+ "toPath": "src/host/orchestrator/expoSdk54AnimationCompatibility.ts",
+ "sourcePath": "src/host/orchestrator/templates.ts"
+ },
{
"fromPath": "src/host/secrets/projectSecretService.test.ts",
"toPath": "src/authSettings.ts",
@@ -23291,6 +23493,30 @@
"callExpression": "createScrollableRuntimeScreenRoot",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "test",
+ "callExpression": "test",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "createScrollableRuntimeScreenRoot",
+ "callExpression": "createScrollableRuntimeScreenRoot",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "visit",
+ "callExpression": "visit",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
"toSymbol": "adminWebSmokeTest",
@@ -23341,14 +23567,14 @@
},
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "toSymbol": "realpath",
- "callExpression": "realpath",
+ "toSymbol": "resolveInstalledPackageProvenance",
+ "callExpression": "resolveInstalledPackageProvenance",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "toSymbol": "expect",
- "callExpression": "expect",
+ "toSymbol": "realpath",
+ "callExpression": "realpath",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
@@ -23359,8 +23585,14 @@
},
{
"fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
- "toSymbol": "isPathInside",
- "callExpression": "isPathInside",
+ "toSymbol": "isPathInsideResolved",
+ "callExpression": "isPathInsideResolved",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/generatedAdminExpoWeb.smoke.test.ts",
+ "toSymbol": "bunLockfileReferencesPackageVersion",
+ "callExpression": "bunLockfileReferencesPackageVersion",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
{
@@ -23531,6 +23763,24 @@
"callExpression": "page.mouseDrag",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "captureSmokeLayout",
+ "callExpression": "page.captureSmokeLayout",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "verifyDesktopSelectionAndUnsupportedGeometry",
+ "toSymbol": "expectCapturedLayoutsToMatch",
+ "callExpression": "expectCapturedLayoutsToMatch",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
+ {
+ "fromSymbol": "expectCapturedLayoutsToMatch",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "waitForUnsupportedGeometry",
"toSymbol": "readUnsupportedGeometry",
@@ -23873,6 +24123,12 @@
"callExpression": "this.evaluate",
"sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
},
+ {
+ "fromSymbol": "captureSmokeLayout",
+ "toSymbol": "evaluate",
+ "callExpression": "this.evaluate",
+ "sourcePath": "src/host/generatedAdminExpoWeb.smoke.test.ts"
+ },
{
"fromSymbol": "readRuntimeNodeCenter",
"toSymbol": "evaluate",
@@ -24329,6 +24585,102 @@
"callExpression": "resolveWorkspaceRoot",
"sourcePath": "src/host/hostOwnership.test.ts"
},
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "afterEach",
+ "callExpression": "afterEach",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "rm",
+ "callExpression": "rm",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "createTemporaryRoot",
+ "toSymbol": "mkdtemp",
+ "callExpression": "mkdtemp",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "createTemporaryRoot",
+ "toSymbol": "tmpdir",
+ "callExpression": "tmpdir",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "describe",
+ "callExpression": "describe",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "it",
+ "callExpression": "it",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "createTemporaryRoot",
+ "callExpression": "createTemporaryRoot",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "mkdir",
+ "callExpression": "mkdir",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "resolveInstalledPackageProvenance",
+ "callExpression": "resolveInstalledPackageProvenance",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "isPathInsideResolved",
+ "callExpression": "isPathInsideResolved",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "symlink",
+ "callExpression": "symlink",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "realpath",
+ "callExpression": "realpath",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "writeFile",
+ "callExpression": "writeFile",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/installedPackageProvenance.test.ts",
+ "toSymbol": "bunLockfileReferencesPackageVersion",
+ "callExpression": "bunLockfileReferencesPackageVersion",
+ "sourcePath": "src/host/installedPackageProvenance.test.ts"
+ },
+ {
+ "fromSymbol": "resolveInstalledPackageProvenance",
+ "toSymbol": "realpath",
+ "callExpression": "realpath",
+ "sourcePath": "src/host/installedPackageProvenance.ts"
+ },
{
"fromSymbol": "collectRelativeFiles",
"toSymbol": "readdir",
@@ -25187,6 +25539,120 @@
"callExpression": "scheduler.cancel",
"sourcePath": "src/runtime/indicatorRefreshCoordinator.ts"
},
+ {
+ "fromSymbol": "flushNext",
+ "toSymbol": "__type",
+ "callExpression": "entry[1]",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "describe",
+ "callExpression": "describe",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "it",
+ "callExpression": "it",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "createFakeScheduler",
+ "callExpression": "createFakeScheduler",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "createIndicatorSettleCoordinator",
+ "callExpression": "createIndicatorSettleCoordinator",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "trigger",
+ "callExpression": "coordinator.trigger",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "isActive",
+ "callExpression": "coordinator.isActive",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "flushNext",
+ "callExpression": "scheduler.flushNext",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.test.ts",
+ "toSymbol": "cancel",
+ "callExpression": "coordinator.cancel",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.test.ts"
+ },
+ {
+ "fromSymbol": "clearPending",
+ "toSymbol": "cancel",
+ "callExpression": "options.scheduler.cancel",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "cancel",
+ "toSymbol": "clearPending",
+ "callExpression": "clearPending",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "scheduleNext",
+ "toSymbol": "schedule",
+ "callExpression": "options.scheduler.schedule",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/indicatorSettleCoordinator.ts",
+ "toSymbol": "sampleNext",
+ "callExpression": "sampleNext",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "sampleNext",
+ "toSymbol": "sample",
+ "callExpression": "options.sample",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "sampleNext",
+ "toSymbol": "scheduleNext",
+ "callExpression": "scheduleNext",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "sampleNext",
+ "toSymbol": "cancel",
+ "callExpression": "cancel",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "sampleNext",
+ "toSymbol": "areEqual",
+ "callExpression": "options.areEqual",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
+ {
+ "fromSymbol": "trigger",
+ "toSymbol": "scheduleNext",
+ "callExpression": "scheduleNext",
+ "sourcePath": "src/runtime/indicatorSettleCoordinator.ts"
+ },
{
"fromSymbol": "src/runtime/interactionPolicy.test.ts",
"toSymbol": "describe",
@@ -25458,21 +25924,27 @@
"sourcePath": "src/runtime/stationarySelection.ts"
},
{
- "fromSymbol": "measureUnsupportedView",
+ "fromSymbol": "measureUnsupportedWebView",
"toSymbol": "isWebElementLike",
"callExpression": "isWebElementLike",
"sourcePath": "src/runtime/stationarySelection.ts"
},
{
- "fromSymbol": "measureUnsupportedView",
+ "fromSymbol": "measureUnsupportedWebView",
"toSymbol": "unionRects",
"callExpression": "unionRects",
"sourcePath": "src/runtime/stationarySelection.ts"
},
{
- "fromSymbol": "measureUnsupportedView",
- "toSymbol": "measureNativeView",
- "callExpression": "measureNativeView",
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "registerView",
+ "callExpression": "context.registerView",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "requestRefresh",
+ "callExpression": "context?.requestRefresh",
"sourcePath": "src/runtime/stationarySelection.ts"
},
{
@@ -25513,8 +25985,20 @@
},
{
"fromSymbol": "measure",
- "toSymbol": "measureUnsupportedView",
- "callExpression": "measureUnsupportedView",
+ "toSymbol": "measureUnsupportedWebView",
+ "callExpression": "measureUnsupportedWebView",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "registerView",
+ "toSymbol": "registerUnsupportedNode",
+ "callExpression": "ctx.registerUnsupportedNode",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "measure",
+ "toSymbol": "measureNativeView",
+ "callExpression": "measureNativeView",
"sourcePath": "src/runtime/stationarySelection.ts"
},
{
@@ -25553,6 +26037,24 @@
"callExpression": "requestIndicatorRefresh",
"sourcePath": "src/runtime/stationarySelection.ts"
},
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "createIndicatorSettleCoordinator",
+ "callExpression": "createIndicatorSettleCoordinator",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "trigger",
+ "callExpression": "settleCoordinatorRef.current.trigger",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
+ {
+ "fromSymbol": "src/runtime/stationarySelection.ts",
+ "toSymbol": "cancel",
+ "callExpression": "settleCoordinatorRef.current?.cancel",
+ "sourcePath": "src/runtime/stationarySelection.ts"
+ },
{
"fromSymbol": "completePendingInteraction",
"toSymbol": "completePendingInteraction",
@@ -25595,12 +26097,6 @@
"callExpression": "areIndicatorRectsEqual",
"sourcePath": "src/runtime/stationarySelection.ts"
},
- {
- "fromSymbol": "src/runtime/stationarySelection.ts",
- "toSymbol": "refreshIndicatorRects",
- "callExpression": "refreshIndicatorRects",
- "sourcePath": "src/runtime/stationarySelection.ts"
- },
{
"fromSymbol": "src/runtime/stationarySelection.ts",
"toSymbol": "cancelPendingRefresh",
@@ -27275,6 +27771,42 @@
"callExpression": "parseExpoGoogleFontsModuleConfig",
"sourcePath": "src/host/modules/catalog.ts"
},
+ {
+ "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toSymbol": "describe",
+ "callExpression": "describe",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toSymbol": "it",
+ "callExpression": "it",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toSymbol": "readFile",
+ "callExpression": "readFile",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toSymbol": "getPackageJson",
+ "callExpression": "getPackageJson",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toSymbol": "expect",
+ "callExpression": "expect",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
+ {
+ "fromSymbol": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts",
+ "toSymbol": "getBabelConfigJs",
+ "callExpression": "getBabelConfigJs",
+ "sourcePath": "src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts"
+ },
{
"fromSymbol": "src/host/orchestrator/generatedAppFiles.test.ts",
"toSymbol": "describe",
diff --git a/src/host/generatedAdminExpoWeb.smoke.test.ts b/src/host/generatedAdminExpoWeb.smoke.test.ts
index b46e0fad..416b9259 100644
--- a/src/host/generatedAdminExpoWeb.smoke.test.ts
+++ b/src/host/generatedAdminExpoWeb.smoke.test.ts
@@ -13,6 +13,11 @@ import path from 'node:path';
import type { AppManifest, UiNode } from '@ankhorage/contracts';
import { expect, test } from 'bun:test';
+import {
+ bunLockfileReferencesPackageVersion,
+ isPathInsideResolved,
+ resolveInstalledPackageProvenance,
+} from './installedPackageProvenance';
import { ModuleManager } from './orchestrator/moduleManager';
import { ProjectManager } from './orchestrator/projectManager';
import { satisfiesCaretSemverRange } from './orchestrator/semverRange';
@@ -147,6 +152,177 @@ function createScrollableRuntimeScreenRoot(): UiNode {
type: 'SmokeStudioProbe',
props: {},
},
+ {
+ id: 'native-layout-fixture',
+ type: 'Box',
+ props: {
+ testID: 'native-layout-fixture',
+ style: { gap: 12 },
+ },
+ children: [
+ {
+ id: 'native-row-parent',
+ type: 'SmokeLayoutBox',
+ props: {
+ testID: 'native-row-parent',
+ style: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ height: 72,
+ width: 320,
+ },
+ },
+ children: [
+ {
+ id: 'native-row-sibling-a',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Row A',
+ testID: 'native-row-sibling-a',
+ style: { height: 44, width: 70 },
+ },
+ },
+ {
+ id: 'native-row-target',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Row target',
+ stateful: true,
+ testID: 'native-row-target',
+ style: { height: 52, width: 130 },
+ },
+ },
+ {
+ id: 'native-row-sibling-b',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Row B',
+ testID: 'native-row-sibling-b',
+ style: { height: 40, width: 80 },
+ },
+ },
+ ],
+ },
+ {
+ id: 'native-flex-parent',
+ type: 'SmokeLayoutBox',
+ props: {
+ testID: 'native-flex-parent',
+ style: { flexDirection: 'row', height: 64, width: 320 },
+ },
+ children: [
+ {
+ id: 'native-flex-target',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Flex grow',
+ testID: 'native-flex-target',
+ style: { flexGrow: 1, height: 52 },
+ },
+ },
+ {
+ id: 'native-flex-sibling',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Fixed',
+ testID: 'native-flex-sibling',
+ style: { height: 52, width: 88 },
+ },
+ },
+ ],
+ },
+ {
+ id: 'native-align-parent',
+ type: 'SmokeLayoutBox',
+ props: {
+ testID: 'native-align-parent',
+ style: { height: 90, width: 320 },
+ },
+ children: [
+ {
+ id: 'native-align-target',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Align self',
+ testID: 'native-align-target',
+ style: { alignSelf: 'flex-end', height: 42, width: 140 },
+ },
+ },
+ ],
+ },
+ {
+ id: 'native-percentage-parent',
+ type: 'SmokeLayoutBox',
+ props: {
+ testID: 'native-percentage-parent',
+ style: { height: 60, width: 300 },
+ },
+ children: [
+ {
+ id: 'native-percentage-target',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Fifty percent',
+ testID: 'native-percentage-target',
+ style: { height: 44, width: '50%' },
+ },
+ },
+ ],
+ },
+ {
+ id: 'native-absolute-parent',
+ type: 'SmokeLayoutBox',
+ props: {
+ testID: 'native-absolute-parent',
+ style: { height: 110, position: 'relative', width: 320 },
+ },
+ children: [
+ {
+ id: 'native-absolute-sibling',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Flow sibling',
+ testID: 'native-absolute-sibling',
+ style: { height: 40, width: 100 },
+ },
+ },
+ {
+ id: 'native-absolute-target',
+ type: 'SmokeLayoutBox',
+ props: {
+ label: 'Absolute',
+ testID: 'native-absolute-target',
+ style: {
+ height: 52,
+ left: 150,
+ position: 'absolute',
+ top: 24,
+ width: 130,
+ },
+ },
+ },
+ ],
+ },
+ {
+ id: 'native-nested-scroll',
+ type: 'SmokeNestedScroll',
+ props: {
+ testID: 'native-nested-scroll',
+ },
+ children: [
+ ...Array.from({ length: 12 }, (_, index) => ({
+ id: `native-nested-scroll-row-${index}`,
+ type: 'SmokeLayoutBox',
+ props: {
+ label: `Nested row ${index + 1}`,
+ testID: `native-nested-scroll-row-${index}`,
+ style: { height: 64, width: 260 },
+ },
+ })),
+ ],
+ },
+ ],
+ },
...Array.from({ length: 16 }, (_, index) => ({
id: `dashboard-runtime-row-${index}`,
type: 'Text',
@@ -161,10 +337,40 @@ function createScrollableRuntimeScreenRoot(): UiNode {
};
}
+test('native unsupported layout fixture covers layout-sensitive Runtime relationships', () => {
+ const root = createScrollableRuntimeScreenRoot();
+ const nodes = new Map();
+ const visit = (node: UiNode): void => {
+ nodes.set(node.id, node);
+ node.children?.forEach(visit);
+ };
+ visit(root);
+
+ expect(nodes.get('native-row-parent')?.children?.length).toBe(3);
+ expect(nodes.get('native-flex-target')?.props?.style).toMatchObject({ flexGrow: 1 });
+ expect(nodes.get('native-align-target')?.props?.style).toMatchObject({
+ alignSelf: 'flex-end',
+ });
+ expect(nodes.get('native-percentage-target')?.props?.style).toMatchObject({
+ width: '50%',
+ });
+ expect(nodes.get('native-absolute-target')?.props?.style).toMatchObject({
+ position: 'absolute',
+ });
+ expect(nodes.get('native-row-target')?.props?.stateful).toBe(true);
+ expect(nodes.get('native-nested-scroll')?.children?.length).toBe(12);
+});
+
adminWebSmokeTest(
'loads generated Studio admin routes through Expo web without a theme update loop',
async () => {
- const workspaceRoot = await mkdtemp(path.join(tmpdir(), 'ankh-admin-web-smoke-'));
+ const rawPreservedWorkspaceRoot: unknown = process.env.ANKH_STUDIO_ADMIN_WEB_SMOKE_WORKSPACE;
+ const preservedWorkspaceRoot =
+ typeof rawPreservedWorkspaceRoot === 'string' && rawPreservedWorkspaceRoot.length > 0
+ ? rawPreservedWorkspaceRoot
+ : undefined;
+ const workspaceRoot =
+ preservedWorkspaceRoot ?? (await mkdtemp(path.join(tmpdir(), 'ankh-admin-web-smoke-')));
const debugPort = await reservePort();
let expoProcess: ChildProcessWithoutNullStreams | null = null;
let chromeProcess: ChildProcessWithoutNullStreams | null = null;
@@ -187,7 +393,11 @@ adminWebSmokeTest(
) as { dependencies?: Record };
const generatedZoraRange = generatedPackage.dependencies?.['@ankhorage/zora'];
const installedZoraRoot = path.join(projectRoot, 'node_modules', '@ankhorage', 'zora');
- const resolvedZoraRoot = await realpath(installedZoraRoot);
+ const { resolvedCandidatePath: resolvedZoraRoot, resolvedWorkspacePath } =
+ await resolveInstalledPackageProvenance(workspaceRoot, installedZoraRoot);
+ const resolvedRepositoryZoraRoot = await realpath(
+ path.join(process.cwd(), 'node_modules', '@ankhorage', 'zora'),
+ );
const resolvedZoraPackage = JSON.parse(
await readFile(path.join(resolvedZoraRoot, 'package.json'), 'utf8'),
) as { version?: string };
@@ -198,11 +408,15 @@ adminWebSmokeTest(
expect(
satisfiesCaretSemverRange(resolvedZoraPackage.version ?? '', generatedZoraRange ?? ''),
).toBe(true);
- expect(isPathInside(workspaceRoot, installedZoraRoot)).toBe(true);
- expect(isPathInside(process.cwd(), installedZoraRoot)).toBe(false);
- expect(generatedLockfile).toContain(
- `"@ankhorage/zora": ["@ankhorage/zora@${resolvedZoraPackage.version}"`,
- );
+ expect(isPathInsideResolved(resolvedWorkspacePath, resolvedZoraRoot)).toBe(true);
+ expect(resolvedZoraRoot).not.toBe(resolvedRepositoryZoraRoot);
+ expect(
+ bunLockfileReferencesPackageVersion(
+ generatedLockfile,
+ '@ankhorage/zora',
+ resolvedZoraPackage.version ?? '',
+ ),
+ ).toBe(true);
expect(rootLayout).toContain('function GeneratedZoraThemeConfigSync');
expect(rootLayout).toContain('lastSyncedThemeConfigSignatureRef');
expect(rootLayout).not.toContain('}, [setThemeConfig, themeConfig]);');
@@ -247,7 +461,9 @@ adminWebSmokeTest(
await studioApi?.close();
stopProcess(chromeProcess);
stopProcess(expoProcess);
- await rm(workspaceRoot, { force: true, recursive: true });
+ if (!preservedWorkspaceRoot) {
+ await rm(workspaceRoot, { force: true, recursive: true });
+ }
}
},
TEST_TIMEOUT_MS,
@@ -269,6 +485,21 @@ interface UnsupportedGeometrySnapshot {
readonly pointerEvents: string;
}
+interface CapturedLayoutRect {
+ readonly height: number;
+ readonly width: number;
+ readonly x: number;
+ readonly y: number;
+}
+
+interface CapturedLayoutSnapshot {
+ readonly instances: Readonly>;
+ readonly rects: Readonly>;
+ readonly scrollContentSizes: Readonly<
+ Record
+ >;
+}
+
async function verifyDesktopSelectionAndUnsupportedGeometry(page: ChromePage): Promise {
const initialGeometry = await waitForUnsupportedGeometry(page, 15_000);
@@ -430,25 +661,77 @@ async function verifyDesktopSelectionAndUnsupportedGeometry(page: ChromePage): P
'studio-stationary-selection-root:edit:dashboard-runtime-row-1:3',
);
- await page.evaluate(`globalThis.__studioSmokeTogglePreview?.()`);
- await waitForStudioSmokeState(
- page,
- 'mode=preview;selection=dashboard-runtime-row-1;changes=3',
- 15_000,
+ const privateStateBefore = await page.evaluate(
+ `document.querySelector('[data-testid="native-row-target-private-state"]')?.textContent ?? ''`,
+ );
+ const statefulTarget = await page.readRuntimeNodeCenter('native-row-target');
+ await page.touchTap(statefulTarget.x, statefulTarget.y);
+ await waitForStudioSmokeState(page, 'mode=edit;selection=native-row-target;changes=4', 15_000);
+ const privateStateAfterInteraction = await page.evaluate(
+ `document.querySelector('[data-testid="native-row-target-private-state"]')?.textContent ?? ''`,
);
+ expect(privateStateAfterInteraction).not.toBe(privateStateBefore);
+
+ const editLayoutSnapshot = await page.captureSmokeLayout();
+ expect(editLayoutSnapshot.rects['native-row-parent']).not.toBeNull();
+ expect(editLayoutSnapshot.rects['native-flex-target']).not.toBeNull();
+ expect(editLayoutSnapshot.rects['native-align-target']).not.toBeNull();
+ expect(editLayoutSnapshot.rects['native-percentage-target']).not.toBeNull();
+ expect(editLayoutSnapshot.rects['native-absolute-target']).not.toBeNull();
+ expect(editLayoutSnapshot.scrollContentSizes['native-nested-scroll']?.height).toBeGreaterThan(
+ 220,
+ );
+
+ await page.evaluate(`globalThis.__studioSmokeTogglePreview?.()`);
+ await waitForStudioSmokeState(page, 'mode=preview;selection=native-row-target;changes=4', 15_000);
expect(
await page.evaluate(
`document.getElementById('studio-unsupported-indicator-unsupported-runtime-target') === null`,
),
).toBe(true);
+ const previewLayoutSnapshot = await page.captureSmokeLayout();
+ expectCapturedLayoutsToMatch(previewLayoutSnapshot, editLayoutSnapshot);
+ expect(
+ await page.evaluate(
+ `document.querySelector('[data-testid="native-row-target-private-state"]')?.textContent ?? ''`,
+ ),
+ ).toBe(privateStateAfterInteraction);
- await page.mouseClick(desktopTarget.x, desktopTarget.y);
+ const previewDesktopTarget = await page.readRuntimeNodeCenter('desktop-pointer-target');
+ await page.mouseClick(previewDesktopTarget.x, previewDesktopTarget.y);
await Bun.sleep(250);
expect(await page.readStudioSmokeState()).toBe(
- 'mode=preview;selection=dashboard-runtime-row-1;changes=3',
+ 'mode=preview;selection=native-row-target;changes=4',
);
}
+function expectCapturedLayoutsToMatch(
+ actual: CapturedLayoutSnapshot,
+ expected: CapturedLayoutSnapshot,
+): void {
+ const actualAnchor = actual.rects['native-row-parent'];
+ const expectedAnchor = expected.rects['native-row-parent'];
+ expect(actualAnchor).not.toBeNull();
+ expect(expectedAnchor).not.toBeNull();
+ const viewportOffsetX = actualAnchor && expectedAnchor ? actualAnchor.x - expectedAnchor.x : 0;
+ const viewportOffsetY = actualAnchor && expectedAnchor ? actualAnchor.y - expectedAnchor.y : 0;
+
+ expect(actual.instances).toEqual(expected.instances);
+ expect(actual.scrollContentSizes).toEqual(expected.scrollContentSizes);
+ expect(Object.keys(actual.rects).sort()).toEqual(Object.keys(expected.rects).sort());
+ for (const [testID, expectedRect] of Object.entries(expected.rects)) {
+ const actualRect = actual.rects[testID];
+ expect(actualRect == null).toBe(expectedRect == null);
+ if (!actualRect || !expectedRect) {
+ continue;
+ }
+ expect(Math.abs(actualRect.x - viewportOffsetX - expectedRect.x)).toBeLessThanOrEqual(2);
+ expect(Math.abs(actualRect.y - viewportOffsetY - expectedRect.y)).toBeLessThanOrEqual(2);
+ expect(Math.abs(actualRect.width - expectedRect.width)).toBeLessThanOrEqual(2);
+ expect(Math.abs(actualRect.height - expectedRect.height)).toBeLessThanOrEqual(2);
+ }
+}
+
async function waitForUnsupportedGeometry(
page: ChromePage,
timeoutMs: number,
@@ -501,11 +784,6 @@ function rectsOverlap(left: BrowserRect, right: BrowserRect): boolean {
);
}
-function isPathInside(parentPath: string, candidatePath: string): boolean {
- const relativePath = path.relative(parentPath, candidatePath);
- return relativePath !== '' && !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..';
-}
-
async function createGeneratedAdminProject(workspaceRoot: string): Promise {
await mkdir(path.join(workspaceRoot, 'apps', 'studio'), { recursive: true });
await writeFile(
@@ -612,9 +890,89 @@ async function writeSmokeRuntimeExtensions(projectRoot: string): Promise {
await writeFile(
path.join(generatedRoot, 'SmokeStudioComponents.tsx'),
`import { useStudio } from '@ankhorage/studio';
+import { useStudioUnsupportedNodeMeasurement } from '@ankhorage/studio/runtime';
import { Box, Text } from '@ankhorage/zora';
-import React, { useEffect, useRef, useState } from 'react';
-import { View } from 'react-native';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+ Pressable,
+ ScrollView,
+ Text as NativeText,
+ View,
+ type ViewStyle,
+} from 'react-native';
+
+type ViewRef = React.ElementRef;
+
+interface SmokeRect {
+ readonly height: number;
+ readonly width: number;
+ readonly x: number;
+ readonly y: number;
+}
+
+const smokeMeasurements = new Map Promise>();
+const smokeInstances = new Map();
+const smokeScrollContentSizes = new Map();
+let nextSmokeInstanceId = 1;
+
+function useSmokeViewRegistration(testID: string | undefined) {
+ const measurement = useStudioUnsupportedNodeMeasurement();
+ const viewRef = useRef(null);
+ const instanceIdRef = useRef(nextSmokeInstanceId++);
+
+ const setViewRef = useCallback(
+ (view: ViewRef | null) => {
+ viewRef.current = view;
+ measurement.ref(view);
+ },
+ [measurement.ref],
+ );
+
+ useEffect(() => {
+ if (!testID) return;
+ const measure = () =>
+ new Promise((resolve) => {
+ const view = viewRef.current;
+ if (!view) {
+ resolve(null);
+ return;
+ }
+ view.measureInWindow((x, y, width, height) => {
+ resolve({ x, y, width, height });
+ });
+ });
+ smokeMeasurements.set(testID, measure);
+ smokeInstances.set(testID, instanceIdRef.current);
+ return () => {
+ if (smokeMeasurements.get(testID) === measure) {
+ smokeMeasurements.delete(testID);
+ smokeInstances.delete(testID);
+ }
+ };
+ }, [testID]);
+
+ return {
+ instanceId: instanceIdRef.current,
+ onLayout: measurement.onLayout,
+ ref: setViewRef,
+ };
+}
+
+async function captureSmokeLayout() {
+ const rects = Object.fromEntries(
+ await Promise.all(
+ [...smokeMeasurements.entries()].map(async ([testID, measure]) => [
+ testID,
+ await measure(),
+ ]),
+ ),
+ );
+ return {
+ instances: Object.fromEntries(smokeInstances),
+ rects,
+ scrollContentSizes: Object.fromEntries(smokeScrollContentSizes),
+ };
+}
export function SmokeUnsupported({
label,
@@ -623,8 +981,11 @@ export function SmokeUnsupported({
label?: string;
testID?: string;
}) {
+ const registration = useSmokeViewRegistration(testID);
return (
setPrivateCount((count) => count + 1) : undefined}
+ testID={testID}
+ style={[
+ {
+ backgroundColor: '#e2e8f0',
+ borderColor: '#64748b',
+ borderWidth: 1,
+ padding: 6,
+ },
+ style,
+ ]}
+ >
+ {label ? {label} : null}
+ {stateful ? (
+
+ {\`instance=\${registration.instanceId};private=\${privateCount}\`}
+
+ ) : null}
+ {children}
+
+ );
+}
+
+export function SmokeNestedScroll({
+ children,
+ testID,
+}: {
+ children?: React.ReactNode;
+ testID?: string;
+}) {
+ const registration = useSmokeViewRegistration(testID);
+ return (
+
+ {
+ if (testID) smokeScrollContentSizes.set(testID, { width, height });
+ }}
+ testID={testID ? \`\${testID}-scroll-view\` : undefined}
+ contentContainerStyle={{ gap: 8, padding: 8 }}
+ style={{ borderColor: '#94a3b8', borderWidth: 1 }}
+ >
+ {children}
+
+
+ );
+}
+
export function SmokeStudioProbe() {
const studio = useStudio();
const previousSelectedNodeIdRef = useRef(studio.selectedNodeId);
const [selectionChangeCount, setSelectionChangeCount] = useState(0);
+ const [layoutSnapshot, setLayoutSnapshot] = useState('not-captured');
useEffect(() => {
const smokeGlobal = globalThis as typeof globalThis & {
+ __studioSmokeCaptureLayout?: typeof captureSmokeLayout;
__studioSmokeTogglePreview?: () => void;
};
+ smokeGlobal.__studioSmokeCaptureLayout = captureSmokeLayout;
smokeGlobal.__studioSmokeTogglePreview = studio.togglePreviewMode;
return () => {
+ delete smokeGlobal.__studioSmokeCaptureLayout;
delete smokeGlobal.__studioSmokeTogglePreview;
};
}, [studio.togglePreviewMode]);
@@ -664,6 +1102,31 @@ export function SmokeStudioProbe() {
{\`mode=\${studio.previewMode ? 'preview' : 'edit'};selection=\${studio.selectedNodeId ?? 'none'};changes=\${selectionChangeCount}\`}
+
+ Toggle Edit Preview
+
+ {
+ void captureSmokeLayout().then((snapshot) => {
+ setLayoutSnapshot(JSON.stringify(snapshot));
+ });
+ }}
+ testID="studio-smoke-capture-layout"
+ >
+ Capture native layout
+
+
+
+ {layoutSnapshot === 'not-captured' ? layoutSnapshot : 'captured'}
+
+
);
}
@@ -673,9 +1136,16 @@ export function SmokeStudioProbe() {
await writeFile(
path.join(generatedRoot, 'appExtensionRegistry.ts'),
`import type { ComponentRegistry } from '@ankhorage/runtime';
-import { SmokeStudioProbe, SmokeUnsupported } from './SmokeStudioComponents';
+import {
+ SmokeLayoutBox,
+ SmokeNestedScroll,
+ SmokeStudioProbe,
+ SmokeUnsupported,
+} from './SmokeStudioComponents';
export const APP_EXTENSION_COMPONENT_REGISTRY: ComponentRegistry = {
+ SmokeLayoutBox,
+ SmokeNestedScroll,
SmokeStudioProbe,
SmokeUnsupported,
};
@@ -1032,6 +1502,12 @@ class ChromePage {
);
}
+ async captureSmokeLayout(): Promise {
+ return this.evaluate(
+ `globalThis.__studioSmokeCaptureLayout?.() ?? Promise.reject(new Error('Smoke layout capture unavailable'))`,
+ );
+ }
+
async readRuntimeNodeCenter(nodeId: string): Promise<{ readonly x: number; readonly y: number }> {
const encodedNodeId = encodeURIComponent(nodeId);
const center = await this.evaluate<{ readonly x: number; readonly y: number } | null>(`(() => {
@@ -1051,7 +1527,11 @@ class ChromePage {
};
const target = findRenderedElement(wrapper);
if (!(target instanceof HTMLElement)) return null;
- const rect = target.getBoundingClientRect();
+ let rect = target.getBoundingClientRect();
+ if (rect.top < 0 || rect.bottom > window.innerHeight) {
+ target.scrollIntoView({ block: 'center' });
+ rect = target.getBoundingClientRect();
+ }
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
})()`);
if (!center) {
diff --git a/src/host/installedPackageProvenance.test.ts b/src/host/installedPackageProvenance.test.ts
new file mode 100644
index 00000000..8becb791
--- /dev/null
+++ b/src/host/installedPackageProvenance.test.ts
@@ -0,0 +1,83 @@
+import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+
+import { afterEach, describe, expect, it } from 'bun:test';
+
+import {
+ bunLockfileReferencesPackageVersion,
+ isPathInsideResolved,
+ resolveInstalledPackageProvenance,
+} from './installedPackageProvenance';
+
+const temporaryRoots: string[] = [];
+
+afterEach(async () => {
+ await Promise.all(
+ temporaryRoots
+ .splice(0)
+ .map((temporaryRoot) => rm(temporaryRoot, { force: true, recursive: true })),
+ );
+});
+
+async function createTemporaryRoot(): Promise {
+ const temporaryRoot = await mkdtemp(path.join(tmpdir(), 'studio-package-provenance-'));
+ temporaryRoots.push(temporaryRoot);
+ return temporaryRoot;
+}
+
+describe('installed package provenance', () => {
+ it('accepts a real installed package directory inside the resolved workspace', async () => {
+ const temporaryRoot = await createTemporaryRoot();
+ const workspaceRoot = path.join(temporaryRoot, 'workspace');
+ const packageRoot = path.join(workspaceRoot, 'node_modules', '@ankhorage', 'zora');
+ await mkdir(packageRoot, { recursive: true });
+
+ const provenance = await resolveInstalledPackageProvenance(workspaceRoot, packageRoot);
+
+ expect(
+ isPathInsideResolved(provenance.resolvedWorkspacePath, provenance.resolvedCandidatePath),
+ ).toBe(true);
+ });
+
+ it('rejects a workspace symlink that resolves to a package outside the workspace', async () => {
+ const temporaryRoot = await createTemporaryRoot();
+ const workspaceRoot = path.join(temporaryRoot, 'workspace');
+ const externalPackageRoot = path.join(temporaryRoot, 'external', 'zora');
+ const installedPackageRoot = path.join(workspaceRoot, 'node_modules', '@ankhorage', 'zora');
+ await mkdir(path.dirname(installedPackageRoot), { recursive: true });
+ await mkdir(externalPackageRoot, { recursive: true });
+ await symlink(externalPackageRoot, installedPackageRoot);
+
+ const provenance = await resolveInstalledPackageProvenance(workspaceRoot, installedPackageRoot);
+
+ expect(
+ isPathInsideResolved(provenance.resolvedWorkspacePath, provenance.resolvedCandidatePath),
+ ).toBe(false);
+ });
+
+ it('rejects traversal and sibling paths that only share a string prefix', async () => {
+ const temporaryRoot = await createTemporaryRoot();
+ const workspaceRoot = path.join(temporaryRoot, 'workspace');
+ const siblingRoot = path.join(temporaryRoot, 'workspace-copy');
+ await mkdir(workspaceRoot, { recursive: true });
+ await mkdir(siblingRoot, { recursive: true });
+ const [resolvedWorkspaceRoot, resolvedSiblingRoot] = await Promise.all([
+ realpath(workspaceRoot),
+ realpath(path.join(workspaceRoot, '..', 'workspace-copy')),
+ ]);
+
+ expect(isPathInsideResolved(resolvedWorkspaceRoot, resolvedSiblingRoot)).toBe(false);
+ expect(resolvedSiblingRoot.startsWith(resolvedWorkspaceRoot)).toBe(true);
+ });
+
+ it('matches the exact installed package version in the generated lockfile', async () => {
+ const temporaryRoot = await createTemporaryRoot();
+ const lockfilePath = path.join(temporaryRoot, 'bun.lock');
+ const lockfile = '"@ankhorage/zora": ["@ankhorage/zora@2.9.1", "", {}, "sha512-test"]\n';
+ await writeFile(lockfilePath, lockfile);
+
+ expect(bunLockfileReferencesPackageVersion(lockfile, '@ankhorage/zora', '2.9.1')).toBe(true);
+ expect(bunLockfileReferencesPackageVersion(lockfile, '@ankhorage/zora', '2.9.0')).toBe(false);
+ });
+});
diff --git a/src/host/installedPackageProvenance.ts b/src/host/installedPackageProvenance.ts
new file mode 100644
index 00000000..4a039521
--- /dev/null
+++ b/src/host/installedPackageProvenance.ts
@@ -0,0 +1,45 @@
+import { realpath } from 'node:fs/promises';
+import path from 'node:path';
+
+export interface InstalledPackageProvenance {
+ readonly resolvedCandidatePath: string;
+ readonly resolvedWorkspacePath: string;
+}
+
+export function isPathInsideResolved(
+ resolvedParentPath: string,
+ resolvedCandidatePath: string,
+): boolean {
+ const relativePath = path.relative(resolvedParentPath, resolvedCandidatePath);
+ return (
+ relativePath !== '' &&
+ relativePath !== '..' &&
+ !relativePath.startsWith(`..${path.sep}`) &&
+ !path.isAbsolute(relativePath)
+ );
+}
+
+export async function resolveInstalledPackageProvenance(
+ workspacePath: string,
+ candidatePath: string,
+): Promise {
+ const [resolvedWorkspacePath, resolvedCandidatePath] = await Promise.all([
+ realpath(workspacePath),
+ realpath(candidatePath),
+ ]);
+
+ return {
+ resolvedCandidatePath,
+ resolvedWorkspacePath,
+ };
+}
+
+export function bunLockfileReferencesPackageVersion(
+ lockfile: string,
+ packageName: string,
+ version: string,
+): boolean {
+ return lockfile.includes(
+ `${JSON.stringify(packageName)}: [${JSON.stringify(`${packageName}@${version}`)}`,
+ );
+}
diff --git a/src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts b/src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts
new file mode 100644
index 00000000..05c8925b
--- /dev/null
+++ b/src/host/orchestrator/expoSdk54AnimationCompatibility.test.ts
@@ -0,0 +1,54 @@
+import { readFile } from 'node:fs/promises';
+import path from 'node:path';
+
+import { describe, expect, it } from 'bun:test';
+
+import { EXPO_SDK_54_ANIMATION_COMPATIBILITY } from './expoSdk54AnimationCompatibility';
+import { getBabelConfigJs, getPackageJson } from './templates';
+
+interface StudioAppPackage {
+ readonly dependencies?: Readonly>;
+}
+
+describe('Expo SDK 54 first-party animation compatibility', () => {
+ it('keeps generated apps and the supported Studio Expo app on one canonical stack', async () => {
+ const repositoryRoot = path.resolve(import.meta.dir, '..', '..', '..');
+ const studioPackage = JSON.parse(
+ await readFile(path.join(repositoryRoot, 'apps', 'studio', 'package.json'), 'utf8'),
+ ) as StudioAppPackage;
+ const studioBabelConfig = await readFile(
+ path.join(repositoryRoot, 'apps', 'studio', 'babel.config.js'),
+ 'utf8',
+ );
+ const generatedPackage = getPackageJson({
+ name: 'generated-sdk-54-app',
+ includeStudio: true,
+ });
+ const generatedDependencies = generatedPackage.dependencies as Record;
+ const studioDependencies = studioPackage.dependencies ?? {};
+
+ expect(
+ generatedDependencies.expo?.startsWith(`~${EXPO_SDK_54_ANIMATION_COMPATIBILITY.expoSdk}.`),
+ ).toBe(true);
+ expect(
+ studioDependencies.expo?.startsWith(`~${EXPO_SDK_54_ANIMATION_COMPATIBILITY.expoSdk}.`),
+ ).toBe(true);
+ expect(generatedDependencies['react-native-reanimated']).toBe(
+ EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated,
+ );
+ expect(generatedDependencies['react-native-worklets']).toBe(
+ EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets,
+ );
+ expect(studioDependencies['react-native-reanimated']).toBe(
+ EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated,
+ );
+ expect(studioDependencies['react-native-worklets']).toBe(
+ EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets,
+ );
+ expect(getBabelConfigJs()).toContain(`'${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}'`);
+ expect(studioBabelConfig).toContain(`'${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}'`);
+ expect(studioBabelConfig).not.toContain("'react-native-reanimated/plugin'");
+ expect(Object.values(studioDependencies)).not.toContain('~4.1.1');
+ expect(Object.values(studioDependencies)).not.toContain('0.5.1');
+ });
+});
diff --git a/src/host/orchestrator/expoSdk54AnimationCompatibility.ts b/src/host/orchestrator/expoSdk54AnimationCompatibility.ts
new file mode 100644
index 00000000..51fab6e8
--- /dev/null
+++ b/src/host/orchestrator/expoSdk54AnimationCompatibility.ts
@@ -0,0 +1,6 @@
+export const EXPO_SDK_54_ANIMATION_COMPATIBILITY = {
+ babelPlugin: 'react-native-worklets/plugin',
+ expoSdk: '54',
+ reanimated: '4.3.0',
+ worklets: '0.8.3',
+} as const;
diff --git a/src/host/orchestrator/templates.test.ts b/src/host/orchestrator/templates.test.ts
index 83fa1be1..d1fa0151 100644
--- a/src/host/orchestrator/templates.test.ts
+++ b/src/host/orchestrator/templates.test.ts
@@ -1,11 +1,7 @@
import { describe, expect, it } from 'bun:test';
-import {
- GENERATED_ANIMATION_DEPENDENCY_PAIR,
- getAppConfigTs,
- getBabelConfigJs,
- getPackageJson,
-} from './templates';
+import { EXPO_SDK_54_ANIMATION_COMPATIBILITY } from './expoSdk54AnimationCompatibility';
+import { getAppConfigTs, getBabelConfigJs, getPackageJson } from './templates';
describe('generated OAuth scaffold templates', () => {
it('pins the canonical runtime, ZORA, Supabase auth, and Expo persistence dependencies', () => {
@@ -34,21 +30,22 @@ describe('generated OAuth scaffold templates', () => {
const pkg = getPackageJson({ name: 'native-app', includeStudio: true });
const dependencies = pkg.dependencies as Record;
- expect(GENERATED_ANIMATION_DEPENDENCY_PAIR).toEqual({
+ expect(EXPO_SDK_54_ANIMATION_COMPATIBILITY).toEqual({
+ babelPlugin: 'react-native-worklets/plugin',
expoSdk: '54',
reanimated: '4.3.0',
worklets: '0.8.3',
});
expect(dependencies['react-native-reanimated']).toBe(
- GENERATED_ANIMATION_DEPENDENCY_PAIR.reanimated,
+ EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated,
);
expect(dependencies['react-native-worklets']).toBe(
- GENERATED_ANIMATION_DEPENDENCY_PAIR.worklets,
+ EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets,
);
expect(getPackageJson({ name: 'second-native-app', includeStudio: true }).dependencies).toEqual(
pkg.dependencies,
);
- expect(getBabelConfigJs()).toContain("'react-native-worklets/plugin'");
+ expect(getBabelConfigJs()).toContain(`'${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}'`);
expect(getBabelConfigJs()).not.toContain("'react-native-reanimated/plugin'");
});
diff --git a/src/host/orchestrator/templates.ts b/src/host/orchestrator/templates.ts
index 024e7ea3..373164a1 100644
--- a/src/host/orchestrator/templates.ts
+++ b/src/host/orchestrator/templates.ts
@@ -6,6 +6,8 @@ import {
resolveExpoRuntimeNativeOutput,
} from '@ankhorage/expo-runtime/planning';
+import { EXPO_SDK_54_ANIMATION_COMPATIBILITY } from './expoSdk54AnimationCompatibility.js';
+
export type GeneratedAuthProvider = 'supabase' | null;
export type GeneratedStorageProvider = 'supabase' | null;
const EXPO_MODULES_CORE_VERSION = '~3.0.30';
@@ -15,11 +17,6 @@ const SUPABASE_AUTH_VERSION = '^1.0.0';
const ZORA_VERSION = '^2.9.0';
const EXPO_SECURE_STORE_VERSION = '~15.0.8';
const EXPO_WEB_BROWSER_VERSION = '~15.0.11';
-export const GENERATED_ANIMATION_DEPENDENCY_PAIR = {
- expoSdk: '54',
- reanimated: '4.3.0',
- worklets: '0.8.3',
-} as const;
const RESERVED_NATIVE_IDENTIFIER_SEGMENTS = new Set(
[
@@ -287,7 +284,7 @@ export function getBabelConfigJs() {
},
},
],
- 'react-native-worklets/plugin',
+ '${EXPO_SDK_54_ANIMATION_COMPATIBILITY.babelPlugin}',
],
};
};
@@ -362,12 +359,12 @@ export function getPackageJson(args: {
'react-dom': '19.1.0',
'react-native': '0.81.5',
'react-native-gesture-handler': '~2.28.0',
- 'react-native-reanimated': GENERATED_ANIMATION_DEPENDENCY_PAIR.reanimated,
+ 'react-native-reanimated': EXPO_SDK_54_ANIMATION_COMPATIBILITY.reanimated,
'react-native-safe-area-context': '~5.6.0',
'react-native-screens': '~4.16.0',
'react-native-svg': '~15.12.1',
'react-native-web': '^0.21.2',
- 'react-native-worklets': GENERATED_ANIMATION_DEPENDENCY_PAIR.worklets,
+ 'react-native-worklets': EXPO_SDK_54_ANIMATION_COMPATIBILITY.worklets,
'reanimated-color-picker': '^4.2.0',
},
devDependencies: {
diff --git a/src/runtime/indicatorSettleCoordinator.test.ts b/src/runtime/indicatorSettleCoordinator.test.ts
new file mode 100644
index 00000000..6cd0524f
--- /dev/null
+++ b/src/runtime/indicatorSettleCoordinator.test.ts
@@ -0,0 +1,173 @@
+import { describe, expect, it } from 'bun:test';
+
+import { createIndicatorSettleCoordinator } from './indicatorSettleCoordinator';
+
+function createFakeScheduler() {
+ let nextHandle = 1;
+ const callbacks = new Map void>();
+
+ return {
+ callbacks,
+ scheduler: {
+ cancel: (handle: number) => {
+ callbacks.delete(handle);
+ },
+ schedule: (callback: () => void, _delayMs: number) => {
+ const handle = nextHandle++;
+ callbacks.set(handle, callback);
+ return handle;
+ },
+ },
+ flushNext: async () => {
+ const entry = callbacks.entries().next().value as readonly [number, () => void] | undefined;
+ if (!entry) {
+ return;
+ }
+ callbacks.delete(entry[0]);
+ entry[1]();
+ await Promise.resolve();
+ await Promise.resolve();
+ },
+ };
+}
+
+describe('indicator settle coordinator', () => {
+ it('keeps rapid triggers in one active settle sequence', () => {
+ const scheduler = createFakeScheduler();
+ const coordinator = createIndicatorSettleCoordinator({
+ areEqual: Object.is,
+ intervalMs: 60,
+ maxSamples: 10,
+ sample: () => Promise.resolve(1),
+ scheduler: scheduler.scheduler,
+ stableSampleCount: 3,
+ });
+
+ expect(coordinator.trigger()).toBe(true);
+ expect(coordinator.trigger()).toBe(false);
+ expect(coordinator.trigger()).toBe(false);
+ expect(scheduler.callbacks.size).toBe(1);
+ expect(coordinator.isActive()).toBe(true);
+ });
+
+ it('restarts the same sequence when new input arrives', async () => {
+ const scheduler = createFakeScheduler();
+ let nextSnapshot = 1;
+ const coordinator = createIndicatorSettleCoordinator({
+ areEqual: Object.is,
+ intervalMs: 60,
+ maxSamples: 10,
+ sample: () => Promise.resolve(nextSnapshot),
+ scheduler: scheduler.scheduler,
+ stableSampleCount: 2,
+ });
+
+ coordinator.trigger();
+ await scheduler.flushNext();
+ expect(scheduler.callbacks.size).toBe(1);
+
+ nextSnapshot = 2;
+ expect(coordinator.trigger()).toBe(false);
+ expect(scheduler.callbacks.size).toBe(1);
+ await scheduler.flushNext();
+
+ expect(coordinator.isActive()).toBe(true);
+ expect(scheduler.callbacks.size).toBe(1);
+ });
+
+ it('stops after the required stable samples', async () => {
+ const scheduler = createFakeScheduler();
+ let sampleCount = 0;
+ const coordinator = createIndicatorSettleCoordinator({
+ areEqual: Object.is,
+ intervalMs: 60,
+ maxSamples: 10,
+ sample: () => {
+ sampleCount += 1;
+ return Promise.resolve(4);
+ },
+ scheduler: scheduler.scheduler,
+ stableSampleCount: 3,
+ });
+
+ coordinator.trigger();
+ await scheduler.flushNext();
+ await scheduler.flushNext();
+ await scheduler.flushNext();
+
+ expect(sampleCount).toBe(3);
+ expect(coordinator.isActive()).toBe(false);
+ expect(scheduler.callbacks.size).toBe(0);
+ });
+
+ it('stops at the maximum bound when geometry never stabilizes', async () => {
+ const scheduler = createFakeScheduler();
+ let snapshot = 0;
+ const coordinator = createIndicatorSettleCoordinator({
+ areEqual: Object.is,
+ intervalMs: 60,
+ maxSamples: 4,
+ sample: () => Promise.resolve(++snapshot),
+ scheduler: scheduler.scheduler,
+ stableSampleCount: 3,
+ });
+
+ coordinator.trigger();
+ await scheduler.flushNext();
+ await scheduler.flushNext();
+ await scheduler.flushNext();
+ await scheduler.flushNext();
+
+ expect(snapshot).toBe(4);
+ expect(coordinator.isActive()).toBe(false);
+ expect(scheduler.callbacks.size).toBe(0);
+ });
+
+ it('cancels all pending work for Preview or unmount cleanup', async () => {
+ const scheduler = createFakeScheduler();
+ let sampleCount = 0;
+ const coordinator = createIndicatorSettleCoordinator({
+ areEqual: Object.is,
+ intervalMs: 60,
+ maxSamples: 10,
+ sample: () => Promise.resolve(++sampleCount),
+ scheduler: scheduler.scheduler,
+ stableSampleCount: 3,
+ });
+
+ coordinator.trigger();
+ coordinator.cancel();
+ await scheduler.flushNext();
+ expect(sampleCount).toBe(0);
+ expect(coordinator.isActive()).toBe(false);
+
+ coordinator.trigger();
+ coordinator.cancel();
+ await scheduler.flushNext();
+ expect(sampleCount).toBe(0);
+ expect(scheduler.callbacks.size).toBe(0);
+ });
+
+ it('stops without rescheduling when measurement reports no unsupported nodes', async () => {
+ const scheduler = createFakeScheduler();
+ let sampleCount = 0;
+ const coordinator = createIndicatorSettleCoordinator({
+ areEqual: Object.is,
+ intervalMs: 60,
+ maxSamples: 10,
+ sample: () => {
+ sampleCount += 1;
+ return Promise.resolve(null);
+ },
+ scheduler: scheduler.scheduler,
+ stableSampleCount: 3,
+ });
+
+ coordinator.trigger();
+ await scheduler.flushNext();
+
+ expect(sampleCount).toBe(1);
+ expect(coordinator.isActive()).toBe(false);
+ expect(scheduler.callbacks.size).toBe(0);
+ });
+});
diff --git a/src/runtime/indicatorSettleCoordinator.ts b/src/runtime/indicatorSettleCoordinator.ts
new file mode 100644
index 00000000..162a9596
--- /dev/null
+++ b/src/runtime/indicatorSettleCoordinator.ts
@@ -0,0 +1,108 @@
+interface IndicatorSettleScheduler {
+ readonly cancel: (handle: Handle) => void;
+ readonly schedule: (callback: () => void, delayMs: number) => Handle;
+}
+
+export interface IndicatorSettleCoordinator {
+ cancel(): void;
+ isActive(): boolean;
+ trigger(): boolean;
+}
+
+export interface IndicatorSettleCoordinatorOptions {
+ readonly areEqual: (left: Snapshot, right: Snapshot) => boolean;
+ readonly intervalMs: number;
+ readonly maxSamples: number;
+ readonly sample: () => Promise;
+ readonly scheduler: IndicatorSettleScheduler;
+ readonly stableSampleCount: number;
+}
+
+export function createIndicatorSettleCoordinator(
+ options: IndicatorSettleCoordinatorOptions,
+): IndicatorSettleCoordinator {
+ let active = false;
+ let pendingHandle: Handle | null = null;
+ let sampling = false;
+ let revision = 0;
+ let sampleCount = 0;
+ let stableSampleCount = 0;
+ let previousSnapshot: Snapshot | null = null;
+
+ function clearPending(): void {
+ if (pendingHandle === null) {
+ return;
+ }
+ options.scheduler.cancel(pendingHandle);
+ pendingHandle = null;
+ }
+
+ function cancel(): void {
+ active = false;
+ revision += 1;
+ sampleCount = 0;
+ stableSampleCount = 0;
+ previousSnapshot = null;
+ clearPending();
+ }
+
+ function scheduleNext(delayMs: number): void {
+ if (!active || sampling || pendingHandle !== null) {
+ return;
+ }
+ pendingHandle = options.scheduler.schedule(() => {
+ pendingHandle = null;
+ void sampleNext();
+ }, delayMs);
+ }
+
+ async function sampleNext(): Promise {
+ if (!active || sampling) {
+ return;
+ }
+
+ const sampleRevision = revision;
+ sampling = true;
+ const snapshot = await options.sample();
+ sampling = false;
+
+ if (sampleRevision !== revision) {
+ scheduleNext(0);
+ return;
+ }
+ if (snapshot === null) {
+ cancel();
+ return;
+ }
+
+ sampleCount += 1;
+ if (previousSnapshot !== null && options.areEqual(previousSnapshot, snapshot)) {
+ stableSampleCount += 1;
+ } else {
+ stableSampleCount = 1;
+ }
+ previousSnapshot = snapshot;
+
+ if (stableSampleCount >= options.stableSampleCount || sampleCount >= options.maxSamples) {
+ cancel();
+ return;
+ }
+
+ scheduleNext(options.intervalMs);
+ }
+
+ return {
+ cancel,
+ isActive: () => active,
+ trigger: () => {
+ const wasActive = active;
+ active = true;
+ revision += 1;
+ sampleCount = 0;
+ stableSampleCount = 0;
+ previousSnapshot = null;
+ scheduleNext(0);
+ return !wasActive;
+ },
+ };
+}
diff --git a/src/runtime/stationarySelection.test.ts b/src/runtime/stationarySelection.test.ts
index c95ce162..0e09fa78 100644
--- a/src/runtime/stationarySelection.test.ts
+++ b/src/runtime/stationarySelection.test.ts
@@ -65,16 +65,27 @@ describe('stationarySelection RN integration', () => {
expect(source).not.toContain('onResponderReject');
});
- it('uses display contents on web and a retained unstyled native measurement view', () => {
- expect(source).toContain("Platform.OS === 'web' || !props.showUnsupportedIndicator");
- expect(source).toContain('collapsable: false');
+ it('keeps the per-node recorder layout-neutral on every platform', () => {
+ expect(source).toContain("style: { display: 'contents' }");
+ expect(source).not.toContain('collapsable: false');
+ });
+
+ it('measures unsupported native content through its existing public View ref', () => {
+ expect(source).toContain('export function useStudioUnsupportedNodeMeasurement()');
+ expect(source).toContain('measure: () => measureNativeView(view)');
+ expect(source).toContain('UnsupportedNodeMeasurementContext.Provider');
+ expect(source).not.toContain('measureUnsupportedView');
});
it('renders root-owned unsupported indicators with pointerEvents none', () => {
- expect(source).toContain('...indicatorRects.map((rect) =>');
+ expect(source).toContain('...(props.isEditMode ? indicatorRects : []).map((rect) =>');
expect(source).toContain("pointerEvents: 'none'");
});
+ it('hides unsupported indicators synchronously in Preview', () => {
+ expect(source).toContain('...(props.isEditMode ? indicatorRects : []).map((rect) =>');
+ });
+
it('retains duplicate live wrapper measurements for the same Runtime node', () => {
expect(source).toContain('new Map>()');
expect(source).toContain('measurements.add(measurement)');
@@ -82,7 +93,9 @@ describe('stationarySelection RN integration', () => {
});
it('places pointerEvents as a View prop, not inside style', () => {
- const indicatorStart = source.indexOf('...indicatorRects.map((rect) =>');
+ const indicatorStart = source.indexOf(
+ '...(props.isEditMode ? indicatorRects : []).map((rect) =>',
+ );
const indicatorBlock = source.slice(indicatorStart);
expect(indicatorBlock).toContain("pointerEvents: 'none'");
const styleBlock = /style:\s*\{([\s\S]*?)\n\s*\},/.exec(indicatorBlock);
@@ -133,7 +146,7 @@ describe('stationarySelection RN integration', () => {
expect(source).toContain('new ResizeObserver(() => requestIndicatorRefresh())');
expect(source).toContain('onLayout: requestIndicatorRefresh');
expect(source).toContain('onTouchMove: requestScrollIndicatorRefresh');
- expect(source).toContain('}, NATIVE_SCROLL_SETTLE_MS)');
+ expect(source).toContain('createIndicatorSettleCoordinator');
expect(source).not.toContain('measureNextFrame');
});
diff --git a/src/runtime/stationarySelection.ts b/src/runtime/stationarySelection.ts
index 56946ad2..885a8f20 100644
--- a/src/runtime/stationarySelection.ts
+++ b/src/runtime/stationarySelection.ts
@@ -13,6 +13,10 @@ import {
createIndicatorRefreshCoordinator,
type IndicatorRefreshCoordinator,
} from './indicatorRefreshCoordinator.js';
+import {
+ createIndicatorSettleCoordinator,
+ type IndicatorSettleCoordinator,
+} from './indicatorSettleCoordinator.js';
import type { StationarySelectionCoordinator } from './stationarySelectionCoordinator.js';
import { createStationarySelectionCoordinator } from './stationarySelectionCoordinator.js';
import {
@@ -58,7 +62,17 @@ interface TrackerContextValue {
}
const TrackerContext = React.createContext(null);
-const NATIVE_SCROLL_SETTLE_MS = 800;
+const NATIVE_SETTLE_INTERVAL_MS = 60;
+const NATIVE_SETTLE_MAX_SAMPLES = 80;
+const NATIVE_SETTLE_STABLE_SAMPLE_COUNT = 3;
+
+interface UnsupportedNodeMeasurementContextValue {
+ readonly registerView: (view: ViewRef) => () => void;
+ readonly requestRefresh: () => void;
+}
+
+const UnsupportedNodeMeasurementContext =
+ React.createContext(null);
interface WebRectLike {
readonly left: number;
@@ -153,18 +167,18 @@ function measureRootView(view: ViewRef | null): Promise {
return measureNativeView(view);
}
-function measureUnsupportedView(view: ViewRef | null): Promise {
+function measureUnsupportedWebView(view: ViewRef | null): Promise {
if (!view) {
return Promise.resolve(null);
}
- if (Platform.OS === 'web' && isWebElementLike(view)) {
+ if (isWebElementLike(view)) {
return Promise.resolve(
unionRects(Array.from(view.children).flatMap((child) => measureRenderedBoxes(child))),
);
}
- return measureNativeView(view);
+ return Promise.resolve(null);
}
function areIndicatorRectsEqual(
@@ -190,6 +204,39 @@ function areIndicatorRectsEqual(
});
}
+export function useStudioUnsupportedNodeMeasurement(): {
+ readonly onLayout: (_event: LayoutChangeEvent) => void;
+ readonly ref: (view: ViewRef | null) => void;
+} {
+ const context = React.useContext(UnsupportedNodeMeasurementContext);
+ const unregisterRef = React.useRef<(() => void) | null>(null);
+
+ const setViewRef = React.useCallback(
+ (view: ViewRef | null) => {
+ unregisterRef.current?.();
+ unregisterRef.current = view && context ? context.registerView(view) : null;
+ },
+ [context],
+ );
+
+ React.useEffect(
+ () => () => {
+ unregisterRef.current?.();
+ unregisterRef.current = null;
+ },
+ [],
+ );
+
+ const onLayout = React.useCallback(
+ (_event: LayoutChangeEvent) => {
+ context?.requestRefresh();
+ },
+ [context],
+ );
+
+ return React.useMemo(() => ({ onLayout, ref: setViewRef }), [onLayout, setViewRef]);
+}
+
function StudioNodeTouchRecorder(props: {
readonly nodeId: string | undefined;
readonly showUnsupportedIndicator: boolean;
@@ -247,9 +294,9 @@ function StudioNodeTouchRecorder(props: {
(view: ViewRef | null) => {
unregisterMeasurementRef.current?.();
unregisterMeasurementRef.current = null;
- if (view && ctx && props.nodeId && props.showUnsupportedIndicator) {
+ if (Platform.OS === 'web' && view && ctx && props.nodeId && props.showUnsupportedIndicator) {
unregisterMeasurementRef.current = ctx.registerUnsupportedNode(props.nodeId, {
- measure: () => measureUnsupportedView(view),
+ measure: () => measureUnsupportedWebView(view),
resizeTargets: getWebDescendantResizeTargets(view),
});
}
@@ -257,31 +304,49 @@ function StudioNodeTouchRecorder(props: {
[ctx, props.nodeId, props.showUnsupportedIndicator],
);
+ const nativeMeasurementContext =
+ React.useMemo(() => {
+ const { nodeId } = props;
+ if (Platform.OS === 'web' || !ctx || !nodeId || !props.showUnsupportedIndicator) {
+ return null;
+ }
+
+ return {
+ registerView: (view) =>
+ ctx.registerUnsupportedNode(nodeId, {
+ measure: () => measureNativeView(view),
+ resizeTargets: [],
+ }),
+ requestRefresh: ctx.requestIndicatorRefresh,
+ };
+ }, [ctx, props.nodeId, props.showUnsupportedIndicator]);
+
return React.createElement(
- View,
- {
- collapsable: false,
- ref: setViewRef,
- nativeID: props.nodeId
- ? `studio-runtime-node-${encodeURIComponent(props.nodeId)}`
- : undefined,
- testID:
- props.nodeId && props.showUnsupportedIndicator
- ? `studio-unsupported-recorder-${props.nodeId}`
- : undefined,
- style:
- Platform.OS === 'web' || !props.showUnsupportedIndicator
- ? { display: 'contents' }
- : undefined,
- onLayout: handleLayout,
- onPointerCancel: handleInteractionCompletion,
- onPointerDown: handlePointerDown,
- onPointerUp: handleInteractionCompletion,
- onTouchCancel: handleInteractionCompletion,
- onTouchEnd: handleInteractionCompletion,
- onTouchStart: handleTouchStart,
- },
- props.children,
+ UnsupportedNodeMeasurementContext.Provider,
+ { value: nativeMeasurementContext },
+ React.createElement(
+ View,
+ {
+ ref: setViewRef,
+ nativeID:
+ Platform.OS === 'web' && props.nodeId
+ ? `studio-runtime-node-${encodeURIComponent(props.nodeId)}`
+ : undefined,
+ testID:
+ Platform.OS === 'web' && props.nodeId && props.showUnsupportedIndicator
+ ? `studio-unsupported-recorder-${props.nodeId}`
+ : undefined,
+ style: { display: 'contents' },
+ onLayout: handleLayout,
+ onPointerCancel: handleInteractionCompletion,
+ onPointerDown: handlePointerDown,
+ onPointerUp: handleInteractionCompletion,
+ onTouchCancel: handleInteractionCompletion,
+ onTouchEnd: handleInteractionCompletion,
+ onTouchStart: handleTouchStart,
+ },
+ props.children,
+ ),
);
}
@@ -332,6 +397,7 @@ function StationaryTapSelector(props: {
const [indicatorRects, setIndicatorRects] = React.useState(
[],
);
+ const latestIndicatorRectsRef = React.useRef([]);
coordinatorRef.current ??= createStationarySelectionCoordinator();
@@ -344,10 +410,12 @@ function StationaryTapSelector(props: {
const generationRef = React.useRef(0);
const inputStateRef = React.useRef(null);
const refreshCoordinatorRef = React.useRef(null);
- const refreshIndicatorRectsRef = React.useRef<() => void>(() => undefined);
+ const settleCoordinatorRef = React.useRef(null);
+ const refreshIndicatorRectsRef = React.useRef<
+ () => Promise
+ >(() => Promise.resolve(null));
const geometryRevisionRef = React.useRef(0);
const mountedRef = React.useRef(true);
- const settledScrollRefreshTimerRef = React.useRef | null>(null);
isEditModeRef.current = props.isEditMode;
selectedNodeIdRef.current = props.selectedNodeId;
@@ -362,9 +430,10 @@ function StationaryTapSelector(props: {
const inputState = inputStateRef.current;
const requestIndicatorRefresh = React.useCallback(() => {
- geometryRevisionRef.current += 1;
refreshCoordinatorRef.current ??= createIndicatorRefreshCoordinator(
- () => refreshIndicatorRectsRef.current(),
+ () => {
+ void refreshIndicatorRectsRef.current();
+ },
{
request: (callback) => requestAnimationFrame(callback),
cancel: (frameId) => cancelAnimationFrame(frameId),
@@ -375,13 +444,24 @@ function StationaryTapSelector(props: {
const requestScrollIndicatorRefresh = React.useCallback(() => {
requestIndicatorRefresh();
- if (settledScrollRefreshTimerRef.current !== null) {
- clearTimeout(settledScrollRefreshTimerRef.current);
+ if (Platform.OS === 'web' || !isEditModeRef.current || unsupportedNodesRef.current.size === 0) {
+ return;
}
- settledScrollRefreshTimerRef.current = setTimeout(() => {
- settledScrollRefreshTimerRef.current = null;
- requestIndicatorRefresh();
- }, NATIVE_SCROLL_SETTLE_MS);
+ settleCoordinatorRef.current ??= createIndicatorSettleCoordinator<
+ readonly UnsupportedIndicatorRect[],
+ ReturnType
+ >({
+ areEqual: areIndicatorRectsEqual,
+ intervalMs: NATIVE_SETTLE_INTERVAL_MS,
+ maxSamples: NATIVE_SETTLE_MAX_SAMPLES,
+ sample: () => refreshIndicatorRectsRef.current(),
+ scheduler: {
+ cancel: (handle) => clearTimeout(handle),
+ schedule: (callback, delayMs) => setTimeout(callback, delayMs),
+ },
+ stableSampleCount: NATIVE_SETTLE_STABLE_SAMPLE_COUNT,
+ });
+ settleCoordinatorRef.current.trigger();
}, [requestIndicatorRefresh]);
const registerUnsupportedNode = React.useCallback(
@@ -405,6 +485,9 @@ function StationaryTapSelector(props: {
if (registeredMeasurements.size === 0) {
unsupportedNodesRef.current.delete(nodeId);
}
+ if (unsupportedNodesRef.current.size === 0) {
+ settleCoordinatorRef.current?.cancel();
+ }
requestIndicatorRefresh();
};
},
@@ -424,16 +507,19 @@ function StationaryTapSelector(props: {
[inputState, registerUnsupportedNode, requestIndicatorRefresh, requestScrollIndicatorRefresh],
);
- const refreshIndicatorRects = React.useCallback(async () => {
- const geometryRevision = geometryRevisionRef.current;
+ const refreshIndicatorRects = React.useCallback(async (): Promise<
+ readonly UnsupportedIndicatorRect[] | null
+ > => {
+ const geometryRevision = ++geometryRevisionRef.current;
if (!isEditModeRef.current || unsupportedNodesRef.current.size === 0) {
+ latestIndicatorRectsRef.current = [];
setIndicatorRects((current) => (current.length === 0 ? current : []));
- return;
+ return null;
}
const rootRect = await measureRootView(rootViewRef.current);
if (!rootRect) {
- return;
+ return latestIndicatorRectsRef.current;
}
const measurements = await Promise.all(
@@ -464,18 +550,21 @@ function StationaryTapSelector(props: {
.sort((left, right) => left.nodeId.localeCompare(right.nodeId));
if (!mountedRef.current || geometryRevision !== geometryRevisionRef.current) {
- return;
+ return mountedRef.current ? latestIndicatorRectsRef.current : null;
}
+ latestIndicatorRectsRef.current = nextRects;
setIndicatorRects((current) =>
areIndicatorRectsEqual(current, nextRects) ? current : nextRects,
);
+ return nextRects;
}, []);
- refreshIndicatorRectsRef.current = () => {
- void refreshIndicatorRects();
- };
+ refreshIndicatorRectsRef.current = refreshIndicatorRects;
React.useEffect(() => {
+ if (!props.isEditMode) {
+ settleCoordinatorRef.current?.cancel();
+ }
requestIndicatorRefresh();
}, [props.isEditMode, requestIndicatorRefresh]);
@@ -485,10 +574,7 @@ function StationaryTapSelector(props: {
mountedRef.current = false;
geometryRevisionRef.current += 1;
refreshCoordinatorRef.current?.cancelPendingRefresh();
- if (settledScrollRefreshTimerRef.current !== null) {
- clearTimeout(settledScrollRefreshTimerRef.current);
- settledScrollRefreshTimerRef.current = null;
- }
+ settleCoordinatorRef.current?.cancel();
};
}, []);
@@ -609,7 +695,7 @@ function StationaryTapSelector(props: {
style: { flex: 1, position: 'relative' },
},
props.children,
- ...indicatorRects.map((rect) =>
+ ...(props.isEditMode ? indicatorRects : []).map((rect) =>
React.createElement(View, {
key: rect.nodeId,
nativeID: `studio-unsupported-indicator-${encodeURIComponent(rect.nodeId)}`,