@hustle-together/api-dev-tools 3.11.1 → 3.12.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/.claude/agents/code-reviewer.md +170 -0
- package/.claude/agents/docs-generator.md +80 -0
- package/.claude/agents/implementation-reviewer.md +119 -0
- package/.claude/agents/parallel-researcher.md +52 -0
- package/.claude/agents/research-validator.md +116 -0
- package/.claude/agents/schema-generator.md +70 -0
- package/.claude/agents/test-writer.md +104 -0
- package/.claude/api-dev-state.json +305 -56
- package/.claude/commands/README.md +21 -10
- package/.claude/commands/add-command.md +8 -5
- package/.claude/commands/api-create.md +36 -25
- package/.claude/commands/api-env.md +1 -0
- package/.claude/commands/api-interview.md +32 -19
- package/.claude/commands/api-research.md +47 -21
- package/.claude/commands/api-status.md +21 -1
- package/.claude/commands/api-verify.md +14 -13
- package/.claude/commands/beepboop.md +4 -5
- package/.claude/commands/busycommit.md +2 -3
- package/.claude/commands/commit.md +2 -3
- package/.claude/commands/cycle.md +2 -7
- package/.claude/commands/gap.md +2 -3
- package/.claude/commands/green.md +2 -7
- package/.claude/commands/issue.md +3 -8
- package/.claude/commands/ntfy-setup.md +91 -0
- package/.claude/commands/ntfy-test.md +74 -0
- package/.claude/commands/plan.md +2 -3
- package/.claude/commands/pr.md +2 -3
- package/.claude/commands/publish.md +40 -0
- package/.claude/commands/red.md +2 -7
- package/.claude/commands/refactor.md +2 -7
- package/.claude/commands/spike.md +2 -7
- package/.claude/commands/summarize.md +2 -3
- package/.claude/commands/tdd.md +2 -7
- package/.claude/commands/worktree-add.md +208 -216
- package/.claude/commands/worktree-cleanup.md +172 -178
- package/.claude/settings.json +63 -12
- package/.claude/settings.local.json +2 -1
- package/.claude-plugin/marketplace.json +2 -11
- package/.skills/README.md +55 -53
- package/.skills/_shared/settings.json +1 -1
- package/.skills/add-command/SKILL.md +10 -5
- package/.skills/api-create/SKILL.md +146 -35
- package/.skills/api-env/SKILL.md +1 -0
- package/.skills/api-interview/SKILL.md +32 -19
- package/.skills/api-research/SKILL.md +47 -21
- package/.skills/api-status/SKILL.md +21 -1
- package/.skills/api-verify/SKILL.md +14 -13
- package/.skills/beepboop/SKILL.md +6 -5
- package/.skills/busycommit/SKILL.md +4 -3
- package/.skills/commit/SKILL.md +4 -3
- package/.skills/cycle/SKILL.md +4 -7
- package/.skills/gap/SKILL.md +4 -3
- package/.skills/green/SKILL.md +4 -7
- package/.skills/issue/SKILL.md +5 -8
- package/.skills/plan/SKILL.md +4 -3
- package/.skills/pr/SKILL.md +4 -3
- package/.skills/publish/SKILL.md +160 -0
- package/.skills/red/SKILL.md +4 -7
- package/.skills/refactor/SKILL.md +4 -7
- package/.skills/spike/SKILL.md +4 -7
- package/.skills/summarize/SKILL.md +4 -3
- package/.skills/tdd/SKILL.md +4 -7
- package/.skills/update-todos/SKILL.md +22 -0
- package/.skills/worktree-add/SKILL.md +210 -216
- package/.skills/worktree-cleanup/SKILL.md +183 -187
- package/CHANGELOG.md +97 -79
- package/README.md +161 -7142
- package/bin/cli.js +448 -805
- package/commands/README.md +66 -31
- package/commands/add-command.md +8 -5
- package/commands/beepboop.md +4 -5
- package/commands/busycommit.md +2 -3
- package/commands/commit.md +2 -3
- package/commands/cycle.md +2 -7
- package/commands/gap.md +2 -3
- package/commands/green.md +2 -7
- package/commands/hustle-api-continue.md +8 -5
- package/commands/hustle-api-create.md +70 -29
- package/commands/hustle-api-env.md +1 -0
- package/commands/hustle-api-interview.md +32 -19
- package/commands/hustle-api-research.md +47 -21
- package/commands/hustle-api-sessions.md +8 -7
- package/commands/hustle-api-status.md +21 -1
- package/commands/hustle-api-verify.md +14 -13
- package/commands/hustle-combine.md +488 -241
- package/commands/hustle-ui-create-page.md +113 -50
- package/commands/hustle-ui-create.md +179 -26
- package/commands/issue.md +3 -8
- package/commands/plan.md +2 -3
- package/commands/pr.md +2 -3
- package/commands/red.md +2 -7
- package/commands/refactor.md +2 -7
- package/commands/spike.md +2 -7
- package/commands/summarize.md +2 -3
- package/commands/tdd.md +2 -7
- package/commands/worktree-add.md +208 -216
- package/commands/worktree-cleanup.md +172 -178
- package/hooks/api-workflow-check.py +5 -3
- package/hooks/enforce-component-type-confirm.py +97 -0
- package/hooks/lib/__init__.py +1 -0
- package/hooks/lib/greptile.py +355 -0
- package/hooks/lib/ntfy.py +209 -0
- package/hooks/notify-input-needed.py +73 -0
- package/hooks/notify-phase-complete.py +90 -0
- package/hooks/run-code-review.py +246 -0
- package/hooks/track-token-usage.py +121 -0
- package/package.json +13 -3
- package/scripts/collect-test-results.ts +102 -77
- package/scripts/extract-parameters.ts +112 -70
- package/scripts/generate-test-manifest.ts +118 -77
- package/templates/.env.example +57 -0
- package/templates/BRAND_GUIDE.md +92 -52
- package/templates/CLAUDE-SECTION.md +40 -37
- package/templates/SPEC.json +186 -38
- package/templates/api-dev-state.json +33 -4
- package/templates/api-showcase/_components/APICard.tsx +22 -18
- package/templates/api-showcase/_components/APIModal.tsx +110 -64
- package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
- package/templates/api-showcase/_components/APITester.tsx +128 -67
- package/templates/api-showcase/page.tsx +4 -4
- package/templates/api-test/page.tsx +51 -30
- package/templates/api-test/test-structure/route.ts +43 -34
- package/templates/component/Component.stories.tsx +41 -39
- package/templates/component/Component.test.tsx +96 -78
- package/templates/component/Component.tsx +63 -52
- package/templates/component/Component.types.ts +10 -6
- package/templates/component/Component.visual.spec.ts +170 -0
- package/templates/component/index.ts +2 -2
- package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
- package/templates/dev-tools/page.tsx +4 -3
- package/templates/mcp-servers.json +30 -2
- package/templates/page/page.e2e.test.ts +56 -48
- package/templates/page/page.tsx +3 -3
- package/templates/shared/HeroHeader.tsx +16 -15
- package/templates/shared/index.ts +1 -1
- package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
- package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
- package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
- package/templates/ui-showcase/page.tsx +4 -4
|
@@ -1,164 +1,178 @@
|
|
|
1
|
-
import { render, screen, fireEvent } from
|
|
2
|
-
import { describe, it, expect, vi } from
|
|
3
|
-
import { __COMPONENT_NAME__ } from
|
|
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(
|
|
6
|
-
describe(
|
|
7
|
-
it(
|
|
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(
|
|
9
|
+
expect(screen.getByText("Test Content")).toBeInTheDocument();
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it(
|
|
12
|
+
it("renders as a button element", () => {
|
|
13
13
|
render(<__COMPONENT_NAME__>Click me</__COMPONENT_NAME__>);
|
|
14
|
-
expect(screen.getByRole(
|
|
14
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it(
|
|
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(
|
|
25
|
-
it(
|
|
26
|
-
const { container } = render(
|
|
27
|
-
|
|
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(
|
|
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(
|
|
36
|
+
expect(container.firstChild).toHaveClass("bg-secondary");
|
|
35
37
|
});
|
|
36
38
|
|
|
37
|
-
it(
|
|
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(
|
|
43
|
+
expect(container.firstChild).toHaveClass("bg-destructive");
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
it(
|
|
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(
|
|
50
|
+
expect(container.firstChild).toHaveClass("border");
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
it(
|
|
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(
|
|
57
|
+
expect(container.firstChild).toHaveClass("hover:bg-accent");
|
|
56
58
|
});
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
describe(
|
|
60
|
-
it(
|
|
61
|
-
const { container } = render(
|
|
62
|
-
|
|
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(
|
|
66
|
-
const { container } = render(
|
|
67
|
-
|
|
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(
|
|
71
|
-
const { container } = render(
|
|
72
|
-
|
|
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(
|
|
77
|
-
it(
|
|
84
|
+
describe("States", () => {
|
|
85
|
+
it("handles disabled state", () => {
|
|
78
86
|
render(<__COMPONENT_NAME__ disabled>Disabled</__COMPONENT_NAME__>);
|
|
79
|
-
expect(screen.getByRole(
|
|
87
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
80
88
|
});
|
|
81
89
|
|
|
82
|
-
it(
|
|
90
|
+
it("handles loading state", () => {
|
|
83
91
|
render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
|
|
84
|
-
const button = screen.getByRole(
|
|
92
|
+
const button = screen.getByRole("button");
|
|
85
93
|
expect(button).toBeDisabled();
|
|
86
|
-
expect(button).toHaveAttribute(
|
|
94
|
+
expect(button).toHaveAttribute("aria-busy", "true");
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
it(
|
|
90
|
-
const { container } = render(
|
|
91
|
-
|
|
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(
|
|
96
|
-
it(
|
|
105
|
+
describe("Interaction", () => {
|
|
106
|
+
it("calls onClick when clicked", () => {
|
|
97
107
|
const handleClick = vi.fn();
|
|
98
|
-
render(
|
|
99
|
-
|
|
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(
|
|
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(
|
|
122
|
+
fireEvent.click(screen.getByRole("button"));
|
|
111
123
|
expect(handleClick).not.toHaveBeenCalled();
|
|
112
124
|
});
|
|
113
125
|
|
|
114
|
-
it(
|
|
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(
|
|
133
|
+
fireEvent.click(screen.getByRole("button"));
|
|
122
134
|
expect(handleClick).not.toHaveBeenCalled();
|
|
123
135
|
});
|
|
124
136
|
});
|
|
125
137
|
|
|
126
|
-
describe(
|
|
127
|
-
it(
|
|
138
|
+
describe("Accessibility", () => {
|
|
139
|
+
it("is focusable", () => {
|
|
128
140
|
render(<__COMPONENT_NAME__>Focus me</__COMPONENT_NAME__>);
|
|
129
|
-
const button = screen.getByRole(
|
|
141
|
+
const button = screen.getByRole("button");
|
|
130
142
|
button.focus();
|
|
131
143
|
expect(button).toHaveFocus();
|
|
132
144
|
});
|
|
133
145
|
|
|
134
|
-
it(
|
|
146
|
+
it("is not focusable when disabled", () => {
|
|
135
147
|
render(<__COMPONENT_NAME__ disabled>Disabled</__COMPONENT_NAME__>);
|
|
136
|
-
const button = screen.getByRole(
|
|
137
|
-
expect(button).toHaveAttribute(
|
|
148
|
+
const button = screen.getByRole("button");
|
|
149
|
+
expect(button).toHaveAttribute("disabled");
|
|
138
150
|
});
|
|
139
151
|
|
|
140
|
-
it(
|
|
152
|
+
it("has aria-busy when loading", () => {
|
|
141
153
|
render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
|
|
142
|
-
expect(screen.getByRole(
|
|
154
|
+
expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true");
|
|
143
155
|
});
|
|
144
156
|
});
|
|
145
157
|
|
|
146
|
-
describe(
|
|
147
|
-
it(
|
|
158
|
+
describe("Custom className", () => {
|
|
159
|
+
it("accepts custom className", () => {
|
|
148
160
|
const { container } = render(
|
|
149
|
-
<__COMPONENT_NAME__ className="custom-class">
|
|
161
|
+
<__COMPONENT_NAME__ className="custom-class">
|
|
162
|
+
Custom
|
|
163
|
+
</__COMPONENT_NAME__>,
|
|
150
164
|
);
|
|
151
|
-
expect(container.firstChild).toHaveClass(
|
|
165
|
+
expect(container.firstChild).toHaveClass("custom-class");
|
|
152
166
|
});
|
|
153
167
|
|
|
154
|
-
it(
|
|
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(
|
|
161
|
-
expect(container.firstChild).toHaveClass(
|
|
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(
|
|
172
|
-
it(
|
|
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(
|
|
201
|
+
it("should not re-render excessively on prop change", () => {
|
|
188
202
|
let renderCount = 0;
|
|
189
203
|
|
|
190
|
-
const TestWrapper = ({
|
|
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(
|
|
223
|
+
it("should not cause unnecessary re-renders with same props", () => {
|
|
206
224
|
let renderCount = 0;
|
|
207
225
|
|
|
208
|
-
const TestWrapper = ({ variant }: { variant:
|
|
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(
|
|
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
|
|
2
|
-
import { cva, type VariantProps } from
|
|
3
|
-
import { cn } from
|
|
4
|
-
import type { __COMPONENT_NAME__Props } from
|
|
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
|
-
|
|
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:
|
|
17
|
-
secondary:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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:
|
|
24
|
-
md:
|
|
25
|
-
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:
|
|
30
|
-
size:
|
|
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
|
-
>(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
97
|
+
__COMPONENT_NAME__.displayName = "__COMPONENT_NAME__";
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import type { VariantProps } from
|
|
2
|
-
import type { ComponentPropsWithoutRef } from
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
15
|
+
} from "./__COMPONENT_NAME__.types";
|