diff --git a/.gitignore b/.gitignore
index f0bd4fe..8b39166 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ dist-ssr
playwright-report/
test-results/
blob-report/
+screenshots/
# Editor directories and files
.vscode/*
diff --git a/docs/plans/todo.md b/docs/plans/todo.md
index 5431c1f..0383195 100644
--- a/docs/plans/todo.md
+++ b/docs/plans/todo.md
@@ -4,6 +4,54 @@ Shared execution plan for humans and LLM agents. Update this file before, during
---
+## 2026-03-03 — Revamp Pre-Made Templates (Show-Stopper Level)
+
+### Context
+Current templates are basic — generic types (no proper shapes/icons), no colors, no threats, linear/wonky layouts, only 3 templates. Need to make them professional, complex, and visually impressive.
+
+### Plan
+- [x] Rewrite 3 existing templates with proper component library types
+ - [x] Use `web_browser`, `api_gateway`, `sql_database`, `cdn`, `load_balancer`, etc. for proper shapes (hexagon, database barrel, rect)
+ - [x] Add 12-13 elements per template (up from 5-8)
+ - [x] Add colored trust boundaries (different colors per zone type)
+ - [x] Add 5 pre-populated STRIDE threats with mitigations per template
+ - [x] Add rich descriptions, technologies, stores on all elements
+ - [x] Design professional spatial layouts (layered, hub-and-spoke)
+- [x] Add 3 new templates (total 6 for 2x3 grid)
+ - [x] SaaS Platform
+ - [x] IoT Smart Building
+ - [x] Healthcare Data Platform
+- [x] Update EmptyCanvas grid layout for 6 templates (2x3 grid, per-template icons + accent colors)
+- [x] Visual validation with Playwright
+ - [x] Screenshot each template after loading
+ - [x] Fix onboarding suppression (correct localStorage keys + format)
+ - [x] Iterate on positions — spread columns wider for flow label clarity
+ - [x] Verify colors, shapes, boundary sizing — all rendering correctly
+- [x] Validate: biome clean, tsc clean, vitest 408/408 pass
+
+### Files Modified
+| File | Change |
+|------|--------|
+| `src/lib/templates.ts` | Complete rewrite — 6 templates (was 3), 12-13 elements each, colored boundaries, 5 STRIDE threats per template, proper component library types |
+| `src/components/canvas/canvas.tsx` | 2x3 template grid (was max-w-lg), per-template Lucide icons + accent colors |
+| `e2e/screenshot-templates.spec.ts` | New — Playwright visual validation script for all templates |
+| `docs/plans/todo.md` | This plan |
+
+### Files Deleted
+| File | Why |
+|------|-----|
+| `scripts/screenshot-templates.ts` | Superseded by `e2e/screenshot-templates.spec.ts` |
+
+### Notes
+- Template 1: E-Commerce Platform (revamp of Web Application)
+- Template 2: Cloud Microservices (revamp of Microservices)
+- Template 3: Mobile Banking (revamp of Mobile App)
+- Template 4: SaaS Platform (new)
+- Template 5: IoT Smart Building (new)
+- Template 6: Healthcare Data Platform (new)
+
+---
+
## 2026-03-03 — Remove Keyboard Shortcuts Dialog + Fix Broken E2E Tests
### Context
diff --git a/e2e/screenshot-templates.spec.ts b/e2e/screenshot-templates.spec.ts
new file mode 100644
index 0000000..52864c6
--- /dev/null
+++ b/e2e/screenshot-templates.spec.ts
@@ -0,0 +1,69 @@
+/**
+ * Playwright script to screenshot all templates for visual validation.
+ * Usage: npx playwright test e2e/screenshot-templates.spec.ts
+ */
+import { test } from "@playwright/test";
+
+const TEMPLATES = [
+ "ecommerce-platform",
+ "cloud-microservices",
+ "mobile-banking",
+ "saas-platform",
+ "iot-smart-building",
+ "healthcare-system",
+];
+
+/** Suppress all onboarding dialogs by pre-setting localStorage */
+async function suppressOnboarding(page: import("@playwright/test").Page) {
+ await page.addInitScript(() => {
+ // Mark What's New as seen — must match CURRENT_VERSION exactly ("1.0.0")
+ localStorage.setItem("threatforge-last-seen-version", "1.0.0");
+ // Mark all onboarding guides as completed (flat format, not Zustand persist)
+ localStorage.setItem(
+ "threatforge-onboarding",
+ JSON.stringify({
+ completedGuideIds: [
+ "welcome",
+ "dfd-basics",
+ "stride-analysis",
+ "ai-assistant",
+ ],
+ dismissedGuideIds: [],
+ }),
+ );
+ });
+}
+
+test("screenshot empty state", async ({ page }) => {
+ await suppressOnboarding(page);
+ await page.goto("/app");
+ await page.waitForSelector('[data-testid="empty-canvas"]', { timeout: 10000 });
+ await page.waitForTimeout(500);
+ await page.screenshot({
+ path: "screenshots/empty-state.png",
+ fullPage: false,
+ });
+});
+
+for (const templateId of TEMPLATES) {
+ test(`screenshot template: ${templateId}`, async ({ page }) => {
+ await suppressOnboarding(page);
+ await page.goto("/app");
+ await page.waitForSelector('[data-testid="empty-canvas"]', { timeout: 10000 });
+
+ // Click the template card
+ await page.click(`[data-testid="template-${templateId}"]`);
+
+ // Wait for canvas to render with nodes
+ await page.waitForSelector('[data-testid="canvas-area"]', { timeout: 10000 });
+ await page.waitForSelector(".react-flow__node", { timeout: 10000 });
+
+ // Give ReactFlow a moment to settle layout
+ await page.waitForTimeout(1000);
+
+ await page.screenshot({
+ path: `screenshots/template-${templateId}.png`,
+ fullPage: false,
+ });
+ });
+}
diff --git a/src/components/canvas/canvas.tsx b/src/components/canvas/canvas.tsx
index 6da21b7..17ffad4 100644
--- a/src/components/canvas/canvas.tsx
+++ b/src/components/canvas/canvas.tsx
@@ -1,4 +1,15 @@
-import { FileText, FolderOpen, Github } from "lucide-react";
+import {
+ Building2,
+ Cloud,
+ CreditCard,
+ FolderOpen,
+ Github,
+ HeartPulse,
+ type LucideIcon,
+ Radio,
+ ShoppingCart,
+ Smartphone,
+} from "lucide-react";
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useFileOperations } from "@/hooks/use-file-operations";
import { buildLayoutFromModel } from "@/lib/model-layout-utils";
@@ -85,7 +96,7 @@ function EmptyCanvas() {
{/* Templates */}
-
+
Start from a template
@@ -120,6 +131,25 @@ function EmptyCanvas() {
);
}
+const TEMPLATE_ICONS: Record
= {
+ "ecommerce-platform": ShoppingCart,
+ "cloud-microservices": Cloud,
+ "mobile-banking": Smartphone,
+ "saas-platform": CreditCard,
+ "iot-smart-building": Building2,
+ "healthcare-system": HeartPulse,
+};
+
+/** Accent colors per template (matching boundary color families) */
+const TEMPLATE_ACCENT: Record = {
+ "ecommerce-platform": "text-orange-400",
+ "cloud-microservices": "text-blue-400",
+ "mobile-banking": "text-red-400",
+ "saas-platform": "text-purple-400",
+ "iot-smart-building": "text-emerald-400",
+ "healthcare-system": "text-rose-400",
+};
+
function TemplateCard({
template,
onSelect,
@@ -127,6 +157,9 @@ function TemplateCard({
template: TemplateInfo;
onSelect: (id: string) => void;
}) {
+ const Icon = TEMPLATE_ICONS[template.id] ?? Radio;
+ const accentClass = TEMPLATE_ACCENT[template.id] ?? "text-muted-foreground";
+
return (
diff --git a/src/components/canvas/edges/data-flow-edge.tsx b/src/components/canvas/edges/data-flow-edge.tsx
index cd2210a..bb22519 100644
--- a/src/components/canvas/edges/data-flow-edge.tsx
+++ b/src/components/canvas/edges/data-flow-edge.tsx
@@ -291,7 +291,7 @@ export function DataFlowEdge({
ref={labelRef}
type="button"
className={cn(
- "pointer-events-auto nodrag nopan absolute flex items-center gap-1 rounded border px-1.5 py-0.5 text-left transition-colors",
+ "pointer-events-auto nodrag nopan absolute z-10 flex flex-col items-start gap-0.5 rounded border px-1.5 py-1 text-left transition-colors",
selected
? "border-tf-signal bg-tf-signal/10 text-foreground cursor-grab active:cursor-grabbing"
: "border-border bg-card text-muted-foreground hover:border-muted-foreground cursor-pointer",
@@ -300,30 +300,23 @@ export function DataFlowEdge({
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
>
- {edgeData?.flowNumber != null && (
-
- {edgeData.flowNumber}
-
- )}
- {isAuthenticated && }
- {hasTextLabel && (
-
- {edgeData?.name && (
-
{edgeData.name}
- )}
- {(edgeData?.protocol || (edgeData?.data && edgeData.data.length > 0)) && (
-
- {edgeData?.protocol && {edgeData.protocol}}
- {edgeData?.protocol && edgeData?.data && edgeData.data.length > 0 && (
- ·
- )}
- {edgeData?.data && edgeData.data.length > 0 && (
- {edgeData.data.join(", ")}
- )}
-
+ {(edgeData?.flowNumber != null || isAuthenticated) && (
+
+ {edgeData?.flowNumber != null && (
+
+ {edgeData.flowNumber}
+
)}
+ {isAuthenticated && }
)}
+ {edgeData?.name && (
+
{edgeData.name}
+ )}
+ {edgeData?.protocol &&
{edgeData.protocol}
}
+ {edgeData?.data && edgeData.data.length > 0 && (
+
{edgeData.data.join(", ")}
+ )}
)}
{isEditing && (
@@ -340,7 +333,7 @@ export function DataFlowEdge({
{!hasLabel && !isEditing && (isHovered || selected) && (