@hustle-together/api-dev-tools 3.6.4 → 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.
Files changed (61) hide show
  1. package/README.md +5307 -258
  2. package/bin/cli.js +348 -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} +22 -2
  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.md +825 -0
  14. package/hooks/api-workflow-check.py +385 -19
  15. package/hooks/cache-research.py +337 -0
  16. package/hooks/check-playwright-setup.py +103 -0
  17. package/hooks/check-storybook-setup.py +81 -0
  18. package/hooks/detect-interruption.py +165 -0
  19. package/hooks/enforce-brand-guide.py +131 -0
  20. package/hooks/enforce-documentation.py +60 -8
  21. package/hooks/enforce-freshness.py +184 -0
  22. package/hooks/enforce-questions-sourced.py +146 -0
  23. package/hooks/enforce-schema-from-interview.py +248 -0
  24. package/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/hooks/enforce-ui-interview.py +130 -0
  26. package/hooks/generate-manifest-entry.py +981 -0
  27. package/hooks/session-logger.py +297 -0
  28. package/hooks/session-startup.py +65 -10
  29. package/hooks/track-scope-coverage.py +220 -0
  30. package/hooks/track-tool-use.py +81 -1
  31. package/hooks/update-api-showcase.py +149 -0
  32. package/hooks/update-registry.py +352 -0
  33. package/hooks/update-ui-showcase.py +148 -0
  34. package/package.json +8 -2
  35. package/templates/BRAND_GUIDE.md +299 -0
  36. package/templates/CLAUDE-SECTION.md +56 -24
  37. package/templates/SPEC.json +640 -0
  38. package/templates/api-dev-state.json +179 -161
  39. package/templates/api-showcase/APICard.tsx +153 -0
  40. package/templates/api-showcase/APIModal.tsx +375 -0
  41. package/templates/api-showcase/APIShowcase.tsx +231 -0
  42. package/templates/api-showcase/APITester.tsx +522 -0
  43. package/templates/api-showcase/page.tsx +41 -0
  44. package/templates/component/Component.stories.tsx +172 -0
  45. package/templates/component/Component.test.tsx +237 -0
  46. package/templates/component/Component.tsx +86 -0
  47. package/templates/component/Component.types.ts +55 -0
  48. package/templates/component/index.ts +15 -0
  49. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  50. package/templates/dev-tools/page.tsx +10 -0
  51. package/templates/page/page.e2e.test.ts +218 -0
  52. package/templates/page/page.tsx +42 -0
  53. package/templates/performance-budgets.json +58 -0
  54. package/templates/registry.json +13 -0
  55. package/templates/settings.json +74 -0
  56. package/templates/shared/HeroHeader.tsx +261 -0
  57. package/templates/shared/index.ts +1 -0
  58. package/templates/ui-showcase/PreviewCard.tsx +315 -0
  59. package/templates/ui-showcase/PreviewModal.tsx +676 -0
  60. package/templates/ui-showcase/UIShowcase.tsx +262 -0
  61. package/templates/ui-showcase/page.tsx +26 -0
@@ -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
+ }