From 61399be94e614fb8dc6f7e4fa11cea559c4ea9fb Mon Sep 17 00:00:00 2001
From: Quang Tran <16215255+trmquang93@users.noreply.github.com>
Date: Wed, 1 Apr 2026 10:02:20 +0700
Subject: [PATCH] feat: add transition type annotations to connections
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Exposes the existing transitionType field with an expanded set of
transition options (push, modal, fullScreenCover, replace, pop, tab,
fade, slideUp, slideLeft, custom) in both ConnectionEditModal and
HotspotModal. Extracts TRANSITION_TYPES and TRANSITION_LABELS to
constants.js as a single source of truth.
Canvas connection badges now render a distinct SVG icon alongside the
transition label so types are visually distinguishable at a glance.
Navigation.md outputs human-readable transition labels. Build-guide.md
includes a platform-specific Transition Types table for all four
supported frameworks (SwiftUI, React Native, Flutter, Jetpack Compose)
as well as expanded per-type guidance in the auto-platform path.
No file format change — transitionType field already exists and
importFlow.js already backfills it.
---
src/components/ConnectionEditModal.jsx | 10 ++--
src/components/ConnectionLines.jsx | 65 ++++++++++++++++++--
src/components/HotspotModal.jsx | 10 ++--
src/constants.js | 18 ++++++
src/utils/generateInstructionFiles.js | 24 ++++++--
src/utils/generateInstructionFiles.test.js | 34 +++++++++++
src/utils/instructionRenderers.js | 69 ++++++++++++++++++++++
src/utils/instructionRenderers.test.js | 54 +++++++++++++++++
8 files changed, 263 insertions(+), 21 deletions(-)
diff --git a/src/components/ConnectionEditModal.jsx b/src/components/ConnectionEditModal.jsx
index 984d81a..dd8e73c 100644
--- a/src/components/ConnectionEditModal.jsx
+++ b/src/components/ConnectionEditModal.jsx
@@ -2,6 +2,7 @@ import { useState } from "react";
import { COLORS, styles } from "../styles/theme";
import { generateId } from "../utils/generateId";
import { DataFlowEditor } from "./DataFlowEditor";
+import { TRANSITION_TYPES } from "../constants";
export function ConnectionEditModal({ connection, groupConnections, screens, fromScreen, onSave, onDelete, onClose }) {
const isConditional = groupConnections.length > 1 || !!connection.conditionGroupId;
@@ -230,12 +231,9 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
style={styles.select}
>
-
-
-
-
-
-
+ {TRANSITION_TYPES.map(t => (
+
+ ))}
diff --git a/src/components/ConnectionLines.jsx b/src/components/ConnectionLines.jsx
index 87896d9..2d51435 100644
--- a/src/components/ConnectionLines.jsx
+++ b/src/components/ConnectionLines.jsx
@@ -1,6 +1,57 @@
import { COLORS, FONTS } from "../styles/theme";
import { HEADER_HEIGHT, BORDER_WIDTH, DEFAULT_SCREEN_WIDTH, DEFAULT_IMAGE_HEIGHT, BEZIER_FACTOR, BEZIER_MIN_CP } from "../constants";
+// Small SVG icon renderers for transition types, each drawn into a ~10x10px space.
+// Each function takes (cx, cy, fill) where cx/cy is the icon center point.
+const TRANSITION_ICONS = {
+ push: (cx, cy, fill) => (
+
+ ),
+ modal: (cx, cy, fill) => (
+
+ ),
+ fullScreenCover: (cx, cy, fill) => (
+
+ ),
+ replace: (cx, cy, fill) => (
+ <>
+
+
+
+
+ >
+ ),
+ pop: (cx, cy, fill) => (
+
+ ),
+ tab: (cx, cy, fill) => (
+ <>
+
+
+
+ >
+ ),
+ fade: (cx, cy, fill) => (
+ <>
+
+
+ >
+ ),
+ slideUp: (cx, cy, fill) => (
+
+ ),
+ slideLeft: (cx, cy, fill) => (
+
+ ),
+ custom: (cx, cy, fill) => (
+ *
+ ),
+};
+
const BORDER = BORDER_WIDTH;
function getScreenCenterY(screen) {
@@ -220,14 +271,17 @@ export function ConnectionLines({
{conn.transitionType && (() => {
const mx = (fromX + toX) / 2;
const my = (fromY + toY) / 2 + (conn.label || conn.condition ? 12 : 0);
- const badgeLabel = conn.transitionType === "custom"
+ const shortLabel = conn.transitionType === "custom"
? (conn.transitionLabel || "custom")
: conn.transitionType;
- const badgeW = badgeLabel.length * 5.5 + 10;
+ const iconFn = TRANSITION_ICONS[conn.transitionType];
+ const iconW = iconFn ? 14 : 0;
+ const badgeW = shortLabel.length * 5.5 + 10 + iconW;
+ const badgeX = mx - badgeW / 2;
return (
+ {iconFn && iconFn(badgeX + 7, my, COLORS.accentLight)}
- {badgeLabel}
+ {shortLabel}
);
diff --git a/src/components/HotspotModal.jsx b/src/components/HotspotModal.jsx
index 0ce96f6..492f878 100644
--- a/src/components/HotspotModal.jsx
+++ b/src/components/HotspotModal.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import { COLORS, FONTS, styles } from "../styles/theme";
import { generateId } from "../utils/generateId";
import { DataFlowEditor } from "./DataFlowEditor";
+import { TRANSITION_TYPES } from "../constants";
function FollowUpSection({ title, titleColor, action, setAction, targetId, setTargetId,
customDesc, setCustomDesc, otherScreens, dataFlow, onDataFlowChange }) {
@@ -748,12 +749,9 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents =
style={styles.select}
>
-
-
-
-
-
-
+ {TRANSITION_TYPES.map(t => (
+
+ ))}
diff --git a/src/constants.js b/src/constants.js
index 1b0d0ff..361d271 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -71,3 +71,21 @@ export const COLLAB_CURSOR_FADE_MS = 3000;
// ── Sticky Notes ─────────────────────────────
export const STICKY_NOTE_MIN_WIDTH = 150;
+
+// ── Transition Types ─────────────────────────
+export const TRANSITION_TYPES = [
+ { value: "push", label: "Push (stack navigation)" },
+ { value: "modal", label: "Modal sheet" },
+ { value: "fullScreenCover", label: "Full-screen cover" },
+ { value: "replace", label: "Replace stack" },
+ { value: "pop", label: "Pop (go back)" },
+ { value: "tab", label: "Tab switch" },
+ { value: "fade", label: "Fade" },
+ { value: "slideUp", label: "Slide up" },
+ { value: "slideLeft", label: "Slide from left" },
+ { value: "custom", label: "Custom..." },
+];
+
+export const TRANSITION_LABELS = Object.fromEntries(
+ TRANSITION_TYPES.map(t => [t.value, t.label])
+);
diff --git a/src/utils/generateInstructionFiles.js b/src/utils/generateInstructionFiles.js
index 816ce1b..8ec3a25 100644
--- a/src/utils/generateInstructionFiles.js
+++ b/src/utils/generateInstructionFiles.js
@@ -1,6 +1,7 @@
import { analyzeNavGraph } from "./analyzeNavGraph.js";
-import { PLATFORM_TERMINOLOGY, renderHotspotDetailBlock, renderBuildGuideActionTable } from "./instructionRenderers.js";
+import { PLATFORM_TERMINOLOGY, renderHotspotDetailBlock, renderBuildGuideActionTable, renderBuildGuideTransitionTable } from "./instructionRenderers.js";
import { screenReqId, connectionReqId } from "./generateReqIds.js";
+import { TRANSITION_LABELS } from "../constants.js";
// --- Helpers ---
@@ -664,7 +665,9 @@ function generateNavigationMd(screens, connections, navAnalysis) {
else if (c.connectionPath === "api-error") actionCol += " (error)";
else if (c.connectionPath && c.connectionPath.startsWith("condition-")) actionCol += " (conditional)";
const transitionCol = c.transitionType
- ? (c.transitionType === "custom" ? (c.transitionLabel || "custom") : c.transitionType)
+ ? (c.transitionType === "custom"
+ ? (c.transitionLabel || "custom")
+ : (TRANSITION_LABELS[c.transitionType] || c.transitionType))
: "\u2014";
const conditionCol = c.condition || "\u2014";
const dataCol = c.dataFlow?.length > 0
@@ -726,7 +729,17 @@ function generateBuildGuideMd(screens, connections, options, screenGroups = [])
md += ` - **conditional** — Branch to different screens based on a condition (see screens.md for branch definitions)\n`;
md += ` - **custom** — Implement custom logic as described in screens.md\n`;
md += `6. Set up proper navigation stack/router with all routes\n`;
- md += `7. Add smooth transitions between screens matching platform conventions\n`;
+ md += `7. For connections with a specified transition type (see Transition column in navigation.md), implement the matching animation:\n`;
+ md += ` - **push** — Standard stack push (default slide-from-right)\n`;
+ md += ` - **modal** — Modal sheet sliding up from the bottom\n`;
+ md += ` - **fullScreenCover** — Full-screen modal covering the entire display\n`;
+ md += ` - **replace** — Replace the current screen without adding to the back stack\n`;
+ md += ` - **pop** — Dismiss/pop back to the previous screen\n`;
+ md += ` - **tab** — Switch to a tab in the tab bar\n`;
+ md += ` - **fade** — Cross-fade between screens\n`;
+ md += ` - **slideUp** — Slide in from the bottom\n`;
+ md += ` - **slideLeft** — Slide in from the right (left direction)\n`;
+ md += ` - **custom** — See connection label for transition description\n`;
md += `8. Ensure responsive layout that adapts to different screen sizes\n`;
md += `\n`;
md += `## Sub-Agent Implementation Workflow\n\n`;
@@ -755,12 +768,15 @@ function generateBuildGuideMd(screens, connections, options, screenGroups = [])
md += renderBuildGuideActionTable(platform);
+ const transitionTable = renderBuildGuideTransitionTable(platform);
+ if (transitionTable) md += transitionTable;
+
md += `### Steps\n\n`;
md += `1. Implement each screen from screens.md as a separate ${pt.name} view/component\n`;
md += `2. For EACH screen, open its reference image from the \`images/\` folder and replicate the visual design exactly — colors, typography, spacing, layout, and component hierarchy\n`;
md += `3. Wire up navigation flows from navigation.md using the patterns above\n`;
md += `4. Handle API actions with proper error handling and loading states\n`;
- md += `5. Add smooth transitions matching ${pt.name} platform conventions\n`;
+ md += `5. For each connection with a specified transition type, use the matching pattern from the Transition Types table above\n`;
md += `\n`;
md += `## Sub-Agent Implementation Workflow\n\n`;
md += `Each screen is implemented by a dedicated sub-agent. The sub-agent MUST follow these\n`;
diff --git a/src/utils/generateInstructionFiles.test.js b/src/utils/generateInstructionFiles.test.js
index 0fa4025..03d4b58 100644
--- a/src/utils/generateInstructionFiles.test.js
+++ b/src/utils/generateInstructionFiles.test.js
@@ -184,4 +184,38 @@ describe("generateInstructionFiles", () => {
const navFile = result.files.find((f) => f.name === "navigation.md");
expect(navFile.content).toContain("Settings");
});
+
+ it("renders human-readable transition labels in navigation.md", () => {
+ const screens = [
+ { ...minimalScreen, id: "s1", name: "Home" },
+ { ...minimalScreen, id: "s2", name: "Detail", x: 400 },
+ ];
+ const connections = [
+ { id: "c1", fromScreenId: "s1", toScreenId: "s2", transitionType: "fullScreenCover" },
+ { id: "c2", fromScreenId: "s1", toScreenId: "s2", transitionType: "slideUp" },
+ { id: "c3", fromScreenId: "s1", toScreenId: "s2", transitionType: "fade" },
+ ];
+ const result = generateInstructionFiles(screens, connections, defaultOptions);
+ const navFile = result.files.find((f) => f.name === "navigation.md");
+ expect(navFile.content).toContain("Full-screen cover");
+ expect(navFile.content).toContain("Slide up");
+ expect(navFile.content).toContain("Fade");
+ expect(navFile.content).not.toContain("fullScreenCover");
+ expect(navFile.content).not.toContain("slideUp");
+ });
+
+ it("includes Transition Types section in build-guide.md for platform-specific output", () => {
+ const result = generateInstructionFiles([minimalScreen], [], { platform: "swiftui" });
+ const buildGuide = result.files.find((f) => f.name === "build-guide.md");
+ expect(buildGuide.content).toContain("### Transition Types");
+ expect(buildGuide.content).toContain(".fullScreenCover");
+ expect(buildGuide.content).toContain(".transition(.opacity)");
+ });
+
+ it("includes transition type guidance in build-guide.md for auto platform", () => {
+ const result = generateInstructionFiles([minimalScreen], [], { platform: "auto" });
+ const buildGuide = result.files.find((f) => f.name === "build-guide.md");
+ expect(buildGuide.content).toContain("fullScreenCover");
+ expect(buildGuide.content).toContain("slideUp");
+ });
});
diff --git a/src/utils/instructionRenderers.js b/src/utils/instructionRenderers.js
index abfc70f..bd0724d 100644
--- a/src/utils/instructionRenderers.js
+++ b/src/utils/instructionRenderers.js
@@ -11,6 +11,18 @@ export const PLATFORM_TERMINOLOGY = {
custom: "Add a `// TODO: custom action` comment with the description.",
stack: "Set up a `NavigationStack` with a path-based router using `@State private var path = NavigationPath()`.",
tabs: "Use `TabView` with `.tabItem { Label(\"Title\", systemImage: \"icon\") }` for each tab.",
+ transitions: {
+ push: "Standard `NavigationStack` push via `.navigationDestination(for:)`.",
+ modal: "`.sheet(isPresented:)` — presents as a card that slides up from the bottom.",
+ fullScreenCover: "`.fullScreenCover(isPresented:)` — covers the entire screen including safe areas.",
+ replace: "Replace the navigation path: `path = NavigationPath([newScreen])`.",
+ pop: "`dismiss()` via `@Environment(\\.dismiss)` to pop the current view.",
+ tab: "`TabView` selection binding: `@State private var selectedTab: Int`.",
+ fade: "`.transition(.opacity)` inside `withAnimation(.easeInOut(duration: 0.3))`.",
+ slideUp: "`.transition(.move(edge: .bottom))` inside `withAnimation(.easeOut(duration: 0.3))`.",
+ slideLeft: "`.transition(.move(edge: .trailing))` inside `withAnimation(.easeOut(duration: 0.3))`.",
+ custom: "Custom `AnyTransition` — see connection label for transition description.",
+ },
},
"react-native": {
name: "React Native",
@@ -22,6 +34,18 @@ export const PLATFORM_TERMINOLOGY = {
custom: "Add a `// TODO: custom action` comment with the description.",
stack: "Set up `createNativeStackNavigator()` with `NavigationContainer` wrapping `Stack.Navigator`.",
tabs: "Use `createBottomTabNavigator()` with `Tab.Screen` for each tab.",
+ transitions: {
+ push: "Default stack push — `navigation.navigate('ScreenName')` with no extra options.",
+ modal: "`presentation: 'modal'` on the `Stack.Screen` options.",
+ fullScreenCover: "`presentation: 'fullScreenModal'` on the `Stack.Screen` options.",
+ replace: "`navigation.replace('ScreenName')` to swap without adding to the stack.",
+ pop: "`navigation.goBack()` or `navigation.pop()` to return to the previous screen.",
+ tab: "`navigation.navigate('TabName')` targeting a tab in `createBottomTabNavigator()`.",
+ fade: "`animation: 'fade'` in `Stack.Screen` options — fades between screens.",
+ slideUp: "`animation: 'slide_from_bottom'` in `Stack.Screen` options.",
+ slideLeft: "`animation: 'slide_from_right'` in `Stack.Screen` options (default for iOS push).",
+ custom: "Custom `cardStyleInterpolator` or `transitionSpec` in `Stack.Screen` options — see connection label.",
+ },
},
flutter: {
name: "Flutter",
@@ -33,6 +57,18 @@ export const PLATFORM_TERMINOLOGY = {
custom: "Add a `// TODO: custom action` comment with the description.",
stack: "Set up `MaterialApp` with named routes or `GoRouter` for declarative routing.",
tabs: "Use `BottomNavigationBar` inside a `Scaffold` with an `IndexedStack` for tab content.",
+ transitions: {
+ push: "`Navigator.push(context, MaterialPageRoute(builder: (_) => TargetScreen()))` — default slide-from-right.",
+ modal: "`showModalBottomSheet(context: context, builder: (_) => TargetWidget())`.",
+ fullScreenCover: "`MaterialPageRoute(builder: (_) => TargetScreen(), fullscreenDialog: true)`.",
+ replace: "`Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => TargetScreen()))`.",
+ pop: "`Navigator.pop(context)` to return to the previous route.",
+ tab: "`DefaultTabController` with `TabBar` + `TabBarView`, or update `_selectedIndex` for `BottomNavigationBar`.",
+ fade: "`PageRouteBuilder` with `FadeTransition(opacity: animation, child: page)` as `transitionsBuilder`.",
+ slideUp: "`PageRouteBuilder` with `SlideTransition(position: Tween(begin: Offset(0, 1)).animate(animation))`.",
+ slideLeft: "`PageRouteBuilder` with `SlideTransition(position: Tween(begin: Offset(-1, 0)).animate(animation))`.",
+ custom: "Custom `PageRouteBuilder` with a `transitionsBuilder` — see connection label for details.",
+ },
},
"jetpack-compose": {
name: "Jetpack Compose",
@@ -44,6 +80,18 @@ export const PLATFORM_TERMINOLOGY = {
custom: "Add a `// TODO: custom action` comment with the description.",
stack: "Set up `NavHost(navController, startDestination)` with `composable(\"route\") { }` for each screen.",
tabs: "Use `Scaffold` with `NavigationBar` and `NavigationBarItem` for each tab.",
+ transitions: {
+ push: "`navController.navigate(\"route\")` — pair with `enterTransition`/`exitTransition` on `composable()`.",
+ modal: "`ModalBottomSheet { }` or `Dialog { }` composable for overlay presentation.",
+ fullScreenCover: "`composable()` with `enterTransition = { slideInVertically(initialOffsetY = { it }) }` covering the full screen.",
+ replace: "`navController.navigate(\"route\") { popUpTo(currentRoute) { inclusive = true } }` to replace without backstack.",
+ pop: "`navController.popBackStack()` to return to the previous destination.",
+ tab: "`NavigationBar` with `NavigationBarItem`, update `selectedDestination` state to switch tabs.",
+ fade: "`enterTransition = { fadeIn() }` and `exitTransition = { fadeOut() }` on `composable()`.",
+ slideUp: "`enterTransition = { slideInVertically(initialOffsetY = { it }) }` on `composable()`.",
+ slideLeft: "`enterTransition = { slideInHorizontally(initialOffsetX = { -it }) }` on `composable()`.",
+ custom: "Custom `EnterTransition`/`ExitTransition` on `composable()` — see connection label for details.",
+ },
},
};
@@ -147,6 +195,27 @@ export function renderHotspotDetailBlock(h, screens, documents) {
return renderer.detailBlock(h, screens, documents);
}
+// Generate the full ### Transition Types table for a specific platform.
+// Returns null when platform is "auto" or unknown — callers handle those paths separately.
+export function renderBuildGuideTransitionTable(platform) {
+ const pt = PLATFORM_TERMINOLOGY[platform];
+ if (!pt || !pt.transitions) return null;
+
+ let md = `### Transition Types\n\n`;
+ md += `| Transition | Implementation |\n`;
+ md += `|------------|---------------|\n`;
+
+ const transitionOrder = ["push", "modal", "fullScreenCover", "replace", "pop", "tab", "fade", "slideUp", "slideLeft", "custom"];
+ for (const t of transitionOrder) {
+ if (pt.transitions[t]) {
+ md += `| **${t}** | ${pt.transitions[t]} |\n`;
+ }
+ }
+ md += `\n`;
+
+ return md;
+}
+
// Generate the full ### Action Types table for a specific platform.
// Returns null when platform is "auto" or unknown — callers handle those paths separately.
export function renderBuildGuideActionTable(platform) {
diff --git a/src/utils/instructionRenderers.test.js b/src/utils/instructionRenderers.test.js
index 456d80f..04b63d6 100644
--- a/src/utils/instructionRenderers.test.js
+++ b/src/utils/instructionRenderers.test.js
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
renderHotspotDetailBlock,
renderBuildGuideActionTable,
+ renderBuildGuideTransitionTable,
} from "./instructionRenderers.js";
const makeScreen = (id, name) => ({ id, name });
@@ -151,3 +152,56 @@ describe("renderBuildGuideActionTable", () => {
expect(result).toContain("**custom**");
});
});
+
+describe("renderBuildGuideTransitionTable", () => {
+ it("returns null for platform 'auto'", () => {
+ expect(renderBuildGuideTransitionTable("auto")).toBeNull();
+ });
+
+ it("returns null for unknown platform", () => {
+ expect(renderBuildGuideTransitionTable("cobol")).toBeNull();
+ });
+
+ it("returns a table string for swiftui with platform-specific patterns", () => {
+ const result = renderBuildGuideTransitionTable("swiftui");
+ expect(result).toContain("### Transition Types");
+ expect(result).toContain(".sheet");
+ expect(result).toContain(".fullScreenCover");
+ expect(result).toContain(".transition(.opacity)");
+ });
+
+ it("returns a table string for react-native with platform-specific patterns", () => {
+ const result = renderBuildGuideTransitionTable("react-native");
+ expect(result).toContain("### Transition Types");
+ expect(result).toContain("fullScreenModal");
+ expect(result).toContain("slide_from_bottom");
+ });
+
+ it("returns a table string for flutter with platform-specific patterns", () => {
+ const result = renderBuildGuideTransitionTable("flutter");
+ expect(result).toContain("### Transition Types");
+ expect(result).toContain("FadeTransition");
+ expect(result).toContain("fullscreenDialog");
+ });
+
+ it("returns a table string for jetpack-compose with platform-specific patterns", () => {
+ const result = renderBuildGuideTransitionTable("jetpack-compose");
+ expect(result).toContain("### Transition Types");
+ expect(result).toContain("fadeIn");
+ expect(result).toContain("slideInVertically");
+ });
+
+ it("includes all 10 transition types in the generated table", () => {
+ const result = renderBuildGuideTransitionTable("swiftui");
+ expect(result).toContain("**push**");
+ expect(result).toContain("**modal**");
+ expect(result).toContain("**fullScreenCover**");
+ expect(result).toContain("**replace**");
+ expect(result).toContain("**pop**");
+ expect(result).toContain("**tab**");
+ expect(result).toContain("**fade**");
+ expect(result).toContain("**slideUp**");
+ expect(result).toContain("**slideLeft**");
+ expect(result).toContain("**custom**");
+ });
+});