Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dist-ssr
playwright-report/
test-results/
blob-report/
screenshots/

# Editor directories and files
.vscode/*
Expand Down
48 changes: 48 additions & 0 deletions docs/plans/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions e2e/screenshot-templates.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
}
39 changes: 36 additions & 3 deletions src/components/canvas/canvas.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -85,7 +96,7 @@ function EmptyCanvas() {
</div>

{/* Templates */}
<div className="mt-8 w-full max-w-lg">
<div className="mt-8 w-full max-w-2xl">
<p className="mb-3 text-center text-xs font-medium uppercase tracking-wider text-muted-foreground/60">
Start from a template
</p>
Expand Down Expand Up @@ -120,21 +131,43 @@ function EmptyCanvas() {
);
}

const TEMPLATE_ICONS: Record<string, LucideIcon> = {
"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<string, string> = {
"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,
}: {
template: TemplateInfo;
onSelect: (id: string) => void;
}) {
const Icon = TEMPLATE_ICONS[template.id] ?? Radio;
const accentClass = TEMPLATE_ACCENT[template.id] ?? "text-muted-foreground";

return (
<button
type="button"
data-testid={`template-${template.id}`}
onClick={() => onSelect(template.id)}
className="flex flex-col items-start gap-1.5 rounded-lg border border-border p-3 text-left transition-colors hover:bg-accent/50 hover:border-accent"
>
<FileText className="h-4 w-4 text-muted-foreground" />
<Icon className={`h-4 w-4 ${accentClass}`} />
<span className="text-sm font-medium text-foreground">{template.name}</span>
<span className="text-xs text-muted-foreground leading-snug">{template.description}</span>
</button>
Expand Down
39 changes: 16 additions & 23 deletions src/components/canvas/edges/data-flow-edge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -300,30 +300,23 @@ export function DataFlowEdge({
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
>
{edgeData?.flowNumber != null && (
<span className="flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-muted text-[9px] font-semibold text-muted-foreground">
{edgeData.flowNumber}
</span>
)}
{isAuthenticated && <Lock className="h-3 w-3 shrink-0 text-tf-signal" />}
{hasTextLabel && (
<div>
{edgeData?.name && (
<div className="text-[11px] font-medium text-foreground">{edgeData.name}</div>
)}
{(edgeData?.protocol || (edgeData?.data && edgeData.data.length > 0)) && (
<div className="text-[9px]">
{edgeData?.protocol && <span>{edgeData.protocol}</span>}
{edgeData?.protocol && edgeData?.data && edgeData.data.length > 0 && (
<span> · </span>
)}
{edgeData?.data && edgeData.data.length > 0 && (
<span>{edgeData.data.join(", ")}</span>
)}
</div>
{(edgeData?.flowNumber != null || isAuthenticated) && (
<div className="flex items-center gap-1">
{edgeData?.flowNumber != null && (
<span className="flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-muted text-[9px] font-semibold text-muted-foreground">
{edgeData.flowNumber}
</span>
)}
{isAuthenticated && <Lock className="h-3 w-3 shrink-0 text-tf-signal" />}
</div>
)}
{edgeData?.name && (
<div className="text-[11px] font-medium text-foreground">{edgeData.name}</div>
)}
{edgeData?.protocol && <div className="text-[9px]">{edgeData.protocol}</div>}
{edgeData?.data && edgeData.data.length > 0 && (
<div className="text-[9px]">{edgeData.data.join(", ")}</div>
)}
</button>
)}
{isEditing && (
Expand All @@ -340,7 +333,7 @@ export function DataFlowEdge({
{!hasLabel && !isEditing && (isHovered || selected) && (
<button
type="button"
className="pointer-events-auto nodrag nopan absolute flex h-5 w-5 items-center justify-center rounded-full border border-border bg-card text-muted-foreground transition-colors hover:bg-accent"
className="pointer-events-auto nodrag nopan absolute z-10 flex h-5 w-5 items-center justify-center rounded-full border border-border bg-card text-muted-foreground transition-colors hover:bg-accent"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
Expand Down
Loading
Loading