@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.
Files changed (139) hide show
  1. package/.claude/agents/code-reviewer.md +170 -0
  2. package/.claude/agents/docs-generator.md +80 -0
  3. package/.claude/agents/implementation-reviewer.md +119 -0
  4. package/.claude/agents/parallel-researcher.md +52 -0
  5. package/.claude/agents/research-validator.md +116 -0
  6. package/.claude/agents/schema-generator.md +70 -0
  7. package/.claude/agents/test-writer.md +104 -0
  8. package/.claude/api-dev-state.json +305 -56
  9. package/.claude/commands/README.md +21 -10
  10. package/.claude/commands/add-command.md +8 -5
  11. package/.claude/commands/api-create.md +36 -25
  12. package/.claude/commands/api-env.md +1 -0
  13. package/.claude/commands/api-interview.md +32 -19
  14. package/.claude/commands/api-research.md +47 -21
  15. package/.claude/commands/api-status.md +21 -1
  16. package/.claude/commands/api-verify.md +14 -13
  17. package/.claude/commands/beepboop.md +4 -5
  18. package/.claude/commands/busycommit.md +2 -3
  19. package/.claude/commands/commit.md +2 -3
  20. package/.claude/commands/cycle.md +2 -7
  21. package/.claude/commands/gap.md +2 -3
  22. package/.claude/commands/green.md +2 -7
  23. package/.claude/commands/issue.md +3 -8
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +2 -3
  27. package/.claude/commands/pr.md +2 -3
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +2 -7
  30. package/.claude/commands/refactor.md +2 -7
  31. package/.claude/commands/spike.md +2 -7
  32. package/.claude/commands/summarize.md +2 -3
  33. package/.claude/commands/tdd.md +2 -7
  34. package/.claude/commands/worktree-add.md +208 -216
  35. package/.claude/commands/worktree-cleanup.md +172 -178
  36. package/.claude/settings.json +63 -12
  37. package/.claude/settings.local.json +2 -1
  38. package/.claude-plugin/marketplace.json +2 -11
  39. package/.skills/README.md +55 -53
  40. package/.skills/_shared/settings.json +1 -1
  41. package/.skills/add-command/SKILL.md +10 -5
  42. package/.skills/api-create/SKILL.md +146 -35
  43. package/.skills/api-env/SKILL.md +1 -0
  44. package/.skills/api-interview/SKILL.md +32 -19
  45. package/.skills/api-research/SKILL.md +47 -21
  46. package/.skills/api-status/SKILL.md +21 -1
  47. package/.skills/api-verify/SKILL.md +14 -13
  48. package/.skills/beepboop/SKILL.md +6 -5
  49. package/.skills/busycommit/SKILL.md +4 -3
  50. package/.skills/commit/SKILL.md +4 -3
  51. package/.skills/cycle/SKILL.md +4 -7
  52. package/.skills/gap/SKILL.md +4 -3
  53. package/.skills/green/SKILL.md +4 -7
  54. package/.skills/issue/SKILL.md +5 -8
  55. package/.skills/plan/SKILL.md +4 -3
  56. package/.skills/pr/SKILL.md +4 -3
  57. package/.skills/publish/SKILL.md +160 -0
  58. package/.skills/red/SKILL.md +4 -7
  59. package/.skills/refactor/SKILL.md +4 -7
  60. package/.skills/spike/SKILL.md +4 -7
  61. package/.skills/summarize/SKILL.md +4 -3
  62. package/.skills/tdd/SKILL.md +4 -7
  63. package/.skills/update-todos/SKILL.md +22 -0
  64. package/.skills/worktree-add/SKILL.md +210 -216
  65. package/.skills/worktree-cleanup/SKILL.md +183 -187
  66. package/CHANGELOG.md +97 -79
  67. package/README.md +161 -7142
  68. package/bin/cli.js +448 -805
  69. package/commands/README.md +66 -31
  70. package/commands/add-command.md +8 -5
  71. package/commands/beepboop.md +4 -5
  72. package/commands/busycommit.md +2 -3
  73. package/commands/commit.md +2 -3
  74. package/commands/cycle.md +2 -7
  75. package/commands/gap.md +2 -3
  76. package/commands/green.md +2 -7
  77. package/commands/hustle-api-continue.md +8 -5
  78. package/commands/hustle-api-create.md +70 -29
  79. package/commands/hustle-api-env.md +1 -0
  80. package/commands/hustle-api-interview.md +32 -19
  81. package/commands/hustle-api-research.md +47 -21
  82. package/commands/hustle-api-sessions.md +8 -7
  83. package/commands/hustle-api-status.md +21 -1
  84. package/commands/hustle-api-verify.md +14 -13
  85. package/commands/hustle-combine.md +488 -241
  86. package/commands/hustle-ui-create-page.md +113 -50
  87. package/commands/hustle-ui-create.md +179 -26
  88. package/commands/issue.md +3 -8
  89. package/commands/plan.md +2 -3
  90. package/commands/pr.md +2 -3
  91. package/commands/red.md +2 -7
  92. package/commands/refactor.md +2 -7
  93. package/commands/spike.md +2 -7
  94. package/commands/summarize.md +2 -3
  95. package/commands/tdd.md +2 -7
  96. package/commands/worktree-add.md +208 -216
  97. package/commands/worktree-cleanup.md +172 -178
  98. package/hooks/api-workflow-check.py +5 -3
  99. package/hooks/enforce-component-type-confirm.py +97 -0
  100. package/hooks/lib/__init__.py +1 -0
  101. package/hooks/lib/greptile.py +355 -0
  102. package/hooks/lib/ntfy.py +209 -0
  103. package/hooks/notify-input-needed.py +73 -0
  104. package/hooks/notify-phase-complete.py +90 -0
  105. package/hooks/run-code-review.py +246 -0
  106. package/hooks/track-token-usage.py +121 -0
  107. package/package.json +13 -3
  108. package/scripts/collect-test-results.ts +102 -77
  109. package/scripts/extract-parameters.ts +112 -70
  110. package/scripts/generate-test-manifest.ts +118 -77
  111. package/templates/.env.example +57 -0
  112. package/templates/BRAND_GUIDE.md +92 -52
  113. package/templates/CLAUDE-SECTION.md +40 -37
  114. package/templates/SPEC.json +186 -38
  115. package/templates/api-dev-state.json +33 -4
  116. package/templates/api-showcase/_components/APICard.tsx +22 -18
  117. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  118. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  119. package/templates/api-showcase/_components/APITester.tsx +128 -67
  120. package/templates/api-showcase/page.tsx +4 -4
  121. package/templates/api-test/page.tsx +51 -30
  122. package/templates/api-test/test-structure/route.ts +43 -34
  123. package/templates/component/Component.stories.tsx +41 -39
  124. package/templates/component/Component.test.tsx +96 -78
  125. package/templates/component/Component.tsx +63 -52
  126. package/templates/component/Component.types.ts +10 -6
  127. package/templates/component/Component.visual.spec.ts +170 -0
  128. package/templates/component/index.ts +2 -2
  129. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  130. package/templates/dev-tools/page.tsx +4 -3
  131. package/templates/mcp-servers.json +30 -2
  132. package/templates/page/page.e2e.test.ts +56 -48
  133. package/templates/page/page.tsx +3 -3
  134. package/templates/shared/HeroHeader.tsx +16 -15
  135. package/templates/shared/index.ts +1 -1
  136. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  137. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  138. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  139. package/templates/ui-showcase/page.tsx +4 -4
