1
0
forked from baron/baron-sso

Merge branch 'dev' into feature/issue-917-sub-email-support

This commit is contained in:
2026-05-29 13:23:06 +09:00
186 changed files with 11084 additions and 2370 deletions

View File

@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- dev - dev
paths-ignore:
- "docs/badges/**"
pull_request: pull_request:
branches: branches:
- dev - dev
@@ -49,6 +51,14 @@ on:
required: true required: true
type: boolean type: boolean
default: true default: true
run_front_coverage:
description: "Run adminfront/devfront/orgfront Vitest coverage and upload reports"
required: true
type: boolean
default: true
permissions:
contents: write
jobs: jobs:
lint: lint:
@@ -92,34 +102,37 @@ jobs:
run: | run: |
cd adminfront cd adminfront
npx pnpm install -C ../common --no-frozen-lockfile npx pnpm install -C ../common --no-frozen-lockfile
npx pnpm install --no-frozen-lockfile
- name: Biome check adminfront (lint + format) - name: Biome check adminfront (lint + format)
run: | run: |
cd adminfront cd adminfront
npx biome check . --formatter-enabled=false --organize-imports-enabled=false npx biome check . --formatter-enabled=false --assist-enabled=false
npx biome check . --linter-enabled=false --organize-imports-enabled=false npx biome check . --linter-enabled=false --assist-enabled=false
- name: Install devfront dependencies - name: Install devfront dependencies
run: | run: |
cd devfront cd devfront
npx pnpm install -C ../common --no-frozen-lockfile npx pnpm install -C ../common --no-frozen-lockfile
npx pnpm install --no-frozen-lockfile
- name: Biome check devfront (lint + format) - name: Biome check devfront (lint + format)
run: | run: |
cd devfront cd devfront
npx biome check . --formatter-enabled=false --organize-imports-enabled=false npx biome check . --formatter-enabled=false --assist-enabled=false
npx biome check . --linter-enabled=false --organize-imports-enabled=false npx biome check . --linter-enabled=false --assist-enabled=false
- name: Install orgfront dependencies - name: Install orgfront dependencies
run: | run: |
cd orgfront cd orgfront
npx pnpm install -C ../common --no-frozen-lockfile npx pnpm install -C ../common --no-frozen-lockfile
npx pnpm install --no-frozen-lockfile
- name: Biome check orgfront (lint + format) - name: Biome check orgfront (lint + format)
run: | run: |
cd orgfront cd orgfront
npx biome check . --formatter-enabled=false --organize-imports-enabled=false npx biome check . --formatter-enabled=false --assist-enabled=false
npx biome check . --linter-enabled=false --organize-imports-enabled=false npx biome check . --linter-enabled=false --assist-enabled=false
- name: Lint Go backend - name: Lint Go backend
run: | run: |
@@ -148,6 +161,54 @@ jobs:
cd userfront cd userfront
flutter analyze --no-fatal-warnings --no-fatal-infos flutter analyze --no-fatal-warnings --no-fatal-infos
biome-check:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_lint == true }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install adminfront dependencies
run: |
cd adminfront
npx pnpm install -C ../common --no-frozen-lockfile
npx pnpm install --no-frozen-lockfile
- name: Biome check adminfront
run: |
cd adminfront
npx biome check . --formatter-enabled=false --assist-enabled=false
npx biome check . --linter-enabled=false --assist-enabled=false
- name: Install devfront dependencies
run: |
cd devfront
npx pnpm install -C ../common --no-frozen-lockfile
npx pnpm install --no-frozen-lockfile
- name: Biome check devfront
run: |
cd devfront
npx biome check . --formatter-enabled=false --assist-enabled=false
npx biome check . --linter-enabled=false --assist-enabled=false
- name: Install orgfront dependencies
run: |
cd orgfront
npx pnpm install -C ../common --no-frozen-lockfile
npx pnpm install --no-frozen-lockfile
- name: Biome check orgfront
run: |
cd orgfront
npx biome check . --formatter-enabled=false --assist-enabled=false
npx biome check . --linter-enabled=false --assist-enabled=false
backend-tests: backend-tests:
needs: lint needs: lint
if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_backend_tests == true) }} if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_backend_tests == true) }}
@@ -570,6 +631,189 @@ jobs:
userfront-e2e/test-results userfront-e2e/test-results
if-no-files-found: ignore if-no-files-found: ignore
front-vitest-coverage:
needs: lint
if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_front_coverage == true) }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install front workspace dependencies
run: |
mkdir -p reports
set +e
npm install -g pnpm
cd common
pnpm install --no-frozen-lockfile --shamefully-hoist 2>&1 | tee ../reports/front-coverage-install.log
install_exit_code=${PIPESTATUS[0]}
cd ..
set -e
if [ "$install_exit_code" -ne 0 ]; then
{
echo "# Front Vitest Coverage Failure Report"
echo
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
echo "- Job: \`front-vitest-coverage\`"
echo "- Reason: \`Dependency install failed\`"
echo "- Exit Code: \`$install_exit_code\`"
echo
echo "## Command"
echo "\`cd common && pnpm install --no-frozen-lockfile --shamefully-hoist\`"
echo
echo "## Install Log Tail (last 200 lines)"
echo '```text'
tail -n 200 reports/front-coverage-install.log
echo '```'
} > reports/front-vitest-coverage-failure-report.md
exit 1
fi
for app in adminfront devfront orgfront; do
set +e
cd "$app"
pnpm install --no-frozen-lockfile --shamefully-hoist 2>&1 | tee -a ../reports/front-coverage-install.log
app_install_exit_code=${PIPESTATUS[0]}
cd ..
set -e
if [ "$app_install_exit_code" -ne 0 ]; then
{
echo "# Front Vitest Coverage Failure Report"
echo
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
echo "- Job: \`front-vitest-coverage\`"
echo "- Package: \`$app\`"
echo "- Reason: \`Dependency install failed\`"
echo "- Exit Code: \`$app_install_exit_code\`"
echo
echo "## Command"
echo "\`cd $app && pnpm install --no-frozen-lockfile --shamefully-hoist\`"
echo
echo "## Install Log Tail (last 200 lines)"
echo '```text'
tail -n 200 reports/front-coverage-install.log
echo '```'
} > reports/front-vitest-coverage-failure-report.md
exit 1
fi
done
- name: Run adminfront Vitest coverage
run: |
set +e
cd adminfront
pnpm run test:coverage 2>&1 | tee ../reports/adminfront-vitest-coverage.log
test_exit_code=${PIPESTATUS[0]}
cd ..
set -e
if [ "$test_exit_code" -ne 0 ]; then
{
echo "# Front Vitest Coverage Failure Report"
echo
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
echo "- Job: \`front-vitest-coverage\`"
echo "- Package: \`adminfront\`"
echo "- Exit Code: \`$test_exit_code\`"
echo
echo "## Log Tail (last 200 lines)"
echo '```text'
tail -n 200 reports/adminfront-vitest-coverage.log
echo '```'
} > reports/front-vitest-coverage-failure-report.md
exit 1
fi
- name: Run devfront Vitest coverage
run: |
set +e
cd devfront
pnpm run test:coverage 2>&1 | tee ../reports/devfront-vitest-coverage.log
test_exit_code=${PIPESTATUS[0]}
cd ..
set -e
if [ "$test_exit_code" -ne 0 ]; then
{
echo "# Front Vitest Coverage Failure Report"
echo
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
echo "- Job: \`front-vitest-coverage\`"
echo "- Package: \`devfront\`"
echo "- Exit Code: \`$test_exit_code\`"
echo
echo "## Log Tail (last 200 lines)"
echo '```text'
tail -n 200 reports/devfront-vitest-coverage.log
echo '```'
} > reports/front-vitest-coverage-failure-report.md
exit 1
fi
- name: Run orgfront Vitest coverage
run: |
set +e
cd orgfront
pnpm run test:coverage 2>&1 | tee ../reports/orgfront-vitest-coverage.log
test_exit_code=${PIPESTATUS[0]}
cd ..
set -e
if [ "$test_exit_code" -ne 0 ]; then
{
echo "# Front Vitest Coverage Failure Report"
echo
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
echo "- Job: \`front-vitest-coverage\`"
echo "- Package: \`orgfront\`"
echo "- Exit Code: \`$test_exit_code\`"
echo
echo "## Log Tail (last 200 lines)"
echo '```text'
tail -n 200 reports/orgfront-vitest-coverage.log
echo '```'
} > reports/front-vitest-coverage-failure-report.md
exit 1
fi
- name: Generate Vitest coverage summary
run: |
node scripts/summarize_vitest_coverage.mjs
cat reports/vitest-coverage-summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Publish front Vitest coverage failure summary
if: ${{ failure() }}
run: |
if [ -f reports/front-vitest-coverage-failure-report.md ]; then
cat reports/front-vitest-coverage-failure-report.md >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload front Vitest coverage report artifact
if: ${{ always() }}
uses: actions/upload-artifact@v3
continue-on-error: true
with:
name: front-vitest-coverage-report
path: |
reports/vitest-coverage-summary.md
reports/vitest-coverage-summary.json
reports/front-vitest-coverage-failure-report.md
reports/front-coverage-install.log
reports/adminfront-vitest-coverage.log
reports/devfront-vitest-coverage.log
reports/orgfront-vitest-coverage.log
adminfront/coverage
devfront/coverage
orgfront/coverage
if-no-files-found: ignore
adminfront-tests: adminfront-tests:
needs: lint needs: lint
if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_adminfront_tests == true) }} if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_adminfront_tests == true) }}
@@ -696,7 +940,10 @@ jobs:
run: | run: |
mkdir -p ../reports mkdir -p ../reports
set +e set +e
pnpm install -C ../common --no-frozen-lockfile 2>&1 | tee ../reports/devfront-install.log {
pnpm install -C ../common --no-frozen-lockfile
pnpm install --no-frozen-lockfile
} 2>&1 | tee ../reports/devfront-install.log
install_exit_code=${PIPESTATUS[0]} install_exit_code=${PIPESTATUS[0]}
set -e set -e
@@ -874,7 +1121,10 @@ jobs:
set +e set +e
cd orgfront cd orgfront
npm install -g pnpm npm install -g pnpm
pnpm install -C ../common --no-frozen-lockfile 2>&1 | tee ../reports/orgfront-install.log {
pnpm install -C ../common --no-frozen-lockfile
pnpm install --no-frozen-lockfile
} 2>&1 | tee ../reports/orgfront-install.log
install_exit_code=${PIPESTATUS[0]} install_exit_code=${PIPESTATUS[0]}
cd .. cd ..
set -e set -e
@@ -1021,3 +1271,63 @@ jobs:
orgfront/playwright-report orgfront/playwright-report
orgfront/test-results orgfront/test-results
if-no-files-found: ignore if-no-files-found: ignore
badge-updater:
needs:
- lint
- biome-check
- backend-tests
- userfront-tests
- userfront-e2e-tests
- front-vitest-coverage
- adminfront-tests
- devfront-tests
- orgfront-tests
if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Download Vitest coverage report artifact
uses: actions/download-artifact@v3
continue-on-error: true
with:
name: front-vitest-coverage-report
path: badge-artifacts/front-vitest-coverage-report
- name: Update badge files
env:
LINT_RESULT: ${{ needs.lint.result }}
BIOME_RESULT: ${{ needs['biome-check'].result }}
BACKEND_RESULT: ${{ needs['backend-tests'].result }}
USERFRONT_RESULT: ${{ needs['userfront-tests'].result }}
USERFRONT_E2E_RESULT: ${{ needs['userfront-e2e-tests'].result }}
USERFRONT_E2E_FULL: ${{ github.event_name == 'workflow_dispatch' && inputs.run_userfront_e2e_full == true }}
COVERAGE_RESULT: ${{ needs['front-vitest-coverage'].result }}
ADMINFRONT_RESULT: ${{ needs['adminfront-tests'].result }}
DEVFRONT_RESULT: ${{ needs['devfront-tests'].result }}
ORGFRONT_RESULT: ${{ needs['orgfront-tests'].result }}
run: |
node scripts/update_code_check_badges.mjs
cat docs/badges/badges.json
- name: Commit badge updates
run: |
if [ -z "$(git status --porcelain docs/badges)" ]; then
echo "No badge changes."
exit 0
fi
git config user.name "gitea-actions"
git config user.email "gitea-actions@hmac.kr"
git add docs/badges
git commit -m "chore: update code check badges [skip ci]"
git push

3
.gitignore vendored
View File

@@ -50,6 +50,9 @@ orgfront/test-results/
adminfront/playwright-report/ adminfront/playwright-report/
devfront/playwright-report/ devfront/playwright-report/
orgfront/playwright-report/ orgfront/playwright-report/
adminfront/coverage/
devfront/coverage/
orgfront/coverage/
orgfront/node_modules/ orgfront/node_modules/
orgfront/dist/ orgfront/dist/
orgfront/.vite/ orgfront/.vite/

View File

@@ -1,5 +1,15 @@
# Baron SSO # Baron SSO
[![Code Check](docs/badges/code-check.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
[![Biome](docs/badges/biome.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
[![userfront e2e fast](docs/badges/userfront-e2e-fast.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
[![userfront e2e full](docs/badges/userfront-e2e-full.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
[![adminfront coverage](docs/badges/adminfront-coverage.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
[![devfront coverage](docs/badges/devfront-coverage.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
[![orgfront coverage](docs/badges/orgfront-coverage.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev)
badge는 `Code Check`가 dev 브랜치에서 갱신합니다. 최신 HTML/LCOV/JSON summary는 Gitea `Code Check``front-vitest-coverage-report` artifact에서 확인할 수 있습니다.
**Baron 로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다. **Baron 로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다.
## 📂 프로젝트 구조 (Project Structure) ## 📂 프로젝트 구조 (Project Structure)

View File

@@ -1,6 +1,4 @@
{ {
"extends": ["../common/config/biome.base.json"], "root": true,
"files": { "extends": ["../common/config/biome.base.json"]
"ignore": [".vite"]
}
} }

View File

@@ -32,6 +32,7 @@
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.16",
"@playwright/test": "^1.60.0", "@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
@@ -41,13 +42,15 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "4.1.6",
"autoprefixer": "^10.5.0", "autoprefixer": "^10.5.0",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
"playwright": "1.60.0",
"postcss": "^8.5.14", "postcss": "^8.5.14",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.12", "vite": "^8.0.14",
"vitest": "^4.1.6" "vitest": "^4.1.6"
}, },
"engines": { "engines": {
@@ -130,14 +133,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.28.5", "@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
@@ -145,17 +148,42 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-string-parser": {
"version": "7.28.5", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.29.2", "version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
@@ -166,6 +194,205 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@biomejs/biome": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz",
"integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.21.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.4.16",
"@biomejs/cli-darwin-x64": "2.4.16",
"@biomejs/cli-linux-arm64": "2.4.16",
"@biomejs/cli-linux-arm64-musl": "2.4.16",
"@biomejs/cli-linux-x64": "2.4.16",
"@biomejs/cli-linux-x64-musl": "2.4.16",
"@biomejs/cli-win32-arm64": "2.4.16",
"@biomejs/cli-win32-x64": "2.4.16"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz",
"integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz",
"integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz",
"integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz",
"integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz",
"integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz",
"integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz",
"integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz",
"integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT OR Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.21.3"
}
},
"node_modules/@bramus/specificity": { "node_modules/@bramus/specificity": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
@@ -506,9 +733,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.130.0", "version": "0.132.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -2002,9 +2229,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2019,9 +2246,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2036,9 +2263,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2053,9 +2280,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2070,9 +2297,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2087,13 +2314,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2104,13 +2334,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2121,13 +2354,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2138,13 +2374,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2155,13 +2394,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2172,13 +2414,16 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2189,9 +2434,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2206,9 +2451,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -2225,9 +2470,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2242,9 +2487,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2572,6 +2817,37 @@
} }
} }
}, },
"node_modules/@vitest/coverage-v8": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz",
"integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.6",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"magicast": "^0.5.2",
"obug": "^2.1.1",
"std-env": "^4.0.0-rc.1",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "4.1.6",
"vitest": "4.1.6"
},
"peerDependenciesMeta": {
"@vitest/browser": {
"optional": true
}
}
},
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
@@ -2782,6 +3058,25 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/ast-v8-to-istanbul": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz",
"integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.31",
"estree-walker": "^3.0.3",
"js-tokens": "^10.0.0"
}
},
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3541,6 +3836,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -3593,6 +3898,13 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0" "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
} }
}, },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/http-proxy-agent": { "node_modules/http-proxy-agent": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -3709,6 +4021,45 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "1.21.7", "version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -4122,6 +4473,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/magicast": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.3",
"@babel/types": "^7.29.0",
"source-map-js": "^1.2.1"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4390,9 +4769,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.14", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4410,7 +4789,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -4854,13 +5233,13 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.130.0", "@oxc-project/types": "=0.132.0",
"@rolldown/pluginutils": "^1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
@@ -4870,21 +5249,21 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-android-arm64": "1.0.2",
"@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.2",
"@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.2",
"@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
"@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.2",
"@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.2",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.2",
"@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.2",
"@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.2",
"@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.2",
"@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.2",
"@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.2",
"@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.2",
"@rolldown/binding-win32-x64-msvc": "1.0.1" "@rolldown/binding-win32-x64-msvc": "1.0.2"
} }
}, },
"node_modules/run-parallel": { "node_modules/run-parallel": {
@@ -4930,6 +5309,19 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-cookie-parser": { "node_modules/set-cookie-parser": {
"version": "2.7.2", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
@@ -5003,6 +5395,19 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -5373,16 +5778,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.13", "version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.14", "postcss": "^8.5.15",
"rolldown": "1.0.1", "rolldown": "1.0.2",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.16"
}, },
"bin": { "bin": {

View File

@@ -14,6 +14,7 @@
"format": "biome format . --write", "format": "biome format . --write",
"preview": "vite preview", "preview": "vite preview",
"test": "playwright test", "test": "playwright test",
"test:coverage": "vitest run --coverage",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:ui": "playwright test --ui", "test:ui": "playwright test --ui",
"i18n-scan": "cd .. && node tools/i18n-scanner/index.js && node tools/i18n-scanner/report.js" "i18n-scan": "cd .. && node tools/i18n-scanner/index.js && node tools/i18n-scanner/report.js"
@@ -43,6 +44,7 @@
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.16",
"@playwright/test": "^1.60.0", "@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
@@ -52,8 +54,10 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "4.1.6",
"autoprefixer": "^10.5.0", "autoprefixer": "^10.5.0",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
"playwright": "1.60.0",
"postcss": "^8.5.14", "postcss": "^8.5.14",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@@ -75,6 +75,9 @@ importers:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3 version: 4.4.3
devDependencies: devDependencies:
'@biomejs/biome':
specifier: 2.4.16
version: 2.4.16
'@playwright/test': '@playwright/test':
specifier: ^1.60.0 specifier: ^1.60.0
version: 1.60.0 version: 1.60.0
@@ -102,12 +105,18 @@ importers:
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.2(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7)) version: 6.0.2(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7))
'@vitest/coverage-v8':
specifier: 4.1.6
version: 4.1.6(vitest@4.1.6)
autoprefixer: autoprefixer:
specifier: ^10.5.0 specifier: ^10.5.0
version: 10.5.0(postcss@8.5.14) version: 10.5.0(postcss@8.5.14)
jsdom: jsdom:
specifier: ^28.1.0 specifier: ^28.1.0
version: 28.1.0 version: 28.1.0
playwright:
specifier: 1.60.0
version: 1.60.0
postcss: postcss:
specifier: ^8.5.14 specifier: ^8.5.14
version: 8.5.14 version: 8.5.14
@@ -125,7 +134,7 @@ importers:
version: 8.0.14(@types/node@25.8.0)(jiti@1.21.7) version: 8.0.14(@types/node@25.8.0)(jiti@1.21.7)
vitest: vitest:
specifier: ^4.1.6 specifier: ^4.1.6
version: 4.1.6(@types/node@25.8.0)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7)) version: 4.1.6(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7))
packages: packages:
@@ -157,14 +166,92 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5': '@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.29.2': '@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@biomejs/biome@2.4.16':
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.4.16':
resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.4.16':
resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.4.16':
resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@2.4.16':
resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.16':
resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@2.4.16':
resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.16':
resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.4.16':
resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@bramus/specificity@2.4.2': '@bramus/specificity@2.4.2':
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true hasBin: true
@@ -877,6 +964,15 @@ packages:
babel-plugin-react-compiler: babel-plugin-react-compiler:
optional: true optional: true
'@vitest/coverage-v8@4.1.6':
resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==}
peerDependencies:
'@vitest/browser': 4.1.6
vitest: 4.1.6
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@4.1.6': '@vitest/expect@4.1.6':
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
@@ -947,6 +1043,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
ast-v8-to-istanbul@1.0.2:
resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==}
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -1198,6 +1297,10 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1214,6 +1317,9 @@ packages:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
@@ -1253,10 +1359,25 @@ packages:
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
jiti@1.21.7: jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true hasBin: true
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1370,6 +1491,13 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.5.3:
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1645,6 +1773,11 @@ packages:
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
semver@7.8.1:
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
engines: {node: '>=10'}
hasBin: true
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
@@ -1670,6 +1803,10 @@ packages:
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
hasBin: true hasBin: true
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-preserve-symlinks-flag@1.0.0: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1932,10 +2069,60 @@ snapshots:
js-tokens: 4.0.0 js-tokens: 4.0.0
picocolors: 1.1.1 picocolors: 1.1.1
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/parser@7.29.7':
dependencies:
'@babel/types': 7.29.7
'@babel/runtime@7.29.2': {} '@babel/runtime@7.29.2': {}
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.4.16':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.16
'@biomejs/cli-darwin-x64': 2.4.16
'@biomejs/cli-linux-arm64': 2.4.16
'@biomejs/cli-linux-arm64-musl': 2.4.16
'@biomejs/cli-linux-x64': 2.4.16
'@biomejs/cli-linux-x64-musl': 2.4.16
'@biomejs/cli-win32-arm64': 2.4.16
'@biomejs/cli-win32-x64': 2.4.16
'@biomejs/cli-darwin-arm64@2.4.16':
optional: true
'@biomejs/cli-darwin-x64@2.4.16':
optional: true
'@biomejs/cli-linux-arm64-musl@2.4.16':
optional: true
'@biomejs/cli-linux-arm64@2.4.16':
optional: true
'@biomejs/cli-linux-x64-musl@2.4.16':
optional: true
'@biomejs/cli-linux-x64@2.4.16':
optional: true
'@biomejs/cli-win32-arm64@2.4.16':
optional: true
'@biomejs/cli-win32-x64@2.4.16':
optional: true
'@bramus/specificity@2.4.2': '@bramus/specificity@2.4.2':
dependencies: dependencies:
css-tree: 3.2.1 css-tree: 3.2.1
@@ -2576,6 +2763,20 @@ snapshots:
'@rolldown/pluginutils': 1.0.1 '@rolldown/pluginutils': 1.0.1
vite: 8.0.14(@types/node@25.8.0)(jiti@1.21.7) vite: 8.0.14(@types/node@25.8.0)(jiti@1.21.7)
'@vitest/coverage-v8@4.1.6(vitest@4.1.6)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.6
ast-v8-to-istanbul: 1.0.2
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.3
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.6(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7))
'@vitest/expect@4.1.6': '@vitest/expect@4.1.6':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -2650,6 +2851,12 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
ast-v8-to-istanbul@1.0.2:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
asynckit@0.4.0: {} asynckit@0.4.0: {}
autoprefixer@10.5.0(postcss@8.5.14): autoprefixer@10.5.0(postcss@8.5.14):
@@ -2882,6 +3089,8 @@ snapshots:
gopd@1.2.0: {} gopd@1.2.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2: has-tostringtag@1.0.2:
@@ -2898,6 +3107,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@noble/hashes' - '@noble/hashes'
html-escaper@2.0.2: {}
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2:
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
@@ -2939,8 +3150,23 @@ snapshots:
is-potential-custom-element-name@1.0.1: {} is-potential-custom-element-name@1.0.1: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jiti@1.21.7: {} jiti@1.21.7: {}
js-tokens@10.0.0: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
jsdom@28.1.0: jsdom@28.1.0:
@@ -3037,6 +3263,16 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
magicast@0.5.3:
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.8.1
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
mdn-data@2.27.1: {} mdn-data@2.27.1: {}
@@ -3275,6 +3511,8 @@ snapshots:
scheduler@0.27.0: {} scheduler@0.27.0: {}
semver@7.8.1: {}
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
siginfo@2.0.0: {} siginfo@2.0.0: {}
@@ -3299,6 +3537,10 @@ snapshots:
tinyglobby: 0.2.16 tinyglobby: 0.2.16
ts-interface-checker: 0.1.13 ts-interface-checker: 0.1.13
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
symbol-tree@3.2.4: {} symbol-tree@3.2.4: {}
@@ -3423,7 +3665,7 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 1.21.7 jiti: 1.21.7
vitest@4.1.6(@types/node@25.8.0)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7)): vitest@4.1.6(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7)):
dependencies: dependencies:
'@vitest/expect': 4.1.6 '@vitest/expect': 4.1.6
'@vitest/mocker': 4.1.6(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7)) '@vitest/mocker': 4.1.6(vite@8.0.14(@types/node@25.8.0)(jiti@1.21.7))
@@ -3447,6 +3689,7 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 25.8.0 '@types/node': 25.8.0
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
jsdom: 28.1.0 jsdom: 28.1.0
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw

