diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f681798 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +# PR 에서는 배포하지 않고 검증만 한다. 깨진 채로 main 에 들어가지 않게 하는 것이 목적이다. +on: + pull_request: + branches: [main] + +jobs: + verify: + name: 빌드 · 타입 · 린트 · 화면 검증 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: 의존성 설치 + run: npm ci + + - name: 타입 검사 + run: npm run typecheck + + - name: 린트 + run: npx eslint src scripts + + - name: 빌드 + run: npm run build + + # 이전에 손으로 잡던 것들이다. /data 표가 그리드 칸을 밀어 가로로 넘쳤고, + # 표에 caption 이 없어 스크린리더가 무슨 표인지 알려 주지 못했다. + - name: 화면 검증 (반응형 · 접근성) + run: | + npx serve out -p 4173 -L & + npx wait-on http://localhost:4173 --timeout 60000 + node scripts/verify-pages.mjs http://localhost:4173 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..856d82c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,93 @@ +name: Deploy + +on: + push: + branches: [main] + # 사이트에 적힌 수집량은 백엔드가 매주 새로 받아 온다. 다시 빌드해야 반영된다. + schedule: + - cron: '0 20 * * 1' # 매주 화요일 05:00 KST (어린이집·유치원 동기화 이후) + workflow_dispatch: + +# 배포는 한 번에 하나만. 뒤에 들어온 것이 이기게 두면 낡은 빌드가 최종본이 될 수 있다. +concurrency: + group: pages + cancel-in-progress: false + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + name: 정적 파일 생성 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: 의존성 설치 + run: npm ci + + - uses: actions/configure-pages@v5 + id: pages + + - name: 빌드 + # 프로젝트 페이지는 /<저장소명> 아래에 붙는다. basePath 가 없으면 CSS·JS 가 전부 404 다. + env: + NEXT_PUBLIC_BASE_PATH: ${{ steps.pages.outputs.base_path }} + NEXT_PUBLIC_SITE_URL: ${{ steps.pages.outputs.base_url }} + run: npm run build + + - uses: actions/upload-pages-artifact@v3 + with: + path: out + + deploy: + name: Pages 배포 + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/deploy-pages@v4 + id: deployment + + smoke: + name: 배포 후 확인 + needs: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # 배포가 성공했다는 것과 사이트가 실제로 뜬다는 것은 다르다. + # 파일이 올라갔는데 basePath 가 어긋나 CSS 가 404 인 경우가 대표적이다. + - name: 페이지 응답 확인 + env: + SITE: ${{ needs.deploy.outputs.page_url || format('https://{0}.github.io/{1}/', github.repository_owner, github.event.repository.name) }} + run: | + base="${SITE%/}" + fail=0 + for path in "/" "/benefits/" "/facilities/" "/data/" "/trust/" "/sitemap.xml" "/robots.txt"; do + code=$(curl -sS -o /dev/null -w '%{http_code}' --retry 5 --retry-delay 5 --retry-all-errors "$base$path") + printf ' %-16s %s\n' "$path" "$code" + [ "$code" = "200" ] || fail=1 + done + + # 스타일이 붙었는지까지 봐야 의미가 있다. basePath 가 어긋나면 여기서 걸린다. + css=$(curl -sS "$base/" | grep -oE '/[^"]*\.css' | head -1) + if [ -n "$css" ]; then + code=$(curl -sS -o /dev/null -w '%{http_code}' "$base$css") + printf ' %-16s %s\n' "css" "$code" + [ "$code" = "200" ] || fail=1 + else + echo " css 링크를 찾지 못했습니다" + fail=1 + fi + + exit $fail diff --git a/README.md b/README.md index 0480908..acf9332 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,82 @@ npm run dev # http://localhost:3000 npm run build # out/ 에 정적 파일 생성 ``` -서버가 할 일이 없어 `output: 'export'` 로 정적 출력만 만듭니다. +서버가 할 일이 없어 `output: 'export'` 로 정적 출력만 만듭니다. `out/` 를 그대로 어디든 +올리면 됩니다. + +## 페이지 + +| 경로 | 내용 | +|------|------| +| `/` | 개요. 월령 축을 옮기면 지원금이 유효·소급가능·지남으로 갈립니다 | +| `/benefits` | 자격 판정, 수령액 계산, 금액 신뢰도 | +| `/facilities` | 정원 관측, 빈자리 알림, 대기 기간 통계 | +| `/data` | 출처 네 곳과 **아는 것 / 모르는 것** 대조 | +| `/trust` | 수집 항목, 민감정보 처리, 정보주체 권리 | + +## 디자인 + +방향은 **육아수첩 · 관공서 기록부**입니다. 이 분야에서 흔한 파스텔 베이비 톤도, 핀테크식 +무채색도 쓰지 않았습니다. 정부가 나눠 주는 아기수첩 — 모눈 인쇄, 개월 수 탭, 도장, 표 — 을 +기준으로 삼았습니다. + +| 토큰 | 값 | 쓰임 | +|------|-----|------| +| `--paper` | `#F6F8F3` | 재생지에 가까운 차가운 오프화이트 | +| `--ink` | `#1C2620` | 도장 잉크에 가까운 진한 녹흑 | +| `--seal` | `#36AA1C` | 유효·현재 (앱과 같은 브랜드 그린) | +| `--past` | `#A8B2A5` | 이미 지나간 것 | +| `--stamp` | `#C2453D` | 마감·경고 (경보 빨강이 아니라 도장 빨강) | +| `--rule` | `#DCE3D6` | 모눈선 | + +### 표제를 얇게 쓰는 이유 + +이 서비스의 목소리는 "다 받으세요"가 아니라 "이건 알고 이건 모릅니다"입니다. +700 굵기 고딕 헤드라인은 그 성격에 맞지 않아, Pretendard 300 을 크게 놓아 표제를 만듭니다. + +한글 명조를 쓰면 더 좋았겠지만 `next/font` 에 한글 서브셋을 제공하는 구글 폰트가 없고, +런타임에 외부에서 받아 오면 방문자 IP 가 매번 나갑니다. 개인정보 고지를 따로 두는 서비스에서 +그건 앞뒤가 맞지 않아 자체 호스팅한 Pretendard 만 씁니다. 숫자는 IBM Plex Mono 로 자릿수를 +맞춥니다. + +### 시그니처 + +홈의 **월령 타임라인**입니다. 문구로 설득하는 대신 축을 놓고 직접 옮겨 보게 합니다. +밴드 구성은 실제 정책 데이터 모델(대상 연령 상·하한, 지급액, 소급 개월)을 따랐고, +금액을 모르는 정책은 `미상` 으로 둡니다. + +## 배포 + +`main` 에 들어가면 GitHub Actions 가 빌드해 GitHub Pages 로 올립니다. PR 에서는 배포하지 않고 +빌드·타입·린트와 화면 검증만 돕니다. + +배포가 끝나면 페이지 5개와 `sitemap.xml`·`robots.txt` 가 실제로 200 인지, 그리고 **CSS 가 붙는지** +확인합니다. 파일은 올라갔는데 `basePath` 가 어긋나 스타일만 404 인 경우가 흔해서입니다. + +```bash +npm run verify # 로컬에서 같은 검사 (out/ 을 먼저 서빙해야 합니다) +``` + +### 수치 갱신 + +사이트에 적힌 수집량은 `src/content/figures.ts` 한 곳에 있고 `MEASURED_ON` 과 함께 둡니다. +백엔드가 매주 공공데이터를 새로 받아 오므로 값이 낡습니다. + +1. `CareCode_Interface` 에서 `./gradlew liveSyncCheck` 로 실측 +2. `figures.ts` 의 값과 `MEASURED_ON` 을 **함께** 수정 +3. `main` 에 반영하면 자동 배포 + +값만 고치고 날짜를 안 고치면 거짓말이 됩니다. 매주 화요일 새벽에도 자동으로 다시 빌드하지만, +그건 코드에 적힌 값을 다시 그리는 것일 뿐 실측값을 새로 받아 오지는 않습니다. + +## 원칙 + +**근거 없는 숫자는 쓰지 않습니다.** `/data` 에 무엇을 모르는지 적어 둔 것도 같은 이유입니다. +어린이집 자리와 지원금은 부모가 실제 결정을 내리는 정보라, 그럴듯한 추정치를 믿고 다른 +선택지를 포기한 사람이 손해를 봅니다. + +수치를 고칠 때는 [CareCode_Interface 문서](https://github.com/CareCode-Repo/CareCode_Interface/tree/main/docs)의 +실측값을 근거로 삼으세요. ## 스택 diff --git a/next.config.ts b/next.config.ts index 692e351..4e5d090 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,14 @@ import type { NextConfig } from 'next' +/* + * GitHub Pages 프로젝트 페이지는 /<저장소명> 아래에 붙는다. basePath 를 주지 않으면 + * CSS·JS 를 루트에서 찾아 전부 404 가 난다. 로컬 개발에서는 루트로 서비스하므로 + * 값을 비워 두고, 배포 워크플로에서만 넣는다. + * + * 커스텀 도메인을 붙이면 이 변수를 비우면 된다. + */ +const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '' + const nextConfig: NextConfig = { /* * 소개 사이트는 서버가 할 일이 없다. 정적 파일로 뽑아 어디든 올릴 수 있게 한다. @@ -8,6 +17,7 @@ const nextConfig: NextConfig = { output: 'export', images: { unoptimized: true }, trailingSlash: true, + basePath, } export default nextConfig diff --git a/package-lock.json b/package-lock.json index fa86ba6..963a2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,15 +15,18 @@ "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.39.5", "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9.39.5", "eslint-config-next": "^15.3.5", + "puppeteer": "^25.5.0", + "serve": "^14.2.6", "tailwindcss": "^4.1.11", - "typescript": "^5" + "typescript": "^5", + "wait-on": "^9.1.0" } }, "node_modules/@alloc/quick-lru": { @@ -214,24 +217,16 @@ } }, "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } } }, "node_modules/@eslint/object-schema": { @@ -258,6 +253,60 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.7.tgz", + "integrity": "sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1114,6 +1163,35 @@ "node": ">=12.4.0" } }, + "node_modules/@puppeteer/browsers": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.1.0.tgz", + "integrity": "sha512-RDLpio3fH/qrj5k4DVY6eyiN8tCS0Zovd/6jW//n605oeqkWcUjn+3k+9ZtZBnbwMpsu0F7xDIiKXvVmG5c5Bw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1128,6 +1206,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2124,6 +2209,13 @@ "win32" ] }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -2147,6 +2239,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2164,6 +2269,74 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2180,6 +2353,34 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2374,6 +2575,13 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2400,6 +2608,19 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2417,6 +2638,42 @@ "dev": true, "license": "MIT" }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -2452,6 +2709,16 @@ "node": ">=10.16.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2512,6 +2779,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001809", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", @@ -2549,90 +2829,297 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "chalk": "^4.1.2" }, "engines": { - "node": ">=7.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/chromium-bidi": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "mitt": "^3.0.1", + "zod": "^3.24.1" }, "engines": { - "node": ">= 8" + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2683,6 +3170,16 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2726,6 +3223,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2736,6 +3243,13 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1653615", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", + "integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2764,6 +3278,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2984,6 +3505,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3340,19 +3871,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3417,6 +3935,30 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3468,6 +4010,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3542,6 +4101,27 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3558,6 +4138,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3612,6 +4232,29 @@ "node": ">= 0.4" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3651,6 +4294,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -3826,6 +4482,30 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3863,6 +4543,13 @@ "node": ">=0.8.19" } }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4023,6 +4710,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-document.all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", @@ -4065,6 +4768,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4151,6 +4864,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4199,6 +4925,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -4296,6 +5035,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4338,6 +5090,25 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz", + "integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4722,6 +5493,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4738,6 +5522,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4778,6 +5569,13 @@ "node": ">= 0.4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4802,6 +5600,49 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4825,6 +5666,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", + "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4873,6 +5731,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "15.3.5", "resolved": "https://registry.npmjs.org/next/-/next-15.3.5.tgz", @@ -4985,6 +5853,19 @@ "semver": "bin/semver.js" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5108,6 +5989,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5200,6 +6107,13 @@ "node": ">=8" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5217,6 +6131,13 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5297,6 +6218,16 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5304,7 +6235,47 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.5.0.tgz", + "integrity": "sha512-qpp73xblxNr+bF0nSXTodM3v+zcK5IPo/GkjLsdUqRf/qpLJp/1KxBUbstoMMnwnPw9xD6OMei8kmYf6CLWfGw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.1.0", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1653615", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.5.0", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.5.0.tgz", + "integrity": "sha512-XPNT0dQJtphqQ4I29zxlG4IIPbg1iEHAQKWuQgtMJGXjACV77pZSmJvDi51IIIfd+DTKICcopJwUx4upVQ4XbA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.1.0", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1653615", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.1" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/queue-microtask": { @@ -5328,6 +6299,42 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -5400,6 +6407,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5479,6 +6520,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -5499,6 +6550,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5553,6 +6625,95 @@ "node": ">=10" } }, + "node_modules/serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/serve/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5746,6 +6907,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5784,6 +6952,24 @@ "node": ">=10.0.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -5898,6 +7084,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5908,6 +7110,16 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6110,6 +7322,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -6188,6 +7413,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6266,6 +7498,17 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6276,6 +7519,43 @@ "punycode": "^2.1.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wait-on": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.1.0.tgz", + "integrity": "sha512-PymrLXHLBM1Ju/Xspb2ADUhbPSMvbnuNvy/mN2hWtpbJ3da0h3Ky1LqwKPG5QSVR57liyO0iUpfipYl/s5qNvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.18.1", + "joi": "^18.2.3", + "lodash": "^4.18.1", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6381,6 +7661,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6391,6 +7687,114 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6403,6 +7807,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 78b78fe..7ee0f2c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "verify": "node scripts/verify-pages.mjs" }, "dependencies": { "next": "15.3.5", @@ -18,14 +19,17 @@ "devDependencies": { "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.39.5", "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9.39.5", "eslint-config-next": "^15.3.5", + "puppeteer": "^25.5.0", + "serve": "^14.2.6", "tailwindcss": "^4.1.11", - "typescript": "^5" + "typescript": "^5", + "wait-on": "^9.1.0" } } diff --git a/scripts/verify-pages.mjs b/scripts/verify-pages.mjs new file mode 100644 index 0000000..1e87faa --- /dev/null +++ b/scripts/verify-pages.mjs @@ -0,0 +1,121 @@ +/** + * 빌드된 사이트를 실제 브라우저로 열어 검사한다. + * + * 여기서 보는 것은 전부 "손으로 확인하다 실제로 걸렸던 것" 이다. + * + * - 가로 오버플로 — /data 의 표가 그리드 칸을 밀어 페이지가 옆으로 넘쳤다. + * (그리드 자식의 min-width: auto 때문. min-w-0 이 없으면 재발한다.) + * - 제목 구조·라벨 — 스크린리더로 읽히지 않는 표와 링크가 생기기 쉽다. + * + * 사용법: + * node scripts/verify-pages.mjs http://localhost:3000 + * + * CHROME_PATH 로 브라우저를 지정할 수 있다. 없으면 puppeteer 가 받아 둔 것을 쓴다. + */ +import puppeteer from 'puppeteer' + +const BASE = (process.argv[2] ?? 'http://localhost:3000').replace(/\/$/, '') +const PATHS = ['/', '/benefits/', '/facilities/', '/data/', '/trust/'] +/** 휴대폰 · 태블릿 · 데스크톱. 레이아웃이 갈리는 지점마다 본다. */ +const WIDTHS = [390, 768, 1440] + +const failures = [] + +const browser = await puppeteer.launch({ + headless: true, + executablePath: process.env.CHROME_PATH || undefined, + args: ['--no-sandbox', '--disable-dev-shm-usage'], +}) + +for (const width of WIDTHS) { + for (const path of PATHS) { + const page = await browser.newPage() + await page.setViewport({ width, height: 900 }) + + const url = BASE + path + const response = await page.goto(url, { waitUntil: 'networkidle0', timeout: 60_000 }) + + // 같은 페이지를 폭만 바꿔 여러 번 열기 때문에 두 번째부터는 304 로 온다. + // ok() 는 304 를 실패로 보므로 상태 코드를 직접 본다. + const status = response?.status() ?? 0 + if (status !== 200 && status !== 304) { + failures.push(`${path} @${width} — 응답 ${status || '없음'}`) + await page.close() + continue + } + + const result = await page.evaluate(() => { + const root = document.documentElement + const problems = [] + + // 가로 스크롤이 생기면 모바일에서 본문이 잘린다. + if (root.scrollWidth > root.clientWidth + 1) { + const culprits = [] + for (const el of document.querySelectorAll('body *')) { + const box = el.getBoundingClientRect() + if (box.width > 0 && box.right > root.clientWidth + 1) { + culprits.push(`${el.tagName.toLowerCase()}.${String(el.className).slice(0, 50)}`) + } + } + problems.push(`가로 오버플로 ${root.scrollWidth}px > ${root.clientWidth}px (${culprits[0] ?? '원인 불명'})`) + } + + return problems + }) + + result.forEach((p) => failures.push(`${path} @${width} — ${p}`)) + await page.close() + } +} + +// 접근성은 폭과 무관하므로 한 번만 본다. +for (const path of PATHS) { + const page = await browser.newPage() + await page.setViewport({ width: 1280, height: 900 }) + await page.goto(BASE + path, { waitUntil: 'networkidle0', timeout: 60_000 }) + + const problems = await page.evaluate(() => { + const found = [] + + const h1s = document.querySelectorAll('h1') + if (h1s.length !== 1) found.push(`h1 이 ${h1s.length}개 (1개여야 함)`) + + const levels = [...document.querySelectorAll('h1,h2,h3,h4')].map((h) => Number(h.tagName[1])) + for (let i = 1; i < levels.length; i += 1) { + if (levels[i] - levels[i - 1] > 1) found.push(`제목 레벨 건너뜀 h${levels[i - 1]}→h${levels[i]}`) + } + + document.querySelectorAll('a').forEach((a) => { + if (!a.textContent.trim() && !a.getAttribute('aria-label')) found.push('이름 없는 링크') + }) + document.querySelectorAll('button').forEach((b) => { + if (!b.textContent.trim() && !b.getAttribute('aria-label')) found.push('이름 없는 버튼') + }) + document.querySelectorAll('input').forEach((input) => { + const labelled = input.id && document.querySelector(`label[for="${input.id}"]`) + if (!labelled && !input.getAttribute('aria-label')) found.push(`라벨 없는 입력 (${input.type})`) + }) + document.querySelectorAll('img').forEach((img) => { + if (img.alt === null) found.push('alt 없는 이미지') + }) + // 표는 캡션이 없으면 스크린리더가 무슨 표인지 알려 주지 못한다. + document.querySelectorAll('table').forEach((t) => { + if (!t.querySelector('caption')) found.push('caption 없는 표') + }) + + return found + }) + + problems.forEach((p) => failures.push(`${path} — ${p}`)) + await page.close() +} + +await browser.close() + +if (failures.length > 0) { + console.error(`검증 실패 ${failures.length}건`) + failures.forEach((f) => console.error(` ✗ ${f}`)) + process.exit(1) +} + +console.log(`검증 통과 — ${PATHS.length}개 페이지 × ${WIDTHS.length}개 폭, 접근성 포함`) diff --git a/src/app/benefits/page.tsx b/src/app/benefits/page.tsx new file mode 100644 index 0000000..56ceff4 --- /dev/null +++ b/src/app/benefits/page.tsx @@ -0,0 +1,162 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { ReactNode } from 'react' +import { Figure, PageHead, Section } from '@/components/Page' + +export const metadata: Metadata = { + title: '지원금', + description: + '놓친 지원금 찾기, 거주지별 비교, 실수령액 제보로 확정하는 금액. 추정치는 추정치라고 표기합니다.', +} + +/** 자격 판정 결과. 소득 미입력을 탈락으로 처리하지 않는 것이 이 서비스의 판단이다. */ +const VERDICTS = [ + { + tone: 'var(--seal-deep)', + head: '대상입니다', + body: '자녀 수와 소득 조건을 모두 만족합니다. 예상 총액에 넣습니다.', + }, + { + tone: 'var(--past)', + head: '판단 보류', + body: '소득을 입력하지 않으셨습니다. 탈락으로 처리하면 받을 수 있었던 돈이 목록에서 통째로 사라지므로, 보류로 두고 함께 보여 드립니다.', + }, + { + tone: 'var(--stamp)', + head: '대상 아닙니다', + body: '자녀 수가 모자라거나 소득이 기준을 넘습니다. 왜 아닌지 함께 적습니다.', + }, +] + +export default function BenefitsPage(): ReactNode { + return ( + <> + 얼마를 받을 수 있는지, 왜 그런지} + lead={ + <> + 목록을 보여 주는 것만으로는 부족합니다. 이 가정이 실제로 얼마를 받을 수 있는지 계산하고, + 그 근거가 된 정책을 함께 보여 드립니다. + + } + /> + +
+ +
+ +
+
+

