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 && (