View File

@@ -1,2 +1,3 @@
allowBuilds: allowBuilds:
'@biomejs/biome': true
esbuild: false esbuild: false

View File

@@ -1,5 +1,5 @@
import { createServer } from "node:http";
import { readFile, stat } from "node:fs/promises"; import { readFile, stat } from "node:fs/promises";
import { createServer } from "node:http";
import { extname, join, normalize, resolve } from "node:path"; import { extname, join, normalize, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -24,7 +24,9 @@ const contentTypes = {
}; };
function getContentType(filePath) { function getContentType(filePath) {
return contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream"; return (
contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream"
);
} }
function sendJson(res, statusCode, body) { function sendJson(res, statusCode, body) {
@@ -132,7 +134,10 @@ async function serveStatic(req, res, pathname) {
createServer(async (req, res) => { createServer(async (req, res) => {
try { try {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); const url = new URL(
req.url ?? "/",
`http://${req.headers.host ?? "localhost"}`,
);
const { pathname, search } = url; const { pathname, search } = url;
if (pathname === "/api" || pathname.startsWith("/api/")) { if (pathname === "/api" || pathname.startsWith("/api/")) {
@@ -149,5 +154,7 @@ createServer(async (req, res) => {
}); });
} }
}).listen(port, host, () => { }).listen(port, host, () => {
console.log(`Adminfront production server listening on http://${host}:${port}`); console.log(
`Adminfront production server listening on http://${host}:${port}`,
);
}); });

View File

@@ -1,5 +1,5 @@
import { createBrowserRouter } from "react-router-dom";
import type { RouteObject } from "react-router-dom"; import type { RouteObject } from "react-router-dom";
import { createBrowserRouter } from "react-router-dom";
import AppLayout from "../components/layout/AppLayout"; import AppLayout from "../components/layout/AppLayout";
import ApiKeyCreatePage from "../features/api-keys/ApiKeyCreatePage"; import ApiKeyCreatePage from "../features/api-keys/ApiKeyCreatePage";
import ApiKeyListPage from "../features/api-keys/ApiKeyListPage"; import ApiKeyListPage from "../features/api-keys/ApiKeyListPage";

View File

@@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import LanguageSelector from "./LanguageSelector"; import LanguageSelector from "./LanguageSelector";

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LOCALE_STORAGE_KEY } from "../../../../common/core/i18n"; import { LOCALE_STORAGE_KEY } from "../../../../common/core/i18n";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
const SUPPORTED_LOCALES = ["ko", "en"] as const; const SUPPORTED_LOCALES = ["ko", "en"] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number]; type Locale = (typeof SUPPORTED_LOCALES)[number];

View File

@@ -22,13 +22,13 @@ import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { import {
AppSidebar, AppSidebar,
type ShellSidebarNavItem,
type ShellTranslator,
applyShellTheme, applyShellTheme,
buildShellProfileSummary, buildShellProfileSummary,
buildShellSessionStatus, buildShellSessionStatus,
readShellSessionExpiryEnabled, readShellSessionExpiryEnabled,
readShellTheme, readShellTheme,
type ShellSidebarNavItem,
type ShellTranslator,
shellLayoutClasses, shellLayoutClasses,
writeShellSessionExpiryEnabled, writeShellSessionExpiryEnabled,
} from "../../../../common/shell"; } from "../../../../common/shell";
@@ -310,13 +310,16 @@ function AppLayout() {
window.addEventListener(DEV_ROLE_CHANGED_EVENT, rerenderDevelopmentShell); window.addEventListener(DEV_ROLE_CHANGED_EVENT, rerenderDevelopmentShell);
return () => { return () => {
window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); window.removeEventListener(
LOCALE_CHANGED_EVENT,
rerenderDevelopmentShell,
);
window.removeEventListener( window.removeEventListener(
DEV_ROLE_CHANGED_EVENT, DEV_ROLE_CHANGED_EVENT,
rerenderDevelopmentShell, rerenderDevelopmentShell,
); );
}; };
}, [isDevelopmentRuntime]); }, []);
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
@@ -429,7 +432,6 @@ function AppLayout() {
auth.isAuthenticated, auth.isAuthenticated,
auth.isLoading, auth.isLoading,
auth.user?.expires_at, auth.user?.expires_at,
isDevelopmentRuntime,
isSessionExpiryEnabled, isSessionExpiryEnabled,
]); ]);
@@ -668,7 +670,10 @@ function AppLayout() {
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">
{t("ui.shell.session.auto_extend", "세션 만료 관리")} {t(
"ui.shell.session.auto_extend",
"세션 만료 관리",
)}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{isSessionExpiryEnabled ? ( {isSessionExpiryEnabled ? (
@@ -677,7 +682,10 @@ function AppLayout() {
t={t} t={t}
/> />
) : ( ) : (
t("ui.shell.session.disabled", "세션 만료 비활성화") t(
"ui.shell.session.disabled",
"세션 만료 비활성화",
)
)} )}
</p> </p>
</div> </div>

View File

@@ -44,4 +44,4 @@ const AvatarFallback = React.forwardRef<
)); ));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback }; export { Avatar, AvatarFallback, AvatarImage };

View File

@@ -50,9 +50,9 @@ function CardFooter({
export { export {
Card, Card,
CardContent,
CardDescription,
CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription,
CardContent,
CardFooter,
}; };

View File

@@ -144,18 +144,20 @@ const DialogClose = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
DialogClose.displayName = "DialogClose"; DialogClose.displayName = "DialogClose";
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
HTMLDivElement, HTMLButtonElement,
React.HTMLAttributes<HTMLDivElement> React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, onMouseDown, ...props }, ref) => { >(({ className, onMouseDown, ...props }, ref) => {
const { setOpen } = useDialogContext("DialogOverlay"); const { setOpen } = useDialogContext("DialogOverlay");
return ( return (
<div <button
type="button"
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 border-0 bg-black/80 p-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className, className,
)} )}
data-state="open" data-state="open"
aria-label="Close dialog"
onMouseDown={composeEventHandlers(onMouseDown, (event) => { onMouseDown={composeEventHandlers(onMouseDown, (event) => {
if (event.target === event.currentTarget) { if (event.target === event.currentTarget) {
setOpen(false); setOpen(false);
@@ -273,13 +275,13 @@ DialogDescription.displayName = "DialogDescription";
export { export {
Dialog, Dialog,
DialogPortal,
DialogOverlay,
DialogClose, DialogClose,
DialogTrigger,
DialogContent, DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription, DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}; };

View File

@@ -183,18 +183,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioItem, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuTrigger,
}; };

View File

@@ -146,13 +146,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { export {
Select, Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent, SelectContent,
SelectLabel, SelectGroup,
SelectItem, SelectItem,
SelectSeparator, SelectLabel,
SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}; };

View File

@@ -92,11 +92,11 @@ TableCaption.displayName = "TableCaption";
export { export {
Table, Table,
TableHeader,
TableBody, TableBody,
TableCaption,
TableCell,
TableFooter, TableFooter,
TableHead, TableHead,
TableHeader,
TableRow, TableRow,
TableCell,
TableCaption,
}; };

View File

@@ -84,4 +84,4 @@ const TabsContent = React.forwardRef<
}); });
TabsContent.displayName = "TabsContent"; TabsContent.displayName = "TabsContent";
export { Tabs, TabsList, TabsTrigger, TabsContent }; export { Tabs, TabsContent, TabsList, TabsTrigger };

View File

@@ -31,6 +31,8 @@ describe("AuthPage", () => {
expect(screen.getByText("Auth Guard")).toBeInTheDocument(); expect(screen.getByText("Auth Guard")).toBeInTheDocument();
expect(screen.getByText("ReBAC permission checker")).toBeInTheDocument(); expect(screen.getByText("ReBAC permission checker")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Check permission" })).toBeInTheDocument(); expect(
screen.getByRole("button", { name: "Check permission" }),
).toBeInTheDocument();
}); });
}); });

View File

