From 854450ed07485bae3aff1a8fca0709010bf47f10 Mon Sep 17 00:00:00 2001 From: Quang Tran <16215255+trmquang93@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:07:48 +0700 Subject: [PATCH 1/2] feat: add template flows for common mobile app patterns Pre-built flow templates (Auth, Onboarding, Settings, Tab Bar, E-Commerce, Social Feed) that users can insert onto the canvas to eliminate cold-start. Each template includes wireframe screen images, hotspots with configured actions/API endpoints, and connections between screens. Templates are code-split via dynamic imports and IDs are regenerated on insert to prevent conflicts. --- src/Drawd.jsx | 19 + src/components/CanvasArea.jsx | 5 +- src/components/EmptyState.jsx | 31 +- src/components/ModalsLayer.jsx | 10 + src/components/ShortcutsPanel.jsx | 1 + src/components/TemplateBrowserModal.jsx | 179 +++++++ src/components/ToolBar.jsx | 13 +- src/components/TopBar.jsx | 11 +- src/hooks/useKeyboardShortcuts.js | 14 +- src/hooks/useTemplateInserter.js | 26 + src/pages/docs/userGuide.md | 37 +- src/templates/authFlow.js | 375 ++++++++++++++ src/templates/ecommerceFlow.js | 347 +++++++++++++ src/templates/index.js | 50 ++ src/templates/onboardingFlow.js | 299 ++++++++++++ src/templates/settingsFlow.js | 191 ++++++++ src/templates/socialFeedFlow.js | 405 ++++++++++++++++ src/templates/tabBarFlow.js | 621 ++++++++++++++++++++++++ src/templates/templates.test.js | 138 ++++++ src/templates/wireframe.js | 409 ++++++++++++++++ src/utils/mergeFlowAtPosition.js | 43 ++ src/utils/mergeFlowAtPosition.test.js | 108 +++++ 22 files changed, 3326 insertions(+), 6 deletions(-) create mode 100644 src/components/TemplateBrowserModal.jsx create mode 100644 src/hooks/useTemplateInserter.js create mode 100644 src/templates/authFlow.js create mode 100644 src/templates/ecommerceFlow.js create mode 100644 src/templates/index.js create mode 100644 src/templates/onboardingFlow.js create mode 100644 src/templates/settingsFlow.js create mode 100644 src/templates/socialFeedFlow.js create mode 100644 src/templates/tabBarFlow.js create mode 100644 src/templates/templates.test.js create mode 100644 src/templates/wireframe.js create mode 100644 src/utils/mergeFlowAtPosition.js create mode 100644 src/utils/mergeFlowAtPosition.test.js diff --git a/src/Drawd.jsx b/src/Drawd.jsx index b15d3c8..5047e85 100644 --- a/src/Drawd.jsx +++ b/src/Drawd.jsx @@ -19,6 +19,7 @@ import { useFileActions } from "./hooks/useFileActions"; import { useCollabSync } from "./hooks/useCollabSync"; import { useInteractionCallbacks } from "./hooks/useInteractionCallbacks"; import { useDerivedCanvasState } from "./hooks/useDerivedCanvasState"; +import { useTemplateInserter } from "./hooks/useTemplateInserter"; import { TopBar } from "./components/TopBar"; import { Sidebar } from "./components/Sidebar"; import { StickyNoteSidebar } from "./components/StickyNoteSidebar"; @@ -158,6 +159,18 @@ export default function Drawd({ initialRoomCode }) { const [renameModal, setRenameModal] = useState(null); const [showShortcuts, setShowShortcuts] = useState(false); const [formSummaryScreen, setFormSummaryScreen] = useState(null); + const [showTemplateBrowser, setShowTemplateBrowser] = useState(false); + + // ── Template inserter ───────────────────────────────────────────────── + const { insertTemplate } = useTemplateInserter({ + screens, mergeAll, replaceAll, pan, zoom, canvasRef, + }); + + const onTemplates = useCallback(() => setShowTemplateBrowser(true), []); + const onInsertTemplate = useCallback((data) => { + insertTemplate(data); + setShowTemplateBrowser(false); + }, [insertTemplate]); // ── Instruction generation ───────────────────────────────────────────── const { instructions, showInstructions, setShowInstructions, onGenerate, buildInstructionResult } = @@ -315,6 +328,7 @@ export default function Drawd({ initialRoomCode }) { selectedScreenGroup, setSelectedScreenGroup, deleteScreenGroup, undo, redo, saveNow, isFileSystemSupported, onSaveAs, onExport, onOpen, setActiveTool, + onTemplates, isReadOnly, }); @@ -379,6 +393,7 @@ export default function Drawd({ initialRoomCode }) { ) : null} onToggleParticipants={() => setShowParticipants((v) => !v)} showParticipants={showParticipants} + onTemplates={onTemplates} />
@@ -488,6 +503,7 @@ export default function Drawd({ initialRoomCode }) { setGroupContextMenu={setGroupContextMenu} handleImageUpload={handleImageUpload} addScreenAtCenter={addScreenAtCenter} + onTemplates={onTemplates} /> {selectedScreenData && ( @@ -573,6 +589,9 @@ export default function Drawd({ initialRoomCode }) { setFigmaError={setFigmaError} formSummaryScreen={formSummaryScreen} setFormSummaryScreen={setFormSummaryScreen} + showTemplateBrowser={showTemplateBrowser} + setShowTemplateBrowser={setShowTemplateBrowser} + onInsertTemplate={onInsertTemplate} />
); diff --git a/src/components/CanvasArea.jsx b/src/components/CanvasArea.jsx index 3689349..6c65c17 100644 --- a/src/components/CanvasArea.jsx +++ b/src/components/CanvasArea.jsx @@ -52,6 +52,8 @@ export function CanvasArea({ groupContextMenu, setGroupContextMenu, // ToolBar setActiveTool, handleImageUpload, addScreenAtCenter, + // Templates + onTemplates, }) { return (
- {screens.length === 0 && } + {screens.length === 0 && } {/* Zoom indicator */}
); diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index e17205b..b6cde2a 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -1,6 +1,6 @@ import { COLORS, FONTS } from "../styles/theme"; -export function EmptyState() { +export function EmptyState({ onTemplates }) { return (
wireframes, screenshots, or mockups
+ {onTemplates && ( + + )}
); } diff --git a/src/components/ModalsLayer.jsx b/src/components/ModalsLayer.jsx index 8fba927..86ca49a 100644 --- a/src/components/ModalsLayer.jsx +++ b/src/components/ModalsLayer.jsx @@ -11,6 +11,7 @@ import { ShortcutsPanel } from "./ShortcutsPanel"; import { ShareModal } from "./ShareModal"; import { HostLeftModal } from "./HostLeftModal"; import { FormSummaryPanel } from "./FormSummaryPanel"; +import { TemplateBrowserModal } from "./TemplateBrowserModal"; export function ModalsLayer({ // Hotspot modal @@ -43,6 +44,8 @@ export function ModalsLayer({ figmaProcessing, figmaError, setFigmaError, // Form summary formSummaryScreen, setFormSummaryScreen, + // Template browser + showTemplateBrowser, setShowTemplateBrowser, onInsertTemplate, }) { return ( <> @@ -212,6 +215,13 @@ export function ModalsLayer({ )} + {showTemplateBrowser && ( + setShowTemplateBrowser(false)} + /> + )} + {formSummaryScreen && ( ( + + + + + + +); + +function TemplateCard({ template, onSelect, loading }) { + const [hovered, setHovered] = useState(false); + + return ( + + ); +} + +export function TemplateBrowserModal({ onInsert, onClose }) { + const [loading, setLoading] = useState(false); + + const handleSelect = async (template) => { + setLoading(true); + try { + const data = await template.getData(); + onInsert(data); + } catch { + setLoading(false); + } + }; + + return ( +
+
e.stopPropagation()} + style={{ + ...styles.modalCard, + width: 540, + maxHeight: "80vh", + display: "flex", + flexDirection: "column", + }} + > +
+
+ +
+

Templates

+

+ Start with a pre-built flow and customize it +

+
+
+ +
+ +
+ {TEMPLATES.map((template) => ( + + ))} +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/ToolBar.jsx b/src/components/ToolBar.jsx index 0b22044..ea36a69 100644 --- a/src/components/ToolBar.jsx +++ b/src/components/ToolBar.jsx @@ -89,7 +89,16 @@ function ActionButton({ icon: Icon, label, shortcutKey, onClick }) { ); } -export function ToolBar({ activeTool, onToolChange, onUpload, onAddBlank, onAddStickyNote, isReadOnly }) { +const TemplateIcon = () => ( + + + + + + +); + +export function ToolBar({ activeTool, onToolChange, onUpload, onAddBlank, onAddStickyNote, isReadOnly, onTemplates }) { return (
+
+ )}
diff --git a/src/components/TopBar.jsx b/src/components/TopBar.jsx index 7d81846..49fbdea 100644 --- a/src/components/TopBar.jsx +++ b/src/components/TopBar.jsx @@ -94,7 +94,7 @@ function ShareIcon() { ); } -export function TopBar({ screenCount, connectionCount, onExport, onImport, onGenerate, canUndo, canRedo, onUndo, onRedo, connectedFileName, saveStatus, isFileSystemSupported, onNew, onOpen, onSaveAs, onDocuments, documentCount = 0, onDataModels, dataModelCount = 0, collabState, onShare, collabBadge, collabPresence, onToggleParticipants, showParticipants }) { +export function TopBar({ screenCount, connectionCount, onExport, onImport, onGenerate, canUndo, canRedo, onUndo, onRedo, connectedFileName, saveStatus, isFileSystemSupported, onNew, onOpen, onSaveAs, onDocuments, documentCount = 0, onDataModels, dataModelCount = 0, collabState, onShare, collabBadge, collabPresence, onToggleParticipants, showParticipants, onTemplates }) { const [fileMenuOpen, setFileMenuOpen] = useState(false); const fileMenuRef = useRef(null); @@ -360,6 +360,15 @@ export function TopBar({ screenCount, connectionCount, onExport, onImport, onGen New + + {isFileSystemSupported && (