@hustle-together/api-dev-tools 3.6.5 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5599 -258
- package/bin/cli.js +395 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +35 -15
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create-page.md +933 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +545 -21
- package/hooks/cache-research.py +337 -0
- package/hooks/check-api-routes.py +168 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-a11y-audit.py +202 -0
- package/hooks/enforce-brand-guide.py +241 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-page-components.py +186 -0
- package/hooks/enforce-page-data-schema.py +155 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +1161 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +160 -15
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +212 -0
- package/package.json +8 -3
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +217 -161
- package/templates/api-showcase/_components/APICard.tsx +153 -0
- package/templates/api-showcase/_components/APIModal.tsx +375 -0
- package/templates/api-showcase/_components/APIShowcase.tsx +231 -0
- package/templates/api-showcase/_components/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +90 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/_components/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/_components/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
- package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
- package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
- package/demo/hustle-together/index.html +0 -1312
- package/demo/workflow-demo-v3.5-backup.html +0 -5008
- package/demo/workflow-demo.html +0 -6202
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { __COMPONENT_NAME__ } from './__COMPONENT_NAME__';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* __COMPONENT_NAME__ - __COMPONENT_DESCRIPTION__
|
|
6
|
+
*
|
|
7
|
+
* This component was created using the Hustle UI Create workflow.
|
|
8
|
+
*/
|
|
9
|
+
const meta: Meta<typeof __COMPONENT_NAME__> = {
|
|
10
|
+
title: 'Components/__COMPONENT_NAME__',
|
|
11
|
+
component: __COMPONENT_NAME__,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component: '__COMPONENT_DESCRIPTION__',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
tags: ['autodocs'],
|
|
21
|
+
argTypes: {
|
|
22
|
+
variant: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['primary', 'secondary', 'destructive', 'outline', 'ghost'],
|
|
25
|
+
description: 'Visual style variant',
|
|
26
|
+
table: {
|
|
27
|
+
defaultValue: { summary: 'primary' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
size: {
|
|
31
|
+
control: 'select',
|
|
32
|
+
options: ['sm', 'md', 'lg'],
|
|
33
|
+
description: 'Size variant',
|
|
34
|
+
table: {
|
|
35
|
+
defaultValue: { summary: 'md' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
loading: {
|
|
39
|
+
control: 'boolean',
|
|
40
|
+
description: 'Shows loading spinner',
|
|
41
|
+
table: {
|
|
42
|
+
defaultValue: { summary: 'false' },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
disabled: {
|
|
46
|
+
control: 'boolean',
|
|
47
|
+
description: 'Disables the component',
|
|
48
|
+
table: {
|
|
49
|
+
defaultValue: { summary: 'false' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
children: {
|
|
53
|
+
control: 'text',
|
|
54
|
+
description: 'Content inside the component',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default meta;
|
|
60
|
+
type Story = StoryObj<typeof meta>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Default primary variant
|
|
64
|
+
*/
|
|
65
|
+
export const Primary: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
variant: 'primary',
|
|
68
|
+
children: 'Primary __COMPONENT_NAME__',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Secondary variant for less prominent actions
|
|
74
|
+
*/
|
|
75
|
+
export const Secondary: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
variant: 'secondary',
|
|
78
|
+
children: 'Secondary __COMPONENT_NAME__',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Destructive variant for dangerous actions
|
|
84
|
+
*/
|
|
85
|
+
export const Destructive: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
variant: 'destructive',
|
|
88
|
+
children: 'Delete Item',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Outline variant with border
|
|
94
|
+
*/
|
|
95
|
+
export const Outline: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
variant: 'outline',
|
|
98
|
+
children: 'Outline __COMPONENT_NAME__',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Ghost variant with no background
|
|
104
|
+
*/
|
|
105
|
+
export const Ghost: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
variant: 'ghost',
|
|
108
|
+
children: 'Ghost __COMPONENT_NAME__',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Small size variant
|
|
114
|
+
*/
|
|
115
|
+
export const Small: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
size: 'sm',
|
|
118
|
+
children: 'Small __COMPONENT_NAME__',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Large size variant
|
|
124
|
+
*/
|
|
125
|
+
export const Large: Story = {
|
|
126
|
+
args: {
|
|
127
|
+
size: 'lg',
|
|
128
|
+
children: 'Large __COMPONENT_NAME__',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Loading state with spinner
|
|
134
|
+
*/
|
|
135
|
+
export const Loading: Story = {
|
|
136
|
+
args: {
|
|
137
|
+
loading: true,
|
|
138
|
+
children: 'Loading...',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Disabled state
|
|
144
|
+
*/
|
|
145
|
+
export const Disabled: Story = {
|
|
146
|
+
args: {
|
|
147
|
+
disabled: true,
|
|
148
|
+
children: 'Disabled __COMPONENT_NAME__',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* All variants displayed together
|
|
154
|
+
*/
|
|
155
|
+
export const AllVariants: Story = {
|
|
156
|
+
render: () => (
|
|
157
|
+
<div className="flex flex-col gap-4">
|
|
158
|
+
<div className="flex gap-2">
|
|
159
|
+
<__COMPONENT_NAME__ variant="primary">Primary</__COMPONENT_NAME__>
|
|
160
|
+
<__COMPONENT_NAME__ variant="secondary">Secondary</__COMPONENT_NAME__>
|
|
161
|
+
<__COMPONENT_NAME__ variant="destructive">Destructive</__COMPONENT_NAME__>
|
|
162
|
+
<__COMPONENT_NAME__ variant="outline">Outline</__COMPONENT_NAME__>
|
|
163
|
+
<__COMPONENT_NAME__ variant="ghost">Ghost</__COMPONENT_NAME__>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
<__COMPONENT_NAME__ size="sm">Small</__COMPONENT_NAME__>
|
|
167
|
+
<__COMPONENT_NAME__ size="md">Medium</__COMPONENT_NAME__>
|
|
168
|
+
<__COMPONENT_NAME__ size="lg">Large</__COMPONENT_NAME__>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
),
|
|
172
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
|
|
5
|
+
describe('__COMPONENT_NAME__', () => {
|
|
6
|
+
describe('Rendering', () => {
|
|
7
|
+
it('renders children correctly', () => {
|
|
8
|
+
render(<__COMPONENT_NAME__>Test Content</__COMPONENT_NAME__>);
|
|
9
|
+
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders as a button element', () => {
|
|
13
|
+
render(<__COMPONENT_NAME__>Click me</__COMPONENT_NAME__>);
|
|
14
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('forwards ref correctly', () => {
|
|
18
|
+
const ref = vi.fn();
|
|
19
|
+
render(<__COMPONENT_NAME__ ref={ref}>Test</__COMPONENT_NAME__>);
|
|
20
|
+
expect(ref).toHaveBeenCalled();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
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');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('applies secondary variant', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<__COMPONENT_NAME__ variant="secondary">Secondary</__COMPONENT_NAME__>
|
|
33
|
+
);
|
|
34
|
+
expect(container.firstChild).toHaveClass('bg-secondary');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies destructive variant', () => {
|
|
38
|
+
const { container } = render(
|
|
39
|
+
<__COMPONENT_NAME__ variant="destructive">Delete</__COMPONENT_NAME__>
|
|
40
|
+
);
|
|
41
|
+
expect(container.firstChild).toHaveClass('bg-destructive');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('applies outline variant', () => {
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<__COMPONENT_NAME__ variant="outline">Outline</__COMPONENT_NAME__>
|
|
47
|
+
);
|
|
48
|
+
expect(container.firstChild).toHaveClass('border');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('applies ghost variant', () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<__COMPONENT_NAME__ variant="ghost">Ghost</__COMPONENT_NAME__>
|
|
54
|
+
);
|
|
55
|
+
expect(container.firstChild).toHaveClass('hover:bg-accent');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
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');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('applies small size', () => {
|
|
66
|
+
const { container } = render(<__COMPONENT_NAME__ size="sm">Small</__COMPONENT_NAME__>);
|
|
67
|
+
expect(container.firstChild).toHaveClass('h-8');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('applies large size', () => {
|
|
71
|
+
const { container } = render(<__COMPONENT_NAME__ size="lg">Large</__COMPONENT_NAME__>);
|
|
72
|
+
expect(container.firstChild).toHaveClass('h-12');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('States', () => {
|
|
77
|
+
it('handles disabled state', () => {
|
|
78
|
+
render(<__COMPONENT_NAME__ disabled>Disabled</__COMPONENT_NAME__>);
|
|
79
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('handles loading state', () => {
|
|
83
|
+
render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
|
|
84
|
+
const button = screen.getByRole('button');
|
|
85
|
+
expect(button).toBeDisabled();
|
|
86
|
+
expect(button).toHaveAttribute('aria-busy', 'true');
|
|
87
|
+
});
|
|
88
|
+
|
|
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();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Interaction', () => {
|
|
96
|
+
it('calls onClick when clicked', () => {
|
|
97
|
+
const handleClick = vi.fn();
|
|
98
|
+
render(<__COMPONENT_NAME__ onClick={handleClick}>Click</__COMPONENT_NAME__>);
|
|
99
|
+
fireEvent.click(screen.getByRole('button'));
|
|
100
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not call onClick when disabled', () => {
|
|
104
|
+
const handleClick = vi.fn();
|
|
105
|
+
render(
|
|
106
|
+
<__COMPONENT_NAME__ disabled onClick={handleClick}>
|
|
107
|
+
Disabled
|
|
108
|
+
</__COMPONENT_NAME__>
|
|
109
|
+
);
|
|
110
|
+
fireEvent.click(screen.getByRole('button'));
|
|
111
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('does not call onClick when loading', () => {
|
|
115
|
+
const handleClick = vi.fn();
|
|
116
|
+
render(
|
|
117
|
+
<__COMPONENT_NAME__ loading onClick={handleClick}>
|
|
118
|
+
Loading
|
|
119
|
+
</__COMPONENT_NAME__>
|
|
120
|
+
);
|
|
121
|
+
fireEvent.click(screen.getByRole('button'));
|
|
122
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Accessibility', () => {
|
|
127
|
+
it('is focusable', () => {
|
|
128
|
+
render(<__COMPONENT_NAME__>Focus me</__COMPONENT_NAME__>);
|
|
129
|
+
const button = screen.getByRole('button');
|
|
130
|
+
button.focus();
|
|
131
|
+
expect(button).toHaveFocus();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('is not focusable when disabled', () => {
|
|
135
|
+
render(<__COMPONENT_NAME__ disabled>Disabled</__COMPONENT_NAME__>);
|
|
136
|
+
const button = screen.getByRole('button');
|
|
137
|
+
expect(button).toHaveAttribute('disabled');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('has aria-busy when loading', () => {
|
|
141
|
+
render(<__COMPONENT_NAME__ loading>Loading</__COMPONENT_NAME__>);
|
|
142
|
+
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Custom className', () => {
|
|
147
|
+
it('accepts custom className', () => {
|
|
148
|
+
const { container } = render(
|
|
149
|
+
<__COMPONENT_NAME__ className="custom-class">Custom</__COMPONENT_NAME__>
|
|
150
|
+
);
|
|
151
|
+
expect(container.firstChild).toHaveClass('custom-class');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('merges custom className with variant classes', () => {
|
|
155
|
+
const { container } = render(
|
|
156
|
+
<__COMPONENT_NAME__ variant="primary" className="custom-class">
|
|
157
|
+
Merged
|
|
158
|
+
</__COMPONENT_NAME__>
|
|
159
|
+
);
|
|
160
|
+
expect(container.firstChild).toHaveClass('bg-primary');
|
|
161
|
+
expect(container.firstChild).toHaveClass('custom-class');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ===================================
|
|
166
|
+
// Performance Tests (TDD GATES)
|
|
167
|
+
// These thresholds match .claude/performance-budgets.json
|
|
168
|
+
// Tests FAIL if exceeded, triggering TDD loop-back
|
|
169
|
+
// ===================================
|
|
170
|
+
|
|
171
|
+
describe('Performance', () => {
|
|
172
|
+
it('should not re-render excessively on mount', () => {
|
|
173
|
+
let renderCount = 0;
|
|
174
|
+
|
|
175
|
+
const TestWrapper = () => {
|
|
176
|
+
renderCount++;
|
|
177
|
+
return <__COMPONENT_NAME__>Test</__COMPONENT_NAME__>;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
render(<TestWrapper />);
|
|
181
|
+
|
|
182
|
+
// THRESHOLD: Mount renders max 1
|
|
183
|
+
// If this fails, check for: useEffect dependencies, state initialization
|
|
184
|
+
expect(renderCount).toBeLessThanOrEqual(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should not re-render excessively on prop change', () => {
|
|
188
|
+
let renderCount = 0;
|
|
189
|
+
|
|
190
|
+
const TestWrapper = ({ variant }: { variant: 'primary' | 'secondary' }) => {
|
|
191
|
+
renderCount++;
|
|
192
|
+
return <__COMPONENT_NAME__ variant={variant}>Test</__COMPONENT_NAME__>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const { rerender } = render(<TestWrapper variant="primary" />);
|
|
196
|
+
renderCount = 0; // Reset after initial render
|
|
197
|
+
|
|
198
|
+
rerender(<TestWrapper variant="secondary" />);
|
|
199
|
+
|
|
200
|
+
// THRESHOLD: Prop change renders max 1
|
|
201
|
+
// If this fails, check for: missing useMemo/useCallback, unstable references
|
|
202
|
+
expect(renderCount).toBeLessThanOrEqual(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not cause unnecessary re-renders with same props', () => {
|
|
206
|
+
let renderCount = 0;
|
|
207
|
+
|
|
208
|
+
const TestWrapper = ({ variant }: { variant: 'primary' }) => {
|
|
209
|
+
renderCount++;
|
|
210
|
+
return <__COMPONENT_NAME__ variant={variant}>Test</__COMPONENT_NAME__>;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const { rerender } = render(<TestWrapper variant="primary" />);
|
|
214
|
+
renderCount = 0; // Reset after initial render
|
|
215
|
+
|
|
216
|
+
// Rerender with SAME props
|
|
217
|
+
rerender(<TestWrapper variant="primary" />);
|
|
218
|
+
|
|
219
|
+
// THRESHOLD: Same-prop renders max 0 (should be memoized)
|
|
220
|
+
// If this fails, consider wrapping component with React.memo
|
|
221
|
+
// Note: This is a warning, not a hard fail in some cases
|
|
222
|
+
expect(renderCount).toBeLessThanOrEqual(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should render within time budget', () => {
|
|
226
|
+
const startTime = performance.now();
|
|
227
|
+
|
|
228
|
+
render(<__COMPONENT_NAME__>Performance Test</__COMPONENT_NAME__>);
|
|
229
|
+
|
|
230
|
+
const renderTime = performance.now() - startTime;
|
|
231
|
+
|
|
232
|
+
// THRESHOLD: Initial render max 16ms (one frame at 60fps)
|
|
233
|
+
// If this fails, check for: expensive computations, large DOM trees
|
|
234
|
+
expect(renderTime).toBeLessThan(16);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* __COMPONENT_NAME__ variants using class-variance-authority
|
|
8
|
+
* Customize variants based on interview decisions
|
|
9
|
+
*/
|
|
10
|
+
const __COMPONENT_NAME_LOWER__Variants = cva(
|
|
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',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
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',
|
|
21
|
+
},
|
|
22
|
+
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
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: 'primary',
|
|
30
|
+
size: 'md',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* __COMPONENT_NAME__ component
|
|
37
|
+
*
|
|
38
|
+
* @description __COMPONENT_DESCRIPTION__
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* <__COMPONENT_NAME__ variant="primary" size="md">
|
|
43
|
+
* Click me
|
|
44
|
+
* </__COMPONENT_NAME__>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const __COMPONENT_NAME__ = React.forwardRef<
|
|
48
|
+
HTMLButtonElement,
|
|
49
|
+
__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
|
+
});
|
|
85
|
+
|
|
86
|
+
__COMPONENT_NAME__.displayName = '__COMPONENT_NAME__';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* __COMPONENT_NAME__ variant configuration
|
|
6
|
+
* Generated from interview decisions
|
|
7
|
+
*/
|
|
8
|
+
export type __COMPONENT_NAME__Variant = 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* __COMPONENT_NAME__ size configuration
|
|
12
|
+
*/
|
|
13
|
+
export type __COMPONENT_NAME__Size = 'sm' | 'md' | 'lg';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props for the __COMPONENT_NAME__ component
|
|
17
|
+
*
|
|
18
|
+
* @property variant - Visual style variant
|
|
19
|
+
* @property size - Size variant
|
|
20
|
+
* @property loading - Shows loading spinner and disables interaction
|
|
21
|
+
* @property disabled - Disables the component
|
|
22
|
+
* @property className - Additional CSS classes
|
|
23
|
+
* @property children - Content to render inside the component
|
|
24
|
+
*/
|
|
25
|
+
export interface __COMPONENT_NAME__Props
|
|
26
|
+
extends ComponentPropsWithoutRef<'button'> {
|
|
27
|
+
/**
|
|
28
|
+
* Visual style variant
|
|
29
|
+
* @default 'primary'
|
|
30
|
+
*/
|
|
31
|
+
variant?: __COMPONENT_NAME__Variant;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Size variant
|
|
35
|
+
* @default 'md'
|
|
36
|
+
*/
|
|
37
|
+
size?: __COMPONENT_NAME__Size;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shows loading spinner and disables interaction
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
loading?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Disables the component
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Content to render inside the component
|
|
53
|
+
*/
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* __COMPONENT_NAME__ Component
|
|
3
|
+
*
|
|
4
|
+
* @description __COMPONENT_DESCRIPTION__
|
|
5
|
+
* @see {@link ./README.md} for usage documentation
|
|
6
|
+
*
|
|
7
|
+
* Created with Hustle UI Create workflow (v3.9.0)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { __COMPONENT_NAME__ } from './__COMPONENT_NAME__';
|
|
11
|
+
export type {
|
|
12
|
+
__COMPONENT_NAME__Props,
|
|
13
|
+
__COMPONENT_NAME__Variant,
|
|
14
|
+
__COMPONENT_NAME__Size,
|
|
15
|
+
} from './__COMPONENT_NAME__.types';
|