@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.
- 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 +228 -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,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useCallback, useState } from
|
|
4
|
-
import { Sandpack, SandpackTheme } from
|
|
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:
|
|
22
|
+
type: "component" | "page";
|
|
23
23
|
data: RegistryItem;
|
|
24
24
|
onClose: () => void;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
type ViewportSize =
|
|
27
|
+
type ViewportSize = "desktop" | "tablet" | "mobile";
|
|
28
28
|
|
|
29
29
|
const VIEWPORT_WIDTHS: Record<ViewportSize, string> = {
|
|
30
|
-
desktop:
|
|
31
|
-
tablet:
|
|
32
|
-
mobile:
|
|
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 ===
|
|
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(
|
|
59
|
-
document.body.style.overflow =
|
|
58
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
59
|
+
document.body.style.overflow = "hidden";
|
|
60
60
|
|
|
61
61
|
return () => {
|
|
62
|
-
document.removeEventListener(
|
|
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(
|
|
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
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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:
|
|
185
|
-
surface2:
|
|
186
|
-
surface3:
|
|
187
|
-
clickable:
|
|
188
|
-
base:
|
|
189
|
-
disabled:
|
|
190
|
-
hover:
|
|
191
|
-
accent:
|
|
192
|
-
error:
|
|
193
|
-
errorSurface:
|
|
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:
|
|
197
|
-
comment: { color:
|
|
198
|
-
keyword:
|
|
199
|
-
tag:
|
|
200
|
-
punctuation:
|
|
201
|
-
definition:
|
|
202
|
-
property:
|
|
203
|
-
static:
|
|
204
|
-
string:
|
|
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:
|
|
210
|
-
lineHeight:
|
|
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(
|
|
216
|
-
|
|
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
|
|
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 ===
|
|
232
|
-
background: '${variant ===
|
|
233
|
-
color: '${variant ===
|
|
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
|
|
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 ===
|
|
246
|
-
background: '${v ===
|
|
247
|
-
color: '${v ===
|
|
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
|
|
260
|
+
}}>${v}</button>`,
|
|
261
|
+
)
|
|
262
|
+
.join("\n ")}
|
|
250
263
|
</div>
|
|
251
264
|
</div>
|
|
252
265
|
);
|
|
253
266
|
}`;
|
|
254
267
|
|
|
255
|
-
case
|
|
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 ===
|
|
263
|
-
boxShadow: '${variant ===
|
|
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
|
|
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 ===
|
|
326
|
+
${variant === "email" ? "Email Address" : variant === "password" ? "Password" : variant === "textarea" ? "Message" : "Username"}
|
|
314
327
|
</label>
|
|
315
328
|
|
|
316
|
-
${
|
|
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
|
-
/>`
|
|
344
|
+
/>`
|
|
345
|
+
: `<input
|
|
330
346
|
type="${variant}"
|
|
331
347
|
value={value}
|
|
332
348
|
onChange={handleChange}
|
|
333
|
-
placeholder="${variant ===
|
|
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">
|
|
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
|
-
?
|
|
433
|
-
:
|
|
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
|
-
|
|
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">
|
|
466
|
-
|
|
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">
|
|
473
|
-
|
|
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">
|
|
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">
|
|
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>
|
|
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>(
|
|
519
|
-
const [routeStatus, setRouteStatus] = useState<
|
|
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:
|
|
526
|
-
setRouteStatus(res.ok ?
|
|
553
|
+
const res = await fetch(route, { method: "HEAD" });
|
|
554
|
+
setRouteStatus(res.ok ? "exists" : "not-found");
|
|
527
555
|
} catch {
|
|
528
|
-
setRouteStatus(
|
|
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(
|
|
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 ===
|
|
542
|
-
?
|
|
543
|
-
:
|
|
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(
|
|
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 ===
|
|
567
|
-
?
|
|
568
|
-
:
|
|
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(
|
|
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 ===
|
|
591
|
-
?
|
|
592
|
-
:
|
|
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 ===
|
|
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
|
|
622
|
-
|
|
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 ===
|
|
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">
|
|
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
|
|
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't exist yet.
|
|
649
688
|
</p>
|
|
650
689
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
651
|
-
Create the page at
|
|
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 ===
|
|
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>
|