@pattern-stack/frontend-patterns 0.0.1 → 0.0.3
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 +6 -6
- package/package.json +3 -5
- package/src/App.css +0 -42
- package/src/App.tsx +0 -54
- package/src/__tests__/README.md +0 -221
- package/src/__tests__/atoms/hooks/simple-hooks.test.ts +0 -44
- package/src/__tests__/atoms/ui/button.test.tsx +0 -68
- package/src/__tests__/atoms/utils/simple.test.ts +0 -18
- package/src/__tests__/atoms/utils/utils.test.ts +0 -77
- package/src/__tests__/features/auth/simple-auth.test.tsx +0 -40
- package/src/__tests__/molecules/layout/simple-layout.test.tsx +0 -81
- package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +0 -167
- package/src/__tests__/setup.ts +0 -51
- package/src/__tests__/utils.tsx +0 -123
- package/src/atoms/composed/Accordion/Accordion.tsx +0 -271
- package/src/atoms/composed/Accordion/index.ts +0 -1
- package/src/atoms/composed/Alert/Alert.tsx +0 -132
- package/src/atoms/composed/Alert/index.ts +0 -1
- package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +0 -83
- package/src/atoms/composed/Breadcrumb/index.ts +0 -1
- package/src/atoms/composed/Chart/Chart.tsx +0 -425
- package/src/atoms/composed/Chart/index.ts +0 -2
- package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +0 -72
- package/src/atoms/composed/ColorSwatch/index.ts +0 -1
- package/src/atoms/composed/DarkModeToggle.tsx +0 -66
- package/src/atoms/composed/DataBadge/DataBadge.tsx +0 -81
- package/src/atoms/composed/DataBadge/index.ts +0 -1
- package/src/atoms/composed/DataTable/DataTable.tsx +0 -394
- package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +0 -41
- package/src/atoms/composed/DataTable/index.ts +0 -2
- package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +0 -611
- package/src/atoms/composed/DateTimePicker/index.ts +0 -2
- package/src/atoms/composed/DetailedCard/DetailedCard.tsx +0 -181
- package/src/atoms/composed/DetailedCard/index.ts +0 -2
- package/src/atoms/composed/EmptyState/EmptyState.tsx +0 -90
- package/src/atoms/composed/EmptyState/index.ts +0 -1
- package/src/atoms/composed/FileUpload/FileUpload.tsx +0 -477
- package/src/atoms/composed/FileUpload/index.ts +0 -2
- package/src/atoms/composed/FormField/FormField.tsx +0 -92
- package/src/atoms/composed/FormField/index.ts +0 -1
- package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +0 -37
- package/src/atoms/composed/GlobalSearch/index.ts +0 -1
- package/src/atoms/composed/IconBadge/IconBadge.tsx +0 -95
- package/src/atoms/composed/IconBadge/index.ts +0 -2
- package/src/atoms/composed/Modal/Modal.tsx +0 -223
- package/src/atoms/composed/Modal/index.ts +0 -2
- package/src/atoms/composed/PaletteSwitcher.tsx +0 -386
- package/src/atoms/composed/ProgressBar/ProgressBar.tsx +0 -116
- package/src/atoms/composed/ProgressBar/index.ts +0 -1
- package/src/atoms/composed/StatCard/StatCard.tsx +0 -219
- package/src/atoms/composed/StatCard/index.ts +0 -1
- package/src/atoms/composed/StyleGuide.tsx +0 -717
- package/src/atoms/composed/Toast/Toast.tsx +0 -219
- package/src/atoms/composed/Toast/index.ts +0 -1
- package/src/atoms/composed/Tooltip/Tooltip.tsx +0 -213
- package/src/atoms/composed/Tooltip/index.ts +0 -1
- package/src/atoms/composed/UserAvatar/UserAvatar.tsx +0 -139
- package/src/atoms/composed/UserAvatar/index.ts +0 -1
- package/src/atoms/composed/UserMenu/UserMenu.tsx +0 -16
- package/src/atoms/composed/UserMenu/index.ts +0 -1
- package/src/atoms/composed/index.ts +0 -29
- package/src/atoms/hooks/useApi.ts +0 -80
- package/src/atoms/hooks/useHealth.ts +0 -17
- package/src/atoms/index.ts +0 -13
- package/src/atoms/services/api/client.ts +0 -134
- package/src/atoms/services/auth-service.ts +0 -248
- package/src/atoms/services/health.ts +0 -15
- package/src/atoms/services/index.ts +0 -3
- package/src/atoms/shared/config/constants.ts +0 -17
- package/src/atoms/shared/config/dashboard-sizes.ts +0 -111
- package/src/atoms/shared/config/environment.ts +0 -10
- package/src/atoms/shared/index.ts +0 -4
- package/src/atoms/shared/styles/color-palettes.css +0 -566
- package/src/atoms/types/auth.ts +0 -62
- package/src/atoms/types/generated.ts +0 -1469
- package/src/atoms/types/index.ts +0 -4
- package/src/atoms/types/loading.ts +0 -28
- package/src/atoms/ui/Badge.tsx +0 -30
- package/src/atoms/ui/ErrorBoundary.tsx +0 -59
- package/src/atoms/ui/Select.tsx +0 -53
- package/src/atoms/ui/Switch.tsx +0 -42
- package/src/atoms/ui/Tabs.tsx +0 -118
- package/src/atoms/ui/avatar.tsx +0 -48
- package/src/atoms/ui/button.tsx +0 -70
- package/src/atoms/ui/card.tsx +0 -76
- package/src/atoms/ui/dropdown-menu.tsx +0 -199
- package/src/atoms/ui/index.ts +0 -39
- package/src/atoms/ui/input.tsx +0 -23
- package/src/atoms/ui/label.tsx +0 -23
- package/src/atoms/ui/skeleton.tsx +0 -13
- package/src/atoms/ui/spinner.tsx +0 -49
- package/src/atoms/ui/table.tsx +0 -116
- package/src/atoms/utils/animations.ts +0 -135
- package/src/atoms/utils/tooltip-helpers.ts +0 -140
- package/src/atoms/utils/utils.ts +0 -9
- package/src/features/auth/components/LoginForm.tsx +0 -168
- package/src/features/auth/components/LogoutButton.tsx +0 -19
- package/src/features/auth/components/ProtectedRoute.tsx +0 -60
- package/src/features/auth/components/index.ts +0 -4
- package/src/features/auth/hooks/index.ts +0 -2
- package/src/features/auth/hooks/useAuth.tsx +0 -205
- package/src/features/auth/hooks/usePermissions.ts +0 -35
- package/src/features/auth/index.ts +0 -2
- package/src/features/index.ts +0 -2
- package/src/index.css +0 -704
- package/src/index.ts +0 -13
- package/src/main.tsx +0 -48
- package/src/molecules/.gitkeep +0 -0
- package/src/molecules/forms/FormGroup.tsx +0 -75
- package/src/molecules/forms/SearchInput.tsx +0 -259
- package/src/molecules/forms/index.ts +0 -4
- package/src/molecules/index.ts +0 -4
- package/src/molecules/layout/AppHeader/AppHeader.tsx +0 -42
- package/src/molecules/layout/AppHeader/index.ts +0 -1
- package/src/molecules/layout/AppLayout.tsx +0 -29
- package/src/molecules/layout/PageTemplate.tsx +0 -87
- package/src/molecules/layout/SectionHeader/SectionHeader.tsx +0 -87
- package/src/molecules/layout/SectionHeader/index.ts +0 -1
- package/src/molecules/layout/ShowcaseSection.tsx +0 -57
- package/src/molecules/layout/Sidebar.tsx +0 -144
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +0 -99
- package/src/molecules/layout/SidebarButton/index.ts +0 -1
- package/src/molecules/layout/SidebarContext.tsx +0 -31
- package/src/molecules/layout/index.ts +0 -7
- package/src/molecules/navigation/NavMenu.tsx +0 -188
- package/src/molecules/navigation/Pagination.tsx +0 -172
- package/src/molecules/navigation/index.ts +0 -4
- package/src/organisms/index.ts +0 -5
- package/src/organisms/showcase/ComponentShowcasePage.tsx +0 -2496
- package/src/organisms/showcase/index.ts +0 -1
- package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +0 -242
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +0 -171
- package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +0 -385
- package/src/pages/AdminShowcase/index.tsx +0 -3
- package/src/pages/ComponentShowcase/BadgesShowcase.tsx +0 -188
- package/src/pages/ComponentShowcase/CardsShowcase.tsx +0 -392
- package/src/pages/ComponentShowcase/PalettesShowcase.tsx +0 -207
- package/src/pages/ComponentShowcase/StatesShowcase.tsx +0 -485
- package/src/pages/ComponentShowcase/TablesShowcase.tsx +0 -134
- package/src/pages/ComponentShowcase/TypographyShowcase.tsx +0 -255
- package/src/pages/ComponentShowcase/index.tsx +0 -188
- package/src/pages/index.ts +0 -2
- package/src/templates/AuthTemplate.tsx +0 -216
- package/src/templates/ComponentShowcaseTemplate.tsx +0 -173
- package/src/templates/DashboardTemplate.tsx +0 -232
- package/src/templates/DataTemplate.tsx +0 -319
- package/src/templates/admin/AdminCRUDTemplate.tsx +0 -630
- package/src/templates/admin/AdminDashboardTemplate.tsx +0 -351
- package/src/templates/admin/AdminDetailTemplate.tsx +0 -563
- package/src/templates/admin/index.ts +0 -29
- package/src/templates/factory.tsx +0 -169
- package/src/templates/index.ts +0 -37
- package/src/vite-env.d.ts +0 -1
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, it, expect } from 'vitest'
|
|
3
|
-
import { render, screen } from '../../utils'
|
|
4
|
-
|
|
5
|
-
// Simple layout component for testing
|
|
6
|
-
const SimpleHeader = ({ title, showNav = true }: { title: string; showNav?: boolean }) => (
|
|
7
|
-
<header role="banner" className="bg-background border-b border-border">
|
|
8
|
-
<div className="flex items-center justify-between p-4">
|
|
9
|
-
<h1 className="text-xl font-semibold text-foreground">{title}</h1>
|
|
10
|
-
{showNav && (
|
|
11
|
-
<nav aria-label="Main navigation">
|
|
12
|
-
<ul className="flex space-x-4">
|
|
13
|
-
<li>
|
|
14
|
-
<a href="/" className="text-primary hover:text-primary-hover">Home</a>
|
|
15
|
-
</li>
|
|
16
|
-
<li>
|
|
17
|
-
<a href="/about" className="text-primary hover:text-primary-hover">About</a>
|
|
18
|
-
</li>
|
|
19
|
-
</ul>
|
|
20
|
-
</nav>
|
|
21
|
-
)}
|
|
22
|
-
</div>
|
|
23
|
-
</header>
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
describe('Layout Components (Molecule)', () => {
|
|
27
|
-
it('renders header with title', () => {
|
|
28
|
-
render(<SimpleHeader title="Test App" />)
|
|
29
|
-
|
|
30
|
-
const header = screen.getByRole('banner')
|
|
31
|
-
expect(header).toBeInTheDocument()
|
|
32
|
-
|
|
33
|
-
const title = screen.getByRole('heading', { level: 1 })
|
|
34
|
-
expect(title).toHaveTextContent('Test App')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('renders navigation when showNav is true', () => {
|
|
38
|
-
render(<SimpleHeader title="Test App" showNav={true} />)
|
|
39
|
-
|
|
40
|
-
const nav = screen.getByRole('navigation', { name: /main navigation/i })
|
|
41
|
-
expect(nav).toBeInTheDocument()
|
|
42
|
-
|
|
43
|
-
expect(screen.getByRole('link', { name: /home/i })).toBeInTheDocument()
|
|
44
|
-
expect(screen.getByRole('link', { name: /about/i })).toBeInTheDocument()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('hides navigation when showNav is false', () => {
|
|
48
|
-
render(<SimpleHeader title="Test App" showNav={false} />)
|
|
49
|
-
|
|
50
|
-
const nav = screen.queryByRole('navigation')
|
|
51
|
-
expect(nav).not.toBeInTheDocument()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('uses design system classes', () => {
|
|
55
|
-
render(<SimpleHeader title="Test App" />)
|
|
56
|
-
|
|
57
|
-
const header = screen.getByRole('banner')
|
|
58
|
-
|
|
59
|
-
// Should use design system classes
|
|
60
|
-
expect(header).toHaveClass('bg-background')
|
|
61
|
-
expect(header).toHaveClass('border-border')
|
|
62
|
-
|
|
63
|
-
// Should not use hardcoded color classes
|
|
64
|
-
expect(header.className).not.toMatch(/bg-(blue|red|green)-\d+/)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('has proper accessibility attributes', () => {
|
|
68
|
-
render(<SimpleHeader title="Test App" />)
|
|
69
|
-
|
|
70
|
-
const header = screen.getByRole('banner')
|
|
71
|
-
expect(header).toBeInTheDocument()
|
|
72
|
-
|
|
73
|
-
const nav = screen.getByRole('navigation')
|
|
74
|
-
expect(nav).toHaveAttribute('aria-label', 'Main navigation')
|
|
75
|
-
|
|
76
|
-
const links = screen.getAllByRole('link')
|
|
77
|
-
links.forEach(link => {
|
|
78
|
-
expect(link).toHaveAttribute('href')
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
})
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, it, expect } from 'vitest'
|
|
3
|
-
import { render, screen, userEvent } from '../../utils'
|
|
4
|
-
import { cn } from '../../../atoms/utils/utils'
|
|
5
|
-
|
|
6
|
-
// Simple showcase component for testing
|
|
7
|
-
const SimpleShowcase = () => {
|
|
8
|
-
const [activeTab, setActiveTab] = React.useState('components')
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<main className="p-6">
|
|
12
|
-
<h1 className="text-3xl font-bold text-foreground mb-6">Component Showcase</h1>
|
|
13
|
-
|
|
14
|
-
<div className="mb-6">
|
|
15
|
-
<nav aria-label="Showcase navigation">
|
|
16
|
-
<div role="tablist" className="flex space-x-4 border-b border-border">
|
|
17
|
-
<button
|
|
18
|
-
role="tab"
|
|
19
|
-
aria-selected={activeTab === 'components'}
|
|
20
|
-
className={cn(
|
|
21
|
-
'px-4 py-2 font-medium',
|
|
22
|
-
activeTab === 'components'
|
|
23
|
-
? 'text-primary border-b-2 border-primary'
|
|
24
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
25
|
-
)}
|
|
26
|
-
onClick={() => setActiveTab('components')}
|
|
27
|
-
>
|
|
28
|
-
Components
|
|
29
|
-
</button>
|
|
30
|
-
<button
|
|
31
|
-
role="tab"
|
|
32
|
-
aria-selected={activeTab === 'colors'}
|
|
33
|
-
className={cn(
|
|
34
|
-
'px-4 py-2 font-medium',
|
|
35
|
-
activeTab === 'colors'
|
|
36
|
-
? 'text-primary border-b-2 border-primary'
|
|
37
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
38
|
-
)}
|
|
39
|
-
onClick={() => setActiveTab('colors')}
|
|
40
|
-
>
|
|
41
|
-
Colors
|
|
42
|
-
</button>
|
|
43
|
-
</div>
|
|
44
|
-
</nav>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div role="tabpanel">
|
|
48
|
-
{activeTab === 'components' && (
|
|
49
|
-
<div data-testid="components-panel">
|
|
50
|
-
<h2 className="text-xl font-semibold mb-4">UI Components</h2>
|
|
51
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
52
|
-
<button className="bg-primary text-primary-foreground px-4 py-2 rounded">
|
|
53
|
-
Primary Button
|
|
54
|
-
</button>
|
|
55
|
-
<button className="bg-secondary text-secondary-foreground px-4 py-2 rounded">
|
|
56
|
-
Secondary Button
|
|
57
|
-
</button>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
|
-
|
|
62
|
-
{activeTab === 'colors' && (
|
|
63
|
-
<div data-testid="colors-panel">
|
|
64
|
-
<h2 className="text-xl font-semibold mb-4">Color Palette</h2>
|
|
65
|
-
<div className="grid grid-cols-4 gap-2">
|
|
66
|
-
<div className="bg-primary h-12 rounded flex items-center justify-center text-primary-foreground text-xs">
|
|
67
|
-
Primary
|
|
68
|
-
</div>
|
|
69
|
-
<div className="bg-secondary h-12 rounded flex items-center justify-center text-secondary-foreground text-xs">
|
|
70
|
-
Secondary
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
76
|
-
</main>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe('Showcase Page (Organism)', () => {
|
|
81
|
-
it('renders main showcase structure', () => {
|
|
82
|
-
render(<SimpleShowcase />)
|
|
83
|
-
|
|
84
|
-
const main = screen.getByRole('main')
|
|
85
|
-
expect(main).toBeInTheDocument()
|
|
86
|
-
|
|
87
|
-
const heading = screen.getByRole('heading', { level: 1 })
|
|
88
|
-
expect(heading).toHaveTextContent('Component Showcase')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('has accessible tab navigation', () => {
|
|
92
|
-
render(<SimpleShowcase />)
|
|
93
|
-
|
|
94
|
-
const tablist = screen.getByRole('tablist')
|
|
95
|
-
expect(tablist).toBeInTheDocument()
|
|
96
|
-
|
|
97
|
-
const componentsTab = screen.getByRole('tab', { name: /components/i })
|
|
98
|
-
const colorsTab = screen.getByRole('tab', { name: /colors/i })
|
|
99
|
-
|
|
100
|
-
expect(componentsTab).toHaveAttribute('aria-selected', 'true')
|
|
101
|
-
expect(colorsTab).toHaveAttribute('aria-selected', 'false')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('switches between tabs correctly', async () => {
|
|
105
|
-
const user = userEvent.setup()
|
|
106
|
-
render(<SimpleShowcase />)
|
|
107
|
-
|
|
108
|
-
// Initially shows components panel
|
|
109
|
-
expect(screen.getByTestId('components-panel')).toBeInTheDocument()
|
|
110
|
-
expect(screen.queryByTestId('colors-panel')).not.toBeInTheDocument()
|
|
111
|
-
|
|
112
|
-
// Click colors tab
|
|
113
|
-
const colorsTab = screen.getByRole('tab', { name: /colors/i })
|
|
114
|
-
await user.click(colorsTab)
|
|
115
|
-
|
|
116
|
-
// Should now show colors panel
|
|
117
|
-
expect(screen.getByTestId('colors-panel')).toBeInTheDocument()
|
|
118
|
-
expect(screen.queryByTestId('components-panel')).not.toBeInTheDocument()
|
|
119
|
-
|
|
120
|
-
// Tab should be selected
|
|
121
|
-
expect(colorsTab).toHaveAttribute('aria-selected', 'true')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('displays interactive components', () => {
|
|
125
|
-
render(<SimpleShowcase />)
|
|
126
|
-
|
|
127
|
-
// Should have interactive buttons in showcase
|
|
128
|
-
const buttons = screen.getAllByRole('button')
|
|
129
|
-
expect(buttons.length).toBeGreaterThanOrEqual(2) // Tabs + showcase buttons
|
|
130
|
-
|
|
131
|
-
const primaryButton = screen.getByText('Primary Button')
|
|
132
|
-
expect(primaryButton).toBeInTheDocument()
|
|
133
|
-
expect(primaryButton).toHaveClass('bg-primary')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('uses design system classes consistently', () => {
|
|
137
|
-
render(<SimpleShowcase />)
|
|
138
|
-
|
|
139
|
-
const main = screen.getByRole('main')
|
|
140
|
-
|
|
141
|
-
// Should use design system spacing and colors
|
|
142
|
-
expect(main).toHaveClass('p-6')
|
|
143
|
-
|
|
144
|
-
const heading = screen.getByRole('heading', { level: 1 })
|
|
145
|
-
expect(heading).toHaveClass('text-foreground')
|
|
146
|
-
|
|
147
|
-
// Check that components use design system colors
|
|
148
|
-
const primaryButton = screen.getByText('Primary Button')
|
|
149
|
-
expect(primaryButton).toHaveClass('bg-primary', 'text-primary-foreground')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('is responsive and accessible', () => {
|
|
153
|
-
render(<SimpleShowcase />)
|
|
154
|
-
|
|
155
|
-
// Should have proper heading hierarchy
|
|
156
|
-
const h1 = screen.getByRole('heading', { level: 1 })
|
|
157
|
-
const h2s = screen.getAllByRole('heading', { level: 2 })
|
|
158
|
-
|
|
159
|
-
expect(h1).toBeInTheDocument()
|
|
160
|
-
expect(h2s.length).toBeGreaterThan(0)
|
|
161
|
-
|
|
162
|
-
// Should have responsive grid classes
|
|
163
|
-
const gridElements = screen.getByTestId('components-panel').querySelector('.grid')
|
|
164
|
-
expect(gridElements?.className).toMatch(/grid-cols-/)
|
|
165
|
-
expect(gridElements?.className).toMatch(/md:grid-cols-/)
|
|
166
|
-
})
|
|
167
|
-
})
|
package/src/__tests__/setup.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import '@testing-library/jest-dom'
|
|
2
|
-
import { beforeAll, afterEach, afterAll } from 'vitest'
|
|
3
|
-
import { cleanup } from '@testing-library/react'
|
|
4
|
-
|
|
5
|
-
// Global test setup (equivalent to conftest.py)
|
|
6
|
-
|
|
7
|
-
// Clean up after each test
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
cleanup()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
// Mock environment variables for testing
|
|
13
|
-
beforeAll(() => {
|
|
14
|
-
// Mock environment variables
|
|
15
|
-
process.env.VITE_API_URL = 'http://localhost:3001/api/v1'
|
|
16
|
-
process.env.VITE_APP_NAME = 'Test App'
|
|
17
|
-
|
|
18
|
-
// Mock window.matchMedia (required for responsive components)
|
|
19
|
-
Object.defineProperty(window, 'matchMedia', {
|
|
20
|
-
writable: true,
|
|
21
|
-
value: (query: string) => ({
|
|
22
|
-
matches: false,
|
|
23
|
-
media: query,
|
|
24
|
-
onchange: null,
|
|
25
|
-
addListener: () => {},
|
|
26
|
-
removeListener: () => {},
|
|
27
|
-
addEventListener: () => {},
|
|
28
|
-
removeEventListener: () => {},
|
|
29
|
-
dispatchEvent: () => {},
|
|
30
|
-
}),
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// Mock ResizeObserver (required for some components)
|
|
34
|
-
global.ResizeObserver = class ResizeObserver {
|
|
35
|
-
observe() {}
|
|
36
|
-
unobserve() {}
|
|
37
|
-
disconnect() {}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Mock IntersectionObserver (required for some components)
|
|
41
|
-
global.IntersectionObserver = class IntersectionObserver {
|
|
42
|
-
constructor() {}
|
|
43
|
-
observe() {}
|
|
44
|
-
unobserve() {}
|
|
45
|
-
disconnect() {}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
afterAll(() => {
|
|
50
|
-
// Clean up any global mocks if needed
|
|
51
|
-
})
|
package/src/__tests__/utils.tsx
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { render, RenderOptions } from '@testing-library/react'
|
|
3
|
-
import { BrowserRouter } from 'react-router-dom'
|
|
4
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
-
|
|
6
|
-
// Test utilities (equivalent to pytest fixtures)
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Custom render function that wraps components with necessary providers
|
|
10
|
-
* Similar to how conftest.py provides fixtures for database, auth, etc.
|
|
11
|
-
*/
|
|
12
|
-
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
|
13
|
-
// Add custom options here
|
|
14
|
-
initialRoute?: string
|
|
15
|
-
queryClient?: QueryClient
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const AllTheProviders = ({
|
|
19
|
-
children,
|
|
20
|
-
initialRoute = '/',
|
|
21
|
-
queryClient
|
|
22
|
-
}: {
|
|
23
|
-
children: React.ReactNode
|
|
24
|
-
initialRoute?: string
|
|
25
|
-
queryClient?: QueryClient
|
|
26
|
-
}) => {
|
|
27
|
-
// Create a fresh QueryClient for each test to avoid test pollution
|
|
28
|
-
const testQueryClient = queryClient || new QueryClient({
|
|
29
|
-
defaultOptions: {
|
|
30
|
-
queries: {
|
|
31
|
-
retry: false, // Don't retry in tests
|
|
32
|
-
gcTime: 0, // Disable garbage collection
|
|
33
|
-
},
|
|
34
|
-
mutations: {
|
|
35
|
-
retry: false,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
// Set initial route for testing
|
|
41
|
-
if (initialRoute !== '/') {
|
|
42
|
-
window.history.pushState({}, 'Test page', initialRoute)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<QueryClientProvider client={testQueryClient}>
|
|
47
|
-
<BrowserRouter>
|
|
48
|
-
{children}
|
|
49
|
-
</BrowserRouter>
|
|
50
|
-
</QueryClientProvider>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const customRender = (
|
|
55
|
-
ui: React.ReactElement,
|
|
56
|
-
options?: CustomRenderOptions
|
|
57
|
-
) => {
|
|
58
|
-
const { initialRoute, queryClient, ...renderOptions } = options || {}
|
|
59
|
-
|
|
60
|
-
return render(ui, {
|
|
61
|
-
wrapper: (props) => (
|
|
62
|
-
<AllTheProviders
|
|
63
|
-
{...props}
|
|
64
|
-
initialRoute={initialRoute}
|
|
65
|
-
queryClient={queryClient}
|
|
66
|
-
/>
|
|
67
|
-
),
|
|
68
|
-
...renderOptions,
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Create a mock API client for testing
|
|
74
|
-
* Similar to how you might mock database connections in backend tests
|
|
75
|
-
*/
|
|
76
|
-
export const createMockApiClient = () => ({
|
|
77
|
-
get: vi.fn(),
|
|
78
|
-
post: vi.fn(),
|
|
79
|
-
put: vi.fn(),
|
|
80
|
-
delete: vi.fn(),
|
|
81
|
-
patch: vi.fn(),
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Mock user data factory
|
|
86
|
-
* Similar to factory patterns in backend testing
|
|
87
|
-
*/
|
|
88
|
-
export const createMockUser = (overrides = {}) => ({
|
|
89
|
-
id: '1',
|
|
90
|
-
name: 'Test User',
|
|
91
|
-
email: 'test@example.com',
|
|
92
|
-
role: 'user',
|
|
93
|
-
...overrides,
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Mock auth context for testing
|
|
98
|
-
*/
|
|
99
|
-
export const createMockAuthContext = (overrides = {}) => ({
|
|
100
|
-
user: createMockUser(),
|
|
101
|
-
isAuthenticated: true,
|
|
102
|
-
login: vi.fn(),
|
|
103
|
-
logout: vi.fn(),
|
|
104
|
-
loading: false,
|
|
105
|
-
...overrides,
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Helper to test error boundaries
|
|
110
|
-
*/
|
|
111
|
-
export const ThrowError = ({ shouldThrow }: { shouldThrow?: boolean }) => {
|
|
112
|
-
if (shouldThrow) {
|
|
113
|
-
throw new Error('Test error')
|
|
114
|
-
}
|
|
115
|
-
return <div>No error</div>
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Re-export everything from testing-library
|
|
119
|
-
export * from '@testing-library/react'
|
|
120
|
-
export { default as userEvent } from '@testing-library/user-event'
|
|
121
|
-
|
|
122
|
-
// Export our custom render as the default render
|
|
123
|
-
export { customRender as render }
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import { cn } from '../../utils/utils';
|
|
3
|
-
import { getAnimationClasses } from '../../utils/animations';
|
|
4
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
export interface AccordionItem {
|
|
7
|
-
id: string;
|
|
8
|
-
title: string;
|
|
9
|
-
content: React.ReactNode;
|
|
10
|
-
disabled?: boolean;
|
|
11
|
-
icon?: React.ReactNode;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AccordionProps {
|
|
15
|
-
items: AccordionItem[];
|
|
16
|
-
variant?: 'default' | 'bordered' | 'filled';
|
|
17
|
-
allowMultiple?: boolean;
|
|
18
|
-
defaultOpen?: string | string[];
|
|
19
|
-
collapsible?: boolean;
|
|
20
|
-
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
21
|
-
className?: string;
|
|
22
|
-
onItemToggle?: (itemId: string, isOpen: boolean) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Accordion = ({
|
|
26
|
-
items,
|
|
27
|
-
variant = 'default',
|
|
28
|
-
allowMultiple = false,
|
|
29
|
-
defaultOpen = [],
|
|
30
|
-
collapsible = true,
|
|
31
|
-
category,
|
|
32
|
-
className,
|
|
33
|
-
onItemToggle
|
|
34
|
-
}: AccordionProps) => {
|
|
35
|
-
// Initialize openItems state based on defaultOpen
|
|
36
|
-
const [openItems, setOpenItems] = useState<Set<string>>(() => {
|
|
37
|
-
if (Array.isArray(defaultOpen)) {
|
|
38
|
-
return new Set(defaultOpen);
|
|
39
|
-
} else if (defaultOpen) {
|
|
40
|
-
return new Set([defaultOpen]);
|
|
41
|
-
}
|
|
42
|
-
return new Set();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const handleItemToggle = (itemId: string) => {
|
|
46
|
-
const isCurrentlyOpen = openItems.has(itemId);
|
|
47
|
-
|
|
48
|
-
if (!allowMultiple) {
|
|
49
|
-
// Single mode: close others, toggle current
|
|
50
|
-
if (isCurrentlyOpen && collapsible) {
|
|
51
|
-
setOpenItems(new Set());
|
|
52
|
-
onItemToggle?.(itemId, false);
|
|
53
|
-
} else if (!isCurrentlyOpen) {
|
|
54
|
-
setOpenItems(new Set([itemId]));
|
|
55
|
-
onItemToggle?.(itemId, true);
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
// Multiple mode: toggle current
|
|
59
|
-
const newOpenItems = new Set(openItems);
|
|
60
|
-
if (isCurrentlyOpen && collapsible) {
|
|
61
|
-
newOpenItems.delete(itemId);
|
|
62
|
-
onItemToggle?.(itemId, false);
|
|
63
|
-
} else if (!isCurrentlyOpen) {
|
|
64
|
-
newOpenItems.add(itemId);
|
|
65
|
-
onItemToggle?.(itemId, true);
|
|
66
|
-
}
|
|
67
|
-
setOpenItems(newOpenItems);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const getVariantClasses = () => {
|
|
72
|
-
switch (variant) {
|
|
73
|
-
case 'bordered':
|
|
74
|
-
return 'border border-border rounded-lg overflow-hidden';
|
|
75
|
-
case 'filled':
|
|
76
|
-
return 'bg-muted/30 rounded-lg overflow-hidden';
|
|
77
|
-
default:
|
|
78
|
-
return 'space-y-2';
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const getItemClasses = (isOpen: boolean) => {
|
|
83
|
-
const baseClasses = variant === 'default'
|
|
84
|
-
? 'border border-border rounded-lg overflow-hidden'
|
|
85
|
-
: variant === 'bordered'
|
|
86
|
-
? 'border-b border-border last:border-b-0'
|
|
87
|
-
: '';
|
|
88
|
-
|
|
89
|
-
return cn(
|
|
90
|
-
baseClasses,
|
|
91
|
-
isOpen && category && variant !== 'filled' && `shadow-category-${category}/20 shadow-lg`,
|
|
92
|
-
'transition-all duration-200'
|
|
93
|
-
);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const getHeaderClasses = (item: AccordionItem, isOpen: boolean) => {
|
|
97
|
-
return cn(
|
|
98
|
-
'w-full flex items-center justify-between p-4 text-left',
|
|
99
|
-
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
100
|
-
'transition-all duration-200',
|
|
101
|
-
getAnimationClasses({ type: 'subtle', timing: 'normal' }),
|
|
102
|
-
|
|
103
|
-
// Variant-specific styling
|
|
104
|
-
variant === 'filled' && 'bg-muted/50',
|
|
105
|
-
variant === 'default' && 'bg-card hover:bg-muted/30',
|
|
106
|
-
variant === 'bordered' && 'bg-card hover:bg-muted/30',
|
|
107
|
-
|
|
108
|
-
// Category coloring when open
|
|
109
|
-
isOpen && category && `bg-category-${category}/5 border-category-${category}/20`,
|
|
110
|
-
|
|
111
|
-
// Disabled state
|
|
112
|
-
item.disabled && 'opacity-50 cursor-not-allowed',
|
|
113
|
-
!item.disabled && 'cursor-pointer',
|
|
114
|
-
|
|
115
|
-
// Focus and hover states
|
|
116
|
-
!item.disabled && 'hover:bg-accent/20',
|
|
117
|
-
|
|
118
|
-
'group'
|
|
119
|
-
);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const getIconClasses = (isOpen: boolean) => {
|
|
123
|
-
return cn(
|
|
124
|
-
'w-4 h-4 transition-transform duration-200 flex-shrink-0',
|
|
125
|
-
isOpen ? 'rotate-90' : 'rotate-0',
|
|
126
|
-
category ? `text-category-${category}` : 'text-muted-foreground',
|
|
127
|
-
'group-hover:scale-110'
|
|
128
|
-
);
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<div
|
|
133
|
-
className={cn(
|
|
134
|
-
getVariantClasses(),
|
|
135
|
-
className
|
|
136
|
-
)}
|
|
137
|
-
data-component-name="Accordion"
|
|
138
|
-
data-variant={variant}
|
|
139
|
-
role="tablist"
|
|
140
|
-
>
|
|
141
|
-
{items.map((item) => {
|
|
142
|
-
const isOpen = openItems.has(item.id);
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<AccordionItem
|
|
146
|
-
key={item.id}
|
|
147
|
-
item={item}
|
|
148
|
-
isOpen={isOpen}
|
|
149
|
-
onToggle={() => handleItemToggle(item.id)}
|
|
150
|
-
variant={variant}
|
|
151
|
-
category={category}
|
|
152
|
-
itemClasses={getItemClasses(isOpen)}
|
|
153
|
-
headerClasses={getHeaderClasses(item, isOpen)}
|
|
154
|
-
iconClasses={getIconClasses(isOpen)}
|
|
155
|
-
/>
|
|
156
|
-
);
|
|
157
|
-
})}
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// Separate AccordionItem component for better performance and cleaner code
|
|
163
|
-
interface AccordionItemComponentProps {
|
|
164
|
-
item: AccordionItem;
|
|
165
|
-
isOpen: boolean;
|
|
166
|
-
onToggle: () => void;
|
|
167
|
-
variant: 'default' | 'bordered' | 'filled';
|
|
168
|
-
category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
169
|
-
itemClasses: string;
|
|
170
|
-
headerClasses: string;
|
|
171
|
-
iconClasses: string;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const AccordionItem = ({
|
|
175
|
-
item,
|
|
176
|
-
isOpen,
|
|
177
|
-
onToggle,
|
|
178
|
-
variant,
|
|
179
|
-
category,
|
|
180
|
-
itemClasses,
|
|
181
|
-
headerClasses,
|
|
182
|
-
iconClasses
|
|
183
|
-
}: AccordionItemComponentProps) => {
|
|
184
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
185
|
-
const [contentHeight, setContentHeight] = useState<number | undefined>(undefined);
|
|
186
|
-
|
|
187
|
-
useEffect(() => {
|
|
188
|
-
if (contentRef.current) {
|
|
189
|
-
if (isOpen) {
|
|
190
|
-
setContentHeight(contentRef.current.scrollHeight);
|
|
191
|
-
} else {
|
|
192
|
-
setContentHeight(0);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}, [isOpen, item.content]);
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<div className={itemClasses}>
|
|
199
|
-
{/* Header */}
|
|
200
|
-
<button
|
|
201
|
-
className={headerClasses}
|
|
202
|
-
onClick={onToggle}
|
|
203
|
-
disabled={item.disabled}
|
|
204
|
-
aria-expanded={isOpen}
|
|
205
|
-
aria-controls={`accordion-content-${item.id}`}
|
|
206
|
-
id={`accordion-header-${item.id}`}
|
|
207
|
-
role="tab"
|
|
208
|
-
data-component-name="AccordionHeader"
|
|
209
|
-
>
|
|
210
|
-
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
211
|
-
{/* Custom icon or default chevron */}
|
|
212
|
-
{item.icon ? (
|
|
213
|
-
<div className={cn(
|
|
214
|
-
'flex-shrink-0',
|
|
215
|
-
category ? `text-category-${category}` : 'text-muted-foreground'
|
|
216
|
-
)}>
|
|
217
|
-
{item.icon}
|
|
218
|
-
</div>
|
|
219
|
-
) : (
|
|
220
|
-
<ChevronRight className={iconClasses} />
|
|
221
|
-
)}
|
|
222
|
-
|
|
223
|
-
{/* Title */}
|
|
224
|
-
<span className={cn(
|
|
225
|
-
'font-medium text-sm truncate',
|
|
226
|
-
isOpen && category ? `text-category-${category}` : 'text-foreground',
|
|
227
|
-
'group-hover:text-foreground transition-colors duration-200'
|
|
228
|
-
)}>
|
|
229
|
-
{item.title}
|
|
230
|
-
</span>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
{/* Collapse indicator */}
|
|
234
|
-
<ChevronDown
|
|
235
|
-
className={cn(
|
|
236
|
-
'w-4 h-4 transition-transform duration-200 flex-shrink-0 ml-2',
|
|
237
|
-
isOpen ? 'rotate-180' : 'rotate-0',
|
|
238
|
-
category && isOpen ? `text-category-${category}` : 'text-muted-foreground',
|
|
239
|
-
'group-hover:scale-110'
|
|
240
|
-
)}
|
|
241
|
-
data-component-name="AccordionChevron"
|
|
242
|
-
/>
|
|
243
|
-
</button>
|
|
244
|
-
|
|
245
|
-
{/* Content */}
|
|
246
|
-
<div
|
|
247
|
-
style={{
|
|
248
|
-
height: contentHeight,
|
|
249
|
-
overflow: 'hidden',
|
|
250
|
-
transition: 'height 250ms cubic-bezier(0.4, 0, 0.2, 1)'
|
|
251
|
-
}}
|
|
252
|
-
aria-labelledby={`accordion-header-${item.id}`}
|
|
253
|
-
id={`accordion-content-${item.id}`}
|
|
254
|
-
role="tabpanel"
|
|
255
|
-
data-component-name="AccordionContent"
|
|
256
|
-
>
|
|
257
|
-
<div
|
|
258
|
-
ref={contentRef}
|
|
259
|
-
className={cn(
|
|
260
|
-
'p-4 pt-0',
|
|
261
|
-
variant === 'filled' && 'bg-background/50',
|
|
262
|
-
isOpen && category && variant !== 'filled' && `bg-category-${category}/2`,
|
|
263
|
-
'text-muted-foreground text-sm leading-relaxed'
|
|
264
|
-
)}
|
|
265
|
-
>
|
|
266
|
-
{item.content}
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
</div>
|
|
270
|
-
);
|
|
271
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Accordion, type AccordionProps, type AccordionItem } from './Accordion';
|