@hustle-together/api-dev-tools 3.10.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.
Files changed (178) hide show
  1. package/.claude/agents/code-reviewer.md +170 -0
  2. package/.claude/agents/docs-generator.md +80 -0
  3. package/.claude/agents/implementation-reviewer.md +119 -0
  4. package/.claude/agents/parallel-researcher.md +52 -0
  5. package/.claude/agents/research-validator.md +116 -0
  6. package/.claude/agents/schema-generator.md +70 -0
  7. package/.claude/agents/test-writer.md +104 -0
  8. package/.claude/api-dev-state.json +331 -0
  9. package/.claude/commands/README.md +196 -0
  10. package/.claude/commands/add-command.md +212 -0
  11. package/.claude/commands/api-create.md +510 -0
  12. package/.claude/commands/api-env.md +51 -0
  13. package/.claude/commands/api-interview.md +344 -0
  14. package/.claude/commands/api-research.md +357 -0
  15. package/.claude/commands/api-status.md +279 -0
  16. package/.claude/commands/api-verify.md +232 -0
  17. package/.claude/commands/beepboop.md +96 -0
  18. package/.claude/commands/busycommit.md +111 -0
  19. package/.claude/commands/commit.md +82 -0
  20. package/.claude/commands/cycle.md +137 -0
  21. package/.claude/commands/gap.md +85 -0
  22. package/.claude/commands/green.md +137 -0
  23. package/.claude/commands/issue.md +187 -0
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +167 -0
  27. package/.claude/commands/pr.md +121 -0
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +137 -0
  30. package/.claude/commands/refactor.md +137 -0
  31. package/.claude/commands/spike.md +137 -0
  32. package/.claude/commands/summarize.md +93 -0
  33. package/.claude/commands/tdd.md +139 -0
  34. package/.claude/commands/worktree-add.md +307 -0
  35. package/.claude/commands/worktree-cleanup.md +275 -0
  36. package/.claude/hooks/api-workflow-check.py +227 -0
  37. package/.claude/hooks/enforce-deep-research.py +185 -0
  38. package/.claude/hooks/enforce-disambiguation.py +155 -0
  39. package/.claude/hooks/enforce-documentation.py +192 -0
  40. package/.claude/hooks/enforce-environment.py +253 -0
  41. package/.claude/hooks/enforce-external-research.py +328 -0
  42. package/.claude/hooks/enforce-interview.py +421 -0
  43. package/.claude/hooks/enforce-refactor.py +189 -0
  44. package/.claude/hooks/enforce-research.py +159 -0
  45. package/.claude/hooks/enforce-schema.py +186 -0
  46. package/.claude/hooks/enforce-scope.py +160 -0
  47. package/.claude/hooks/enforce-tdd-red.py +250 -0
  48. package/.claude/hooks/enforce-verify.py +186 -0
  49. package/.claude/hooks/periodic-reground.py +154 -0
  50. package/.claude/hooks/session-startup.py +151 -0
  51. package/.claude/hooks/track-tool-use.py +626 -0
  52. package/.claude/hooks/verify-after-green.py +282 -0
  53. package/.claude/hooks/verify-implementation.py +225 -0
  54. package/.claude/research/index.json +6 -0
  55. package/.claude/settings.json +144 -0
  56. package/.claude/settings.local.json +12 -0
  57. package/.claude-plugin/marketplace.json +103 -0
  58. package/.skills/README.md +293 -0
  59. package/.skills/_shared/convert-commands.py +192 -0
  60. package/.skills/_shared/hooks/api-workflow-check.py +227 -0
  61. package/.skills/_shared/hooks/enforce-deep-research.py +185 -0
  62. package/.skills/_shared/hooks/enforce-disambiguation.py +155 -0
  63. package/.skills/_shared/hooks/enforce-documentation.py +192 -0
  64. package/.skills/_shared/hooks/enforce-environment.py +253 -0
  65. package/.skills/_shared/hooks/enforce-external-research.py +328 -0
  66. package/.skills/_shared/hooks/enforce-interview.py +421 -0
  67. package/.skills/_shared/hooks/enforce-refactor.py +189 -0
  68. package/.skills/_shared/hooks/enforce-research.py +159 -0
  69. package/.skills/_shared/hooks/enforce-schema.py +186 -0
  70. package/.skills/_shared/hooks/enforce-scope.py +160 -0
  71. package/.skills/_shared/hooks/enforce-tdd-red.py +250 -0
  72. package/.skills/_shared/hooks/enforce-verify.py +186 -0
  73. package/.skills/_shared/hooks/periodic-reground.py +154 -0
  74. package/.skills/_shared/hooks/session-startup.py +151 -0
  75. package/.skills/_shared/hooks/track-tool-use.py +626 -0
  76. package/.skills/_shared/hooks/verify-after-green.py +282 -0
  77. package/.skills/_shared/hooks/verify-implementation.py +225 -0
  78. package/.skills/_shared/install.sh +114 -0
  79. package/.skills/_shared/settings.json +93 -0
  80. package/.skills/add-command/SKILL.md +227 -0
  81. package/.skills/api-create/SKILL.md +623 -0
  82. package/.skills/api-env/SKILL.md +64 -0
  83. package/.skills/api-interview/SKILL.md +357 -0
  84. package/.skills/api-research/SKILL.md +370 -0
  85. package/.skills/api-status/SKILL.md +292 -0
  86. package/.skills/api-verify/SKILL.md +245 -0
  87. package/.skills/beepboop/SKILL.md +111 -0
  88. package/.skills/busycommit/SKILL.md +126 -0
  89. package/.skills/commit/SKILL.md +97 -0
  90. package/.skills/cycle/SKILL.md +152 -0
  91. package/.skills/gap/SKILL.md +100 -0
  92. package/.skills/green/SKILL.md +152 -0
  93. package/.skills/issue/SKILL.md +202 -0
  94. package/.skills/plan/SKILL.md +182 -0
  95. package/.skills/pr/SKILL.md +136 -0
  96. package/.skills/publish/SKILL.md +160 -0
  97. package/.skills/red/SKILL.md +152 -0
  98. package/.skills/refactor/SKILL.md +152 -0
  99. package/.skills/spike/SKILL.md +152 -0
  100. package/.skills/summarize/SKILL.md +108 -0
  101. package/.skills/tdd/SKILL.md +154 -0
  102. package/.skills/update-todos/SKILL.md +250 -0
  103. package/.skills/worktree-add/SKILL.md +322 -0
  104. package/.skills/worktree-cleanup/SKILL.md +290 -0
  105. package/CHANGELOG.md +115 -0
  106. package/README.md +161 -7101
  107. package/bin/cli.js +448 -805
  108. package/commands/README.md +66 -31
  109. package/commands/add-command.md +8 -5
  110. package/commands/beepboop.md +4 -5
  111. package/commands/busycommit.md +2 -3
  112. package/commands/commit.md +2 -3
  113. package/commands/cycle.md +2 -7
  114. package/commands/gap.md +2 -3
  115. package/commands/green.md +2 -7
  116. package/commands/hustle-api-continue.md +8 -5
  117. package/commands/hustle-api-create.md +70 -29
  118. package/commands/hustle-api-env.md +1 -0
  119. package/commands/hustle-api-interview.md +32 -19
  120. package/commands/hustle-api-research.md +47 -21
  121. package/commands/hustle-api-sessions.md +8 -7
  122. package/commands/hustle-api-status.md +21 -1
  123. package/commands/hustle-api-verify.md +14 -13
  124. package/commands/hustle-combine.md +488 -241
  125. package/commands/hustle-ui-create-page.md +113 -50
  126. package/commands/hustle-ui-create.md +179 -26
  127. package/commands/issue.md +3 -8
  128. package/commands/plan.md +2 -3
  129. package/commands/pr.md +2 -3
  130. package/commands/red.md +2 -7
  131. package/commands/refactor.md +2 -7
  132. package/commands/spike.md +2 -7
  133. package/commands/summarize.md +2 -3
  134. package/commands/tdd.md +2 -7
  135. package/commands/worktree-add.md +208 -216
  136. package/commands/worktree-cleanup.md +172 -178
  137. package/hooks/api-workflow-check.py +5 -3
  138. package/hooks/enforce-component-type-confirm.py +97 -0
  139. package/hooks/lib/__init__.py +1 -0
  140. package/hooks/lib/greptile.py +355 -0
  141. package/hooks/lib/ntfy.py +209 -0
  142. package/hooks/notify-input-needed.py +73 -0
  143. package/hooks/notify-phase-complete.py +90 -0
  144. package/hooks/run-code-review.py +246 -0
  145. package/hooks/track-token-usage.py +121 -0
  146. package/package.json +33 -12
  147. package/scripts/collect-test-results.ts +102 -77
  148. package/scripts/extract-parameters.ts +112 -70
  149. package/scripts/generate-test-manifest.ts +118 -77
  150. package/templates/.env.example +57 -0
  151. package/templates/BRAND_GUIDE.md +92 -52
  152. package/templates/CLAUDE-SECTION.md +40 -37
  153. package/templates/SPEC.json +186 -38
  154. package/templates/api-dev-state.json +33 -4
  155. package/templates/api-showcase/_components/APICard.tsx +22 -18
  156. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  157. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  158. package/templates/api-showcase/_components/APITester.tsx +128 -67
  159. package/templates/api-showcase/page.tsx +4 -4
  160. package/templates/api-test/page.tsx +51 -30
  161. package/templates/api-test/test-structure/route.ts +43 -34
  162. package/templates/component/Component.stories.tsx +41 -39
  163. package/templates/component/Component.test.tsx +96 -78
  164. package/templates/component/Component.tsx +63 -52
  165. package/templates/component/Component.types.ts +10 -6
  166. package/templates/component/Component.visual.spec.ts +170 -0
  167. package/templates/component/index.ts +2 -2
  168. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  169. package/templates/dev-tools/page.tsx +4 -3
  170. package/templates/mcp-servers.json +30 -2
  171. package/templates/page/page.e2e.test.ts +56 -48
  172. package/templates/page/page.tsx +3 -3
  173. package/templates/shared/HeroHeader.tsx +16 -15
  174. package/templates/shared/index.ts +1 -1
  175. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  176. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  177. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  178. package/templates/ui-showcase/page.tsx +4 -4
