@hustle-together/api-dev-tools 3.6.5 → 3.9.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/README.md +5307 -258
- package/bin/cli.js +348 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +22 -2
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +385 -19
- package/hooks/cache-research.py +337 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-brand-guide.py +131 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +981 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +65 -10
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +148 -0
- package/package.json +8 -2
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +179 -161
- package/templates/api-showcase/APICard.tsx +153 -0
- package/templates/api-showcase/APIModal.tsx +375 -0
- package/templates/api-showcase/APIShowcase.tsx +231 -0
- package/templates/api-showcase/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +74 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
package/templates/settings.json
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
{
|
|
23
23
|
"type": "command",
|
|
24
24
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-startup.py"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/detect-interruption.py"
|
|
25
29
|
}
|
|
26
30
|
]
|
|
27
31
|
}
|
|
@@ -44,6 +48,10 @@
|
|
|
44
48
|
"type": "command",
|
|
45
49
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-disambiguation.py"
|
|
46
50
|
},
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-ui-disambiguation.py"
|
|
54
|
+
},
|
|
47
55
|
{
|
|
48
56
|
"type": "command",
|
|
49
57
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-scope.py"
|
|
@@ -56,6 +64,10 @@
|
|
|
56
64
|
"type": "command",
|
|
57
65
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-interview.py"
|
|
58
66
|
},
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-ui-interview.py"
|
|
70
|
+
},
|
|
59
71
|
{
|
|
60
72
|
"type": "command",
|
|
61
73
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-deep-research.py"
|
|
@@ -87,6 +99,35 @@
|
|
|
87
99
|
{
|
|
88
100
|
"type": "command",
|
|
89
101
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-documentation.py"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "command",
|
|
105
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-schema-from-interview.py"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"type": "command",
|
|
109
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-freshness.py"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"type": "command",
|
|
113
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-brand-guide.py"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"type": "command",
|
|
117
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-storybook-setup.py"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"type": "command",
|
|
121
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-playwright-setup.py"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"matcher": "AskUserQuestion",
|
|
127
|
+
"hooks": [
|
|
128
|
+
{
|
|
129
|
+
"type": "command",
|
|
130
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-questions-sourced.py"
|
|
90
131
|
}
|
|
91
132
|
]
|
|
92
133
|
}
|
|
@@ -102,6 +143,10 @@
|
|
|
102
143
|
{
|
|
103
144
|
"type": "command",
|
|
104
145
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/periodic-reground.py"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"type": "command",
|
|
149
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/track-scope-coverage.py"
|
|
105
150
|
}
|
|
106
151
|
]
|
|
107
152
|
},
|
|
@@ -113,6 +158,31 @@
|
|
|
113
158
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify-after-green.py"
|
|
114
159
|
}
|
|
115
160
|
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"matcher": "Write|Edit",
|
|
164
|
+
"hooks": [
|
|
165
|
+
{
|
|
166
|
+
"type": "command",
|
|
167
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/cache-research.py"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"type": "command",
|
|
171
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/generate-manifest-entry.py"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"type": "command",
|
|
175
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/update-registry.py"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"type": "command",
|
|
179
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/update-api-showcase.py"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "command",
|
|
183
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/update-ui-showcase.py"
|
|
184
|
+
}
|
|
185
|
+
]
|
|
116
186
|
}
|
|
117
187
|
],
|
|
118
188
|
"Stop": [
|
|
@@ -121,6 +191,10 @@
|
|
|
121
191
|
{
|
|
122
192
|
"type": "command",
|
|
123
193
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/api-workflow-check.py"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"type": "command",
|
|
197
|
+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-logger.py"
|
|
124
198
|
}
|
|
125
199
|
]
|
|
126
200
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface HeroHeaderProps {
|
|
6
|
+
title: string;
|
|
7
|
+
description: React.ReactNode;
|
|
8
|
+
badge?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HeroHeader Component
|
|
13
|
+
*
|
|
14
|
+
* Animated 3D perspective grid hero section with Hustle Together branding.
|
|
15
|
+
* Features:
|
|
16
|
+
* - Canvas-based 3D grid animation
|
|
17
|
+
* - Hustle red (#BA0C2F) accent highlights
|
|
18
|
+
* - Dark/light mode support
|
|
19
|
+
* - Left-aligned responsive layout
|
|
20
|
+
*
|
|
21
|
+
* Created with Hustle Dev Tools (v3.9.2)
|
|
22
|
+
*/
|
|
23
|
+
export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
|
|
24
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
25
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const [isDark, setIsDark] = useState(false);
|
|
27
|
+
|
|
28
|
+
// Detect dark mode
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const checkDarkMode = () => {
|
|
31
|
+
setIsDark(
|
|
32
|
+
document.documentElement.classList.contains('dark') ||
|
|
33
|
+
document.documentElement.getAttribute('data-theme') === 'dark'
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
checkDarkMode();
|
|
38
|
+
|
|
39
|
+
// Watch for theme changes
|
|
40
|
+
const observer = new MutationObserver(checkDarkMode);
|
|
41
|
+
observer.observe(document.documentElement, {
|
|
42
|
+
attributes: true,
|
|
43
|
+
attributeFilter: ['class', 'data-theme'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
// Grid animation
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const canvas = canvasRef.current;
|
|
52
|
+
const header = headerRef.current;
|
|
53
|
+
if (!canvas || !header) return;
|
|
54
|
+
|
|
55
|
+
const ctx = canvas.getContext('2d');
|
|
56
|
+
if (!ctx) return;
|
|
57
|
+
|
|
58
|
+
let animationId: number;
|
|
59
|
+
let offset = 0;
|
|
60
|
+
|
|
61
|
+
// Grid configuration
|
|
62
|
+
const speed = 0.2;
|
|
63
|
+
const tileSize = 60;
|
|
64
|
+
const gridWidth = 60;
|
|
65
|
+
const gridDepth = 30;
|
|
66
|
+
const horizonY = -150;
|
|
67
|
+
const fov = 350;
|
|
68
|
+
const camHeight = 200;
|
|
69
|
+
const zNear = 20;
|
|
70
|
+
|
|
71
|
+
interface GridCell {
|
|
72
|
+
active: boolean;
|
|
73
|
+
alpha: number;
|
|
74
|
+
color: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let gridRows: GridCell[][] = [];
|
|
78
|
+
|
|
79
|
+
const createRow = (): GridCell[] => {
|
|
80
|
+
const cells: GridCell[] = [];
|
|
81
|
+
for (let c = 0; c < gridWidth; c++) {
|
|
82
|
+
cells.push({ active: false, alpha: 0, color: 'rgba(186, 12, 47' });
|
|
83
|
+
}
|
|
84
|
+
return cells;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Initialize rows
|
|
88
|
+
for (let r = 0; r < gridDepth + 5; r++) {
|
|
89
|
+
gridRows.push(createRow());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const resize = () => {
|
|
93
|
+
canvas.width = header.clientWidth;
|
|
94
|
+
canvas.height = header.clientHeight;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const project = (
|
|
98
|
+
x: number,
|
|
99
|
+
z: number
|
|
100
|
+
): { x: number; y: number; scale: number } | null => {
|
|
101
|
+
if (z <= 0) return null;
|
|
102
|
+
const scale = fov / z;
|
|
103
|
+
const px = x * scale + canvas.width / 2;
|
|
104
|
+
const py = camHeight * scale + canvas.height / 2 + horizonY;
|
|
105
|
+
return { x: px, y: py, scale };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const updateGridState = () => {
|
|
109
|
+
const secondaryColor = isDark ? 'rgba(60, 60, 60' : 'rgba(30, 30, 30';
|
|
110
|
+
|
|
111
|
+
if (Math.random() > 0.92) {
|
|
112
|
+
const r = Math.floor(Math.random() * (gridRows.length - 5)) + 2;
|
|
113
|
+
const c = Math.floor(Math.random() * gridWidth);
|
|
114
|
+
const cell = gridRows[r][c];
|
|
115
|
+
if (!cell.active) {
|
|
116
|
+
cell.active = true;
|
|
117
|
+
cell.alpha = 1.0;
|
|
118
|
+
cell.color = Math.random() > 0.7 ? secondaryColor : 'rgba(186, 12, 47';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
gridRows.forEach((row) =>
|
|
123
|
+
row.forEach((cell) => {
|
|
124
|
+
if (cell.active) {
|
|
125
|
+
cell.alpha -= 0.005;
|
|
126
|
+
if (cell.alpha <= 0) {
|
|
127
|
+
cell.active = false;
|
|
128
|
+
cell.alpha = 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const draw = () => {
|
|
136
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
137
|
+
offset += speed;
|
|
138
|
+
|
|
139
|
+
if (offset >= tileSize) {
|
|
140
|
+
offset -= tileSize;
|
|
141
|
+
gridRows.shift();
|
|
142
|
+
gridRows.push(createRow());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
updateGridState();
|
|
146
|
+
ctx.lineWidth = 1;
|
|
147
|
+
|
|
148
|
+
const lineColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
|
|
149
|
+
|
|
150
|
+
// Draw rows (back to front)
|
|
151
|
+
for (let r = gridRows.length - 1; r >= 0; r--) {
|
|
152
|
+
const zNearLine = r * tileSize - offset + zNear;
|
|
153
|
+
const zFarLine = (r + 1) * tileSize - offset + zNear;
|
|
154
|
+
if (zNearLine <= 0) continue;
|
|
155
|
+
|
|
156
|
+
for (let c = 0; c < gridWidth; c++) {
|
|
157
|
+
const cell = gridRows[r][c];
|
|
158
|
+
if (cell.active && cell.alpha > 0.05) {
|
|
159
|
+
const cOffset = c - gridWidth / 2;
|
|
160
|
+
const xL = cOffset * tileSize;
|
|
161
|
+
const xR = (cOffset + 1) * tileSize;
|
|
162
|
+
|
|
163
|
+
const p1 = project(xL, zFarLine);
|
|
164
|
+
const p2 = project(xR, zFarLine);
|
|
165
|
+
const p3 = project(xR, zNearLine);
|
|
166
|
+
const p4 = project(xL, zNearLine);
|
|
167
|
+
|
|
168
|
+
if (p1 && p2 && p3 && p4) {
|
|
169
|
+
ctx.fillStyle = `${cell.color}, ${cell.alpha})`;
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
ctx.moveTo(p1.x, p1.y);
|
|
172
|
+
ctx.lineTo(p2.x, p2.y);
|
|
173
|
+
ctx.lineTo(p3.x, p3.y);
|
|
174
|
+
ctx.lineTo(p4.x, p4.y);
|
|
175
|
+
ctx.fill();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Horizontal grid lines
|
|
181
|
+
const xL = (-gridWidth / 2) * tileSize;
|
|
182
|
+
const xR = (gridWidth / 2) * tileSize;
|
|
183
|
+
const pL = project(xL, zFarLine);
|
|
184
|
+
const pR = project(xR, zFarLine);
|
|
185
|
+
if (pL && pR) {
|
|
186
|
+
ctx.strokeStyle = lineColor;
|
|
187
|
+
ctx.beginPath();
|
|
188
|
+
ctx.moveTo(pL.x, pL.y);
|
|
189
|
+
ctx.lineTo(pR.x, pR.y);
|
|
190
|
+
ctx.stroke();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Vertical grid lines
|
|
195
|
+
const zMax = gridRows.length * tileSize + zNear;
|
|
196
|
+
for (let c = 0; c <= gridWidth; c++) {
|
|
197
|
+
const cOffset = c - gridWidth / 2;
|
|
198
|
+
const x = cOffset * tileSize;
|
|
199
|
+
const pStart = project(x, zNear);
|
|
200
|
+
const pEnd = project(x, zMax);
|
|
201
|
+
if (pStart && pEnd) {
|
|
202
|
+
ctx.strokeStyle = lineColor;
|
|
203
|
+
ctx.beginPath();
|
|
204
|
+
ctx.moveTo(pStart.x, pStart.y);
|
|
205
|
+
ctx.lineTo(pEnd.x, pEnd.y);
|
|
206
|
+
ctx.stroke();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fog / fade gradient
|
|
211
|
+
const fadeColor = isDark ? '0,0,0' : '255,255,255';
|
|
212
|
+
const g = ctx.createLinearGradient(0, 0, 0, canvas.height / 1.5);
|
|
213
|
+
g.addColorStop(0, `rgba(${fadeColor}, 1)`);
|
|
214
|
+
g.addColorStop(0.3, `rgba(${fadeColor}, 0.8)`);
|
|
215
|
+
g.addColorStop(1, `rgba(${fadeColor}, 0)`);
|
|
216
|
+
ctx.fillStyle = g;
|
|
217
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
218
|
+
|
|
219
|
+
animationId = requestAnimationFrame(draw);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
resize();
|
|
223
|
+
window.addEventListener('resize', resize);
|
|
224
|
+
draw();
|
|
225
|
+
|
|
226
|
+
return () => {
|
|
227
|
+
window.removeEventListener('resize', resize);
|
|
228
|
+
cancelAnimationFrame(animationId);
|
|
229
|
+
};
|
|
230
|
+
}, [isDark]);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<header
|
|
234
|
+
ref={headerRef}
|
|
235
|
+
className="relative flex h-[300px] w-full flex-col items-center justify-center overflow-hidden border-b-2 border-black text-left dark:border-gray-600"
|
|
236
|
+
>
|
|
237
|
+
{/* Animated Grid Canvas */}
|
|
238
|
+
<canvas
|
|
239
|
+
ref={canvasRef}
|
|
240
|
+
className="pointer-events-none absolute inset-0 z-0 opacity-60 blur-[1.5px]"
|
|
241
|
+
/>
|
|
242
|
+
|
|
243
|
+
{/* Content - Uses same container as main content for alignment */}
|
|
244
|
+
<div className="container relative z-10 mx-auto px-4">
|
|
245
|
+
{badge && (
|
|
246
|
+
<span className="mb-4 inline-block border-2 border-[#BA0C2F] bg-[#BA0C2F]/10 px-3 py-1 text-sm font-bold text-[#BA0C2F]">
|
|
247
|
+
{badge}
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
250
|
+
<h1 className="mb-5 text-4xl font-extrabold leading-tight tracking-tight text-gray-900 dark:text-gray-100 md:text-5xl lg:text-6xl">
|
|
251
|
+
{title}
|
|
252
|
+
</h1>
|
|
253
|
+
<p className="max-w-2xl text-lg leading-relaxed text-gray-600 dark:text-gray-400">
|
|
254
|
+
{description}
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
</header>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default HeroHeader;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HeroHeader } from './HeroHeader';
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface PreviewCardProps {
|
|
4
|
+
id: string;
|
|
5
|
+
type: 'component' | 'page';
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
variants?: string[];
|
|
9
|
+
usesComponents?: string[];
|
|
10
|
+
route?: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
onClick: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Preview Card Component
|
|
17
|
+
*
|
|
18
|
+
* Displays a preview card for a component or page in the UI Showcase grid.
|
|
19
|
+
* - Pages: Shows ACTUAL iframe preview of the live route (scaled down)
|
|
20
|
+
* - Components: Shows generated HTML preview matching Sandpack style
|
|
21
|
+
*
|
|
22
|
+
* Hustle Together branding with boxy 90s style.
|
|
23
|
+
* Hover: Solid red shadow (4px 4px 0 #BA0C2F), no border change.
|
|
24
|
+
*
|
|
25
|
+
* Created with Hustle API Dev Tools (v3.9.2)
|
|
26
|
+
*/
|
|
27
|
+
export function PreviewCard({
|
|
28
|
+
id,
|
|
29
|
+
type,
|
|
30
|
+
name,
|
|
31
|
+
description,
|
|
32
|
+
variants,
|
|
33
|
+
usesComponents,
|
|
34
|
+
route,
|
|
35
|
+
file,
|
|
36
|
+
onClick,
|
|
37
|
+
}: PreviewCardProps) {
|
|
38
|
+
// Get page route from file path or prop
|
|
39
|
+
const getPageRoute = () => {
|
|
40
|
+
if (route) return route;
|
|
41
|
+
if (file?.includes('src/app/')) {
|
|
42
|
+
const match = file.match(/src\/app\/(.+?)\/page\.tsx?$/);
|
|
43
|
+
if (match) return `/${match[1]}`;
|
|
44
|
+
}
|
|
45
|
+
return `/${id}`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
className="group relative flex flex-col overflow-hidden border-2 border-black bg-white text-left transition-all hover:shadow-[4px_4px_0_#BA0C2F] focus:outline-none focus:ring-2 focus:ring-[#BA0C2F] focus:ring-offset-2 dark:border-gray-700 dark:bg-gray-900"
|
|
52
|
+
>
|
|
53
|
+
{/* Preview Area */}
|
|
54
|
+
<div className="relative aspect-video w-full overflow-hidden bg-gray-100 dark:bg-gray-800">
|
|
55
|
+
{type === 'page' ? (
|
|
56
|
+
// REAL iframe preview for pages - scaled down to fit
|
|
57
|
+
<iframe
|
|
58
|
+
src={getPageRoute()}
|
|
59
|
+
title={`Preview of ${name}`}
|
|
60
|
+
className="pointer-events-none h-full w-full origin-top-left scale-[0.5]"
|
|
61
|
+
style={{ width: '200%', height: '200%' }}
|
|
62
|
+
loading="lazy"
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
// Component preview - generated HTML matching Sandpack style
|
|
66
|
+
<ComponentPreview
|
|
67
|
+
id={id}
|
|
68
|
+
name={name}
|
|
69
|
+
variants={variants}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{/* Type Badge */}
|
|
74
|
+
<div className="absolute right-2 top-2">
|
|
75
|
+
<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'}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Hover Overlay */}
|
|
81
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity group-hover:opacity-100">
|
|
82
|
+
<span className="border-2 border-white bg-[#BA0C2F] px-4 py-2 text-sm font-bold text-white">
|
|
83
|
+
Click to Preview
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Card Content */}
|
|
89
|
+
<div className="flex flex-1 flex-col border-t-2 border-black p-4 dark:border-gray-700">
|
|
90
|
+
<h3 className="font-bold text-black group-hover:text-[#BA0C2F] dark:text-white">
|
|
91
|
+
{name}
|
|
92
|
+
</h3>
|
|
93
|
+
|
|
94
|
+
{description && (
|
|
95
|
+
<p className="mt-2 line-clamp-2 text-sm text-gray-600 dark:text-gray-400">
|
|
96
|
+
{description}
|
|
97
|
+
</p>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{/* Variants or Used Components */}
|
|
101
|
+
<div className="mt-auto pt-3">
|
|
102
|
+
{variants && variants.length > 0 && (
|
|
103
|
+
<div className="flex flex-wrap gap-1">
|
|
104
|
+
{variants.slice(0, 3).map((variant) => (
|
|
105
|
+
<span
|
|
106
|
+
key={variant}
|
|
107
|
+
className="border border-gray-300 bg-gray-50 px-1.5 py-0.5 text-xs text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
|
108
|
+
>
|
|
109
|
+
{variant}
|
|
110
|
+
</span>
|
|
111
|
+
))}
|
|
112
|
+
{variants.length > 3 && (
|
|
113
|
+
<span className="border border-gray-300 bg-gray-50 px-1.5 py-0.5 text-xs text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
|
|
114
|
+
+{variants.length - 3}
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{usesComponents && usesComponents.length > 0 && (
|
|
121
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
122
|
+
Uses: {usesComponents.slice(0, 3).join(', ')}
|
|
123
|
+
{usesComponents.length > 3 && ` +${usesComponents.length - 3}`}
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</button>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Component Preview
|
|
134
|
+
* Generates a mini HTML preview of the component based on its name/type.
|
|
135
|
+
* Uses the same approach as the modal's Sandpack but renders as inline HTML.
|
|
136
|
+
*/
|
|
137
|
+
function ComponentPreview({
|
|
138
|
+
id,
|
|
139
|
+
name,
|
|
140
|
+
variants,
|
|
141
|
+
}: {
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
variants?: string[];
|
|
145
|
+
}) {
|
|
146
|
+
const previewHtml = generatePreviewHtml(name, variants?.[0] || 'primary');
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<iframe
|
|
150
|
+
srcDoc={previewHtml}
|
|
151
|
+
title={`Preview of ${name}`}
|
|
152
|
+
className="pointer-events-none h-full w-full"
|
|
153
|
+
loading="lazy"
|
|
154
|
+
sandbox="allow-scripts"
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generate HTML preview for component types
|
|
161
|
+
* Matches the same visual style as the modal's Sandpack previews
|
|
162
|
+
*/
|
|
163
|
+
function generatePreviewHtml(name: string, variant: string): string {
|
|
164
|
+
const lowerName = name.toLowerCase();
|
|
165
|
+
|
|
166
|
+
let content = '';
|
|
167
|
+
|
|
168
|
+
if (lowerName.includes('button')) {
|
|
169
|
+
content = `
|
|
170
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
171
|
+
<button style="
|
|
172
|
+
padding: 8px 16px;
|
|
173
|
+
font-size: 12px;
|
|
174
|
+
font-weight: bold;
|
|
175
|
+
border: 2px solid #BA0C2F;
|
|
176
|
+
background: #BA0C2F;
|
|
177
|
+
color: white;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
">Primary</button>
|
|
180
|
+
<button style="
|
|
181
|
+
padding: 8px 16px;
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
font-weight: bold;
|
|
184
|
+
border: 2px solid #000;
|
|
185
|
+
background: white;
|
|
186
|
+
color: black;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
">Secondary</button>
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
} else if (lowerName.includes('card')) {
|
|
192
|
+
content = `
|
|
193
|
+
<div style="
|
|
194
|
+
border: 2px solid #000;
|
|
195
|
+
background: white;
|
|
196
|
+
width: 140px;
|
|
197
|
+
font-size: 11px;
|
|
198
|
+
">
|
|
199
|
+
<div style="padding: 8px; border-bottom: 1px solid #eee; font-weight: bold;">Card Title</div>
|
|
200
|
+
<div style="padding: 8px; color: #666;">Card content goes here...</div>
|
|
201
|
+
<div style="padding: 8px; border-top: 1px solid #eee; background: #f8f8f8;">
|
|
202
|
+
<button style="padding: 4px 8px; background: #BA0C2F; color: white; border: none; font-size: 10px; font-weight: bold;">Action</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
`;
|
|
206
|
+
} else if (lowerName.includes('input') || lowerName.includes('field') || lowerName.includes('form')) {
|
|
207
|
+
content = `
|
|
208
|
+
<div style="width: 140px;">
|
|
209
|
+
<label style="display: block; font-size: 11px; font-weight: bold; margin-bottom: 4px;">Label</label>
|
|
210
|
+
<input type="text" placeholder="Enter text..." style="
|
|
211
|
+
width: 100%;
|
|
212
|
+
padding: 6px 8px;
|
|
213
|
+
border: 2px solid #000;
|
|
214
|
+
font-size: 11px;
|
|
215
|
+
box-sizing: border-box;
|
|
216
|
+
" />
|
|
217
|
+
<p style="font-size: 10px; color: #666; margin: 4px 0 0;">Helper text</p>
|
|
218
|
+
</div>
|
|
219
|
+
`;
|
|
220
|
+
} else if (lowerName.includes('table')) {
|
|
221
|
+
content = `
|
|
222
|
+
<div style="border: 2px solid #000; font-size: 10px; width: 160px;">
|
|
223
|
+
<div style="display: flex; background: #f0f0f0; border-bottom: 1px solid #ccc;">
|
|
224
|
+
<div style="flex: 1; padding: 4px 6px; font-weight: bold;">Name</div>
|
|
225
|
+
<div style="flex: 1; padding: 4px 6px; font-weight: bold;">Status</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div style="display: flex; border-bottom: 1px solid #eee;">
|
|
228
|
+
<div style="flex: 1; padding: 4px 6px;">Item 1</div>
|
|
229
|
+
<div style="flex: 1; padding: 4px 6px; color: #22c55e;">Active</div>
|
|
230
|
+
</div>
|
|
231
|
+
<div style="display: flex;">
|
|
232
|
+
<div style="flex: 1; padding: 4px 6px;">Item 2</div>
|
|
233
|
+
<div style="flex: 1; padding: 4px 6px; color: #BA0C2F;">Pending</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
`;
|
|
237
|
+
} else if (lowerName.includes('header') || lowerName.includes('nav')) {
|
|
238
|
+
content = `
|
|
239
|
+
<div style="border: 2px solid #000; background: white; padding: 8px 12px; width: 180px;">
|
|
240
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
241
|
+
<div style="width: 24px; height: 12px; background: #BA0C2F;"></div>
|
|
242
|
+
<div style="display: flex; gap: 8px; font-size: 10px;">
|
|
243
|
+
<span>Home</span>
|
|
244
|
+
<span>About</span>
|
|
245
|
+
<span>Contact</span>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
`;
|
|
250
|
+
} else if (lowerName.includes('modal') || lowerName.includes('dialog')) {
|
|
251
|
+
content = `
|
|
252
|
+
<div style="position: relative; width: 140px; height: 100px;">
|
|
253
|
+
<div style="position: absolute; inset: 0; background: rgba(0,0,0,0.3);"></div>
|
|
254
|
+
<div style="
|
|
255
|
+
position: absolute;
|
|
256
|
+
top: 50%;
|
|
257
|
+
left: 50%;
|
|
258
|
+
transform: translate(-50%, -50%);
|
|
259
|
+
background: white;
|
|
260
|
+
border: 2px solid #000;
|
|
261
|
+
padding: 12px;
|
|
262
|
+
width: 100px;
|
|
263
|
+
font-size: 10px;
|
|
264
|
+
">
|
|
265
|
+
<div style="font-weight: bold; margin-bottom: 8px;">Modal Title</div>
|
|
266
|
+
<div style="background: #f0f0f0; height: 20px; margin-bottom: 8px;"></div>
|
|
267
|
+
<div style="display: flex; justify-content: flex-end; gap: 4px;">
|
|
268
|
+
<button style="padding: 2px 6px; border: 1px solid #000; background: white; font-size: 9px;">Cancel</button>
|
|
269
|
+
<button style="padding: 2px 6px; background: #BA0C2F; color: white; border: none; font-size: 9px;">Save</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
`;
|
|
274
|
+
} else {
|
|
275
|
+
// Generic component preview
|
|
276
|
+
content = `
|
|
277
|
+
<div style="text-align: center; padding: 12px;">
|
|
278
|
+
<div style="
|
|
279
|
+
width: 40px;
|
|
280
|
+
height: 40px;
|
|
281
|
+
margin: 0 auto 8px;
|
|
282
|
+
border: 2px solid #BA0C2F;
|
|
283
|
+
display: flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
justify-content: center;
|
|
286
|
+
background: white;
|
|
287
|
+
">
|
|
288
|
+
<span style="font-size: 16px;">⬛</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div style="font-size: 11px; font-weight: bold;">${name}</div>
|
|
291
|
+
</div>
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return `
|
|
296
|
+
<!DOCTYPE html>
|
|
297
|
+
<html>
|
|
298
|
+
<head>
|
|
299
|
+
<style>
|
|
300
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
301
|
+
body {
|
|
302
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
min-height: 100vh;
|
|
307
|
+
background: #f8f8f8;
|
|
308
|
+
padding: 8px;
|
|
309
|
+
}
|
|
310
|
+
</style>
|
|
311
|
+
</head>
|
|
312
|
+
<body>${content}</body>
|
|
313
|
+
</html>
|
|
314
|
+
`;
|
|
315
|
+
}
|