+ 처음 만들었을 때 한 가정의 예상 총액이{' '} + 295,068,000원으로 나왔습니다. 명백히 틀린 값이라 + 원인을 찾았습니다. +

+
+ +
+
+

원인 하나

+

+ 자녀 수와 소득 조건을 보지 않고 지역에 있는 정책을 전부 더하고 있었습니다. 다자녀 전용 + 지원금이 외동 가정에도 합산됐습니다. +

+
+
+

원인 둘

+

+ 대상 연령을 지급 기간으로 착각했습니다. 월 250만 원짜리 지원금에 대상 + 상한 60개월을 곱해 1억 5천만 원이 한 사람의 예상액에 들어갔습니다. +

+
+
+ +
+ + 295,068,000원 + + + + 80,568,000원 + +
+ +

+ 지급 기간을 별도 항목으로 떼어 내고 자격 판정을 넣은 결과입니다. 융자는 갚아야 하는 돈이라 + 총액에서 빼고 따로 안내합니다. 같은 목적이라 동시에 받을 수 없는 지원금은 가장 큰 것 + 하나만 셉니다. +

+
+ +
+
+

+ 지원금 설명은 자유 문장으로 옵니다.{' '} + “국공립 100,000원, 사립 280,000원”,{' '} + “융자(연 1.5%)” 같은 식입니다. + 기계로 뽑다 실패한 정책을 버리면 목록에서 사라져 존재 자체를 모르게 됩니다. +

