@hustle-together/api-dev-tools 3.6.5 → 3.10.0
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 +5599 -258
- package/bin/cli.js +395 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +35 -15
- 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-page.md +933 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +545 -21
- package/hooks/cache-research.py +337 -0
- package/hooks/check-api-routes.py +168 -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-a11y-audit.py +202 -0
- package/hooks/enforce-brand-guide.py +241 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-page-components.py +186 -0
- package/hooks/enforce-page-data-schema.py +155 -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 +1161 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +160 -15
- 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 +212 -0
- package/package.json +8 -3
- 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 +217 -161
- package/templates/api-showcase/_components/APICard.tsx +153 -0
- package/templates/api-showcase/_components/APIModal.tsx +375 -0
- package/templates/api-showcase/_components/APIShowcase.tsx +231 -0
- package/templates/api-showcase/_components/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 +90 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/_components/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/_components/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
- package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
- package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
- package/demo/hustle-together/index.html +0 -1312
- package/demo/workflow-demo-v3.5-backup.html +0 -5008
- package/demo/workflow-demo.html +0 -6202
|
@@ -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
|
+
}
|