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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/components/ConnectionEditModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -230,12 +231,9 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
style={styles.select}
>
<option value="">— Unspecified —</option>
<option value="push">Push (stack navigation)</option>
<option value="modal">Modal sheet</option>
<option value="replace">Replace stack</option>
<option value="pop">Pop (go back)</option>
<option value="tab">Tab switch</option>
<option value="custom">Custom…</option>
{TRANSITION_TYPES.map(t => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</label>

Expand Down
65 changes: 60 additions & 5 deletions src/components/ConnectionLines.jsx
Original file line number Diff line number Diff line change
@@ -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) => (
<path d={`M${cx-3} ${cy} L${cx+3} ${cy} M${cx} ${cy-3} L${cx+3} ${cy} L${cx} ${cy+3}`}
fill="none" stroke={fill} strokeWidth={1.5} strokeLinejoin="round" />
),
modal: (cx, cy, fill) => (
<rect x={cx-4} y={cy-4} width={8} height={7} rx={2} fill="none" stroke={fill} strokeWidth={1.5} />
),
fullScreenCover: (cx, cy, fill) => (
<rect x={cx-4} y={cy-4} width={8} height={8} rx={1} fill={fill} opacity={0.7} />
),
replace: (cx, cy, fill) => (
<>
<path d={`M${cx-3} ${cy-2} L${cx+3} ${cy-2}`} fill="none" stroke={fill} strokeWidth={1.5} />
<path d={`M${cx+3} ${cy+2} L${cx-3} ${cy+2}`} fill="none" stroke={fill} strokeWidth={1.5} />
<path d={`M${cx+1} ${cy-4} L${cx+3} ${cy-2} L${cx+1} ${cy}`} fill="none" stroke={fill} strokeWidth={1.2} />
<path d={`M${cx-1} ${cy} L${cx-3} ${cy+2} L${cx-1} ${cy+4}`} fill="none" stroke={fill} strokeWidth={1.2} />
</>
),
pop: (cx, cy, fill) => (
<path d={`M${cx+3} ${cy} L${cx-3} ${cy} M${cx} ${cy-3} L${cx-3} ${cy} L${cx} ${cy+3}`}
fill="none" stroke={fill} strokeWidth={1.5} strokeLinejoin="round" />
),
tab: (cx, cy, fill) => (
<>
<circle cx={cx-3} cy={cy} r={1.2} fill={fill} />
<circle cx={cx} cy={cy} r={1.2} fill={fill} />
<circle cx={cx+3} cy={cy} r={1.2} fill={fill} />
</>
),
fade: (cx, cy, fill) => (
<>
<circle cx={cx} cy={cy} r={4} fill={fill} opacity={0.2} />
<circle cx={cx} cy={cy} r={2} fill={fill} opacity={0.6} />
</>
),
slideUp: (cx, cy, fill) => (
<path d={`M${cx} ${cy+3} L${cx} ${cy-3} M${cx-3} ${cy} L${cx} ${cy-3} L${cx+3} ${cy}`}
fill="none" stroke={fill} strokeWidth={1.5} strokeLinejoin="round" />
),
slideLeft: (cx, cy, fill) => (
<path d={`M${cx+3} ${cy} L${cx-3} ${cy} M${cx} ${cy-3} L${cx-3} ${cy} L${cx} ${cy+3}`}
fill="none" stroke={fill} strokeWidth={1.5} strokeLinejoin="round" />
),
custom: (cx, cy, fill) => (
<text x={cx} y={cy+3} fill={fill} fontSize={10} fontFamily="monospace" textAnchor="middle">*</text>
),
};

const BORDER = BORDER_WIDTH;

function getScreenCenterY(screen) {
Expand Down Expand Up @@ -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 (
<g>
<rect
x={mx - badgeW / 2}
x={badgeX}
y={my - 8}
width={badgeW}
height={14}
Expand All @@ -236,15 +290,16 @@ export function ConnectionLines({
stroke="rgba(97,175,239,0.35)"
strokeWidth={1}
/>
{iconFn && iconFn(badgeX + 7, my, COLORS.accentLight)}
<text
x={mx}
x={mx + iconW / 2}
y={my + 2}
fill={COLORS.accentLight}
fontSize={9}
fontFamily={FONTS.mono}
textAnchor="middle"
>
{badgeLabel}
{shortLabel}
</text>
</g>
);
Expand Down
10 changes: 4 additions & 6 deletions src/components/HotspotModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -748,12 +749,9 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents =
style={styles.select}
>
<option value="">— Unspecified —</option>
<option value="push">Push (stack navigation)</option>
<option value="modal">Modal sheet</option>
<option value="replace">Replace stack</option>
<option value="pop">Pop (go back)</option>
<option value="tab">Tab switch</option>
<option value="custom">Custom…</option>
{TRANSITION_TYPES.map(t => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</label>

Expand Down
18 changes: 18 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
);
24 changes: 20 additions & 4 deletions src/utils/generateInstructionFiles.js
Original file line number Diff line number Diff line change
@@ -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 ---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -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`;
Expand Down
34 changes: 34 additions & 0 deletions src/utils/generateInstructionFiles.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Loading
Loading