@@ -1,8 +1,8 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useEffect, useState } from 'react';
4
- import Link from 'next/link';
5
- import { HeroHeader } from '../../shared/HeroHeader';
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('/api/registry')
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 ? '...' : total}
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 || '1.0.0'}
210
+ Version: {registry.version || "1.0.0"}
211
211
  </div>
212
212
  </Link>
213
213
  </div>
@@ -1,8 +1,9 @@
1
- import { DevToolsLanding } from './_components/DevToolsLanding';
1
+ import { DevToolsLanding } from "./_components/DevToolsLanding";
2
2
 
3
3
  export const metadata = {
4
- title: 'Hustle Dev Tools',
5
- description: 'Developer tools for API and UI development with Hustle Together',
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 '@playwright/test';
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('__PAGE_NAME__ Page', () => {
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('/__PAGE_ROUTE__');
13
+ await page.goto("/__PAGE_ROUTE__");
14
14
  });
15
15
 
16
16
  // ===================================
17
17
  // Basic Rendering Tests
18
18
  // ===================================
19
19
 
20
- test('should load successfully', async ({ page }) => {
20
+ test("should load successfully", async ({ page }) => {
21
21
  // Wait for page to be fully loaded
22
- await page.waitForLoadState('networkidle');
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('should display page heading', async ({ page }) => {
29
- const heading = page.getByRole('heading', { level: 1 });
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('__PAGE_TITLE__');
31
+ await expect(heading).toContainText("__PAGE_TITLE__");
32
32
  });
33
33
 
34
- test('should display page description', async ({ page }) => {
35
- const description = page.getByText('__PAGE_DESCRIPTION__');
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('should be responsive on mobile', async ({ page }) => {
43
+ test("should be responsive on mobile", async ({ page }) => {
44
44
  await page.setViewportSize({ width: 375, height: 667 });
45
- await page.goto('/__PAGE_ROUTE__');
45
+ await page.goto("/__PAGE_ROUTE__");
46
46
 
47
47
  // Verify main content is visible
48
- await expect(page.getByRole('main')).toBeVisible();
48
+ await expect(page.getByRole("main")).toBeVisible();
49
49
 
50
50
  // Verify no horizontal scroll
51
- const body = await page.locator('body');
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('should be responsive on tablet', async ({ page }) => {
57
+ test("should be responsive on tablet", async ({ page }) => {
58
58
  await page.setViewportSize({ width: 768, height: 1024 });
59
- await page.goto('/__PAGE_ROUTE__');
59
+ await page.goto("/__PAGE_ROUTE__");
60
60
 
61
- await expect(page.getByRole('main')).toBeVisible();
61
+ await expect(page.getByRole("main")).toBeVisible();
62
62
  });
63
63
 
64
- test('should be responsive on desktop', async ({ page }) => {
64
+ test("should be responsive on desktop", async ({ page }) => {
65
65
  await page.setViewportSize({ width: 1920, height: 1080 });
66
- await page.goto('/__PAGE_ROUTE__');
66
+ await page.goto("/__PAGE_ROUTE__");
67
67
 
68
- await expect(page.getByRole('main')).toBeVisible();
68
+ await expect(page.getByRole("main")).toBeVisible();
69
69
  });
70
70
 
71
71
  // ===================================
72
72
  // Accessibility Tests
73
73
  // ===================================
74
74
 
75
- test('should have no accessibility violations', async ({ page }) => {
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('img').all();
82
+ const images = await page.getByRole("img").all();
83
83
  for (const img of images) {
84
- await expect(img).toHaveAttribute('alt');
84
+ await expect(img).toHaveAttribute("alt");
85
85
  }
86
86
 
87
87
  // All buttons should have accessible names
88
- const buttons = await page.getByRole('button').all();
88
+ const buttons = await page.getByRole("button").all();
89
89
  for (const button of buttons) {
90
- const name = await button.getAttribute('aria-label') || await button.textContent();
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('should be keyboard navigable', async ({ page }) => {
97
+ test("should be keyboard navigable", async ({ page }) => {
96
98
  // Tab through interactive elements
97
- await page.keyboard.press('Tab');
99
+ await page.keyboard.press("Tab");
98
100
 
99
101
  // Verify focus is visible
100
- const focusedElement = page.locator(':focus');
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('should load within performance budget', async ({ page }) => {
112
+ test("should load within performance budget", async ({ page }) => {
111
113
  const startTime = Date.now();
112
- await page.goto('/__PAGE_ROUTE__', { waitUntil: 'networkidle' });
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('should have acceptable memory usage', async ({ page }) => {
121
- await page.goto('/__PAGE_ROUTE__');
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('Performance.getMetrics');
127
+ const metrics = await client.send("Performance.getMetrics");
126
128
 
127
- const jsHeapSize = metrics.metrics.find(m => m.name === 'JSHeapUsedSize')?.value || 0;
128
- const domNodes = metrics.metrics.find(m => m.name === 'Nodes')?.value || 0;
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('should not have layout thrashing', async ({ page }) => {
140
- await page.goto('/__PAGE_ROUTE__');
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('Performance.getMetrics');
147
+ const metrics = await client.send("Performance.getMetrics");
144
148
 
145
- const layoutCount = metrics.metrics.find(m => m.name === 'LayoutCount')?.value || 0;
146
- const layoutDuration = metrics.metrics.find(m => m.name === 'LayoutDuration')?.value || 0;
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('should meet Core Web Vitals', async ({ page }) => {
157
- await page.goto('/__PAGE_ROUTE__');
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('paint');
167
+ const entries = performance.getEntriesByType("paint");
162
168
  return {
163
- fcp: entries.find(e => e.name === 'first-contentful-paint')?.startTime || 0,
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: 'largest-contentful-paint', buffered: true });
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('should handle errors gracefully', async ({ page }) => {
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('/__PAGE_ROUTE__');
221
+ await page.goto("/__PAGE_ROUTE__");
214
222
 
215
223
  // Page should still render without crashing
216
- await expect(page.getByRole('main')).toBeVisible();
224
+ await expect(page.getByRole("main")).toBeVisible();
217
225
  });
218
226
  });
@@ -1,11 +1,11 @@
1
- import type { Metadata } from 'next';
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: '__PAGE_TITLE__',
8
- description: '__PAGE_DESCRIPTION__',
7
+ title: "__PAGE_TITLE__",
8
+ description: "__PAGE_DESCRIPTION__",
9
9
  };
10
10
 
11
11
  /**
@@ -1,6 +1,6 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useEffect, useRef, useState } from 'react';
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('dark') ||
33
- document.documentElement.getAttribute('data-theme') === 'dark'
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: ['class', 'data-theme'],
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('2d');
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: 'rgba(186, 12, 47' });
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 ? 'rgba(60, 60, 60' : 'rgba(30, 30, 30';
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 = Math.random() > 0.7 ? secondaryColor : 'rgba(186, 12, 47';
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 ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
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 ? '0,0,0' : '255,255,255';
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('resize', resize);
224
+ window.addEventListener("resize", resize);
224
225
  draw();
225
226
 
226
227
  return () => {
227
- window.removeEventListener('resize', resize);
228
+ window.removeEventListener("resize", resize);
228
229
  cancelAnimationFrame(animationId);
229
230
  };
230
231
  }, [isDark]);
@@ -1 +1 @@
1
- export { HeroHeader } from './HeroHeader';
1
+ export { HeroHeader } from "./HeroHeader";
@@ -1,8 +1,8 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
3
  interface PreviewCardProps {
4
4
  id: string;
5
- type: 'component' | 'page';
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('src/app/')) {
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 === 'page' ? (
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: '200%', height: '200%' }}
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 === 'component' ? 'Component' : 'Page'}
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] || 'primary');
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('button')) {
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('card')) {
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 (lowerName.includes('input') || lowerName.includes('field') || lowerName.includes('form')) {
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('table')) {
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('header') || lowerName.includes('nav')) {
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('modal') || lowerName.includes('dialog')) {
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>