diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0644af5..f412c9b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,12 +24,12 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: latest + version: 10.23.0 - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "pnpm" registry-url: "https://registry.npmjs.org" diff --git a/.gitignore b/.gitignore index 57b1a04..56b578b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ node_modules/ dist/ +tmp/ +test-results/ +playwright-report/ +e2e/site/playwright-report/ .env # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,windows # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,windows @@ -85,4 +89,4 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,windows *.tgz -colbrush-*.tgz \ No newline at end of file +colbrush-*.tgz diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..081f4b4 --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +# Exclude development and test assets from published packages +e2e/ +scripts/ +test-results/ +tmp/ + +# Ignore repository-specific artifacts +*.tgz +*.log +.DS_Store +.vscode/ +node_modules/ diff --git a/e2e/cli/package.json b/e2e/cli/package.json new file mode 100644 index 0000000..8032aae --- /dev/null +++ b/e2e/cli/package.json @@ -0,0 +1,8 @@ +{ + "name": "colbrush-cli-e2e", + "private": true, + "type": "module", + "scripts": { + "test": "node --test tests/*.test.mjs" + } +} diff --git a/e2e/cli/tests/cli.test.mjs b/e2e/cli/tests/cli.test.mjs new file mode 100644 index 0000000..bb21b2a --- /dev/null +++ b/e2e/cli/tests/cli.test.mjs @@ -0,0 +1,152 @@ +import { before, after, test } from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { spawn } from 'node:child_process'; +import { mkdtemp, readFile, cp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, '../../..'); +const cliEntry = path.join(repoRoot, 'dist/cli.cjs'); +const fixtureCss = path.join(repoRoot, 'e2e/site/src/index.css'); +const packageVersion = JSON.parse( + readFileSync(path.join(repoRoot, 'package.json'), 'utf8') +).version; + +const sandboxes = []; + +const runProcess = (command, args, options = {}) => + new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: options.cwd ?? repoRoot, + env: { ...process.env, ...options.env }, + stdio: options.inheritStdio ? 'inherit' : 'pipe', + }); + + if (options.inheritStdio) { + child.on('exit', (code) => { + resolve({ code, stdout: '', stderr: '' }); + }); + child.on('error', reject); + return; + } + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr?.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('exit', (code) => { + resolve({ code, stdout, stderr }); + }); + child.on('error', reject); + }); + +before(async () => { + console.log('๐Ÿ› ๏ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋นŒ๋“œ๋ฅผ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค...\n'); + const buildResult = await runProcess('pnpm', ['build'], { + inheritStdio: true, + }); + if (buildResult.code !== 0) { + throw new Error('Failed to build library before CLI tests'); + } + console.log('\nโœ… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋นŒ๋“œ ์™„๋ฃŒ\n'); +}); + +after(async () => { + await Promise.all( + sandboxes.map((dir) => rm(dir, { recursive: true, force: true })) + ); +}); + +async function createSandbox() { + const sandboxRoot = await mkdtemp(path.join(tmpdir(), 'colbrush-cli-')); + const targetCssPath = path.join(sandboxRoot, 'input.css'); + await cp(fixtureCss, targetCssPath); + sandboxes.push(sandboxRoot); + return { sandboxRoot, cssPath: targetCssPath }; +} + +async function runCli(args, options) { + return runProcess(process.execPath, [cliEntry, ...args], options); +} + +test('prints version information with --version flag', async () => { + const result = await runCli(['--version']); + + assert.equal(result.code, 0); + assert.ok( + result.stdout.includes(`Colbrush v${packageVersion}`), + `CLI stdout should contain version banner, got: ${result.stdout}` + ); +}); + +test('displays usage instructions with --help', async () => { + const result = await runCli(['--help']); + + assert.equal(result.code, 0); + const normalized = result.stdout.replace(/\u001B\[[0-9;]*m/g, ''); + assert.ok( + normalized.includes('USAGE'), + '๋„์›€๋ง์—๋Š” "USAGE" ์„น์…˜์ด ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค' + ); + assert.ok( + normalized.includes('Generate color-blind accessible themes'), + '๋„์›€๋ง์—๋Š” generate ๋ช…๋ น ์„ค๋ช…์ด ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค' + ); +}); + +test('generates themed CSS and report for a valid input file', async () => { + const { sandboxRoot, cssPath } = await createSandbox(); + const reportPath = path.join(sandboxRoot, 'report.json'); + + const result = await runCli( + ['generate', `--css=${cssPath}`, `--json=${reportPath}`], + { cwd: sandboxRoot } + ); + + assert.equal( + result.code, + 0, + `CLI exited with non-zero code: ${result.stderr}` + ); + assert.ok( + result.stdout.includes('All themes generated successfully'), + 'CLI๋Š” ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค' + ); + + const cssOutput = await readFile(cssPath, 'utf8'); + assert.match( + cssOutput, + /\[data-theme='protanopia']\s*{[\s\S]+--color-primary-500:/, + 'CSS file should contain the protanopia theme block' + ); + assert.match( + cssOutput, + /\[data-theme='deuteranopia']\s*{[\s\S]+--color-primary-500:/, + 'CSS file should contain the deuteranopia theme block' + ); + assert.match( + cssOutput, + /\[data-theme='tritanopia']\s*{[\s\S]+--color-primary-500:/, + 'CSS file should contain the tritanopia theme block' + ); + + const jsonReportRaw = await readFile(reportPath, 'utf8'); + const jsonReport = JSON.parse(jsonReportRaw); + + assert.equal(jsonReport.input, cssPath); + assert.equal(jsonReport.exitCode, 0); + assert.equal(jsonReport.themes.length, 3); + assert.ok( + jsonReport.variables.processed >= 4, + `Expected at least 4 processed variables, got ${jsonReport.variables.processed}` + ); +}); diff --git a/e2e/site/index.html b/e2e/site/index.html new file mode 100644 index 0000000..ef858ff --- /dev/null +++ b/e2e/site/index.html @@ -0,0 +1,12 @@ + + + + + + Colbrush E2E Playground + + +
+ + + diff --git a/e2e/site/package.json b/e2e/site/package.json new file mode 100644 index 0000000..d733352 --- /dev/null +++ b/e2e/site/package.json @@ -0,0 +1,28 @@ +{ + "name": "colbrush-e2e-site", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test:e2e": "node ../../scripts/run-site-e2e.mjs" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.16", + "colbrush": "link:../../", + "playwright": "^1.56.1", + "playwright-core": "^1.56.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4.1.16" + }, + "devDependencies": { + "@playwright/test": "1.56.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.8.0", + "vite": "^6.4.2" + } +} diff --git a/e2e/site/playwright.config.ts b/e2e/site/playwright.config.ts new file mode 100644 index 0000000..26626c0 --- /dev/null +++ b/e2e/site/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test'; + +const useExternalServer = + process.env.PLAYWRIGHT_EXTERNAL_SERVER === '1' || + process.env.PLAYWRIGHT_SKIP_WEB_SERVER === '1'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: false, + retries: process.env.CI ? 2 : 0, + reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]], + use: { + baseURL: 'http://127.0.0.1:4173', + trace: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: useExternalServer + ? undefined + : { + command: 'pnpm dev -- --host 127.0.0.1 --port 4173', + url: 'http://127.0.0.1:4173', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + }, +}); diff --git a/e2e/site/pnpm-lock.yaml b/e2e/site/pnpm-lock.yaml new file mode 100644 index 0000000..39094c6 --- /dev/null +++ b/e2e/site/pnpm-lock.yaml @@ -0,0 +1,1495 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.16 + version: 4.1.16(vite@6.4.2(jiti@2.6.1)(lightningcss@1.30.2)) + colbrush: + specifier: link:../../ + version: link:../.. + playwright: + specifier: ^1.56.1 + version: 1.56.1 + playwright-core: + specifier: ^1.56.1 + version: 1.56.1 + react: + specifier: ^19.0.0 + version: 19.2.0 + react-dom: + specifier: ^19.0.0 + version: 19.2.0(react@19.2.0) + tailwindcss: + specifier: ^4.1.16 + version: 4.1.16 + devDependencies: + '@playwright/test': + specifier: 1.56.1 + version: 1.56.1 + '@types/react': + specifier: ^19.0.0 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^5.1.0 + version: 5.1.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.30.2)) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + vite: + specifier: ^6.4.2 + version: 6.4.2(jiti@2.6.1)(lightningcss@1.30.2) + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/pluginutils@1.0.0-beta.43': + resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==} + + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.1.16': + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} + + '@tailwindcss/oxide-android-arm64@4.1.16': + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.16': + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.16': + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.16': + resolution: {integrity: sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/react-dom@19.2.2': + resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + + '@vitejs/plugin-react@5.1.0': + resolution: {integrity: sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + baseline-browser-mapping@2.8.23: + resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==} + hasBin: true + + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001753: + resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.244: + resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.16: + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.27.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@playwright/test@1.56.1': + dependencies: + playwright: 1.56.1 + + '@rolldown/pluginutils@1.0.0-beta.43': {} + + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + + '@tailwindcss/node@4.1.16': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.16 + + '@tailwindcss/oxide-android-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide@4.1.16': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-x64': 4.1.16 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + + '@tailwindcss/vite@4.1.16(vite@6.4.2(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + tailwindcss: 4.1.16 + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.30.2) + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/estree@1.0.8': {} + + '@types/react-dom@19.2.2(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + + '@types/react@19.2.2': + dependencies: + csstype: 3.1.3 + + '@vitejs/plugin-react@5.1.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.43 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 6.4.2(jiti@2.6.1)(lightningcss@1.30.2) + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.8.23: {} + + browserslist@4.27.0: + dependencies: + baseline-browser-mapping: 2.8.23 + caniuse-lite: 1.0.30001753 + electron-to-chromium: 1.5.244 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.27.0) + + caniuse-lite@1.0.30001753: {} + + convert-source-map@2.0.0: {} + + csstype@3.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.244: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graceful-fs@4.2.11: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-releases@2.0.27: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + playwright-core@1.56.1: {} + + playwright@1.56.1: + dependencies: + playwright-core: 1.56.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react-refresh@0.18.0: {} + + react@19.2.0: {} + + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.16: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + typescript@5.9.3: {} + + update-browserslist-db@1.1.4(browserslist@4.27.0): + dependencies: + browserslist: 4.27.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite@6.4.2(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.1 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + yallist@3.1.1: {} diff --git a/e2e/site/pnpm-workspace.yaml b/e2e/site/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/e2e/site/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/e2e/site/src/App.tsx b/e2e/site/src/App.tsx new file mode 100644 index 0000000..e2c9c64 --- /dev/null +++ b/e2e/site/src/App.tsx @@ -0,0 +1,83 @@ +import { ThemeProvider, ThemeSwitcher } from 'colbrush/client'; +import React from 'react'; +import { SimulationFilter } from 'colbrush/devtools'; +import { DeuteranopiaTest } from './components/DeuteranopiaTest'; +import { ProtanopiaTest } from './components/ProtanopiaTest'; +import { TritanopiaTest } from './components/TritanopiaTest'; +import ThemeStatus from './components/ThemeStatus'; +import TestWrapper from './components/TestWrapper'; + +export default function App() { + const tests = [ + { + key: 'protanopia', + title: '์ ์ƒ‰๋งน ํ…Œ์ŠคํŠธ', + badge: 'Protanopia', + description: + '๋ถ‰์€ ๊ณ„์—ด์ด ์•ฝํ•ด์ง„ ์‹œ๋ ฅ์„ ๊ฐ€์ •ํ•˜๊ณ  UI ๋Œ€๋น„์™€ ์›ํ˜• ํŒจํ„ด์„ ์ ๊ฒ€ํ•ฉ๋‹ˆ๋‹ค.', + render: () => , + }, + { + key: 'deuteranopia', + title: '๋…น์ƒ‰๋งน ํ…Œ์ŠคํŠธ', + badge: 'Deuteranopia', + description: + '๋…น์ƒ‰ ์ธ์‹์ด ์ œํ•œ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณผ ๋•Œ ์ƒ‰์ƒ ์ธต์œ„๊ฐ€ ์œ ์ง€๋˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.', + render: () => , + }, + { + key: 'tritanopia', + title: '์ฒญ์ƒ‰๋งน ํ…Œ์ŠคํŠธ', + badge: 'Tritanopia', + description: + '์ฒญ์ƒ‰๊ณผ ๋…ธ๋ž€์ƒ‰์„ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์–ด๋ ค์šด ํ™˜๊ฒฝ์—์„œ ์ฃผ์š” ์ •๋ณด๊ฐ€ ์œ ์ง€๋˜๋Š”์ง€ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.', + render: () => , + }, + ] as const; + const usageTips = [ + { + title: 'ํ…Œ๋งˆ์™€ ์–ธ์–ด ํ™•์ธ', + detail: '์˜ค๋ฅธ์ชฝ ํ•˜๋‹จ Theme Switcher๋กœ ํ…Œ๋งˆ ยท ์–ธ์–ด๋ฅผ ํ† ๊ธ€ํ•˜๋ฉด ์ปจํ…์ŠคํŠธ๊ฐ€ ์ฆ‰์‹œ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.', + }, + { + title: '์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ•„ํ„ฐ ์ ์šฉ', + detail: 'Vision Simulation ํˆด๋ฐ”์—์„œ ๋ชจ๋“œ๋ฅผ ์„ ํƒํ•˜๋ฉด ์ž๋™์œผ๋กœ SVG ํ…Œ์ŠคํŠธ ์นด๋“œ์— ํ•„ํ„ฐ๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.', + }, + { + title: 'ํ˜„์žฌ ์ƒํƒœ ์ถ”์ ', + detail: '์ขŒ์ธก ์ƒํƒœ ํŒจ๋„์—์„œ theme, language, simulation filter ๊ฐ’์ด ๊ธฐ๋Œ€์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.', + }, + { + title: 'E2E ์Šค๋ƒ…์ƒท', + detail: '๊ฐ ์นด๋“œ ํ•˜๋‹จ ์„ค๋ช…์„ ์ฐธ๊ณ ํ•ด ์Šคํฌ๋ฆฐ์ƒท ๋˜๋Š” ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŠธ ํฌ์ธํŠธ๋ฅผ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.', + }, + ] as const; + + return ( + + + +
+
+ +
+
+ +
+ {tests.map((test) => ( + + {test.render()} + + ))} +
+
+
+
+
+ ); +} diff --git a/e2e/site/src/components/Chip.tsx b/e2e/site/src/components/Chip.tsx new file mode 100644 index 0000000..9b6f3ac --- /dev/null +++ b/e2e/site/src/components/Chip.tsx @@ -0,0 +1,17 @@ +function Chip({ + children, + testId, +}: { + children: React.ReactNode; + testId?: string; +}) { + return ( + + {children} + + ); +} +export default Chip; diff --git a/e2e/site/src/components/DeuteranopiaTest.tsx b/e2e/site/src/components/DeuteranopiaTest.tsx new file mode 100644 index 0000000..6f57c8d --- /dev/null +++ b/e2e/site/src/components/DeuteranopiaTest.tsx @@ -0,0 +1,12866 @@ +import { memo } from "react"; +import type { SVGProps } from "react"; + +interface DeuteranopiaTestProps extends SVGProps { + width?: number; + height?: number; +} + +export const DeuteranopiaTest = memo( + ({ width = "100%", height = "100%", className, ...props }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +); + +DeuteranopiaTest.displayName = "deuteranopiaTest"; diff --git a/e2e/site/src/components/ProtanopiaTest.tsx b/e2e/site/src/components/ProtanopiaTest.tsx new file mode 100644 index 0000000..cb4d1eb --- /dev/null +++ b/e2e/site/src/components/ProtanopiaTest.tsx @@ -0,0 +1,12716 @@ +import { memo } from 'react'; +import type { SVGProps } from 'react'; + +interface ProtanopiaTestProps extends SVGProps { + width?: number; + height?: number; +} + +export const ProtanopiaTest = memo( + ({ width = '100%', height = '100%', className, ...props }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +); + +ProtanopiaTest.displayName = 'protanopiaTest'; diff --git a/e2e/site/src/components/StatusRow.tsx b/e2e/site/src/components/StatusRow.tsx new file mode 100644 index 0000000..7947295 --- /dev/null +++ b/e2e/site/src/components/StatusRow.tsx @@ -0,0 +1,23 @@ +import Chip from './Chip'; + +function StatusRow({ + label, + value, + testId, +}: { + label: string; + value: React.ReactNode; + testId?: string; +}) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} +export default StatusRow; diff --git a/e2e/site/src/components/TestWrapper.tsx b/e2e/site/src/components/TestWrapper.tsx new file mode 100644 index 0000000..6c776a2 --- /dev/null +++ b/e2e/site/src/components/TestWrapper.tsx @@ -0,0 +1,41 @@ +export default function TestWrapper({ + children, + title, + description, + badge = 'Vision Simulation', +}: { + children: React.ReactNode; + title?: string; + description?: string; + badge?: string; +}) { + return ( +
+
+ + {badge} + + {title && ( +

+ {title} +

+ )} +
+ +
+
+ {children} +
+
+ + {description && ( +

+ {description} +

+ )} +
+ ); +} diff --git a/e2e/site/src/components/ThemeStatus.tsx b/e2e/site/src/components/ThemeStatus.tsx new file mode 100644 index 0000000..2672211 --- /dev/null +++ b/e2e/site/src/components/ThemeStatus.tsx @@ -0,0 +1,60 @@ +import { useTheme } from 'colbrush/client'; +import StatusRow from './StatusRow'; + +function ThemeStatus() { + const { theme, language, simulationFilter } = useTheme(); + + return ( +
+
+
+ + Context + +
+

+ Colbrush E2E Playground +

+

+ ThemeProvider๊ฐ€ ๋…ธ์ถœํ•˜๋Š” ํ˜„์žฌ ์ปจํ…์ŠคํŠธ ๊ฐ’์„ + ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•˜์„ธ์š”. +

+
+
+ + Live + + +
+ +
+ + + +
+ +

+ Theme Switcher ๋˜๋Š” Simulation Filter์—์„œ ์˜ต์…˜์„ ๋ณ€๊ฒฝํ•  ๋•Œ๋งˆ๋‹ค + ๋ณธ ์นด๋“œ์˜ ๊ฐ’์ด ์ฆ‰์‹œ ๊ฐฑ์‹ ๋˜์–ด E2E ํ…Œ์ŠคํŠธ์˜ ๋‹จ์ผ ๊ธฐ์ค€์  ์—ญํ• ์„ + ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ’์ด ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅด๋ฉด ์ „์—ญ ์ƒํƒœ ๋˜๋Š” ํฌํ„ธ ์œ„์น˜๋ฅผ ๋จผ์ € + ์ ๊ฒ€ํ•˜์„ธ์š”. +

+
+ ); +} +export default ThemeStatus; diff --git a/e2e/site/src/components/TritanopiaTest.tsx b/e2e/site/src/components/TritanopiaTest.tsx new file mode 100644 index 0000000..5ef3c67 --- /dev/null +++ b/e2e/site/src/components/TritanopiaTest.tsx @@ -0,0 +1,12716 @@ +import { memo } from 'react'; +import type { SVGProps } from 'react'; + +interface TritanopiaTestProps extends SVGProps { + width?: number; + height?: number; +} + +export const TritanopiaTest = memo( + ({ width = '100%', height = '100%', className, ...props }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +); + +TritanopiaTest.displayName = 'tritanopiaTest'; diff --git a/e2e/site/src/index.css b/e2e/site/src/index.css new file mode 100644 index 0000000..1072d58 --- /dev/null +++ b/e2e/site/src/index.css @@ -0,0 +1,111 @@ +@import "tailwindcss"; + +@import "colbrush/styles.css"; + +@theme { + --color-primary-500: #64d2ff; + --color-primary-700: #287cff; + --color-accent-500: #ff9f7a; + --color-accent-700: #ff6d51; + --color-surface-500: #0f1f3a; + --color-surface-700: #08132a; + --color-foreground-500: #f7faff; + --color-foreground-muted: rgba(230, 236, 255, 0.76); + + --color-protanopia-a: #a34245; + --color-protanopia-b: #7e464a; + --color-protanopia-sub-a: #275841; + --color-protanopia-sub-b: #455c4e; + --color-protanopia-sub-c: #525856; + + --color-deuteranopia-a: #11724d; + --color-deuteranopia-b: #5a7d6e; + --color-deuteranopia-sub-a: #b84f47; + --color-deuteranopia-sub-b: #a04d4d; + --color-deuteranopia-sub-c: #6b6363; + + --color-tritanopia-a: #7287e4; + --color-tritanopia-b: #627cac; + --color-tritanopia-sub-a: #5ba192; + --color-tritanopia-sub-b: #4da08b; + --color-tritanopia-sub-c: #4c7e71; +} + +[data-theme='deuteranopia']{ + --color-primary-500: #8677c2; + --color-primary-700: #5b4c8f; + --color-accent-500: #ff6b00; + --color-accent-700: #c53e00; + --color-surface-500: #121222; + --color-surface-700: #000003; + --color-foreground-500: #f2faff; + --color-foreground-muted: rgba(230, 236, 255, 0.76); + --color-protanopia-a: #aa4000; + --color-protanopia-b: #864405; + --color-protanopia-sub-a: #005a77; + --color-protanopia-sub-b: #2d5d6b; + --color-protanopia-sub-c: #4e585d; + --color-deuteranopia-a: #0076a7; + --color-deuteranopia-b: #327f96; + --color-deuteranopia-sub-a: #bf4d00; + --color-deuteranopia-sub-b: #a84b00; + --color-deuteranopia-sub-c: #6e635b; + --color-tritanopia-a: #9884b5; + --color-tritanopia-b: #6a7ba3; + --color-tritanopia-sub-a: #00a4d6; + --color-tritanopia-sub-b: #00a4dc; + --color-tritanopia-sub-c: #0080a4; +} + +[data-theme='protanopia']{ + --color-primary-500: #8677c2; + --color-primary-700: #5b4c8f; + --color-accent-500: #ff6b00; + --color-accent-700: #c53e00; + --color-surface-500: #121222; + --color-surface-700: #000003; + --color-foreground-500: #f2faff; + --color-foreground-muted: rgba(230, 236, 255, 0.76); + --color-protanopia-a: #aa4000; + --color-protanopia-b: #864405; + --color-protanopia-sub-a: #005a77; + --color-protanopia-sub-b: #2d5d6b; + --color-protanopia-sub-c: #4e585d; + --color-deuteranopia-a: #0076a7; + --color-deuteranopia-b: #327f96; + --color-deuteranopia-sub-a: #bf4d00; + --color-deuteranopia-sub-b: #a84b00; + --color-deuteranopia-sub-c: #6e635b; + --color-tritanopia-a: #9884b5; + --color-tritanopia-b: #6a7ba3; + --color-tritanopia-sub-a: #00a4d6; + --color-tritanopia-sub-b: #00a4dc; + --color-tritanopia-sub-c: #0080a4; +} + +[data-theme='tritanopia']{ + --color-primary-500: #be41ff; + --color-primary-700: #8c00c5; + --color-accent-500: #d38b4e; + --color-accent-700: #9f5f24; + --color-surface-500: #1e0b2a; + --color-surface-700: #020006; + --color-foreground-500: #fcf7ff; + --color-foreground-muted: rgba(230, 236, 255, 0.76); + --color-protanopia-a: #914f44; + --color-protanopia-b: #764b4a; + --color-protanopia-sub-a: #0d5a41; + --color-protanopia-sub-b: #3d5e4e; + --color-protanopia-sub-c: #525856; + --color-deuteranopia-a: #00754d; + --color-deuteranopia-b: #537e6e; + --color-deuteranopia-sub-a: #9f6046; + --color-deuteranopia-sub-b: #90574c; + --color-deuteranopia-sub-c: #6a6363; + --color-tritanopia-a: #bf67e5; + --color-tritanopia-b: #8e6eac; + --color-tritanopia-sub-a: #59a192; + --color-tritanopia-sub-b: #44a18b; + --color-tritanopia-sub-c: #487f71; +} + diff --git a/e2e/site/src/main.tsx b/e2e/site/src/main.tsx new file mode 100644 index 0000000..b21722f --- /dev/null +++ b/e2e/site/src/main.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +import './index.css'; + +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Failed to find the root element for the sample app.'); +} + +ReactDOM.createRoot(rootElement).render( + + + +); diff --git a/e2e/site/tests/theme-switcher.spec.ts b/e2e/site/tests/theme-switcher.spec.ts new file mode 100644 index 0000000..4e63533 --- /dev/null +++ b/e2e/site/tests/theme-switcher.spec.ts @@ -0,0 +1,114 @@ +import { expect, test } from '@playwright/test'; + +const toggleButtonName = /theme switcher menu|ํ…Œ๋งˆ ์ „ํ™˜ ๋ฉ”๋‰ด/; +const simulateButtonName = /simulation options|์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋„๊ตฌ/; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); + // Ensure a clean slate for tests while staying on the correct origin. + await page.evaluate(() => localStorage.clear()); + await page.reload(); +}); + +test('์ƒˆ๋กœ์šด ํ…Œ๋งˆ๋ฅผ ์„ ํƒํ•  ๋•Œ HTML์˜ data-theme ์†์„ฑ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.', async ({ + page, +}) => { + await page.getByRole('button', { name: toggleButtonName }).click(); + await page.getByRole('menu', { name: 'Select theme' }).isVisible(); + + await page.getByRole('menuitemradio', { name: 'protanopia' }).click(); + + await expect(page.locator('html')).toHaveAttribute( + 'data-theme', + 'protanopia' + ); + await expect(page.getByTestId('active-theme')).toHaveText('protanopia'); +}); + +test('์–ธ์–ด ์ „ํ™˜ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์–ธ์–ด๋ฅผ ํ•œ๊ตญ์–ด๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค.', async ({ page }) => { + await page.getByRole('button', { name: toggleButtonName }).click(); + await page.getByRole('button', { name: 'English' }).click(); + + await expect(page.getByTestId('active-language')).toHaveText('Korean'); + await expect( + page.getByRole('menuitemradio', { name: '๊ธฐ๋ณธ' }) + ).toBeVisible(); + await expect( + page.getByRole('button', { name: toggleButtonName }) + ).toHaveAttribute('aria-label', 'ํ…Œ๋งˆ ์ „ํ™˜ ๋ฉ”๋‰ด ๋‹ซ๊ธฐ'); +}); + +test('์ƒˆ๋กœ๊ณ ์นจ์„ ํ•˜์—ฌ๋„ ๊ธฐ์กด์— ์„ ํƒํ•œ ํ…Œ๋งˆ๊ฐ€ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.', async ({ page }) => { + await page.getByRole('button', { name: toggleButtonName }).click(); + await page.getByRole('menuitemradio', { name: 'deuteranopia' }).click(); + + await expect(page.locator('html')).toHaveAttribute( + 'data-theme', + 'deuteranopia' + ); + + await page.reload(); + + await expect(page.locator('html')).toHaveAttribute( + 'data-theme', + 'deuteranopia' + ); + await expect(page.getByTestId('active-theme')).toHaveText('deuteranopia'); +}); + +test('์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ…Œ๋งˆ๋ฅผ ์ ์šฉํ•˜๋ฉด ๋ฃจํŠธ ์š”์†Œ์— ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.', async ({ + page, +}) => { + const rootFilter = () => + page.evaluate( + () => document.getElementById('root')?.style.filter ?? '' + ); + + await page.getByRole('button', { name: simulateButtonName }).click(); + await page + .getByRole('group', { + name: /vision simulation modes|์‹œ๊ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ชจ๋“œ/, + }) + .isVisible(); + + await page.getByRole('button', { name: 'protanopia' }).click(); + + await expect.poll(rootFilter).toContain('cb-vision-filter'); + await expect( + page.getByRole('button', { name: 'protanopia' }) + ).toHaveAttribute('aria-pressed', 'true'); + + await page.getByRole('button', { name: 'default' }).click(); + + await expect.poll(rootFilter).toBe(''); + await expect( + page.getByRole('button', { name: 'protanopia' }) + ).toHaveAttribute('aria-pressed', 'false'); + await expect(page.getByRole('button', { name: 'default' })).toHaveAttribute( + 'aria-pressed', + 'true' + ); +}); + +test('์„ ํƒํ•œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ…Œ๋งˆ๋Š” ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.', async ({ + page, +}) => { + const rootFilter = () => + page.evaluate( + () => document.getElementById('root')?.style.filter ?? '' + ); + + await page.getByRole('button', { name: simulateButtonName }).click(); + await page.getByRole('button', { name: 'deuteranopia' }).click(); + + await expect.poll(rootFilter).toContain('cb-vision-filter'); + + await page.reload(); + + await expect.poll(rootFilter).toContain('cb-vision-filter'); + + await page.getByRole('button', { name: simulateButtonName }).click(); + await expect( + page.getByRole('button', { name: 'deuteranopia' }) + ).toHaveAttribute('aria-pressed', 'true'); +}); diff --git a/e2e/site/tsconfig.json b/e2e/site/tsconfig.json new file mode 100644 index 0000000..abe4f40 --- /dev/null +++ b/e2e/site/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "declaration": false, + "declarationMap": false, + "noEmit": true, + "types": ["vite/client"] + }, + "include": [ + "src", + "vite.config.ts", + "playwright.config.ts", + "tests" + ] +} diff --git a/e2e/site/tsconfig.node.json b/e2e/site/tsconfig.node.json new file mode 100644 index 0000000..3ecbd34 --- /dev/null +++ b/e2e/site/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "noEmit": true + }, + "include": [ + "vite.config.ts", + "playwright.config.ts" + ] +} diff --git a/e2e/site/vite.config.ts b/e2e/site/vite.config.ts new file mode 100644 index 0000000..cbc91b7 --- /dev/null +++ b/e2e/site/vite.config.ts @@ -0,0 +1,33 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig, type PluginOption } from 'vite'; + +const configDir = path.dirname(fileURLToPath(import.meta.url)); +const libraryRoot = path.resolve(configDir, '../../src'); +const distStyles = path.resolve(configDir, '../../dist/styles.css'); +const stylesAlias = fs.existsSync(distStyles) + ? distStyles + : path.join(libraryRoot, 'styles.css'); + +const plugins: PluginOption[] = [ + react(), + tailwindcss() as PluginOption, +]; + +export default defineConfig({ + plugins, + server: { + host: '127.0.0.1', + port: 4173, + }, + resolve: { + alias: { + 'colbrush/client': path.join(libraryRoot, 'client.ts'), + 'colbrush/styles.css': stylesAlias, + 'colbrush/devtools': path.join(libraryRoot, 'devtools/index.ts'), + }, + }, +}); diff --git a/package.json b/package.json index 10ace3d..fc765cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "colbrush", - "version": "1.16.0", + "version": "1.21.1", "description": "A React theme switching library that makes it easy to apply color-blind accessible UI themes", "homepage": "https://colbrush.site", "repository": { @@ -54,7 +54,11 @@ "build": "pnpm svgr && tsup --config tsup.config.ts && pnpm build:css", "dev": "tsup --config tsup.config.ts --watch", "prepublishOnly": "pnpm build", - "lint": "eslint ." + "lint": "eslint .", + "test:e2e:site": "node ./scripts/run-site-e2e.mjs", + "test:e2e:cli:smoke": "node ./scripts/run-cli-smoke.mjs", + "test:e2e:cli": "pnpm --dir e2e/cli test", + "test": "pnpm --silent test:e2e:cli && pnpm --silent test:e2e:cli:smoke && pnpm --silent test:e2e:site" }, "keywords": [], "author": "TEAM ColorBrush", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd7b140..947f8fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -889,15 +889,15 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - brace-expansion@5.0.2: - resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} - engines: {node: 20 || >=22} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} @@ -1645,19 +1645,19 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - minimatch@10.2.1: - resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} - engines: {node: 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: @@ -2317,7 +2317,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.1 debug: 4.4.3 - minimatch: 10.2.1 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -2742,7 +2742,7 @@ snapshots: '@typescript-eslint/types': 8.56.0 '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - minimatch: 9.0.5 + minimatch: 9.0.9 semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -2840,16 +2840,16 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.12: + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.2: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.3 @@ -3222,7 +3222,7 @@ snapshots: hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -3276,7 +3276,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.1 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -3393,7 +3393,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 5.1.6 + minimatch: 5.1.9 once: 1.4.0 globalthis@1.0.4: @@ -3673,21 +3673,21 @@ snapshots: mdn-data@2.0.30: {} - minimatch@10.2.1: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.6 - minimatch@3.1.2: + minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.14 - minimatch@5.1.6: + minimatch@5.1.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.1.0 - minimatch@9.0.5: + minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.1.0 minimist@1.2.8: {} diff --git a/scripts/run-cli-smoke.mjs b/scripts/run-cli-smoke.mjs new file mode 100644 index 0000000..b75e6d2 --- /dev/null +++ b/scripts/run-cli-smoke.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { mkdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, '..'); +const siteCss = path.join(repoRoot, 'e2e/site/src/index.css'); +const cliEntry = path.join(repoRoot, 'dist/cli.cjs'); +const reportPath = path.join(repoRoot, 'test-results/cli-report.json'); + +function run(command, args, { cwd = repoRoot, env = {} } = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + stdio: 'inherit', + env: { ...process.env, ...env }, + }); + child.on('exit', (code) => { + if (code === 0) resolve(); + else + reject( + new Error( + `${command} ${args.join(' ')} exited with code ${code}` + ) + ); + }); + child.on('error', reject); + }); +} + +function runColbrush(args, options = {}) { + return run(process.execPath, [cliEntry, ...args], options); +} + +async function runGenerateOnSiteCss() { + await mkdir(path.dirname(reportPath), { recursive: true }); + + await runColbrush(['generate', `--css=${siteCss}`, `--json=${reportPath}`]); + + const generatedCss = await readFile(siteCss, 'utf8'); + if (!generatedCss.includes("[data-theme='protanopia']")) { + throw new Error( + '์ƒ์„ฑ๋œ CSS์—์„œ protanopia ํ…Œ๋งˆ ๋ธ”๋ก์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' + ); + } + const report = JSON.parse(await readFile(reportPath, 'utf8')); + if (report.exitCode !== 0) { + throw new Error('CLI JSON ๋ฆฌํฌํŠธ๊ฐ€ ์„ฑ๊ณต ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'); + } +} + +async function main() { + console.log('๐Ÿ› ๏ธ CLI ํ…Œ์ŠคํŠธ๋ฅผ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค...\n'); + + await runColbrush(['--version']); + await runColbrush(['--help']); + await runColbrush(['--doctor']); + + console.log( + '\n๐Ÿšง ์‹ค์ œ e2e ์‚ฌ์ดํŠธ CSS์— ๋Œ€ํ•ด colbrush generate ๋ช…๋ น์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค...\n' + ); + await runGenerateOnSiteCss(); + console.log('\nโœ… colbrush CLI ๊ธฐ๋ณธ ๋ช…๋ น๋“ค์ด ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.'); +} + +main().catch((error) => { + console.error('โŒ CLI ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', error); + process.exit(1); +}); diff --git a/scripts/run-site-e2e.mjs b/scripts/run-site-e2e.mjs new file mode 100644 index 0000000..d5719b6 --- /dev/null +++ b/scripts/run-site-e2e.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import net from 'node:net'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { setTimeout as sleep } from 'node:timers/promises'; +import fs from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const siteDir = path.resolve(__dirname, '../e2e/site'); +const host = '127.0.0.1'; +const port = 4173; +const pidFile = path.resolve(siteDir, '../test-results/vite-dev.pid'); + +function checkServer() { + return new Promise((resolve) => { + const socket = net.createConnection({ host, port }, () => { + socket.end(); + resolve(true); + }); + socket.on('error', () => { + socket.destroy(); + resolve(false); + }); + socket.setTimeout(500, () => { + socket.destroy(); + resolve(false); + }); + }); +} + +async function waitForServer(timeoutMs = 15000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (await checkServer()) { + return; + } + await sleep(300); + } + throw new Error('๊ฐœ๋ฐœ ์„œ๋ฒ„๊ฐ€ ์˜ˆ์ƒ ์‹œ๊ฐ„ ๋‚ด์— ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'); +} + +function existingPid() { + if (!fs.existsSync(pidFile)) return null; + try { + const pid = Number(readFileSync(pidFile, 'utf8').trim()); + if (!Number.isFinite(pid)) return null; + process.kill(pid, 0); + return pid; + } catch { + return null; + } +} + +async function stopExistingServer() { + const pid = existingPid(); + + if (!pid) { + if (fs.existsSync(pidFile)) { + fs.rmSync(pidFile, { force: true }); + } + + if (await checkServer()) { + throw new Error( + `ํฌํŠธ ${port}์—์„œ ์‹คํ–‰ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋™์œผ๋กœ ์ข…๋ฃŒ ํ›„ ๋‹ค์‹œ ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”.` + ); + } + return; + } + + console.log(`๐Ÿ›‘ ๊ธฐ์กด E2E ์‚ฌ์ดํŠธ ์„œ๋ฒ„๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. (PID ${pid})`); + + try { + process.kill(pid, 'SIGTERM'); + } catch (error) { + if (error?.code === 'ESRCH') { + fs.rmSync(pidFile, { force: true }); + return; + } + throw error; + } + + const gracefulDeadline = Date.now() + 5000; + while (await checkServer()) { + if (Date.now() > gracefulDeadline) { + console.log('โš ๏ธ ์ •์ƒ ์ข…๋ฃŒ๋˜์ง€ ์•Š์•„ ๊ฐ•์ œ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.'); + try { + process.kill(pid, 'SIGKILL'); + } catch (error) { + if (error?.code !== 'ESRCH') { + throw error; + } + } + break; + } + await sleep(200); + } + + const serverStillRunning = await checkServer(); + fs.rmSync(pidFile, { force: true }); + + if (serverStillRunning) { + throw new Error( + `๊ธฐ์กด E2E ์‚ฌ์ดํŠธ ์„œ๋ฒ„(PID ${pid})๋ฅผ ์ข…๋ฃŒํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.` + ); + } + + console.log('โœ… ๊ธฐ์กด E2E ์‚ฌ์ดํŠธ ์„œ๋ฒ„๋ฅผ ์ข…๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค.'); +} + +async function ensureServer() { + if (await checkServer()) { + await stopExistingServer(); + } + + const viteBin = path.resolve(siteDir, 'node_modules/vite/bin/vite.js'); + if (!fs.existsSync(viteBin)) { + console.log('๐Ÿ“ฆ e2e ์‚ฌ์ดํŠธ ์˜์กด์„ฑ์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค...'); + await new Promise((resolve, reject) => { + const child = spawn('pnpm', ['install'], { + cwd: siteDir, + stdio: 'inherit', + }); + child.on('exit', (code) => { + if (code === 0) resolve(); + else reject(new Error('pnpm install ์‹คํŒจ')); + }); + child.on('error', reject); + }); + } + + console.log('๐Ÿš€ E2E ์‚ฌ์ดํŠธ ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...'); + const devProcess = spawn( + process.execPath, + [viteBin, 'dev', '--host', host, '--port', String(port)], + { + cwd: siteDir, + stdio: 'ignore', + detached: true, + } + ); + devProcess.unref(); + fs.mkdirSync(path.dirname(pidFile), { recursive: true }); + writeFileSync(pidFile, String(devProcess.pid)); + + await waitForServer(); + console.log( + `๐ŸŒ ${`http://${host}:${port}`} ์„œ๋ฒ„๊ฐ€ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (PID ${devProcess.pid})` + ); + return true; +} + +async function runPlaywright() { + return new Promise((resolve, reject) => { + const child = spawn('pnpm', ['exec', 'playwright', 'test'], { + cwd: siteDir, + stdio: 'inherit', + env: { + ...process.env, + PLAYWRIGHT_EXTERNAL_SERVER: '1', + }, + }); + + child.on('exit', (code) => resolve(code ?? 1)); + child.on('error', reject); + }); +} + +async function main() { + const startedServer = await ensureServer(); + const exitCode = await runPlaywright(); + + if (exitCode !== 0) { + process.exit(exitCode); + } + + if (startedServer) { + console.log('โ„น๏ธ ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๋Š” ๊ณ„์† ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.'); + } +} + +main().catch((error) => { + console.error('โŒ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', error); + process.exit(1); +}); diff --git a/src/styles.css b/src/styles.css index 6ba3306..a19bac6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1 @@ -@source "./src/**/*.{js,ts,jsx,tsx}"; \ No newline at end of file +@source "./**/*.{js,ts,jsx,tsx}";