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