+

+ 그래서 버리지 않고 미상으로 남깁니다. 대신 실제로 + 받아 보신 분께 금액을 묻고, 같은 값이 세 건 모이면 그때 확정합니다. 관리자가 직접 확인한 + 정책은 지역별 검증 비율과 함께 표시합니다. +

+
+ +
+
+
+
+
+
+ +
+
+

+ 같은 조건이어도 지자체마다 지원이 다릅니다. 다른 지역에 살면 얼마를 더 받는지 계산하고, + 차액의 근거가 된 정책 목록을 함께 드립니다. +

+

+ 숫자만 주면 믿을 이유가 없습니다. 어떤 정책 때문에 차이가 나는지 보이면 직접 확인하실 수 + 있습니다. +

+

+ + 이 데이터가 어디서 오는지 보기 + +

+
+
+ + ) +} diff --git a/src/app/data/page.tsx b/src/app/data/page.tsx new file mode 100644 index 0000000..c602d54 --- /dev/null +++ b/src/app/data/page.tsx @@ -0,0 +1,195 @@ +import type { Metadata } from 'next' +import { ReactNode } from 'react' +import { PageHead, Section, Shell } from '@/components/Page' +import { MEASURED_LABEL, MEASURED_ON, SOURCES, SYNC_SCHEDULE, formatCount } from '@/content/figures' + +export const metadata: Metadata = { + title: '데이터', + description: + '어디서 받아 오는지, 무엇을 알 수 있고 무엇을 알 수 없는지. 비어 있는 지역과 못 채운 값을 그대로 적었습니다.', +} + +/** 이 표가 이 페이지의 이유다. 마케팅 페이지가 보통 쓰지 않는 오른쪽 칸. */ +const LEDGER = [ + { + known: '시설 전체의 정원과 현원', + unknown: '반별 정원', + why: '공공데이터가 시설 단위로만 줍니다. 0세반이 찼는데 5세반에 자리가 난 것일 수 있어, 알림에 그대로 적습니다.', + }, + { + known: '정책의 대상 연령·지역·소득 기준', + unknown: '일부 정책의 지원 금액', + why: '금액이 숫자가 아니라 설명 문장 안에 섞여 옵니다. 기계로 뽑다 실패한 건 미상으로 두고, 받아 보신 분들의 제보로 채웁니다.', + }, + { + known: '관측된 정원 변동', + unknown: '대기 순번이 도는 시점', + why: '정부가 공개하지 않습니다. 부모들이 남긴 입소 기록이 세 건 이상 모인 시설만 통계를 냅니다.', + }, + { + known: '전국 시군구 200여 곳의 시설', + unknown: '광주와 전남의 시설', + why: '두 정부 API 모두 이 두 지역에서 빈 결과를 돌려줍니다. 우리 쪽 문제가 아니라 확인된 사실이라 적어 둡니다.', + }, +] + +export default function DataPage(): ReactNode { + return ( + <> + 아는 것과 모르는 것} + lead={ + <> + 이 서비스가 다루는 정보는 전부 정부가 공개한 것입니다. 그래서 무엇을 알 수 있는지는 + 정부가 무엇을 주는지에 달려 있고, 주지 않는 것은 저희도 모릅니다. 어느 쪽인지 구분해 + 적습니다. + + } + /> + +
+
+ + + + + + + + + + + + {SOURCES.map((s) => ( + + + + + + + ))} + +
공공데이터 출처별 수집 현황 ({MEASURED_LABEL})
+ 출처 + + 대상 + + 수집량 · {MEASURED_ON} + + 형식 +
+ {s.name} + + {s.gets} + + {formatCount(s.count)} + {s.unit} + + {s.format} +
+
+ +

+ 네 곳 모두 응답 형식과 지역 코드 체계가 다릅니다. 시군구를 하나씩 돌면서 모으고, 한 곳이 + 실패해도 나머지는 계속 받습니다. 수집이 아예 멈추거나 호출 한도를 넘기면 사람에게 + 알립니다. +

+ + {/* 이 사이트는 근거 없는 숫자를 쓰지 않겠다고 말한다. 그러면 언제 잰 값인지도 말해야 한다. */} +

+ 위 수집량은 {MEASURED_LABEL} 실측값입니다. 매주 새로 받아 오므로 지금 수치와 다를 수 + 있습니다. +

+
+ + {/* 이 페이지의 위험 감수 지점. 오른쪽 칸을 쓰는 서비스는 드물다. */} +
+ +

대조

+

+ 무엇을 모르는지 적어 두는 이유 +

+

+ 어린이집 자리와 지원금은 부모가 실제 결정을 내리는 정보입니다. 근거 없는 숫자를 + 그럴듯하게 보여 주면, 그걸 믿고 다른 선택지를 포기한 사람이 손해를 봅니다. 모른다고 쓰는 + 편이 낫습니다. +

+ +
    + {LEDGER.map((row) => ( +
  • +
    +
    +

    + 아는 것 +

    +

    {row.known}

    +
    +
    +

    + 모르는 것 +

    +

    {row.unknown}

    +

    + {row.why} +

    +
    +
    +
  • + ))} +
+
+
+ +
+
+ + + + {SYNC_SCHEDULE.map(({ what, when }) => ( + + + + + ))} + +
데이터 갱신 주기
+ {what} + + {when} +
+
+ +

+ 알림은 수집이 끝난 뒤에 나갑니다. 그날 들어온 데이터를 근거로 보내야 하기 때문입니다. +

+
+ + ) +} diff --git a/src/app/facilities/page.tsx b/src/app/facilities/page.tsx new file mode 100644 index 0000000..637d2ed --- /dev/null +++ b/src/app/facilities/page.tsx @@ -0,0 +1,112 @@ +import type { Metadata } from 'next' +import { ReactNode } from 'react' +import { Figure, PageHead, Section } from '@/components/Page' +import VacancyNotice from '@/components/VacancyNotice' + +export const metadata: Metadata = { + title: '어린이집', + description: + '대기 걸어 둔 시설에 자리가 늘면 알립니다. 정원 관측을 쌓아 입소 시점을 추정하되, 근거가 없으면 확률을 만들지 않습니다.', +} + +/** 알리지 않는 경우. 이 표가 알림 설계의 핵심이라 앞에 세운다. */ +const SILENT_CASES = [ + { when: '관측이 한 번뿐일 때', why: '늘었는지 줄었는지 알 수 없습니다' }, + { when: '빈자리가 그대로일 때', why: '이미 알고 계십니다' }, + { when: '빈자리가 줄었을 때', why: '알릴 내용이 아닙니다' }, + { when: '최근 2주 안에 보냈을 때', why: '같은 자리를 두 번 알리면 믿지 않게 됩니다' }, +] + +export default function FacilitiesPage(): ReactNode { + return ( + <> + 자리가 났다는 걸 아무도 알려 주지 않습니다} + lead={ + <> + 대기를 걸어 두면 그다음은 기다리는 일뿐입니다. 시설에 전화해도 “기다려 보세요”가 + 전부입니다. 정부 데이터는 지금 이 순간의 정원만 알려 줄 뿐 과거를 주지 않아, 자리가 잘 + 나는 곳인지도 알 수 없습니다. + + } + /> + +
+
+

+ 시설 정보는 최신값만 남습니다. 덮어쓰면 추이가 사라집니다. 그래서 동기화할 때마다 그날의 + 정원과 현원을 날짜별로 따로 적어 둡니다. 이 이력이 나머지 세 기능의 재료가 됩니다. +

+
+ +
+
+
+
+
+
+ +
+ + +
+

알리지 않는 경우

+
    + {SILENT_CASES.map((c) => ( +
  • + {c.when} + + {c.why} + +
  • + ))} +
+

+ 알림은 다시 앱을 열 유일한 이유이자, 성가시면 앱을 지우게 되는 이유이기도 합니다. 그래서 + 언제 보낼지보다 언제 보내지 않을지를 먼저 정했습니다. +

+
+
+ +
+
+

+ 정원 관측은 “자리가 났는가”만 알려 줍니다. 대기 순번이 언제 도는지는 정부 데이터에 + 없습니다. 겪어 본 부모만 압니다. +

+

+ 그래서 대기 등록과 입소·포기 시점을 직접 기록하실 수 있게 했습니다. 기록이 세 건 + 미만이면 평균이 우연에 좌우되므로, 그때는 숫자 대신 왜 낼 수 없는지를 알려 드립니다. +

+
+ +
+ “입소한 7명의 실제 기록 기준입니다. 절반이 5개월 안에 입소했습니다. 현재 12명이 대기 + 중으로 등록해 두었습니다.” +
+
+ + ) +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..ff75c14 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,106 @@ +import type { Metadata, Viewport } from 'next' +import { IBM_Plex_Mono } from 'next/font/google' +import Link from 'next/link' +import { ReactNode } from 'react' +import SiteNav from '@/components/SiteNav' +import { SITE_DESCRIPTION, SITE_NAME, SITE_TAGLINE, SITE_URL } from '@/content/site' +import { Shell } from '@/components/Page' +import '@/styles/globals.css' + +/** 금액·개월·수치용. 자릿수가 흔들리지 않아야 표와 축이 읽힌다. */ +const plexMono = IBM_Plex_Mono({ + weight: ['400', '500', '600'], + subsets: ['latin'], + display: 'swap', + variable: '--font-figure', +}) + +export const metadata: Metadata = { + // sitemap·OG 가 절대 URL 을 요구한다. 페이지마다 다시 쓰지 않도록 여기서 기준을 잡는다. + metadataBase: new URL(SITE_URL), + title: { + default: `${SITE_NAME} — ${SITE_TAGLINE}`, + template: `%s · ${SITE_NAME}`, + }, + description: SITE_DESCRIPTION, + alternates: { canonical: '/' }, + openGraph: { + siteName: SITE_NAME, + title: `${SITE_NAME} — ${SITE_TAGLINE}`, + description: SITE_DESCRIPTION, + url: SITE_URL, + locale: 'ko_KR', + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: `${SITE_NAME} — ${SITE_TAGLINE}`, + description: SITE_DESCRIPTION, + }, +} + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +} + +export default function RootLayout({ children }: { children: ReactNode }): ReactNode { + return ( + + + + 본문으로 건너뛰기 + + + +
{children}
+ +
+ +
+
+

맘편한

+

+ 이 사이트에 적힌 금액과 예측은 참고 자료입니다. 실제 수령액과 입소 여부는 + 지자체·시설 안내를 따릅니다. +

+
+ + +
+ +

© 2026 CARECODE

+
+
+ + + ) +} diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx new file mode 100644 index 0000000..434a302 --- /dev/null +++ b/src/app/opengraph-image.tsx @@ -0,0 +1,115 @@ +import { ImageResponse } from 'next/og' +import { SITE_NAME, SITE_TAGLINE } from '@/content/site' + +/** + * 공유 썸네일. + * + * 카카오톡·슬랙에 링크를 붙였을 때 뜨는 그림이다. 사이트를 처음 여는 사람이 보는 첫 화면과 + * 같은 것을 보여 줘야 해서, 모눈 배경과 월령 축을 그대로 옮겼다. + * + * 정적 출력이라 빌드 시점에 한 번 그려진다. + */ +/** 정적 출력에서는 이미지 라우트도 빌드 시점에 한 번만 그린다고 명시해야 한다. */ +export const dynamic = 'force-static' + +export const size = { width: 1200, height: 630 } +export const contentType = 'image/png' +export const alt = `${SITE_NAME} — ${SITE_TAGLINE}` + +/** 홈 히어로와 같은 27개월 기준. 부모급여는 지났고 나머지는 유효하다. */ +const MONTHS = 27 +const AXIS_MAX = 72 +const BANDS = [ + { from: 0, to: 23, tone: '#C2453D' }, + { from: 0, to: 71, tone: '#36AA1C' }, + { from: 24, to: 71, tone: '#36AA1C' }, + { from: 0, to: 11, tone: '#A8B2A5' }, +] + +export default function Image() { + return new ImageResponse( + ( +
+
+
+ CARECODE · {SITE_NAME} +
+
+ 받을 수 있었는데 + 몰라서 못 받은 돈 +
+
+ + {/* 월령 축. 사이트의 시그니처를 그대로 축약했다. */} +
+ {BANDS.map((band, i) => ( +
+
+
+
+ ))} + +
+ 0 + 12 + 24 + 36 + 48 + 60 + 72개월 +
+ +
+{MONTHS}개월 아이는 지금 3건을 받을 수 있고, 1건은 아직 소급 신청할 수 있습니다 +
+
+
+ ), + size, + ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..41ea9f2 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,157 @@ +import Link from 'next/link' +import { ReactNode } from 'react' +import MonthTimeline from '@/components/MonthTimeline' +import { MEASURED_LABEL, SOURCES, formatCount } from '@/content/figures' +import { Figure, Section, Shell } from '@/components/Page' + +export default function Home(): ReactNode { + return ( + <> + {/* 히어로. 문구로 설득하는 대신 축 위에서 직접 보게 한다. */} +
+ +
+
+

육아 지원 안내

+

+ 받을 수 있었는데 +
+ 몰라서 못 받은 돈 +

+

+ 육아 지원금은 중앙정부와 지자체가 따로 운영합니다. 대상인지 알기 어렵고, 신청 기한을 + 넘기면 조용히 사라집니다. 옆의 축을 옮겨 보면 아이 개월 수마다 무엇이 열리고 + 닫히는지 한눈에 보입니다. +

+ +
+ + 지원금 찾는 방법 + + + 데이터 출처 보기 + +
+
+ +
+ +
+
+
+
+ +
+
+ {[ + { + head: '놓친 지원금', + body: '아이가 지나온 개월 구간을 거슬러 올라가 대상이었던 지원금을 찾습니다. 소급 신청이 아직 가능한 것과 이미 지난 것을 나눠서 보여 줍니다.', + foot: '이미 지난 것도 보여 주는 이유는, 둘째를 준비하는 부모에게는 그게 중요한 정보이기 때문입니다.', + }, + { + head: '어린이집 빈자리', + body: '대기 걸어 둔 시설의 정원을 매주 관측해 자리가 늘어난 순간에만 알립니다. 계속 자리가 있는 곳은 이미 알고 계실 테니 알리지 않습니다.', + foot: '어느 반에 자리가 났는지는 공공데이터가 주지 않아, 알림에 그 한계를 적어 보냅니다.', + }, + { + head: '신청 마감', + body: '조건에 맞는 지원금의 신청 기한이 일주일 남았을 때, 그리고 하루 남았을 때 알립니다.', + foot: '놓친 뒤에 알려 주는 것보다 놓치기 전에 막는 편이 낫습니다.', + }, + ].map((card) => ( +
+

{card.head}

+

+ {card.body} +

+

+ {card.foot} +

+
+ ))} +
+
+ +
+
+ {SOURCES.map((source) => ( +
+ ))} +
+

+ 네 곳 모두 응답 형식과 지역 코드 체계가 다릅니다. 시군구 200여 곳을 하나씩 돌면서 모으고, + 한 곳이 실패해도 나머지는 계속 받습니다. 지금 비어 있는 지역과 아직 못 채운 값은{' '} + + 데이터 페이지 + + 에 그대로 적어 두었습니다. +

+
+ + {/* 신뢰 구획. 이 서비스가 다른 곳과 갈리는 지점이라 별도로 세운다. */} +
+
+

+ 지원금 금액은 정부가 숫자로 주지 않고 설명 문장 안에 섞여 옵니다. 그걸 기계로 뽑다 보면 + 틀립니다. 틀린 금액을 확정치처럼 보여 주면 그건 신뢰 문제를 넘어 분쟁이 됩니다. +

+

+ 그래서 확실하지 않은 값은 미상으로 두고, 표본이 + 부족한 통계는 숫자 대신 이유를 돌려 드립니다. 받아 본 분들의 실수령액 제보가 세 건 + 모이면 그때 금액을 확정합니다. +

+ +
+ + 관측 기반 + + + 추정치 표기 + + + 출처 공개 + +
+
+
+ + ) +} diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..9c3e6e5 --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,11 @@ +import type { MetadataRoute } from 'next' +import { SITE_URL } from '@/content/site' + +export const dynamic = 'force-static' + +export default function robots(): MetadataRoute.Robots { + return { + rules: { userAgent: '*', allow: '/' }, + sitemap: `${SITE_URL}/sitemap.xml`, + } +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..a3a26d2 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,18 @@ +import type { MetadataRoute } from 'next' +import { SITE_URL } from '@/content/site' + +/** + * 정적 출력에서도 빌드 시점에 sitemap.xml 로 떨어진다. + * 페이지를 추가하면 여기에도 넣어야 검색엔진이 찾아간다. + */ +export const dynamic = 'force-static' + +const PATHS = ['', '/benefits', '/facilities', '/data', '/trust'] + +export default function sitemap(): MetadataRoute.Sitemap { + return PATHS.map((path) => ({ + url: `${SITE_URL}${path}`, + changeFrequency: 'monthly' as const, + priority: path === '' ? 1 : 0.7, + })) +} diff --git a/src/app/trust/page.tsx b/src/app/trust/page.tsx new file mode 100644 index 0000000..d921138 --- /dev/null +++ b/src/app/trust/page.tsx @@ -0,0 +1,162 @@ +import type { Metadata } from 'next' +import { ReactNode } from 'react' +import { PageHead, Section } from '@/components/Page' + +export const metadata: Metadata = { + title: '개인정보', + description: '무엇을 받고 왜 받는지, 건강 정보를 어떻게 따로 다루는지, 탈퇴하면 어떻게 되는지.', +} + +const COLLECTED = [ + { + group: '계정', + items: '이메일, 이름, 전화번호', + why: '로그인과 알림 발송에 씁니다.', + sensitive: false, + }, + { + group: '거주지', + items: '주소, 좌표', + why: '지자체 지원금 대상 판정과 반경 검색에 씁니다. 좌표가 없으면 가까운 시설을 찾을 수 없습니다.', + sensitive: false, + }, + { + group: '가구', + items: '소득분위, 가구원 수', + why: '소득 기준이 있는 지원금의 대상 여부를 가립니다. 입력하지 않으셔도 목록에서 빼지 않습니다.', + sensitive: false, + }, + { + group: '자녀', + items: '이름, 생년월일, 성별, 특수보육 필요 여부', + why: '개월 수로 대상 지원금을 찾고, 연령에 맞는 시설을 고릅니다.', + sensitive: false, + }, + { + group: '건강 기록', + items: '키, 몸무게, 체온, 접종명, 증상, 진단, 처방', + why: '성장 기록과 접종 일정 관리에 씁니다. 민감정보라 별도 동의를 받고, 동의 전에는 저장도 조회도 되지 않습니다.', + sensitive: true, + }, +] + +export default function TrustPage(): ReactNode { + return ( + <> + 아이의 정보를 맡기는 일} + lead={ + <> + 이 서비스는 아이의 진단명과 처방까지 다룹니다. 법이 민감정보로 정한 항목이고, 일반 + 개인정보와 같은 동의로 처리할 수 없습니다. 무엇을 왜 받는지 항목별로 적습니다. + + } + /> + +
+
    + {COLLECTED.map((row) => ( +
  • +
    +
    +

    {row.group}

    + {row.sensitive && ( + + 민감정보 + + )} +
    +
    +

    {row.items}

    +

    + {row.why} +

    +
    +
    +
  • + ))} +
+ +

+ 목록에 없는 항목은 받지 않습니다. 쓰임이 없어진 항목은 지웁니다. +

+
+ +
+
+

+ 건강 기록은 별도 동의 없이는 저장도 조회도 되지 않습니다. 동의하지 않은 채 접근하면 + 화면이 그냥 막히는 게 아니라, 어떤 동의가 필요한지를 함께 알려 + 드립니다. 왜 막혔는지 모른 채 화면만 보게 되면 안 되기 때문입니다. +

+

+ 동의는 언제든 철회할 수 있고, 언제 무엇에 동의했는지 이력도 보실 수 있습니다. +

+
+
+ +
+
    + {[ + { + what: '내려받기', + how: '저장된 개인정보 전체를 한 번에 받아 보실 수 있습니다.', + }, + { + what: '동의 관리', + how: '항목별로 켜고 끌 수 있습니다. 동의 이력도 함께 남습니다.', + }, + { + what: '탈퇴', + how: '식별 정보를 지우고 계정을 닫습니다. 탈퇴한 계정으로는 로그인되지 않습니다. 다만 남기신 커뮤니티 글처럼 다른 분들의 기록과 얽힌 부분은 함께 사라지지 않습니다.', + }, + ].map((r) => ( +
  • + {r.what} + + {r.how} + +
  • + ))} +
+
+ +
+
+
    + {[ + '지원금 금액은 참고 자료이며 추정치입니다. 실제 수령액은 지자체 안내를 따릅니다.', + '입소 예측은 관측을 근거로 한 추정이며 입소를 보장하지 않습니다.', + '빈자리 알림은 시설 전체 기준이라 해당 반의 자리를 보장하지 않습니다.', + '성장 정보는 의학적 진단이 아닙니다.', + ].map((line) => ( +
  • + + {line} +
  • + ))} +
+

+ 잘못된 기대는 그대로 분쟁이 됩니다. 그래서 약관에도 같은 내용을 넣었습니다. +

+
+
+ + ) +} diff --git a/src/assets/fonts/PretendardVariable.woff2 b/src/assets/fonts/PretendardVariable.woff2 new file mode 100644 index 0000000..49c54b5 Binary files /dev/null and b/src/assets/fonts/PretendardVariable.woff2 differ diff --git a/src/components/MonthTimeline.tsx b/src/components/MonthTimeline.tsx new file mode 100644 index 0000000..34f1777 --- /dev/null +++ b/src/components/MonthTimeline.tsx @@ -0,0 +1,168 @@ +'use client' + +import { ReactNode, useMemo, useState } from 'react' +import { AXIS_MAX_MONTHS as AXIS_MAX, BENEFIT_BANDS, formatCount as won } from '@/content/figures' + +/** + * 월령 타임라인. + * + * 이 서비스의 핵심 발견은 "받을 수 있었는데 몰라서 못 받은 돈" 이다. 그걸 설명하는 대신 + * 축 위에서 보여 준다. 표시를 끌면 지나간 지원금이 회색으로 남는다. + * + * 밴드 구성은 실제 정책 데이터 모델(targetAgeMin/Max, benefitAmount, retroactiveMonths)을 + * 그대로 따른다. 금액을 모르는 정책이 실재하므로 '미상' 도 한 줄 넣었다. + * 값은 낡기 때문에 기준일과 함께 @/content/figures 에 모아 둔다. + */ +export default function MonthTimeline(): ReactNode { + const [months, setMonths] = useState(27) + + const rows = useMemo( + () => + BENEFIT_BANDS.map((band) => { + const active = months >= band.from && months <= band.to + const passed = months > band.to + const claimable = passed && months - band.to <= band.retroactive + return { ...band, active, passed, claimable } + }), + [months], + ) + + const activeCount = rows.filter((r) => r.active).length + const claimableCount = rows.filter((r) => r.claimable).length + const expiredCount = rows.filter((r) => r.passed && !r.claimable).length + + return ( +
+
+
+ +

+ {months} + + 개월 + +

+
+

+ 표시를 옮겨 보세요 +

+
+ + setMonths(Number(e.target.value))} + aria-valuetext={`${months}개월`} + className="mt-5 w-full accent-[var(--seal)]" + /> + + {/* 축 눈금. 12개월 단위로 끊어 읽는다. */} + + +
    + {rows.map((row) => { + const left = (Math.min(row.from, AXIS_MAX) / AXIS_MAX) * 100 + const width = + ((Math.min(row.to, AXIS_MAX) - Math.min(row.from, AXIS_MAX) + 1) / AXIS_MAX) * 100 + + const tone = row.active ? 'var(--seal)' : row.claimable ? 'var(--stamp)' : 'var(--past)' + + return ( +
  • + + {row.name} + + +
    +
    + {/* 현재 위치 표시선 */} +
    +
    + + + {row.active ? ( + row.monthly === null ? ( + 금액 미상 + ) : ( + 월 {won(row.monthly)}원 + ) + ) : row.claimable ? ( + '아직 소급 신청 가능' + ) : row.passed ? ( + '기간이 지났습니다' + ) : ( + 아직 대상 아님 + )} + +
  • + ) + })} +
+ +

+ {months}개월 아이는 지금{' '} + {activeCount}건을 받을 수 있습니다. + {claimableCount > 0 && ( + <> + {' '} + {claimableCount}건은 기간이 지났지만 + 아직 소급 신청할 수 있고, + + )} + {expiredCount > 0 && ( + <> + {' '} + {expiredCount}건은 이미 신청 기회가 + 지났습니다. + + )} +

+ +

+ 전국 공통 정책 다섯 가지로만 그린 예시입니다. 실제로는 사는 지역의 지자체 정책이 더해지고, + 소득과 자녀 수에 따라 대상이 갈립니다. +

+
+ ) +} diff --git a/src/components/Page.tsx b/src/components/Page.tsx new file mode 100644 index 0000000..3dbbba8 --- /dev/null +++ b/src/components/Page.tsx @@ -0,0 +1,106 @@ +import { ReactNode } from 'react' + +/** 본문 폭. 표와 타임라인이 들어가야 해서 읽기 폭보다 넓게 잡는다. */ +export function Shell({ children }: { children: ReactNode }): ReactNode { + return
{children}
+} + +/** + * 페이지 머리. 수첩의 속표지에 해당한다. + * + * eyebrow 는 장식이 아니라 이 페이지가 수첩의 어느 항목인지 알려 준다. + */ +export function PageHead({ + eyebrow, + title, + lead, +}: { + eyebrow: string + title: ReactNode + lead: ReactNode +}): ReactNode { + return ( +
+ +

{eyebrow}

+

{title}

+

+ {lead} +

+
+
+ ) +} + +/** 구획. 좌측에 라벨, 우측에 내용 — 기록부의 항목 배치. */ +export function Section({ + label, + title, + children, +}: { + label: string + title?: ReactNode + children: ReactNode +}): ReactNode { + return ( +
+ +
+
+

{label}

+
+ {/* min-w-0 이 없으면 안쪽 표의 min-width 만큼 칸이 벌어져 페이지가 가로로 넘친다. */} +
+ {title &&

{title}

} + {children} +
+
+
+
+ ) +} + +/** 수치 한 칸. 라벨은 작게, 값은 크게, 단위는 값에 붙여 읽는다. */ +export function Figure({ + value, + unit, + label, + note, + muted = false, +}: { + value: string + unit?: string + label: string + note?: string + muted?: boolean +}): ReactNode { + return ( +
+

+ {value} + {unit && ( + + {unit} + + )} +

+

+ {label} +

+ {note && ( +

+ {note} +

+ )} +
+ ) +} diff --git a/src/components/SiteNav.tsx b/src/components/SiteNav.tsx new file mode 100644 index 0000000..62fb246 --- /dev/null +++ b/src/components/SiteNav.tsx @@ -0,0 +1,104 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { ReactNode, useState } from 'react' + +/** 수첩의 탭. 순서가 아니라 분류라서 번호를 붙이지 않는다. */ +const TABS = [ + { href: '/', label: '개요' }, + { href: '/benefits', label: '지원금' }, + { href: '/facilities', label: '어린이집' }, + { href: '/data', label: '데이터' }, + { href: '/trust', label: '개인정보' }, +] as const + +export default function IntroNav(): ReactNode { + const pathname = usePathname() + const [open, setOpen] = useState(false) + + const isActive = (href: string): boolean => + href === '/' ? pathname === '/' : pathname.startsWith(href) + + return ( +
+
+ + + 맘편한 + + CARECODE + + + + + +
+ + {open && ( + + )} +
+ ) +} diff --git a/src/components/VacancyNotice.tsx b/src/components/VacancyNotice.tsx new file mode 100644 index 0000000..355c42a --- /dev/null +++ b/src/components/VacancyNotice.tsx @@ -0,0 +1,54 @@ +import { ReactNode } from 'react' + +/** + * 실제로 발송되는 빈자리 알림. + * + * 마케팅 문구로 바꿔 쓰지 않고 서비스가 보내는 문장을 그대로 둔다. 특히 마지막 한 줄 — + * 공공데이터가 시설 전체 정원만 주므로 어느 반인지 알 수 없다는 고지 — 이 이 제품의 성격이다. + */ +export default function VacancyNotice(): ReactNode { + return ( +
+
+
+ 시설 알림 + + 09:30 + +
+ +

행복어린이집에 자리가 났습니다

+ +

+ 대기 등록해 두신 행복어린이집의 빈자리가 2자리 늘어 + 현재 3자리입니다. + + {' '} + (2026-08-06 관측 기준) + +

+ +

+ 시설 전체 기준이라 해당 반에 자리가 있는지는 시설에 확인해 보세요. +

+
+ +
+ 실제로 발송되는 문구입니다. 마지막 줄이 없으면 부모가 헛걸음합니다. +
+
+ ) +} diff --git a/src/content/figures.ts b/src/content/figures.ts new file mode 100644 index 0000000..d2df7c2 --- /dev/null +++ b/src/content/figures.ts @@ -0,0 +1,77 @@ +/** + * 이 사이트가 보여 주는 수치를 한 곳에 모은다. + * + * 백엔드는 매주 공공데이터를 새로 받아 오므로 여기 적힌 값은 시간이 지나면 반드시 낡는다. + * 페이지마다 흩어져 있으면 낡은 걸 아무도 모른 채 지나간다. + * + * 이 사이트는 "근거 없는 숫자는 쓰지 않는다" 를 내세운다. 그러면서 정작 소개 페이지의 숫자가 + * 언제 기준인지 밝히지 않으면 그 주장 자체가 무너진다. 그래서 값과 기준일을 함께 둔다. + * + * 갱신하려면: CareCode_Interface 에서 `./gradlew liveSyncCheck` 로 실측한 뒤 + * 이 파일의 값과 MEASURED_ON 을 함께 고친다. + */ + +/** 수집량을 실제로 측정한 날. 값만 고치고 이 날짜를 안 고치면 거짓말이 된다. */ +export const MEASURED_ON = '2026-08-06' + +/** 사람이 읽는 표기. "2026-08-06 기준" 처럼 쓴다. */ +export const MEASURED_LABEL = `${MEASURED_ON} 기준` + +export type Source = { + /** 데이터를 제공하는 기관 */ + name: string + /** 무엇을 받아 오는가 */ + gets: string + /** 수집한 개수 */ + count: number + /** 개수의 단위 */ + unit: '곳' | '건' + /** 응답 형식 */ + format: 'XML' | 'JSON' +} + +export const SOURCES: Source[] = [ + { name: '보육통합정보시스템', gets: '어린이집', count: 8_331, unit: '곳', format: 'XML' }, + { name: '유치원알리미', gets: '유치원', count: 7_052, unit: '곳', format: 'JSON' }, + { name: '건강보험심사평가원', gets: '소아청소년과', count: 4_292, unit: '곳', format: 'XML' }, + { name: '보조금24', gets: '정부지원 서비스', count: 60, unit: '건', format: 'JSON' }, +] + +export const formatCount = (n: number): string => n.toLocaleString('ko-KR') + +/** 데이터를 새로 받아 오는 주기. */ +export const SYNC_SCHEDULE: ReadonlyArray<{ what: string; when: string }> = [ + { what: '어린이집 · 유치원', when: '주 1회' }, + { what: '소아청소년과', when: '주 1회' }, + { what: '정부지원 서비스', when: '매일' }, + { what: '주소 좌표 보정', when: '매일' }, +] + +export type BenefitBand = { + name: string + /** 대상 연령 하한(개월) */ + from: number + /** 대상 연령 상한(개월) */ + to: number + /** 월 지급액(원). 공공데이터가 숫자를 주지 않는 정책은 null. */ + monthly: number | null + /** 대상 기간이 지난 뒤에도 소급 신청이 가능한 개월 수 */ + retroactive: number +} + +/** + * 월령 타임라인에 그리는 정책. + * + * 전국 공통 정책만 넣는다. 지자체 정책은 사는 곳마다 달라 예시로 그릴 수 없다. + * 첫만남이용권은 바우처라 월 지급액이 없어 미상으로 둔다. + */ +export const BENEFIT_BANDS: BenefitBand[] = [ + { name: '부모급여', from: 0, to: 23, monthly: 700_000, retroactive: 6 }, + { name: '아동수당', from: 0, to: 95, monthly: 100_000, retroactive: 0 }, + { name: '양육수당', from: 24, to: 86, monthly: 100_000, retroactive: 6 }, + { name: '첫만남이용권', from: 0, to: 11, monthly: null, retroactive: 12 }, + { name: '보육료 지원', from: 0, to: 71, monthly: 280_000, retroactive: 0 }, +] + +/** 타임라인 축의 오른쪽 끝(개월). 취학 전까지를 담는다. */ +export const AXIS_MAX_MONTHS = 72 diff --git a/src/content/site.ts b/src/content/site.ts new file mode 100644 index 0000000..812e7cd --- /dev/null +++ b/src/content/site.ts @@ -0,0 +1,13 @@ +/** + * 사이트 주소. sitemap·robots·OG 태그가 절대 URL 을 요구한다. + * + * 배포 워크플로가 NEXT_PUBLIC_SITE_URL 을 넣는다. 커스텀 도메인을 붙이면 그 값만 바꾸면 된다. + */ +export const SITE_URL = ( + process.env.NEXT_PUBLIC_SITE_URL ?? 'https://carecode-repo.github.io/CareCode_Landing' +).replace(/\/$/, '') + +export const SITE_NAME = '맘편한' +export const SITE_TAGLINE = '놓치는 육아 지원을 줄입니다' +export const SITE_DESCRIPTION = + '받을 수 있는 지원금, 자리가 난 어린이집, 마감이 다가온 신청. 정부 공공데이터를 모아 부모에게 필요한 형태로 알려 드립니다.' diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..b0dfc47 --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,151 @@ +@import 'tailwindcss'; + +/* + * 방향: 육아수첩 · 관공서 기록부. + * + * 파스텔 베이비 톤(이 분야의 클리셰)도, 핀테크식 무채색도 아니다. + * 정부가 나눠 주는 아기수첩 — 모눈 인쇄, 개월 수 탭, 도장, 표 — 을 기준으로 삼는다. + */ + +@font-face { + font-family: 'Pretendard'; + font-weight: 100 900; + font-display: swap; + src: url('../assets/fonts/PretendardVariable.woff2') format('woff2'); +} + +:root { + /* 재생지에 가까운 차가운 오프화이트. 흔한 크림색(#F4F1EA)을 피한다. */ + --paper: #f6f8f3; + --paper-deep: #eef2e9; + /* 도장 잉크에 가까운 진한 녹흑. 순수 검정은 인쇄물처럼 보이지 않는다. */ + --ink: #1c2620; + --ink-soft: #4a564d; + /* 유효·현재. 앱과 같은 브랜드 그린을 쓴다. */ + --seal: #36aa1c; + --seal-deep: #23791a; + /* 이미 지나간 것 */ + --past: #a8b2a5; + /* 마감·경고. 경보 빨강이 아니라 도장 빨강. */ + --stamp: #c2453d; + /* 모눈선 */ + --rule: #dce3d6; +} + +@theme inline { + --color-paper: var(--paper); + --color-ink: var(--ink); + --color-seal: var(--seal); + --font-sans: 'Pretendard', system-ui, sans-serif; +} + +html { + background-color: var(--paper); +} + +body { + background-color: var(--paper); + color: var(--ink); + font-family: 'Pretendard', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* 모눈 종이. 아주 옅게 깔려 배경이 종이라는 것만 알린다. */ +.paper-grid { + background-image: + linear-gradient(to right, var(--rule) 1px, transparent 1px), + linear-gradient(to bottom, var(--rule) 1px, transparent 1px); + background-size: 28px 28px; + background-position: -1px -1px; +} + +/* + * 표제. 굵게 외치는 대신 얇게 크게 놓는다. + * + * 이 서비스의 목소리는 "다 받으세요" 가 아니라 "이건 알고 이건 모릅니다" 라서, + * 700 짜리 고딕 헤드라인은 성격에 맞지 않는다. 한글 명조를 외부에서 받아 오는 방법도 있지만 + * 개인정보 고지를 두는 서비스가 방문자 IP 를 매번 외부로 보낼 수는 없어, 자체 호스팅한 + * Pretendard 의 굵기·크기·자간 대비로 목소리를 만든다. + */ +.display { + font-weight: 300; + letter-spacing: -0.035em; + line-height: 1.26; +} + +/* 금액·개월·수치. 자릿수가 흔들리지 않아야 표와 축이 읽힌다. */ +.figure { + font-family: var(--font-figure), ui-monospace, monospace; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; +} + +/* 수첩의 항목 라벨. 인쇄된 안내문처럼 작고 조금 벌려 놓는다. */ +.label { + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.08em; + color: var(--ink-soft); +} + +/* 값을 모를 때의 표기. 이 서비스에서 '미상'은 일급 상태다. */ +.unknown { + font-family: var(--font-figure), ui-monospace, monospace; + color: var(--past); + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 0.25em; +} + +/* 도장. 종이 위에 찍힌 것처럼 살짝 기울인다. */ +.stamp { + font-family: var(--font-figure), ui-monospace, monospace; + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.1em; + border: 1.5px solid currentColor; + border-radius: 2px; + padding: 0.2rem 0.45rem; + transform: rotate(-3deg); + display: inline-block; +} + +/* 앵커나 건너뛰기 링크로 이동할 때 제목이 상단 고정 메뉴에 가리지 않게 한다. */ +h1, +h2, +h3, +[id] { + scroll-margin-top: 5.5rem; +} + +:focus-visible { + outline: 2px solid var(--seal); + outline-offset: 3px; + border-radius: 2px; +} + +/* 스크롤 등장. 한 번만, 조용히. */ +@keyframes rise { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: none; + } +} + +.rise { + animation: rise 0.5s cubic-bezier(0.22, 0.8, 0.3, 1) both; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +}