@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.
Files changed (72) hide show
  1. package/README.md +5599 -258
  2. package/bin/cli.js +395 -20
  3. package/commands/README.md +459 -71
  4. package/commands/hustle-api-continue.md +158 -0
  5. package/commands/{api-create.md → hustle-api-create.md} +35 -15
  6. package/commands/{api-env.md → hustle-api-env.md} +4 -4
  7. package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
  8. package/commands/{api-research.md → hustle-api-research.md} +3 -3
  9. package/commands/hustle-api-sessions.md +149 -0
  10. package/commands/{api-status.md → hustle-api-status.md} +16 -16
  11. package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
  12. package/commands/hustle-combine.md +763 -0
  13. package/commands/hustle-ui-create-page.md +933 -0
  14. package/commands/hustle-ui-create.md +825 -0
  15. package/hooks/api-workflow-check.py +545 -21
  16. package/hooks/cache-research.py +337 -0
  17. package/hooks/check-api-routes.py +168 -0
  18. package/hooks/check-playwright-setup.py +103 -0
  19. package/hooks/check-storybook-setup.py +81 -0
  20. package/hooks/detect-interruption.py +165 -0
  21. package/hooks/enforce-a11y-audit.py +202 -0
  22. package/hooks/enforce-brand-guide.py +241 -0
  23. package/hooks/enforce-documentation.py +60 -8
  24. package/hooks/enforce-freshness.py +184 -0
  25. package/hooks/enforce-page-components.py +186 -0
  26. package/hooks/enforce-page-data-schema.py +155 -0
  27. package/hooks/enforce-questions-sourced.py +146 -0
  28. package/hooks/enforce-schema-from-interview.py +248 -0
  29. package/hooks/enforce-ui-disambiguation.py +108 -0
  30. package/hooks/enforce-ui-interview.py +130 -0
  31. package/hooks/generate-manifest-entry.py +1161 -0
  32. package/hooks/session-logger.py +297 -0
  33. package/hooks/session-startup.py +160 -15
  34. package/hooks/track-scope-coverage.py +220 -0
  35. package/hooks/track-tool-use.py +81 -1
  36. package/hooks/update-api-showcase.py +149 -0
  37. package/hooks/update-registry.py +352 -0
  38. package/hooks/update-ui-showcase.py +212 -0
  39. package/package.json +8 -3
  40. package/templates/BRAND_GUIDE.md +299 -0
  41. package/templates/CLAUDE-SECTION.md +56 -24
  42. package/templates/SPEC.json +640 -0
  43. package/templates/api-dev-state.json +217 -161
  44. package/templates/api-showcase/_components/APICard.tsx +153 -0
  45. package/templates/api-showcase/_components/APIModal.tsx +375 -0
  46. package/templates/api-showcase/_components/APIShowcase.tsx +231 -0
  47. package/templates/api-showcase/_components/APITester.tsx +522 -0
  48. package/templates/api-showcase/page.tsx +41 -0
  49. package/templates/component/Component.stories.tsx +172 -0
  50. package/templates/component/Component.test.tsx +237 -0
  51. package/templates/component/Component.tsx +86 -0
  52. package/templates/component/Component.types.ts +55 -0
  53. package/templates/component/index.ts +15 -0
  54. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  55. package/templates/dev-tools/page.tsx +10 -0
  56. package/templates/page/page.e2e.test.ts +218 -0
  57. package/templates/page/page.tsx +42 -0
  58. package/templates/performance-budgets.json +58 -0
  59. package/templates/registry.json +13 -0
  60. package/templates/settings.json +90 -0
  61. package/templates/shared/HeroHeader.tsx +261 -0
  62. package/templates/shared/index.ts +1 -0
  63. package/templates/ui-showcase/_components/PreviewCard.tsx +315 -0
  64. package/templates/ui-showcase/_components/PreviewModal.tsx +676 -0
  65. package/templates/ui-showcase/_components/UIShowcase.tsx +262 -0
  66. package/templates/ui-showcase/page.tsx +26 -0
  67. package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
  68. package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
  69. package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
  70. package/demo/hustle-together/index.html +0 -1312
  71. package/demo/workflow-demo-v3.5-backup.html +0 -5008
  72. 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
+ }