@@ -1,164 +1,178 @@
1
- import { render, screen, fireEvent } from '@testing-library/react';
2
- import { describe, it, expect, vi } from 'vitest';
3
- import { __COMPONENT_NAME__ } from './__COMPONENT_NAME__';
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { __COMPONENT_NAME__ } from "./__COMPONENT_NAME__";
4
4
 
5
- describe('__COMPONENT_NAME__', () => {
6
- describe('Rendering', () => {
7
- it('renders children correctly', () => {
5
+ describe("__COMPONENT_NAME__", () => {
6
+ describe("Rendering", () => {
7
+ it("renders children correctly", () => {
8
8
  render(<__COMPONENT_NAME__>Test Content</__COMPONENT_NAME__>);
9
- expect(screen.getByText('Test Content')).toBeInTheDocument();
9
+ expect(screen.getByText("Test Content")).toBeInTheDocument();
10
10
  });
11
11
 
12
- it('renders as a button element', () => {
12
+ it("renders as a button element", () => {
13
13
  render(<__COMPONENT_NAME__>Click me</__COMPONENT_NAME__>);
14
- expect(screen.getByRole('button')).toBeInTheDocument();
14
+ expect(screen.getByRole("button")).toBeInTheDocument();
15
15
  });
16
16
 
17
- it('forwards ref correctly', () => {
17
+ it("forwards ref correctly", () => {
18
18
  const ref = vi.fn();
19
19
  render(<__COMPONENT_NAME__ ref={ref}>Test</__COMPONENT_NAME__>);
20
20
  expect(ref).toHaveBeenCalled();
21
21
  });
22
22
  });
23
23
 
24
- describe('Variants', () => {
25
- it('applies primary variant by default', () => {
26
- const { container } = render(<__COMPONENT_NAME__>Primary</__COMPONENT_NAME__>);
27
- expect(container.firstChild).toHaveClass('bg-primary');
24
+ describe("Variants", () => {
25
+ it("applies primary variant by default", () => {
26
+ const { container } = render(
27
+ <__COMPONENT_NAME__>Primary</__COMPONENT_NAME__>,
28
+ );
29
+ expect(container.firstChild).toHaveClass("bg-primary");
28
30
  });
29
31
 
30
- it('applies secondary variant', () => {
32
+ it("applies secondary variant", () => {
31
33
  const { container } = render(
32
- <__COMPONENT_NAME__ variant="secondary">Secondary</__COMPONENT_NAME__>
34
+ <__COMPONENT_NAME__ variant="secondary">Secondary</__COMPONENT_NAME__>,
33
35
  );
34
- expect(container.firstChild).toHaveClass('bg-secondary');
36
+ expect(container.firstChild).toHaveClass("bg-secondary");
35
37
  });
36
38
 
37
- it('applies destructive variant', () => {
39
+ it("applies destructive variant", () => {
38
40
  const { container } = render(
39
- <__COMPONENT_NAME__ variant="destructive">Delete</__COMPONENT_NAME__>
41
+ <__COMPONENT_NAME__ variant="destructive">Delete</__COMPONENT_NAME__>,
40
42
  );
41
- expect(container.firstChild).toHaveClass('bg-destructive');
43
+ expect(container.firstChild).toHaveClass("bg-destructive");
42
44
  });
43
45
 
44
- it('applies outline variant', () => {
46
+ it("applies outline variant", () => {
45
47
  const { container } = render(
46
- <__COMPONENT_NAME__ variant="outline">Outline</__COMPONENT_NAME__>
48
+ <__COMPONENT_NAME__ variant="outline">Outline</__COMPONENT_NAME__>,
47
49
  );
48
- expect(container.firstChild).toHaveClass('border');
50
+ expect(container.firstChild).toHaveClass("border");
49
51
  });
50
52
 
51
- it('applies ghost variant', () => {
53
+ it("applies ghost variant", () => {
52
54
  const { container } = render(
53
- <__COMPONENT_NAME__ variant="ghost">Ghost</__COMPONENT_NAME__>
55
+ <__COMPONENT_NAME__ variant="ghost">Ghost</__COMPONENT_NAME__>,
54
56
  );
55
- expect(container.firstChild).toHaveClass('hover:bg-accent');
57
+ expect(container.firstChild).toHaveClass("hover:bg-accent");
56
58
  });
57
59
  });
58
60
 
59
- describe('Sizes', () => {
60
- it('applies medium size by default', () => {
61
- const { container } = render(<__COMPONENT_NAME__>Medium</__COMPONENT_NAME__>);
62
- expect(container.firstChild).toHaveClass('h-10');
61
+ describe("Sizes", () => {
62
+ it("applies medium size by default", () => {
63
+ const { container } = render(
64
+ <__COMPONENT_NAME__>Medium</__COMPONENT_NAME__>,
65
+ );
66
+ expect(container.firstChild).toHaveClass("h-10");
63
67
  });
64
68
 
65
- it('applies small size', () => {
66
- const { container } = render(<__COMPONENT_NAME__ size="sm">Small</__COMPONENT_NAME__>);
67
- expect(container.firstChild).toHaveClass('h-8');
69
+ it("applies small size", () => {
70
+ const { container } = render(
71
+ <__COMPONENT_NAME__ size="sm">Small</__COMPONENT_NAME__>,
72
+ );
73
+ expect(container.firstChild).toHaveClass("h-8");
68
74
  });
69
75
 
70
- it('applies large size', () => {
71
- const { container } = render(<__COMPONENT_NAME__ size="lg">Large</__COMPONENT_NAME__>);
72
- expect(container.firstChild).toHaveClass('h-12');
76
+ it("applies large size", () => {
77
+ const { container } = render(
78
+ <__COMPONENT_NAME__ size="lg">Large</__COMPONENT_NAME__>,
79
+ );
80
+ expect(container.firstChild).toHaveClass("h-12");
73
81
  });
74
82
  });
75
83
 
76
- describe('States', () => {
77
- it('handles disabled state', () => {
84
+ describe("States", () => {
85
+ it("handles disabled state", () => {
78
86
  render(<__COMPONENT_NAME__ disabled>Disabled</__COMPONENT_NAME__>);
79
- expect(screen.getByRole('button')).toBeDisabled();
87
+ expect(screen.getByRole("button")).toBeDisabled();
80
88
  });
81
89
 
82
- it('handles loading state', () => {
90
+ it("handles loading state", () => {
83
91
  render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
84
- const button = screen.getByRole('button');
92
+ const button = screen.getByRole("button");
85
93
  expect(button).toBeDisabled();
86
- expect(button).toHaveAttribute('aria-busy', 'true');
94
+ expect(button).toHaveAttribute("aria-busy", "true");
87
95
  });
88
96
 
89
- it('shows loading spinner when loading', () => {
90
- const { container } = render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
91
- expect(container.querySelector('svg.animate-spin')).toBeInTheDocument();
97
+ it("shows loading spinner when loading", () => {
98
+ const { container } = render(
99
+ <__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>,
100
+ );
101
+ expect(container.querySelector("svg.animate-spin")).toBeInTheDocument();
92
102
  });
93
103
  });
94
104
 
95
- describe('Interaction', () => {
96
- it('calls onClick when clicked', () => {
105
+ describe("Interaction", () => {
106
+ it("calls onClick when clicked", () => {
97
107
  const handleClick = vi.fn();
98
- render(<__COMPONENT_NAME__ onClick={handleClick}>Click</__COMPONENT_NAME__>);
99
- fireEvent.click(screen.getByRole('button'));
108
+ render(
109
+ <__COMPONENT_NAME__ onClick={handleClick}>Click</__COMPONENT_NAME__>,
110
+ );
111
+ fireEvent.click(screen.getByRole("button"));
100
112
  expect(handleClick).toHaveBeenCalledTimes(1);
101
113
  });
102
114
 
103
- it('does not call onClick when disabled', () => {
115
+ it("does not call onClick when disabled", () => {
104
116
  const handleClick = vi.fn();
105
117
  render(
106
118
  <__COMPONENT_NAME__ disabled onClick={handleClick}>
107
119
  Disabled
108
- </__COMPONENT_NAME__>
120
+ </__COMPONENT_NAME__>,
109
121
  );
110
- fireEvent.click(screen.getByRole('button'));
122
+ fireEvent.click(screen.getByRole("button"));
111
123
  expect(handleClick).not.toHaveBeenCalled();
112
124
  });
113
125
 
114
- it('does not call onClick when loading', () => {
126
+ it("does not call onClick when loading", () => {
115
127
  const handleClick = vi.fn();
116
128
  render(
117
129
  <__COMPONENT_NAME__ loading onClick={handleClick}>
118
130
  Loading
119
- </__COMPONENT_NAME__>
131
+ </__COMPONENT_NAME__>,
120
132
  );
121
- fireEvent.click(screen.getByRole('button'));
133
+ fireEvent.click(screen.getByRole("button"));
122
134
  expect(handleClick).not.toHaveBeenCalled();
123
135
  });
124
136
  });
125
137
 
126
- describe('Accessibility', () => {
127
- it('is focusable', () => {
138
+ describe("Accessibility", () => {
139
+ it("is focusable", () => {
128
140
  render(<__COMPONENT_NAME__>Focus me</__COMPONENT_NAME__>);
129
- const button = screen.getByRole('button');
141
+ const button = screen.getByRole("button");
130
142
  button.focus();
131
143
  expect(button).toHaveFocus();
132
144
  });
133
145
 
134
- it('is not focusable when disabled', () => {
146
+ it("is not focusable when disabled", () => {
135
147
  render(<__COMPONENT_NAME__ disabled>Disabled</__COMPONENT_NAME__>);
136
- const button = screen.getByRole('button');
137
- expect(button).toHaveAttribute('disabled');
148
+ const button = screen.getByRole("button");
149
+ expect(button).toHaveAttribute("disabled");
138
150
  });
139
151
 
140
- it('has aria-busy when loading', () => {
152
+ it("has aria-busy when loading", () => {
141
153
  render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
142
- expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
154
+ expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true");
143
155
  });
144
156
  });
145
157
 
146
- describe('Custom className', () => {
147
- it('accepts custom className', () => {
158
+ describe("Custom className", () => {
159
+ it("accepts custom className", () => {
148
160
  const { container } = render(
149
- <__COMPONENT_NAME__ className="custom-class">Custom</__COMPONENT_NAME__>
161
+ <__COMPONENT_NAME__ className="custom-class">
162
+ Custom
163
+ </__COMPONENT_NAME__>,
150
164
  );
151
- expect(container.firstChild).toHaveClass('custom-class');
165
+ expect(container.firstChild).toHaveClass("custom-class");
152
166
  });
153
167
 
154
- it('merges custom className with variant classes', () => {
168
+ it("merges custom className with variant classes", () => {
155
169
  const { container } = render(
156
170
  <__COMPONENT_NAME__ variant="primary" className="custom-class">
157
171
  Merged
158
- </__COMPONENT_NAME__>
172
+ </__COMPONENT_NAME__>,
159
173
  );
160
- expect(container.firstChild).toHaveClass('bg-primary');
161
- expect(container.firstChild).toHaveClass('custom-class');
174
+ expect(container.firstChild).toHaveClass("bg-primary");
175
+ expect(container.firstChild).toHaveClass("custom-class");
162
176
  });
163
177
  });
164
178
 
@@ -168,8 +182,8 @@ describe('__COMPONENT_NAME__', () => {
168
182
  // Tests FAIL if exceeded, triggering TDD loop-back
169
183
  // ===================================
170
184
 
171
- describe('Performance', () => {
172
- it('should not re-render excessively on mount', () => {
185
+ describe("Performance", () => {
186
+ it("should not re-render excessively on mount", () => {
173
187
  let renderCount = 0;
174
188
 
175
189
  const TestWrapper = () => {
@@ -184,10 +198,14 @@ describe('__COMPONENT_NAME__', () => {
184
198
  expect(renderCount).toBeLessThanOrEqual(1);
185
199
  });
186
200
 
187
- it('should not re-render excessively on prop change', () => {
201
+ it("should not re-render excessively on prop change", () => {
188
202
  let renderCount = 0;
189
203
 
190
- const TestWrapper = ({ variant }: { variant: 'primary' | 'secondary' }) => {
204
+ const TestWrapper = ({
205
+ variant,
206
+ }: {
207
+ variant: "primary" | "secondary";
208
+ }) => {
191
209
  renderCount++;
192
210
  return <__COMPONENT_NAME__ variant={variant}>Test</__COMPONENT_NAME__>;
193
211
  };
@@ -202,10 +220,10 @@ describe('__COMPONENT_NAME__', () => {
202
220
  expect(renderCount).toBeLessThanOrEqual(1);
203
221
  });
204
222
 
205
- it('should not cause unnecessary re-renders with same props', () => {
223
+ it("should not cause unnecessary re-renders with same props", () => {
206
224
  let renderCount = 0;
207
225
 
208
- const TestWrapper = ({ variant }: { variant: 'primary' }) => {
226
+ const TestWrapper = ({ variant }: { variant: "primary" }) => {
209
227
  renderCount++;
210
228
  return <__COMPONENT_NAME__ variant={variant}>Test</__COMPONENT_NAME__>;
211
229
  };
@@ -222,7 +240,7 @@ describe('__COMPONENT_NAME__', () => {
222
240
  expect(renderCount).toBeLessThanOrEqual(1);
223
241
  });
224
242
 
225
- it('should render within time budget', () => {
243
+ it("should render within time budget", () => {
226
244
  const startTime = performance.now();
227
245
 
228
246
  render(<__COMPONENT_NAME__>Performance Test</__COMPONENT_NAME__>);
@@ -1,7 +1,7 @@
1
- import * as React from 'react';
2
- import { cva, type VariantProps } from 'class-variance-authority';
3
- import { cn } from '@/lib/utils';
4
- import type { __COMPONENT_NAME__Props } from './__COMPONENT_NAME__.types';
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+ import type { __COMPONENT_NAME__Props } from "./__COMPONENT_NAME__.types";
5
5
 
6
6
  /**
7
7
  * __COMPONENT_NAME__ variants using class-variance-authority
@@ -9,27 +9,30 @@ import type { __COMPONENT_NAME__Props } from './__COMPONENT_NAME__.types';
9
9
  */
10
10
  const __COMPONENT_NAME_LOWER__Variants = cva(
11
11
  // Base styles - apply brand guide values
12
- 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
12
+ "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
13
13
  {
14
14
  variants: {
15
15
  variant: {
16
- primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
17
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
18
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
19
- outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
20
- ghost: 'hover:bg-accent hover:text-accent-foreground',
16
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ destructive:
20
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
21
+ outline:
22
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
23
+ ghost: "hover:bg-accent hover:text-accent-foreground",
21
24
  },
22
25
  size: {
23
- sm: 'h-8 px-3 text-sm',
24
- md: 'h-10 px-4 text-base',
25
- lg: 'h-12 px-6 text-lg',
26
+ sm: "h-8 px-3 text-sm",
27
+ md: "h-10 px-4 text-base",
28
+ lg: "h-12 px-6 text-lg",
26
29
  },
27
30
  },
28
31
  defaultVariants: {
29
- variant: 'primary',
30
- size: 'md',
32
+ variant: "primary",
33
+ size: "md",
31
34
  },
32
- }
35
+ },
33
36
  );
34
37
 
35
38
  /**
@@ -47,40 +50,48 @@ const __COMPONENT_NAME_LOWER__Variants = cva(
47
50
  export const __COMPONENT_NAME__ = React.forwardRef<
48
51
  HTMLButtonElement,
49
52
  __COMPONENT_NAME__Props
50
- >(({ variant, size, loading, disabled, className, children, ...props }, ref) => {
51
- return (
52
- <button
53
- ref={ref}
54
- className={cn(__COMPONENT_NAME_LOWER__Variants({ variant, size }), className)}
55
- disabled={disabled || loading}
56
- aria-busy={loading || undefined}
57
- {...props}
58
- >
59
- {loading && (
60
- <svg
61
- className="mr-2 h-4 w-4 animate-spin"
62
- xmlns="http://www.w3.org/2000/svg"
63
- fill="none"
64
- viewBox="0 0 24 24"
65
- >
66
- <circle
67
- className="opacity-25"
68
- cx="12"
69
- cy="12"
70
- r="10"
71
- stroke="currentColor"
72
- strokeWidth="4"
73
- />
74
- <path
75
- className="opacity-75"
76
- fill="currentColor"
77
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
78
- />
79
- </svg>
80
- )}
81
- {children}
82
- </button>
83
- );
84
- });
53
+ >(
54
+ (
55
+ { variant, size, loading, disabled, className, children, ...props },
56
+ ref,
57
+ ) => {
58
+ return (
59
+ <button
60
+ ref={ref}
61
+ className={cn(
62
+ __COMPONENT_NAME_LOWER__Variants({ variant, size }),
63
+ className,
64
+ )}
65
+ disabled={disabled || loading}
66
+ aria-busy={loading || undefined}
67
+ {...props}
68
+ >
69
+ {loading && (
70
+ <svg
71
+ className="mr-2 h-4 w-4 animate-spin"
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ fill="none"
74
+ viewBox="0 0 24 24"
75
+ >
76
+ <circle
77
+ className="opacity-25"
78
+ cx="12"
79
+ cy="12"
80
+ r="10"
81
+ stroke="currentColor"
82
+ strokeWidth="4"
83
+ />
84
+ <path
85
+ className="opacity-75"
86
+ fill="currentColor"
87
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
88
+ />
89
+ </svg>
90
+ )}
91
+ {children}
92
+ </button>
93
+ );
94
+ },
95
+ );
85
96
 
86
- __COMPONENT_NAME__.displayName = '__COMPONENT_NAME__';
97
+ __COMPONENT_NAME__.displayName = "__COMPONENT_NAME__";
@@ -1,16 +1,21 @@
1
- import type { VariantProps } from 'class-variance-authority';
2
- import type { ComponentPropsWithoutRef } from 'react';
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithoutRef } from "react";
3
3
 
4
4
  /**
5
5
  * __COMPONENT_NAME__ variant configuration
6
6
  * Generated from interview decisions
7
7
  */
8
- export type __COMPONENT_NAME__Variant = 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost';
8
+ export type __COMPONENT_NAME__Variant =
9
+ | "primary"
10
+ | "secondary"
11
+ | "destructive"
12
+ | "outline"
13
+ | "ghost";
9
14
 
10
15
  /**
11
16
  * __COMPONENT_NAME__ size configuration
12
17
  */
13
- export type __COMPONENT_NAME__Size = 'sm' | 'md' | 'lg';
18
+ export type __COMPONENT_NAME__Size = "sm" | "md" | "lg";
14
19
 
15
20
  /**
16
21
  * Props for the __COMPONENT_NAME__ component
@@ -22,8 +27,7 @@ export type __COMPONENT_NAME__Size = 'sm' | 'md' | 'lg';
22
27
  * @property className - Additional CSS classes
23
28
  * @property children - Content to render inside the component
24
29
  */
25
- export interface __COMPONENT_NAME__Props
26
- extends ComponentPropsWithoutRef<'button'> {
30
+ export interface __COMPONENT_NAME__Props extends ComponentPropsWithoutRef<"button"> {
27
31
  /**
28
32
  * Visual style variant
29
33
  * @default 'primary'
@@ -0,0 +1,170 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ /**
4
+ * Visual Regression Tests for __COMPONENT_NAME__
5
+ *
6
+ * Created with Hustle UI Create workflow (v3.9.0)
7
+ *
8
+ * These tests capture screenshots of component variants in Storybook
9
+ * and compare them against baseline images.
10
+ *
11
+ * Run with: pnpm playwright test __COMPONENT_NAME__.visual.spec.ts
12
+ *
13
+ * Update baselines: pnpm playwright test --update-snapshots
14
+ */
15
+
16
+ const STORYBOOK_URL = process.env.STORYBOOK_URL || "http://localhost:6006";
17
+
18
+ test.describe("__COMPONENT_NAME__ Visual Regression", () => {
19
+ // ===================================
20
+ // Variant Screenshots
21
+ // ===================================
22
+
23
+ test("Primary variant matches baseline", async ({ page }) => {
24
+ await page.goto(
25
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
26
+ );
27
+ await page.waitForLoadState("networkidle");
28
+
29
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
30
+ "__COMPONENT_NAME__-primary.png",
31
+ );
32
+ });
33
+
34
+ test("Secondary variant matches baseline", async ({ page }) => {
35
+ await page.goto(
36
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--secondary&viewMode=story`,
37
+ );
38
+ await page.waitForLoadState("networkidle");
39
+
40
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
41
+ "__COMPONENT_NAME__-secondary.png",
42
+ );
43
+ });
44
+
45
+ test("Disabled state matches baseline", async ({ page }) => {
46
+ await page.goto(
47
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--disabled&viewMode=story`,
48
+ );
49
+ await page.waitForLoadState("networkidle");
50
+
51
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
52
+ "__COMPONENT_NAME__-disabled.png",
53
+ );
54
+ });
55
+
56
+ test("Loading state matches baseline", async ({ page }) => {
57
+ await page.goto(
58
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--loading&viewMode=story`,
59
+ );
60
+ await page.waitForLoadState("networkidle");
61
+
62
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
63
+ "__COMPONENT_NAME__-loading.png",
64
+ );
65
+ });
66
+
67
+ // ===================================
68
+ // Size Variants (if applicable)
69
+ // ===================================
70
+
71
+ test("Small size matches baseline", async ({ page }) => {
72
+ await page.goto(
73
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--small&viewMode=story`,
74
+ );
75
+ await page.waitForLoadState("networkidle");
76
+
77
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
78
+ "__COMPONENT_NAME__-small.png",
79
+ );
80
+ });
81
+
82
+ test("Large size matches baseline", async ({ page }) => {
83
+ await page.goto(
84
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--large&viewMode=story`,
85
+ );
86
+ await page.waitForLoadState("networkidle");
87
+
88
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
89
+ "__COMPONENT_NAME__-large.png",
90
+ );
91
+ });
92
+
93
+ // ===================================
94
+ // Responsive Viewport Tests
95
+ // ===================================
96
+
97
+ test("renders correctly on mobile viewport", async ({ page }) => {
98
+ await page.setViewportSize({ width: 375, height: 667 });
99
+ await page.goto(
100
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
101
+ );
102
+ await page.waitForLoadState("networkidle");
103
+
104
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
105
+ "__COMPONENT_NAME__-mobile.png",
106
+ );
107
+ });
108
+
109
+ test("renders correctly on tablet viewport", async ({ page }) => {
110
+ await page.setViewportSize({ width: 768, height: 1024 });
111
+ await page.goto(
112
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
113
+ );
114
+ await page.waitForLoadState("networkidle");
115
+
116
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
117
+ "__COMPONENT_NAME__-tablet.png",
118
+ );
119
+ });
120
+
121
+ // ===================================
122
+ // Interaction State Tests
123
+ // ===================================
124
+
125
+ test("hover state matches baseline", async ({ page }) => {
126
+ await page.goto(
127
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
128
+ );
129
+ await page.waitForLoadState("networkidle");
130
+
131
+ // Hover over the component
132
+ await page.locator("#storybook-root > *").first().hover();
133
+
134
+ // Wait for any hover transitions
135
+ await page.waitForTimeout(300);
136
+
137
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
138
+ "__COMPONENT_NAME__-hover.png",
139
+ );
140
+ });
141
+
142
+ test("focus state matches baseline", async ({ page }) => {
143
+ await page.goto(
144
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story`,
145
+ );
146
+ await page.waitForLoadState("networkidle");
147
+
148
+ // Focus the component via keyboard
149
+ await page.keyboard.press("Tab");
150
+
151
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
152
+ "__COMPONENT_NAME__-focus.png",
153
+ );
154
+ });
155
+
156
+ // ===================================
157
+ // Dark Mode Tests (if supported)
158
+ // ===================================
159
+
160
+ test("dark mode matches baseline", async ({ page }) => {
161
+ await page.goto(
162
+ `${STORYBOOK_URL}/iframe.html?id=components-__component_name__--primary&viewMode=story&globals=theme:dark`,
163
+ );
164
+ await page.waitForLoadState("networkidle");
165
+
166
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(
167
+ "__COMPONENT_NAME__-dark.png",
168
+ );
169
+ });
170
+ });
@@ -7,9 +7,9 @@
7
7
  * Created with Hustle UI Create workflow (v3.9.0)
8
8
  */
9
9
 
10
- export { __COMPONENT_NAME__ } from './__COMPONENT_NAME__';
10
+ export { __COMPONENT_NAME__ } from "./__COMPONENT_NAME__";
11
11
  export type {
12
12
  __COMPONENT_NAME__Props,
13
13
  __COMPONENT_NAME__Variant,
14
14
  __COMPONENT_NAME__Size,
15
- } from './__COMPONENT_NAME__.types';
15
+ } from "./__COMPONENT_NAME__.types";