@hustle-together/api-dev-tools 3.11.1 → 3.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/code-reviewer.md +170 -0
- package/.claude/agents/docs-generator.md +80 -0
- package/.claude/agents/implementation-reviewer.md +119 -0
- package/.claude/agents/parallel-researcher.md +52 -0
- package/.claude/agents/research-validator.md +116 -0
- package/.claude/agents/schema-generator.md +70 -0
- package/.claude/agents/test-writer.md +104 -0
- package/.claude/api-dev-state.json +305 -56
- package/.claude/commands/README.md +21 -10
- package/.claude/commands/add-command.md +8 -5
- package/.claude/commands/api-create.md +36 -25
- package/.claude/commands/api-env.md +1 -0
- package/.claude/commands/api-interview.md +32 -19
- package/.claude/commands/api-research.md +47 -21
- package/.claude/commands/api-status.md +21 -1
- package/.claude/commands/api-verify.md +14 -13
- package/.claude/commands/beepboop.md +4 -5
- package/.claude/commands/busycommit.md +2 -3
- package/.claude/commands/commit.md +2 -3
- package/.claude/commands/cycle.md +2 -7
- package/.claude/commands/gap.md +2 -3
- package/.claude/commands/green.md +2 -7
- package/.claude/commands/issue.md +3 -8
- package/.claude/commands/ntfy-setup.md +91 -0
- package/.claude/commands/ntfy-test.md +74 -0
- package/.claude/commands/plan.md +2 -3
- package/.claude/commands/pr.md +2 -3
- package/.claude/commands/publish.md +40 -0
- package/.claude/commands/red.md +2 -7
- package/.claude/commands/refactor.md +2 -7
- package/.claude/commands/spike.md +2 -7
- package/.claude/commands/summarize.md +2 -3
- package/.claude/commands/tdd.md +2 -7
- package/.claude/commands/worktree-add.md +208 -216
- package/.claude/commands/worktree-cleanup.md +172 -178
- package/.claude/settings.json +63 -12
- package/.claude/settings.local.json +2 -1
- package/.claude-plugin/marketplace.json +2 -11
- package/.skills/README.md +55 -53
- package/.skills/_shared/settings.json +1 -1
- package/.skills/add-command/SKILL.md +10 -5
- package/.skills/api-create/SKILL.md +146 -35
- package/.skills/api-env/SKILL.md +1 -0
- package/.skills/api-interview/SKILL.md +32 -19
- package/.skills/api-research/SKILL.md +47 -21
- package/.skills/api-status/SKILL.md +21 -1
- package/.skills/api-verify/SKILL.md +14 -13
- package/.skills/beepboop/SKILL.md +6 -5
- package/.skills/busycommit/SKILL.md +4 -3
- package/.skills/commit/SKILL.md +4 -3
- package/.skills/cycle/SKILL.md +4 -7
- package/.skills/gap/SKILL.md +4 -3
- package/.skills/green/SKILL.md +4 -7
- package/.skills/issue/SKILL.md +5 -8
- package/.skills/plan/SKILL.md +4 -3
- package/.skills/pr/SKILL.md +4 -3
- package/.skills/publish/SKILL.md +160 -0
- package/.skills/red/SKILL.md +4 -7
- package/.skills/refactor/SKILL.md +4 -7
- package/.skills/spike/SKILL.md +4 -7
- package/.skills/summarize/SKILL.md +4 -3
- package/.skills/tdd/SKILL.md +4 -7
- package/.skills/update-todos/SKILL.md +22 -0
- package/.skills/worktree-add/SKILL.md +210 -216
- package/.skills/worktree-cleanup/SKILL.md +183 -187
- package/CHANGELOG.md +97 -79
- package/README.md +161 -7142
- package/bin/cli.js +448 -805
- package/commands/README.md +66 -31
- package/commands/add-command.md +8 -5
- package/commands/beepboop.md +4 -5
- package/commands/busycommit.md +2 -3
- package/commands/commit.md +2 -3
- package/commands/cycle.md +2 -7
- package/commands/gap.md +2 -3
- package/commands/green.md +2 -7
- package/commands/hustle-api-continue.md +8 -5
- package/commands/hustle-api-create.md +70 -29
- package/commands/hustle-api-env.md +1 -0
- package/commands/hustle-api-interview.md +32 -19
- package/commands/hustle-api-research.md +47 -21
- package/commands/hustle-api-sessions.md +8 -7
- package/commands/hustle-api-status.md +21 -1
- package/commands/hustle-api-verify.md +14 -13
- package/commands/hustle-combine.md +488 -241
- package/commands/hustle-ui-create-page.md +113 -50
- package/commands/hustle-ui-create.md +179 -26
- package/commands/issue.md +3 -8
- package/commands/plan.md +2 -3
- package/commands/pr.md +2 -3
- package/commands/red.md +2 -7
- package/commands/refactor.md +2 -7
- package/commands/spike.md +2 -7
- package/commands/summarize.md +2 -3
- package/commands/tdd.md +2 -7
- package/commands/worktree-add.md +208 -216
- package/commands/worktree-cleanup.md +172 -178
- package/hooks/api-workflow-check.py +5 -3
- package/hooks/enforce-component-type-confirm.py +97 -0
- package/hooks/lib/__init__.py +1 -0
- package/hooks/lib/greptile.py +355 -0
- package/hooks/lib/ntfy.py +209 -0
- package/hooks/notify-input-needed.py +73 -0
- package/hooks/notify-phase-complete.py +90 -0
- package/hooks/run-code-review.py +246 -0
- package/hooks/track-token-usage.py +121 -0
- package/package.json +13 -3
- package/scripts/collect-test-results.ts +102 -77
- package/scripts/extract-parameters.ts +112 -70
- package/scripts/generate-test-manifest.ts +118 -77
- package/templates/.env.example +57 -0
- package/templates/BRAND_GUIDE.md +92 -52
- package/templates/CLAUDE-SECTION.md +40 -37
- package/templates/SPEC.json +186 -38
- package/templates/api-dev-state.json +33 -4
- package/templates/api-showcase/_components/APICard.tsx +22 -18
- package/templates/api-showcase/_components/APIModal.tsx +110 -64
- package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
- package/templates/api-showcase/_components/APITester.tsx +128 -67
- package/templates/api-showcase/page.tsx +4 -4
- package/templates/api-test/page.tsx +51 -30
- package/templates/api-test/test-structure/route.ts +43 -34
- package/templates/component/Component.stories.tsx +41 -39
- package/templates/component/Component.test.tsx +96 -78
- package/templates/component/Component.tsx +63 -52
- package/templates/component/Component.types.ts +10 -6
- package/templates/component/Component.visual.spec.ts +170 -0
- package/templates/component/index.ts +2 -2
- package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
- package/templates/dev-tools/page.tsx +4 -3
- package/templates/mcp-servers.json +30 -2
- package/templates/page/page.e2e.test.ts +56 -48
- package/templates/page/page.tsx +3 -3
- package/templates/shared/HeroHeader.tsx +16 -15
- package/templates/shared/index.ts +1 -1
- package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
- package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
- package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
- package/templates/ui-showcase/page.tsx +4 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from
|
|
4
|
-
import Link from
|
|
5
|
-
import { HeroHeader } from
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { HeroHeader } from "../../shared/HeroHeader";
|
|
6
6
|
|
|
7
7
|
interface Registry {
|
|
8
8
|
version?: string;
|
|
@@ -25,7 +25,7 @@ export function DevToolsLanding() {
|
|
|
25
25
|
const [loading, setLoading] = useState(true);
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
28
|
-
fetch(
|
|
28
|
+
fetch("/api/registry")
|
|
29
29
|
.then((res) => res.json())
|
|
30
30
|
.then((data) => {
|
|
31
31
|
setRegistry(data);
|
|
@@ -52,7 +52,7 @@ export function DevToolsLanding() {
|
|
|
52
52
|
badge="Developer Portal"
|
|
53
53
|
description={
|
|
54
54
|
<>
|
|
55
|
-
Central hub for <strong>API development</strong>,{
|
|
55
|
+
Central hub for <strong>API development</strong>,{" "}
|
|
56
56
|
<strong>UI components</strong>, and documentation. Built with the
|
|
57
57
|
Hustle Together workflow.
|
|
58
58
|
</>
|
|
@@ -64,7 +64,7 @@ export function DevToolsLanding() {
|
|
|
64
64
|
<div className="mb-8 flex flex-wrap items-center gap-6 border-2 border-black bg-gray-50 p-6 dark:border-gray-700 dark:bg-gray-900">
|
|
65
65
|
<div className="flex items-center gap-2">
|
|
66
66
|
<span className="text-3xl font-bold text-black dark:text-white">
|
|
67
|
-
{loading ?
|
|
67
|
+
{loading ? "..." : total}
|
|
68
68
|
</span>
|
|
69
69
|
<span className="text-gray-600 dark:text-gray-400">
|
|
70
70
|
Total Items
|
|
@@ -207,7 +207,7 @@ export function DevToolsLanding() {
|
|
|
207
207
|
source of truth for the showcase pages.
|
|
208
208
|
</p>
|
|
209
209
|
<div className="text-sm font-bold text-[#BA0C2F]">
|
|
210
|
-
Version: {registry.version ||
|
|
210
|
+
Version: {registry.version || "1.0.0"}
|
|
211
211
|
</div>
|
|
212
212
|
</Link>
|
|
213
213
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { DevToolsLanding } from
|
|
1
|
+
import { DevToolsLanding } from "./_components/DevToolsLanding";
|
|
2
2
|
|
|
3
3
|
export const metadata = {
|
|
4
|
-
title:
|
|
5
|
-
description:
|
|
4
|
+
title: "Hustle Dev Tools",
|
|
5
|
+
description:
|
|
6
|
+
"Developer tools for API and UI development with Hustle Together",
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export default function DevToolsPage() {
|
|
@@ -2,14 +2,42 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"context7": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["-y", "@upstash/context7-mcp"]
|
|
5
|
+
"args": ["-y", "@upstash/context7-mcp"],
|
|
6
|
+
"description": "Real-time documentation lookup for libraries and APIs"
|
|
6
7
|
},
|
|
7
8
|
"github": {
|
|
8
9
|
"command": "npx",
|
|
9
10
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
10
11
|
"env": {
|
|
11
12
|
"GITHUB_PERSONAL_ACCESS_TOKEN": ""
|
|
12
|
-
}
|
|
13
|
+
},
|
|
14
|
+
"description": "GitHub integration for PRs, issues, and repository management"
|
|
15
|
+
},
|
|
16
|
+
"playwright": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "@anthropics/mcp-playwright"],
|
|
19
|
+
"description": "Browser automation for visual testing and screenshot capture"
|
|
20
|
+
},
|
|
21
|
+
"greptile": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "@anthropics/mcp-greptile"],
|
|
24
|
+
"env": {
|
|
25
|
+
"GREPTILE_API_KEY": "",
|
|
26
|
+
"GITHUB_TOKEN": ""
|
|
27
|
+
},
|
|
28
|
+
"description": "AI-powered code review with full codebase context (Phase 14)"
|
|
13
29
|
}
|
|
30
|
+
},
|
|
31
|
+
"_readme": {
|
|
32
|
+
"installation": [
|
|
33
|
+
"1. Copy this file to your project as .mcp.json",
|
|
34
|
+
"2. Or run: claude mcp add context7 --from-template",
|
|
35
|
+
"3. Add your GitHub token to the env section",
|
|
36
|
+
"4. Restart Claude Code to load new servers"
|
|
37
|
+
],
|
|
38
|
+
"verification": [
|
|
39
|
+
"Run /mcp in Claude Code to verify servers are connected",
|
|
40
|
+
"Use 'claude mcp list' to see all configured servers"
|
|
41
|
+
]
|
|
14
42
|
}
|
|
15
43
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test, expect } from
|
|
1
|
+
import { test, expect } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* E2E Tests for __PAGE_NAME__ Page
|
|
@@ -7,32 +7,32 @@ import { test, expect } from '@playwright/test';
|
|
|
7
7
|
*
|
|
8
8
|
* Run with: pnpm playwright test __PAGE_ROUTE__.spec.ts
|
|
9
9
|
*/
|
|
10
|
-
test.describe(
|
|
10
|
+
test.describe("__PAGE_NAME__ Page", () => {
|
|
11
11
|
test.beforeEach(async ({ page }) => {
|
|
12
12
|
// Navigate to the page before each test
|
|
13
|
-
await page.goto(
|
|
13
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
// ===================================
|
|
17
17
|
// Basic Rendering Tests
|
|
18
18
|
// ===================================
|
|
19
19
|
|
|
20
|
-
test(
|
|
20
|
+
test("should load successfully", async ({ page }) => {
|
|
21
21
|
// Wait for page to be fully loaded
|
|
22
|
-
await page.waitForLoadState(
|
|
22
|
+
await page.waitForLoadState("networkidle");
|
|
23
23
|
|
|
24
24
|
// Check page title
|
|
25
25
|
await expect(page).toHaveTitle(/__PAGE_TITLE__/);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test(
|
|
29
|
-
const heading = page.getByRole(
|
|
28
|
+
test("should display page heading", async ({ page }) => {
|
|
29
|
+
const heading = page.getByRole("heading", { level: 1 });
|
|
30
30
|
await expect(heading).toBeVisible();
|
|
31
|
-
await expect(heading).toContainText(
|
|
31
|
+
await expect(heading).toContainText("__PAGE_TITLE__");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test(
|
|
35
|
-
const description = page.getByText(
|
|
34
|
+
test("should display page description", async ({ page }) => {
|
|
35
|
+
const description = page.getByText("__PAGE_DESCRIPTION__");
|
|
36
36
|
await expect(description).toBeVisible();
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -40,64 +40,66 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
40
40
|
// Responsive Tests
|
|
41
41
|
// ===================================
|
|
42
42
|
|
|
43
|
-
test(
|
|
43
|
+
test("should be responsive on mobile", async ({ page }) => {
|
|
44
44
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
45
|
-
await page.goto(
|
|
45
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
46
46
|
|
|
47
47
|
// Verify main content is visible
|
|
48
|
-
await expect(page.getByRole(
|
|
48
|
+
await expect(page.getByRole("main")).toBeVisible();
|
|
49
49
|
|
|
50
50
|
// Verify no horizontal scroll
|
|
51
|
-
const body = await page.locator(
|
|
51
|
+
const body = await page.locator("body");
|
|
52
52
|
const scrollWidth = await body.evaluate((el) => el.scrollWidth);
|
|
53
53
|
const clientWidth = await body.evaluate((el) => el.clientWidth);
|
|
54
54
|
expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 1); // +1 for rounding
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test(
|
|
57
|
+
test("should be responsive on tablet", async ({ page }) => {
|
|
58
58
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
59
|
-
await page.goto(
|
|
59
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
60
60
|
|
|
61
|
-
await expect(page.getByRole(
|
|
61
|
+
await expect(page.getByRole("main")).toBeVisible();
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
test(
|
|
64
|
+
test("should be responsive on desktop", async ({ page }) => {
|
|
65
65
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
66
|
-
await page.goto(
|
|
66
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
67
67
|
|
|
68
|
-
await expect(page.getByRole(
|
|
68
|
+
await expect(page.getByRole("main")).toBeVisible();
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
// ===================================
|
|
72
72
|
// Accessibility Tests
|
|
73
73
|
// ===================================
|
|
74
74
|
|
|
75
|
-
test(
|
|
75
|
+
test("should have no accessibility violations", async ({ page }) => {
|
|
76
76
|
// Note: Requires @axe-core/playwright
|
|
77
77
|
// const results = await new AxeBuilder({ page }).analyze();
|
|
78
78
|
// expect(results.violations).toEqual([]);
|
|
79
79
|
|
|
80
80
|
// Basic accessibility checks
|
|
81
81
|
// All images should have alt text
|
|
82
|
-
const images = await page.getByRole(
|
|
82
|
+
const images = await page.getByRole("img").all();
|
|
83
83
|
for (const img of images) {
|
|
84
|
-
await expect(img).toHaveAttribute(
|
|
84
|
+
await expect(img).toHaveAttribute("alt");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// All buttons should have accessible names
|
|
88
|
-
const buttons = await page.getByRole(
|
|
88
|
+
const buttons = await page.getByRole("button").all();
|
|
89
89
|
for (const button of buttons) {
|
|
90
|
-
const name =
|
|
90
|
+
const name =
|
|
91
|
+
(await button.getAttribute("aria-label")) ||
|
|
92
|
+
(await button.textContent());
|
|
91
93
|
expect(name?.trim()).toBeTruthy();
|
|
92
94
|
}
|
|
93
95
|
});
|
|
94
96
|
|
|
95
|
-
test(
|
|
97
|
+
test("should be keyboard navigable", async ({ page }) => {
|
|
96
98
|
// Tab through interactive elements
|
|
97
|
-
await page.keyboard.press(
|
|
99
|
+
await page.keyboard.press("Tab");
|
|
98
100
|
|
|
99
101
|
// Verify focus is visible
|
|
100
|
-
const focusedElement = page.locator(
|
|
102
|
+
const focusedElement = page.locator(":focus");
|
|
101
103
|
await expect(focusedElement).toBeVisible();
|
|
102
104
|
});
|
|
103
105
|
|
|
@@ -107,9 +109,9 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
107
109
|
// Tests FAIL if exceeded, triggering TDD loop-back
|
|
108
110
|
// ===================================
|
|
109
111
|
|
|
110
|
-
test(
|
|
112
|
+
test("should load within performance budget", async ({ page }) => {
|
|
111
113
|
const startTime = Date.now();
|
|
112
|
-
await page.goto(
|
|
114
|
+
await page.goto("/__PAGE_ROUTE__", { waitUntil: "networkidle" });
|
|
113
115
|
const loadTime = Date.now() - startTime;
|
|
114
116
|
|
|
115
117
|
// THRESHOLD: Page load max 3000ms
|
|
@@ -117,15 +119,17 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
117
119
|
expect(loadTime).toBeLessThan(3000);
|
|
118
120
|
});
|
|
119
121
|
|
|
120
|
-
test(
|
|
121
|
-
await page.goto(
|
|
122
|
+
test("should have acceptable memory usage", async ({ page }) => {
|
|
123
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
122
124
|
|
|
123
125
|
// Get Chromium-specific metrics
|
|
124
126
|
const client = await page.context().newCDPSession(page);
|
|
125
|
-
const metrics = await client.send(
|
|
127
|
+
const metrics = await client.send("Performance.getMetrics");
|
|
126
128
|
|
|
127
|
-
const jsHeapSize =
|
|
128
|
-
|
|
129
|
+
const jsHeapSize =
|
|
130
|
+
metrics.metrics.find((m) => m.name === "JSHeapUsedSize")?.value || 0;
|
|
131
|
+
const domNodes =
|
|
132
|
+
metrics.metrics.find((m) => m.name === "Nodes")?.value || 0;
|
|
129
133
|
|
|
130
134
|
// THRESHOLD: Memory max 50MB
|
|
131
135
|
// If this fails, check for: memory leaks, large state, unbounded lists
|
|
@@ -136,14 +140,16 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
136
140
|
expect(domNodes).toBeLessThan(1500);
|
|
137
141
|
});
|
|
138
142
|
|
|
139
|
-
test(
|
|
140
|
-
await page.goto(
|
|
143
|
+
test("should not have layout thrashing", async ({ page }) => {
|
|
144
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
141
145
|
|
|
142
146
|
const client = await page.context().newCDPSession(page);
|
|
143
|
-
const metrics = await client.send(
|
|
147
|
+
const metrics = await client.send("Performance.getMetrics");
|
|
144
148
|
|
|
145
|
-
const layoutCount =
|
|
146
|
-
|
|
149
|
+
const layoutCount =
|
|
150
|
+
metrics.metrics.find((m) => m.name === "LayoutCount")?.value || 0;
|
|
151
|
+
const layoutDuration =
|
|
152
|
+
metrics.metrics.find((m) => m.name === "LayoutDuration")?.value || 0;
|
|
147
153
|
|
|
148
154
|
// THRESHOLD: Layout count max 10
|
|
149
155
|
// If this fails, batch DOM updates, use CSS transforms instead of layout properties
|
|
@@ -153,14 +159,16 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
153
159
|
expect(layoutDuration * 1000).toBeLessThan(100);
|
|
154
160
|
});
|
|
155
161
|
|
|
156
|
-
test(
|
|
157
|
-
await page.goto(
|
|
162
|
+
test("should meet Core Web Vitals", async ({ page }) => {
|
|
163
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
158
164
|
|
|
159
165
|
// Get paint timing
|
|
160
166
|
const paintTiming = await page.evaluate(() => {
|
|
161
|
-
const entries = performance.getEntriesByType(
|
|
167
|
+
const entries = performance.getEntriesByType("paint");
|
|
162
168
|
return {
|
|
163
|
-
fcp:
|
|
169
|
+
fcp:
|
|
170
|
+
entries.find((e) => e.name === "first-contentful-paint")?.startTime ||
|
|
171
|
+
0,
|
|
164
172
|
};
|
|
165
173
|
});
|
|
166
174
|
|
|
@@ -175,7 +183,7 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
175
183
|
const entries = list.getEntries();
|
|
176
184
|
const lastEntry = entries[entries.length - 1];
|
|
177
185
|
resolve(lastEntry?.startTime || 0);
|
|
178
|
-
}).observe({ type:
|
|
186
|
+
}).observe({ type: "largest-contentful-paint", buffered: true });
|
|
179
187
|
|
|
180
188
|
// Timeout fallback
|
|
181
189
|
setTimeout(() => resolve(0), 3000);
|
|
@@ -206,13 +214,13 @@ test.describe('__PAGE_NAME__ Page', () => {
|
|
|
206
214
|
// Error Handling Tests
|
|
207
215
|
// ===================================
|
|
208
216
|
|
|
209
|
-
test(
|
|
217
|
+
test("should handle errors gracefully", async ({ page }) => {
|
|
210
218
|
// Intercept API calls to simulate errors (if applicable)
|
|
211
219
|
// await page.route('/api/*', route => route.fulfill({ status: 500 }));
|
|
212
220
|
|
|
213
|
-
await page.goto(
|
|
221
|
+
await page.goto("/__PAGE_ROUTE__");
|
|
214
222
|
|
|
215
223
|
// Page should still render without crashing
|
|
216
|
-
await expect(page.getByRole(
|
|
224
|
+
await expect(page.getByRole("main")).toBeVisible();
|
|
217
225
|
});
|
|
218
226
|
});
|
package/templates/page/page.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Metadata } from
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Page metadata for SEO
|
|
5
5
|
*/
|
|
6
6
|
export const metadata: Metadata = {
|
|
7
|
-
title:
|
|
8
|
-
description:
|
|
7
|
+
title: "__PAGE_TITLE__",
|
|
8
|
+
description: "__PAGE_DESCRIPTION__",
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
5
|
interface HeroHeaderProps {
|
|
6
6
|
title: string;
|
|
@@ -29,8 +29,8 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
29
29
|
useEffect(() => {
|
|
30
30
|
const checkDarkMode = () => {
|
|
31
31
|
setIsDark(
|
|
32
|
-
document.documentElement.classList.contains(
|
|
33
|
-
document.documentElement.getAttribute(
|
|
32
|
+
document.documentElement.classList.contains("dark") ||
|
|
33
|
+
document.documentElement.getAttribute("data-theme") === "dark",
|
|
34
34
|
);
|
|
35
35
|
};
|
|
36
36
|
|
|
@@ -40,7 +40,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
40
40
|
const observer = new MutationObserver(checkDarkMode);
|
|
41
41
|
observer.observe(document.documentElement, {
|
|
42
42
|
attributes: true,
|
|
43
|
-
attributeFilter: [
|
|
43
|
+
attributeFilter: ["class", "data-theme"],
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
return () => observer.disconnect();
|
|
@@ -52,7 +52,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
52
52
|
const header = headerRef.current;
|
|
53
53
|
if (!canvas || !header) return;
|
|
54
54
|
|
|
55
|
-
const ctx = canvas.getContext(
|
|
55
|
+
const ctx = canvas.getContext("2d");
|
|
56
56
|
if (!ctx) return;
|
|
57
57
|
|
|
58
58
|
let animationId: number;
|
|
@@ -79,7 +79,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
79
79
|
const createRow = (): GridCell[] => {
|
|
80
80
|
const cells: GridCell[] = [];
|
|
81
81
|
for (let c = 0; c < gridWidth; c++) {
|
|
82
|
-
cells.push({ active: false, alpha: 0, color:
|
|
82
|
+
cells.push({ active: false, alpha: 0, color: "rgba(186, 12, 47" });
|
|
83
83
|
}
|
|
84
84
|
return cells;
|
|
85
85
|
};
|
|
@@ -96,7 +96,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
96
96
|
|
|
97
97
|
const project = (
|
|
98
98
|
x: number,
|
|
99
|
-
z: number
|
|
99
|
+
z: number,
|
|
100
100
|
): { x: number; y: number; scale: number } | null => {
|
|
101
101
|
if (z <= 0) return null;
|
|
102
102
|
const scale = fov / z;
|
|
@@ -106,7 +106,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
const updateGridState = () => {
|
|
109
|
-
const secondaryColor = isDark ?
|
|
109
|
+
const secondaryColor = isDark ? "rgba(60, 60, 60" : "rgba(30, 30, 30";
|
|
110
110
|
|
|
111
111
|
if (Math.random() > 0.92) {
|
|
112
112
|
const r = Math.floor(Math.random() * (gridRows.length - 5)) + 2;
|
|
@@ -115,7 +115,8 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
115
115
|
if (!cell.active) {
|
|
116
116
|
cell.active = true;
|
|
117
117
|
cell.alpha = 1.0;
|
|
118
|
-
cell.color =
|
|
118
|
+
cell.color =
|
|
119
|
+
Math.random() > 0.7 ? secondaryColor : "rgba(186, 12, 47";
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -128,7 +129,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
128
129
|
cell.alpha = 0;
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
|
-
})
|
|
132
|
+
}),
|
|
132
133
|
);
|
|
133
134
|
};
|
|
134
135
|
|
|
@@ -145,7 +146,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
145
146
|
updateGridState();
|
|
146
147
|
ctx.lineWidth = 1;
|
|
147
148
|
|
|
148
|
-
const lineColor = isDark ?
|
|
149
|
+
const lineColor = isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
|
|
149
150
|
|
|
150
151
|
// Draw rows (back to front)
|
|
151
152
|
for (let r = gridRows.length - 1; r >= 0; r--) {
|
|
@@ -208,7 +209,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
// Fog / fade gradient
|
|
211
|
-
const fadeColor = isDark ?
|
|
212
|
+
const fadeColor = isDark ? "0,0,0" : "255,255,255";
|
|
212
213
|
const g = ctx.createLinearGradient(0, 0, 0, canvas.height / 1.5);
|
|
213
214
|
g.addColorStop(0, `rgba(${fadeColor}, 1)`);
|
|
214
215
|
g.addColorStop(0.3, `rgba(${fadeColor}, 0.8)`);
|
|
@@ -220,11 +221,11 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
|
220
221
|
};
|
|
221
222
|
|
|
222
223
|
resize();
|
|
223
|
-
window.addEventListener(
|
|
224
|
+
window.addEventListener("resize", resize);
|
|
224
225
|
draw();
|
|
225
226
|
|
|
226
227
|
return () => {
|
|
227
|
-
window.removeEventListener(
|
|
228
|
+
window.removeEventListener("resize", resize);
|
|
228
229
|
cancelAnimationFrame(animationId);
|
|
229
230
|
};
|
|
230
231
|
}, [isDark]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { HeroHeader } from
|
|
1
|
+
export { HeroHeader } from "./HeroHeader";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
interface PreviewCardProps {
|
|
4
4
|
id: string;
|
|
5
|
-
type:
|
|
5
|
+
type: "component" | "page";
|
|
6
6
|
name: string;
|
|
7
7
|
description?: string;
|
|
8
8
|
variants?: string[];
|
|
@@ -38,7 +38,7 @@ export function PreviewCard({
|
|
|
38
38
|
// Get page route from file path or prop
|
|
39
39
|
const getPageRoute = () => {
|
|
40
40
|
if (route) return route;
|
|
41
|
-
if (file?.includes(
|
|
41
|
+
if (file?.includes("src/app/")) {
|
|
42
42
|
const match = file.match(/src\/app\/(.+?)\/page\.tsx?$/);
|
|
43
43
|
if (match) return `/${match[1]}`;
|
|
44
44
|
}
|
|
@@ -52,28 +52,24 @@ export function PreviewCard({
|
|
|
52
52
|
>
|
|
53
53
|
{/* Preview Area */}
|
|
54
54
|
<div className="relative aspect-video w-full overflow-hidden bg-gray-100 dark:bg-gray-800">
|
|
55
|
-
{type ===
|
|
55
|
+
{type === "page" ? (
|
|
56
56
|
// REAL iframe preview for pages - scaled down to fit
|
|
57
57
|
<iframe
|
|
58
58
|
src={getPageRoute()}
|
|
59
59
|
title={`Preview of ${name}`}
|
|
60
60
|
className="pointer-events-none h-full w-full origin-top-left scale-[0.5]"
|
|
61
|
-
style={{ width:
|
|
61
|
+
style={{ width: "200%", height: "200%" }}
|
|
62
62
|
loading="lazy"
|
|
63
63
|
/>
|
|
64
64
|
) : (
|
|
65
65
|
// Component preview - generated HTML matching Sandpack style
|
|
66
|
-
<ComponentPreview
|
|
67
|
-
id={id}
|
|
68
|
-
name={name}
|
|
69
|
-
variants={variants}
|
|
70
|
-
/>
|
|
66
|
+
<ComponentPreview id={id} name={name} variants={variants} />
|
|
71
67
|
)}
|
|
72
68
|
|
|
73
69
|
{/* Type Badge */}
|
|
74
70
|
<div className="absolute right-2 top-2">
|
|
75
71
|
<span className="border border-black bg-white px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-black dark:border-gray-600 dark:bg-gray-800 dark:text-white">
|
|
76
|
-
{type ===
|
|
72
|
+
{type === "component" ? "Component" : "Page"}
|
|
77
73
|
</span>
|
|
78
74
|
</div>
|
|
79
75
|
|
|
@@ -119,7 +115,7 @@ export function PreviewCard({
|
|
|
119
115
|
|
|
120
116
|
{usesComponents && usesComponents.length > 0 && (
|
|
121
117
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
122
|
-
Uses: {usesComponents.slice(0, 3).join(
|
|
118
|
+
Uses: {usesComponents.slice(0, 3).join(", ")}
|
|
123
119
|
{usesComponents.length > 3 && ` +${usesComponents.length - 3}`}
|
|
124
120
|
</p>
|
|
125
121
|
)}
|
|
@@ -143,7 +139,7 @@ function ComponentPreview({
|
|
|
143
139
|
name: string;
|
|
144
140
|
variants?: string[];
|
|
145
141
|
}) {
|
|
146
|
-
const previewHtml = generatePreviewHtml(name, variants?.[0] ||
|
|
142
|
+
const previewHtml = generatePreviewHtml(name, variants?.[0] || "primary");
|
|
147
143
|
|
|
148
144
|
return (
|
|
149
145
|
<iframe
|
|
@@ -163,9 +159,9 @@ function ComponentPreview({
|
|
|
163
159
|
function generatePreviewHtml(name: string, variant: string): string {
|
|
164
160
|
const lowerName = name.toLowerCase();
|
|
165
161
|
|
|
166
|
-
let content =
|
|
162
|
+
let content = "";
|
|
167
163
|
|
|
168
|
-
if (lowerName.includes(
|
|
164
|
+
if (lowerName.includes("button")) {
|
|
169
165
|
content = `
|
|
170
166
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
171
167
|
<button style="
|
|
@@ -188,7 +184,7 @@ function generatePreviewHtml(name: string, variant: string): string {
|
|
|
188
184
|
">Secondary</button>
|
|
189
185
|
</div>
|
|
190
186
|
`;
|
|
191
|
-
} else if (lowerName.includes(
|
|
187
|
+
} else if (lowerName.includes("card")) {
|
|
192
188
|
content = `
|
|
193
189
|
<div style="
|
|
194
190
|
border: 2px solid #000;
|
|
@@ -203,7 +199,11 @@ function generatePreviewHtml(name: string, variant: string): string {
|
|
|
203
199
|
</div>
|
|
204
200
|
</div>
|
|
205
201
|
`;
|
|
206
|
-
} else if (
|
|
202
|
+
} else if (
|
|
203
|
+
lowerName.includes("input") ||
|
|
204
|
+
lowerName.includes("field") ||
|
|
205
|
+
lowerName.includes("form")
|
|
206
|
+
) {
|
|
207
207
|
content = `
|
|
208
208
|
<div style="width: 140px;">
|
|
209
209
|
<label style="display: block; font-size: 11px; font-weight: bold; margin-bottom: 4px;">Label</label>
|
|
@@ -217,7 +217,7 @@ function generatePreviewHtml(name: string, variant: string): string {
|
|
|
217
217
|
<p style="font-size: 10px; color: #666; margin: 4px 0 0;">Helper text</p>
|
|
218
218
|
</div>
|
|
219
219
|
`;
|
|
220
|
-
} else if (lowerName.includes(
|
|
220
|
+
} else if (lowerName.includes("table")) {
|
|
221
221
|
content = `
|
|
222
222
|
<div style="border: 2px solid #000; font-size: 10px; width: 160px;">
|
|
223
223
|
<div style="display: flex; background: #f0f0f0; border-bottom: 1px solid #ccc;">
|
|
@@ -234,7 +234,7 @@ function generatePreviewHtml(name: string, variant: string): string {
|
|
|
234
234
|
</div>
|
|
235
235
|
</div>
|
|
236
236
|
`;
|
|
237
|
-
} else if (lowerName.includes(
|
|
237
|
+
} else if (lowerName.includes("header") || lowerName.includes("nav")) {
|
|
238
238
|
content = `
|
|
239
239
|
<div style="border: 2px solid #000; background: white; padding: 8px 12px; width: 180px;">
|
|
240
240
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
@@ -247,7 +247,7 @@ function generatePreviewHtml(name: string, variant: string): string {
|
|
|
247
247
|
</div>
|
|
248
248
|
</div>
|
|
249
249
|
`;
|
|
250
|
-
} else if (lowerName.includes(
|
|
250
|
+
} else if (lowerName.includes("modal") || lowerName.includes("dialog")) {
|
|
251
251
|
content = `
|
|
252
252
|
<div style="position: relative; width: 140px; height: 100px;">
|
|
253
253
|
<div style="position: absolute; inset: 0; background: rgba(0,0,0,0.3);"></div>
|