From adc86d5440ce879fc7dbc46f8b903763e05236c8 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 15 Feb 2026 22:43:04 +1300 Subject: [PATCH] feat: add 3x2 grid layout option to split grid node Change layout selector from count-based to layout-based, allowing both 2x3 and 3x2 (portrait) layouts that produce 6 images. Now shows 7 layout options (2x2, 1x5, 2x3, 3x2, 2x4, 3x3, 2x5) with RxC labels. Co-Authored-By: Claude Opus 4.6 --- src/components/SplitGridSettingsModal.tsx | 59 +++++++++-------- .../__tests__/SplitGridSettingsModal.test.tsx | 66 ++++++++++++------- 2 files changed, 76 insertions(+), 49 deletions(-) diff --git a/src/components/SplitGridSettingsModal.tsx b/src/components/SplitGridSettingsModal.tsx index cb70bb2f..96d245b3 100644 --- a/src/components/SplitGridSettingsModal.tsx +++ b/src/components/SplitGridSettingsModal.tsx @@ -10,7 +10,15 @@ interface SplitGridSettingsModalProps { onClose: () => void; } -const TARGET_COUNT_OPTIONS = [4, 5, 6, 8, 9, 10] as const; +const LAYOUT_OPTIONS = [ + { rows: 2, cols: 2 }, + { rows: 1, cols: 5 }, + { rows: 2, cols: 3 }, + { rows: 3, cols: 2 }, + { rows: 2, cols: 4 }, + { rows: 3, cols: 3 }, + { rows: 2, cols: 5 }, +] as const; const ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]; const RESOLUTIONS: Resolution[] = ["1K", "2K", "4K"]; @@ -19,17 +27,9 @@ const MODELS: { value: ModelType; label: string }[] = [ { value: "nano-banana-pro", label: "Nano Banana Pro" }, ]; -// Calculate grid dimensions from target count -const getGridDimensions = (count: number): { rows: number; cols: number } => { - const grids: Record = { - 4: { rows: 2, cols: 2 }, - 5: { rows: 1, cols: 5 }, // 1x5 vertical strip (e.g., for A+ content) - 6: { rows: 2, cols: 3 }, - 8: { rows: 2, cols: 4 }, - 9: { rows: 3, cols: 3 }, - 10: { rows: 2, cols: 5 }, - }; - return grids[count] || { rows: 2, cols: 3 }; +const findLayoutIndex = (rows: number, cols: number): number => { + const idx = LAYOUT_OPTIONS.findIndex(l => l.rows === rows && l.cols === cols); + return idx >= 0 ? idx : 2; // default to 2x3 }; export function SplitGridSettingsModal({ @@ -39,14 +39,17 @@ export function SplitGridSettingsModal({ }: SplitGridSettingsModalProps) { const { updateNodeData, addNode, onConnect, addEdgeWithType, getNodeById } = useWorkflowStore(); - const [targetCount, setTargetCount] = useState(nodeData.targetCount); + const [selectedLayoutIndex, setSelectedLayoutIndex] = useState( + findLayoutIndex(nodeData.gridRows, nodeData.gridCols) + ); const [defaultPrompt, setDefaultPrompt] = useState(nodeData.defaultPrompt); const [aspectRatio, setAspectRatio] = useState(nodeData.generateSettings.aspectRatio); const [resolution, setResolution] = useState(nodeData.generateSettings.resolution); const [model, setModel] = useState(nodeData.generateSettings.model); const [useGoogleSearch, setUseGoogleSearch] = useState(nodeData.generateSettings.useGoogleSearch); - const { rows, cols } = getGridDimensions(targetCount); + const { rows, cols } = LAYOUT_OPTIONS[selectedLayoutIndex]; + const targetCount = rows * cols; const isNanoBananaPro = model === "nano-banana-pro"; const handleCreate = useCallback(() => { @@ -162,8 +165,8 @@ export function SplitGridSettingsModal({ onClose(); }, [ nodeId, targetCount, defaultPrompt, aspectRatio, resolution, - model, useGoogleSearch, rows, cols, getNodeById, addNode, - updateNodeData, onConnect, addEdgeWithType, onClose + model, useGoogleSearch, rows, cols, selectedLayoutIndex, getNodeById, + addNode, updateNodeData, onConnect, addEdgeWithType, onClose ]); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -183,20 +186,21 @@ export function SplitGridSettingsModal({
- {/* Target count selector with visual preview */} + {/* Layout selector with visual preview */}
- {TARGET_COUNT_OPTIONS.map((count) => { - const { rows: r, cols: c } = getGridDimensions(count); + {LAYOUT_OPTIONS.map((layout, index) => { + const count = layout.rows * layout.cols; + const isSelected = selectedLayoutIndex === index; return ( ); })} diff --git a/src/components/__tests__/SplitGridSettingsModal.test.tsx b/src/components/__tests__/SplitGridSettingsModal.test.tsx index e3aec33d..83977d84 100644 --- a/src/components/__tests__/SplitGridSettingsModal.test.tsx +++ b/src/components/__tests__/SplitGridSettingsModal.test.tsx @@ -68,8 +68,8 @@ describe("SplitGridSettingsModal", () => { }); }); - describe("Number of Images Selection", () => { - it("should render target count options (4, 6, 8, 9, 10)", () => { + describe("Grid Layout Selection", () => { + it("should render all layout options", () => { render( { /> ); - expect(screen.getByText("4")).toBeInTheDocument(); - expect(screen.getByText("6")).toBeInTheDocument(); - expect(screen.getByText("8")).toBeInTheDocument(); - expect(screen.getByText("9")).toBeInTheDocument(); - expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.getByText("2x2")).toBeInTheDocument(); + expect(screen.getByText("1x5")).toBeInTheDocument(); + expect(screen.getByText("2x3")).toBeInTheDocument(); + expect(screen.getByText("3x2")).toBeInTheDocument(); + expect(screen.getByText("2x4")).toBeInTheDocument(); + expect(screen.getByText("3x3")).toBeInTheDocument(); + expect(screen.getByText("2x5")).toBeInTheDocument(); }); - it("should highlight selected target count", () => { + it("should highlight selected layout", () => { render( { /> ); - // Default is 6, check its button has the selected styling + // Default is 2x3, check its button has the selected styling const buttons = screen.getAllByRole("button"); - const sixButton = buttons.find(btn => btn.textContent?.includes("6")); - expect(sixButton).toHaveClass("border-blue-500"); + const selectedButton = buttons.find(btn => btn.textContent?.includes("2x3")); + expect(selectedButton).toHaveClass("border-blue-500"); }); - it("should update target count when option is clicked", () => { + it("should update layout when option is clicked", () => { render( { /> ); - // Click on 9 + // Click on 3x3 const buttons = screen.getAllByRole("button"); - const nineButton = buttons.find(btn => btn.textContent?.includes("9")); - fireEvent.click(nineButton!); + const threeByThreeButton = buttons.find(btn => btn.textContent?.includes("3x3")); + fireEvent.click(threeByThreeButton!); // The grid description should update expect(screen.getByText(/3x3 = 9 images/)).toBeInTheDocument(); @@ -127,9 +129,27 @@ describe("SplitGridSettingsModal", () => { /> ); - // Default is 6, which is 2x3 + // Default is 2x3 expect(screen.getByText(/2x3 = 6 images/)).toBeInTheDocument(); }); + + it("should allow selecting 3x2 layout (6 images, portrait orientation)", () => { + render( + + ); + + // Click on 3x2 + const buttons = screen.getAllByRole("button"); + const threeByTwoButton = buttons.find(btn => btn.textContent?.includes("3x2")); + fireEvent.click(threeByTwoButton!); + + // Should show 3x2 = 6 images + expect(screen.getByText(/3x2 = 6 images/)).toBeInTheDocument(); + }); }); describe("Default Prompt", () => { @@ -344,7 +364,7 @@ describe("SplitGridSettingsModal", () => { expect(screen.getByText("Create 6 Generate Sets")).toBeInTheDocument(); }); - it("should update Create button text when target count changes", () => { + it("should update Create button text when layout changes", () => { render( { /> ); - // Click on 9 + // Click on 3x3 const buttons = screen.getAllByRole("button"); - const nineButton = buttons.find(btn => btn.textContent?.includes("9") && !btn.textContent?.includes("Create")); - fireEvent.click(nineButton!); + const threeByThreeButton = buttons.find(btn => btn.textContent?.includes("3x3") && !btn.textContent?.includes("Create")); + fireEvent.click(threeByThreeButton!); expect(screen.getByText("Create 9 Generate Sets")).toBeInTheDocument(); }); @@ -431,9 +451,9 @@ describe("SplitGridSettingsModal", () => { /> ); - // Each target count button should have a grid preview + // Each layout button should have a grid preview const gridPreviews = container.querySelectorAll(".aspect-video"); - expect(gridPreviews.length).toBe(6); // 6 options: 4, 5, 6, 8, 9, 10 + expect(gridPreviews.length).toBe(7); // 7 layout options }); }); @@ -441,6 +461,8 @@ describe("SplitGridSettingsModal", () => { it("should use node data values as initial state", () => { const nodeData = createDefaultNodeData(); nodeData.targetCount = 9; + nodeData.gridRows = 3; + nodeData.gridCols = 3; nodeData.defaultPrompt = "Existing prompt"; nodeData.generateSettings = { aspectRatio: "16:9",