@@ -42,7 +42,9 @@ describe("LoginPage", () => {
it("shows an actionable error instead of starting PKCE when WebCrypto is unavailable", async () => { it("shows an actionable error instead of starting PKCE when WebCrypto is unavailable", async () => {
renderLoginPage("/login?returnTo=%2F"); renderLoginPage("/login?returnTo=%2F");
await userEvent.click(screen.getByRole("button", { name: /SSO 계정으로 로그인/i })); await userEvent.click(
screen.getByRole("button", { name: /SSO 계정으로 로그인/i }),
);
expect(mockSigninRedirect).not.toHaveBeenCalled(); expect(mockSigninRedirect).not.toHaveBeenCalled();
expect(screen.getByRole("alert")).toHaveTextContent( expect(screen.getByRole("alert")).toHaveTextContent(
@@ -61,7 +63,9 @@ describe("LoginPage", () => {
}); });
renderLoginPage("/login?returnTo=%2Fusers%3Fpage%3D2"); renderLoginPage("/login?returnTo=%2Fusers%3Fpage%3D2");
await userEvent.click(screen.getByRole("button", { name: /SSO 계정으로 로그인/i })); await userEvent.click(
screen.getByRole("button", { name: /SSO 계정으로 로그인/i }),
);
expect(mockSigninRedirect).toHaveBeenCalledWith({ expect(mockSigninRedirect).toHaveBeenCalledWith({
state: { state: {

View File

@@ -48,10 +48,7 @@ function PermissionChecker() {
<Card className="border-primary/20 bg-[var(--color-panel)]"> <Card className="border-primary/20 bg-[var(--color-panel)]">
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-bold"> <CardTitle className="text-lg font-bold">
{t( {t("ui.admin.auth_guard.checker.title", "ReBAC permission checker")}
"ui.admin.auth_guard.checker.title",
"ReBAC permission checker",
)}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
{t( {t(
@@ -92,7 +89,9 @@ function PermissionChecker() {
</select> </select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("ui.admin.auth_guard.checker.relation", "Relation")}</Label> <Label>
{t("ui.admin.auth_guard.checker.relation", "Relation")}
</Label>
<Input <Input
placeholder={t( placeholder={t(
"ui.admin.auth_guard.checker.relation_placeholder", "ui.admin.auth_guard.checker.relation_placeholder",
@@ -103,7 +102,9 @@ function PermissionChecker() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("ui.admin.auth_guard.checker.object_id", "Object ID")}</Label> <Label>
{t("ui.admin.auth_guard.checker.object_id", "Object ID")}
</Label>
<Input <Input
placeholder={t( placeholder={t(
"ui.admin.auth_guard.checker.object_id_placeholder", "ui.admin.auth_guard.checker.object_id_placeholder",
@@ -115,10 +116,7 @@ function PermissionChecker() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label> <Label>
{t( {t("ui.admin.auth_guard.checker.subject", "Subject (User:ID)")}
"ui.admin.auth_guard.checker.subject",
"Subject (User:ID)",
)}
</Label> </Label>
<Input <Input
placeholder={t( placeholder={t(
@@ -155,10 +153,7 @@ function PermissionChecker() {
<> <>
<CheckCircle2 size={48} /> <CheckCircle2 size={48} />
<div className="text-lg font-bold"> <div className="text-lg font-bold">
{t( {t("ui.admin.auth_guard.checker.allowed", "Access ALLOWED")}
"ui.admin.auth_guard.checker.allowed",
"Access ALLOWED",
)}
</div> </div>
<p className="text-center text-sm opacity-80"> <p className="text-center text-sm opacity-80">
{t( {t(
@@ -171,10 +166,7 @@ function PermissionChecker() {
<> <>
<XCircle size={48} /> <XCircle size={48} />
<div className="text-lg font-bold"> <div className="text-lg font-bold">
{t( {t("ui.admin.auth_guard.checker.denied", "Access DENIED")}
"ui.admin.auth_guard.checker.denied",
"Access DENIED",
)}
</div> </div>
<p className="text-center text-sm opacity-80"> <p className="text-center text-sm opacity-80">
{t( {t(

View File

@@ -175,16 +175,16 @@ describe("DataIntegrityPage", () => {
window.localStorage.setItem("locale", "en"); window.localStorage.setItem("locale", "en");
renderPage(); renderPage();
expect( expect(await screen.findByText("Data Integrity Check")).toBeInTheDocument();
await screen.findByText("Data Integrity Check"),
).toBeInTheDocument();
expect( expect(
await screen.findByText( await screen.findByText(
"Review integrity status and inspect checks across the admin data model.", "Review integrity status and inspect checks across the admin data model.",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(await screen.findByText("Tenant integrity")).toBeInTheDocument(); expect(await screen.findByText("Tenant integrity")).toBeInTheDocument();
expect(await screen.findByText("Duplicate tenant slug")).toBeInTheDocument(); expect(
await screen.findByText("Duplicate tenant slug"),
).toBeInTheDocument();
expect( expect(
await screen.findByText( await screen.findByText(
"Checks duplicate active tenant slugs using LOWER(TRIM(slug)).", "Checks duplicate active tenant slugs using LOWER(TRIM(slug)).",

View File

@@ -12,10 +12,10 @@ import { Button } from "../../components/ui/button";
import { import {
type DataIntegrityCheck, type DataIntegrityCheck,
type DataIntegrityStatus, type DataIntegrityStatus,
type OrphanUserLoginID,
deleteOrphanUserLoginIDs, deleteOrphanUserLoginIDs,
fetchDataIntegrityReport, fetchDataIntegrityReport,
fetchOrphanUserLoginIDs, fetchOrphanUserLoginIDs,
type OrphanUserLoginID,
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { getAdminDateLocale } from "../../lib/locale"; import { getAdminDateLocale } from "../../lib/locale";

View File

@@ -17,13 +17,13 @@ import {
import { RoleGuard } from "../../components/auth/RoleGuard"; import { RoleGuard } from "../../components/auth/RoleGuard";
import { import {
type DataIntegrityStatus, type DataIntegrityStatus,
type RPUsageDailyMetric,
type RPUsagePeriod,
type TenantSummary,
fetchAdminOverviewStats, fetchAdminOverviewStats,
fetchAdminRPUsageDaily, fetchAdminRPUsageDaily,
fetchAllTenants, fetchAllTenants,
fetchDataIntegrityReport, fetchDataIntegrityReport,
type RPUsageDailyMetric,
type RPUsagePeriod,
type TenantSummary,
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
@@ -203,10 +203,7 @@ function IntegrityOverviewSummary() {
<AlertTriangle size={18} className="text-amber-600" /> <AlertTriangle size={18} className="text-amber-600" />
)} )}
<h3 className="text-lg font-bold flex items-center gap-2"> <h3 className="text-lg font-bold flex items-center gap-2">
{t( {t("ui.admin.integrity.summary.title", "정합성 최종 검증")}
"ui.admin.integrity.summary.title",
"정합성 최종 검증",
)}
</h3> </h3>
</div> </div>
<div className="flex flex-wrap items-center gap-3 text-sm"> <div className="flex flex-wrap items-center gap-3 text-sm">
@@ -466,7 +463,7 @@ function GlobalOverviewPage() {
const metric = (value: number | undefined) => const metric = (value: number | undefined) =>
value === undefined ? "-" : value.toLocaleString(); value === undefined ? "-" : value.toLocaleString();
const periodControls = ( const periodControls = (
<div className="flex h-8 items-center gap-1" aria-label="집계 단위"> <fieldset className="flex h-8 items-center gap-1" aria-label="집계 단위">
{[ {[
["day", t("ui.common.chart.period.day", "일")], ["day", t("ui.common.chart.period.day", "일")],
["week", t("ui.common.chart.period.week", "주")], ["week", t("ui.common.chart.period.week", "주")],
@@ -486,7 +483,7 @@ function GlobalOverviewPage() {
{label} {label}
</button> </button>
))} ))}
</div> </fieldset>
); );
const chartFilters = ( const chartFilters = (
<div> <div>

View File

@@ -61,17 +61,13 @@ describe("UserProjectionPage", () => {
it("renders projection status for super_admin", async () => { it("renders projection status for super_admin", async () => {
renderPage(); renderPage();
expect( expect(await screen.findByText("사용자 동기화 관리")).toBeInTheDocument();
await screen.findByText("사용자 동기화 관리"),
).toBeInTheDocument();
expect( expect(
await screen.findByText( await screen.findByText(
"Kratos 사용자 read model을 확인하고 동기화 상태를 갱신합니다.", "Kratos 사용자 read model을 확인하고 동기화 상태를 갱신합니다.",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(await screen.findByText("Kratos 사용자 동기화")).toBeInTheDocument();
await screen.findByText("Kratos 사용자 동기화"),
).toBeInTheDocument();
expect(screen.getByText("준비됨")).toBeInTheDocument(); expect(screen.getByText("준비됨")).toBeInTheDocument();
expect(screen.getByText("152")).toBeInTheDocument(); expect(screen.getByText("152")).toBeInTheDocument();
expect(fetchUserProjectionStatus).toHaveBeenCalled(); expect(fetchUserProjectionStatus).toHaveBeenCalled();
@@ -100,9 +96,7 @@ describe("UserProjectionPage", () => {
renderPage(); renderPage();
expect(await screen.findByText("접근 권한이 없습니다")).toBeInTheDocument(); expect(await screen.findByText("접근 권한이 없습니다")).toBeInTheDocument();
expect( expect(screen.queryByText("사용자 동기화 관리")).not.toBeInTheDocument();
screen.queryByText("사용자 동기화 관리"),
).not.toBeInTheDocument();
expect(fetchUserProjectionStatus).not.toHaveBeenCalled(); expect(fetchUserProjectionStatus).not.toHaveBeenCalled();
}); });

View File

@@ -133,10 +133,7 @@ function UserProjectionContent() {
disabled={isWorking} disabled={isWorking}
> >
<RotateCcw size={16} /> <RotateCcw size={16} />
{t( {t("ui.admin.user_projection.actions.reset", "Reset and rebuild")}
"ui.admin.user_projection.actions.reset",
"Reset and rebuild",
)}
</Button> </Button>
</div> </div>
</header> </header>
@@ -230,10 +227,7 @@ function UserProjectionContent() {
</div> </div>
<div> <div>
<dt className="text-sm text-muted-foreground"> <dt className="text-sm text-muted-foreground">
{t( {t("ui.admin.user_projection.summary.updated_at", "Updated at")}
"ui.admin.user_projection.summary.updated_at",
"Updated at",
)}
</dt> </dt>
<dd className="mt-1 text-sm"> <dd className="mt-1 text-sm">
{formatDateTime(data?.updatedAt)} {formatDateTime(data?.updatedAt)}
@@ -261,10 +255,7 @@ export default function UserProjectionPage() {
<main className="p-6 md:p-8"> <main className="p-6 md:p-8">
<section className="rounded-lg border border-border bg-card p-5"> <section className="rounded-lg border border-border bg-card p-5">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{t( {t("ui.admin.user_projection.forbidden.title", "Access denied")}
"ui.admin.user_projection.forbidden.title",
"Access denied",
)}
</h2> </h2>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
{t( {t(

View File

@@ -7,8 +7,8 @@ import {
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTrigger,
DialogTitle, DialogTitle,
DialogTrigger,
} from "../../../components/ui/dialog"; } from "../../../components/ui/dialog";
import { Label } from "../../../components/ui/label"; import { Label } from "../../../components/ui/label";
import type { TenantSummary } from "../../../lib/adminApi"; import type { TenantSummary } from "../../../lib/adminApi";

View File

@@ -41,7 +41,6 @@ import {
} from "../../../components/ui/table"; } from "../../../components/ui/table";
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import { import {
type TenantAdmin,
addTenantAdmin, addTenantAdmin,
addTenantOwner, addTenantOwner,
fetchTenantAdmins, fetchTenantAdmins,
@@ -49,6 +48,7 @@ import {
fetchUsers, fetchUsers,
removeTenantAdmin, removeTenantAdmin,
removeTenantOwner, removeTenantOwner,
type TenantAdmin,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
@@ -69,15 +69,14 @@ export function TenantAdminsAndOwnersTab() {
const auth = useAuth(); const auth = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const currentUserId = auth.user?.profile.sub; const currentUserId = auth.user?.profile.sub;
const { tenantId } = useParams<{ tenantId: string }>(); const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
const tenantId = tenantIdParam ?? "";
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [dialogMode, setDialogMode] = useState<DialogMode | null>(null); const [dialogMode, setDialogMode] = useState<DialogMode | null>(null);
const [pendingOwners, setPendingOwners] = useState<TenantAdmin[]>([]); const [pendingOwners, setPendingOwners] = useState<TenantAdmin[]>([]);
const [pendingAdmins, setPendingAdmins] = useState<TenantAdmin[]>([]); const [pendingAdmins, setPendingAdmins] = useState<TenantAdmin[]>([]);
if (!tenantId) return null;
const ownersQuery = useQuery({ const ownersQuery = useQuery({
queryKey: ["tenant-owners", tenantId], queryKey: ["tenant-owners", tenantId],
queryFn: () => fetchTenantOwners(tenantId), queryFn: () => fetchTenantOwners(tenantId),
@@ -339,6 +338,8 @@ export function TenantAdminsAndOwnersTab() {
} }
}; };
if (!tenantId) return null;
const serverOwners = ownersQuery.data || []; const serverOwners = ownersQuery.data || [];
const serverAdmins = adminsQuery.data || []; const serverAdmins = adminsQuery.data || [];
const currentOwners = mergePendingMembers(serverOwners, pendingOwners); const currentOwners = mergePendingMembers(serverOwners, pendingOwners);

View File

@@ -19,15 +19,15 @@ import { t } from "../../../lib/i18n";
import { DomainTagInput } from "../components/DomainTagInput"; import { DomainTagInput } from "../components/DomainTagInput";
import { ParentTenantSelector } from "../components/ParentTenantSelector"; import { ParentTenantSelector } from "../components/ParentTenantSelector";
import { import {
type ServerDomainConflict,
formatDomainConflictMessage, formatDomainConflictMessage,
type ServerDomainConflict,
} from "../utils/domainTags"; } from "../utils/domainTags";
import { import {
mergeTenantOrgConfig,
ORG_UNIT_TYPE_OPTIONS, ORG_UNIT_TYPE_OPTIONS,
shouldAllowHanmacOrgConfig,
TENANT_VISIBILITY_OPTIONS, TENANT_VISIBILITY_OPTIONS,
type TenantVisibility, type TenantVisibility,
mergeTenantOrgConfig,
shouldAllowHanmacOrgConfig,
} from "../utils/orgConfig"; } from "../utils/orgConfig";
type AdminFrontTestHooks = { type AdminFrontTestHooks = {

View File

@@ -53,13 +53,13 @@ import {
} from "../../../components/ui/table"; } from "../../../components/ui/table";
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import { import {
type GroupSummary,
addGroupMember, addGroupMember,
createGroup, createGroup,
deleteGroup, deleteGroup,
fetchGroups, fetchGroups,
fetchTenant, fetchTenant,
fetchUsers, fetchUsers,
type GroupSummary,
removeGroupMember, removeGroupMember,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";

View File

@@ -27,6 +27,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { PageHeader } from "../../../../../common/core/components/page";
import { import {
SortableTableHead, SortableTableHead,
sortableTableHeadBaseClassName, sortableTableHeadBaseClassName,
@@ -38,7 +39,6 @@ import {
sortItems, sortItems,
toggleSort, toggleSort,
} from "../../../../../common/core/utils"; } from "../../../../../common/core/utils";
import { PageHeader } from "../../../../../common/core/components/page";
import { import {
commonStickyTableHeaderClass, commonStickyTableHeaderClass,
commonTableShellClass, commonTableShellClass,
@@ -92,18 +92,18 @@ import {
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import type { UserProfileResponse } from "../../../lib/adminApi"; import type { UserProfileResponse } from "../../../lib/adminApi";
import { import {
type TenantSummary,
deleteTenant, deleteTenant,
deleteTenantsBulk, deleteTenantsBulk,
exportTenantsCSV, exportTenantsCSV,
fetchMe, fetchMe,
fetchTenants, fetchTenants,
importTenantsCSV, importTenantsCSV,
type TenantSummary,
updateTenant, updateTenant,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
import { normalizeAdminRole } from "../../../lib/roles"; import { normalizeAdminRole } from "../../../lib/roles";
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
import { import {
buildAuthenticatedOrgChartTenantPickerUrl, buildAuthenticatedOrgChartTenantPickerUrl,
filterNonHanmacFamilyTenants, filterNonHanmacFamilyTenants,
@@ -112,20 +112,20 @@ import {
} from "../../users/orgChartPicker"; } from "../../users/orgChartPicker";
import { isSeedTenant } from "../utils/protectedTenants"; import { isSeedTenant } from "../utils/protectedTenants";
import { import {
type TenantImportPreviewRow,
type TenantImportResolution,
buildTenantImportParentOptionGroups, buildTenantImportParentOptionGroups,
buildTenantImportPreview, buildTenantImportPreview,
inferTenantImportRootParentSlug, inferTenantImportRootParentSlug,
parseTenantCSV, parseTenantCSV,
serializeTenantImportCSV, serializeTenantImportCSV,
type TenantImportPreviewRow,
type TenantImportResolution,
} from "../utils/tenantCsvImport"; } from "../utils/tenantCsvImport";
import { import {
type TenantViewMode,
type TenantViewRow,
filterTenantsByScope, filterTenantsByScope,
getTenantViewRows, getTenantViewRows,
resolveTenantSelectionIds, resolveTenantSelectionIds,
type TenantViewMode,
type TenantViewRow,
tenantMatchesListSearch, tenantMatchesListSearch,
} from "./tenantListView"; } from "./tenantListView";
@@ -453,30 +453,6 @@ function TenantListPage() {
}, },
}); });
if (
profile &&
profileRole !== "super_admin" &&
profileRole !== "tenant_admin"
) {
return (
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
<h3 className="text-lg font-bold">
{t("msg.admin.common.forbidden", "접근 권한이 없습니다.")}
</h3>
<Button onClick={() => navigate("/")}>
{t("ui.common.go_home", "홈으로 이동")}
</Button>
</div>
);
}
if (
profileRole === "tenant_admin" &&
(profile?.manageableTenants?.length ?? 0) <= 1
) {
return null;
}
const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
?.data?.error; ?.data?.error;
const fallbackError = const fallbackError =
@@ -574,6 +550,30 @@ function TenantListPage() {
return () => window.removeEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage);
}, [allTenants, scopePickerOpen]); }, [allTenants, scopePickerOpen]);
if (
profile &&
profileRole !== "super_admin" &&
profileRole !== "tenant_admin"
) {
return (
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
<h3 className="text-lg font-bold">
{t("msg.admin.common.forbidden", "접근 권한이 없습니다.")}
</h3>
<Button onClick={() => navigate("/")}>
{t("ui.common.go_home", "홈으로 이동")}
</Button>
</div>
);
}
if (
profileRole === "tenant_admin" &&
(profile?.manageableTenants?.length ?? 0) <= 1
) {
return null;
}
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
if (checked) { if (checked) {
setSelectedIds(deletableTenants.map((t) => t.id)); setSelectedIds(deletableTenants.map((t) => t.id));

View File

@@ -26,34 +26,30 @@ import { t } from "../../../lib/i18n";
import { DomainTagInput } from "../components/DomainTagInput"; import { DomainTagInput } from "../components/DomainTagInput";
import { ParentTenantSelector } from "../components/ParentTenantSelector"; import { ParentTenantSelector } from "../components/ParentTenantSelector";
import { import {
type ServerDomainConflict,
formatDomainConflictMessage, formatDomainConflictMessage,
type ServerDomainConflict,
} from "../utils/domainTags"; } from "../utils/domainTags";
import { import {
ORG_UNIT_TYPE_OPTIONS,
TENANT_VISIBILITY_OPTIONS,
type TenantVisibility,
mergeTenantOrgConfig, mergeTenantOrgConfig,
ORG_UNIT_TYPE_OPTIONS,
readTenantOrgConfig, readTenantOrgConfig,
removeTenantOrgConfig, removeTenantOrgConfig,
shouldAllowHanmacOrgConfig, shouldAllowHanmacOrgConfig,
TENANT_VISIBILITY_OPTIONS,
type TenantVisibility,
} from "../utils/orgConfig"; } from "../utils/orgConfig";
import { isSeedTenant } from "../utils/protectedTenants"; import { isSeedTenant } from "../utils/protectedTenants";
export function TenantProfilePage() { export function TenantProfilePage() {
const { tenantId } = useParams<{ tenantId: string }>(); const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
const tenantId = tenantIdParam ?? "";
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
if (!tenantId) {
return (
<div>{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}</div>
);
}
const tenantQuery = useQuery({ const tenantQuery = useQuery({
queryKey: ["tenant", tenantId], queryKey: ["tenant", tenantId],
queryFn: () => fetchTenant(tenantId), queryFn: () => fetchTenant(tenantId),
enabled: tenantId.length > 0,
}); });
const parentQuery = useQuery({ const parentQuery = useQuery({
@@ -197,6 +193,12 @@ export function TenantProfilePage() {
? isSeedTenant(tenantQuery.data) ? isSeedTenant(tenantQuery.data)
: false; : false;
if (!tenantId) {
return (
<div>{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}</div>
);
}
const handleDelete = () => { const handleDelete = () => {
if (isProtectedSeedTenant) { if (isProtectedSeedTenant) {
return; return;

View File

@@ -18,10 +18,10 @@ import { fetchMe, fetchTenant, updateTenant } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
import { normalizeAdminRole } from "../../../lib/roles"; import { normalizeAdminRole } from "../../../lib/roles";
import { import {
type SchemaField,
createSchemaField, createSchemaField,
isSchemaFieldType, isSchemaFieldType,
normalizeSchemaField, normalizeSchemaField,
type SchemaField,
} from "./tenantSchemaFields"; } from "./tenantSchemaFields";
export function TenantSchemaPage() { export function TenantSchemaPage() {

View File

@@ -38,7 +38,6 @@ import {
} from "../../../components/ui/table"; } from "../../../components/ui/table";
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import { import {
type WorksmobileComparisonItem,
downloadWorksmobileInitialPasswordsCSV, downloadWorksmobileInitialPasswordsCSV,
enqueueWorksmobileBackfillDryRun, enqueueWorksmobileBackfillDryRun,
enqueueWorksmobileOrgUnitDelete, enqueueWorksmobileOrgUnitDelete,
@@ -47,13 +46,10 @@ import {
fetchWorksmobileComparison, fetchWorksmobileComparison,
fetchWorksmobileOverview, fetchWorksmobileOverview,
retryWorksmobileJob, retryWorksmobileJob,
type WorksmobileComparisonItem,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
import { import {
type WorksmobileComparisonColumnKey,
type WorksmobileComparisonColumnVisibility,
type WorksmobileComparisonFilter,
type WorksmobileComparisonSummary,
buildWorksmobilePasswordManageUrl, buildWorksmobilePasswordManageUrl,
canOpenWorksmobilePasswordManage, canOpenWorksmobilePasswordManage,
canSelectWorksmobileRow, canSelectWorksmobileRow,
@@ -71,6 +67,10 @@ import {
getWorksmobileSelectedActionIds, getWorksmobileSelectedActionIds,
getWorksmobileSelectedWorksOnlyOrgUnitIds, getWorksmobileSelectedWorksOnlyOrgUnitIds,
summarizeWorksmobileComparison, summarizeWorksmobileComparison,
type WorksmobileComparisonColumnKey,
type WorksmobileComparisonColumnVisibility,
type WorksmobileComparisonFilter,
type WorksmobileComparisonSummary,
} from "./worksmobileComparison"; } from "./worksmobileComparison";
export function TenantWorksmobilePage() { export function TenantWorksmobilePage() {
@@ -1196,13 +1196,7 @@ function ComparisonTable({
); );
} }
function ComparisonDomainCell({ function ComparisonDomainCell({ name, id }: { name?: string; id?: number }) {
name,
id,
}: {
name?: string;
id?: number;
}) {
if (!name && !id) { if (!name && !id) {
return <span className="text-muted-foreground">-</span>; return <span className="text-muted-foreground">-</span>;
} }

View File

@@ -1,5 +1,5 @@
import type { TenantSummary } from "../../../lib/adminApi"; import type { TenantSummary } from "../../../lib/adminApi";
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
export type TenantViewMode = "tree" | "table"; export type TenantViewMode = "tree" | "table";
export type TenantViewRow = TenantNode & { depth: number }; export type TenantViewRow = TenantNode & { depth: number };

View File

@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { TenantSummary } from "../../../lib/adminApi"; import type { TenantSummary } from "../../../lib/adminApi";
import { import {
ORG_UNIT_TYPE_OPTIONS,
mergeTenantOrgConfig, mergeTenantOrgConfig,
ORG_UNIT_TYPE_OPTIONS,
readTenantOrgConfig, readTenantOrgConfig,
shouldAllowHanmacOrgConfig, shouldAllowHanmacOrgConfig,
} from "./orgConfig"; } from "./orgConfig";

View File

@@ -150,7 +150,6 @@ describe("tenantCsvImport", () => {
expect(csv).not.toContain("local-tenant-id"); expect(csv).not.toContain("local-tenant-id");
}); });
it("preserves source tenant_id when a create resolution does not override it", () => { it("preserves source tenant_id when a create resolution does not override it", () => {
const exportedTenantId = "11111111-2222-4333-8444-555555555555"; const exportedTenantId = "11111111-2222-4333-8444-555555555555";
const rows = parseTenantCSV( const rows = parseTenantCSV(

View File

@@ -403,7 +403,6 @@ function createTenantImportId() {
.padEnd(12, "0")}`; .padEnd(12, "0")}`;
} }
function isUUIDLikeTenantId(value: string) { function isUUIDLikeTenantId(value: string) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
value, value,
@@ -596,7 +595,7 @@ function slugify(value: string) {
: "support", : "support",
}; };
let result = value.trim(); const result = value.trim();
// 1. 전체 매칭 확인 // 1. 전체 매칭 확인
if (commonMappings[result]) { if (commonMappings[result]) {

View File

@@ -21,9 +21,9 @@ import {
TableRow, TableRow,
} from "../../../components/ui/table"; } from "../../../components/ui/table";
import { import {
type TenantSummary,
fetchAllTenants, fetchAllTenants,
fetchGroups, fetchGroups,
type TenantSummary,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
export default function GlobalUserGroupListPage() { export default function GlobalUserGroupListPage() {

View File

@@ -70,17 +70,17 @@ import {
} from "../../../components/ui/tabs"; } from "../../../components/ui/tabs";
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import { import {
type TenantSummary,
type UserSummary,
createUser, createUser,
exportTenantsCSV, exportTenantsCSV,
fetchAllTenants, fetchAllTenants,
fetchUsers, fetchUsers,
type TenantSummary,
type UserSummary,
updateTenant, updateTenant,
updateUser, updateUser,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
// --- Icons & Helpers --- // --- Icons & Helpers ---
const getTenantIcon = (type?: string) => { const getTenantIcon = (type?: string) => {
@@ -482,8 +482,10 @@ function TenantUserGroupsTab() {
mutationFn: ({ mutationFn: ({
id, id,
parentId, parentId,
}: { id: string; parentId: string | undefined }) => }: {
updateTenant(id, { parentId: parentId || "" }), id: string;
parentId: string | undefined;
}) => updateTenant(id, { parentId: parentId || "" }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] }); queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] });
toast.success( toast.success(

View File

@@ -574,9 +574,9 @@ export function UserGroupDetailPage() {
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
groupRoles.map((role, idx) => ( groupRoles.map((role) => (
<TableRow <TableRow
key={`${role.tenantId}-${role.relation}-${idx}`} key={`${role.tenantId}-${role.relation}`}
className="hover:bg-muted/30 transition-colors" className="hover:bg-muted/30 transition-colors"
> >
<TableCell> <TableCell>

View File

@@ -40,21 +40,21 @@ import {
TabsTrigger, TabsTrigger,
} from "../../components/ui/tabs"; } from "../../components/ui/tabs";
import { import {
type TenantSummary,
type UserAppointment,
type UserCreateRequest,
type UserCreateResponse,
createUser, createUser,
fetchAllTenants, fetchAllTenants,
fetchMe, fetchMe,
fetchTenant, fetchTenant,
type TenantSummary,
type UserAppointment,
type UserCreateRequest,
type UserCreateResponse,
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { isSuperAdminRole } from "../../lib/roles"; import { isSuperAdminRole } from "../../lib/roles";
import { import {
type OrgChartTenantSelection,
buildAuthenticatedOrgChartTenantPickerUrl, buildAuthenticatedOrgChartTenantPickerUrl,
filterNonHanmacFamilyTenants, filterNonHanmacFamilyTenants,
type OrgChartTenantSelection,
parseOrgChartTenantSelection, parseOrgChartTenantSelection,
} from "./orgChartPicker"; } from "./orgChartPicker";
import type { UserSchemaField } from "./userSchemaFields"; import type { UserSchemaField } from "./userSchemaFields";
@@ -111,7 +111,10 @@ function createEmptyAppointment(): AppointmentDraft {
tenantId: "", tenantId: "",
tenantName: "", tenantName: "",
tenantSlug: "", tenantSlug: "",
isPrimary: false,
isOwner: false, isOwner: false,
isAdmin: false,
isManager: false,
grade: "", grade: "",
jobTitle: "", jobTitle: "",
position: "", position: "",
@@ -347,8 +350,8 @@ function UserCreatePage() {
if (currentIndex === index) { if (currentIndex === index) {
return { ...appointment, ...patch }; return { ...appointment, ...patch };
} }
if (patch.isOwner === true) { if (patch.isPrimary === true) {
return { ...appointment, isOwner: false }; return { ...appointment, isPrimary: false };
} }
return appointment; return appointment;
}), }),
@@ -463,8 +466,10 @@ function UserCreatePage() {
tenantId: appointment.tenantId, tenantId: appointment.tenantId,
tenantSlug: appointment.tenantSlug, tenantSlug: appointment.tenantSlug,
tenantName: appointment.tenantName, tenantName: appointment.tenantName,
isPrimary: appointment.isOwner, isPrimary: appointment.isPrimary === true,
isOwner: appointment.isOwner, ...(appointment.isOwner === true ? { isOwner: true } : {}),
...(appointment.isAdmin === true ? { isAdmin: true } : {}),
...(appointment.isManager === true ? { isManager: true } : {}),
grade: appointment.grade, grade: appointment.grade,
jobTitle: appointment.jobTitle, jobTitle: appointment.jobTitle,
position: appointment.position, position: appointment.position,
@@ -480,12 +485,11 @@ function UserCreatePage() {
return; return;
} }
const primary = appointments.find((a) => a.isOwner); const primary = appointments.find((a) => a.isPrimary);
if (primary) { if (primary) {
metadata.primaryTenantId = primary.tenantId; metadata.primaryTenantId = primary.tenantId;
metadata.primaryTenantSlug = primary.tenantSlug; metadata.primaryTenantSlug = primary.tenantSlug;
metadata.primaryTenantName = primary.tenantName; metadata.primaryTenantName = primary.tenantName;
metadata.primaryTenantIsOwner = true;
} }
payload.additionalAppointments = appointments; payload.additionalAppointments = appointments;
@@ -916,10 +920,10 @@ function UserCreatePage() {
)} )}
<label className="flex items-center gap-3 text-sm"> <label className="flex items-center gap-3 text-sm">
<Switch <Switch
checked={appointment.isOwner} checked={appointment.isPrimary === true}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
updateAppointment(index, { updateAppointment(index, {
isOwner: checked === true, isPrimary: checked === true,
}) })
} }
aria-label={t( aria-label={t(
@@ -932,6 +936,24 @@ function UserCreatePage() {
"대표 조직", "대표 조직",
)} )}
</label> </label>
<label className="flex items-center gap-3 text-sm">
<Switch
checked={appointment.isManager === true}
onCheckedChange={(checked) =>
updateAppointment(index, {
isManager: checked === true,
})
}
aria-label={t(
"ui.admin.users.detail.form.appointment_manager",
"조직장",
)}
/>
{t(
"ui.admin.users.detail.form.appointment_manager",
"조직장",
)}
</label>
</div> </div>
</div> </div>

View File

@@ -60,10 +60,8 @@ import {
TabsTrigger, TabsTrigger,
} from "../../components/ui/tabs"; } from "../../components/ui/tabs";
import { toast } from "../../components/ui/use-toast"; import { toast } from "../../components/ui/use-toast";
import type { PasswordPolicyResponse } from "../../lib/adminApi";
import { import {
type TenantSummary,
type UserAppointment,
type UserUpdateRequest,
deleteUser, deleteUser,
fetchAllTenants, fetchAllTenants,
fetchMe, fetchMe,
@@ -71,18 +69,20 @@ import {
fetchTenant, fetchTenant,
fetchUser, fetchUser,
fetchUserRpHistory, fetchUserRpHistory,
type TenantSummary,
type UserAppointment,
type UserUpdateRequest,
updateUser, updateUser,
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import type { PasswordPolicyResponse } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { normalizeAdminRole } from "../../lib/roles"; import { normalizeAdminRole } from "../../lib/roles";
import { generateSecurePassword } from "../../lib/utils"; import { generateSecurePassword } from "../../lib/utils";
import { import {
type OrgChartTenantSelection,
buildAuthenticatedOrgChartTenantPickerUrl, buildAuthenticatedOrgChartTenantPickerUrl,
filterNonHanmacFamilyTenants, filterNonHanmacFamilyTenants,
isHanmacFamilyTenant, isHanmacFamilyTenant,
isHanmacFamilyUser, isHanmacFamilyUser,
type OrgChartTenantSelection,
parseOrgChartTenantSelection, parseOrgChartTenantSelection,
} from "./orgChartPicker"; } from "./orgChartPicker";
import type { UserSchemaField } from "./userSchemaFields"; import type { UserSchemaField } from "./userSchemaFields";
@@ -142,6 +142,8 @@ function createEmptyAppointment(): AppointmentDraft {
tenantSlug: "", tenantSlug: "",
isPrimary: false, isPrimary: false,
isOwner: false, isOwner: false,
isAdmin: false,
isManager: false,
grade: "", grade: "",
jobTitle: "", jobTitle: "",
position: "", position: "",
@@ -579,8 +581,8 @@ function UserDetailPage() {
if (currentIndex === index) { if (currentIndex === index) {
return { ...appointment, ...patch }; return { ...appointment, ...patch };
} }
if (patch.isOwner === true) { if (patch.isPrimary === true) {
return { ...appointment, isOwner: false }; return { ...appointment, isPrimary: false };
} }
return appointment; return appointment;
}), }),
@@ -701,6 +703,9 @@ function UserDetailPage() {
isPrimary: isPrimary:
appointment.isPrimary === true || appointment.isPrimary === true ||
appointment.tenantId === primaryFromMetadata?.id, appointment.tenantId === primaryFromMetadata?.id,
isOwner: appointment.isOwner === true,
isAdmin: appointment.isAdmin === true,
isManager: appointment.isManager === true,
draftId: createDraftId(), draftId: createDraftId(),
})) }))
: isUserHanmacFamily : isUserHanmacFamily
@@ -714,6 +719,8 @@ function UserDetailPage() {
isOwner: isOwner:
metadata.primaryTenantIsOwner === true && metadata.primaryTenantIsOwner === true &&
tenant.id === fallbackAppointment?.id, tenant.id === fallbackAppointment?.id,
isAdmin: false,
isManager: false,
grade: user.grade, grade: user.grade,
jobTitle: user.jobTitle, jobTitle: user.jobTitle,
position: user.position, position: user.position,
@@ -727,6 +734,8 @@ function UserDetailPage() {
tenantSlug: fallbackAppointment.slug, tenantSlug: fallbackAppointment.slug,
isPrimary: true, isPrimary: true,
isOwner: metadata.primaryTenantIsOwner === true, isOwner: metadata.primaryTenantIsOwner === true,
isAdmin: false,
isManager: false,
grade: user.grade, grade: user.grade,
jobTitle: user.jobTitle, jobTitle: user.jobTitle,
position: user.position, position: user.position,
@@ -836,23 +845,23 @@ function UserDetailPage() {
tenantId: appointment.tenantId, tenantId: appointment.tenantId,
tenantSlug: appointment.tenantSlug, tenantSlug: appointment.tenantSlug,
tenantName: appointment.tenantName, tenantName: appointment.tenantName,
isPrimary: appointment.isOwner, isPrimary: appointment.isPrimary === true,
isOwner: appointment.isOwner, ...(appointment.isOwner === true ? { isOwner: true } : {}),
...(appointment.isAdmin === true ? { isAdmin: true } : {}),
...(appointment.isManager === true ? { isManager: true } : {}),
grade: appointment.grade, grade: appointment.grade,
jobTitle: appointment.jobTitle, jobTitle: appointment.jobTitle,
position: appointment.position, position: appointment.position,
})); }));
const primary = appointments.find((a) => a.isOwner); const primary = appointments.find((a) => a.isPrimary);
if (primary) { if (primary) {
payload.tenantSlug = primary.tenantSlug; payload.tenantSlug = primary.tenantSlug;
payload.primaryTenantId = primary.tenantId; payload.primaryTenantId = primary.tenantId;
payload.primaryTenantName = primary.tenantName; payload.primaryTenantName = primary.tenantName;
payload.primaryTenantIsOwner = true;
metadata.primaryTenantId = primary.tenantId; metadata.primaryTenantId = primary.tenantId;
metadata.primaryTenantSlug = primary.tenantSlug; metadata.primaryTenantSlug = primary.tenantSlug;
metadata.primaryTenantName = primary.tenantName; metadata.primaryTenantName = primary.tenantName;
metadata.primaryTenantIsOwner = true;
} else { } else {
payload.tenantSlug = undefined; payload.tenantSlug = undefined;
} }
@@ -868,12 +877,10 @@ function UserDetailPage() {
primaryTenantId: primary?.tenantId, primaryTenantId: primary?.tenantId,
primaryTenantName: primary?.tenantName, primaryTenantName: primary?.tenantName,
primaryTenantSlug: primary?.tenantSlug, primaryTenantSlug: primary?.tenantSlug,
primaryTenantIsOwner: primary?.isOwner ?? false,
}; };
payload.tenantSlug = primary?.tenantSlug; payload.tenantSlug = primary?.tenantSlug;
payload.primaryTenantId = primary?.tenantId; payload.primaryTenantId = primary?.tenantId;
payload.primaryTenantName = primary?.tenantName; payload.primaryTenantName = primary?.tenantName;
payload.primaryTenantIsOwner = primary?.isOwner ?? false;
} }
mutation.mutate(payload); mutation.mutate(payload);
@@ -1362,13 +1369,13 @@ function UserDetailPage() {
)} )}
<label className="flex items-center gap-3 text-sm"> <label className="flex items-center gap-3 text-sm">
<Switch <Switch
checked={appointment.isOwner} checked={appointment.isPrimary === true}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
updateAppointment(index, { updateAppointment(index, {
isOwner: checked === true, isPrimary: checked === true,
}) })
} }
disabled={appointment.isPrimary} disabled={appointment.isPrimary === true}
aria-label={t( aria-label={t(
"ui.admin.users.detail.form.appointment_owner", "ui.admin.users.detail.form.appointment_owner",
"대표 조직", "대표 조직",
@@ -1379,6 +1386,24 @@ function UserDetailPage() {
"대표 조직", "대표 조직",
)} )}
</label> </label>
<label className="flex items-center gap-3 text-sm">
<Switch
checked={appointment.isManager === true}
onCheckedChange={(checked) =>
updateAppointment(index, {
isManager: checked === true,
})
}
aria-label={t(
"ui.admin.users.detail.form.appointment_manager",
"조직장",
)}
/>
{t(
"ui.admin.users.detail.form.appointment_manager",
"조직장",
)}
</label>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,192 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createI18nMock } from "../../test/i18nMock";
import UserListPage from "./UserListPage";
const selectRenderCounter = vi.hoisted(() => ({ count: 0 }));
const users = Array.from({ length: 200 }, (_, index) => ({
id: `user-${index}`,
name: `User ${index}`,
email: `user${index}@example.com`,
phone: `010-${String(index).padStart(4, "0")}-0000`,
role: "user",
status: "active",
tenantSlug: "hanmac",
tenant: { id: "tenant-1", name: "한맥", slug: "hanmac" },
metadata: {},
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
}));
const fetchUsersMock = vi.hoisted(() => vi.fn());
const searchRenderBudgetMs =
process.env.npm_lifecycle_event === "test:coverage" ? 500 : 200;
vi.mock("../../lib/i18n", () => createI18nMock());
vi.mock("../../lib/adminApi", () => ({
fetchMe: vi.fn(async () => ({
id: "admin-user",
role: "super_admin",
name: "Admin",
email: "admin@example.com",
})),
fetchAllTenants: vi.fn(async () => ({
items: [{ id: "tenant-1", name: "한맥", slug: "hanmac" }],
total: 1,
})),
fetchTenant: vi.fn(async () => ({
id: "tenant-1",
name: "한맥",
slug: "hanmac",
config: { userSchema: [] },
})),
fetchUsers: fetchUsersMock,
bulkCreateUsers: vi.fn(),
bulkDeleteUsers: vi.fn(),
bulkUpdateUsers: vi.fn(),
deleteUser: vi.fn(),
exportUsersCSV: vi.fn(),
updateUser: vi.fn(),
}));
vi.mock("../../components/ui/select", () => ({
Select: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectTrigger: ({
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
selectRenderCounter.count += 1;
return (
<button type="button" {...props}>
{children}
</button>
);
},
SelectValue: () => <span />,
SelectContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectItem: ({
children,
value: _value,
}: {
children: React.ReactNode;
value: string;
}) => <div>{children}</div>,
}));
function renderUserListPage() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<UserListPage />
</MemoryRouter>
</QueryClientProvider>,
);
}
function createDeferred<T>() {
let resolve: (value: T) => void = () => {};
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve;
});
return { promise, resolve };
}
describe("UserListPage search rendering", () => {
beforeEach(() => {
selectRenderCounter.count = 0;
fetchUsersMock.mockReset();
fetchUsersMock.mockImplementation(
async (_limit: number, _offset: number, search?: string) => {
const normalizedSearch = search?.trim().toLowerCase();
const items = normalizedSearch
? users.filter((user) =>
`${user.name} ${user.email}`
.toLowerCase()
.includes(normalizedSearch),
)
: users;
return { items, total: items.length };
},
);
});
it("does not rerender user table controls while typing a draft search", async () => {
renderUserListPage();
await screen.findByText("User 0");
const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색...");
const renderCountBeforeTyping = selectRenderCounter.count;
fireEvent.change(searchInput, { target: { value: "u" } });
expect(searchInput).toHaveValue("u");
expect(selectRenderCounter.count).toBe(renderCountBeforeTyping);
});
it("keeps rendered row controls below the full 200-user result set", async () => {
renderUserListPage();
await screen.findByText("User 0");
expect(screen.getAllByTestId(/^user-status-select-/).length).toBeLessThan(
200,
);
});
it("renders compact vertically centered user table headers", async () => {
renderUserListPage();
await screen.findByText("User 0");
const nameHeader = screen.getByRole("columnheader", { name: /이름/ });
const content = nameHeader.firstElementChild;
expect(nameHeader).toHaveClass("h-9", "py-1", "align-middle", "text-xs");
expect(content).toHaveClass("flex", "h-full", "items-center");
});
it("centers the initial loading message across the user table", async () => {
const deferred = createDeferred<{ items: typeof users; total: number }>();
fetchUsersMock.mockReturnValueOnce(deferred.promise);
renderUserListPage();
const loadingCell = await screen.findByTestId("user-table-loading-cell");
expect(loadingCell).toHaveClass(
"flex",
"items-center",
"justify-center",
"text-center",
);
expect(loadingCell).toHaveStyle({ gridColumn: "1 / -1" });
deferred.resolve({ items: users, total: users.length });
});
it("renders a 200-user search result update within 200ms after search submit", async () => {
renderUserListPage();
await screen.findByText("User 0");
const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색...");
const startedAt = performance.now();
fireEvent.change(searchInput, { target: { value: "user 19" } });
fireEvent.keyDown(searchInput, { key: "Enter" });
expect(screen.getByText("User 19")).toBeInTheDocument();
expect(screen.queryByText("User 0")).not.toBeInTheDocument();
expect(performance.now() - startedAt).toBeLessThan(searchRenderBudgetMs);
});
});

View File

@@ -1,4 +1,10 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import {
observeElementRect,
type Rect,
useVirtualizer,
type Virtualizer,
} from "@tanstack/react-virtual";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { import {
ArrowDown, ArrowDown,
@@ -7,7 +13,6 @@ import {
ChevronDown, ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Users,
Download, Download,
FileDown, FileDown,
FileSpreadsheet, FileSpreadsheet,
@@ -19,13 +24,13 @@ import {
ShieldCheck, ShieldCheck,
Trash2, Trash2,
Upload, Upload,
Users,
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page"; import { PageHeader } from "../../../../common/core/components/page";
import { import {
SortableTableHead, SortableTableHead,
sortableTableHeadBaseClassName,
sortableTableHeaderClassName, sortableTableHeaderClassName,
} from "../../../../common/core/components/sort"; } from "../../../../common/core/components/sort";
import { import {
@@ -81,7 +86,6 @@ import {
} from "../../components/ui/table"; } from "../../components/ui/table";
import { toast } from "../../components/ui/use-toast"; import { toast } from "../../components/ui/use-toast";
import { import {
type UserSummary,
bulkDeleteUsers, bulkDeleteUsers,
bulkUpdateUsers, bulkUpdateUsers,
deleteUser, deleteUser,
@@ -90,13 +94,15 @@ import {
fetchMe, fetchMe,
fetchTenant, fetchTenant,
fetchUsers, fetchUsers,
type TenantSummary,
type UserSummary,
updateUser, updateUser,
} from "../../lib/adminApi"; } from "../../lib/adminApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { isSuperAdminRole } from "../../lib/roles"; import { isSuperAdminRole } from "../../lib/roles";
import { import {
UserBulkUploadModal,
downloadUserTemplate, downloadUserTemplate,
UserBulkUploadModal,
} from "./components/UserBulkUploadModal"; } from "./components/UserBulkUploadModal";
import { import {
normalizeUserStatusValue, normalizeUserStatusValue,
@@ -113,6 +119,23 @@ type UserSchemaField = {
type UserSortKey = string; type UserSortKey = string;
const USER_ROW_ESTIMATED_HEIGHT = 64;
const USER_ROW_OVERSCAN = 8;
const USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT = 640;
const userFixedColumnWidths = [48, 160, 220, 160, 260, 170, 160, 220] as const;
const userMetadataColumnWidth = 160;
const userCreatedColumnWidth = 150;
type UserRowVirtualizer = Virtualizer<HTMLDivElement, HTMLTableRowElement>;
const userTableHeadClassName =
"h-9 px-3 py-1 text-xs leading-tight align-middle whitespace-nowrap";
const userTableHeadInteractiveClassName = `${userTableHeadClassName} cursor-pointer transition-colors hover:bg-muted/50`;
const userTableHeadContentClassName = "flex h-full items-center gap-1";
const userSortableTableHeadClassName =
"!h-9 !px-3 !py-1 leading-tight whitespace-nowrap";
const userSortableTableHeadContentClassName = "h-full items-center";
const userTableStateCellClassName =
"flex h-24 items-center justify-center p-0 text-center text-sm text-muted-foreground";
const bulkPermissionOptions = [ const bulkPermissionOptions = [
{ {
value: "super_admin", value: "super_admin",
@@ -130,11 +153,124 @@ function assignableSystemRoleValue(role?: string | null) {
return isSuperAdminRole(role) ? "super_admin" : "user"; return isSuperAdminRole(role) ? "super_admin" : "user";
} }
function userMatchesSearch(user: UserSummary, search: string) {
const normalizedSearch = search.trim().toLowerCase();
if (!normalizedSearch) {
return true;
}
return (
user.name?.toLowerCase().includes(normalizedSearch) ||
user.email?.toLowerCase().includes(normalizedSearch) ||
user.phone?.toLowerCase().includes(normalizedSearch) ||
user.id?.toLowerCase().includes(normalizedSearch) ||
user.tenantSlug?.toLowerCase().includes(normalizedSearch) ||
user.tenant?.name?.toLowerCase().includes(normalizedSearch) ||
user.department?.toLowerCase().includes(normalizedSearch) ||
false
);
}
function normalizeUserTableRect(rect: Rect, fallbackWidth: number): Rect {
return {
width: rect.width > 0 ? rect.width : fallbackWidth,
height:
rect.height > 0 ? rect.height : USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT,
};
}
type UserListSearchControlsProps = {
search: string;
selectedCompany: string;
tenants: TenantSummary[];
profileRole?: string | null;
onSearch: (value: string) => void;
onCompanyChange: (value: string) => void;
};
const UserListSearchControls = React.memo(function UserListSearchControls({
search,
selectedCompany,
tenants,
profileRole,
onSearch,
onCompanyChange,
}: UserListSearchControlsProps) {
const [searchDraft, setSearchDraft] = React.useState(search);
React.useEffect(() => {
setSearchDraft(search);
}, [search]);
const handleSearch = React.useCallback(() => {
onSearch(searchDraft);
}, [onSearch, searchDraft]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
handleSearch();
}
},
[handleSearch],
);
const tenantOptions = React.useMemo(
() =>
tenants.map((tenant) => (
<option key={tenant.id} value={tenant.slug}>
{tenant.name}
</option>
)),
[tenants],
);
return (
<SearchFilterBar
primary={
<>
<div className="relative w-48">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t(
"ui.admin.users.list.search_placeholder",
"이름 또는 이메일 검색...",
)}
className="h-9 pl-9"
value={searchDraft}
onChange={(event) => setSearchDraft(event.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<select
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
value={selectedCompany}
onChange={(event) => onCompanyChange(event.target.value)}
disabled={profileRole === "tenant_admin"}
>
<option value="">{t("ui.common.all", "전체 테넌트")}</option>
{tenantOptions}
</select>
<Button
variant="secondary"
size="sm"
onClick={handleSearch}
className="h-9"
>
{t("ui.common.search", "검색")}
</Button>
</>
}
/>
);
});
function UserListPage() { function UserListPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState("");
const [searchDraft, setSearchDraft] = React.useState("");
const [selectedCompany, setSelectedCompany] = React.useState<string>(""); const [selectedCompany, setSelectedCompany] = React.useState<string>("");
const [visibleColumns, setVisibleColumns] = React.useState< const [visibleColumns, setVisibleColumns] = React.useState<
Record<string, boolean> Record<string, boolean>
@@ -148,6 +284,7 @@ function UserListPage() {
const [sortConfig, setSortConfig] = const [sortConfig, setSortConfig] =
React.useState<SortConfig<UserSortKey> | null>(null); React.useState<SortConfig<UserSortKey> | null>(null);
const [bulkUploadOpen, setBulkUploadOpen] = React.useState(false); const [bulkUploadOpen, setBulkUploadOpen] = React.useState(false);
const userTableViewportRef = React.useRef<HTMLDivElement | null>(null);
const limit = 1000; const limit = 1000;
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
@@ -254,16 +391,15 @@ function UserListPage() {
}, },
}); });
const handleSearch = () => { const handleSearch = React.useCallback((nextSearch: string) => {
setSearch(searchDraft); setSearch(nextSearch);
setPage(1); setPage(1);
}; }, []);
const handleKeyDown = (e: React.KeyboardEvent) => { const handleCompanyChange = React.useCallback((nextCompany: string) => {
if (e.key === "Enter") { setSelectedCompany(nextCompany);
handleSearch(); setPage(1);
} }, []);
};
const handleExport = (includeIds = false) => { const handleExport = (includeIds = false) => {
exportMutation.mutate(includeIds); exportMutation.mutate(includeIds);
@@ -279,7 +415,14 @@ function UserListPage() {
) )
: null; : null;
const rawItems = query.data?.items ?? []; const serverItems = query.data?.items ?? [];
const rawItems = React.useMemo(() => {
if (!query.isFetching || search.trim() === "") {
return serverItems;
}
return serverItems.filter((user) => userMatchesSearch(user, search));
}, [query.isFetching, search, serverItems]);
const userSortResolvers = React.useMemo< const userSortResolvers = React.useMemo<
SortResolverMap<UserSummary, UserSortKey> SortResolverMap<UserSummary, UserSortKey>
>( >(
@@ -306,8 +449,55 @@ function UserListPage() {
[userSchema], [userSchema],
); );
const items = React.useMemo(() => { const items = React.useMemo(() => {
if (!sortConfig) {
return rawItems;
}
return sortItems(rawItems, sortConfig, userSortResolvers); return sortItems(rawItems, sortConfig, userSortResolvers);
}, [rawItems, sortConfig, userSortResolvers]); }, [rawItems, sortConfig, userSortResolvers]);
const visibleUserSchemaFields = React.useMemo(
() => userSchema.filter((field) => visibleColumns[field.key] !== false),
[userSchema, visibleColumns],
);
const userTableColumnWidths = React.useMemo(
() => [
...userFixedColumnWidths,
...visibleUserSchemaFields.map(() => userMetadataColumnWidth),
userCreatedColumnWidth,
],
[visibleUserSchemaFields],
);
const userTableGridTemplateColumns = React.useMemo(
() => userTableColumnWidths.map((width) => `${width}px`).join(" "),
[userTableColumnWidths],
);
const userTableMinWidth = React.useMemo(
() => userTableColumnWidths.reduce((sum, width) => sum + width, 0),
[userTableColumnWidths],
);
const observeUserTableElementRect = React.useCallback(
(instance: UserRowVirtualizer, callback: (rect: Rect) => void) =>
observeElementRect(instance, (rect) => {
callback(normalizeUserTableRect(rect, userTableMinWidth));
}),
[userTableMinWidth],
);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => userTableViewportRef.current,
estimateSize: () => USER_ROW_ESTIMATED_HEIGHT,
measureElement: (element) =>
element.getBoundingClientRect().height || USER_ROW_ESTIMATED_HEIGHT,
observeElementRect: observeUserTableElementRect,
overscan: USER_ROW_OVERSCAN,
initialRect: {
width: userTableMinWidth,
height: USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT,
},
});
const virtualRows = rowVirtualizer.getVirtualItems();
const shouldVirtualizeRows = !query.isLoading && items.length > 0;
const tableColumnCount = 9 + visibleUserSchemaFields.length;
const requestSort = (key: UserSortKey) => { const requestSort = (key: UserSortKey) => {
setSortConfig((current) => toggleSort(current, key)); setSortConfig((current) => toggleSort(current, key));
@@ -436,52 +626,13 @@ function UserListPage() {
)} )}
actions={ actions={
<> <>
<SearchFilterBar <UserListSearchControls
primary={ search={search}
<> selectedCompany={selectedCompany}
<div className="relative w-48"> tenants={tenants}
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> profileRole={profile?.role}
<Input onSearch={handleSearch}
placeholder={t( onCompanyChange={handleCompanyChange}
"ui.admin.users.list.search_placeholder",
"이름 또는 이메일 검색...",
)}
className="h-9 pl-9"
value={searchDraft}
onChange={(e) => setSearchDraft(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<select
className="flex h-9 w-[160px] rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
value={selectedCompany}
onChange={(e) => {
setSelectedCompany(e.target.value);
setPage(1);
}}
disabled={profile?.role === "tenant_admin"}
>
<option value="">
{t("ui.common.all", "전체 테넌트")}
</option>
{tenants.map((t) => (
<option key={t.id} value={t.slug}>
{t.name}
</option>
))}
</select>
<Button
variant="secondary"
size="sm"
onClick={handleSearch}
className="h-9"
>
{t("ui.common.search", "검색")}
</Button>
</>
}
/> />
<Button <Button
@@ -643,13 +794,22 @@ function UserListPage() {
)} )}
<div className={commonTableShellClass}> <div className={commonTableShellClass}>
<div className={commonTableViewportClass}> <div
<Table> ref={userTableViewportRef}
<TableHeader className={sortableTableHeaderClassName}> data-testid="user-table-viewport"
<TableRow> className={commonTableViewportClass}
<TableHead
className={`${sortableTableHeadBaseClassName} w-12`}
> >
<Table style={{ display: "grid", minWidth: userTableMinWidth }}>
<TableHeader className={sortableTableHeaderClassName}>
<TableRow
style={{
display: "grid",
gridTemplateColumns: userTableGridTemplateColumns,
minWidth: userTableMinWidth,
}}
>
<TableHead className={`${userTableHeadClassName} w-12`}>
<div className="flex h-full items-center justify-center">
<input <input
type="checkbox" type="checkbox"
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
@@ -659,66 +819,67 @@ function UserListPage() {
} }
onChange={toggleSelectAll} onChange={toggleSelectAll}
/> />
</div>
</TableHead> </TableHead>
<TableHead <TableHead
className="min-w-[120px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={`${userTableHeadInteractiveClassName} min-w-[120px]`}
onClick={() => requestSort("name")} onClick={() => requestSort("name")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t("ui.admin.users.list.table.name", "이름")} {t("ui.admin.users.list.table.name", "이름")}
{getSortIcon("name")} {getSortIcon("name")}
</div> </div>
</TableHead> </TableHead>
<TableHead <TableHead
className="min-w-[180px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={`${userTableHeadInteractiveClassName} min-w-[180px]`}
onClick={() => requestSort("email")} onClick={() => requestSort("email")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t("ui.admin.users.list.table.email", "이메일")} {t("ui.admin.users.list.table.email", "이메일")}
{getSortIcon("email")} {getSortIcon("email")}
</div> </div>
</TableHead> </TableHead>
<TableHead <TableHead
className="min-w-[140px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={`${userTableHeadInteractiveClassName} min-w-[140px]`}
onClick={() => requestSort("phone")} onClick={() => requestSort("phone")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t("ui.admin.users.list.table.phone", "전화번호")} {t("ui.admin.users.list.table.phone", "전화번호")}
{getSortIcon("phone")} {getSortIcon("phone")}
</div> </div>
</TableHead> </TableHead>
<TableHead <TableHead
className="min-w-[220px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={`${userTableHeadInteractiveClassName} min-w-[220px]`}
onClick={() => requestSort("id")} onClick={() => requestSort("id")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t("ui.admin.users.list.table.id", "ID")} {t("ui.admin.users.list.table.id", "ID")}
{getSortIcon("id")} {getSortIcon("id")}
</div> </div>
</TableHead> </TableHead>
<TableHead <TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={userTableHeadInteractiveClassName}
onClick={() => requestSort("status")} onClick={() => requestSort("status")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t("ui.admin.users.list.table.status", "STATUS")} {t("ui.admin.users.list.table.status", "STATUS")}
{getSortIcon("status")} {getSortIcon("status")}
</div> </div>
</TableHead> </TableHead>
<TableHead <TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={userTableHeadInteractiveClassName}
onClick={() => requestSort("role")} onClick={() => requestSort("role")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t("ui.admin.users.list.table.role", "ROLE")} {t("ui.admin.users.list.table.role", "ROLE")}
{getSortIcon("role")} {getSortIcon("role")}
</div> </div>
</TableHead> </TableHead>
<TableHead <TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors" className={userTableHeadInteractiveClassName}
onClick={() => requestSort("tenant_dept")} onClick={() => requestSort("tenant_dept")}
> >
<div className="flex items-center"> <div className={userTableHeadContentClassName}>
{t( {t(
"ui.admin.users.list.table.tenant_dept", "ui.admin.users.list.table.tenant_dept",
"TENANT / DEPT", "TENANT / DEPT",
@@ -727,21 +888,20 @@ function UserListPage() {
</div> </div>
</TableHead> </TableHead>
{/* Dynamic Columns from Schema */} {/* Dynamic Columns from Schema */}
{userSchema.map( {visibleUserSchemaFields.map((field) => (
(field) =>
visibleColumns[field.key] !== false && (
<SortableTableHead <SortableTableHead
key={field.key} key={field.key}
className="whitespace-nowrap" className={userSortableTableHeadClassName}
contentClassName={userSortableTableHeadContentClassName}
label={field.label} label={field.label}
onSort={requestSort} onSort={requestSort}
sortConfig={sortConfig} sortConfig={sortConfig}
sortKey={field.key} sortKey={field.key}
/> />
), ))}
)}
<SortableTableHead <SortableTableHead
className="whitespace-nowrap" className={userSortableTableHeadClassName}
contentClassName={userSortableTableHeadContentClassName}
label={t("ui.admin.users.list.table.created", "CREATED")} label={t("ui.admin.users.list.table.created", "CREATED")}
onSort={requestSort} onSort={requestSort}
sortConfig={sortConfig} sortConfig={sortConfig}
@@ -749,22 +909,51 @@ function UserListPage() {
/> />
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody
style={
shouldVirtualizeRows
? {
display: "grid",
height: `${rowVirtualizer.getTotalSize()}px`,
minWidth: userTableMinWidth,
position: "relative",
}
: undefined
}
>
{query.isLoading && ( {query.isLoading && (
<TableRow> <TableRow
data-testid="user-table-loading-row"
style={{
display: "grid",
gridTemplateColumns: userTableGridTemplateColumns,
minWidth: userTableMinWidth,
}}
>
<TableCell <TableCell
colSpan={7 + userSchema.length} colSpan={tableColumnCount}
className="h-24 text-center" data-testid="user-table-loading-cell"
className={userTableStateCellClassName}
style={{ gridColumn: "1 / -1" }}
> >
{t("msg.common.loading", "로딩 중...")} {t("msg.common.loading", "로딩 중...")}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
{!query.isLoading && items.length === 0 && ( {!query.isLoading && items.length === 0 && (
<TableRow> <TableRow
data-testid="user-table-empty-row"
style={{
display: "grid",
gridTemplateColumns: userTableGridTemplateColumns,
minWidth: userTableMinWidth,
}}
>
<TableCell <TableCell
colSpan={7 + userSchema.length} colSpan={tableColumnCount}
className="h-24 text-center" data-testid="user-table-empty-cell"
className={userTableStateCellClassName}
style={{ gridColumn: "1 / -1" }}
> >
{t( {t(
"msg.admin.users.list.empty", "msg.admin.users.list.empty",
@@ -773,12 +962,30 @@ function UserListPage() {
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
{items.map((user) => ( {shouldVirtualizeRows &&
virtualRows.map((virtualRow) => {
const user = items[virtualRow.index];
if (!user) return null;
return (
<TableRow <TableRow
key={user.id} key={user.id}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
className={ className={
selectedUserIds.includes(user.id) ? "bg-primary/5" : "" selectedUserIds.includes(user.id)
? "bg-primary/5"
: ""
} }
style={{
display: "grid",
gridTemplateColumns: userTableGridTemplateColumns,
height: `${virtualRow.size}px`,
minWidth: userTableMinWidth,
position: "absolute",
transform: `translateY(${virtualRow.start}px)`,
width: "100%",
}}
> >
<TableCell> <TableCell>
<input <input
@@ -831,7 +1038,8 @@ function UserListPage() {
}) })
} }
disabled={ disabled={
statusMutation.isPending || user.id === profile?.id statusMutation.isPending ||
user.id === profile?.id
} }
> >
<SelectTrigger <SelectTrigger
@@ -899,19 +1107,17 @@ function UserListPage() {
</div> </div>
</TableCell> </TableCell>
{/* Dynamic Metadata Cells */} {/* Dynamic Metadata Cells */}
{userSchema.map( {visibleUserSchemaFields.map((field) => (
(field) =>
visibleColumns[field.key] !== false && (
<TableCell key={field.key} className="text-sm"> <TableCell key={field.key} className="text-sm">
{String(user.metadata?.[field.key] ?? "-")} {String(user.metadata?.[field.key] ?? "-")}
</TableCell> </TableCell>
), ))}
)}
<TableCell className="text-sm text-muted-foreground"> <TableCell className="text-sm text-muted-foreground">
{new Date(user.createdAt).toLocaleDateString()} {new Date(user.createdAt).toLocaleDateString()}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} );
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@@ -16,12 +16,12 @@ import { Input } from "../../../components/ui/input";
import { ScrollArea } from "../../../components/ui/scroll-area"; import { ScrollArea } from "../../../components/ui/scroll-area";
import { toast } from "../../../components/ui/use-toast"; import { toast } from "../../../components/ui/use-toast";
import { import {
type GroupSummary,
type TenantSummary,
type UserSummary,
bulkUpdateUsers, bulkUpdateUsers,
fetchAllTenants, fetchAllTenants,
fetchGroups, fetchGroups,
type GroupSummary,
type TenantSummary,
type UserSummary,
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";

View File

@@ -30,17 +30,17 @@ import {
} from "../../../lib/adminApi"; } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n"; import { t } from "../../../lib/i18n";
import { import {
buildTenantImportPreview,
type TenantCSVRow, type TenantCSVRow,
type TenantImportPreviewRow, type TenantImportPreviewRow,
buildTenantImportPreview,
} from "../../tenants/utils/tenantCsvImport"; } from "../../tenants/utils/tenantCsvImport";
import { isHanmacFamilyTenant, isHanmacFamilyUser } from "../orgChartPicker"; import { isHanmacFamilyTenant, isHanmacFamilyUser } from "../orgChartPicker";
import { parseUserCSV } from "../utils/csvParser"; import { parseUserCSV } from "../utils/csvParser";
import {
type HanmacImportEmailPreview,
buildHanmacImportEmailPreview,
} from "../utils/hanmacImportEmail";
import { applyGeneralPlanningOfficePriority } from "../utils/generalPlanningOfficePriority"; import { applyGeneralPlanningOfficePriority } from "../utils/generalPlanningOfficePriority";
import {
buildHanmacImportEmailPreview,
type HanmacImportEmailPreview,
} from "../utils/hanmacImportEmail";
interface UserBulkUploadModalProps { interface UserBulkUploadModalProps {
onSuccess?: () => void; onSuccess?: () => void;
@@ -551,7 +551,10 @@ export function UserBulkUploadModal({
</thead> </thead>
<tbody> <tbody>
{previewData.slice(0, 10).map((u, index) => ( {previewData.slice(0, 10).map((u, index) => (
<tr key={`${u.email}-${index}`} className="border-t"> <tr
key={`${u.email}-${u.tenantSlug ?? ""}-${u.name}`}
className="border-t"
>
<td className="p-2"> <td className="p-2">
<input <input
className="h-8 w-full min-w-[180px] rounded-md border border-input bg-background px-2 font-mono text-xs" className="h-8 w-full min-w-[180px] rounded-md border border-input bg-background px-2 font-mono text-xs"

View File

@@ -59,9 +59,7 @@ describe("orgChartPicker", () => {
buildAuthenticatedOrgChartUrl("https://orgchart.example.com/", { buildAuthenticatedOrgChartUrl("https://orgchart.example.com/", {
includeInternal: false, includeInternal: false,
}), }),
).toBe( ).toBe("https://orgchart.example.com/login?auto=1&returnTo=%2Fchart");
"https://orgchart.example.com/login?auto=1&returnTo=%2Fchart",
);
}); });
it("parses the first tenant id and name from orgfront confirm messages", () => { it("parses the first tenant id and name from orgfront confirm messages", () => {

View File

@@ -12,7 +12,9 @@ export const userStatusValues = [
export type UserStatusValue = (typeof userStatusValues)[number]; export type UserStatusValue = (typeof userStatusValues)[number];
export function normalizeUserStatusValue(status?: string | null): UserStatusValue { export function normalizeUserStatusValue(
status?: string | null,
): UserStatusValue {
switch ((status ?? "").trim().toLowerCase()) { switch ((status ?? "").trim().toLowerCase()) {
case "active": case "active":
return "active"; return "active";

View File

@@ -206,6 +206,12 @@ function cleanAdditionalAppointment(
...(appointment.isOwner !== undefined ...(appointment.isOwner !== undefined
? { isOwner: appointment.isOwner } ? { isOwner: appointment.isOwner }
: {}), : {}),
...(appointment.isAdmin !== undefined
? { isAdmin: appointment.isAdmin }
: {}),
...(appointment.isManager !== undefined
? { isManager: appointment.isManager }
: {}),
...(appointment.department ? { department: appointment.department } : {}), ...(appointment.department ? { department: appointment.department } : {}),
...(appointment.grade ? { grade: appointment.grade } : {}), ...(appointment.grade ? { grade: appointment.grade } : {}),
...(appointment.position ? { position: appointment.position } : {}), ...(appointment.position ? { position: appointment.position } : {}),
@@ -232,9 +238,7 @@ function normalizeHeader(header: string) {
"worksmobile_alias_email", "worksmobile_alias_email",
"worksmobile_alias_emails", "worksmobile_alias_emails",
].includes(separatorNormalized) || ].includes(separatorNormalized) ||
["보조이메일", "보조메일", "추가이메일", "추가메일"].includes( ["보조이메일", "보조메일", "추가이메일", "추가메일"].includes(compactKorean)
compactKorean,
)
) { ) {
return "secondary_emails"; return "secondary_emails";
} }

View File

@@ -26,7 +26,8 @@
--input: 215 25% 24%; --input: 215 25% 24%;
--ring: 209 79% 52%; --ring: 209 79% 52%;
--radius: 0.75rem; --radius: 0.75rem;
--app-background-image: radial-gradient( --app-background-image:
radial-gradient(
circle at 10% 18%, circle at 10% 18%,
rgba(54, 211, 153, 0.16), rgba(54, 211, 153, 0.16),
transparent 28% transparent 28%

View File

@@ -701,7 +701,9 @@ export type UserAppointment = {
tenantSlug?: string; tenantSlug?: string;
tenantName: string; tenantName: string;
isPrimary?: boolean; isPrimary?: boolean;
isOwner: boolean; isOwner?: boolean;
isAdmin?: boolean;
isManager?: boolean;
jobTitle?: string; jobTitle?: string;
grade?: string; grade?: string;
position?: string; position?: string;
@@ -713,6 +715,8 @@ export type BulkUserAppointment = {
tenantName?: string; tenantName?: string;
isPrimary?: boolean; isPrimary?: boolean;
isOwner?: boolean; isOwner?: boolean;
isAdmin?: boolean;
isManager?: boolean;
department?: string; department?: string;
grade?: string; grade?: string;
position?: string; position?: string;

View File

@@ -1,8 +1,6 @@
import axios from "axios"; import axios from "axios";
import { shouldStartLoginRedirect } from "../../../common/core/auth"; import { shouldStartLoginRedirect } from "../../../common/core/auth";
import { import { shouldSuppressDevelopmentSessionRedirect } from "../../../common/core/session";
shouldSuppressDevelopmentSessionRedirect,
} from "../../../common/core/session";
import { userManager } from "./auth"; import { userManager } from "./auth";
let isRedirectingToLogin = false; let isRedirectingToLogin = false;

View File

@@ -38,9 +38,11 @@ describe("common cursor pagination fetch", () => {
expect(response.items).toEqual([{ id: "tenant-1" }, { id: "tenant-2" }]); expect(response.items).toEqual([{ id: "tenant-1" }, { id: "tenant-2" }]);
expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0][0].toString()).toContain( expect(fetchMock.mock.calls[0][0].toString()).toContain(
"/api/v1/admin/tenants?parentId=parent-1&limit=1&offset=0", "/api/v1/admin/tenants?parentId=parent-1&limit=1",
); );
expect(fetchMock.mock.calls[0][0].toString()).not.toContain("offset=");
expect(fetchMock.mock.calls[1][0].toString()).toContain("cursor=cursor-1"); expect(fetchMock.mock.calls[1][0].toString()).toContain("cursor=cursor-1");
expect(fetchMock.mock.calls[1][0].toString()).not.toContain("offset=");
expect(fetchMock.mock.calls[0][1]).toMatchObject({ expect(fetchMock.mock.calls[0][1]).toMatchObject({
headers: { Authorization: "Bearer token" }, headers: { Authorization: "Bearer token" },
credentials: "same-origin", credentials: "same-origin",

View File

@@ -1,5 +1,7 @@
const CLIENT_DEBUG_LOG_ENABLED = new Set(["1", "true", "yes", "y", "on"]).has( const CLIENT_DEBUG_LOG_ENABLED = new Set(["1", "true", "yes", "y", "on"]).has(
String(import.meta.env.VITE_CLIENT_LOG_DEBUG ?? "").trim().toLowerCase(), String(import.meta.env.VITE_CLIENT_LOG_DEBUG ?? "")
.trim()
.toLowerCase(),
); );
export function debugLog(...args: Parameters<typeof console.debug>) { export function debugLog(...args: Parameters<typeof console.debug>) {

View File

@@ -1,4 +1,8 @@
import { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, type Locale } from "../../../common/core/i18n"; import {
DEFAULT_LOCALE,
LOCALE_STORAGE_KEY,
type Locale,
} from "../../../common/core/i18n";
function isLocale(value: string): value is Locale { function isLocale(value: string): value is Locale {
return value === "ko" || value === "en"; return value === "ko" || value === "en";

View File

@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
isSuperAdminRole,
normalizeAdminRole,
ROLE_RP_ADMIN, ROLE_RP_ADMIN,
ROLE_SUPER_ADMIN, ROLE_SUPER_ADMIN,
ROLE_TENANT_ADMIN, ROLE_TENANT_ADMIN,
ROLE_USER, ROLE_USER,
isSuperAdminRole,
normalizeAdminRole,
} from "./roles"; } from "./roles";
describe("admin role helpers", () => { describe("admin role helpers", () => {

View File

@@ -3,8 +3,8 @@ import {
readSessionExpiryEnabled, readSessionExpiryEnabled,
SESSION_RENEW_THRESHOLD_MS, SESSION_RENEW_THRESHOLD_MS,
shouldAttemptSlidingSessionRenew, shouldAttemptSlidingSessionRenew,
shouldSuppressDevelopmentSessionRedirect,
shouldAttemptUnlimitedSessionRenew, shouldAttemptUnlimitedSessionRenew,
shouldSuppressDevelopmentSessionRedirect,
writeSessionExpiryEnabled, writeSessionExpiryEnabled,
} from "./sessionSliding"; } from "./sessionSliding";

View File

@@ -1,9 +1,9 @@
export { export {
DEFAULT_SESSION_RENEW_THROTTLE_MS as SESSION_RENEW_THROTTLE_MS,
DEFAULT_SESSION_RENEW_THRESHOLD_MS as SESSION_RENEW_THRESHOLD_MS, DEFAULT_SESSION_RENEW_THRESHOLD_MS as SESSION_RENEW_THRESHOLD_MS,
DEFAULT_SESSION_RENEW_THROTTLE_MS as SESSION_RENEW_THROTTLE_MS,
readSessionExpiryEnabled, readSessionExpiryEnabled,
shouldAttemptSlidingSessionRenew, shouldAttemptSlidingSessionRenew,
shouldSuppressDevelopmentSessionRedirect,
shouldAttemptUnlimitedSessionRenew, shouldAttemptUnlimitedSessionRenew,
shouldSuppressDevelopmentSessionRedirect,
writeSessionExpiryEnabled, writeSessionExpiryEnabled,
} from "../../../common/core/session"; } from "../../../common/core/session";

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
type SortConfig,
compareNullableValues, compareNullableValues,
type SortConfig,
sortItems, sortItems,
toggleSort, toggleSort,
} from "../../../common/core/utils"; } from "../../../common/core/utils";

View File

@@ -3,9 +3,9 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { AuthProvider } from "react-oidc-context"; import { AuthProvider } from "react-oidc-context";
import { RouterProvider } from "react-router-dom"; import { RouterProvider } from "react-router-dom";
import LocaleRefreshBoundary from "./components/common/LocaleRefreshBoundary";
import { queryClient } from "./app/queryClient"; import { queryClient } from "./app/queryClient";
import { router } from "./app/routes"; import { router } from "./app/routes";
import LocaleRefreshBoundary from "./components/common/LocaleRefreshBoundary";
import { Toaster } from "./components/ui/toaster"; import { Toaster } from "./components/ui/toaster";
import { oidcConfig } from "./lib/auth"; import { oidcConfig } from "./lib/auth";
import "./index.css"; import "./index.css";

View File

@@ -74,8 +74,7 @@ const translations: Record<"ko" | "en", Record<string, string>> = {
"user_login_ids.user_id가 존재하지 않거나 soft-deleted user를 참조하는지 검사합니다.", "user_login_ids.user_id가 존재하지 않거나 soft-deleted user를 참조하는지 검사합니다.",
"msg.admin.integrity.check.orphan_user_tenant_memberships.description": "msg.admin.integrity.check.orphan_user_tenant_memberships.description":
"users.tenant_id가 존재하지 않거나 soft-deleted tenant를 참조하는지 검사합니다.", "users.tenant_id가 존재하지 않거나 soft-deleted tenant를 참조하는지 검사합니다.",
"msg.admin.integrity.recheck.running": "msg.admin.integrity.recheck.running": "정합성 검사를 실행 중입니다.",
"정합성 검사를 실행 중입니다.",
"msg.admin.integrity.recheck.success": "검사가 완료되었습니다.", "msg.admin.integrity.recheck.success": "검사가 완료되었습니다.",
"msg.admin.user_projection.forbidden.description": "msg.admin.user_projection.forbidden.description":
"이 화면은 super_admin 권한으로만 접근할 수 있습니다.", "이 화면은 super_admin 권한으로만 접근할 수 있습니다.",
@@ -103,7 +102,8 @@ const translations: Record<"ko" | "en", Record<string, string>> = {
"ui.admin.auth_guard.checker.denied": "Access DENIED", "ui.admin.auth_guard.checker.denied": "Access DENIED",
"ui.admin.auth_guard.checker.denied_description": "ui.admin.auth_guard.checker.denied_description":
"The subject does not have access to the requested resource.", "The subject does not have access to the requested resource.",
"ui.admin.integrity.check.duplicate_tenant_slugs.title": "Duplicate tenant slug", "ui.admin.integrity.check.duplicate_tenant_slugs.title":
"Duplicate tenant slug",
"ui.admin.integrity.section.tenant_integrity": "Tenant integrity", "ui.admin.integrity.section.tenant_integrity": "Tenant integrity",
"ui.admin.integrity.section.user_integrity": "User integrity", "ui.admin.integrity.section.user_integrity": "User integrity",
"ui.admin.integrity.title": "Data Integrity Check", "ui.admin.integrity.title": "Data Integrity Check",
@@ -173,7 +173,8 @@ function format(template: string, vars?: Vars) {
export function createI18nMock() { export function createI18nMock() {
return { return {
t(key: string, fallback?: string, vars?: Vars) { t(key: string, fallback?: string, vars?: Vars) {
const locale = window.localStorage.getItem("locale") === "en" ? "en" : "ko"; const locale =
window.localStorage.getItem("locale") === "en" ? "en" : "ko";
const template = translations[locale][key] ?? fallback ?? key; const template = translations[locale][key] ?? fallback ?? key;
return format(template, vars); return format(template, vars);
}, },

View File

@@ -164,7 +164,9 @@ test.describe("Tenants Management", () => {
await expect(page.locator("table")).toContainText("Platform"); await expect(page.locator("table")).toContainText("Platform");
await expect(page.locator("table")).toContainText("Acme"); await expect(page.locator("table")).toContainText("Acme");
await page.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i).fill(""); await page
.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i)
.fill("");
await page await page
.locator("tbody tr") .locator("tbody tr")
.filter({ hasText: "Planning" }) .filter({ hasText: "Planning" })
@@ -538,7 +540,10 @@ test.describe("Tenants Management", () => {
test("should create a hanmac-family child tenant with org config", async ({ test("should create a hanmac-family child tenant with org config", async ({
page, page,
}) => { }) => {
test.skip(true, "브라우저별 org picker 상호작용이 불안정하여 unit 테스트로 커버합니다."); test.skip(
true,
"브라우저별 org picker 상호작용이 불안정하여 unit 테스트로 커버합니다.",
);
await page.setViewportSize({ width: 1280, height: 800 }); await page.setViewportSize({ width: 1280, height: 800 });
let createBody = ""; let createBody = "";
const tenants = [ const tenants = [

View File

@@ -470,6 +470,193 @@ test.describe("User Management", () => {
.toMatchObject({ status: "preboarding" }); .toMatchObject({ status: "preboarding" });
}); });
test("should center users table loading state and use compact headers", async ({
page,
}) => {
let resolveUsers: (() => void) | undefined;
const usersGate = new Promise<void>((resolve) => {
resolveUsers = resolve;
});
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
await usersGate;
return route.fulfill({
json: {
items: [],
total: 0,
limit: 50,
offset: 0,
},
});
});
await page.goto("/users");
const loadingCell = page.getByTestId("user-table-loading-cell");
await expect(loadingCell).toBeVisible();
await expect(loadingCell).toHaveCSS("display", "flex");
await expect(loadingCell).toHaveCSS("align-items", "center");
await expect(loadingCell).toHaveCSS("justify-content", "center");
const nameHeader = page.getByRole("columnheader", { name: /이름|Name/i });
await expect(nameHeader).toHaveClass(/h-9/);
await expect(nameHeader.locator("> div")).toHaveClass(/h-full/);
resolveUsers?.();
await expect(page.getByTestId("user-table-empty-cell")).toBeVisible();
});
test("should virtualize large user result rows in the users table", async ({
page,
}) => {
const manyUsers = Array.from({ length: 500 }, (_, index) => ({
id: `u-${index}`,
name: `User ${index}`,
email: `user${index}@test.com`,
phone: "010-1111-2222",
loginId: `user${index}`,
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
}));
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
return route.fulfill({
json: {
items: manyUsers,
total: manyUsers.length,
limit: manyUsers.length,
offset: 0,
},
});
});
await page.goto("/users");
await expect(page.getByText("User 0")).toBeVisible();
const renderedStatusControls = await page
.getByTestId(/^user-status-select-/)
.count();
expect(renderedStatusControls).toBeLessThan(manyUsers.length);
await expect(page.getByText("User 499")).toHaveCount(0);
await page.getByTestId("user-table-viewport").evaluate((element) => {
element.scrollTop = element.scrollHeight;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
});
await expect(page.getByText("User 499")).toBeVisible();
});
test("should keep large user search rendering under 200ms", async ({
page,
}) => {
const manyUsers = Array.from({ length: 20_000 }, (_, index) => ({
id: `load-u-${index}`,
name: `Load User ${index}`,
email: `load-user-${index}@test.com`,
phone: "010-1111-2222",
loginId: `load-user-${index}`,
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
}));
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
const url = new URL(route.request().url());
const normalizedSearch = url.searchParams
.get("search")
?.trim()
.toLowerCase();
const items = normalizedSearch
? manyUsers.filter((user) =>
`${user.name} ${user.email}`
.toLowerCase()
.includes(normalizedSearch),
)
: manyUsers;
return route.fulfill({
json: {
items,
total: items.length,
limit: items.length,
offset: 0,
},
});
});
const initialStartedAt = performance.now();
await page.goto("/users");
await expect(page.getByText("Load User 0")).toBeVisible();
const initialMs = performance.now() - initialStartedAt;
const searchInput = page.getByPlaceholder("이름 또는 이메일 검색...");
await searchInput.fill("Load User 19999");
const searchMs = await page.evaluate(async () => {
const input = Array.from(document.querySelectorAll("input")).find(
(candidate) => candidate.placeholder === "이름 또는 이메일 검색...",
);
if (!input) {
throw new Error("User search input was not found.");
}
return await new Promise<number>((resolve, reject) => {
const startedAt = performance.now();
const timeout = window.setTimeout(() => {
observer.disconnect();
reject(new Error("Timed out waiting for large user search result."));
}, 1000);
const observer = new MutationObserver(() => {
const bodyText = document.body.textContent ?? "";
if (
bodyText.includes("Load User 19999") &&
!bodyText.includes("Load User 0")
) {
window.clearTimeout(timeout);
observer.disconnect();
resolve(performance.now() - startedAt);
}
});
observer.observe(document.body, {
childList: true,
characterData: true,
subtree: true,
});
input.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "Enter",
}),
);
});
});
await expect(page.getByText("Load User 19999")).toBeVisible();
await expect(page.getByText("Load User 0")).toHaveCount(0);
console.log(
`[perf] users initial render with ${manyUsers.length} rows: ${initialMs.toFixed(1)}ms`,
);
console.log(
`[perf] users search update with ${manyUsers.length} rows: ${searchMs.toFixed(1)}ms`,
);
expect(searchMs).toBeLessThan(200);
});
test("should expose internal user uuid in the users table", async ({ test("should expose internal user uuid in the users table", async ({
page, page,
}) => { }) => {
@@ -782,12 +969,10 @@ test.describe("User Management", () => {
tenantSlug: "hanmac-team", tenantSlug: "hanmac-team",
primaryTenantId: "hanmac-team-id", primaryTenantId: "hanmac-team-id",
primaryTenantName: "한맥팀", primaryTenantName: "한맥팀",
primaryTenantIsOwner: true,
metadata: { metadata: {
primaryTenantId: "hanmac-team-id", primaryTenantId: "hanmac-team-id",
primaryTenantName: "한맥팀", primaryTenantName: "한맥팀",
primaryTenantSlug: "hanmac-team", primaryTenantSlug: "hanmac-team",
primaryTenantIsOwner: true,
additionalAppointments: [ additionalAppointments: [
{ {
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35", tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
@@ -797,6 +982,14 @@ test.describe("User Management", () => {
], ],
}, },
}); });
expect(updatePayload?.primaryTenantIsOwner).toBeUndefined();
expect(
(updatePayload?.metadata as Record<string, unknown>)
?.primaryTenantIsOwner,
).toBeUndefined();
const appointments = (updatePayload?.metadata as Record<string, unknown>)
?.additionalAppointments as Array<Record<string, unknown>>;
expect(appointments[1].isOwner).toBeUndefined();
}); });
test("should show conflict error when creating with an existing Login ID", async ({ test("should show conflict error when creating with an existing Login ID", async ({

View File

@@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "path"; import path from "path";
import { defineConfig } from "vite";
const buildOutDir = process.env.ADMINFRONT_BUILD_OUT_DIR ?? "dist"; const buildOutDir = process.env.ADMINFRONT_BUILD_OUT_DIR ?? "dist";

View File

@@ -1,6 +1,15 @@
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
const commonRoot = fileURLToPath(new URL("../common", import.meta.url)).replace(
/\\/g,
"/",
);
const commonCoverageIncludes = ["core", "shell", "theme", "ui"].map(
(directory) => `${commonRoot}/${directory}/**/*.{ts,tsx}`,
);
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
esbuild: { esbuild: {
@@ -11,6 +20,29 @@ export default defineConfig({
environment: "jsdom", environment: "jsdom",
setupFiles: "./src/test/setup.ts", setupFiles: "./src/test/setup.ts",
include: ["src/**/*.{test,spec}.{ts,tsx}"], include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov", "json-summary"],
reportsDirectory: "coverage",
all: true,
allowExternal: true,
include: ["src/**/*.{ts,tsx}", ...commonCoverageIncludes],
exclude: [
"**/*.{test,spec}.{ts,tsx}",
"**/*.d.ts",
"**/node_modules/**",
"**/dist/**",
"**/coverage/**",
"src/test/**",
"src/main.tsx",
"src/vite-env.d.ts",
"../common/**/node_modules/**",
"../common/.pnpm-store/**",
`${commonRoot}/theme/**`,
`${commonRoot}/core/pagination/*.worker.ts`,
`${commonRoot}/core/query/queryClient.ts`,
],
},
}, },
server: { server: {
fs: { fs: {

View File

@@ -156,7 +156,7 @@ type orgContextMember struct {
Position string `json:"position,omitempty"` Position string `json:"position,omitempty"`
JobTitle string `json:"jobTitle,omitempty"` JobTitle string `json:"jobTitle,omitempty"`
IsOwner bool `json:"isOwner"` IsOwner bool `json:"isOwner"`
IsLeader bool `json:"isLeader"` IsManager bool `json:"isManager"`
IsPrimary bool `json:"isPrimary"` IsPrimary bool `json:"isPrimary"`
} }
@@ -2412,12 +2412,12 @@ func mapOrgContextMember(user domain.User, appointment map[string]any, includeUs
department = value department = value
} }
isOwner := false isOwner := false
if value, ok := metadataBoolFromMap(appointment, "isOwner", "isManager"); ok { if value, ok := metadataBoolFromMap(appointment, "isOwner"); ok {
isOwner = value isOwner = value
} }
isLeader := isOwner isManager := false
if value, ok := metadataBoolFromMap(appointment, "lead", "isLead"); ok { if value, ok := metadataBoolFromMap(appointment, "isManager", "lead", "isLead"); ok {
isLeader = value isManager = value
} }
isPrimary := false isPrimary := false
if value, ok := metadataBoolFromMap(appointment, "representative", "isPrimary", "primary"); ok { if value, ok := metadataBoolFromMap(appointment, "representative", "isPrimary", "primary"); ok {
@@ -2439,7 +2439,7 @@ func mapOrgContextMember(user domain.User, appointment map[string]any, includeUs
Position: position, Position: position,
JobTitle: jobTitle, JobTitle: jobTitle,
IsOwner: isOwner, IsOwner: isOwner,
IsLeader: isLeader, IsManager: isManager,
IsPrimary: isPrimary, IsPrimary: isPrimary,
} }
} }

View File

@@ -690,7 +690,7 @@ func TestTenantHandler_GetOrgContextJSONDefaultsToHanmacFamilyForApiKey(t *testi
"additionalAppointments": []any{ "additionalAppointments": []any{
map[string]any{ map[string]any{
"tenantSlug": "sso", "tenantSlug": "sso",
"lead": true, "isManager": true,
"position": "파트장", "position": "파트장",
}, },
}, },
@@ -743,8 +743,9 @@ func TestTenantHandler_GetOrgContextJSONDefaultsToHanmacFamilyForApiKey(t *testi
require.Equal(t, "lead@example.com", firstUser["email"]) require.Equal(t, "lead@example.com", firstUser["email"])
require.Equal(t, "플랫폼 리드", firstUser["name"]) require.Equal(t, "플랫폼 리드", firstUser["name"])
require.Equal(t, true, firstUser["isOwner"]) require.Equal(t, true, firstUser["isOwner"])
require.Equal(t, true, firstUser["isLeader"]) require.Equal(t, false, firstUser["isManager"])
require.Equal(t, true, firstUser["isPrimary"]) require.Equal(t, true, firstUser["isPrimary"])
require.NotContains(t, firstUser, "isLeader")
require.Equal(t, "수석", firstUser["grade"]) require.Equal(t, "수석", firstUser["grade"])
require.Equal(t, "실장", firstUser["position"]) require.Equal(t, "실장", firstUser["position"])
require.Equal(t, "기술기획", firstUser["jobTitle"]) require.Equal(t, "기술기획", firstUser["jobTitle"])
@@ -754,7 +755,8 @@ func TestTenantHandler_GetOrgContextJSONDefaultsToHanmacFamilyForApiKey(t *testi
appointmentOnly := ssoMembers[0].(map[string]any) appointmentOnly := ssoMembers[0].(map[string]any)
require.Equal(t, "appointment@example.com", appointmentOnly["email"]) require.Equal(t, "appointment@example.com", appointmentOnly["email"])
require.Equal(t, false, appointmentOnly["isOwner"]) require.Equal(t, false, appointmentOnly["isOwner"])
require.Equal(t, true, appointmentOnly["isLeader"]) require.Equal(t, true, appointmentOnly["isManager"])
require.NotContains(t, appointmentOnly, "isLeader")
tree := got["tree"].(map[string]any) tree := got["tree"].(map[string]any)
require.Equal(t, "group-hanmac-family", tree["id"]) require.Equal(t, "group-hanmac-family", tree["id"])

View File

@@ -191,8 +191,8 @@ func BuildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain
type worksmobileAppointment struct { type worksmobileAppointment struct {
TenantID string TenantID string
IsPrimary bool IsPrimary bool
IsOwner bool IsManager bool
HasOwner bool HasManager bool
JobTitle string JobTitle string
PositionID string PositionID string
} }
@@ -247,8 +247,8 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
Primary: appointment.IsPrimary, Primary: appointment.IsPrimary,
PositionID: appointment.PositionID, PositionID: appointment.PositionID,
} }
if appointment.HasOwner { if appointment.HasManager {
isManager := appointment.IsOwner isManager := appointment.IsManager
orgUnit.IsManager = &isManager orgUnit.IsManager = &isManager
} }
organizations = append(organizations, WorksmobileUserOrganization{ organizations = append(organizations, WorksmobileUserOrganization{
@@ -285,9 +285,9 @@ func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileA
JobTitle: metadataString(domain.JSONMap(item), "jobTitle", "job_title", "task"), JobTitle: metadataString(domain.JSONMap(item), "jobTitle", "job_title", "task"),
PositionID: metadataString(domain.JSONMap(item), "worksmobilePositionId", "positionId", "position_id"), PositionID: metadataString(domain.JSONMap(item), "worksmobilePositionId", "positionId", "position_id"),
} }
if isOwner, ok := metadataOptionalBool(domain.JSONMap(item), "isOwner", "isManager"); ok { if isManager, ok := metadataOptionalBool(domain.JSONMap(item), "isManager", "lead", "isLead"); ok {
appointment.IsOwner = isOwner appointment.IsManager = isManager
appointment.HasOwner = true appointment.HasManager = true
} }
appointments = append(appointments, appointment) appointments = append(appointments, appointment)
} }

View File

@@ -150,14 +150,14 @@ func TestBuildWorksmobileUserPayloadMapsAdditionalAppointmentsToOrgUnits(t *test
map[string]any{ map[string]any{
"tenantId": secondaryTenantID, "tenantId": secondaryTenantID,
"isPrimary": false, "isPrimary": false,
"isOwner": true, "isManager": true,
"jobTitle": "PM", "jobTitle": "PM",
"position": "팀장", "position": "팀장",
}, },
map[string]any{ map[string]any{
"tenantId": primaryTenantID, "tenantId": primaryTenantID,
"isPrimary": true, "isPrimary": true,
"isOwner": false, "isOwner": true,
"jobTitle": "Engineering", "jobTitle": "Engineering",
"position": "책임", "position": "책임",
}, },
@@ -194,8 +194,7 @@ func TestBuildWorksmobileUserPayloadMapsAdditionalAppointmentsToOrgUnits(t *test
require.True(t, payload.Organizations[0].Primary) require.True(t, payload.Organizations[0].Primary)
require.Equal(t, "externalKey:"+primaryTenantID, payload.Organizations[0].OrgUnits[0].OrgUnitID) require.Equal(t, "externalKey:"+primaryTenantID, payload.Organizations[0].OrgUnits[0].OrgUnitID)
require.True(t, payload.Organizations[0].OrgUnits[0].Primary) require.True(t, payload.Organizations[0].OrgUnits[0].Primary)
require.NotNil(t, payload.Organizations[0].OrgUnits[0].IsManager) require.Nil(t, payload.Organizations[0].OrgUnits[0].IsManager)
require.False(t, *payload.Organizations[0].OrgUnits[0].IsManager)
require.Equal(t, int64(1002), payload.Organizations[1].DomainID) require.Equal(t, int64(1002), payload.Organizations[1].DomainID)
require.False(t, payload.Organizations[1].Primary) require.False(t, payload.Organizations[1].Primary)
require.Equal(t, "externalKey:"+secondaryTenantID, payload.Organizations[1].OrgUnits[0].OrgUnitID) require.Equal(t, "externalKey:"+secondaryTenantID, payload.Organizations[1].OrgUnits[0].OrgUnitID)

4
common/biome.json Normal file
View File

@@ -0,0 +1,4 @@
{
"root": true,
"extends": ["./config/biome.base.json"]
}

View File

@@ -1,32 +1,42 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "root": false,
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "space" "indentStyle": "space"
}, },
"css": {
"parser": {
"tailwindDirectives": true
}
},
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"style": { "style": {
"useNodejsImportProtocol": "off",
"useEnumInitializers": "off" "useEnumInitializers": "off"
}, },
"suspicious": {
"noUnknownAtRules": "off"
},
"a11y": { "a11y": {
"noLabelWithoutControl": "off" "noLabelWithoutControl": "off"
} }
} }
}, },
"organizeImports": { "assist": { "actions": { "source": { "organizeImports": "on" } } },
"enabled": true
},
"files": { "files": {
"ignore": [ "includes": [
"dist", "**",
".vite", "!**/dist/**",
"node_modules", "!**/.vite/**",
"tsconfig*.json", "!**/node_modules/**",
"test-results", "!**/coverage/**",
"test-results.nobody-backup", "!**/tsconfig*.json",
"playwright-report" "!**/test-results/**",
"!**/test-results.nobody-backup/**",
"!**/playwright-report/**"
] ]
} }
} }

View File

@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { type UserConfig, defineConfig } from "vite"; import { defineConfig, type UserConfig } from "vite";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const commonWorkspaceDir = path.resolve( const commonWorkspaceDir = path.resolve(

View File

@@ -1,17 +1,8 @@
import { ChevronDown, ChevronUp, Copy } from "lucide-react"; import { ChevronDown, ChevronUp, Copy } from "lucide-react";
import * as React from "react"; import * as React from "react";
import type { CommonAuditLog } from "../../audit";
import { import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../audit";
import {
getCommonBadgeClasses,
type CommonBadgeVariant, type CommonBadgeVariant,
getCommonBadgeClasses,
} from "../../../ui/badge"; } from "../../../ui/badge";
import { getCommonButtonClasses } from "../../../ui/button"; import { getCommonButtonClasses } from "../../../ui/button";
import { import {
@@ -26,6 +17,15 @@ import {
commonTableViewportClass, commonTableViewportClass,
commonTableWrapperClass, commonTableWrapperClass,
} from "../../../ui/table"; } from "../../../ui/table";
import type { CommonAuditLog } from "../../audit";
import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../audit";
type AuditTranslate = ( type AuditTranslate = (
key: string, key: string,
@@ -77,7 +77,10 @@ export function AuditLogTable({
<div className={commonTableWrapperClass}> <div className={commonTableWrapperClass}>
<table className={cx(commonTableClass, "table-fixed")}> <table className={cx(commonTableClass, "table-fixed")}>
<thead <thead
className={cx(commonTableHeaderClass, commonStickyTableHeaderClass)} className={cx(
commonTableHeaderClass,
commonStickyTableHeaderClass,
)}
> >
<tr className={commonTableRowClass}> <tr className={commonTableRowClass}>
<th className={cx(commonTableHeadClass, "w-[190px]")}> <th className={cx(commonTableHeadClass, "w-[190px]")}>
@@ -115,7 +118,10 @@ export function AuditLogTable({
<tr className={commonTableRowClass}> <tr className={commonTableRowClass}>
<td <td
colSpan={6} colSpan={6}
className={cx(commonTableCellClass, "text-center text-muted-foreground")} className={cx(
commonTableCellClass,
"text-center text-muted-foreground",
)}
> >
{t("msg.common.audit.empty", "No audit logs found.")} {t("msg.common.audit.empty", "No audit logs found.")}
</td> </td>
@@ -132,7 +138,12 @@ export function AuditLogTable({
return ( return (
<React.Fragment key={rowKey}> <React.Fragment key={rowKey}>
<tr className={cx(commonTableRowClass, "bg-card/40")}> <tr className={cx(commonTableRowClass, "bg-card/40")}>
<td className={cx(commonTableCellClass, "text-xs text-muted-foreground")}> <td
className={cx(
commonTableCellClass,
"text-xs text-muted-foreground",
)}
>
<div className="space-y-1"> <div className="space-y-1">
<div>{date}</div> <div>{date}</div>
<div>{time}</div> <div>{time}</div>
@@ -165,14 +176,20 @@ export function AuditLogTable({
</div> </div>
</td> </td>
<td <td
className={cx(commonTableCellClass, "text-xs text-muted-foreground")} className={cx(
commonTableCellClass,
"text-xs text-muted-foreground",
)}
> >
<div className="font-semibold text-foreground"> <div className="font-semibold text-foreground">
{actionLabel} {actionLabel}
</div> </div>
</td> </td>
<td <td
className={cx(commonTableCellClass, "text-xs text-muted-foreground")} className={cx(
commonTableCellClass,
"text-xs text-muted-foreground",
)}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="break-all">{targetLabel}</span> <span className="break-all">{targetLabel}</span>
@@ -230,18 +247,26 @@ export function AuditLogTable({
</tr> </tr>
{expanded ? ( {expanded ? (
<tr className={cx(commonTableRowClass, "bg-card/20")}> <tr className={cx(commonTableRowClass, "bg-card/20")}>
<td colSpan={6} className={cx(commonTableCellClass, "text-xs")}> <td
colSpan={6}
className={cx(commonTableCellClass, "text-xs")}
>
<div className="grid gap-4 text-muted-foreground md:grid-cols-3"> <div className="grid gap-4 text-muted-foreground md:grid-cols-3">
<div className="space-y-1"> <div className="space-y-1">
<div className="uppercase tracking-[0.16em]"> <div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.request", "Request")} {t(
"ui.common.audit.details.request",
"Request",
)}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
"ui.common.audit.details.request_id", "ui.common.audit.details.request_id",
"Request ID · {{value}}", "Request ID · {{value}}",
{ {
value: formatAuditValue(details.request_id), value: formatAuditValue(
details.request_id,
),
}, },
)} )}
</div> </div>
@@ -255,9 +280,13 @@ export function AuditLogTable({
)} )}
</div> </div>
<div> <div>
{t("ui.common.audit.details.ip", "IP · {{value}}", { {t(
"ui.common.audit.details.ip",
"IP · {{value}}",
{
value: formatAuditValue(row.ip_address), value: formatAuditValue(row.ip_address),
})} },
)}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(
@@ -306,7 +335,9 @@ export function AuditLogTable({
"ui.common.audit.details.tenant", "ui.common.audit.details.tenant",
"Tenant · {{value}}", "Tenant · {{value}}",
{ {
value: formatAuditValue(details.tenant_id), value: formatAuditValue(
details.tenant_id,
),
}, },
)} )}
</div> </div>
@@ -329,7 +360,10 @@ export function AuditLogTable({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="uppercase tracking-[0.16em]"> <div className="uppercase tracking-[0.16em]">
{t("ui.common.audit.details.result", "Result")} {t(
"ui.common.audit.details.result",
"Result",
)}
</div> </div>
<div className="break-all"> <div className="break-all">
{t( {t(

View File

@@ -1,3 +1,3 @@
export { OverviewMetric } from "./OverviewMetric";
export { OverviewAxisNotes } from "./OverviewAxisNotes"; export { OverviewAxisNotes } from "./OverviewAxisNotes";
export { OverviewMetric } from "./OverviewMetric";
export { OverviewSelectionChips } from "./OverviewSelectionChips"; export { OverviewSelectionChips } from "./OverviewSelectionChips";

View File

@@ -1,15 +1,13 @@
import type { ReactNode, ThHTMLAttributes } from "react"; import type { ReactNode, ThHTMLAttributes } from "react";
import type { SortConfig } from "../../utils";
import { import {
commonStickyTableHeaderClass, commonStickyTableHeaderClass,
commonTableHeadClass, commonTableHeadClass,
} from "../../../ui/table"; } from "../../../ui/table";
import type { SortConfig } from "../../utils";
export const sortableTableHeadBaseClassName = export const sortableTableHeadBaseClassName = commonTableHeadClass;
commonTableHeadClass;
export const sortableTableHeaderClassName = export const sortableTableHeaderClassName = commonStickyTableHeaderClass;
commonStickyTableHeaderClass;
function SortAscendingIcon() { function SortAscendingIcon() {
return ( return (
@@ -126,7 +124,7 @@ export function SortableTableHead<Key extends string>({
...props ...props
}: SortableTableHeadProps<Key>) { }: SortableTableHeadProps<Key>) {
const isActive = sortConfig?.key === sortKey; const isActive = sortConfig?.key === sortKey;
const direction = isActive ? sortConfig?.direction ?? null : null; const direction = isActive ? (sortConfig?.direction ?? null) : null;
return ( return (
<th <th

View File

@@ -2,8 +2,8 @@ export { createTomlTranslator } from "./loader";
export { export {
DEFAULT_LOCALE, DEFAULT_LOCALE,
LOCALE_STORAGE_KEY, LOCALE_STORAGE_KEY,
SUPPORTED_LOCALES,
type Locale, type Locale,
SUPPORTED_LOCALES,
type TomlObject, type TomlObject,
type TomlValue, type TomlValue,
type TranslatorInput, type TranslatorInput,

View File

@@ -1,8 +1,8 @@
import { import {
DEFAULT_LOCALE, DEFAULT_LOCALE,
LOCALE_STORAGE_KEY, LOCALE_STORAGE_KEY,
SUPPORTED_LOCALES,
type Locale, type Locale,
SUPPORTED_LOCALES,
type TomlObject, type TomlObject,
type TomlValue, type TomlValue,
type TranslatorInput, type TranslatorInput,
@@ -155,10 +155,16 @@ export function createTomlTranslator(
const translations: Record<Locale, TomlObject> = { const translations: Record<Locale, TomlObject> = {
ko: input.ko ko: input.ko
.map((raw) => parseToml(raw)) .map((raw) => parseToml(raw))
.reduce<TomlObject>((merged, current) => mergeTomlObjects(merged, current), {}), .reduce<TomlObject>(
(merged, current) => mergeTomlObjects(merged, current),
{},
),
en: input.en en: input.en
.map((raw) => parseToml(raw)) .map((raw) => parseToml(raw))
.reduce<TomlObject>((merged, current) => mergeTomlObjects(merged, current), {}), .reduce<TomlObject>(
(merged, current) => mergeTomlObjects(merged, current),
{},
),
}; };
return function t( return function t(

View File

@@ -34,9 +34,12 @@ function shouldUseWorker(useWorker: boolean | undefined) {
async function fetchAllCursorPagesInWorker<TItem>( async function fetchAllCursorPagesInWorker<TItem>(
request: CursorFetchRequest, request: CursorFetchRequest,
): Promise<CursorPageResponse<TItem>> { ): Promise<CursorPageResponse<TItem>> {
const worker = new Worker(new URL("./cursorFetch.worker.ts", import.meta.url), { const worker = new Worker(
new URL("./cursorFetch.worker.ts", import.meta.url),
{
type: "module", type: "module",
}); },
);
const id = createRequestId(); const id = createRequestId();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -1,7 +1,7 @@
import { import {
fetchAllCursorPagesMainThread,
type CursorFetchRequest, type CursorFetchRequest,
type CursorPageResponse, type CursorPageResponse,
fetchAllCursorPagesMainThread,
} from "./cursorFetchCore"; } from "./cursorFetchCore";
type CursorWorkerRequestMessage = { type CursorWorkerRequestMessage = {
@@ -21,7 +21,9 @@ type CursorWorkerResponseMessage<TItem> =
error: string; error: string;
}; };
self.addEventListener("message", async (event: MessageEvent<CursorWorkerRequestMessage>) => { self.addEventListener(
"message",
async (event: MessageEvent<CursorWorkerRequestMessage>) => {
const { id, request } = event.data; const { id, request } = event.data;
try { try {
@@ -38,6 +40,5 @@ self.addEventListener("message", async (event: MessageEvent<CursorWorkerRequestM
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
} satisfies CursorWorkerResponseMessage<unknown>); } satisfies CursorWorkerResponseMessage<unknown>);
} }
}); },
);
export {};

View File

@@ -44,7 +44,6 @@ function buildCursorFetchUrl(
} }
url.searchParams.set("limit", String(pageSize)); url.searchParams.set("limit", String(pageSize));
url.searchParams.set("offset", "0");
if (cursor) { if (cursor) {
url.searchParams.set("cursor", cursor); url.searchParams.set("cursor", cursor);
} else { } else {
@@ -75,7 +74,9 @@ export async function fetchAllCursorPagesMainThread<TItem>({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Cursor page request failed with status ${response.status}`); throw new Error(
`Cursor page request failed with status ${response.status}`,
);
} }
const page = (await response.json()) as CursorPageResponse<TItem>; const page = (await response.json()) as CursorPageResponse<TItem>;

View File

@@ -1,6 +1,6 @@
export { export {
fetchAllCursorPages,
fetchAllCursorPagesMainThread,
type CursorFetchRequest, type CursorFetchRequest,
type CursorPageResponse, type CursorPageResponse,
fetchAllCursorPages,
fetchAllCursorPagesMainThread,
} from "./cursorFetch"; } from "./cursorFetch";

View File

@@ -29,7 +29,7 @@
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "2.4.16",
"@playwright/test": "^1.58.0", "@playwright/test": "^1.58.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.5", "@types/react": "^19.2.5",
@@ -114,11 +114,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@biomejs/biome": { "node_modules/@biomejs/biome": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz",
"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==",
"dev": true, "dev": true,
"hasInstallScript": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"bin": { "bin": {
"biome": "bin/biome" "biome": "bin/biome"
@@ -131,20 +130,20 @@
"url": "https://opencollective.com/biome" "url": "https://opencollective.com/biome"
}, },
"optionalDependencies": { "optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-arm64": "2.4.16",
"@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-darwin-x64": "2.4.16",
"@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64": "2.4.16",
"@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-arm64-musl": "2.4.16",
"@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64": "2.4.16",
"@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-linux-x64-musl": "2.4.16",
"@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-arm64": "2.4.16",
"@biomejs/cli-win32-x64": "1.9.4" "@biomejs/cli-win32-x64": "2.4.16"
} }
}, },
"node_modules/@biomejs/cli-darwin-arm64": { "node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz",
"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -159,9 +158,9 @@
} }
}, },
"node_modules/@biomejs/cli-darwin-x64": { "node_modules/@biomejs/cli-darwin-x64": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz",
"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -176,9 +175,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64": { "node_modules/@biomejs/cli-linux-arm64": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz",
"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -196,9 +195,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64-musl": { "node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz",
"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -216,9 +215,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64": { "node_modules/@biomejs/cli-linux-x64": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz",
"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -236,9 +235,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64-musl": { "node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz",
"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -256,9 +255,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-arm64": { "node_modules/@biomejs/cli-win32-arm64": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz",
"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -273,9 +272,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-x64": { "node_modules/@biomejs/cli-win32-x64": {
"version": "1.9.4", "version": "2.4.16",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz",
"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],

View File

@@ -4,45 +4,47 @@
"scripts": { "scripts": {
"dev:all": "pnpm -r run dev", "dev:all": "pnpm -r run dev",
"build:all": "pnpm -r run build", "build:all": "pnpm -r run build",
"lint": "biome check .",
"lint:fix": "biome check . --write",
"lint:all": "pnpm -r run lint" "lint:all": "pnpm -r run lint"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "2.4.16",
"@playwright/test": "^1.58.0", "@playwright/test": "^1.58.0",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"jsdom": "^28.1.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^8.0.3", "vite": "^8.0.3",
"vitest": "^4.1.5", "vitest": "^4.1.5"
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^28.1.0"
}, },
"dependencies": { "dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^6.28.2",
"@tanstack/react-query": "^5.66.8",
"@tanstack/react-query-devtools": "^5.66.8",
"axios": "^1.7.9",
"lucide-react": "^0.563.0",
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"class-variance-authority": "^0.7.1",
"zod": "^3.24.1",
"react-hook-form": "^7.71.1",
"oidc-client-ts": "^3.4.1",
"react-oidc-context": "^3.3.0",
"@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-avatar": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-scroll-area": "^1.1.2", "@radix-ui/react-scroll-area": "^1.1.2",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.2" "@radix-ui/react-switch": "^1.1.2",
"@tanstack/react-query": "^5.66.8",
"@tanstack/react-query-devtools": "^5.66.8",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"oidc-client-ts": "^3.4.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.71.1",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^6.28.2",
"tailwind-merge": "^3.4.0",
"zod": "^3.24.1"
} }
} }

320
common/pnpm-lock.yaml generated
View File

@@ -73,8 +73,8 @@ importers:
version: 3.25.76 version: 3.25.76
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: ^1.9.4 specifier: 2.4.16
version: 1.9.4 version: 2.4.16
'@playwright/test': '@playwright/test':
specifier: ^1.58.0 specifier: ^1.58.0
version: 1.60.0 version: 1.60.0
@@ -113,7 +113,7 @@ importers:
version: 8.0.12(@types/node@24.12.4)(jiti@1.21.7) version: 8.0.12(@types/node@24.12.4)(jiti@1.21.7)
vitest: vitest:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.1.6(@types/node@24.12.4)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)) version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7))
../adminfront: ../adminfront:
dependencies: dependencies:
@@ -184,6 +184,9 @@ importers:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3 version: 4.4.3
devDependencies: devDependencies:
'@biomejs/biome':
specifier: 2.4.16
version: 2.4.16
'@playwright/test': '@playwright/test':
specifier: ^1.60.0 specifier: ^1.60.0
version: 1.60.0 version: 1.60.0
@@ -211,12 +214,18 @@ importers:
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/coverage-v8':
specifier: 4.1.6
version: 4.1.6(vitest@4.1.6)
autoprefixer: autoprefixer:
specifier: ^10.5.0 specifier: ^10.5.0
version: 10.5.0(postcss@8.5.14) version: 10.5.0(postcss@8.5.14)
jsdom: jsdom:
specifier: ^28.1.0 specifier: ^28.1.0
version: 28.1.0 version: 28.1.0
playwright:
specifier: 1.60.0
version: 1.60.0
postcss: postcss:
specifier: ^8.5.14 specifier: ^8.5.14
version: 8.5.14 version: 8.5.14
@@ -234,7 +243,7 @@ importers:
version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7) version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7)
vitest: vitest:
specifier: ^4.1.6 specifier: ^4.1.6
version: 4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
../devfront: ../devfront:
dependencies: dependencies:
@@ -302,6 +311,9 @@ importers:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3 version: 4.4.3
devDependencies: devDependencies:
'@biomejs/biome':
specifier: 2.4.16
version: 2.4.16
'@playwright/test': '@playwright/test':
specifier: ^1.60.0 specifier: ^1.60.0
version: 1.60.0 version: 1.60.0
@@ -316,10 +328,16 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/coverage-v8':
specifier: 4.1.6
version: 4.1.6(vitest@4.1.6)
autoprefixer: autoprefixer:
specifier: ^10.5.0 specifier: ^10.5.0
version: 10.5.0(postcss@8.5.14) version: 10.5.0(postcss@8.5.14)
jsdom:
specifier: ^28.1.0
version: 28.1.0
postcss: postcss:
specifier: ^8.5.14 specifier: ^8.5.14
version: 8.5.14 version: 8.5.14
@@ -333,11 +351,11 @@ importers:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
vite: vite:
specifier: ^8.0.12 specifier: ^8.0.14
version: 8.0.12(@types/node@25.7.0)(jiti@1.21.7) version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7)
vitest: vitest:
specifier: ^4.1.6 specifier: ^4.1.6
version: 4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
../orgfront: ../orgfront:
dependencies: dependencies:
@@ -408,6 +426,9 @@ importers:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3 version: 4.4.3
devDependencies: devDependencies:
'@biomejs/biome':
specifier: 2.4.16
version: 2.4.16
'@playwright/test': '@playwright/test':
specifier: ^1.60.0 specifier: ^1.60.0
version: 1.60.0 version: 1.60.0
@@ -422,7 +443,10 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/coverage-v8':
specifier: 4.1.6
version: 4.1.6(vitest@4.1.6)
autoprefixer: autoprefixer:
specifier: ^10.5.0 specifier: ^10.5.0
version: 10.5.0(postcss@8.5.14) version: 10.5.0(postcss@8.5.14)
@@ -442,11 +466,11 @@ importers:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
vite: vite:
specifier: ^8.0.12 specifier: ^8.0.14
version: 8.0.12(@types/node@25.7.0)(jiti@1.21.7) version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7)
vitest: vitest:
specifier: ^4.1.6 specifier: ^4.1.6
version: 4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
packages: packages:
@@ -478,67 +502,84 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5': '@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/runtime@7.29.2': '@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@biomejs/biome@1.9.4': '@babel/types@7.29.7':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@biomejs/biome@2.4.16':
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.4.16':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.4.16':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.4.16':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.4.16':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.4.16':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.4.16':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.4.16':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.4.16':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -1381,6 +1422,15 @@ packages:
babel-plugin-react-compiler: babel-plugin-react-compiler:
optional: true optional: true
'@vitest/coverage-v8@4.1.6':
resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==}
peerDependencies:
'@vitest/browser': 4.1.6
vitest: 4.1.6
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@4.1.6': '@vitest/expect@4.1.6':
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
@@ -1460,6 +1510,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
ast-v8-to-istanbul@1.0.2:
resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==}
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -1755,6 +1808,10 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1771,6 +1828,9 @@ packages:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
@@ -1810,10 +1870,25 @@ packages:
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
jiti@1.21.7: jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true hasBin: true
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1932,6 +2007,13 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.5.3:
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2225,6 +2307,11 @@ packages:
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
semver@7.8.1:
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
engines: {node: '>=10'}
hasBin: true
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
@@ -2250,6 +2337,10 @@ packages:
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
hasBin: true hasBin: true
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-preserve-symlinks-flag@1.0.0: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2577,47 +2668,60 @@ snapshots:
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0 js-tokens: 4.0.0
picocolors: 1.1.1 picocolors: 1.1.1
'@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/parser@7.29.7':
dependencies:
'@babel/types': 7.29.7
'@babel/runtime@7.29.2': {} '@babel/runtime@7.29.2': {}
'@biomejs/biome@1.9.4': '@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.4.16':
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4 '@biomejs/cli-darwin-arm64': 2.4.16
'@biomejs/cli-darwin-x64': 1.9.4 '@biomejs/cli-darwin-x64': 2.4.16
'@biomejs/cli-linux-arm64': 1.9.4 '@biomejs/cli-linux-arm64': 2.4.16
'@biomejs/cli-linux-arm64-musl': 1.9.4 '@biomejs/cli-linux-arm64-musl': 2.4.16
'@biomejs/cli-linux-x64': 1.9.4 '@biomejs/cli-linux-x64': 2.4.16
'@biomejs/cli-linux-x64-musl': 1.9.4 '@biomejs/cli-linux-x64-musl': 2.4.16
'@biomejs/cli-win32-arm64': 1.9.4 '@biomejs/cli-win32-arm64': 2.4.16
'@biomejs/cli-win32-x64': 1.9.4 '@biomejs/cli-win32-x64': 2.4.16
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.4.16':
optional: true optional: true
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.4.16':
optional: true optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.4.16':
optional: true optional: true
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.4.16':
optional: true optional: true
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.4.16':
optional: true optional: true
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.4.16':
optional: true optional: true
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.4.16':
optional: true optional: true
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.4.16':
optional: true optional: true
'@bramus/specificity@2.4.2': '@bramus/specificity@2.4.2':
@@ -3340,16 +3444,25 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.12(@types/node@24.12.4)(jiti@1.21.7) vite: 8.0.12(@types/node@24.12.4)(jiti@1.21.7)
'@vitejs/plugin-react@6.0.1(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.12(@types/node@25.7.0)(jiti@1.21.7)
'@vitejs/plugin-react@6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))': '@vitejs/plugin-react@6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.14(@types/node@25.7.0)(jiti@1.21.7) vite: 8.0.14(@types/node@25.7.0)(jiti@1.21.7)
'@vitest/coverage-v8@4.1.6(vitest@4.1.6)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.6
ast-v8-to-istanbul: 1.0.2
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.3
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/expect@4.1.6': '@vitest/expect@4.1.6':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -3367,14 +3480,6 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 8.0.12(@types/node@24.12.4)(jiti@1.21.7) vite: 8.0.12(@types/node@24.12.4)(jiti@1.21.7)
'@vitest/mocker@4.1.6(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7))':
dependencies:
'@vitest/spy': 4.1.6
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.12(@types/node@25.7.0)(jiti@1.21.7)
'@vitest/mocker@4.1.6(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))': '@vitest/mocker@4.1.6(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))':
dependencies: dependencies:
'@vitest/spy': 4.1.6 '@vitest/spy': 4.1.6
@@ -3463,6 +3568,12 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
ast-v8-to-istanbul@1.0.2:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
asynckit@0.4.0: {} asynckit@0.4.0: {}
autoprefixer@10.5.0(postcss@8.5.14): autoprefixer@10.5.0(postcss@8.5.14):
@@ -3741,6 +3852,8 @@ snapshots:
gopd@1.2.0: {} gopd@1.2.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2: has-tostringtag@1.0.2:
@@ -3757,6 +3870,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@noble/hashes' - '@noble/hashes'
html-escaper@2.0.2: {}
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2:
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
@@ -3798,8 +3913,23 @@ snapshots:
is-potential-custom-element-name@1.0.1: {} is-potential-custom-element-name@1.0.1: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jiti@1.21.7: {} jiti@1.21.7: {}
js-tokens@10.0.0: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
jsdom@28.1.0: jsdom@28.1.0:
@@ -3900,6 +4030,16 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
magicast@0.5.3:
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.8.1
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
mdn-data@2.27.1: {} mdn-data@2.27.1: {}
@@ -4171,6 +4311,8 @@ snapshots:
scheduler@0.27.0: {} scheduler@0.27.0: {}
semver@7.8.1: {}
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
siginfo@2.0.0: {} siginfo@2.0.0: {}
@@ -4195,6 +4337,10 @@ snapshots:
tinyglobby: 0.2.16 tinyglobby: 0.2.16
ts-interface-checker: 0.1.13 ts-interface-checker: 0.1.13
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
symbol-tree@3.2.4: {} symbol-tree@3.2.4: {}
@@ -4323,18 +4469,6 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 1.21.7 jiti: 1.21.7
vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.14
rolldown: 1.0.0
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 25.7.0
fsevents: 2.3.3
jiti: 1.21.7
vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7): vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
@@ -4347,7 +4481,7 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 1.21.7 jiti: 1.21.7
vitest@4.1.6(@types/node@24.12.4)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)): vitest@4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)):
dependencies: dependencies:
'@vitest/expect': 4.1.6 '@vitest/expect': 4.1.6
'@vitest/mocker': 4.1.6(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)) '@vitest/mocker': 4.1.6(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7))
@@ -4371,39 +4505,12 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.12.4 '@types/node': 24.12.4
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
jsdom: 28.1.0 jsdom: 28.1.0
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw
vitest@4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)): vitest@4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)):
dependencies:
'@vitest/expect': 4.1.6
'@vitest/mocker': 4.1.6(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/pretty-format': 4.1.6
'@vitest/runner': 4.1.6
'@vitest/snapshot': 4.1.6
'@vitest/spy': 4.1.6
'@vitest/utils': 4.1.6
es-module-lexer: 2.1.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.1.2
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
vite: 8.0.12(@types/node@25.7.0)(jiti@1.21.7)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.7.0
jsdom: 28.1.0
transitivePeerDependencies:
- msw
vitest@4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)):
dependencies: dependencies:
'@vitest/expect': 4.1.6 '@vitest/expect': 4.1.6
'@vitest/mocker': 4.1.6(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) '@vitest/mocker': 4.1.6(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))
@@ -4427,6 +4534,7 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 25.7.0 '@types/node': 25.7.0
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
jsdom: 28.1.0 jsdom: 28.1.0
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw

View File

@@ -1,6 +1,6 @@
import { import {
SESSION_EXPIRY_STORAGE_KEY,
readSessionExpiryEnabled, readSessionExpiryEnabled,
SESSION_EXPIRY_STORAGE_KEY,
writeSessionExpiryEnabled, writeSessionExpiryEnabled,
} from "../core/session"; } from "../core/session";
@@ -27,8 +27,8 @@ type ShellProfileSummaryParams = {
export const SHELL_THEME_STORAGE_KEY = "admin_theme"; export const SHELL_THEME_STORAGE_KEY = "admin_theme";
export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY; export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY;
export { AppSidebar } from "./AppSidebar";
export type { ShellSidebarNavItem } from "./AppSidebar"; export type { ShellSidebarNavItem } from "./AppSidebar";
export { AppSidebar } from "./AppSidebar";
export { shellLayoutClasses } from "./layout"; export { shellLayoutClasses } from "./layout";
export function readShellTheme(): ShellTheme { export function readShellTheme(): ShellTheme {

View File

@@ -1,6 +1,4 @@
{ {
"extends": ["../common/config/biome.base.json"], "root": true,
"files": { "extends": ["../common/config/biome.base.json"]
"ignore": [".vite"]
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -12,22 +12,26 @@
"lint": "biome check .", "lint": "biome check .",
"preview": "vite preview", "preview": "vite preview",
"test": "playwright test", "test": "playwright test",
"test:coverage": "vitest run --coverage",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:roles": "playwright test tests/devfront-role-switch-report.spec.ts", "test:roles": "playwright test tests/devfront-role-switch-report.spec.ts",
"test:ui": "playwright test --ui" "test:ui": "playwright test --ui"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.16",
"@playwright/test": "^1.60.0", "@playwright/test": "^1.60.0",
"@types/node": "^25.7.0", "@types/node": "^25.7.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitest/coverage-v8": "4.1.6",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0", "autoprefixer": "^10.5.0",
"jsdom": "^28.1.0",
"postcss": "^8.5.14", "postcss": "^8.5.14",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.12", "vite": "^8.0.14",
"vitest": "^4.1.6" "vitest": "^4.1.6"
}, },
"dependencies": { "dependencies": {

615
devfront/pnpm-lock.yaml generated
View File

@@ -72,6 +72,9 @@ importers:
specifier: ^4.4.3 specifier: ^4.4.3
version: 4.4.3 version: 4.4.3
devDependencies: devDependencies:
'@biomejs/biome':
specifier: 2.4.16
version: 2.4.16
'@playwright/test': '@playwright/test':
specifier: ^1.60.0 specifier: ^1.60.0
version: 1.60.0 version: 1.60.0
@@ -87,9 +90,15 @@ importers:
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) version: 6.0.1(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/coverage-v8':
specifier: 4.1.6
version: 4.1.6(vitest@4.1.6)
autoprefixer: autoprefixer:
specifier: ^10.5.0 specifier: ^10.5.0
version: 10.5.0(postcss@8.5.14) version: 10.5.0(postcss@8.5.14)
jsdom:
specifier: ^28.1.0
version: 28.1.0
postcss: postcss:
specifier: ^8.5.14 specifier: ^8.5.14
version: 8.5.14 version: 8.5.14
@@ -107,14 +116,149 @@ importers:
version: 8.0.13(@types/node@25.7.0)(jiti@1.21.7) version: 8.0.13(@types/node@25.7.0)(jiti@1.21.7)
vitest: vitest:
specifier: ^4.1.6 specifier: ^4.1.6
version: 4.1.6(@types/node@25.7.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7))
packages: packages:
'@acemir/cssom@0.9.31':
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
'@alloc/quick-lru@5.2.0': '@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@asamuzakjp/css-color@5.1.11':
resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
'@asamuzakjp/dom-selector@6.8.1':
resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==}
'@asamuzakjp/generational-cache@1.0.1':
resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@biomejs/biome@2.4.16':
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.4.16':
resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.4.16':
resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.4.16':
resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@2.4.16':
resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.16':
resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@2.4.16':
resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.16':
resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.4.16':
resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@bramus/specificity@2.4.2':
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true
'@csstools/color-helpers@6.0.2':
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
engines: {node: '>=20.19.0'}
'@csstools/css-calc@3.2.1':
resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-color-parser@4.1.1':
resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-parser-algorithms': ^4.0.0
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-parser-algorithms@4.0.0':
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-syntax-patches-for-csstree@1.1.4':
resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==}
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
'@csstools/css-tokenizer@4.0.0':
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@emnapi/core@1.10.0': '@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -124,6 +268,15 @@ packages:
'@emnapi/wasi-threads@1.2.1': '@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@exodus/bytes@1.15.1':
resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
'@noble/hashes': ^1.8.0 || ^2.0.0
peerDependenciesMeta:
'@noble/hashes':
optional: true
'@floating-ui/core@1.7.5': '@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
@@ -731,6 +884,15 @@ packages:
babel-plugin-react-compiler: babel-plugin-react-compiler:
optional: true optional: true
'@vitest/coverage-v8@4.1.6':
resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==}
peerDependencies:
'@vitest/browser': 4.1.6
vitest: 4.1.6
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@4.1.6': '@vitest/expect@4.1.6':
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
@@ -764,6 +926,10 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
any-promise@1.3.0: any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -782,6 +948,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
ast-v8-to-istanbul@1.0.2:
resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==}
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -800,6 +969,9 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -854,14 +1026,26 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
css-tree@3.2.1:
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
cssesc@3.0.0: cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
hasBin: true hasBin: true
cssstyle@6.2.0:
resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==}
engines: {node: '>=20'}
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
data-urls@7.0.0:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -871,6 +1055,9 @@ packages:
supports-color: supports-color:
optional: true optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
delayed-stream@1.0.0: delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@@ -895,6 +1082,10 @@ packages:
electron-to-chromium@1.5.355: electron-to-chromium@1.5.355:
resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==}
entities@8.0.0:
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
engines: {node: '>=20.19.0'}
es-define-property@1.0.1: es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -998,6 +1189,10 @@ packages:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1010,10 +1205,25 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
is-binary-path@2.1.0: is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1034,10 +1244,37 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
jiti@1.21.7: jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true hasBin: true
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
jsdom@28.1.0:
resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
jwt-decode@4.0.0: jwt-decode@4.0.0:
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1123,6 +1360,10 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lru-cache@11.5.1:
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
engines: {node: 20 || >=22}
lucide-react@1.14.0: lucide-react@1.14.0:
resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==}
peerDependencies: peerDependencies:
@@ -1131,10 +1372,20 @@ packages:
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.5.3:
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
merge2@1.4.1: merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1184,6 +1435,9 @@ packages:
resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==}
engines: {node: '>=18'} engines: {node: '>=18'}
parse5@8.0.1:
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
path-parse@1.0.7: path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -1270,6 +1524,10 @@ packages:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'} engines: {node: '>=10'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -1349,6 +1607,10 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resolve@1.22.12: resolve@1.22.12:
resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1366,9 +1628,18 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
semver@7.8.1:
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
engines: {node: '>=10'}
hasBin: true
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
@@ -1390,10 +1661,17 @@ packages:
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
hasBin: true hasBin: true
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
supports-preserve-symlinks-flag@1.0.0: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tailwind-merge@3.6.0: tailwind-merge@3.6.0:
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
@@ -1429,10 +1707,25 @@ packages:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
tldts-core@7.4.0:
resolution: {integrity: sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==}
tldts@7.4.0:
resolution: {integrity: sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==}
hasBin: true
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
tough-cookie@6.0.1:
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
engines: {node: '>=16'}
tr46@6.0.0:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
ts-interface-checker@0.1.13: ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@@ -1447,6 +1740,10 @@ packages:
undici-types@7.21.0: undici-types@7.21.0:
resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==}
undici@7.26.0:
resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==}
engines: {node: '>=20.18.1'}
update-browserslist-db@1.2.3: update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true hasBin: true
@@ -1565,18 +1862,141 @@ packages:
jsdom: jsdom:
optional: true optional: true
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
webidl-conversions@8.0.1:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
whatwg-mimetype@5.0.0:
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
engines: {node: '>=20'}
whatwg-url@16.0.1:
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
why-is-node-running@2.3.0: why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
zod@4.4.3: zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
snapshots: snapshots:
'@acemir/cssom@0.9.31': {}
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@asamuzakjp/css-color@5.1.11':
dependencies:
'@asamuzakjp/generational-cache': 1.0.1
'@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@asamuzakjp/dom-selector@6.8.1':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
bidi-js: 1.0.3
css-tree: 3.2.1
is-potential-custom-element-name: 1.0.1
lru-cache: 11.5.1
'@asamuzakjp/generational-cache@1.0.1': {}
'@asamuzakjp/nwsapi@2.3.9': {}
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/parser@7.29.7':
dependencies:
'@babel/types': 7.29.7
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.4.16':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.16
'@biomejs/cli-darwin-x64': 2.4.16
'@biomejs/cli-linux-arm64': 2.4.16
'@biomejs/cli-linux-arm64-musl': 2.4.16
'@biomejs/cli-linux-x64': 2.4.16
'@biomejs/cli-linux-x64-musl': 2.4.16
'@biomejs/cli-win32-arm64': 2.4.16
'@biomejs/cli-win32-x64': 2.4.16
'@biomejs/cli-darwin-arm64@2.4.16':
optional: true
'@biomejs/cli-darwin-x64@2.4.16':
optional: true
'@biomejs/cli-linux-arm64-musl@2.4.16':
optional: true
'@biomejs/cli-linux-arm64@2.4.16':
optional: true
'@biomejs/cli-linux-x64-musl@2.4.16':
optional: true
'@biomejs/cli-linux-x64@2.4.16':
optional: true
'@biomejs/cli-win32-arm64@2.4.16':
optional: true
'@biomejs/cli-win32-x64@2.4.16':
optional: true
'@bramus/specificity@2.4.2':
dependencies:
css-tree: 3.2.1
'@csstools/color-helpers@6.0.2': {}
'@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/color-helpers': 6.0.2
'@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
'@csstools/css-tokenizer@4.0.0': {}
'@emnapi/core@1.10.0': '@emnapi/core@1.10.0':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.2.1 '@emnapi/wasi-threads': 1.2.1
@@ -1593,6 +2013,8 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@exodus/bytes@1.15.1': {}
'@floating-ui/core@1.7.5': '@floating-ui/core@1.7.5':
dependencies: dependencies:
'@floating-ui/utils': 0.2.11 '@floating-ui/utils': 0.2.11
@@ -2132,6 +2554,20 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-rc.7 '@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.13(@types/node@25.7.0)(jiti@1.21.7) vite: 8.0.13(@types/node@25.7.0)(jiti@1.21.7)
'@vitest/coverage-v8@4.1.6(vitest@4.1.6)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.6
ast-v8-to-istanbul: 1.0.2
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.3
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7))
'@vitest/expect@4.1.6': '@vitest/expect@4.1.6':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -2179,6 +2615,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
agent-base@7.1.4: {}
any-promise@1.3.0: {} any-promise@1.3.0: {}
anymatch@3.1.3: anymatch@3.1.3:
@@ -2194,6 +2632,12 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
ast-v8-to-istanbul@1.0.2:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
asynckit@0.4.0: {} asynckit@0.4.0: {}
autoprefixer@10.5.0(postcss@8.5.14): autoprefixer@10.5.0(postcss@8.5.14):
@@ -2217,6 +2661,10 @@ snapshots:
baseline-browser-mapping@2.10.29: {} baseline-browser-mapping@2.10.29: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
braces@3.0.3: braces@3.0.3:
@@ -2270,14 +2718,35 @@ snapshots:
cookie@1.1.1: {} cookie@1.1.1: {}
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
source-map-js: 1.2.1
cssesc@3.0.0: {} cssesc@3.0.0: {}
cssstyle@6.2.0:
dependencies:
'@asamuzakjp/css-color': 5.1.11
'@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1)
css-tree: 3.2.1
lru-cache: 11.5.1
csstype@3.2.3: {} csstype@3.2.3: {}
data-urls@7.0.0:
dependencies:
whatwg-mimetype: 5.0.0
whatwg-url: 16.0.1
transitivePeerDependencies:
- '@noble/hashes'
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decimal.js@10.6.0: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
@@ -2296,6 +2765,8 @@ snapshots:
electron-to-chromium@1.5.355: {} electron-to-chromium@1.5.355: {}
entities@8.0.0: {}
es-define-property@1.0.1: {} es-define-property@1.0.1: {}
es-errors@1.3.0: {} es-errors@1.3.0: {}
@@ -2391,6 +2862,8 @@ snapshots:
gopd@1.2.0: {} gopd@1.2.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2: has-tostringtag@1.0.2:
@@ -2401,6 +2874,21 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
html-encoding-sniffer@6.0.0:
dependencies:
'@exodus/bytes': 1.15.1
transitivePeerDependencies:
- '@noble/hashes'
html-escaper@2.0.2: {}
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
@@ -2408,6 +2896,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
is-binary-path@2.1.0: is-binary-path@2.1.0:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
@@ -2424,8 +2919,52 @@ snapshots:
is-number@7.0.0: {} is-number@7.0.0: {}
is-potential-custom-element-name@1.0.1: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jiti@1.21.7: {} jiti@1.21.7: {}
js-tokens@10.0.0: {}
jsdom@28.1.0:
dependencies:
'@acemir/cssom': 0.9.31
'@asamuzakjp/dom-selector': 6.8.1
'@bramus/specificity': 2.4.2
'@exodus/bytes': 1.15.1
cssstyle: 6.2.0
data-urls: 7.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 6.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
parse5: 8.0.1
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 6.0.1
undici: 7.26.0
w3c-xmlserializer: 5.0.0
webidl-conversions: 8.0.1
whatwg-mimetype: 5.0.0
whatwg-url: 16.0.1
xml-name-validator: 5.0.0
transitivePeerDependencies:
- '@noble/hashes'
- supports-color
jwt-decode@4.0.0: {} jwt-decode@4.0.0: {}
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
@@ -2481,6 +3020,8 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
lru-cache@11.5.1: {}
lucide-react@1.14.0(react@19.2.6): lucide-react@1.14.0(react@19.2.6):
dependencies: dependencies:
react: 19.2.6 react: 19.2.6
@@ -2489,8 +3030,20 @@ snapshots:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
magicast@0.5.3:
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.8.1
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
mdn-data@2.27.1: {}
merge2@1.4.1: {} merge2@1.4.1: {}
micromatch@4.0.8: micromatch@4.0.8:
@@ -2528,6 +3081,10 @@ snapshots:
dependencies: dependencies:
jwt-decode: 4.0.0 jwt-decode: 4.0.0
parse5@8.0.1:
dependencies:
entities: 8.0.0
path-parse@1.0.7: {} path-parse@1.0.7: {}
pathe@2.0.3: {} pathe@2.0.3: {}
@@ -2589,6 +3146,8 @@ snapshots:
proxy-from-env@2.1.0: {} proxy-from-env@2.1.0: {}
punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-dom@19.2.6(react@19.2.6): react-dom@19.2.6(react@19.2.6):
@@ -2656,6 +3215,8 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.2 picomatch: 2.3.2
require-from-string@2.0.2: {}
resolve@1.22.12: resolve@1.22.12:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -2690,8 +3251,14 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
scheduler@0.27.0: {} scheduler@0.27.0: {}
semver@7.8.1: {}
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
siginfo@2.0.0: {} siginfo@2.0.0: {}
@@ -2712,8 +3279,14 @@ snapshots:
tinyglobby: 0.2.16 tinyglobby: 0.2.16
ts-interface-checker: 0.1.13 ts-interface-checker: 0.1.13
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
symbol-tree@3.2.4: {}
tailwind-merge@3.6.0: {} tailwind-merge@3.6.0: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.19): tailwindcss-animate@1.0.7(tailwindcss@3.4.19):
@@ -2767,10 +3340,24 @@ snapshots:
tinyrainbow@3.1.0: {} tinyrainbow@3.1.0: {}
tldts-core@7.4.0: {}
tldts@7.4.0:
dependencies:
tldts-core: 7.4.0
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
tough-cookie@6.0.1:
dependencies:
tldts: 7.4.0
tr46@6.0.0:
dependencies:
punycode: 2.3.1
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
tslib@2.8.1: {} tslib@2.8.1: {}
@@ -2779,6 +3366,8 @@ snapshots:
undici-types@7.21.0: {} undici-types@7.21.0: {}
undici@7.26.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.2): update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies: dependencies:
browserslist: 4.28.2 browserslist: 4.28.2
@@ -2818,7 +3407,7 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 1.21.7 jiti: 1.21.7
vitest@4.1.6(@types/node@25.7.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)): vitest@4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)):
dependencies: dependencies:
'@vitest/expect': 4.1.6 '@vitest/expect': 4.1.6
'@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7))
@@ -2842,12 +3431,34 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 25.7.0 '@types/node': 25.7.0
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
jsdom: 28.1.0
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
webidl-conversions@8.0.1: {}
whatwg-mimetype@5.0.0: {}
whatwg-url@16.0.1:
dependencies:
'@exodus/bytes': 1.15.1
tr46: 6.0.0
webidl-conversions: 8.0.1
transitivePeerDependencies:
- '@noble/hashes'
why-is-node-running@2.3.0: why-is-node-running@2.3.0:
dependencies: dependencies:
siginfo: 2.0.0 siginfo: 2.0.0
stackback: 0.0.2 stackback: 0.0.2
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
zod@4.4.3: {} zod@4.4.3: {}

View File

@@ -1,4 +1,4 @@
import { type RouteObject, createBrowserRouter } from "react-router-dom"; import { createBrowserRouter, type RouteObject } from "react-router-dom";
import AppLayout from "../components/layout/AppLayout"; import AppLayout from "../components/layout/AppLayout";
import AuditLogsPage from "../features/audit/AuditLogsPage"; import AuditLogsPage from "../features/audit/AuditLogsPage";
import AuthCallbackPage from "../features/auth/AuthCallbackPage"; import AuthCallbackPage from "../features/auth/AuthCallbackPage";

View File

@@ -15,13 +15,13 @@ import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { import {
AppSidebar, AppSidebar,
type ShellSidebarNavItem,
type ShellTranslator,
applyShellTheme, applyShellTheme,
buildShellProfileSummary, buildShellProfileSummary,
buildShellSessionStatus, buildShellSessionStatus,
readShellSessionExpiryEnabled, readShellSessionExpiryEnabled,
readShellTheme, readShellTheme,
type ShellSidebarNavItem,
type ShellTranslator,
shellLayoutClasses, shellLayoutClasses,
writeShellSessionExpiryEnabled, writeShellSessionExpiryEnabled,
} from "../../../../common/shell"; } from "../../../../common/shell";
@@ -156,9 +156,12 @@ function AppLayout() {
window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
return () => { return () => {
window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); window.removeEventListener(
LOCALE_CHANGED_EVENT,
rerenderDevelopmentShell,
);
}; };
}, [isDevelopmentRuntime]); }, []);
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
@@ -271,7 +274,6 @@ function AppLayout() {
auth.isAuthenticated, auth.isAuthenticated,
auth.isLoading, auth.isLoading,
auth.user?.expires_at, auth.user?.expires_at,
isDevelopmentRuntime,
isSessionExpiryEnabled, isSessionExpiryEnabled,
]); ]);
@@ -481,7 +483,10 @@ function AppLayout() {
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">
{t("ui.shell.session.auto_extend", "Session expiry")} {t(
"ui.shell.session.auto_extend",
"Session expiry",
)}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{isSessionExpiryEnabled ? ( {isSessionExpiryEnabled ? (

View File

@@ -44,4 +44,4 @@ const AvatarFallback = React.forwardRef<
)); ));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback }; export { Avatar, AvatarFallback, AvatarImage };

View File

@@ -50,9 +50,9 @@ function CardFooter({
export { export {
Card, Card,
CardContent,
CardDescription,
CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription,
CardContent,
CardFooter,
}; };

View File

@@ -92,11 +92,11 @@ TableCaption.displayName = "TableCaption";
export { export {
Table, Table,
TableHeader,
TableBody, TableBody,
TableCaption,
TableCell,
TableFooter, TableFooter,
TableHead, TableHead,
TableHeader,
TableRow, TableRow,
TableCell,
TableCaption,
}; };

View File

@@ -1,8 +1,7 @@
import { AlertTriangle, ExternalLink, LogIn, ShieldHalf } from "lucide-react"; import { AlertTriangle, ExternalLink, LogIn, ShieldHalf } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { import {
Card, Card,

View File

@@ -14,7 +14,7 @@ import {
Upload, Upload,
X, X,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useAuth } from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import { Link, useNavigate, useParams } from "react-router-dom"; import { Link, useNavigate, useParams } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page"; import { PageHeader } from "../../../../common/core/components/page";
@@ -32,6 +32,13 @@ import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch"; import { Switch } from "../../components/ui/switch";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast"; import { toast } from "../../components/ui/use-toast";
import type {
ClientStatus,
ClientType,
ClientUpsertRequest,
MyTenantSummary,
TenantSummary,
} from "../../lib/devApi";
import { import {
type ClientRelation, type ClientRelation,
createClient, createClient,
@@ -44,13 +51,6 @@ import {
updateClient, updateClient,
updateClientStatus, updateClientStatus,
} from "../../lib/devApi"; } from "../../lib/devApi";
import type {
ClientStatus,
ClientType,
ClientUpsertRequest,
MyTenantSummary,
TenantSummary,
} from "../../lib/devApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
import { resolveProfileRole } from "../../lib/role"; import { resolveProfileRole } from "../../lib/role";
import { cn } from "../../lib/utils"; import { cn } from "../../lib/utils";
@@ -408,6 +408,59 @@ function ClientGeneralPage() {
]); ]);
const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]); const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);
const tenantScopeDescription = t(
"msg.dev.clients.scopes.tenant",
"소속 테넌트 정보 접근",
);
const buildTenantScope = useCallback(
(id: string): ScopeItem => ({
id,
name: "tenant",
description: tenantScopeDescription,
mandatory: true,
locked: true,
}),
[tenantScopeDescription],
);
const normalizeScopesForTenantAccess = useCallback(
(nextScopes: ScopeItem[], restricted: boolean): ScopeItem[] => {
const normalized = nextScopes.map((scope) => {
if (scope.name.trim() !== "tenant") {
return scope;
}
return {
...scope,
description: scope.description || tenantScopeDescription,
mandatory: restricted,
locked: restricted,
};
});
if (
restricted &&
!normalized.some((scope) => scope.name.trim() === "tenant")
) {
normalized.push(buildTenantScope(`tenant-${Date.now()}`));
}
const openidScopes = normalized.filter(
(scope) => scope.name.trim() === "openid",
);
const tenantScopes = normalized.filter(
(scope) => scope.name.trim() === "tenant",
);
const remainingScopes = normalized.filter((scope) => {
const name = scope.name.trim();
return name !== "openid" && name !== "tenant";
});
return [...openidScopes, ...tenantScopes, ...remainingScopes];
},
[buildTenantScope, tenantScopeDescription],
);
useEffect(() => { useEffect(() => {
if (!data) return; if (!data) return;
const { client } = data; const { client } = data;
@@ -511,7 +564,7 @@ function ClientGeneralPage() {
); );
} }
setIdTokenClaims(readIdTokenClaimsMetadata(metadata)); setIdTokenClaims(readIdTokenClaimsMetadata(metadata));
}, [data]); }, [data, normalizeScopesForTenantAccess]);
const securityProfile: SecurityProfile = const securityProfile: SecurityProfile =
clientType === "pkce" ? "pkce" : "private"; clientType === "pkce" ? "pkce" : "private";
@@ -574,56 +627,6 @@ function ClientGeneralPage() {
} }
}; };
const tenantScopeDescription = t(
"msg.dev.clients.scopes.tenant",
"소속 테넌트 정보 접근",
);
const buildTenantScope = (id: string): ScopeItem => ({
id,
name: "tenant",
description: tenantScopeDescription,
mandatory: true,
locked: true,
});
function normalizeScopesForTenantAccess(
nextScopes: ScopeItem[],
restricted: boolean,
): ScopeItem[] {
const normalized = nextScopes.map((scope) => {
if (scope.name.trim() !== "tenant") {
return scope;
}
return {
...scope,
description: scope.description || tenantScopeDescription,
mandatory: restricted,
locked: restricted,
};
});
if (
restricted &&
!normalized.some((scope) => scope.name.trim() === "tenant")
) {
normalized.push(buildTenantScope(`tenant-${Date.now()}`));
}
const openidScopes = normalized.filter(
(scope) => scope.name.trim() === "openid",
);
const tenantScopes = normalized.filter(
(scope) => scope.name.trim() === "tenant",
);
const remainingScopes = normalized.filter((scope) => {
const name = scope.name.trim();
return name !== "openid" && name !== "tenant";
});
return [...openidScopes, ...tenantScopes, ...remainingScopes];
}
const handleTenantAccessToggle = (enabled: boolean) => { const handleTenantAccessToggle = (enabled: boolean) => {
setTenantAccessRestricted(enabled); setTenantAccessRestricted(enabled);
setIsTenantSearchOpen(enabled); setIsTenantSearchOpen(enabled);
@@ -2307,7 +2310,7 @@ function ClientGeneralPage() {
</span> </span>
{securityProfile === "private" && ( {securityProfile === "private" && (
<div <fieldset
className="mt-4 flex items-center justify-between border-t border-primary/20 pt-4" className="mt-4 flex items-center justify-between border-t border-primary/20 pt-4"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
@@ -2335,7 +2338,7 @@ function ClientGeneralPage() {
onCheckedChange={handleHeadlessToggle} onCheckedChange={handleHeadlessToggle}
disabled={isGeneralSettingsReadOnly} disabled={isGeneralSettingsReadOnly}
/> />
</div> </fieldset>
)} )}
</label> </label>
@@ -2674,8 +2677,7 @@ function ClientGeneralPage() {
</div> </div>
{currentHeadlessJwksCache.parsedKeys?.length ? ( {currentHeadlessJwksCache.parsedKeys?.length ? (
<div className="space-y-3"> <div className="space-y-3">
{currentHeadlessJwksCache.parsedKeys.map( {currentHeadlessJwksCache.parsedKeys.map((key) => {
(key, index) => {
const normalizedAlgorithm = key.alg?.trim() ?? ""; const normalizedAlgorithm = key.alg?.trim() ?? "";
const isMissingAlgorithm = const isMissingAlgorithm =
normalizedAlgorithm === ""; normalizedAlgorithm === "";
@@ -2687,7 +2689,7 @@ function ClientGeneralPage() {
return ( return (
<div <div
key={`${key.kid || "key"}-${index}`} key={`${key.kid ?? "missing-kid"}-${key.kty ?? ""}-${key.alg ?? ""}-${key.n ?? ""}`}
className={cn( className={cn(
"rounded-xl border bg-muted/30 p-3", "rounded-xl border bg-muted/30 p-3",
isUnsupportedAlgorithm || isMissingAlgorithm isUnsupportedAlgorithm || isMissingAlgorithm
@@ -2770,8 +2772,7 @@ function ClientGeneralPage() {
</div> </div>
</div> </div>
); );
}, })}
)}
</div> </div>
) : ( ) : (
<div className="rounded-lg border border-dashed border-border px-4 py-5 text-sm text-muted-foreground"> <div className="rounded-lg border border-dashed border-border px-4 py-5 text-sm text-muted-foreground">

View File

@@ -26,8 +26,8 @@ import {
} from "../../components/ui/table"; } from "../../components/ui/table";
import { toast } from "../../components/ui/use-toast"; import { toast } from "../../components/ui/use-toast";
import { import {
type DevAssignableUser,
addClientRelation, addClientRelation,
type DevAssignableUser,
fetchClient, fetchClient,
fetchClientRelations, fetchClientRelations,
fetchDevUsers, fetchDevUsers,
@@ -355,7 +355,10 @@ function ClientRelationsPage() {
</nav> </nav>
<PageHeader <PageHeader
icon={<ShieldHalf size={20} />} icon={<ShieldHalf size={20} />}
title={t("ui.dev.clients.relationships.title", "Client Relationships")} title={t(
"ui.dev.clients.relationships.title",
"Client Relationships",
)}
description={t( description={t(
"msg.dev.clients.relationships.subtitle", "msg.dev.clients.relationships.subtitle",
"RP direct operator relation을 조회하고 User 단위로 추가·삭제합니다.", "RP direct operator relation을 조회하고 User 단위로 추가·삭제합니다.",

Some files were not shown because too many files have changed in this diff Show More