@react-spa-scaffold/mcp 0.3.0 → 1.1.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 +42 -20
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +59 -25
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.d.ts +1 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/features/versions.json +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/get-example.d.ts +4 -6
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +2 -2
- package/dist/tools/get-example.js.map +1 -1
- package/dist/tools/get-scaffold.d.ts +3 -10
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +44 -20
- package/dist/tools/get-scaffold.js.map +1 -1
- package/dist/utils/examples.d.ts.map +1 -1
- package/dist/utils/examples.js +19 -16
- package/dist/utils/examples.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +15 -2
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/scaffold.d.ts +6 -1
- package/dist/utils/scaffold.d.ts.map +1 -1
- package/dist/utils/scaffold.js +86 -13
- package/dist/utils/scaffold.js.map +1 -1
- package/package.json +2 -2
- package/templates/.env.example +21 -0
- package/templates/.github/PULL_REQUEST_TEMPLATE.md +24 -0
- package/templates/.github/actions/setup-node-deps/action.yml +32 -0
- package/templates/.github/dependabot.yml +104 -0
- package/templates/.github/workflows/ci.yml +156 -0
- package/templates/.husky/commit-msg +1 -0
- package/templates/.husky/pre-commit +2 -0
- package/templates/.nvmrc +1 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/commitlint.config.js +1 -0
- package/templates/components.json +21 -0
- package/templates/docs/API_REFERENCE.md +0 -1
- package/templates/docs/INTERNATIONALIZATION.md +26 -0
- package/templates/e2e/fixtures/index.ts +10 -0
- package/templates/e2e/tests/home.spec.ts +42 -0
- package/templates/e2e/tests/language.spec.ts +42 -0
- package/templates/e2e/tests/navigation.spec.ts +18 -0
- package/templates/e2e/tests/theme.spec.ts +35 -0
- package/templates/eslint.config.js +42 -0
- package/templates/gitignore +33 -0
- package/templates/index.html +13 -0
- package/templates/lighthouse-budget.json +17 -0
- package/templates/lighthouserc.json +23 -0
- package/templates/lingui.config.js +18 -0
- package/templates/package.json +125 -0
- package/templates/playwright.config.ts +30 -0
- package/templates/prettier.config.js +1 -0
- package/templates/public/favicon.svg +4 -0
- package/templates/src/components/shared/RegisterForm/RegisterForm.tsx +91 -0
- package/templates/src/components/shared/RegisterForm/index.ts +1 -0
- package/templates/src/components/shared/index.ts +1 -0
- package/templates/src/components/ui/card.tsx +70 -0
- package/templates/src/components/ui/input.tsx +19 -0
- package/templates/src/components/ui/label.tsx +19 -0
- package/templates/src/hooks/index.ts +1 -1
- package/templates/src/hooks/useRegisterForm.ts +36 -0
- package/templates/src/lib/index.ts +1 -11
- package/templates/src/lib/validations.ts +6 -13
- package/templates/src/pages/Home.tsx +29 -10
- package/templates/tests/unit/components/RegisterForm.test.tsx +105 -0
- package/templates/tests/unit/hooks/useRegisterForm.test.tsx +153 -0
- package/templates/tests/unit/lib/validations.test.ts +22 -33
- package/templates/tests/unit/stores/preferencesStore.test.ts +81 -0
- package/templates/tsconfig.app.json +10 -0
- package/templates/tsconfig.json +11 -0
- package/templates/tsconfig.node.json +4 -0
- package/templates/vite.config.ts +54 -0
- package/templates/vitest.config.ts +38 -0
- package/templates/src/hooks/useContactForm.ts +0 -33
- package/templates/src/lib/constants.ts +0 -8
- package/templates/src/lib/format.ts +0 -119
- package/templates/tests/unit/hooks/useContactForm.test.ts +0 -60
- package/templates/tests/unit/lib/format.test.ts +0 -100
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
name: Lint
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 10
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
- uses: ./.github/actions/setup-node-deps
|
|
23
|
+
- run: npm run lint
|
|
24
|
+
- run: npm run format:check
|
|
25
|
+
|
|
26
|
+
typecheck:
|
|
27
|
+
name: Type Check
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
timeout-minutes: 10
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v6
|
|
32
|
+
- uses: ./.github/actions/setup-node-deps
|
|
33
|
+
- run: npm run typecheck
|
|
34
|
+
|
|
35
|
+
security:
|
|
36
|
+
name: Security Audit
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
timeout-minutes: 10
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v6
|
|
41
|
+
- uses: ./.github/actions/setup-node-deps
|
|
42
|
+
- run: npm audit --audit-level=moderate
|
|
43
|
+
|
|
44
|
+
build:
|
|
45
|
+
name: Build
|
|
46
|
+
needs: [lint, typecheck]
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
timeout-minutes: 10
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v6
|
|
51
|
+
- uses: ./.github/actions/setup-node-deps
|
|
52
|
+
- name: Build
|
|
53
|
+
run: npm run build
|
|
54
|
+
env:
|
|
55
|
+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
56
|
+
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
|
57
|
+
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
|
58
|
+
- uses: actions/upload-artifact@v6
|
|
59
|
+
with:
|
|
60
|
+
name: dist
|
|
61
|
+
path: dist/
|
|
62
|
+
retention-days: 7
|
|
63
|
+
|
|
64
|
+
test:
|
|
65
|
+
name: Unit Tests
|
|
66
|
+
needs: [lint, typecheck]
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
timeout-minutes: 15
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v6
|
|
71
|
+
- uses: ./.github/actions/setup-node-deps
|
|
72
|
+
- run: npm run test:coverage
|
|
73
|
+
- uses: actions/upload-artifact@v6
|
|
74
|
+
with:
|
|
75
|
+
name: coverage
|
|
76
|
+
path: coverage/
|
|
77
|
+
retention-days: 14
|
|
78
|
+
|
|
79
|
+
e2e:
|
|
80
|
+
name: E2E Tests
|
|
81
|
+
needs: [lint, typecheck]
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
timeout-minutes: 15
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v6
|
|
86
|
+
- uses: ./.github/actions/setup-node-deps
|
|
87
|
+
- name: Cache Playwright browsers
|
|
88
|
+
uses: actions/cache@v5
|
|
89
|
+
id: playwright-cache
|
|
90
|
+
with:
|
|
91
|
+
path: ~/.cache/ms-playwright
|
|
92
|
+
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
|
93
|
+
- name: Install Playwright browsers
|
|
94
|
+
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
95
|
+
run: npx playwright install chromium --with-deps
|
|
96
|
+
- name: Install Playwright deps (cached)
|
|
97
|
+
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
|
98
|
+
run: npx playwright install-deps chromium
|
|
99
|
+
- run: npm run e2e
|
|
100
|
+
- uses: actions/upload-artifact@v6
|
|
101
|
+
if: failure()
|
|
102
|
+
with:
|
|
103
|
+
name: playwright-report
|
|
104
|
+
path: playwright-report/
|
|
105
|
+
retention-days: 7
|
|
106
|
+
|
|
107
|
+
lighthouse:
|
|
108
|
+
name: Lighthouse CI
|
|
109
|
+
needs: [build]
|
|
110
|
+
runs-on: ubuntu-latest
|
|
111
|
+
timeout-minutes: 10
|
|
112
|
+
continue-on-error: true
|
|
113
|
+
steps:
|
|
114
|
+
- uses: actions/checkout@v6
|
|
115
|
+
- uses: ./.github/actions/setup-node-deps
|
|
116
|
+
- uses: actions/download-artifact@v7
|
|
117
|
+
with:
|
|
118
|
+
name: dist
|
|
119
|
+
path: dist/
|
|
120
|
+
- name: Run Lighthouse CI
|
|
121
|
+
uses: treosh/lighthouse-ci-action@v12
|
|
122
|
+
with:
|
|
123
|
+
configPath: ./lighthouserc.json
|
|
124
|
+
uploadArtifacts: true
|
|
125
|
+
temporaryPublicStorage: true
|
|
126
|
+
- uses: actions/upload-artifact@v6
|
|
127
|
+
if: always()
|
|
128
|
+
with:
|
|
129
|
+
name: lighthouse-report
|
|
130
|
+
path: .lighthouseci/
|
|
131
|
+
retention-days: 14
|
|
132
|
+
|
|
133
|
+
publish:
|
|
134
|
+
name: Publish to npm
|
|
135
|
+
needs: [build, test]
|
|
136
|
+
if: github.event_name == 'release'
|
|
137
|
+
runs-on: ubuntu-latest
|
|
138
|
+
timeout-minutes: 15
|
|
139
|
+
permissions:
|
|
140
|
+
contents: read
|
|
141
|
+
id-token: write
|
|
142
|
+
steps:
|
|
143
|
+
- uses: actions/checkout@v6
|
|
144
|
+
- uses: ./.github/actions/setup-node-deps
|
|
145
|
+
- name: Configure npm authentication
|
|
146
|
+
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
|
147
|
+
env:
|
|
148
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
149
|
+
- name: Publish @react-spa-scaffold/tsconfig
|
|
150
|
+
run: npm publish -w @react-spa-scaffold/tsconfig --provenance --access public || echo "Package may already exist at this version"
|
|
151
|
+
- name: Publish @react-spa-scaffold/eslint-config
|
|
152
|
+
run: npm publish -w @react-spa-scaffold/eslint-config --provenance --access public || echo "Package may already exist at this version"
|
|
153
|
+
- name: Publish @react-spa-scaffold/prettier-config
|
|
154
|
+
run: npm publish -w @react-spa-scaffold/prettier-config --provenance --access public || echo "Package may already exist at this version"
|
|
155
|
+
- name: Publish @react-spa-scaffold/mcp
|
|
156
|
+
run: npm publish -w @react-spa-scaffold/mcp --provenance --access public || echo "Package may already exist at this version"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx commitlint --edit $1
|
package/templates/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
22
|
package/templates/CLAUDE.md
CHANGED
|
@@ -113,14 +113,16 @@ Need general info? → WebSearch (fallback only)
|
|
|
113
113
|
|
|
114
114
|
## Translations (CRITICAL)
|
|
115
115
|
|
|
116
|
-
All user-facing text MUST
|
|
116
|
+
All user-facing text MUST be wrapped and include translator comments. ESLint enforces this.
|
|
117
117
|
|
|
118
118
|
```tsx
|
|
119
119
|
<Trans comment="Dashboard heading">Welcome back</Trans>;
|
|
120
120
|
t({ message: 'Close', comment: 'Close button' });
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Technical identifiers (`id`, `htmlFor`, `autoComplete`, `register()`) are auto-ignored.
|
|
124
|
+
|
|
125
|
+
See [docs/INTERNATIONALIZATION.md](docs/INTERNATIONALIZATION.md) for full details.
|
|
124
126
|
|
|
125
127
|
## Testing
|
|
126
128
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { extends: ['@commitlint/config-conventional'] };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-nova",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/index.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -9,7 +9,6 @@ Quick reference for what's available. For architectural decisions, see [Architec
|
|
|
9
9
|
| `api.ts` | HTTP client | Any API calls - handles errors, timeouts, JSON. Includes API_CONFIG |
|
|
10
10
|
| `config.ts` | App configuration | Access APP_CONFIG, SENTRY_CONFIG |
|
|
11
11
|
| `env.ts` | Environment variables | Type-safe `env.VITE_*` access |
|
|
12
|
-
| `format.ts` | Formatters | Dates, numbers, currency, bytes - all locale-aware |
|
|
13
12
|
| `routes.ts` | Route constants | Type-safe navigation, avoid magic strings |
|
|
14
13
|
| `storage.ts` | localStorage wrapper | SSR-safe, typed storage with error handling |
|
|
15
14
|
| `storageKeys.ts` | Storage key constants | Centralized key management |
|
|
@@ -62,6 +62,32 @@ The `eslint-plugin-lingui` enforces translations:
|
|
|
62
62
|
|
|
63
63
|
Excluded from checks: tests, mocks, UI primitives, config files.
|
|
64
64
|
|
|
65
|
+
### Auto-Ignored Technical Identifiers
|
|
66
|
+
|
|
67
|
+
The ESLint config automatically ignores strings that are technical identifiers:
|
|
68
|
+
|
|
69
|
+
**Prop names** (values don't need translation):
|
|
70
|
+
|
|
71
|
+
- HTML: `id`, `htmlFor`, `autoComplete`, `aria-invalid`
|
|
72
|
+
- Styling: `className`, `styleName`
|
|
73
|
+
- Components: `type`, `variant`, `size`, `role`, `name`
|
|
74
|
+
- Routing: `href`, `to`, `path`
|
|
75
|
+
- Data: `queryKey`, `data-testid`
|
|
76
|
+
|
|
77
|
+
**Function arguments**:
|
|
78
|
+
|
|
79
|
+
- `register('fieldName')` - React Hook Form field names
|
|
80
|
+
- `console.*`, `Error()` - Debug/error messages
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// These are fine - no translation needed
|
|
84
|
+
<Label htmlFor="email">
|
|
85
|
+
<Input id="email" autoComplete="email" {...register('email')} />
|
|
86
|
+
|
|
87
|
+
// This DOES need translation (user-facing placeholder)
|
|
88
|
+
placeholder={t({ message: 'Enter email', comment: 'Email input hint' })}
|
|
89
|
+
```
|
|
90
|
+
|
|
65
91
|
## Adding a New Locale
|
|
66
92
|
|
|
67
93
|
Edit `lingui.config.js` and add the locale code to the `locales` array.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Navigate to page with clean state (clears localStorage)
|
|
5
|
+
*/
|
|
6
|
+
export async function setupPage(page: Page, path = '/') {
|
|
7
|
+
await page.goto(path);
|
|
8
|
+
await page.evaluate(() => localStorage.clear());
|
|
9
|
+
await page.reload();
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('Home Page', () => {
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
await page.goto('/');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test('displays welcome heading', async ({ page }) => {
|
|
9
|
+
await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('has correct page structure', async ({ page }) => {
|
|
13
|
+
// Header present
|
|
14
|
+
await expect(page.getByRole('banner')).toBeVisible();
|
|
15
|
+
|
|
16
|
+
// Main content area
|
|
17
|
+
await expect(page.getByRole('main')).toBeVisible();
|
|
18
|
+
|
|
19
|
+
// App title in header
|
|
20
|
+
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('skip link navigates to main content', async ({ page }) => {
|
|
24
|
+
const skipLink = page.getByRole('link', { name: /skip to main content/i });
|
|
25
|
+
|
|
26
|
+
// Ensure skip link exists in DOM
|
|
27
|
+
await expect(skipLink).toBeAttached();
|
|
28
|
+
|
|
29
|
+
// Focus the skip link explicitly (more reliable than Tab in E2E)
|
|
30
|
+
await skipLink.focus();
|
|
31
|
+
await expect(skipLink).toBeFocused();
|
|
32
|
+
|
|
33
|
+
// Verify skip link becomes visible when focused
|
|
34
|
+
await expect(skipLink).toBeVisible();
|
|
35
|
+
|
|
36
|
+
// Click skip link to navigate to main content
|
|
37
|
+
await skipLink.click();
|
|
38
|
+
|
|
39
|
+
// Main should be scrolled into view
|
|
40
|
+
await expect(page.locator('#main')).toBeInViewport();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { setupPage } from '../fixtures';
|
|
4
|
+
|
|
5
|
+
test.describe('Language Switcher', () => {
|
|
6
|
+
test.beforeEach(async ({ page }) => {
|
|
7
|
+
await setupPage(page);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('displays language switcher button', async ({ page }) => {
|
|
11
|
+
await expect(page.getByRole('button', { name: /change language/i })).toBeVisible();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('opens dropdown with language options', async ({ page }) => {
|
|
15
|
+
await page.getByRole('button', { name: /change language/i }).click();
|
|
16
|
+
|
|
17
|
+
await expect(page.getByText('English')).toBeVisible();
|
|
18
|
+
await expect(page.getByText('Español')).toBeVisible();
|
|
19
|
+
await expect(page.getByText('Deutsch')).toBeVisible();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('closes dropdown after selection', async ({ page }) => {
|
|
23
|
+
await page.getByRole('button', { name: /change language/i }).click();
|
|
24
|
+
await page.getByText('Español').click();
|
|
25
|
+
|
|
26
|
+
// Dropdown should close - other options not visible
|
|
27
|
+
await expect(page.getByText('Deutsch')).not.toBeVisible();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('persists language preference across reload', async ({ page }) => {
|
|
31
|
+
// Change to Spanish
|
|
32
|
+
await page.getByRole('button', { name: /change language/i }).click();
|
|
33
|
+
await page.getByText('Español').click();
|
|
34
|
+
|
|
35
|
+
// Wait for language change to apply
|
|
36
|
+
await expect(page.getByRole('heading', { name: /bienvenido/i })).toBeVisible();
|
|
37
|
+
|
|
38
|
+
// Reload and verify Spanish persisted
|
|
39
|
+
await page.reload();
|
|
40
|
+
await expect(page.getByRole('heading', { name: /bienvenido/i })).toBeVisible();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('Navigation', () => {
|
|
4
|
+
test('shows 404 page for unknown routes', async ({ page }) => {
|
|
5
|
+
await page.goto('/non-existent-page');
|
|
6
|
+
await expect(page.getByRole('heading', { name: '404' })).toBeVisible();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('header is present on all pages', async ({ page }) => {
|
|
10
|
+
// Home page
|
|
11
|
+
await page.goto('/');
|
|
12
|
+
await expect(page.getByRole('banner')).toBeVisible();
|
|
13
|
+
|
|
14
|
+
// 404 page
|
|
15
|
+
await page.goto('/unknown');
|
|
16
|
+
await expect(page.getByRole('banner')).toBeVisible();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { setupPage } from '../fixtures';
|
|
4
|
+
|
|
5
|
+
test.describe('Theme Toggle', () => {
|
|
6
|
+
test.beforeEach(async ({ page }) => {
|
|
7
|
+
await setupPage(page);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('defaults to light theme', async ({ page }) => {
|
|
11
|
+
await expect(page.locator('html')).not.toHaveClass(/dark/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('toggles between light and dark theme', async ({ page }) => {
|
|
15
|
+
const html = page.locator('html');
|
|
16
|
+
|
|
17
|
+
// Toggle to dark
|
|
18
|
+
await page.getByRole('button', { name: /switch to dark mode/i }).click();
|
|
19
|
+
await expect(html).toHaveClass(/dark/);
|
|
20
|
+
|
|
21
|
+
// Toggle back to light
|
|
22
|
+
await page.getByRole('button', { name: /switch to light mode/i }).click();
|
|
23
|
+
await expect(html).not.toHaveClass(/dark/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('persists theme preference across reload', async ({ page }) => {
|
|
27
|
+
// Set dark theme
|
|
28
|
+
await page.getByRole('button', { name: /switch to dark mode/i }).click();
|
|
29
|
+
await expect(page.locator('html')).toHaveClass(/dark/);
|
|
30
|
+
|
|
31
|
+
// Reload and verify
|
|
32
|
+
await page.reload();
|
|
33
|
+
await expect(page.locator('html')).toHaveClass(/dark/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint configuration for react-spa-scaffold
|
|
3
|
+
*
|
|
4
|
+
* Uses @react-spa-scaffold/eslint-config with local overrides for monorepo packages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import config from '@react-spa-scaffold/eslint-config';
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
// Main app uses React config
|
|
11
|
+
...config,
|
|
12
|
+
|
|
13
|
+
// Additional ignore for monorepo packages dist
|
|
14
|
+
{ ignores: ['packages/**/dist'] },
|
|
15
|
+
|
|
16
|
+
// UI components from shadcn and context/provider files - don't modify
|
|
17
|
+
{
|
|
18
|
+
files: ['**/components/ui/**/*.{ts,tsx}', '**/contexts/**/*.{ts,tsx}', '**/test/**/*.{ts,tsx}'],
|
|
19
|
+
rules: {
|
|
20
|
+
'react-refresh/only-export-components': 'off',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Packages use Node.js rules (override the React/i18n rules from main config)
|
|
25
|
+
{
|
|
26
|
+
files: ['packages/**/*.ts'],
|
|
27
|
+
rules: {
|
|
28
|
+
// Disable React-specific rules (not a React app)
|
|
29
|
+
'react-hooks/rules-of-hooks': 'off',
|
|
30
|
+
'react-hooks/exhaustive-deps': 'off',
|
|
31
|
+
'react-refresh/only-export-components': 'off',
|
|
32
|
+
// Disable i18n rules (server-side code)
|
|
33
|
+
'lingui/no-unlocalized-strings': 'off',
|
|
34
|
+
'lingui/t-call-in-function': 'off',
|
|
35
|
+
'lingui/no-single-variables-to-translate': 'off',
|
|
36
|
+
'lingui/no-expression-in-message': 'off',
|
|
37
|
+
'lingui/no-trans-inside-trans': 'off',
|
|
38
|
+
// Allow console for server logging
|
|
39
|
+
'no-console': 'off',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
dist-ssr/
|
|
7
|
+
*.tsbuildinfo
|
|
8
|
+
|
|
9
|
+
# IDE
|
|
10
|
+
.vscode/
|
|
11
|
+
.idea/
|
|
12
|
+
*.swp
|
|
13
|
+
*.swo
|
|
14
|
+
.DS_Stores
|
|
15
|
+
|
|
16
|
+
# Environment
|
|
17
|
+
.env
|
|
18
|
+
.env.local
|
|
19
|
+
.env.*.local
|
|
20
|
+
|
|
21
|
+
# Testing
|
|
22
|
+
coverage/
|
|
23
|
+
playwright-report/
|
|
24
|
+
test-results/
|
|
25
|
+
playwright/.cache/
|
|
26
|
+
|
|
27
|
+
# i18n compiled catalogs (generated by Vite plugin during build)
|
|
28
|
+
src/locales/*.mjs
|
|
29
|
+
|
|
30
|
+
# Misc
|
|
31
|
+
tmp
|
|
32
|
+
*.log
|
|
33
|
+
npm-debug.log*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>My App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/*",
|
|
4
|
+
"resourceSizes": [
|
|
5
|
+
{ "resourceType": "script", "budget": 500 },
|
|
6
|
+
{ "resourceType": "stylesheet", "budget": 100 },
|
|
7
|
+
{ "resourceType": "total", "budget": 1000 }
|
|
8
|
+
],
|
|
9
|
+
"resourceCounts": [{ "resourceType": "third-party", "budget": 10 }],
|
|
10
|
+
"timings": [
|
|
11
|
+
{ "metric": "first-contentful-paint", "budget": 2000 },
|
|
12
|
+
{ "metric": "interactive", "budget": 5000 },
|
|
13
|
+
{ "metric": "largest-contentful-paint", "budget": 3000 },
|
|
14
|
+
{ "metric": "cumulative-layout-shift", "budget": 0.1 }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ci": {
|
|
3
|
+
"collect": {
|
|
4
|
+
"staticDistDir": "./dist",
|
|
5
|
+
"numberOfRuns": 3,
|
|
6
|
+
"settings": {
|
|
7
|
+
"preset": "desktop",
|
|
8
|
+
"budgetPath": "./lighthouse-budget.json"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"assert": {
|
|
12
|
+
"assertions": {
|
|
13
|
+
"categories:performance": ["warn", { "minScore": 0.9 }],
|
|
14
|
+
"categories:accessibility": ["warn", { "minScore": 0.9 }],
|
|
15
|
+
"categories:best-practices": ["warn", { "minScore": 0.9 }],
|
|
16
|
+
"categories:seo": ["warn", { "minScore": 0.9 }]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"upload": {
|
|
20
|
+
"target": "temporary-public-storage"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from '@lingui/cli';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
locales: ['en', 'es', 'de'],
|
|
5
|
+
sourceLocale: 'en',
|
|
6
|
+
fallbackLocales: {
|
|
7
|
+
default: 'en',
|
|
8
|
+
},
|
|
9
|
+
catalogs: [
|
|
10
|
+
{
|
|
11
|
+
path: '<rootDir>/src/locales/{locale}',
|
|
12
|
+
include: ['src'],
|
|
13
|
+
exclude: ['**/node_modules/**', '**/*.test.{ts,tsx}'],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
format: 'po',
|
|
17
|
+
compileNamespace: 'es',
|
|
18
|
+
});
|