@hustle-together/api-dev-tools 3.11.1 → 3.12.1

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 +228 -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,7 +1,7 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useEffect, useCallback, useState } from 'react';
4
- import { Sandpack, SandpackTheme } from '@codesandbox/sandpack-react';
3
+ import { useEffect, useCallback, useState } from "react";
4
+ import { Sandpack, SandpackTheme } from "@codesandbox/sandpack-react";
5
5
 
6
6
  interface RegistryItem {
7
7
  name: string;
@@ -19,17 +19,17 @@ interface RegistryItem {
19
19
 
20
20
  interface PreviewModalProps {
21
21
  id: string;
22
- type: 'component' | 'page';
22
+ type: "component" | "page";
23
23
  data: RegistryItem;
24
24
  onClose: () => void;
25
25
  }
26
26
 
27
- type ViewportSize = 'desktop' | 'tablet' | 'mobile';
27
+ type ViewportSize = "desktop" | "tablet" | "mobile";
28
28
 
29
29
  const VIEWPORT_WIDTHS: Record<ViewportSize, string> = {
30
- desktop: '100%',
31
- tablet: '768px',
32
- mobile: '375px',
30
+ desktop: "100%",
31
+ tablet: "768px",
32
+ mobile: "375px",
33
33
  };
34
34
 
35
35
  /**
@@ -47,27 +47,27 @@ export function PreviewModal({ id, type, data, onClose }: PreviewModalProps) {
47
47
  // Close on Escape key
48
48
  const handleKeyDown = useCallback(
49
49
  (e: KeyboardEvent) => {
50
- if (e.key === 'Escape') {
50
+ if (e.key === "Escape") {
51
51
  onClose();
52
52
  }
53
53
  },
54
- [onClose]
54
+ [onClose],
55
55
  );
56
56
 
57
57
  useEffect(() => {
58
- document.addEventListener('keydown', handleKeyDown);
59
- document.body.style.overflow = 'hidden';
58
+ document.addEventListener("keydown", handleKeyDown);
59
+ document.body.style.overflow = "hidden";
60
60
 
61
61
  return () => {
62
- document.removeEventListener('keydown', handleKeyDown);
63
- document.body.style.overflow = '';
62
+ document.removeEventListener("keydown", handleKeyDown);
63
+ document.body.style.overflow = "";
64
64
  };
65
65
  }, [handleKeyDown]);
66
66
 
67
67
  // Get page route from file path
68
68
  const getPageRoute = () => {
69
69
  if (data.route) return data.route;
70
- if (data.file?.includes('src/app/')) {
70
+ if (data.file?.includes("src/app/")) {
71
71
  const match = data.file.match(/src\/app\/(.+?)\/page\.tsx?$/);
72
72
  if (match) return `/${match[1]}`;
73
73
  }
@@ -94,11 +94,14 @@ export function PreviewModal({ id, type, data, onClose }: PreviewModalProps) {
94
94
  <div className="h-1 w-full bg-[#BA0C2F]" />
95
95
  <header className="flex items-center justify-between border-b-2 border-black px-6 py-4 dark:border-gray-700">
96
96
  <div>
97
- <h2 id="modal-title" className="text-lg font-bold text-black dark:text-white">
97
+ <h2
98
+ id="modal-title"
99
+ className="text-lg font-bold text-black dark:text-white"
100
+ >
98
101
  {data.name || id}
99
102
  </h2>
100
103
  <p className="text-sm text-gray-600 dark:text-gray-400">
101
- {type === 'component' ? 'Component Preview' : 'Page Preview'}
104
+ {type === "component" ? "Component Preview" : "Page Preview"}
102
105
  </p>
103
106
  </div>
104
107
  <button
@@ -125,7 +128,7 @@ export function PreviewModal({ id, type, data, onClose }: PreviewModalProps) {
125
128
 
126
129
  {/* Preview Area */}
127
130
  <div className="flex-1 overflow-auto">
128
- {type === 'component' ? (
131
+ {type === "component" ? (
129
132
  <ComponentPreview id={id} data={data} />
130
133
  ) : (
131
134
  <PagePreview route={getPageRoute()} />
@@ -147,7 +150,7 @@ export function PreviewModal({ id, type, data, onClose }: PreviewModalProps) {
147
150
 
148
151
  {/* Actions */}
149
152
  <div className="flex gap-2">
150
- {type === 'page' && (
153
+ {type === "page" && (
151
154
  <a
152
155
  href={getPageRoute()}
153
156
  target="_blank"
@@ -159,10 +162,12 @@ export function PreviewModal({ id, type, data, onClose }: PreviewModalProps) {
159
162
  )}
160
163
  <button
161
164
  onClick={() => {
162
- const importPath = data.file?.replace(/^src\//, '@/').replace(/\.tsx?$/, '');
165
+ const importPath = data.file
166
+ ?.replace(/^src\//, "@/")
167
+ .replace(/\.tsx?$/, "");
163
168
  if (importPath) {
164
169
  navigator.clipboard.writeText(
165
- `import { ${data.name || id} } from '${importPath}';`
170
+ `import { ${data.name || id} } from '${importPath}';`,
166
171
  );
167
172
  }
168
173
  }}
@@ -181,43 +186,47 @@ export function PreviewModal({ id, type, data, onClose }: PreviewModalProps) {
181
186
  // Hustle Together theme for Sandpack
182
187
  const hustleTheme: SandpackTheme = {
183
188
  colors: {
184
- surface1: '#ffffff',
185
- surface2: '#f8f8f8',
186
- surface3: '#f0f0f0',
187
- clickable: '#666666',
188
- base: '#000000',
189
- disabled: '#cccccc',
190
- hover: '#BA0C2F',
191
- accent: '#BA0C2F',
192
- error: '#ef4444',
193
- errorSurface: '#fef2f2',
189
+ surface1: "#ffffff",
190
+ surface2: "#f8f8f8",
191
+ surface3: "#f0f0f0",
192
+ clickable: "#666666",
193
+ base: "#000000",
194
+ disabled: "#cccccc",
195
+ hover: "#BA0C2F",
196
+ accent: "#BA0C2F",
197
+ error: "#ef4444",
198
+ errorSurface: "#fef2f2",
194
199
  },
195
200
  syntax: {
196
- plain: '#000000',
197
- comment: { color: '#666666', fontStyle: 'italic' },
198
- keyword: '#BA0C2F',
199
- tag: '#BA0C2F',
200
- punctuation: '#000000',
201
- definition: '#000000',
202
- property: '#BA0C2F',
203
- static: '#BA0C2F',
204
- string: '#22c55e',
201
+ plain: "#000000",
202
+ comment: { color: "#666666", fontStyle: "italic" },
203
+ keyword: "#BA0C2F",
204
+ tag: "#BA0C2F",
205
+ punctuation: "#000000",
206
+ definition: "#000000",
207
+ property: "#BA0C2F",
208
+ static: "#BA0C2F",
209
+ string: "#22c55e",
205
210
  },
206
211
  font: {
207
212
  body: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif',
208
213
  mono: '"SF Mono", Monaco, Inconsolata, "Fira Code", monospace',
209
- size: '13px',
210
- lineHeight: '1.5',
214
+ size: "13px",
215
+ lineHeight: "1.5",
211
216
  },
212
217
  };
213
218
 
214
219
  // Generate example code for different component types
215
- function generateComponentCode(name: string, variants: string[], selectedVariant: string | null): string {
216
- const variant = selectedVariant || variants[0] || 'primary';
220
+ function generateComponentCode(
221
+ name: string,
222
+ variants: string[],
223
+ selectedVariant: string | null,
224
+ ): string {
225
+ const variant = selectedVariant || variants[0] || "primary";
217
226
 
218
227
  // Generate code based on component type
219
228
  switch (name.toLowerCase()) {
220
- case 'button':
229
+ case "button":
221
230
  return `export default function App() {
222
231
  return (
223
232
  <div style={{ padding: '2rem', display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
@@ -228,9 +237,9 @@ function generateComponentCode(name: string, variants: string[], selectedVariant
228
237
  padding: '0.75rem 1.5rem',
229
238
  fontSize: '14px',
230
239
  fontWeight: 'bold',
231
- border: '2px solid ${variant === 'ghost' ? '#000' : '#BA0C2F'}',
232
- background: '${variant === 'ghost' ? 'transparent' : variant === 'secondary' ? '#fff' : '#BA0C2F'}',
233
- color: '${variant === 'ghost' || variant === 'secondary' ? '#000' : '#fff'}',
240
+ border: '2px solid ${variant === "ghost" ? "#000" : "#BA0C2F"}',
241
+ background: '${variant === "ghost" ? "transparent" : variant === "secondary" ? "#fff" : "#BA0C2F"}',
242
+ color: '${variant === "ghost" || variant === "secondary" ? "#000" : "#fff"}',
234
243
  cursor: 'pointer',
235
244
  }}>
236
245
  Click Me
@@ -238,29 +247,33 @@ function generateComponentCode(name: string, variants: string[], selectedVariant
238
247
 
239
248
  {/* All variants */}
240
249
  <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
241
- ${variants.map(v => `<button style={{
250
+ ${variants
251
+ .map(
252
+ (v) => `<button style={{
242
253
  padding: '0.5rem 1rem',
243
254
  fontSize: '12px',
244
255
  fontWeight: 'bold',
245
- border: '2px solid ${v === 'ghost' ? '#000' : '#BA0C2F'}',
246
- background: '${v === 'ghost' ? 'transparent' : v === 'secondary' ? '#fff' : '#BA0C2F'}',
247
- color: '${v === 'ghost' || v === 'secondary' ? '#000' : '#fff'}',
256
+ border: '2px solid ${v === "ghost" ? "#000" : "#BA0C2F"}',
257
+ background: '${v === "ghost" ? "transparent" : v === "secondary" ? "#fff" : "#BA0C2F"}',
258
+ color: '${v === "ghost" || v === "secondary" ? "#000" : "#fff"}',
248
259
  cursor: 'pointer',
249
- }}>${v}</button>`).join('\n ')}
260
+ }}>${v}</button>`,
261
+ )
262
+ .join("\n ")}
250
263
  </div>
251
264
  </div>
252
265
  );
253
266
  }`;
254
267
 
255
- case 'card':
268
+ case "card":
256
269
  return `export default function App() {
257
270
  return (
258
271
  <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
259
272
  <h2 style={{ margin: '0 0 1rem' }}>Card - ${variant}</h2>
260
273
 
261
274
  <div style={{
262
- border: '${variant === 'bordered' ? '2px solid #000' : '1px solid #ccc'}',
263
- boxShadow: '${variant === 'elevated' ? '4px 4px 0 rgba(0,0,0,0.1)' : 'none'}',
275
+ border: '${variant === "bordered" ? "2px solid #000" : "1px solid #ccc"}',
276
+ boxShadow: '${variant === "elevated" ? "4px 4px 0 rgba(0,0,0,0.1)" : "none"}',
264
277
  background: '#fff',
265
278
  maxWidth: '320px',
266
279
  }}>
@@ -292,7 +305,7 @@ function generateComponentCode(name: string, variants: string[], selectedVariant
292
305
  );
293
306
  }`;
294
307
 
295
- case 'formfield':
308
+ case "formfield":
296
309
  return `import { useState } from 'react';
297
310
 
298
311
  export default function App() {
@@ -310,10 +323,12 @@ export default function App() {
310
323
 
311
324
  <div style={{ marginBottom: '1rem' }}>
312
325
  <label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold', fontSize: '14px' }}>
313
- ${variant === 'email' ? 'Email Address' : variant === 'password' ? 'Password' : variant === 'textarea' ? 'Message' : 'Username'}
326
+ ${variant === "email" ? "Email Address" : variant === "password" ? "Password" : variant === "textarea" ? "Message" : "Username"}
314
327
  </label>
315
328
 
316
- ${variant === 'textarea' ? `<textarea
329
+ ${
330
+ variant === "textarea"
331
+ ? `<textarea
317
332
  value={value}
318
333
  onChange={handleChange}
319
334
  placeholder="Enter your message..."
@@ -326,11 +341,12 @@ export default function App() {
326
341
  fontFamily: 'inherit',
327
342
  boxSizing: 'border-box',
328
343
  }}
329
- />` : `<input
344
+ />`
345
+ : `<input
330
346
  type="${variant}"
331
347
  value={value}
332
348
  onChange={handleChange}
333
- placeholder="${variant === 'email' ? 'you@example.com' : variant === 'password' ? '••••••••' : 'Enter text...'}"
349
+ placeholder="${variant === "email" ? "you@example.com" : variant === "password" ? "••••••••" : "Enter text..."}"
334
350
  style={{
335
351
  width: '100%',
336
352
  padding: '0.75rem',
@@ -338,7 +354,8 @@ export default function App() {
338
354
  fontSize: '14px',
339
355
  boxSizing: 'border-box',
340
356
  }}
341
- />`}
357
+ />`
358
+ }
342
359
 
343
360
  {error && (
344
361
  <p style={{ color: '#ef4444', fontSize: '12px', marginTop: '0.5rem' }}>
@@ -381,7 +398,7 @@ export default function App() {
381
398
  </div>
382
399
 
383
400
  <p style={{ margin: 0, fontWeight: 'bold' }}>${name} Component</p>
384
- ${selectedVariant ? `<p style={{ margin: '0.5rem 0 0', color: '#BA0C2F', fontSize: '14px' }}>Variant: ${selectedVariant}</p>` : ''}
401
+ ${selectedVariant ? `<p style={{ margin: '0.5rem 0 0', color: '#BA0C2F', fontSize: '14px' }}>Variant: ${selectedVariant}</p>` : ""}
385
402
 
386
403
  <p style={{ margin: '1rem 0 0', color: '#666', fontSize: '14px' }}>
387
404
  Edit the code on the left to customize this component
@@ -399,21 +416,15 @@ export default function App() {
399
416
  * Uses CodeSandbox's Sandpack to render live, editable component previews.
400
417
  * No server/client boundary issues - runs entirely in the browser.
401
418
  */
402
- function ComponentPreview({
403
- id,
404
- data,
405
- }: {
406
- id: string;
407
- data: RegistryItem;
408
- }) {
419
+ function ComponentPreview({ id, data }: { id: string; data: RegistryItem }) {
409
420
  const [selectedVariant, setSelectedVariant] = useState<string | null>(
410
- data.variants?.[0] || null
421
+ data.variants?.[0] || null,
411
422
  );
412
423
 
413
424
  const componentCode = generateComponentCode(
414
425
  data.name || id,
415
426
  data.variants || [],
416
- selectedVariant
427
+ selectedVariant,
417
428
  );
418
429
 
419
430
  return (
@@ -421,7 +432,9 @@ function ComponentPreview({
421
432
  {/* Variant Controls */}
422
433
  {data.variants && data.variants.length > 0 && (
423
434
  <div className="mb-4">
424
- <h3 className="mb-3 text-sm font-bold text-black dark:text-white">Variants</h3>
435
+ <h3 className="mb-3 text-sm font-bold text-black dark:text-white">
436
+ Variants
437
+ </h3>
425
438
  <div className="flex flex-wrap gap-2">
426
439
  {data.variants.map((variant) => (
427
440
  <button
@@ -429,8 +442,8 @@ function ComponentPreview({
429
442
  onClick={() => setSelectedVariant(variant)}
430
443
  className={`border-2 px-3 py-1.5 text-sm font-medium transition-colors ${
431
444
  selectedVariant === variant
432
- ? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
433
- : 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white'
445
+ ? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
446
+ : "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white"
434
447
  }`}
435
448
  >
436
449
  {variant}
@@ -446,7 +459,7 @@ function ComponentPreview({
446
459
  template="react"
447
460
  theme={hustleTheme}
448
461
  files={{
449
- '/App.js': componentCode,
462
+ "/App.js": componentCode,
450
463
  }}
451
464
  options={{
452
465
  showNavigator: false,
@@ -462,21 +475,31 @@ function ComponentPreview({
462
475
  <div className="mt-4 grid gap-4 md:grid-cols-2">
463
476
  {data.props_interface && (
464
477
  <div className="border-2 border-black bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
465
- <h3 className="mb-2 text-sm font-bold text-black dark:text-white">Props Interface</h3>
466
- <code className="font-mono text-sm text-gray-700 dark:text-gray-300">{data.props_interface}</code>
478
+ <h3 className="mb-2 text-sm font-bold text-black dark:text-white">
479
+ Props Interface
480
+ </h3>
481
+ <code className="font-mono text-sm text-gray-700 dark:text-gray-300">
482
+ {data.props_interface}
483
+ </code>
467
484
  </div>
468
485
  )}
469
486
 
470
487
  {data.file && (
471
488
  <div className="border-2 border-black bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
472
- <h3 className="mb-2 text-sm font-bold text-black dark:text-white">File Location</h3>
473
- <code className="font-mono text-sm text-gray-700 dark:text-gray-300">{data.file}</code>
489
+ <h3 className="mb-2 text-sm font-bold text-black dark:text-white">
490
+ File Location
491
+ </h3>
492
+ <code className="font-mono text-sm text-gray-700 dark:text-gray-300">
493
+ {data.file}
494
+ </code>
474
495
  </div>
475
496
  )}
476
497
 
477
498
  {data.uses_components && data.uses_components.length > 0 && (
478
499
  <div className="border-2 border-black bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
479
- <h3 className="mb-2 text-sm font-bold text-black dark:text-white">Uses Components</h3>
500
+ <h3 className="mb-2 text-sm font-bold text-black dark:text-white">
501
+ Uses Components
502
+ </h3>
480
503
  <div className="flex flex-wrap gap-1">
481
504
  {data.uses_components.map((comp) => (
482
505
  <span
@@ -491,7 +514,9 @@ function ComponentPreview({
491
514
  )}
492
515
 
493
516
  <div className="border-2 border-black bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
494
- <h3 className="mb-2 text-sm font-bold text-black dark:text-white">Powered by</h3>
517
+ <h3 className="mb-2 text-sm font-bold text-black dark:text-white">
518
+ Powered by
519
+ </h3>
495
520
  <p className="text-sm text-gray-600 dark:text-gray-400">
496
521
  <a
497
522
  href="https://sandpack.codesandbox.io/"
@@ -500,7 +525,8 @@ function ComponentPreview({
500
525
  className="text-[#BA0C2F] hover:underline"
501
526
  >
502
527
  Sandpack
503
- </a> by CodeSandbox - Edit the code live!
528
+ </a>{" "}
529
+ by CodeSandbox - Edit the code live!
504
530
  </p>
505
531
  </div>
506
532
  </div>
@@ -515,17 +541,19 @@ function ComponentPreview({
515
541
  * Checks if the route exists before rendering to avoid 404s.
516
542
  */
517
543
  function PagePreview({ route }: { route: string }) {
518
- const [viewport, setViewport] = useState<ViewportSize>('desktop');
519
- const [routeStatus, setRouteStatus] = useState<'checking' | 'exists' | 'not-found'>('checking');
544
+ const [viewport, setViewport] = useState<ViewportSize>("desktop");
545
+ const [routeStatus, setRouteStatus] = useState<
546
+ "checking" | "exists" | "not-found"
547
+ >("checking");
520
548
 
521
549
  // Check if the route exists
522
550
  useEffect(() => {
523
551
  const checkRoute = async () => {
524
552
  try {
525
- const res = await fetch(route, { method: 'HEAD' });
526
- setRouteStatus(res.ok ? 'exists' : 'not-found');
553
+ const res = await fetch(route, { method: "HEAD" });
554
+ setRouteStatus(res.ok ? "exists" : "not-found");
527
555
  } catch {
528
- setRouteStatus('not-found');
556
+ setRouteStatus("not-found");
529
557
  }
530
558
  };
531
559
  checkRoute();
@@ -536,11 +564,11 @@ function PagePreview({ route }: { route: string }) {
536
564
  {/* Responsive Size Controls */}
537
565
  <div className="mb-4 flex justify-center gap-2">
538
566
  <button
539
- onClick={() => setViewport('desktop')}
567
+ onClick={() => setViewport("desktop")}
540
568
  className={`flex items-center gap-1.5 border-2 px-3 py-1.5 text-sm font-medium transition-colors ${
541
- viewport === 'desktop'
542
- ? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
543
- : 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white'
569
+ viewport === "desktop"
570
+ ? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
571
+ : "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white"
544
572
  }`}
545
573
  >
546
574
  <svg
@@ -561,11 +589,11 @@ function PagePreview({ route }: { route: string }) {
561
589
  Desktop
562
590
  </button>
563
591
  <button
564
- onClick={() => setViewport('tablet')}
592
+ onClick={() => setViewport("tablet")}
565
593
  className={`flex items-center gap-1.5 border-2 px-3 py-1.5 text-sm font-medium transition-colors ${
566
- viewport === 'tablet'
567
- ? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
568
- : 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white'
594
+ viewport === "tablet"
595
+ ? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
596
+ : "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white"
569
597
  }`}
570
598
  >
571
599
  <svg
@@ -585,11 +613,11 @@ function PagePreview({ route }: { route: string }) {
585
613
  Tablet
586
614
  </button>
587
615
  <button
588
- onClick={() => setViewport('mobile')}
616
+ onClick={() => setViewport("mobile")}
589
617
  className={`flex items-center gap-1.5 border-2 px-3 py-1.5 text-sm font-medium transition-colors ${
590
- viewport === 'mobile'
591
- ? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
592
- : 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white'
618
+ viewport === "mobile"
619
+ ? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
620
+ : "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-700 dark:text-white"
593
621
  }`}
594
622
  >
595
623
  <svg
@@ -615,14 +643,19 @@ function PagePreview({ route }: { route: string }) {
615
643
  className="mx-auto overflow-hidden border-2 border-black bg-white transition-all duration-300 dark:border-gray-700"
616
644
  style={{ width: VIEWPORT_WIDTHS[viewport] }}
617
645
  >
618
- {routeStatus === 'checking' ? (
646
+ {routeStatus === "checking" ? (
619
647
  <div className="flex h-[500px] items-center justify-center bg-gray-50 dark:bg-gray-800">
620
648
  <div className="text-center">
621
- <div className="mx-auto mb-4 h-8 w-8 animate-spin border-4 border-gray-300 border-t-[#BA0C2F]" style={{ borderRadius: '50%' }} />
622
- <p className="text-sm text-gray-600 dark:text-gray-400">Checking route...</p>
649
+ <div
650
+ className="mx-auto mb-4 h-8 w-8 animate-spin border-4 border-gray-300 border-t-[#BA0C2F]"
651
+ style={{ borderRadius: "50%" }}
652
+ />
653
+ <p className="text-sm text-gray-600 dark:text-gray-400">
654
+ Checking route...
655
+ </p>
623
656
  </div>
624
657
  </div>
625
- ) : routeStatus === 'not-found' ? (
658
+ ) : routeStatus === "not-found" ? (
626
659
  <div className="flex h-[500px] items-center justify-center bg-gray-50 dark:bg-gray-800">
627
660
  <div className="max-w-sm p-8 text-center">
628
661
  <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center border-2 border-black bg-gray-100 dark:border-gray-600 dark:bg-gray-700">
@@ -643,12 +676,20 @@ function PagePreview({ route }: { route: string }) {
643
676
  <line x1="9" x2="15" y1="15" y2="15" />
644
677
  </svg>
645
678
  </div>
646
- <h3 className="mb-2 font-bold text-black dark:text-white">Page Not Found</h3>
679
+ <h3 className="mb-2 font-bold text-black dark:text-white">
680
+ Page Not Found
681
+ </h3>
647
682
  <p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
648
- The route <code className="border border-gray-300 bg-gray-100 px-1 dark:border-gray-600 dark:bg-gray-700">{route}</code> doesn&apos;t exist yet.
683
+ The route{" "}
684
+ <code className="border border-gray-300 bg-gray-100 px-1 dark:border-gray-600 dark:bg-gray-700">
685
+ {route}
686
+ </code>{" "}
687
+ doesn&apos;t exist yet.
649
688
  </p>
650
689
  <p className="text-xs text-gray-500 dark:text-gray-400">
651
- Create the page at <code className="text-[#BA0C2F]">src/app{route}/page.tsx</code> to see the preview.
690
+ Create the page at{" "}
691
+ <code className="text-[#BA0C2F]">src/app{route}/page.tsx</code>{" "}
692
+ to see the preview.
652
693
  </p>
653
694
  </div>
654
695
  </div>
@@ -665,7 +706,7 @@ function PagePreview({ route }: { route: string }) {
665
706
  {/* Viewport Info */}
666
707
  <p className="mt-4 text-center text-sm text-gray-600 dark:text-gray-400">
667
708
  Viewport: {VIEWPORT_WIDTHS[viewport]} • Route: {route}
668
- {routeStatus === 'not-found' && (
709
+ {routeStatus === "not-found" && (
669
710
  <span className="ml-2 border border-yellow-400 bg-yellow-50 px-2 py-0.5 text-xs text-yellow-700 dark:border-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400">
670
711
  Route does not exist
671
712
  </span>