diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml deleted file mode 100644 index db6b0392..00000000 --- a/.github/workflows/chromatic.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: 'Chromatic' - -on: push - -jobs: - chromatic: - name: Run Chromatic - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - # ⚠️ See your package manager's documentation for the correct command to install dependencies in a CI environment. - run: npm ci - - name: Run Chromatic - uses: chromaui/action@latest - with: - # ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml new file mode 100644 index 00000000..a3e7b15d --- /dev/null +++ b/.github/workflows/code-review.yml @@ -0,0 +1,31 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + id-token: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + 이 PR을 리뷰해줘. 아래 항목을 중심으로 분석하고 PR에 리뷰 코멘트를 남겨줘. + + ## 체크 항목 + - 코드 품질 및 가독성 + - FSD 레이어 규칙 준수 (shared → entities → features → widgets → pages → app) + - Tailwind 사용 (Emotion 신규 작성 금지) + - 버그 가능성 (타입 오류, 엣지 케이스 등) + - 웹 접근성 (aria 속성, 키보드 네비게이션) + - 보안 취약점 (XSS, 민감 정보 노출 등) + claude_args: "--max-turns 5" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..89e5bb14 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,204 @@ +# MOING Frontend - 리팩토링 가이드라인 + +## 프로젝트 개요 +여행 매칭 플랫폼. React → Next.js 긴급 마이그레이션 이후 체계적인 리팩토링 진행 중. +- **Framework**: Next.js 14 (App Router) + React 18 + TypeScript +- **현재 상태**: 448개 파일, ~40,460줄, 대부분 Client Component + +--- + +## 리팩토링 목표 +1. **Next.js 방식으로 개편** - Server Component 전략 (Option B: 하이브리드) +2. **FSD 폴더 구조** - Feature-Sliced Design 완전 적용 +3. **성능 최적화** - 테스트 기반 측정 후 개선 +4. **TDD + 에러 핸들링** - Vitest (단위/통합) + Playwright (E2E) +5. **Tailwind CSS 전환** - Emotion 제거 +6. **UX 개편** - 진행 중 별도 지시 + +--- + +## 확정된 기술 결정 + +| 항목 | AS-IS | TO-BE | +|------|-------|-------| +| 스타일링 | Emotion (CSS-in-JS) | Tailwind CSS | +| 폴더 구조 | 도메인 분리 구조 | FSD (Feature-Sliced Design) | +| 테스트 | 없음 | Vitest + Playwright | +| Server/Client | 전부 Client Component | 하이브리드 (Option B) | +| 타입 강화 | 후순위 (나중에) | - | +| 라이브러리 | Zustand / React Query / Axios | 유지 | + +--- + +## FSD 목표 구조 + +``` +src/ +├── app/ # Next.js App Router (라우팅만) +│ ├── layout.tsx +│ ├── (auth)/ +│ ├── trip/ +│ └── ... +├── pages/ # 페이지 조합 레이어 +├── widgets/ # 독립적 UI 블록 +├── features/ # 사용자 시나리오 단위 기능 +├── entities/ # 도메인 모델 (trip, user, comment...) +└── shared/ # 공통 재사용 요소 + ├── ui/ # 디자인 시스템 (버튼, 인풋, 모달...) + ├── api/ # axios 인스턴스, 공통 fetch + ├── hooks/ # 범용 훅 + ├── lib/ # 외부 라이브러리 래핑 + ├── constants/ # 전역 상수 + └── types/ # 전역 타입 +``` + +--- + +## 진행 단계 (Phase) + +### Phase 0: 기반 구축 ✅ +- [x] Vitest 설정 +- [x] Playwright 설정 +- [x] Tailwind 설치 (Emotion과 공존) +- [x] FSD 디렉토리 스캐폴딩 + +### Phase 1: shared 레이어 +- `components/designSystem/` → `shared/ui/` 이전 +- Tailwind 첫 마이그레이션 +- 각 컴포넌트 단위 테스트 작성 + +### Phase 2: entities 레이어 +- `model/` + `api/` → `entities/{domain}/` +- 도메인별 타입, API, 기본 모델 정의 + +### Phase 3: features 레이어 +- `hooks/` + `components/` → `features/{feature}/` +- 각 feature마다 TDD로 진행 + +### Phase 4: pages / widgets 레이어 +- `page/` → `pages/` + `widgets/` +- Server Component 전환 (하이브리드) +- 성능 테스트 → 최적화 + +--- + +## 협업 프로토콜 + +### 모든 작업은 아래 절차를 따른다 +1. **계획 제시** → 사용자 검수 및 승인 +2. **작업 실행** +3. **결과 보고** → 사용자 검수 및 승인 +4. 승인 없이 다음 단계로 넘어가지 않는다 + +### 문서화 규칙 +> **목적**: 블로그 포스팅 및 이력서 작성 재료로 활용. 단순 작업 기록이 아닌 **문제 → 원인 → 결정 → 결과** 서술 형식을 유지한다. + +- 각 Phase 완료 시 `docs/refactoring/phase-{N}.md` 작성 +- 기술 결정 사항은 `docs/decisions/` 에 ADR 형식으로 기록 +- 변경된 파일 목록과 이유를 항상 기록 + +#### Phase 문서 필수 포함 항목 +1. **배경 / 문제 정의** - 왜 이 작업이 필요했는가 (Before 상태, 수치 포함) +2. **선택지와 의사결정** - 어떤 옵션을 검토했고 왜 이것을 골랐는가 +3. **구현 과정** - 핵심 기술적 도전과 해결 방법 +4. **Before / After 비교** - 코드 예시 포함 +5. **결과 및 수치** - 파일 수, 라인 수, 테스트 커버리지, 성능 지표 등 +6. **트러블슈팅** - 겪은 문제와 해결 과정 (블로그 소재로 가장 중요) +7. **회고 / 배운 점** - 다음에 다르게 할 것, 인사이트 + +### 코딩 규칙 +- 새로 작성하는 코드는 반드시 Tailwind 사용 (Emotion 신규 작성 금지) +- 새로 작성하는 컴포넌트는 FSD 구조에 맞게 위치 +- 모든 새 기능은 테스트 먼저 작성 (TDD) +- console.log 사용 금지 (Sentry 또는 logger 사용) +- `any` 타입 신규 사용 금지 + +--- + +## 웹 접근성 (Web Accessibility) 가이드라인 + +> Phase 1.5 이후 모든 새 컴포넌트 및 수정 컴포넌트에 적용한다. + +### 기본 원칙 + +**컴포넌트 고정값 vs 컨텍스트 의존값** + +| 구분 | 적용 위치 | 예시 | +|------|-----------|------| +| **고정값** (디자인 시스템에서 처리) | 컴포넌트 내부 하드코딩 | `role="dialog"`, `aria-modal="true"`, `aria-expanded`, `aria-haspopup` | +| **컨텍스트 의존값** (호출부에서 주입) | prop으로 받기 | `aria-label`, `aria-describedby`, `aria-labelledby` | + +### 컴포넌트별 접근성 규칙 + +#### 버튼 / 인터랙티브 요소 +- `CloseButton`: `aria-label="닫기"` 기본값 (prop으로 override 허용) +- `RemoveButton`: `aria-label="삭제"` 기본값 +- 아이콘만 있는 버튼은 반드시 `aria-label` 또는 `` 포함 +- `type="button"` 명시 (form submit 방지) + +#### 폼 / 입력 요소 +- `StateInputField`: 에러 메시지와 `aria-describedby`로 연결 (ID 기반) + - 에러 메시지 `

`, 입력 `aria-describedby="{inputId}-error"` +- `CodeInput`: 각 셀에 `aria-label="n번째 숫자"` (1~6) +- `aria-invalid="true"` — 에러 상태 시 추가 +- `aria-required` — 필수 필드에 명시 + +#### 선택 / 드롭다운 +- `Select`: `role="combobox"`, `aria-expanded`, `aria-haspopup="listbox"` +- 옵션 목록: `role="listbox"`, 각 옵션: `role="option"`, `aria-selected` + +#### 모달 / 다이얼로그 +- `BaseModal`: `role="dialog"`, `aria-modal="true"`, `aria-labelledby` (제목 ID 연결) +- `BottomSheetModal`: 동일 +- **Focus Trap 필수**: 모달 열릴 때 포커스 진입, 닫힐 때 트리거 버튼으로 복귀 +- `Escape` 키로 모달 닫기 지원 + +#### 이미지 +- `RoundedImage`: Phase 1에서 `div + background-image` 사용 → Phase 1.5에서 `` + `alt` prop 전환 고려 + +### 키보드 네비게이션 체크리스트 +- [ ] `Tab` / `Shift+Tab` 으로 모든 인터랙티브 요소 접근 가능 +- [ ] `Enter` / `Space` 로 버튼 활성화 +- [ ] `Escape` 로 모달/드롭다운 닫기 +- [ ] focus outline 항상 표시 (`outline-none` 사용 금지, `focus-visible` 활용) +- [ ] 모달 내부 focus trap 적용 + +### 자동 접근성 테스트 (jest-axe) + +```typescript +// 모든 shared/ui 컴포넌트 테스트에 포함할 패턴 +import { axe, toHaveNoViolations } from 'jest-axe'; +expect.extend(toHaveNoViolations); + +it('접근성 위반이 없어야 한다', async () => { + const { container } = render(); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); +``` + +### Tailwind 접근성 유틸리티 +- `sr-only`: 시각적으로 숨기되 스크린리더에 읽힘 (아이콘 버튼 텍스트) +- `focus-visible:ring-2`: 키보드 포커스 표시 (마우스 클릭 시 ring 미표시) +- `not-sr-only`: sr-only 해제 + +### 참고 기준 +- WCAG 2.1 AA 준수 목표 +- ARIA Authoring Practices Guide (APG) 패턴 참조 + +--- + +## 문서 구조 + +``` +docs/ +├── refactoring/ +│ ├── phase-0.md +│ ├── phase-1.md +│ └── ... +├── decisions/ # Architecture Decision Records +│ ├── 001-tailwind.md +│ ├── 002-fsd.md +│ └── ... +└── progress.md # 전체 진행 현황 +``` diff --git a/docs/decisions/001-tailwind.md b/docs/decisions/001-tailwind.md new file mode 100644 index 00000000..50e92d49 --- /dev/null +++ b/docs/decisions/001-tailwind.md @@ -0,0 +1,28 @@ +# ADR 001: Emotion → Tailwind CSS 전환 + +## 상태 +확정 (2026-03-20) + +## 배경 +기존 프로젝트는 Emotion (CSS-in-JS)을 사용 중이었다. +Next.js App Router 환경에서 Emotion은 런타임에 CSS를 생성하기 때문에 +Server Component와 호환되지 않아 모든 컴포넌트가 Client Component로 강제된다. + +## 결정 +Emotion을 Tailwind CSS로 전환한다. + +## 근거 +- Tailwind는 빌드타임에 CSS를 생성 → Server Component와 완벽 호환 +- 번들 사이즈 감소 (런타임 CSS 생성 제거) +- Next.js 공식 권장 스타일링 방식 중 하나 +- 하이브리드 Server Component 전략(Option B) 실현을 위한 필수 조건 + +## 마이그레이션 전략 +- Emotion과 Tailwind 설치 공존 (Phase 0) +- Phase 1부터 신규 코드는 Tailwind만 사용 +- 기존 컴포넌트는 FSD 이전 시 Tailwind로 교체 +- Emotion 신규 작성 금지 + +## 영향 +- 모든 styled-component 패턴 제거 필요 +- className 기반 스타일링으로 전환 diff --git a/docs/decisions/002-fsd.md b/docs/decisions/002-fsd.md new file mode 100644 index 00000000..0b750830 --- /dev/null +++ b/docs/decisions/002-fsd.md @@ -0,0 +1,42 @@ +# ADR 002: FSD (Feature-Sliced Design) 완전 적용 + +## 상태 +확정 (2026-03-20) + +## 배경 +React → Next.js 긴급 마이그레이션으로 인해 폴더 구조가 도메인 분리 형태로 +구성되어 있으나, 컴포넌트 간 의존성이 불명확하고 관심사 분리가 부족하다. + +## 결정 +FSD(Feature-Sliced Design) 아키텍처를 완전 적용한다. + +## FSD 계층 구조 + +``` +shared → entities → features → widgets → pages → app +(공통) (도메인) (기능) (UI블록) (페이지) (라우팅) +``` + +- 상위 레이어는 하위 레이어에 의존할 수 없다 (단방향 의존성) +- 같은 레이어 내 슬라이스 간 직접 참조 금지 + +## 각 레이어 책임 + +| 레이어 | 책임 | 현재 코드 출처 | +|--------|------|----------------| +| `app/` | Next.js 라우팅만 | `src/app/` (유지) | +| `pages/` | 페이지 조합 | `src/page/` | +| `widgets/` | 독립 UI 블록 | 대형 컴포넌트 분리 | +| `features/` | 사용자 시나리오 | `src/hooks/` + `src/components/` | +| `entities/` | 도메인 모델 | `src/model/` + `src/api/` | +| `shared/` | 공통 요소 | `src/components/designSystem/`, `src/utils/`, `src/constants/` | + +## 근거 +- 단방향 의존성으로 코드 복잡도 감소 +- feature 단위로 독립적 개발/테스트 가능 +- 대형 컴포넌트(862줄) 분할의 명확한 기준 제공 + +## 마이그레이션 전략 +- Phase 0: 빈 디렉토리 스캐폴딩 +- Phase 1: shared 레이어부터 시작 +- Phase 2~4: 하위→상위 순서로 순차 이전 diff --git a/docs/decisions/003-testing.md b/docs/decisions/003-testing.md new file mode 100644 index 00000000..5610c4be --- /dev/null +++ b/docs/decisions/003-testing.md @@ -0,0 +1,36 @@ +# ADR 003: 테스트 전략 - Vitest + Playwright + +## 상태 +확정 (2026-03-20) + +## 배경 +현재 테스트 코드가 전혀 없는 상태에서 대규모 리팩토링을 진행한다. +테스트 없이 리팩토링하면 회귀 버그를 감지할 수 없다. + +## 결정 +- **단위/통합 테스트**: Vitest + React Testing Library +- **E2E 테스트**: Playwright +- **방식**: TDD (테스트 먼저 작성 후 구현) + +## 테스트 레벨 + +| 레벨 | 도구 | 대상 | +|------|------|------| +| 단위 테스트 | Vitest | shared/ui 컴포넌트, utils, hooks | +| 통합 테스트 | Vitest + RTL | features, entities API | +| E2E 테스트 | Playwright | 주요 사용자 플로우 | + +## 주요 E2E 시나리오 (Phase 4에서 작성) +- 로그인 / 소셜 로그인 +- 여행 생성 플로우 +- 여행 검색 및 신청 +- 마이페이지 수정 + +## TDD 원칙 +1. 실패하는 테스트 먼저 작성 +2. 테스트를 통과하는 최소한의 코드 작성 +3. 리팩토링 + +## 성능 테스트 (Phase 4) +- Lighthouse CI로 빌드 시 성능 점수 측정 +- Core Web Vitals (LCP, FID, CLS) 기준점 설정 후 개선 diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 00000000..1da7bf62 --- /dev/null +++ b/docs/progress.md @@ -0,0 +1,104 @@ +# MOING Frontend 리팩토링 진행 현황 + +## 전체 진행률 + +| Phase | 이름 | 상태 | 시작일 | 완료일 | +|-------|------|------|--------|--------| +| Phase 0 | 기반 구축 | ✅ 완료 | 2026-03-20 | 2026-03-20 | +| Phase 1 | shared 레이어 | ✅ 완료 | 2026-03-20 | 2026-03-20 | +| Phase 1.5 | 웹 접근성 보강 | ✅ 완료 | 2026-03-20 | 2026-03-20 | +| Phase 2 | entities 레이어 | 🔜 대기 | - | - | +| Phase 3 | features 레이어 | 🔜 대기 | - | - | +| Phase 4 | pages / widgets 레이어 | 🔜 대기 | - | - | + +--- + +## Phase 0: 기반 구축 + +### 체크리스트 +- [x] Vitest 설정 +- [x] Playwright 설정 +- [x] Tailwind CSS 설치 및 설정 (Emotion 공존) +- [x] FSD 디렉토리 스캐폴딩 +- [x] docs 문서 구조 생성 + +### 변경 파일 목록 +_작업 완료 후 기록_ + +--- + +## Phase 1: shared 레이어 + +### 체크리스트 +- [x] 1-1: Button 그룹 (6개 컴포넌트, 18개 테스트) +- [x] 1-2: Badge, Select, Tag, Text 그룹 (9개 컴포넌트, 20개 테스트) +- [x] 1-3: Input 그룹 (7개 컴포넌트, 36개 테스트) +- [x] 1-4: Toast 그룹 (5개 컴포넌트, 12개 테스트) — FSD 위반 제거, BaseToast 추출 +- [x] 1-5: Modal 그룹 (9개 컴포넌트, 23개 테스트) — ModalDimmed/BaseModal/BottomSheetModal 추출 +- [x] 1-6: Profile (1개 컴포넌트, 4개 테스트) — RoundedImage + +### 누계 테스트 +- 최종: **113개** 통과 (26개 테스트 파일) + +--- + +## Phase 1.5: 웹 접근성 보강 + +> **결정 배경**: 접근성은 shared/ui 레벨에서 보장해야 하나, Phase 1 흐름을 끊지 않기 위해 분리. + +### 전략 +- **컴포넌트 고정값** (design system에서 처리): `aria-expanded`, `role`, `aria-modal`, focus trap 등 +- **컨텍스트 의존값** (prop으로 받기): `aria-label`, `aria-describedby` 등 + +### 체크리스트 +- [x] `jest-axe` 기반 접근성 자동 검사 도입 (setup.ts 전역 등록) +- [x] Select: `role="combobox"`, `aria-expanded`, `aria-haspopup="listbox"`, `role="listbox"`, `role="option"`, `aria-selected` +- [x] CloseButton: `aria-label` prop (기본값: text prop과 동일) +- [x] RemoveButton: `aria-label="삭제"` (아이콘 전용 버튼) +- [x] StateInputField: `aria-describedby` 연결 (`errorMessageId` prop) +- [x] CodeInput: 각 셀에 `aria-label="{n}번째 숫자"` (1~6) +- [x] BaseModal: `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus trap, Escape 닫기 +- [x] BottomSheetModal: 동일 접근성 속성 적용 +- [x] 키보드 네비게이션 (Escape, Tab focus trap 구현 완료) + +### 누계 테스트 +- 최종: **134개** 통과 (27개 테스트 파일) + +--- + +## Phase 2: entities 레이어 + +### 체크리스트 +- [ ] `model/` → `entities/{domain}/model` +- [ ] `api/` → `entities/{domain}/api` +- [ ] 도메인별 타입 정리 +- [ ] 각 entity 테스트 작성 + +### 변경 파일 목록 +_작업 완료 후 기록_ + +--- + +## Phase 3: features 레이어 + +### 체크리스트 +- [ ] `hooks/` + `components/` → `features/{feature}/` +- [ ] 각 feature TDD 적용 +- [ ] 통합 테스트 작성 + +### 변경 파일 목록 +_작업 완료 후 기록_ + +--- + +## Phase 4: pages / widgets 레이어 + +### 체크리스트 +- [ ] `page/` → `pages/` + `widgets/` +- [ ] Server Component 전환 (하이브리드) +- [ ] 성능 테스트 측정 +- [ ] 성능 최적화 +- [ ] E2E 테스트 작성 + +### 변경 파일 목록 +_작업 완료 후 기록_ diff --git a/docs/refactoring/_template.md b/docs/refactoring/_template.md new file mode 100644 index 00000000..097c9bef --- /dev/null +++ b/docs/refactoring/_template.md @@ -0,0 +1,100 @@ +# Phase N: [제목] + +> 이 문서는 블로그/이력서 작성 재료입니다. 기술적 판단 근거와 트러블슈팅 과정을 상세히 기록합니다. + +## 1. 배경 및 문제 정의 + +### 왜 이 작업이 필요했는가 + + + +### 문제로 인한 실제 영향 + + +--- + +## 2. 선택지와 의사결정 + +### 검토한 옵션들 +| 옵션 | 장점 | 단점 | 선택 여부 | +|------|------|------|-----------| +| 옵션 A | ... | ... | ❌ | +| 옵션 B | ... | ... | ✅ | + +### 최종 결정 및 근거 + + +--- + +## 3. 구현 과정 + +### 핵심 기술적 도전 + + +### 해결 방법 + + +```tsx +// Before +... + +// After +... +``` + +--- + +## 4. Before / After 비교 + +### 구조 변화 +``` +Before: +... + +After: +... +``` + +### 코드 품질 변화 +| 지표 | Before | After | +|------|--------|-------| +| 파일 수 | | | +| 평균 컴포넌트 라인 수 | | | +| 테스트 커버리지 | | | +| 중복 코드 | | | + +--- + +## 5. 트러블슈팅 + +### 문제 1: [제목] +- **현상**: +- **원인**: +- **해결**: + +### 문제 2: [제목] +- **현상**: +- **원인**: +- **해결**: + +--- + +## 6. 결과 및 수치 + +- 변경된 파일: N개 +- 제거된 라인: N줄 +- 추가된 테스트: N개 +- 기타 성과: + +--- + +## 7. 회고 / 배운 점 + +### 잘 된 것 +- + +### 다음에 다르게 할 것 +- + +### 핵심 인사이트 + diff --git a/docs/refactoring/phase-0.md b/docs/refactoring/phase-0.md new file mode 100644 index 00000000..d0f54e56 --- /dev/null +++ b/docs/refactoring/phase-0.md @@ -0,0 +1,173 @@ +# Phase 0: 기반 구축 + +> 이 문서는 블로그/이력서 작성 재료입니다. 기술적 판단 근거와 트러블슈팅 과정을 상세히 기록합니다. + +## 1. 배경 및 문제 정의 + +### 왜 이 작업이 필요했는가 +MOING은 React 프로젝트를 Next.js로 긴급 마이그레이션한 서비스로, 대규모 리팩토링을 앞두고 있었다. +리팩토링 전 다음 기반 환경이 전혀 갖춰지지 않은 상태였다. + +- **테스트**: 448개 파일, ~40,460줄의 코드에 테스트 코드 0개 +- **스타일링**: Emotion(CSS-in-JS)은 Next.js App Router의 Server Component와 구조적으로 호환되지 않음 +- **폴더 구조**: 도메인 분리 구조로 FSD 이전을 위한 목표 구조 부재 + +### 문제로 인한 실제 영향 +- 테스트 없이 리팩토링 시 회귀 버그를 감지할 수 없음 +- Emotion이 모든 컴포넌트를 강제로 Client Component화 → SSR 이점 포기 +- 구조적 목표 없이 파일 이동 시 혼란 야기 + +--- + +## 2. 선택지와 의사결정 + +### 테스트 프레임워크 + +| 옵션 | 장점 | 단점 | 선택 여부 | +|------|------|------|-----------| +| Jest | 생태계 성숙, 레퍼런스 많음 | Next.js 환경 설정 복잡, 느림 | ❌ | +| Vitest | Vite 기반으로 빠름, ESM 네이티브, Jest 호환 API | 상대적으로 레퍼런스 적음 | ✅ | + +**결정**: Vitest 선택. Next.js 14 + App Router 환경에서 Vite 기반이 설정이 간결하고, Jest 호환 API 덕분에 러닝커브가 낮다. + +### DOM 환경 + +| 옵션 | 장점 | 단점 | 선택 여부 | +|------|------|------|-----------| +| jsdom | 가장 널리 사용됨, 풍부한 Web API 구현 | v29에서 Node 20.11.0과 ESM 충돌 | ❌ | +| happy-dom | 가볍고 빠름, ESM 친화적 | jsdom보다 Web API 커버리지 낮음 | ✅ | + +**결정**: happy-dom 선택. jsdom v29가 `@exodus/bytes` (ESM-only)에 의존하면서 Node 20.11.0 환경에서 CJS 충돌 발생. happy-dom이 이 환경에서 안정적으로 동작. + +### 스타일링 전환 + +| 옵션 | 장점 | 단점 | 선택 여부 | +|------|------|------|-----------| +| Emotion 유지 | 기존 코드 변경 없음 | Server Component 사용 불가, 번들에 런타임 CSS 생성 코드 포함 | ❌ | +| Tailwind CSS | Server Component 완벽 호환, 빌드타임 CSS 생성, 번들 사이즈 감소 | 기존 코드 전면 교체 필요 | ✅ | + +**결정**: Tailwind CSS v4 선택. "Next.js스럽게" 개편하는 핵심 목표를 위해서는 Server Component 호환이 필수였고, CSS-in-JS 런타임 오버헤드 제거가 성능 최적화에도 직결된다. + +--- + +## 3. 구현 과정 + +### 핵심 기술적 도전: Node.js 20.11.0 환경 제약 + +프로젝트 환경이 Node.js 20.11.0으로 고정되어 있어 최신 패키지들과의 호환성 문제가 연쇄적으로 발생했다. + +#### 도전 1: vitest@4 실행 불가 +vitest v4는 내부적으로 `rolldown`을 사용하는데, rolldown이 Node.js 20.12.0+의 `node:util`의 `styleText` API를 요구한다. + +``` +SyntaxError: The requested module 'node:util' does not provide an export named 'styleText' +``` + +**해결**: vitest@3으로 다운그레이드. v3는 rolldown 없이 기존 Vite 번들러를 사용. + +#### 도전 2: vitest config 로드 실패 (CJS → ESM 충돌) +vitest v3의 config 로더(`vitest/dist/config.cjs`)가 CJS 방식으로 Vite를 require()하려 하지만, Vite v6+는 순수 ESM 모듈이라 충돌 발생. + +``` +Error [ERR_REQUIRE_ESM]: require() of ES Module .../vite/dist/node/index.js +from .../vitest/dist/config.cjs not supported. +``` + +**해결**: `vitest.config.ts` → `vitest.config.mts`로 변경. `.mts` 확장자가 파일을 ESM으로 강제 로딩하여 dynamic import()로 Vite를 불러옴. + +#### 도전 3: jsdom v29 ESM 의존성 충돌 +jsdom v29가 의존하는 `html-encoding-sniffer`가 `@exodus/bytes`(ESM-only)를 CJS 방식으로 require()하면서 충돌. + +``` +Error [ERR_REQUIRE_ESM]: require() of ES Module .../encoding-lite.js +from .../html-encoding-sniffer/lib/html-encoding-sniffer.js not supported. +``` + +**해결**: jsdom 대신 `happy-dom` 사용. ESM 친화적이며 React Testing Library와 완벽 호환. + +### Tailwind CSS v4 설정 변경점 + +Tailwind v4는 v3과 설정 방식이 크게 다르다. + +```js +// v3 방식 (postcss.config.js) +module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } } + +// v4 방식 +module.exports = { plugins: { '@tailwindcss/postcss': {} } } +``` + +```css +/* v3 방식 (globals.css) */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* v4 방식 */ +@import "tailwindcss"; + +@theme { + --font-pretendard: "Pretendard", sans-serif; +} +``` + +v4에서는 `tailwind.config.ts`가 불필요하며, 테마 설정을 CSS의 `@theme` 블록에서 관리한다. + +--- + +## 4. Before / After 비교 + +### 환경 변화 +| 항목 | Before | After | +|------|--------|-------| +| 테스트 환경 | 없음 | Vitest v3 + happy-dom | +| E2E 환경 | 없음 | Playwright v1.58 | +| 스타일링 | Emotion (런타임) | Tailwind v4 (빌드타임) + Emotion 공존 | +| 폴더 구조 목표 | 없음 | FSD 스캐폴딩 완료 | +| 테스트 명령어 | 없음 | `yarn test`, `yarn test:run`, `yarn test:e2e` | + +--- + +## 5. 트러블슈팅 + +### 문제 1: rolldown의 Node.js 버전 요구 +- **현상**: `vitest@4` 설치 후 `yarn test:run` 실행 시 `styleText` import 오류 +- **원인**: vitest v4가 의존하는 rolldown 번들러가 Node 20.12.0+ 전용 API 사용 +- **해결**: `vitest@3`으로 다운그레이드. v3은 rolldown 없이 Rollup 기반으로 동작 + +### 문제 2: ESM/CJS 충돌 연쇄 +- **현상**: vitest config 로드 실패 → jsdom 환경 실행 실패 두 번의 ESM/CJS 충돌 +- **원인**: Node.js 생태계가 ESM으로 전환 중인 과도기. CJS 모듈이 ESM-only 모듈을 require()하면서 충돌 +- **해결**: 설정 파일 `.mts` 변환 + jsdom → happy-dom 교체로 ESM 충돌 우회 +- **인사이트**: 최신 패키지일수록 ESM-only 경향. Node.js LTS 버전 관리의 중요성 체감 + +### 문제 3: yarn + npm lock 파일 공존 경고 +- **현상**: `package-lock.json`이 존재해 yarn 사용 시 경고 발생 +- **원인**: 기존에 npm으로 설치된 흔적 +- **현황**: 기능적 문제 없음. Phase 0 범위 외로 판단, 추후 정리 예정 + +--- + +## 6. 결과 및 수치 + +- 신규 설치 패키지: 10개 (vitest, playwright, tailwind 관련) +- 신규 설정 파일: 5개 (vitest.config.mts, playwright.config.ts, postcss.config.js, tailwind.config.ts, src/test/setup.ts) +- FSD 디렉토리: 10개 신규 생성 +- 문서: CLAUDE.md + docs/ 구조 전체 + +--- + +## 7. 회고 / 배운 점 + +### 잘 된 것 +- 기존 코드를 전혀 건드리지 않고 기반 환경 구축 완료 +- Emotion과 Tailwind 공존 전략으로 점진적 마이그레이션 기반 마련 + +### 다음에 다르게 할 것 +- 프로젝트 시작 시 Node.js 버전을 `.nvmrc`로 고정해두면 이런 충돌을 방지할 수 있음 +- `package-lock.json` 제거 후 yarn으로 통일하는 작업을 초기에 처리 + +### 핵심 인사이트 +> Node.js 생태계의 CJS → ESM 전환 과도기에서 패키지 버전 간 충돌은 흔하다. +> 최신 패키지가 항상 좋은 것은 아니며, 환경 제약에 맞는 버전 선택이 중요하다. +> vitest config 파일의 `.mts` 확장자 트릭처럼, 작은 설정 변경이 큰 문제를 해결하기도 한다. diff --git a/docs/refactoring/phase-1.5.md b/docs/refactoring/phase-1.5.md new file mode 100644 index 00000000..cb52815e --- /dev/null +++ b/docs/refactoring/phase-1.5.md @@ -0,0 +1,242 @@ +# Phase 1.5: 웹 접근성 보강 + +> 이 문서는 블로그/이력서 작성 재료입니다. 기술적 판단 근거와 트러블슈팅 과정을 상세히 기록합니다. + +## 1. 배경 및 문제 정의 + +### 왜 이 작업이 필요했는가 + +Phase 1에서 31개 컴포넌트를 `shared/ui`로 이전하면서 기능은 동작했으나 접근성(Accessibility)이 누락되어 있었다: + +- **아이콘 전용 버튼**: `RemoveButton`(X 아이콘)에 `aria-label` 없음 → 스크린리더가 버튼 목적을 알 수 없음 +- **Select 드롭다운**: `role`, `aria-expanded` 없어 스크린리더가 드롭다운 상태를 인식 불가 +- **모달**: `role="dialog"`, `aria-modal` 없음, Escape 닫기 미지원, focus trap 없음 → 키보드 사용자가 모달 밖으로 이탈 가능 +- **에러 메시지 연결**: `StateInputField`의 에러 메시지가 시각적으로만 표시, 스크린리더에 연결 안 됨 +- **OTP 입력**: `CodeInput` 6개 셀 각각의 목적이 스크린리더에 불분명 + +--- + +## 2. 선택지와 의사결정 + +### Phase 분리 결정 + +Phase 1 진행 중 접근성을 함께 추가하면 흐름이 끊기므로 Phase 1.5로 분리. + +### 고정값 vs 컨텍스트 의존값 전략 + +| 구분 | 방식 | 예시 | +|------|------|------| +| **고정값** (항상 동일) | 컴포넌트 내부 하드코딩 | `role="dialog"`, `aria-modal="true"`, `aria-expanded` | +| **컨텍스트 의존값** (호출부마다 다름) | prop으로 주입 | `aria-label`, `aria-labelledby`, `aria-describedby` | + +**결정 근거**: ARIA Authoring Practices Guide(APG) 패턴에 따르면 dialog role처럼 의미가 고정된 속성은 컴포넌트에서 보장해야 하고, 제목 텍스트처럼 문맥에 따라 달라지는 것은 prop으로 받아야 한다. + +### jest-axe 도입 + +`axe-core` 기반 자동 접근성 검사를 Vitest + `jest-axe`로 도입. 수동 체크리스트만으로는 WCAG 위반을 놓칠 수 있으므로 CI에서 자동 검증. + +--- + +## 3. 구현 과정 + +### 3-1. jest-axe 설치 및 전역 설정 + +```bash +npm install --save-dev jest-axe @types/jest-axe +``` + +`src/test/setup.ts`에 전역 등록: +```typescript +import { toHaveNoViolations } from 'jest-axe'; +import { expect } from 'vitest'; +expect.extend(toHaveNoViolations); +``` + +이후 모든 테스트에서 `await axe(container)`만으로 WCAG 검사 가능. + +### 3-2. RemoveButton — 아이콘 전용 버튼 접근성 + +**Before**: 아이콘만 있는 버튼에 접근성 이름 없음 +```tsx + +``` + +**After**: `aria-label` 고정값 추가 +```tsx + +``` + +**WCAG 기준**: WCAG 2.1 SC 4.1.2 - 모든 UI 컴포넌트는 접근성 이름(Accessible Name)이 있어야 함. + +### 3-3. CloseButton — aria-label prop 지원 + +CloseButton은 텍스트("닫기")를 렌더링하므로 접근성 이름은 이미 있음. 하지만: +- 향후 아이콘 전용으로 사용될 경우를 위해 `aria-label` prop 지원 +- 기본값을 `text` prop과 동일하게 설정하여 불일치 방지 + +```tsx +// aria-label이 없으면 text prop을 그대로 사용 +const CloseButton = ({ onClick, text = '닫기', 'aria-label': ariaLabel }) => ( + + + +); +``` + +### Tailwind 전환: 동적 스타일 처리 + +Button에서 disabled 상태처럼 정적으로 결정되는 스타일은 Tailwind 조건부 클래스로, +색상/크기를 props로 받는 부분은 CSS 변수(`var(--color-*)`) + inline style 조합: + +```tsx +// CSS 변수 기반 색상 (globals.css @theme에서 관리) +className="text-[var(--color-keycolor)]" + +// 런타임 동적 값은 inline style 유지 +style={style} // 부모에서 내려온 커스텀 색상 +``` + +### FilterButton 오타 수정 + 하위 호환 + +```tsx +// 기존 레거시 래퍼 (intializeOnClick → initializeOnClick 어댑터) +export default function LegacyFilterButton({ intializeOnClick, ...props }) { + return ; +} +``` + +--- + +## 4. Before / After 비교 + +### 구조 변화 +``` +Before: +src/components/designSystem/Buttons/ +├── Button.tsx (75줄, ButtonContainer 포함) +├── FilterButton.tsx (97줄, ButtonContainer 중복) +├── ApplyListButton.tsx (119줄, ButtonContainer 중복) +├── CloseButton.tsx (38줄) +├── EditAndDeleteButton.tsx (98줄) +└── ReportButton.tsx (63줄) + +After: +src/shared/ui/button/ +├── Button.tsx (44줄, 재사용 가능한 베이스) +├── FilterButton.tsx (50줄, Button 조합) +├── ApplyListButton.tsx (60줄, Button 조합) +├── CloseButton.tsx (32줄) +├── EditAndDeleteButton.tsx (52줄) +├── ReportButton.tsx (32줄) +└── [테스트 파일 4개, 18개 테스트] + +src/components/designSystem/Buttons/ → re-export만 남음 +``` + +### 코드 품질 변화 (Button 그룹 기준) + +| 지표 | Before | After | +|------|--------|-------| +| 총 라인 수 | 490줄 | 270줄 (Button 그룹 구현부) | +| 중복 ButtonContainer | 3곳 | 0곳 | +| 테스트 | 0개 | 18개 | +| CSS 방식 | Emotion (런타임) | Tailwind (빌드타임) | +| FSD 레이어 | 없음 | shared/ui 준수 | + +--- + +## 5. 트러블슈팅 + +### 1-2: TextareaField clone 트릭과 CSS 분리 + +**문제**: `TextareaField`는 유연한 높이 계산을 위해 화면 밖에 숨겨진 `Clone` textarea를 사용하는데, +Emotion의 `styled.textarea`로 만들어진 `Clone`과 `DetailTextArea`가 동일한 CSS를 중복으로 보유. + +**원인**: `Clone`에는 `visibility: hidden; position: absolute; top: -9999px` 등이 추가될 뿐, +나머지 폰트/패딩/스크롤바 CSS는 `DetailTextArea`와 완전히 동일. + +**해결**: `sharedTextareaStyle` 공통 스타일 객체로 추출하여 두 엘리먼트에 동일하게 적용. +스크롤바 CSS는 `globals.css`의 `.textarea-scrollbar` 클래스로 분리. + +```tsx +// 공통 스타일 객체로 중복 제거 +const sharedTextareaStyle: React.CSSProperties = { + padding, fontSize, lineHeight, letterSpacing: '-0.025em', + fontFamily: '"Pretendard"', height: computedHeight, color, +}; +``` + +### 1-2: TextareaField height number 단위 누락 버그 수정 + +**문제**: 원본 Emotion 코드에 `height: ${props.height}` (number인 경우 단위 없음)가 있었는데, +CSS에서 `height: 48`처럼 단위 없는 값은 무효. + +**분석**: 실제로는 height가 항상 string(`"31svh"`, `"100%"`)으로 전달되어 잠재적 버그. + +**수정**: `typeof height === 'number' ? \`${height}px\` : height` → `${height}px`로 수정. + +### 1-2: CodeInput input-bar 숨김 CSS → globals.css + +**문제**: 원본 Emotion 코드의 `&:not(:placeholder-shown) ~ .input-bar` 선택자는 +Emotion에서만 작동하는 부모-자식 CSS 범위. + +**해결**: globals.css에 `.code-input-cell` 래퍼 클래스를 추가하고, +해당 클래스 내부에서 선택자 적용: +```css +.code-input-cell input:not(:placeholder-shown) ~ .input-bar, +.code-input-cell input:focus ~ .input-bar { display: none; } +``` + +### 1-2: StateInputField shake 애니메이션 클래스화 + +**문제**: Emotion `keyframes`와 `animation: ${props.shake ? css\`...\` : 'none'}` 패턴을 +Tailwind로 대체해야 했음. + +**해결**: +1. `@keyframes shake` 선언 → globals.css (이미 존재) +2. `.animate-shake { animation: shake 0.3s; }` → globals.css에 추가 +3. 조건부 Tailwind 클래스: `shake ? 'animate-shake' : ''` + +--- + +## 6. 현재 진행 상황 + +### 완료 +- [x] 팔레트 색상 → Tailwind `@theme` 등록 +- [x] **1-1: Button 그룹** (6개 컴포넌트, 18개 테스트 통과) +- [x] **1-2: Badge, Select, Tag, Text 그룹** (9개 컴포넌트, 20개 테스트 추가 → 누계 38개) +- [x] **1-3: Input 그룹** (7개 컴포넌트, 36개 테스트 추가 → 누계 74개) + - RemoveButton, InputField, StateInputField, ValidationInputField + - TextareaField, CodeInput, CommentInput + - globals.css: `.textarea-scrollbar`, `.animate-shake`, `.code-input-cell`, `.code-number-input` +- [x] **1-4: Toast 그룹** (5개 컴포넌트, 12개 테스트 추가 → 누계 86개) + - BaseToast (신규), ErrorToast (FSD 위반 제거), ResultToast, WarningToast, TripToast + - errorToast: 스토어 어댑터 패턴으로 하위 호환 유지 +- [x] **1-5: Modal 그룹** (9개 컴포넌트, 23개 테스트 추가 → 누계 109개) + - ModalDimmed (신규), BaseModal (신규), BottomSheetModal (신규) + - CheckingModal, NoticeModal, ResultModal, EditAndDeleteModal, ReportModal, ImageModal + - window.innerWidth 직접 사용 → Tailwind 대체 (SSR 안전) + +### 진행 예정 +- [x] **1-6: Profile** (1개 컴포넌트, 4개 테스트 추가 → 누계 113개) + - RoundedImage: Emotion → Tailwind + inline style + +--- + +## 7. 회고 / 배운 점 + +### 기술적 인사이트 + +**Emotion → Tailwind 전환의 핵심 패턴** +- 정적 레이아웃/타이포 → Tailwind 클래스 +- 상태 기반 색상(focused, hasError 등) → inline `style` prop (계산된 값은 Tailwind로 표현 불가) +- 복잡한 CSS (scrollbar, keyframes, sibling selector) → `globals.css` 커스텀 클래스 + +**FSD 위반 해결 전략** +- `ErrorToast` (store 직접 import): shared 레이어를 순수 props 기반으로 변경 → 기존 위치에 store 어댑터 배치 +- 패턴: shared는 항상 props/콜백만 알고, store 연결은 feature/widget 레이어에서 담당 + +**중복 제거 패턴 3가지** +1. 조합(Composition): `BaseToast + icon → ResultToast / WarningToast` +2. 래퍼 추출: `BaseModal → CheckingModal / NoticeModal / ResultModal` +3. 공통 컴포넌트: `ModalDimmed` (3곳 DarkWrapper 중복) + +**TDD에서 portal 컴포넌트 테스트** +- `createPortal` mock + `beforeEach`에서 DOM 엘리먼트 생성 패턴 확립 +- 테스트 격리: `afterEach`에서 portal root 제거 + +### 버그 수집 (블로그 소재) +| 버그 | 원인 | 교훈 | +|------|------|------| +| `height: ${number}` (단위 없음) | Emotion 템플릿 리터럴 타입 안전성 부재 | TypeScript로도 잡기 어려운 CSS 단위 버그 | +| `opacity: 0px` | CSS 단위 오기입 | opacity는 단위 없는 숫자 | +| `white-space: "pre-line"` (JS 문자열) | styled-component 내 JS 값 혼용 | CSS 문자열을 JS 문자열로 잘못 감쌈 | +| `import { styleText } from 'util'` | 미사용 import 방치 | 린터 설정 중요성 | +| `window.innerWidth` SSR 위험 | Client-only API 직접 사용 | 반응형은 CSS로 처리 | +| `clip-path` (소문자) | HTML 속성 → JSX camelCase 혼동 | JSX는 camelCase | +| `display: flex` 2번 선언 | styled-component 복붙 | 코드 리뷰의 중요성 | + +### Phase 1 최종 성과 +- **컴포넌트**: 31개 → `shared/ui` 레이어로 이전 완료 (FSD 준수) +- **테스트**: 0개 → **113개** (26개 테스트 파일) +- **Emotion 제거**: 모든 designSystem 컴포넌트에서 Emotion 완전 제거 +- **중복 제거**: ButtonContainer 3중복, DarkWrapper 3중복, BaseToast/BaseModal 추출 +- **버그 수정**: 7건 +- **하위 호환**: 기존 import 경로 전부 유지 (re-export 전략) diff --git a/e2e/.gitkeep b/e2e/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/next.config.js b/next.config.js index f67d0a77..29f0c3f0 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,16 @@ const nextConfig = { experimental: { scrollRestoration: true, }, + async rewrites() { + const apiBaseUrl = process.env.API_BASE_URL; + if (!apiBaseUrl) return []; + return [ + { + source: '/api/:path*', + destination: `${apiBaseUrl}/api/:path*`, + }, + ]; + }, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 24058d18..82c9198a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,12 +20,14 @@ "@tanstack/react-query-devtools": "^5.64.0", "@turf/turf": "^7.2.0", "@vis.gl/react-google-maps": "^1.5.1", + "autoprefixer": "^10.4.27", "axios": "^1.7.9", "dayjs": "^1.11.13", "embla-carousel": "^8.5.2", "embla-carousel-react": "^8.5.2", "next": "14.0.4", "next-view-transitions": "^0.3.4", + "postcss": "^8.5.8", "react": "18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.2.0", @@ -37,6 +39,7 @@ "react-slick": "^0.30.3", "react-transition-group": "^4.4.5", "slick-carousel": "^1.8.1", + "tailwindcss": "^4.2.2", "uuid": "^11.1.0", "zod": "^3.24.1", "zustand": "^5.0.3" @@ -45,14 +48,20 @@ "@chromatic-com/storybook": "^3.2.3", "@eslint/eslintrc": "^3.2.0", "@mswjs/http-middleware": "^0.10.3", + "@playwright/test": "^1.58.2", "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/addon-onboarding": "^8.4.7", "@storybook/blocks": "^8.4.7", "@storybook/builder-vite": "^8.4.7", "@storybook/nextjs": "^8.4.7", + "@tailwindcss/postcss": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/cors": "^2.8.17", "@types/express": "^5.0.1", + "@types/jest-axe": "^3.5.9", "@types/node": "^20.17.12", "@types/react": "^19.0.5", "@types/react-copy-to-clipboard": "^5.0.7", @@ -62,6 +71,7 @@ "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "3", "chromatic": "^11.22.2", "cors": "^2.8.5", "eslint": "^9.18.0", @@ -71,10 +81,15 @@ "eslint-plugin-react": "^7.37.3", "eslint-plugin-storybook": "^0.11.2", "express": "^5.1.0", + "happy-dom": "^20.8.4", + "jest-axe": "^10.0.0", + "jsdom": "^29.0.1", "msw": "^2.7.3", "prettier": "^3.4.2", "storybook": "^8.4.7", - "typescript": "5.0.4" + "typescript": "5.0.4", + "vite": "6", + "vitest": "3" } }, "node_modules/@adobe/css-tools": { @@ -83,6 +98,18 @@ "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", "dev": true }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -96,14 +123,70 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", + "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "dev": true, + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -406,9 +489,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "engines": { "node": ">=6.9.0" } @@ -1782,6 +1865,27 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", @@ -1830,6 +1934,140 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2487,6 +2725,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@googlemaps/markerclusterer": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz", @@ -2626,6 +2881,151 @@ "@types/node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/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, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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, + "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/@isaacs/cliui/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, + "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/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -2639,6 +3039,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2666,14 +3076,14 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3282,6 +3692,16 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -3294,6 +3714,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -3376,8 +3811,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.34.8", @@ -3390,8 +3824,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.34.8", @@ -3404,8 +3837,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.34.8", @@ -3418,8 +3850,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.34.8", @@ -3432,8 +3863,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.34.8", @@ -3446,8 +3876,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.34.8", @@ -3460,8 +3889,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.34.8", @@ -3474,8 +3902,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.34.8", @@ -3488,8 +3915,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.34.8", @@ -3502,8 +3928,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.34.8", @@ -3516,8 +3941,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.34.8", @@ -3530,8 +3954,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.34.8", @@ -3544,8 +3967,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.34.8", @@ -3558,8 +3980,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.34.8", @@ -3572,8 +3993,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.34.8", @@ -3586,8 +4006,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.34.8", @@ -3600,8 +4019,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.34.8", @@ -3614,8 +4032,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.34.8", @@ -3628,8 +4045,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -3783,6 +4199,12 @@ "node": ">=8" } }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true + }, "node_modules/@storybook/addon-actions": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.7.tgz", @@ -5016,96 +5438,7 @@ "storybook": "^8.4.7" } }, - "node_modules/@storybook/theming": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", - "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.64.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.0.tgz", - "integrity": "sha512-/MPJt/AaaMzdWJZTafgMyYhEX/lGjQrNz8+NDQSk8fNoU5PHqh05FhQaBrEQafW2PeBHsRbefEf//qKMiSAbQQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-devtools": { - "version": "5.62.16", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz", - "integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.64.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.0.tgz", - "integrity": "sha512-tBMzlROROUcTDMpDt1NC3n9ndKnJHPB3RCpa6Bf9f31TFvqhLz879x8jldtKU+6IwMSw1Pn4K1AKA+2SYyA6TA==", - "dependencies": { - "@tanstack/query-core": "5.64.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-query-devtools": { - "version": "5.64.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.0.tgz", - "integrity": "sha512-XORJjlbcBwPJaNbWBfZudaVVMi5TtlN1lYkHYU71hlG2c/jYpceO2yfAhZfgeyTNtqmTJ7jXOitgoGqtunsBAA==", - "dependencies": { - "@tanstack/query-devtools": "5.62.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-query": "^5.64.0", - "react": "^18 || ^19" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { + "node_modules/@storybook/test/node_modules/@testing-library/jest-dom": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", @@ -5125,7 +5458,20 @@ "yarn": ">=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "node_modules/@storybook/test/node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@storybook/test/node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", @@ -5138,98 +5484,504 @@ "node": ">=8" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "node_modules/@storybook/test/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true }, - "node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "node_modules/@storybook/theming": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", + "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", "dev": true, - "engines": { - "node": ">=12", - "npm": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@turf/along": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.2.0.tgz", - "integrity": "sha512-Cf+d2LozABdb0TJoIcJwFKB+qisJY4nMUW9z6PAuZ9UCH7AR//hy2Z06vwYCKFZKP4a7DRPkOMBadQABCyoYuw==", - "dependencies": { - "@turf/bearing": "^7.2.0", - "@turf/destination": "^7.2.0", - "@turf/distance": "^7.2.0", - "@turf/helpers": "^7.2.0", - "@turf/invariant": "^7.2.0", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@turf/angle": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/angle/-/angle-7.2.0.tgz", - "integrity": "sha512-b28rs1NO8Dt/MXadFhnpqH7GnEWRsl+xF5JeFtg9+eM/+l/zGrdliPYMZtAj12xn33w22J1X4TRprAI0rruvVQ==", + "node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", "dependencies": { - "@turf/bearing": "^7.2.0", - "@turf/helpers": "^7.2.0", - "@turf/invariant": "^7.2.0", - "@turf/rhumb-bearing": "^7.2.0", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" + "tslib": "^2.4.0" } }, - "node_modules/@turf/area": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.2.0.tgz", - "integrity": "sha512-zuTTdQ4eoTI9nSSjerIy4QwgvxqwJVciQJ8tOPuMHbXJ9N/dNjI7bU8tasjhxas/Cx3NE9NxVHtNpYHL0FSzoA==", + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, "dependencies": { - "@turf/helpers": "^7.2.0", - "@turf/meta": "^7.2.0", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" } }, - "node_modules/@turf/bbox": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.2.0.tgz", - "integrity": "sha512-wzHEjCXlYZiDludDbXkpBSmv8Zu6tPGLmJ1sXQ6qDwpLE1Ew3mcWqt8AaxfTP5QwDNQa3sf2vvgTEzNbPQkCiA==", - "dependencies": { - "@turf/helpers": "^7.2.0", - "@turf/meta": "^7.2.0", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/@turf/bbox-clip": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@turf/bbox-clip/-/bbox-clip-7.2.0.tgz", - "integrity": "sha512-q6RXTpqeUQAYLAieUL1n3J6ukRGsNVDOqcYtfzaJbPW+0VsAf+1cI16sN700t0sekbeU1DH/RRVAHhpf8+36wA==", - "dependencies": { - "@turf/helpers": "^7.2.0", - "@turf/invariant": "^7.2.0", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "engines": { + "node": ">= 20" }, - "funding": { - "url": "https://opencollective.com/turf" + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.64.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.0.tgz", + "integrity": "sha512-/MPJt/AaaMzdWJZTafgMyYhEX/lGjQrNz8+NDQSk8fNoU5PHqh05FhQaBrEQafW2PeBHsRbefEf//qKMiSAbQQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.62.16", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz", + "integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.64.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.0.tgz", + "integrity": "sha512-tBMzlROROUcTDMpDt1NC3n9ndKnJHPB3RCpa6Bf9f31TFvqhLz879x8jldtKU+6IwMSw1Pn4K1AKA+2SYyA6TA==", + "dependencies": { + "@tanstack/query-core": "5.64.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.64.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.0.tgz", + "integrity": "sha512-XORJjlbcBwPJaNbWBfZudaVVMi5TtlN1lYkHYU71hlG2c/jYpceO2yfAhZfgeyTNtqmTJ7jXOitgoGqtunsBAA==", + "dependencies": { + "@tanstack/query-devtools": "5.62.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.64.0", + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@turf/along": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.2.0.tgz", + "integrity": "sha512-Cf+d2LozABdb0TJoIcJwFKB+qisJY4nMUW9z6PAuZ9UCH7AR//hy2Z06vwYCKFZKP4a7DRPkOMBadQABCyoYuw==", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/destination": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/angle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/angle/-/angle-7.2.0.tgz", + "integrity": "sha512-b28rs1NO8Dt/MXadFhnpqH7GnEWRsl+xF5JeFtg9+eM/+l/zGrdliPYMZtAj12xn33w22J1X4TRprAI0rruvVQ==", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/area": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.2.0.tgz", + "integrity": "sha512-zuTTdQ4eoTI9nSSjerIy4QwgvxqwJVciQJ8tOPuMHbXJ9N/dNjI7bU8tasjhxas/Cx3NE9NxVHtNpYHL0FSzoA==", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.2.0.tgz", + "integrity": "sha512-wzHEjCXlYZiDludDbXkpBSmv8Zu6tPGLmJ1sXQ6qDwpLE1Ew3mcWqt8AaxfTP5QwDNQa3sf2vvgTEzNbPQkCiA==", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox-clip": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-clip/-/bbox-clip-7.2.0.tgz", + "integrity": "sha512-q6RXTpqeUQAYLAieUL1n3J6ukRGsNVDOqcYtfzaJbPW+0VsAf+1cI16sN700t0sekbeU1DH/RRVAHhpf8+36wA==", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, "node_modules/@turf/bbox-polygon": { @@ -7126,6 +7878,16 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -7155,6 +7917,12 @@ "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "node_modules/@types/doctrine": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", @@ -7232,6 +8000,91 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest-axe": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@types/jest-axe/-/jest-axe-3.5.9.tgz", + "integrity": "sha512-z98CzR0yVDalCEuhGXXO4/zN4HHuSebAukXDjTLJyjEAgoUf1H1i+sr7SUB/mz8CRS/03/XChsx0dcLjHkndoQ==", + "dev": true, + "dependencies": { + "@types/jest": "*", + "axe-core": "^3.5.5" + } + }, + "node_modules/@types/jest-axe/node_modules/axe-core": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.6.tgz", + "integrity": "sha512-LEUDjgmdJoA3LqklSTwKYqkjcZ4HKc4ddIYGSAiSkr46NTjzg2L9RNB+lekO9P7Dlpa87+hBtzc2Fzn/+GUWMQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -7360,6 +8213,12 @@ "@types/send": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, "node_modules/@types/statuses": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", @@ -7378,6 +8237,36 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.19.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", @@ -7618,6 +8507,48 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/expect": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", @@ -7660,6 +8591,53 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker/node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/pretty-format": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", @@ -7672,6 +8650,90 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/spy": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", @@ -8329,11 +9391,63 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -8595,6 +9709,17 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -8607,6 +9732,15 @@ "node": ">=12.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -8841,10 +9975,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -8860,10 +9993,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -8934,6 +10068,15 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -9000,9 +10143,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001692", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", - "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", "funding": [ { "type": "opencollective", @@ -9028,9 +10171,9 @@ } }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "dependencies": { "assertion-error": "^2.0.1", @@ -9040,7 +10183,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -9136,6 +10279,21 @@ "node": ">=6.0" } }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/cipher-base": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", @@ -9600,6 +10758,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -9659,6 +10830,28 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -9716,9 +10909,9 @@ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { "ms": "^2.1.3" }, @@ -9731,6 +10924,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -9855,11 +11054,19 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "dev": true, - "optional": true, "engines": { "node": ">=8" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -10009,6 +11216,12 @@ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, + "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 + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -10016,10 +11229,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.80", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", - "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", - "dev": true + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==" }, "node_modules/elliptic": { "version": "6.6.1", @@ -10103,13 +11315,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -10266,9 +11478,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true }, "node_modules/es-object-atoms": { @@ -10380,7 +11592,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "engines": { "node": ">=6" } @@ -10981,6 +12192,32 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -11299,6 +12536,22 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", @@ -11367,6 +12620,18 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -11660,6 +12925,35 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/happy-dom": { + "version": "20.8.4", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.4.tgz", + "integrity": "sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ==", + "dev": true, + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -11803,6 +13097,18 @@ "react-is": "^16.7.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -11819,6 +13125,12 @@ } ] }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -12366,6 +13678,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -12486,67 +13804,461 @@ "call-bound": "^1.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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, + "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", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-axe": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jest-axe/-/jest-axe-10.0.0.tgz", + "integrity": "sha512-9QR0M7//o5UVRnEUUm68IsGapHrcKGakYy9dKWWMX79LmeUKguDI6DREyljC5I13j78OUmtKLF5My6ccffLFBg==", + "dev": true, + "dependencies": { + "axe-core": "4.10.2", + "chalk": "4.1.2", + "jest-matcher-utils": "29.2.2", + "lodash.merge": "4.6.2" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/jest-axe/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, + "node_modules/jest-axe/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-axe/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/jest-matcher-utils": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", + "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, "dependencies": { - "is-docker": "^2.0.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-worker": { @@ -12619,6 +14331,76 @@ "node": ">=12.0.0" } }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12745,12 +14527,261 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lines-and-columns": { @@ -12820,9 +14851,9 @@ } }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true }, "node_modules/lower-case": { @@ -12853,12 +14884,23 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -12916,6 +14958,12 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -13087,6 +15135,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13158,9 +15215,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -13356,10 +15413,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -13640,6 +15696,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -13701,6 +15763,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13758,6 +15844,28 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -13772,6 +15880,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -13878,6 +15992,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pnp-webpack-plugin": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz", @@ -13939,10 +16097,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -13958,7 +16115,7 @@ } ], "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14098,8 +16255,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -14971,7 +17127,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -15168,6 +17323,18 @@ } } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -15477,6 +17644,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -15511,6 +17684,15 @@ "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/slick-carousel": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", @@ -15565,6 +17747,33 @@ "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -15580,6 +17789,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, "node_modules/storybook": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.7.tgz", @@ -15698,6 +17913,39 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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 + }, + "node_modules/string-width-cjs/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -15838,6 +18086,19 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -15886,6 +18147,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, "node_modules/style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", @@ -15969,6 +18248,12 @@ "tinyqueue": "^2.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/synckit": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", @@ -15985,13 +18270,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==" + }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { @@ -16025,33 +18319,134 @@ "terser": "^5.31.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.2" }, - "peerDependencies": { - "webpack": "^5.1.0" + "engines": { + "node": "18 || 20 || >=22" }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, "node_modules/third-party-capital": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz", @@ -16075,6 +18470,72 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyqueue": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", @@ -16098,6 +18559,24 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", + "dev": true, + "dependencies": { + "tldts-core": "^7.0.26" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -16182,6 +18661,18 @@ "node": ">= 4.0.0" } }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", @@ -16427,6 +18918,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "dev": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -16505,10 +19005,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", - "dev": true, + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -16632,7 +19131,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", @@ -16699,12 +19197,202 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vitest/node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -16717,6 +19405,15 @@ "node": ">=10.13.0" } }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/webpack": { "version": "5.97.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", @@ -16882,6 +19579,29 @@ "node": ">=10.13.0" } }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -16981,6 +19701,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17004,6 +19740,36 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -17023,9 +19789,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -17043,6 +19809,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 8f384ee3..d98a2267 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,15 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 9999 ", + "dev": "next dev -p 8080", "build": "next build", "start": "next start", "lint": "next lint", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "chromatic": "chromatic --exit-zero-on-changes", @@ -25,12 +30,14 @@ "@tanstack/react-query-devtools": "^5.64.0", "@turf/turf": "^7.2.0", "@vis.gl/react-google-maps": "^1.5.1", + "autoprefixer": "^10.4.27", "axios": "^1.7.9", "dayjs": "^1.11.13", "embla-carousel": "^8.5.2", "embla-carousel-react": "^8.5.2", "next": "14.0.4", "next-view-transitions": "^0.3.4", + "postcss": "^8.5.8", "react": "18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "18.2.0", @@ -42,6 +49,7 @@ "react-slick": "^0.30.3", "react-transition-group": "^4.4.5", "slick-carousel": "^1.8.1", + "tailwindcss": "^4.2.2", "uuid": "^11.1.0", "zod": "^3.24.1", "zustand": "^5.0.3" @@ -50,14 +58,20 @@ "@chromatic-com/storybook": "^3.2.3", "@eslint/eslintrc": "^3.2.0", "@mswjs/http-middleware": "^0.10.3", + "@playwright/test": "^1.58.2", "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/addon-onboarding": "^8.4.7", "@storybook/blocks": "^8.4.7", "@storybook/builder-vite": "^8.4.7", "@storybook/nextjs": "^8.4.7", + "@tailwindcss/postcss": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/cors": "^2.8.17", "@types/express": "^5.0.1", + "@types/jest-axe": "^3.5.9", "@types/node": "^20.17.12", "@types/react": "^19.0.5", "@types/react-copy-to-clipboard": "^5.0.7", @@ -67,6 +81,7 @@ "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "3", "chromatic": "^11.22.2", "cors": "^2.8.5", "eslint": "^9.18.0", @@ -76,10 +91,15 @@ "eslint-plugin-react": "^7.37.3", "eslint-plugin-storybook": "^0.11.2", "express": "^5.1.0", + "happy-dom": "^20.8.4", + "jest-axe": "^10.0.0", + "jsdom": "^29.0.1", "msw": "^2.7.3", "prettier": "^3.4.2", "storybook": "^8.4.7", - "typescript": "5.0.4" + "typescript": "5.0.4", + "vite": "6", + "vitest": "3" }, "msw": { "workerDirectory": [ diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..89b7f0ea --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:8080', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + ], + webServer: { + command: 'yarn dev', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..e5640725 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/src/api/index.ts b/src/api/index.ts index bd46a03a..34f22e5a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,7 +2,9 @@ import { getJWTHeader } from "@/utils/user"; import axios, { AxiosResponse } from "axios"; export const axiosInstance = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || process.env.API_BASE_URL, + baseURL: typeof window === 'undefined' + ? (process.env.API_BASE_URL ?? '') // 서버 사이드: 백엔드 직접 호출 + : '', // 클라이언트 사이드: Next.js 프록시(/api/*)로 중계 timeout: 5000, headers: { "Content-Type": "application/json", @@ -22,7 +24,7 @@ interface ApiResponse { } let retryCount = 0; -const MAX_RETRY_COUNT = 50; +const MAX_RETRY_COUNT = 5; axiosInstance.interceptors.response.use( (response) => response, diff --git a/src/app/globals.css b/src/app/globals.css index 50425cb1..68fe0075 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,35 @@ +@import "tailwindcss"; + +/* Tailwind v4 테마 - palette.ts 기반 */ +@theme { + --font-pretendard: "Pretendard", sans-serif; + --font-mitr: "Mitr", sans-serif; + + /* 브랜드 색상 */ + --color-keycolor: rgba(62, 141, 0, 1); + --color-keycolor-bg: #E3EFD9; + --color-button-active: #F1F7EC; + --color-button-hover: rgba(241, 247, 236, 1); + --color-green-variant: rgba(252, 255, 250, 1); + + /* 텍스트 색상 */ + --color-text-base: rgba(26, 26, 26, 1); + --color-text-muted: rgba(132, 132, 132, 1); + --color-text-muted2: rgba(171, 171, 171, 1); + + /* 배경 / 경계 색상 */ + --color-muted3: rgba(205, 205, 205, 1); + --color-muted4: rgba(240, 240, 240, 1); + --color-muted5: rgba(237, 237, 237, 1); + --color-search-bg: rgba(245, 245, 245, 1); + --color-bg: rgba(253, 253, 253, 1); + + /* 상태 색상 */ + --color-like: #EA2A2A; + --color-error-variant: #FFF7F7; + --color-error-border: #ED1E1E; +} + @import url("https://fonts.googleapis.com/css2?family=Mitr:wght@200;300;400;500;600;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap"); @font-face { @@ -52,163 +84,207 @@ body { backface-visibility: hidden; } -:root { - --dimmed-zindex: 10; - --alert-zindex: 11; -} -a { - color: inherit; - text-decoration: none; -} -html, -body, -div, -span, -applet, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -abbr, -acronym, -address, -big, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -s, -samp, -small, -strike, -strong, -sub, -sup, -tt, -var, -b, -u, -i, -center, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -embed, -figure, -figcaption, -footer, -header, -hgroup, -menu, -nav, -output, -ruby, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} -/* HTML5 display-role reset for older browsers */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} -body { - line-height: 1; - font-family: "Pretendard", sans-serif; - letter-spacing: -0.4px !important; - overflow-x: hidden; -} -ol, -ul { - list-style: none; -} -blockquote, -q { - quotes: none; +@layer base { + :root { + --dimmed-zindex: 10; + --alert-zindex: 11; + } + a { + color: inherit; + text-decoration: none; + } + html, + body, + div, + span, + applet, + object, + iframe, + h1, + h2, + h3, + h4, + h5, + h6, + p, + blockquote, + pre, + a, + abbr, + acronym, + address, + big, + cite, + code, + del, + dfn, + em, + img, + ins, + kbd, + q, + s, + samp, + small, + strike, + strong, + sub, + sup, + tt, + var, + b, + u, + i, + center, + dl, + dt, + dd, + ol, + ul, + li, + fieldset, + form, + label, + legend, + table, + caption, + tbody, + tfoot, + thead, + tr, + th, + td, + article, + aside, + canvas, + details, + embed, + figure, + figcaption, + footer, + header, + hgroup, + menu, + nav, + output, + ruby, + section, + summary, + time, + mark, + audio, + video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + } + /* HTML5 display-role reset for older browsers */ + article, + aside, + details, + figcaption, + figure, + footer, + header, + hgroup, + menu, + nav, + section { + display: block; + } + body { + line-height: 1; + font-family: "Pretendard", sans-serif; + letter-spacing: -0.4px !important; + overflow-x: hidden; + } + ol, + ul { + list-style: none; + } + blockquote, + q { + quotes: none; + } + blockquote:before, + blockquote:after, + q:before, + q:after { + content: ""; + content: none; + } + table { + border-collapse: collapse; + border-spacing: 0; + } + input { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + box-shadow: none; + font-family: "Pretendard", sans-serif; + } + button { + border: none; + margin: 0; + padding: 0; + width: auto; + overflow: visible; + background: transparent; + color: inherit; + font: inherit; + line-height: normal; + } + * { + box-sizing: border-box; + font-family: "Noto Color Emoji", sans-serif; + } } -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ""; - content: none; + +/* Select 컴포넌트 커스텀 스크롤바 */ +.select-scrollbar::-webkit-scrollbar { width: 1px; } +.select-scrollbar::-webkit-scrollbar-track { background: transparent; } +.select-scrollbar::-webkit-scrollbar-thumb { border-radius: 1rem; background: rgba(217, 217, 217, 1); } +.select-scrollbar::-webkit-scrollbar-button { width: 0; height: 0; display: flex; } + +/* Textarea 컴포넌트 커스텀 스크롤바 */ +.textarea-scrollbar::-webkit-scrollbar { width: 3px; } +.textarea-scrollbar::-webkit-scrollbar-track { background: transparent; } +.textarea-scrollbar::-webkit-scrollbar-thumb { border-radius: 1rem; background: rgba(217, 217, 217, 1); } +.textarea-scrollbar::-webkit-scrollbar-button { width: 0; height: 0; } +.textarea-scrollbar::-webkit-scrollbar-button:vertical:start:decrement, +.textarea-scrollbar::-webkit-scrollbar-button:vertical:start:increment { display: block; height: 10px; } +.textarea-scrollbar::-webkit-scrollbar-button:vertical:end:decrement, +.textarea-scrollbar::-webkit-scrollbar-button:vertical:end:increment { display: block; height: 10px; } + +/* StateInputField shake 애니메이션 유틸리티 */ +.animate-shake { + animation: shake 0.3s; } -table { - border-collapse: collapse; - border-spacing: 0; + +/* CodeInput: 값이 있거나 포커스되면 placeholder bar 숨김 */ +.code-input-cell input:not(:placeholder-shown) ~ .input-bar, +.code-input-cell input:focus ~ .input-bar { + display: none; } -input { - appearance: none; - -moz-appearance: none; + +/* CodeInput: 숫자 스피너 제거 */ +.code-number-input::-webkit-outer-spin-button, +.code-number-input::-webkit-inner-spin-button { -webkit-appearance: none; - box-shadow: none; - font-family: "Pretendard", sans-serif; -} -button { - border: none; margin: 0; - padding: 0; - width: auto; - overflow: visible; - background: transparent; - - color: inherit; - font: inherit; - line-height: normal; } -* { - box-sizing: border-box; - font-family: "Noto Color Emoji", sans-serif; +.code-number-input[type=number] { + -moz-appearance: textfield; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } } @keyframes slide-from-right { diff --git a/src/components/Terms.tsx b/src/components/Terms.tsx index 7fd82cd3..0a553ccc 100644 --- a/src/components/Terms.tsx +++ b/src/components/Terms.tsx @@ -84,15 +84,7 @@ const Terms = ({ closeShowTerms }: TermsProps) => { {check.privacy && check.service ? ( - )} - - {text} - {!disabled && nowEnrollmentCount > 0 && hostUserCheck && ( - {nowEnrollmentCount} - )} - - - ); -}; -const AppliedPersonCircle = styled.div` - background-color: ${palette.BG}; - color: ${palette.keycolor}; - padding: 1px 5px; - min-width: 16px; - text-align: center; - height: 16px; - /* padding: 1px 5px 1px 4px; */ - gap: 10px; - border-radius: 20px; - opacity: 0px; - font-size: 12px; - font-weight: 600; - margin-left: 8px; - display: flex; - justify-content: center; - align-items: center; -`; - -const ApplyListButtonWrapper = styled.div` - width: 100%; - display: flex; - justify-content: center; - display: flex; - align-items: center; - gap: 16px; -`; -const ButtonContainer = styled.button<{ disabled: boolean }>` - @media (max-width: 390px) { - width: 100%; - } - @media (min-width: 390px) { - width: 342px; - } - height: 48px; - border-radius: 40px; - cursor: pointer; - justify-content: center; - font-size: 18px; - padding: 10px 20px 10px 20px; - display: flex; - align-items: center; - background-color: ${(props) => props.disabled && "rgba(220, 220, 220, 1)"}; - color: ${(props) => props.disabled && palette.비강조}; - border: none; -`; - -export default ApplyListButton; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/button/ApplyListButton 에 있습니다. +export { default } from '@/shared/ui/button/ApplyListButton'; diff --git a/src/components/designSystem/Buttons/Button.tsx b/src/components/designSystem/Buttons/Button.tsx index 80011e0a..a136255f 100644 --- a/src/components/designSystem/Buttons/Button.tsx +++ b/src/components/designSystem/Buttons/Button.tsx @@ -1,75 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -interface ButtonProps { - text: string; - addStyle?: { - backgroundColor?: string; - color?: string; - boxShadow?: string; - weight?: "regular" | "medium" | "semiBold" | "bold"; - }; - type?: "button" | "reset" | "submit" | undefined; - children?: React.ReactNode; - disabled?: boolean; - onClick?: (event: React.MouseEvent) => void; -} -// 사용 방식 -{ - /* - - {text} - {children} - - + + {children} + ); -}; - -const FilterButtonWrapper = styled.div` - width: 100%; - display: flex; - justify-content: center; - display: flex; - align-items: center; - gap: 16px; -`; -const ButtonContainer = styled.button<{ disabled: boolean }>` - @media (max-width: 390px) { - width: 100%; - } - @media (min-width: 390px) { - width: 342px; - } - - height: 48px; - border-radius: 40px; - cursor: pointer; - justify-content: center; - font-size: 18px; - padding: 10px 20px 10px 20px; - display: flex; - align-items: center; - background-color: ${(props) => props.disabled && "rgba(220, 220, 220, 1)"}; - color: ${(props) => props.disabled && palette.비강조}; - border: none; -`; - -export default FilterButton; +} diff --git a/src/components/designSystem/Buttons/ReportButton.tsx b/src/components/designSystem/Buttons/ReportButton.tsx index 6ef9821a..aa8ddbdf 100644 --- a/src/components/designSystem/Buttons/ReportButton.tsx +++ b/src/components/designSystem/Buttons/ReportButton.tsx @@ -1,63 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React from "react"; - -interface ReportButtonProps { - isOpen: boolean; - - reportClickHandler: (event: React.MouseEvent) => void; - reportText?: string; -} -export default function ReportButton({ - reportClickHandler, - - reportText = "신고하기", - isOpen, -}: ReportButtonProps) { - return ( - - {reportText} - - ); -} - -const BtnBox = styled.div<{ isOpen: boolean }>` - display: flex; - flex-direction: column; - /* justify-content: center; */ - align-items: center; - background-color: #f0f0f0; - border-radius: 20px; - height: 52px; - - transform: ${(props) => - props.isOpen ? "translateY(-5%)" : "translateY(20%)"}; - - transition: transform 0.5s ease; -`; - -const ReportBtn = styled.button` - height: 100%; - cursor: pointer; - @media (max-width: 390px) { - width: 100%; - } - @media (min-width: 390px) { - width: 342px; - } - display: flex; - justify-content: center; - align-items: center; - font-size: 16px; - font-weight: 600; - line-height: 16px; - text-align: center; - color: ${palette.like}; - border: none; - border-radius: 0px 0px 20px 20px; - &:active { - background-color: ${palette.비강조3}; - border-radius: 0px 0px 20px 20px; - } -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/button/ReportButton 에 있습니다. +export { default } from '@/shared/ui/button/ReportButton'; diff --git a/src/components/designSystem/Select.tsx b/src/components/designSystem/Select.tsx index 0bfa865b..a2d74697 100644 --- a/src/components/designSystem/Select.tsx +++ b/src/components/designSystem/Select.tsx @@ -1,269 +1,3 @@ -"use client"; -import styled from "@emotion/styled"; -import { forwardRef, SelectHTMLAttributes, useEffect, useState } from "react"; -import SelectArrow from "../icons/SelectArrow"; -import Spacing from "../Spacing"; -import { palette } from "@/styles/palette"; -import { keyframes } from "@emotion/react"; - -interface SelectProps { - id?: string; - list: string[]; - setValue: (element: string) => void; - value?: string | number; - initOpen?: boolean; - noneValue?: string; - width?: "fit-content" | "100%"; -} - -// none value는 일종의 label값 같은 느낌 -// value가 undefined인 초깃값일 때 보여주기 위한 값 - -const Select = ({ list, id, width = "fit-content", value, initOpen = false, setValue, noneValue }: SelectProps) => { - const [active, setActive] = useState(initOpen); - const [animatedItems, setAnimatedItems] = useState([]); - const changeValue = (element: string) => { - setValue(element); - setActive(false); - }; - - useEffect(() => { - if (active) { - // active가 true일 때 각 항목에 대해 애니메이션 추가 - const timers: NodeJS.Timeout[] = []; - const newAnimatedItems = Array(list.length).fill(false); - - list.forEach((_, index) => { - timers.push( - setTimeout(() => { - newAnimatedItems[index] = true; // 애니메이션 시작 - setAnimatedItems([...newAnimatedItems]); - }, index * 200) // 200ms 간격으로 각 항목 보이기 - ); - }); - - return () => { - timers.forEach((timer) => clearTimeout(timer)); // 클린업 - }; - } else { - setAnimatedItems(Array(list.length).fill(false)); // active가 false일 경우 초기화 - } - }, [active, list]); - return ( - - {active && ( - { - e.preventDefault(); - setActive(false); - }} - /> - )} - - -

{noneValue}
} -
- -
- - - {list.map((element: string, index) => { - return ( - - changeValue(element)} - active={animatedItems[index]} - > - {element} - - - - ); - })} - - - - - ); -}; - -const OptionItem = styled.div` - display: flex; - gap: 7px; - padding: 0 16px; - &:hover { - background-color: ${palette.keycolorBG}; - } -`; - -const Background = styled.div` - pointer-events: auto; - position: fixed; - width: 100%; - height: 100svh; - z-index: 1001; - top: 0; - left: 0; - right: 0; - - transition: 0.2s all ease-in-out; - bottom: 0; - background-color: rgba(26, 26, 26, 0.3); - opacity: 0.8; - @media (min-width: 440px) { - width: 390px; - left: 50%; - height: 100svh; - transform: translateX(-50%); - overflow-x: hidden; - } -`; - -const activeContainer = ({ active = true }) => { - return `${ - active - ? `@media (max-height: 777px) { - // 화면 높이가 많이 작을 때의 드롭다운 높이 설정 - max-height: 160px; - } - @media (min-height: 777px) { - // 화면 높이가 많이 작을 때의 드롭다운 높이 설정 - max-height: 300px; - }` - : `max-height: 40px; - &::-webkit-scrollbar { - display: none; -}` - }`; -}; - -const AllContainer = styled.div``; - -const Container = styled.div<{ width: "fit-content" | "100%" }>` - position: relative; - &::-webkit-scrollbar { - display: none; - } - z-index: 1002; - width: ${(props) => props.width}; - border-radius: 20px; - background-color: ${palette.BG}; - height: auto; - cursor: pointer; -`; - -const OptionList = styled.div<{ active: boolean }>` - border-radius: 20px; - border-bottom-left-radius: ${(props) => (props.active ? "0px" : "20px")}; - border-bottom-right-radius: ${(props) => (props.active ? "0px" : "20px")}; - &::-webkit-scrollbar { - display: none; - } - position: relative; - z-index: 3; - border: 1px solid ${(props) => (props.active ? "none" : palette.비강조3)}; - height: max-content; - min-height: 44px; - ${activeContainer}; - - background-color: white; -`; - -const Label = styled.button` - width: 100%; - font-size: 14px; - font-weight: 400; - color: ${palette.비강조}; - min-width: 95px; - padding: 12px 16px; - border: none; - outline: none; - cursor: pointer; - min-height: 44px; - gap: 10px; - box-sizing: border-box; - display: flex; - justify-content: space-between; - align-items: center; -`; - -const activeExist = ({ active = true }) => { - return `${ - active - ? `@media (max-height: 777px) { - // 화면 높이가 많이 작을 때의 드롭다운 높이 설정 - max-height: 100px; - } - @media (min-height: 777px) { - // 화면 높이가 많이 작을 때의 드롭다운 높이 설정 - max-height: 241px; - }` - : "max-height:0;" - }`; -}; - -const fadeIn = keyframes` - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -`; - -const StyledOptionList = styled.ul<{ active: boolean }>` - list-style-type: none; // ul을 커스텀할 때 필요한 부분. - width: 100%; - position: absolute; - max-height: ${({ active }) => (active ? "170px" : "0")}; - top: 43px; - left: 0; - right: 0; - z-index: 5; - padding-bottom: 4px; - font-size: 16px; - line-height: 20px; - font-weight: 400; - border-radius: 20px; // 동글동글하게 아래부분을 만들어야해서 border-radius를 줌. - border-top-left-radius: 0px; - border-top-right-radius: 0px; - background-color: ${palette.BG}; - overflow-y: auto; // 스크롤이 필요할 때 나타나도록 설정 - - transition: 0.2s max-height ease-in-out; // max-height로 애니메이션 효과 적용 - &::-webkit-scrollbar { - // scrollbar 자체의 설정 - width: 1px; - } - &::-webkit-scrollbar-track { - background: transparent; - } - &::-webkit-scrollbar-thumb { - border-radius: 1rem; - background: rgba(217, 217, 217, 1); - } - &::-webkit-scrollbar-button { - width: 0; - height: 0; - display: flex; - } -`; - -const StyledOptionItem = styled.li<{ active: boolean }>` - padding: 10px 0px; - - opacity: ${({ active }) => (active ? 1 : 0)}; - transform: ${({ active }) => (active ? "scale(100%)" : "scale(0)")}; - font-size: 14px; - width: 100%; - &:nth-child(n) { - transition: - opacity 0.15s cubic-bezier(0.25, 1.5, 0.5, 1), - transform 0.15s cubic-bezier(0.25, 1.5, 0.5, 1); - } - font-weight: 500; -`; - -export default Select; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/select/Select 에 있습니다. +export { default } from '@/shared/ui/select/Select'; diff --git a/src/components/designSystem/input/CodeInput.tsx b/src/components/designSystem/input/CodeInput.tsx index 9b32ec93..51f59a2f 100644 --- a/src/components/designSystem/input/CodeInput.tsx +++ b/src/components/designSystem/input/CodeInput.tsx @@ -1,220 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import { - FocusEvent, - FocusEventHandler, - FormEvent, - KeyboardEvent, - MouseEvent, - RefObject, - useCallback, - useEffect, - useState, -} from "react"; - -interface CodeInputProps extends React.InputHTMLAttributes { - refs: RefObject<(HTMLInputElement | null)[]>; - onValueChange: (values: string[]) => void; -} - -interface ContainerProps { - bgColor: string; - borderColor: string; -} - -const CodeInput = ({ refs, onBlur, onFocus, onValueChange, ...props }: CodeInputProps) => { - const [focused, setFocused] = useState(-1); - const bgColor = focused >= 0 ? palette.greenVariant : props.value === "" ? palette.검색창 : palette.비강조4; - const borderColor = focused >= 0 ? palette.keycolor : bgColor; - - const isValidIndex = (index: number): boolean => { - return index >= 0 && index < 6; - }; - - const updateValues = () => { - if (!refs.current) return; - const newValues = refs.current.map((input) => input?.value || ""); - onValueChange(newValues); - }; - - const handleFocus = useCallback( - (event: FocusEvent, index: number) => { - event.stopPropagation(); - setFocused(index); - - onFocus?.(event); - }, - [focused] - ); - - const handleBlur: FocusEventHandler = (event) => { - setFocused(-1); - onBlur?.(event); - }; - - const handleInput = (e: FormEvent, index: number) => { - if (!refs.current) return; - - const currentInput = refs.current[index]; - if (currentInput?.value && isNaN(parseInt(currentInput?.value))) { - currentInput.value = ""; - return; - } - if (currentInput && currentInput?.value.length > 1) { - currentInput.value = currentInput.value[currentInput?.value.length - 1]; - } - - if (currentInput?.value.length === 1 && isValidIndex(index + 1)) { - refs.current[index + 1]?.focus({ preventScroll: true }); - } - updateValues(); - }; - - const clickContainer = (e: MouseEvent) => { - e.stopPropagation(); - const firstEmptyRef = refs.current?.find((ref) => ref?.value === ""); - if (firstEmptyRef) { - firstEmptyRef.focus({ preventScroll: true }); - } else { - // 빈 값이 없으면 원래 클릭한 input에 포커스 - refs.current[refs.current.length - 1]?.focus({ preventScroll: true }); - } - }; - - const handleKeyDown = (index: number, e: KeyboardEvent) => { - if (!refs.current) return; - - const currentInput = refs.current[index]; - - if (e.key === "Backspace" && currentInput?.value === "") { - e.preventDefault(); - if (isValidIndex(index - 1)) { - const prevInput = refs.current[index - 1]; - if (prevInput) { - prevInput.value = ""; - } - prevInput?.focus({ preventScroll: true }); - } - } - updateValues(); - }; - - const handlePaste = (index: number, e: React.ClipboardEvent) => { - e.preventDefault(); - const pastedData = e.clipboardData.getData("text").slice(0, 6 - index); - - if (!refs.current || !pastedData) return; - - [...pastedData].forEach((char, i) => { - const targetIndex = index + i; - if (isValidIndex(targetIndex)) { - const input = refs.current[targetIndex]; - if (input) { - input.value = char; - } - } - }); - updateValues(); - refs.current[Math.min(pastedData.length + index, 5)]?.focus({ preventScroll: true }); - }; - - return ( - - {[...Array(6)].map((_, index) => ( - - - handleFocus(e, index)} - {...props} - type="number" - id={String(index)} - ref={(el) => { - if (refs.current) { - refs.current[index] = el; - } - }} - onInput={(e) => handleInput(e, index)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={(e) => handlePaste(index, e)} - /> - - - - ))} - - ); -}; - -const Container = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: 100%; - gap: 4px; - height: 48px; - padding: 0px 16px; - border-radius: 50px; - overflow-x: hidden; - box-sizing: border-box; - border: 1px solid ${(props) => props.borderColor}; - background-color: ${(props) => props.bgColor}; -`; - -const InputContainer = styled.div` - width: 40px; - height: 42px; -`; - -const StyledLabel = styled.div` - position: relative; - display: block; - width: 100%; - height: 100%; -`; - -const Bar = styled.div` - width: 16px; - height: 3px; - position: absolute; - transform: translate(-50%, -50%); - top: 50%; - display: block; - left: 50%; - background-color: ${palette.비강조3}; - border-radius: 3px; - z-index: 1; -`; - -const Input = styled.input` - width: 100%; - text-align: center; - height: 100%; - border: none; - font-weight: 600; - outline: none; - display: block; - padding: 0; - font-size: 30px; - line-height: 16px; - color: #000; - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - letter-spacing: -0.025em; - background-color: transparent; - &::placeholder { - opacity: 0; - } - &:not(:placeholder-shown) ~ .input-bar, - &:focus ~ .input-bar { - display: none; - } -`; - -export default CodeInput; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/CodeInput 에 있습니다. +export { default } from '@/shared/ui/input/CodeInput'; diff --git a/src/components/designSystem/input/CommentInput.tsx b/src/components/designSystem/input/CommentInput.tsx index 1bf20f08..c58a3c1a 100644 --- a/src/components/designSystem/input/CommentInput.tsx +++ b/src/components/designSystem/input/CommentInput.tsx @@ -1,82 +1,3 @@ -"use client"; -import UpArrowIcon from "@/components/icons/UpArrowIcon"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import { forwardRef, useEffect, useState } from "react"; - -interface CommentInputProps extends React.TextareaHTMLAttributes { - setReset: () => void; -} - -const CommentInput = forwardRef( - ({ setReset, placeholder, value, onChange }, ref) => { - const [focused, setFocused] = useState(false); - - useEffect(() => { - if (!focused) { - if (value === "") { - setReset(); - } - } - }, [focused, value]); - return ( - - setFocused(true)} - onBlur={() => setFocused(false)} - ref={ref} - placeholder={placeholder} - onChange={onChange} - value={value} - /> - - - ); - } -); - -export default CommentInput; - -const InputContainer = styled.div<{ focused: boolean }>` - width: 100%; - border-radius: 30px; - box-shadow: 0 0 0 1px ${(props) => (props.focused ? palette.keycolor : palette.비강조3)} inset; - display: flex; - background-color: ${(props) => (props.focused ? palette.greenVariant : "white")}; - align-items: center; - - padding: 8px; - min-height: 48px; - max-height: 100px; - height: auto; - box-sizing: border-box; -`; -const Input = styled.textarea` - flex: 1; - width: 100%; - border: none; - outline: none; - background-color: transparent; - height: 32px; - font-size: 16px; - font-weight: 400; - line-height: 22px; - font-family: Pretendard; - padding: 5px 16px; - resize: none; - height: 32px; - wrap: hard; - overflow-y: auto; /* 내용이 넘칠 때 스크롤 생성 */ -`; - -const Button = styled.button<{ canSubmit: boolean }>` - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background-color: ${(props) => (props.canSubmit ? palette.keycolor : palette.비강조3)}; -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/CommentInput 에 있습니다. +export { default } from '@/shared/ui/input/CommentInput'; diff --git a/src/components/designSystem/input/InputField.tsx b/src/components/designSystem/input/InputField.tsx index 0e49baf9..4e409eb1 100644 --- a/src/components/designSystem/input/InputField.tsx +++ b/src/components/designSystem/input/InputField.tsx @@ -1,95 +1,3 @@ -"use client"; -import styled from "@emotion/styled"; -import React, { FocusEventHandler, forwardRef, useState } from "react"; -import RemoveButton from "./RemoveButton"; -import { palette } from "@/styles/palette"; - -// React.InputHTMLAttributes { - handleRemoveValue?: () => void; - icon?: React.ReactNode; - isRemove?: boolean; - isHome?: boolean; -} - -interface ContainerProps { - bgColor: string; - borderColor: string; -} - -// forwardRef : 부모 컴포넌트에서 자식 컴포넌트 안의 DOM element에 접근하고 싶을 때 사용한다. -// 첫번째 generic으로 ref type, 두번째로 지정한 props type -// React.InputHTMLAttributes 이걸 줬기 때문에 input의 프로퍼티들도 props로 내려줄 수 있음 - -// 사용방법 -// 기본적으로 input 사용하듯이 props 사용하면 됨 -// onBlur와 onFocus 함수 추가 가능 -const InputField = forwardRef( - ({ icon, handleRemoveValue = () => {}, isRemove = true, onFocus, onBlur, isHome = false, ...props }, ref) => { - const [focused, setFocused] = useState(false); - - const bgColor = isHome - ? "#fff" - : focused - ? palette.greenVariant - : props.value === "" - ? palette.검색창 - : palette.비강조4; - const borderColor = focused ? palette.keycolor : bgColor; - const handleFocus: FocusEventHandler = (event) => { - setFocused(true); - onFocus?.(event); - }; - - const handleBlur: FocusEventHandler = (event) => { - setFocused(false); - onBlur?.(event); - }; - - return ( - - {icon && {icon}} - -
{props.value === "" ? <> : isRemove && }
-
- ); - } -); - -const Container = styled.div` - display: flex; - align-items: center; - - width: 100%; - height: 48px; - padding: 0px 16px; - border-radius: 50px; - overflow-x: hidden; - box-sizing: border-box; - border: 1px solid ${(props) => props.borderColor}; - background-color: ${(props) => props.bgColor}; -`; - -const IconContainer = styled.div` - margin-right: 11px; -`; - -const Input = styled.input` - flex: 1; - width: 100%; - font-family: "Pretendard"; - &::placeholder { - color: ${palette.비강조2}; - } - height: 100%; - outline: none; - font-weight: 400; - border: none; - background-color: transparent; - font-size: 16px; - letter-spacing: -0.04px; - border: #cdcdcd; -`; - -export default InputField; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/InputField 에 있습니다. +export { default } from '@/shared/ui/input/InputField'; diff --git a/src/components/designSystem/input/RemoveButton.tsx b/src/components/designSystem/input/RemoveButton.tsx index 0d04cfd7..4751d530 100644 --- a/src/components/designSystem/input/RemoveButton.tsx +++ b/src/components/designSystem/input/RemoveButton.tsx @@ -1,27 +1,3 @@ -'use client' -import XIcon from '@/components/icons/XIcon' -import styled from '@emotion/styled' - -const RemoveButton = ({ onClick }: { onClick: () => void }) => { - return ( - - - - ) -} - -const XIconButton = styled.button` - cursor: pointer; - display: inline-block; - border: none; - outline: none; - background-color: transparent; - width: 18px; - height: 18px; - justify-items: center; - align-items: center; -` - -export default RemoveButton +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/RemoveButton 에 있습니다. +export { default } from '@/shared/ui/input/RemoveButton'; diff --git a/src/components/designSystem/input/StateInputField.tsx b/src/components/designSystem/input/StateInputField.tsx index cf3035f7..e2295ba6 100644 --- a/src/components/designSystem/input/StateInputField.tsx +++ b/src/components/designSystem/input/StateInputField.tsx @@ -1,158 +1,3 @@ -"use client"; -import CheckIcon from "@/components/icons/CheckIcon"; -import { css, keyframes } from "@emotion/react"; -import styled from "@emotion/styled"; -import React, { FocusEventHandler, forwardRef, useState } from "react"; -import RemoveButton from "./RemoveButton"; -import { palette } from "@/styles/palette"; - -// React.InputHTMLAttributes { - hasError?: boolean; - success?: boolean; - shake?: boolean; - height?: number; - showSuccessIcon?: boolean; - showIcon?: boolean; - handleRemoveValue: () => void; -} - -interface ContainerProps { - bgColor: string; - shake: boolean; - height: number; - borderColor: string; -} - -interface InputProps { - bgColor: string; -} - -// forwardRef : 부모 컴포넌트에서 자식 컴포넌트 안의 DOM element에 접근하고 싶을 때 사용한다. -// 첫번째 generic으로 ref type, 두번째로 지정한 props type -// React.InputHTMLAttributes 이걸 줬기 때문에 input의 프로퍼티들도 props로 내려줄 수 있음 - -// 사용방법 -// 기본적으로 input 사용하듯이 props 사용하면 됨 -// onBlur와 onFocus 함수 추가 가능 -// hasError: error 상태인지 -// success: 검증을 통과한 상태인지 -// shake: true로 바뀌면 0.3초동안 애니메이션 실행 (처음 true인채로 렌더링 되거나 false에서 true가 되는 순간에만 실행) -// icon: input 내에 icon 컴포넌트 -// showIcon: 우측 상호작용을 보여줄 건지 -// height: InputField의 높이 설정 -const StateInputField = forwardRef( - ( - { - hasError = false, - success = false, - shake = false, - showSuccessIcon = true, - handleRemoveValue, - onFocus, - showIcon = true, - onBlur, - height = 48, - ...props - }, - ref - ) => { - const [focused, setFocused] = useState(false); - - const SuccessIcon = showSuccessIcon ? CheckIcon : React.Fragment; - // 우선순위 1.에러가 있는지? 2. 포커싱 되어있는지 - - const bgColor = hasError - ? palette.errorVariant - : focused - ? palette.greenVariant - : props.value === "" - ? palette.검색창 - : "#F5F5F5"; - const borderColor = hasError ? palette.errorBorder : focused ? palette.keycolor : bgColor; - const handleFocus: FocusEventHandler = (event) => { - setFocused(true); - onFocus?.(event); - }; - - const handleBlur: FocusEventHandler = (event) => { - setFocused(false); - onBlur?.(event); - }; - // margin같은 속성은 유동적으로 나타나는 애들한테 주는게 좋음 - return ( - - -
- {showIcon && - (success ? ( - focused ? ( - - ) : ( - - ) - ) : props.value === "" ? ( - - ) : ( - - ))} -
-
- ); - } -); - -const shake = keyframes` - 0% { transform: translateX(0); } - 25% { transform: translateX(-5px); } - 50% { transform: translateX(5px); } - 75% { transform: translateX(-5px); } - 100% { transform: translateX(0); } -`; - -const Container = styled.div` - display: flex; - align-items: center; - width: 100%; - height: ${(props) => props.height}px; - padding: 0px 16px; - border-radius: 50px; - overflow-x: hidden; - box-sizing: border-box; - - border: 1px solid ${(props) => props.borderColor}; - background-color: ${(props) => props.bgColor}; - animation: ${(props) => - props.shake - ? css` - ${shake} 0.3s - ` - : "none"}; -`; - -const Input = styled.input` - flex: 1; - width: 100%; - &::placeholder { - color: #cdcdcd; - font-weight: 300; - } - font-family: Pretendard; - height: 100%; - outline: none; - font-weight: 400; - border: none; - background-color: ${(props) => props.bgColor}; - font-size: 16px; - letter-spacing: -0.04px; - border: #cdcdcd; -`; -export default StateInputField; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/StateInputField 에 있습니다. +export { default } from '@/shared/ui/input/StateInputField'; diff --git a/src/components/designSystem/input/TextareaField.tsx b/src/components/designSystem/input/TextareaField.tsx index b67dadc2..d2039ab4 100644 --- a/src/components/designSystem/input/TextareaField.tsx +++ b/src/components/designSystem/input/TextareaField.tsx @@ -1,241 +1,3 @@ -"use client"; -import { useTextAreaScroll } from "@/hooks/createTrip/useInputScroll"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import { ChangeEvent, FocusEventHandler, useRef, useState } from "react"; - -interface TextareaFieldProps extends React.InputHTMLAttributes { - height?: number | string; - padding?: string; - fontSize?: string; - lineHeight?: string; - color?: string; - isReport?: boolean; - minRows?: number; - maxRows?: number; - placeholderColor?: string; - isFlexible?: boolean; -} - -const TextareaField = ({ - height = "31svh", - padding = "16px", - fontSize = "16px", - lineHeight = "22px", - minRows, - maxRows, - isFlexible = false, - isReport = false, - placeholderColor = palette.비강조2, - color = palette.기본, - onChange, - ...rest -}: TextareaFieldProps) => { - const textAreaRef = useRef(null); - - const [focused, setFocused] = useState(false); - useTextAreaScroll(textAreaRef); - const borderColor = focused ? palette.keycolor : palette.검색창; - const bgColor = focused ? palette.greenVariant : palette.검색창; - const cloneRef = useRef(null); - const handleFocus: FocusEventHandler = (event) => { - setFocused(true); - }; - - const handleBlur: FocusEventHandler = (event) => { - setFocused(false); - }; - - const handleChange = (e: ChangeEvent) => { - const elem = textAreaRef.current; - const cloneElem = cloneRef.current; - if (!elem || !cloneElem) return; - if (isFlexible && minRows && maxRows) { - cloneElem.value = elem.value; - - elem.rows = Math.min( - Math.max(Math.ceil((cloneElem.scrollHeight - 32) / (cloneElem.clientHeight - 32)), minRows), - maxRows - ); - } - onChange?.(e); - }; - return ( - <> - - - - ); -}; - -const Clone = styled.textarea<{ - height: string | number; - borderColor: string; - bgColor: string; - padding: string; - fontSize: string; - lineHeight: string; - color: string; - isFlexible: boolean; - placeholderColor: string; -}>` - font-family: "Pretendard" !important; - overflow-y: scroll; - width: 100%; - border: 1px solid ${(props) => props.borderColor}; - outline: none; - resize: none; - visibility: hidden; - position: absolute; - top: -9999px; - left: -9999px; - z-index: -1; - width: 100%; - color: ${(props) => props.color}; - height: ${(props) => - props.isFlexible ? "auto" : typeof props.height === "number" ? `${props.height}` : props.height}; - - padding: ${(props) => props.padding}; - font-family: "Pretendard" !important; - background-color: ${(props) => props.bgColor}; - &::placeholder { - color: ${(props) => props.placeholderColor}; - word-break: keep-all; - font-size: ${(props) => props.fontSize}; - font-weight: 400; - line-height: ${(props) => props.lineHeight}; - letter-spacing: -0.025em; - font-family: "Pretendard" !important; - } - font-size: ${(props) => props.fontSize}; - &::-webkit-scrollbar { - // scrollbar 자체의 설정 - // 너비를 작게 설정 - width: 3px; - } - &::-webkit-scrollbar-track { - // scrollbar의 배경부분 설정 - // 부모와 동일하게 함(나중에 절전모드, 밤모드 추가되면 수정하기 번거로우니까... 미리 보이는 노동은 최소화) - background: transparent; - } - &::-webkit-scrollbar-thumb { - // scrollbar의 bar 부분 설정 - // 동글동글한 회색 바를 만든다. - border-radius: 1rem; - - background: rgba(217, 217, 217, 1); - } - &::-webkit-scrollbar-button { - // scrollbar의 상하단 위/아래 이동 버튼 - // 크기를 안줘서 안보이게 함. - width: 0; - height: 0; - } - &::-webkit-scrollbar-button:vertical:start:decrement, - &::-webkit-scrollbar-button:vertical:start:increment { - display: block; - height: 10px; - } - &::-webkit-scrollbar-button:vertical:end:decrement, - &::-webkit-scrollbar-button:vertical:end:increment { - display: block; - height: 10px; - } - line-height: ${(props) => props.lineHeight}; - letter-spacing: -0.025em; - text-align: left; - border-radius: 20px; - border: 0px; - outline: none; - resize: none; -`; - -const DetailTextArea = styled.textarea<{ - height: string | number; - borderColor: string; - bgColor: string; - padding: string; - fontSize: string; - lineHeight: string; - color: string; - isFlexible: boolean; - placeholderColor: string; -}>` - width: 100%; - color: ${(props) => props.color}; - height: ${(props) => - props.isFlexible ? "auto" : typeof props.height === "number" ? `${props.height}` : props.height}; - - padding: ${(props) => props.padding}; - font-family: "Pretendard" !important; - background-color: ${(props) => props.bgColor}; - &::placeholder { - color: ${(props) => props.placeholderColor}; - word-break: keep-all; - font-size: ${(props) => props.fontSize}; - font-weight: 400; - line-height: ${(props) => props.lineHeight}; - letter-spacing: -0.025em; - font-family: "Pretendard" !important; - } - font-size: ${(props) => props.fontSize}; - &::-webkit-scrollbar { - // scrollbar 자체의 설정 - // 너비를 작게 설정 - width: 3px; - } - &::-webkit-scrollbar-track { - // scrollbar의 배경부분 설정 - // 부모와 동일하게 함(나중에 절전모드, 밤모드 추가되면 수정하기 번거로우니까... 미리 보이는 노동은 최소화) - background: transparent; - } - &::-webkit-scrollbar-thumb { - // scrollbar의 bar 부분 설정 - // 동글동글한 회색 바를 만든다. - border-radius: 1rem; - - background: rgba(217, 217, 217, 1); - } - &::-webkit-scrollbar-button { - // scrollbar의 상하단 위/아래 이동 버튼 - // 크기를 안줘서 안보이게 함. - width: 0; - height: 0; - } - &::-webkit-scrollbar-button:vertical:start:decrement, - &::-webkit-scrollbar-button:vertical:start:increment { - display: block; - height: 10px; - } - &::-webkit-scrollbar-button:vertical:end:decrement, - &::-webkit-scrollbar-button:vertical:end:increment { - display: block; - height: 10px; - } - line-height: ${(props) => props.lineHeight}; - letter-spacing: -0.025em; - text-align: left; - border-radius: 20px; - border: 1px solid ${(props) => props.borderColor}; - outline: none; - resize: none; -`; - -export default TextareaField; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/TextareaField 에 있습니다. +export { default } from '@/shared/ui/input/TextareaField'; diff --git a/src/components/designSystem/input/ValidationInputField.tsx b/src/components/designSystem/input/ValidationInputField.tsx index e52678d0..dd1c5736 100644 --- a/src/components/designSystem/input/ValidationInputField.tsx +++ b/src/components/designSystem/input/ValidationInputField.tsx @@ -1,59 +1,3 @@ -'use client' -import Spacing from '@/components/Spacing' -import StateInputField from './StateInputField' -import InfoText from '../text/InfoText' -import React from 'react' - -interface ValidationInputFieldProps { - type: string - name: string - onChange: React.ChangeEventHandler - shake?: boolean - value: string - hasError?: boolean - success?: boolean - placeholder?: string - showSuccess?: boolean - message: string - handleRemoveValue: () => void -} - -export default function ValidationInputField({ - type, - name, - onChange, - value, - handleRemoveValue, - hasError, - success, - showSuccess = false, - placeholder, - shake, - message -}: ValidationInputFieldProps) { - return ( - <> - - -
- {hasError ? ( - {message} - ) : showSuccess && success ? ( - {message} - ) : ( - - )} -
- - ) -} +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/input/ValidationInputField 에 있습니다. +export { default } from '@/shared/ui/input/ValidationInputField'; diff --git a/src/components/designSystem/modal/CheckingModal.tsx b/src/components/designSystem/modal/CheckingModal.tsx index d64b043e..a7672f9d 100644 --- a/src/components/designSystem/modal/CheckingModal.tsx +++ b/src/components/designSystem/modal/CheckingModal.tsx @@ -1,183 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React, { useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -interface CheckingModalProps { - isModalOpen: boolean; - modalMsg: string; - modalTitle: string; - modalButtonText: string; - setModalOpen: - | React.Dispatch> - | ((bool: boolean) => void); - setIsSelected?: React.Dispatch>; - onClick?: () => void; -} -// setIsSelectd : 수락, 거절 등 버튼을 눌렀을 때, 상위 컴포넌트에서 api요청 해줌. -export default function CheckingModal({ - isModalOpen, - modalMsg, - modalTitle, - modalButtonText, - setIsSelected, - setModalOpen, - onClick, -}: CheckingModalProps) { - const modalRef = useRef(null); // 모달 참조 - const handleClickOutside = (e: React.MouseEvent) => { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - setModalOpen(false); // 외부 클릭 시 모달 닫기 - } - }; - - const clickHandler = () => { - if (setIsSelected) { - setIsSelected(true); - } else if (onClick) { - onClick(); - } - - setModalOpen(false); - }; - - if (!isModalOpen) return null; - - return createPortal( - - - e.stopPropagation()} - ref={modalRef} - isModalOpen={isModalOpen} - > - - {modalTitle} - {modalMsg} - - - setModalOpen(false)}>닫기 - {modalButtonText} - - - , - document.getElementById("checking-modal") as HTMLElement - ); -} -const Title = styled.div` - font-size: 20px; - font-weight: 600; - line-height: 23.87px; - text-align: left; - margin-bottom: 8px; - color: ${palette.기본}; -`; -const Msg = styled.div` - font-size: 16px; - font-weight: 400; - line-height: 22.4px; - text-align: center; - margin-left: 32px; - margin-right: 32px; - word-break: keep-all; - color: ${palette.비강조}; - white-space: pre-line; -`; -const ButtonBox = styled.div` - display: flex; - width: 100%; - border-top: 1px solid ${palette.비강조5}; - margin-top: 16px; - height: 48px; -`; -const CloseBtn = styled.button` - font-size: 16px; - font-weight: 400; - line-height: 16px; - text-align: center; - color: ${palette.비강조2}; - display: flex; - cursor: pointer; - justify-content: center; - align-items: center; - width: 50%; - &:active { - background-color: ${palette.buttonActive}; - } -`; -const SelectBtn = styled.button` - font-size: 16px; - font-weight: 600; - line-height: 16px; - cursor: pointer; - text-align: center; - color: ${palette.keycolor}; - display: flex; - justify-content: center; - align-items: center; - width: 50%; - &:active { - background-color: ${palette.buttonActive}; - } -`; -const ModalContainer = styled.div<{ isModalOpen: boolean }>` - height: 100svh; - padding: 0px 45px; - width: 100%; - pointer-events: none; - position: fixed; - top: 0; - left: 0; - z-index: 1001; - display: flex; - align-items: center; - justify-content: center; - white-space: "pre-line"; - visibility: ${({ isModalOpen }) => (isModalOpen ? "visible" : "hidden")}; - opacity: ${({ isModalOpen }) => (isModalOpen ? 1 : 0)}; - transition: - opacity 0.3s ease-in-out, - visibility 0.3s ease-in-out; -`; -const ContentBox = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 76px; -`; -const Modal = styled.div<{ isModalOpen: boolean }>` - width: 300px; - position: absolute; - pointer-events: auto; - padding-top: 24px; - background-color: #ffffff; - z-index: 1003; - - height: 164px; - - gap: 16px; - border-radius: 20px; - opacity: 0px; - transform: ${({ isModalOpen }) => - isModalOpen ? "translateY(0)" : "translateY(30%)"}; - transition: transform 0.3s ease-in-out; -`; -const DarkWrapper = styled.div` - pointer-events: auto; - position: absolute; - width: 100%; - height: 100svh; - z-index: 1001; - top: 0; - bottom: 0; - background-color: rgba(26, 26, 26, 0.3); - opacity: 0.8; - @media (min-width: 440px) { - width: 390px; - left: 50%; - height: 100svh; - transform: translateX(-50%); - overflow-x: hidden; - } -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/modal/CheckingModal 에 있습니다. +export { default } from '@/shared/ui/modal/CheckingModal'; diff --git a/src/components/designSystem/modal/EditAndDeleteModal.tsx b/src/components/designSystem/modal/EditAndDeleteModal.tsx index 05b33213..b3758f41 100644 --- a/src/components/designSystem/modal/EditAndDeleteModal.tsx +++ b/src/components/designSystem/modal/EditAndDeleteModal.tsx @@ -1,129 +1,3 @@ -"use client"; -import styled from "@emotion/styled"; -import React, { SetStateAction, useEffect, useRef, useState } from "react"; -import CloseButton from "../Buttons/CloseButton"; -import EditAndDeleteButton from "../Buttons/EditAndDeleteButton"; -import { createPortal } from "react-dom"; -interface EditAndDeleteModalProps { - isOpen: boolean; - setIsOpen: React.Dispatch>; - setIsEditBtnClicked: React.Dispatch>; - setIsDeleteBtnClicked: React.Dispatch>; - isMyApplyTrip?: boolean; // 내가 참가한 여행에서도 사용하기 위함. - deleteText?: string; -} -export default function EditAndDeleteModal({ - setIsEditBtnClicked, - setIsDeleteBtnClicked, - isOpen, - setIsOpen, - isMyApplyTrip = false, - deleteText = "삭제하기", -}: EditAndDeleteModalProps) { - const modalRef = useRef(null); // 모달 참조 - const [isListening, setIsListening] = useState(false); // 모달 창이 열리고, 이벤트 등록이 동기적으로 일어나도록 제한. - const handleClickOutside = (e: React.MouseEvent) => { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - setIsOpen(false); // 외부 클릭 시 모달 닫기 - } - }; - - const deleteHandler = () => { - setIsDeleteBtnClicked(true); - setIsOpen(false); - }; - const editHandler = () => { - setIsEditBtnClicked(true); - setIsOpen(false); - }; - - useEffect(() => { - if (typeof window !== "undefined") { - setIsListening(true); - } - }, []); - - if (!isListening) return null; - return createPortal( - - 390 ? 390 : window.innerWidth} - > - {!isMyApplyTrip ? ( - - ) : ( - - )} - - - - - - , - document.getElementById("end-modal") as HTMLElement - ); -} - -const Modal = styled.div<{ isOpen: boolean; nowWidth: number }>` - width: ${({ nowWidth }) => `calc(${nowWidth}px - 48px)`}; - position: absolute; - pointer-events: auto; - padding-top: 24px; - z-index: 1003; - bottom: 40px; - gap: 16px; - border-radius: 20px; - opacity: ${({ isOpen }) => (isOpen ? 1 : 0)}; - transform: ${({ isOpen }) => (isOpen ? "translateY(0)" : "translateY(30%)")}; - transition: - transform 0.3s ease-in-out, - opacity 0.3s ease-in-out; -`; -const Container = styled.div<{ isOpen: boolean }>` - height: 100svh; - width: 100%; - - position: fixed; - top: 0; - left: 0; - z-index: 1001; - display: flex; - justify-content: center; - white-space: "pre-line"; - visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")}; - opacity: ${({ isOpen }) => (isOpen ? 1 : 0)}; - transition: - opacity 0.3s ease-in-out, - visibility 0.3s ease-in-out; -`; -const DarkWrapper = styled.div` - pointer-events: auto; - position: absolute; - width: 100%; - height: 100svh; - z-index: 1001; - top: 0; - bottom: 0; - background-color: rgba(26, 26, 26, 0.3); - opacity: 0.8; - @media (min-width: 440px) { - width: 390px; - left: 50%; - height: 100svh; - transform: translateX(-50%); - overflow-x: hidden; - } -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/modal/EditAndDeleteModal 에 있습니다. +export { default } from '@/shared/ui/modal/EditAndDeleteModal'; diff --git a/src/components/designSystem/modal/ImageModal.tsx b/src/components/designSystem/modal/ImageModal.tsx index 3cd3c59a..1087483c 100644 --- a/src/components/designSystem/modal/ImageModal.tsx +++ b/src/components/designSystem/modal/ImageModal.tsx @@ -1,67 +1,3 @@ -"use client"; -import styled from "@emotion/styled"; -import React from "react"; - -interface ImageModalProps { - setModalOpen: React.Dispatch>; - image: string; - count: number; - allCount: number; -} - -const ImageModal = ({ setModalOpen, image, count, allCount }: ImageModalProps) => { - const handleCloseModal = () => { - setModalOpen(false); - }; - return ( - - - 사진{" "} - - {count}/{allCount} - - - {"big - - ); -}; - -const Container = styled.div` - height: 100svh; - position: fixed; - z-index: 9999; - width: 100%; - flex-direction: column; - top: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; - @media (min-width: 440px) { - width: 390px; - left: 50%; - transform: translateX(-50%); - } - - background-color: rgba(0, 0, 0, 0.6); -`; - -const Image = styled.img` - width: 100%; - - max-height: 80vh; - object-fit: contain; - margin: auto; -`; -const TopText = styled.button` - position: absolute; - top: 6.2svh; - left: 50%; - transform: translateX(-50%); - font-size: 16px; - font-weight: 600; - line-height: 20px; - color: white; -`; - -export default ImageModal; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/modal/ImageModal 에 있습니다. +export { default } from '@/shared/ui/modal/ImageModal'; diff --git a/src/components/designSystem/modal/NoticeModal.tsx b/src/components/designSystem/modal/NoticeModal.tsx index cda6cd54..663c9902 100644 --- a/src/components/designSystem/modal/NoticeModal.tsx +++ b/src/components/designSystem/modal/NoticeModal.tsx @@ -1,197 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React, { useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -interface ResultModalProps { - isModalOpen: boolean; - modalMsg: string; - modalTitle: string; - setModalOpen: - | React.Dispatch> - | ((bool: boolean) => void); -} -export default function NoticeModal({ - isModalOpen, - modalMsg, - modalTitle, - setModalOpen, -}: ResultModalProps) { - const modalRef = useRef(null); // 모달 참조 - - const [isListening, setIsListening] = useState(false); // 모달 창이 열리고, 이벤트 등록이 동기적으로 일어나도록 제한. - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - isListening && - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { - setModalOpen(false); // 외부 클릭 시 모달 닫기 - } - }; - - if (isModalOpen) { - // 모달이 열릴 때 이벤트 리스너 등록 - setIsListening(true); - document.addEventListener("click", handleClickOutside); - } else { - setIsListening(false); - } - - // 컴포넌트가 언마운트되거나 모달이 닫힐 때 이벤트 리스너 제거 - return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [isModalOpen, isListening]); // isModalOpen이 변경될 때마다 실행 - - if (!isListening) return null; - return createPortal( - - - -
- - - - - - - - - - - - -
- {modalTitle} - {modalMsg} -
- - setModalOpen(false)}>닫기 - -
- - -
, - document.getElementById("checking-modal") as HTMLElement - ); -} -const Title = styled.div` - font-size: 20px; - font-weight: 600; - line-height: 23.87px; - text-align: left; - margin: 8px 0px; - color: ${palette.기본}; -`; -const Msg = styled.div` - font-size: 16px; - font-weight: 400; - line-height: 22.4px; - text-align: center; - color: ${palette.비강조}; - white-space: pre-line; -`; -const ButtonBox = styled.div` - display: flex; - width: 100%; - border-top: 1px solid ${palette.비강조5}; - margin-top: 16px; - height: 48px; -`; -const CloseBtn = styled.button` - font-size: 16px; - font-weight: 400; - cursor: pointer; - line-height: 16px; - text-align: center; - color: ${palette.비강조2}; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - &:active { - background-color: ${palette.buttonActive}; - } -`; - -const ModalContainer = styled.div<{ isModalOpen: boolean }>` - height: 100svh; - padding: 0px 45px; - width: 100%; - pointer-events: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1005; - display: flex; - align-items: center; - justify-content: center; - white-space: "pre-line"; - visibility: ${({ isModalOpen }) => (isModalOpen ? "visible" : "hidden")}; - opacity: ${({ isModalOpen }) => (isModalOpen ? 1 : 0)}; - transition: - opacity 0.3s ease-in-out, - visibility 0.3s ease-in-out; -`; -const ContentBox = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 108px; -`; -const Modal = styled.div<{ isModalOpen: boolean }>` - width: 300px; - position: absolute; - pointer-events: auto; - padding-top: 24px; - background-color: #ffffff; - z-index: 1003; - - height: 196px; - - gap: 16px; - border-radius: 20px; - opacity: 0px; - transform: ${({ isModalOpen }) => - isModalOpen ? "translateY(0)" : "translateY(30%)"}; - transition: transform 0.3s ease-in-out; -`; -const DarkWrapper = styled.div` - position: absolute; - width: 100%; - height: 100svh; - z-index: 1001; - top: 0; - bottom: 0; - background-color: rgba(26, 26, 26, 0.3); - opacity: 0.8; - @media (min-width: 440px) { - width: 390px; - left: 50%; - height: 100svh; - transform: translateX(-50%); - overflow-x: hidden; - } -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/modal/NoticeModal 에 있습니다. +export { default } from '@/shared/ui/modal/NoticeModal'; diff --git a/src/components/designSystem/modal/ReportModal.tsx b/src/components/designSystem/modal/ReportModal.tsx index 605e62ec..08cb6dc2 100644 --- a/src/components/designSystem/modal/ReportModal.tsx +++ b/src/components/designSystem/modal/ReportModal.tsx @@ -1,104 +1,3 @@ -"use client"; -import styled from "@emotion/styled"; -import React, { SetStateAction, useEffect, useRef, useState } from "react"; -import CloseButton from "../Buttons/CloseButton"; -import ReportButton from "../Buttons/ReportButton"; -import { createPortal } from "react-dom"; -interface ReportModalProps { - isOpen: boolean; - setIsOpen: React.Dispatch>; - setIsReportBtnClicked: React.Dispatch>; - - reportText?: string; -} -export default function ReportModal({ - setIsReportBtnClicked, - isOpen, - setIsOpen, - - reportText = "신고하기", -}: ReportModalProps) { - const modalRef = useRef(null); // 모달 참조 - const [isListening, setIsListening] = useState(false); // 모달 창이 열리고, 이벤트 등록이 동기적으로 일어나도록 제한. - const handleClickOutside = (e: React.MouseEvent) => { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - setIsOpen(false); // 외부 클릭 시 모달 닫기 - } - }; - - const reportHandler = () => { - setIsReportBtnClicked(true); - setIsOpen(false); - }; - useEffect(() => { - if (typeof window !== "undefined") { - setIsListening(true); - } - }, []); - - if (!isListening) return null; - - return createPortal( - - 390 ? 390 : window.innerWidth}> - - - - - - - , - document.getElementById("checking-modal") as HTMLElement - ); -} - -const Modal = styled.div<{ isOpen: boolean; nowWidth: number }>` - width: ${({ nowWidth }) => `calc(${nowWidth}px - 48px)`}; - position: absolute; - pointer-events: auto; - padding-top: 24px; - z-index: 1003; - bottom: 40px; - gap: 16px; - border-radius: 20px; - opacity: ${({ isOpen }) => (isOpen ? 1 : 0)}; - transform: ${({ isOpen }) => (isOpen ? "translateY(0)" : "translateY(30%)")}; - transition: - transform 0.3s ease-in-out, - opacity 0.3s ease-in-out; -`; -const Container = styled.div<{ isOpen: boolean }>` - height: 100svh; - width: 100%; - - position: fixed; - top: 0; - left: 0; - z-index: 1001; - display: flex; - justify-content: center; - white-space: "pre-line"; - visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")}; - opacity: ${({ isOpen }) => (isOpen ? 1 : 0)}; - transition: - opacity 0.3s ease-in-out, - visibility 0.3s ease-in-out; -`; -const DarkWrapper = styled.div` - pointer-events: auto; - position: absolute; - width: 100%; - height: 100svh; - z-index: 1001; - top: 0; - bottom: 0; - background-color: rgba(26, 26, 26, 0.3); - opacity: 0.8; - @media (min-width: 440px) { - width: 390px; - left: 50%; - height: 100svh; - transform: translateX(-50%); - overflow-x: hidden; - } -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/modal/ReportModal 에 있습니다. +export { default } from '@/shared/ui/modal/ReportModal'; diff --git a/src/components/designSystem/modal/ResultModal.tsx b/src/components/designSystem/modal/ResultModal.tsx index 03a65442..208a9b80 100644 --- a/src/components/designSystem/modal/ResultModal.tsx +++ b/src/components/designSystem/modal/ResultModal.tsx @@ -1,171 +1,3 @@ -"use client"; -import CheckIcon from "@/components/icons/CheckIcon"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React, { useEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { styleText } from "util"; -interface ResultModalProps { - isModalOpen: boolean; - modalMsg: string; - modalTitle: string; - setModalOpen: React.Dispatch>; -} -export default function ResultModal({ - isModalOpen, - modalMsg, - modalTitle, - setModalOpen, -}: ResultModalProps) { - const modalRef = useRef(null); // 모달 참조 - - const [isListening, setIsListening] = useState(false); // 모달 창이 열리고, 이벤트 등록이 동기적으로 일어나도록 제한. - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - isListening && - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { - setModalOpen(false); // 외부 클릭 시 모달 닫기 - } - }; - - if (isModalOpen) { - // 모달이 열릴 때 이벤트 리스너 등록 - setIsListening(true); - document.addEventListener("click", handleClickOutside); - } else { - setIsListening(false); - } - - // 컴포넌트가 언마운트되거나 모달이 닫힐 때 이벤트 리스너 제거 - return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [isModalOpen, isListening]); // isModalOpen이 변경될 때마다 실행 - - if (!isModalOpen) return null; - - return createPortal( - - - -
- -
- {modalTitle} - {modalMsg} -
- - setModalOpen(false)}>닫기 - -
- - -
, - document.getElementById("checking-modal") as HTMLElement - ); -} -const Title = styled.div` - font-size: 20px; - font-weight: 600; - line-height: 23.87px; - text-align: left; - margin: 8px 0px; - color: ${palette.기본}; -`; -const Msg = styled.div` - font-size: 16px; - font-weight: 400; - line-height: 22.4px; - text-align: center; - color: ${palette.비강조}; - white-space: pre-line; -`; -const ButtonBox = styled.div` - display: flex; - width: 100%; - border-top: 1px solid ${palette.비강조5}; - margin-top: 16px; - height: 48px; -`; -const CloseBtn = styled.button` - font-size: 16px; - font-weight: 400; - line-height: 16px; - text-align: center; - cursor: pointer; - color: ${palette.비강조2}; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - &:active { - background-color: ${palette.buttonActive}; - } -`; - -const ModalContainer = styled.div<{ isModalOpen: boolean }>` - height: 100svh; - padding: 0px 45px; - width: 100%; - pointer-events: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 1001; - display: flex; - align-items: center; - justify-content: center; - white-space: "pre-line"; - visibility: ${({ isModalOpen }) => (isModalOpen ? "visible" : "hidden")}; - opacity: ${({ isModalOpen }) => (isModalOpen ? 1 : 0)}; - transition: - opacity 0.3s ease-in-out, - visibility 0.3s ease-in-out; -`; -const ContentBox = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 108px; -`; -const Modal = styled.div<{ isModalOpen: boolean }>` - width: 300px; - position: absolute; - pointer-events: auto; - padding-top: 24px; - background-color: #ffffff; - z-index: 1003; - - height: 196px; - - gap: 16px; - border-radius: 20px; - opacity: 0px; - transform: ${({ isModalOpen }) => - isModalOpen ? "translateY(0)" : "translateY(30%)"}; - transition: transform 0.3s ease-in-out; -`; -const DarkWrapper = styled.div` - position: absolute; - width: 100%; - height: 100svh; - z-index: 1001; - top: 0; - bottom: 0; - background-color: rgba(26, 26, 26, 0.3); - opacity: 0.8; - @media (min-width: 440px) { - width: 390px; - left: 50%; - height: 100svh; - transform: translateX(-50%); - overflow-x: hidden; - } -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/modal/ResultModal 에 있습니다. +export { default } from '@/shared/ui/modal/ResultModal'; diff --git a/src/components/designSystem/profile/RoundedImage.tsx b/src/components/designSystem/profile/RoundedImage.tsx index 24d9e746..0ef3fdaf 100644 --- a/src/components/designSystem/profile/RoundedImage.tsx +++ b/src/components/designSystem/profile/RoundedImage.tsx @@ -1,28 +1,3 @@ -'use client' -import styled from '@emotion/styled' - -interface RoundedImageProps { - size: number - src: string -} - -const RoundedImage = ({ size, src }: RoundedImageProps) => { - return ( - - ) -} - -const Image = styled.div<{ size: number; src: string }>` - border-radius: 50%; - height: ${props => props.size}px; - width: ${props => props.size}px; - background-image: url(${props => props.src}); - background-color: ${props => - props.src === '' ? 'rgba(217, 217, 217, 1)' : 'inherit'}; - background-size: cover; -` - -export default RoundedImage +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/profile/RoundedImage 에 있습니다. +export { default } from '@/shared/ui/profile/RoundedImage'; diff --git a/src/components/designSystem/tag/BoxLayoutTag.tsx b/src/components/designSystem/tag/BoxLayoutTag.tsx index b6127131..add89a70 100644 --- a/src/components/designSystem/tag/BoxLayoutTag.tsx +++ b/src/components/designSystem/tag/BoxLayoutTag.tsx @@ -1,80 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import { forwardRef, RefObject } from "react"; - -interface BoxLayoutTagProps { - text: React.ReactNode; - size?: "small" | "medium" | "large"; - - addStyle?: { - backgroundColor?: string; - color?: string; - height?: string; - border?: string; - borderRadius?: string; - padding?: string; - fontSize?: string; - fontWeight?: string; - margin?: string; - }; -} -const BoxLayoutTag = forwardRef( - ( - { - text, - size, - - addStyle = { - backgroundColor: `${palette.비강조4}`, - padding: "4px 10px 4px 10px", - color: `${palette.비강조}`, - borderRadius: "20px", - fontSize: "12px", - fontWeight: "400", - margin: "0 8px 0 0", - }, - }: BoxLayoutTagProps, - ref - ) => { - const style = size - ? size === "large" - ? { - ...addStyle, - padding: "14px 24px", - fontSize: "16px", - height: "48px", - borderRadius: "30px", - } - : size === "medium" - ? { - ...addStyle, - padding: "10px 20px", - fontSize: "16px", - height: "42px", - borderRadius: "30px", - } - : { - ...addStyle, - padding: "8px 14px", - fontSize: "14px", - height: "33px", - borderRadius: "16px", - } - : addStyle; - return ( - - {text} - - ); - } -); - -const Tag = styled.div` - display: flex; - justify-content: center; - align-items: center; - transition: all 0.1s ease; -`; - -export default BoxLayoutTag; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/tag/BoxLayoutTag 에 있습니다. +export { default } from '@/shared/ui/tag/BoxLayoutTag'; diff --git a/src/components/designSystem/tag/SearchFilterTag.tsx b/src/components/designSystem/tag/SearchFilterTag.tsx index e7705819..ab105956 100644 --- a/src/components/designSystem/tag/SearchFilterTag.tsx +++ b/src/components/designSystem/tag/SearchFilterTag.tsx @@ -1,109 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import { usePathname } from "next/navigation"; -import { forwardRef, useEffect, useRef } from "react"; -interface SearchFilterTagProps extends React.ButtonHTMLAttributes { - text: string; - iconPosition?: "start" | "end"; - idx: number; - addStyle?: { - backgroundColor?: string; - color?: string; - border?: string; - borderRadius?: string; - padding?: string; - fontWeight?: string; - lineHeight?: string; - fontSize?: string; - }; - disabled?: boolean; - icon?: React.ReactNode; - - active?: boolean; - onClick?: (event: React.MouseEvent) => void; -} -// 사용 방식 -// idx 는 -// 어느 버튼을 눌렀는지 확인하기 위해 id property에 넣는 값. -// 클릭했을 때, 해당 id를 이용해서, 배열의 해당 인덱스의 active 상태를 표현. active 상태를 나타내는 boolean 배열을 state로사용. -// button 사용하듯이 props 사용 가능. -// active 값으로 바탕색이 초록색이 됨. -{ - /* */ -} - -const SearchFilterTag = forwardRef( - ( - { - text, - idx, - active = false, - onClick, - disabled = false, - icon, - iconPosition = "start", - addStyle = { - backgroundColor: active ? palette.keycolorBG : palette.검색창, - color: active ? palette.keycolor : palette.기본, - border: active ? `1px solid ${palette.keycolor}` : "none", - borderRadius: "16px", - padding: "8px 14px", - fontWeight: "600", - lineHeight: "17px", - fontSize: "14px", - }, - ...props - }, - ref - ) => { - const pathname = usePathname(); - const isCreateTrip = pathname === "/createTripDetail"; - const fixedAddStyle = { - ...addStyle, - border: "none", - boxShadow: addStyle.border === "none" ? "none" : `0 0 0 ${addStyle.border?.replace("solid", "")} inset`, - }; - - return ( - - {iconPosition === "start" && icon} - {text} - {iconPosition === "end" && icon} - - ); - } -); - -const SearchFilterTagContainer = styled.button<{ isCreateTrip: boolean }>` - height: ${(props) => (props.isCreateTrip ? "42px" : "auto")}; - line-height: ${(props) => (props.isCreateTrip ? "22px" : "normal")}; - padding: 8px 14px; - display: flex; - align-items: center; - box-sizing: border-box; - gap: 8px; - cursor: pointer; - transition: - width, - background-color 0.3s ease-in-out; - min-width: fit-content; - white-space: nowrap; - border-radius: 16px; -`; - -const TextContainer = styled.div` - transition: all 0.2s ease-in-out; - overflow: hidden; - text-overflow: ellipsis; -`; -export default SearchFilterTag; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/tag/SearchFilterTag 에 있습니다. +export { default } from '@/shared/ui/tag/SearchFilterTag'; diff --git a/src/components/designSystem/text/InfoText.tsx b/src/components/designSystem/text/InfoText.tsx index 38fcb0a1..d0207d48 100644 --- a/src/components/designSystem/text/InfoText.tsx +++ b/src/components/designSystem/text/InfoText.tsx @@ -1,90 +1,3 @@ -"use client"; -import InfoIcon from "@/components/icons/InfoIcon"; -import { css, keyframes } from "@emotion/react"; -import styled from "@emotion/styled"; - -interface InfoTextProps { - hasError?: boolean; - success?: boolean; - children: React.ReactNode; - shake?: boolean; -} - -interface ContainerProps { - color: string; - shake: boolean; -} - -// 사용법 -// hasError: error 상태인지 -// success: 검증을 통과한 상태인지 -// children: text 부분 -const InfoText = ({ - hasError = false, - success = false, - children, - shake = false, -}: InfoTextProps) => { - const color = hasError ? "#ED1E1E" : success ? "#5DB21B" : "#ABABAB"; - - return ( - - {hasError ? ( - - ) : success ? ( - - - - - - - - - - - - ) : ( - - )} - {children} - - ); -}; - -const shake = keyframes` - 0% { transform: translateX(0); } - 25% { transform: translateX(-5px); } - 50% { transform: translateX(5px); } - 75% { transform: translateX(-5px); } - 100% { transform: translateX(0); } -`; - -const Container = styled.div` - display: flex; - align-items: center; - gap: 7px; - color: ${(props) => props.color}; - font-size: 14px; - line-height: 16px; - font-weight: 400; - animation: ${(props) => - props.shake - ? css` - ${shake} 0.3s - ` - : "none"}; -`; - -export default InfoText; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/text/InfoText 에 있습니다. +export { default } from '@/shared/ui/text/InfoText'; diff --git a/src/components/designSystem/text/TextButton.tsx b/src/components/designSystem/text/TextButton.tsx index 3907231c..97ea955b 100644 --- a/src/components/designSystem/text/TextButton.tsx +++ b/src/components/designSystem/text/TextButton.tsx @@ -1,93 +1,3 @@ -'use client' -import RightVector from '@/components/icons/RightVector' -import { palette } from '@/styles/palette' -import styled from '@emotion/styled' - -interface TextButtonProps { - onClick?: (e:MouseEvent) => void - text: React.ReactNode - isRightVector: boolean - rightText?: string - isLeftVector: boolean - leftIconSrc?: string - titleWeight?: 'regular' | 'semibold' -} - -const TextButton = ({ - onClick, - isRightVector, - text, - rightText = '', - leftIconSrc = '', - isLeftVector, - titleWeight = 'regular' -}: TextButtonProps) => { - return ( - -
- {isLeftVector && ( - icon - )} - {text} -
- - {rightText !== '' && ( - {rightText} - )} - {isRightVector && ( -
- -
- )} -
-
- ) -} - -export default TextButton - -const Box = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - width: 100%; - height: 52px; - box-sizing: border-box; - padding: 14px 8px; - &:hover { - background-color: ${palette.buttonHover}; - } - transition: 0.2s ease-in-out; - opacity: 0px; - &:active { - background-color: ${palette.buttonActive}; - } -` - -const SmallTitle = styled.span<{ fontWeight: 'regular' | 'semibold' }>` - font-family: Pretendard; - font-size: 16px; - font-weight: ${props => (props.fontWeight === 'regular' ? '500' : '600')}; - line-height: 16px; - letter-spacing: -0.25px; - text-align: center; - - color: ${palette.기본}; -` - -const Value = styled.div` - font-size: 16px; - font-weight: 400; - line-height: 16px; - color: ${palette.비강조}; - text-align: center; -` -const Right = styled.div` - display: flex; - justify-content: space-between; - align-items: center; -` +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/text/TextButton 에 있습니다. +export { default } from '@/shared/ui/text/TextButton'; diff --git a/src/components/designSystem/toastMessage/WarningToast.tsx b/src/components/designSystem/toastMessage/WarningToast.tsx index af2a9c22..359fbb9a 100644 --- a/src/components/designSystem/toastMessage/WarningToast.tsx +++ b/src/components/designSystem/toastMessage/WarningToast.tsx @@ -1,79 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React, { useEffect } from "react"; -import { createPortal } from "react-dom"; -interface warningToastProps { - isShow: boolean; - setIsShow: React.Dispatch> | ((bool: boolean) => void); - text: string; - bottom?: string; - height?: number; -} - -export default function WarningToast({ bottom = "20px", isShow, setIsShow, height = 20, text }: warningToastProps) { - // 약 1초 후 다시 메시지가 아래로 내려감. - useEffect(() => { - if (isShow) { - setTimeout(() => { - setIsShow(false); - }, 1500); - } - }, [isShow]); - if (!document || !document.getElementById("result-toast")) return null; - return createPortal( - - - - - - - - - - - - - - - - {text} - - , - document.getElementById("result-toast") as HTMLElement - ); -} -const Container = styled.div<{ isShow: boolean; bottom: string }>` - position: fixed; - width: 100%; - bottom: ${({ isShow, bottom }) => - isShow ? bottom : "-100px"}; /* Toast 위치: 나타날 때는 40px, 사라질 때는 아래로 사라짐 */ - transition: - bottom 0.4s ease-in-out, - opacity 0.4s ease-in-out; - opacity: ${({ isShow }) => (isShow ? 1 : 0)}; /* 나타날 때는 투명도 1, 사라질 때는 0 */ - pointer-events: none; /* Toast는 클릭할 수 없도록함 */ - display: flex; - justify-content: center; - left: 0; - z-index: 4000; -`; -const ToastMsg = styled.div<{ height: number }>` - position: absolute; - bottom: ${(props: { height: number }) => props.height}px; - height: 42px; - border-radius: 20px; - background-color: ${palette.keycolor}; - padding: 10px 16px; - display: flex; - justify-content: center; - align-items: center; -`; -const Text = styled.div` - color: white; - font-size: 16px; - font-weight: 400; - line-height: 22.4px; - text-align: left; - margin-left: 8px; -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/toast/WarningToast 에 있습니다. +export { default } from '@/shared/ui/toast/WarningToast'; diff --git a/src/components/designSystem/toastMessage/errorToast.tsx b/src/components/designSystem/toastMessage/errorToast.tsx index 96e42f6c..cb4b6bc1 100644 --- a/src/components/designSystem/toastMessage/errorToast.tsx +++ b/src/components/designSystem/toastMessage/errorToast.tsx @@ -1,68 +1,23 @@ -'use client' -import Warning from '@/components/icons/Warning' -import { errorStore } from '@/store/client/errorStore' -import { errorToastUI } from '@/store/client/toastUI' -import { palette } from '@/styles/palette' -import styled from '@emotion/styled' -import React, { useEffect } from 'react' +'use client'; +// 스토어 연결 어댑터: shared/ui/toast/ErrorToast (순수 컴포넌트)를 감싸 +// FSD 원칙상 shared는 store를 알 수 없으므로, store 연결은 이 레이어에서 담당. +// ErrorCatcher 등 기존 사용처는 변경 없이 유지됨. +import ErrorToast from '@/shared/ui/toast/ErrorToast'; +import { errorStore } from '@/store/client/errorStore'; +import { errorToastUI } from '@/store/client/toastUI'; + +export default function ErrorToastAdapter() { + const { errorToastShow, setErrorToastShow } = errorToastUI(); + const { error, setIsMutationError } = errorStore(); -export default function ErrorToast() { - const { errorToastShow, setErrorToastShow } = errorToastUI() - const { error, setIsMutationError } = errorStore() - // 1초 후 다시 메시지가 아래로 내려감. - useEffect(() => { - if (errorToastShow) { - setTimeout(() => { - setErrorToastShow(false) - setIsMutationError(false) - }, 1500) - } - }, [errorToastShow]) - const noPageError = error?.message.includes('404')! return ( - - - - {error?.message} - {/* 문제가 발생했습니다. */} - - - ) + { + setErrorToastShow(false); + setIsMutationError(false); + }} + /> + ); } -const Container = styled.div<{ isShow: boolean }>` - position: fixed; - width: 100%; - bottom: ${({ isShow }) => - isShow - ? '250px' - : '-100px'}; /* Toast 위치: 나타날 때는 40px, 사라질 때는 아래로 사라짐 */ - transition: - bottom 0.4s ease-in-out, - opacity 0.4s ease-in-out; - opacity: ${({ isShow }) => - isShow ? 1 : 0}; /* 나타날 때는 투명도 1, 사라질 때는 0 */ - pointer-events: none; /* Toast는 클릭할 수 없도록함 */ - display: flex; - justify-content: center; - left: 0; - z-index: 1000; -` -const ToastMsg = styled.div<{ height: number }>` - position: absolute; - bottom: ${(props: { height: number }) => props.height}px; - height: 42px; - border-radius: 20px; - background-color: ${palette.기본}; - padding: 10px 16px; - display: flex; - justify-content: center; - align-items: center; -` -const Text = styled.div` - color: white; - font-size: 16px; - font-weight: 400; - line-height: 22.4px; - text-align: left; - margin-left: 8px; -` diff --git a/src/components/designSystem/toastMessage/resultToast.tsx b/src/components/designSystem/toastMessage/resultToast.tsx index 5a6bd4fb..af70c698 100644 --- a/src/components/designSystem/toastMessage/resultToast.tsx +++ b/src/components/designSystem/toastMessage/resultToast.tsx @@ -1,77 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React, { useEffect } from "react"; -import { createPortal } from "react-dom"; -interface resultToastProps { - isShow: boolean; - setIsShow: React.Dispatch> | ((bool: boolean) => void); - text: string; - bottom?: string; - height?: number; -} - -export default function ResultToast({ bottom = "20px", isShow, setIsShow, height = 20, text }: resultToastProps) { - // 1초 후 다시 메시지가 아래로 내려감. - useEffect(() => { - if (isShow) { - setTimeout(() => { - setIsShow(false); - }, 1500); - } - }, [isShow]); - if (!document || !document.getElementById("result-toast")) return null; - return createPortal( - - - - - - - - {text} - - , - document.getElementById("result-toast") as HTMLElement - ); -} -const Container = styled.div<{ isShow: boolean; bottom: string }>` - position: fixed; - width: 100%; - bottom: ${({ isShow, bottom }) => - isShow ? bottom : "-100px"}; /* Toast 위치: 나타날 때는 40px, 사라질 때는 아래로 사라짐 */ - transition: - bottom 0.4s ease-in-out, - opacity 0.4s ease-in-out; - opacity: ${({ isShow }) => (isShow ? 1 : 0)}; /* 나타날 때는 투명도 1, 사라질 때는 0 */ - pointer-events: none; /* Toast는 클릭할 수 없도록함 */ - display: flex; - justify-content: center; - left: 0; - z-index: 4000; -`; -const ToastMsg = styled.div<{ height: number }>` - position: absolute; - bottom: ${(props: { height: number }) => props.height}px; - height: 42px; - border-radius: 20px; - background-color: ${palette.keycolor}; - padding: 10px 16px; - display: flex; - justify-content: center; - align-items: center; -`; -const Text = styled.div` - color: white; - font-size: 16px; - font-weight: 400; - line-height: 22.4px; - text-align: left; - margin-left: 8px; -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/toast/ResultToast 에 있습니다. +export { default } from '@/shared/ui/toast/ResultToast'; diff --git a/src/components/designSystem/toastMessage/tripToast.tsx b/src/components/designSystem/toastMessage/tripToast.tsx index 34d448f9..4a0d5896 100644 --- a/src/components/designSystem/toastMessage/tripToast.tsx +++ b/src/components/designSystem/toastMessage/tripToast.tsx @@ -1,72 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import React, { useEffect } from "react"; -import { createPortal } from "react-dom"; -interface tripToastProps { - isShow: boolean; - setIsMapFull: React.Dispatch> | ((bool: boolean) => void); - setModalHeight: React.Dispatch> | ((number: number) => void); - bottom?: string; - height?: number; -} - -export default function TripToast({ - setIsMapFull, - setModalHeight, - bottom = "120px", - isShow, - height = 36, -}: tripToastProps) { - if (!document?.getElementById("trip-toast")) return null; - return createPortal( - - { - setModalHeight(0); - setIsMapFull(true); - }} - height={height} - > - ✨ 여행 일정을 추가해 보세요 - - - - - , - document?.getElementById("trip-toast") as HTMLElement - ); -} -const Container = styled.div<{ isShow: boolean; bottom: string }>` - position: fixed; - width: 100%; - bottom: ${({ isShow, bottom }) => - isShow ? bottom : "-100px"}; /* Toast 위치: 나타날 때는 40px, 사라질 때는 아래로 사라짐 */ - transition: opacity 0.4s ease-in-out; - opacity: ${({ isShow }) => (isShow ? 1 : 0)}; /* 나타날 때는 투명도 1, 사라질 때는 0 */ - pointer-events: none; /* Toast는 클릭할 수 없도록함 */ - display: flex; - justify-content: center; - left: 0; - z-index: 100; -`; -const ToastMsg = styled.div<{ height: number }>` - position: absolute; - bottom: 0; - height: 36px; - border-radius: 30px; - background-color: ${palette.기본}; - padding: 8px 16px; - display: flex; - pointer-events: auto; - justify-content: center; - align-items: center; -`; -const Text = styled.div` - color: white; - font-size: 14px; - font-weight: 400; - line-height: 20px; - text-align: left; - margin-right: 8px; -`; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/toast/TripToast 에 있습니다. +export { default } from '@/shared/ui/toast/TripToast'; diff --git a/src/entities/.gitkeep b/src/entities/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/entities/index.ts b/src/entities/index.ts new file mode 100644 index 00000000..a685c775 --- /dev/null +++ b/src/entities/index.ts @@ -0,0 +1,2 @@ +// FSD entities layer +// 도메인 모델 (trip, user, comment, community 등) diff --git a/src/features/.gitkeep b/src/features/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/features/index.ts b/src/features/index.ts new file mode 100644 index 00000000..19351217 --- /dev/null +++ b/src/features/index.ts @@ -0,0 +1,2 @@ +// FSD features layer +// 사용자 시나리오 단위 기능 (여행 생성, 검색, 북마크 등) diff --git a/src/mocks/http.ts b/src/mocks/http.ts index dced2107..a18e76d7 100644 --- a/src/mocks/http.ts +++ b/src/mocks/http.ts @@ -6,7 +6,7 @@ import { handlers } from "./handler"; const app = express(); const port = 9090; // 서버 포트 -app.use(cors({ origin: "http://localhost:9999", optionsSuccessStatus: 200, credentials: true })); +app.use(cors({ origin: "http://localhost:8080", optionsSuccessStatus: 200, credentials: true })); app.use(express.json()); app.use(createMiddleware(...handlers)); app.listen(port, () => console.log(`Mock server is running on port: ${port}`)); diff --git a/src/page/MyPage/MyPage.tsx b/src/page/MyPage/MyPage.tsx index e8dd159c..6cb13d11 100644 --- a/src/page/MyPage/MyPage.tsx +++ b/src/page/MyPage/MyPage.tsx @@ -93,7 +93,7 @@ export default function MyPage() { leftIconSrc="/images/createTripBtn.png" /> { + onClick={() => { router.push("/contact"); }} text="1:1 문의하기" diff --git a/src/shared/api/.gitkeep b/src/shared/api/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/constants/.gitkeep b/src/shared/constants/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/hooks/.gitkeep b/src/shared/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/index.ts b/src/shared/index.ts new file mode 100644 index 00000000..a0deec70 --- /dev/null +++ b/src/shared/index.ts @@ -0,0 +1,2 @@ +// FSD shared layer +// 공통 재사용 요소 - ui, api, hooks, lib, constants, types diff --git a/src/shared/lib/.gitkeep b/src/shared/lib/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/types/.gitkeep b/src/shared/types/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/ui/.gitkeep b/src/shared/ui/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/shared/ui/badge/Badge.test.tsx b/src/shared/ui/badge/Badge.test.tsx new file mode 100644 index 00000000..3396b524 --- /dev/null +++ b/src/shared/ui/badge/Badge.test.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import Badge from './Badge'; + +describe('Badge', () => { + it('text를 렌더링한다', () => { + render(); + expect(screen.getByText('여행')).toBeInTheDocument(); + }); + + it('isDueDate=true이면 D-N 형식으로 렌더링된다', () => { + render(); + expect(screen.getByText(/D-3/)).toBeInTheDocument(); + }); + + it('isClose=true이면 마감을 렌더링한다', () => { + render(); + expect(screen.getByText('마감')).toBeInTheDocument(); + }); + + it('isClose=true일 때 D-N을 렌더링하지 않는다', () => { + render(); + expect(screen.queryByText(/D-3/)).not.toBeInTheDocument(); + }); +}); diff --git a/src/shared/ui/badge/Badge.tsx b/src/shared/ui/badge/Badge.tsx new file mode 100644 index 00000000..aaebb71a --- /dev/null +++ b/src/shared/ui/badge/Badge.tsx @@ -0,0 +1,60 @@ +'use client'; + +interface BadgeProps { + daysLeft?: number; + text: React.ReactNode; + isDueDate?: boolean; + width?: string; + backgroundColor?: string; + color?: string; + borderRadius?: string; + height?: string; + fontWeight?: string; + isClose?: boolean; + padding?: string; +} + +/** + * D-day 및 레이블 배지 컴포넌트. + * 기존 isDueDate 분기로 인한 중복 렌더링을 단일 JSX로 통합. + */ +const Badge = ({ + daysLeft, + text, + isClose = false, + isDueDate = true, + width = 'max-content', + backgroundColor = 'rgba(62, 141, 0, 1)', + color = 'rgba(255, 255, 255, 1)', + borderRadius = '20px', + height = '23px', + fontWeight = '700', +}: BadgeProps) => { + const bgColor = isClose ? 'rgba(171, 171, 171, 1)' : backgroundColor; + const textColor = isClose ? 'white' : color; + + const content = isDueDate + ? isClose + ? '마감' + : <>{text} D-{daysLeft} + : text; + + return ( +
+ {content} +
+ ); +}; + +export default Badge; diff --git a/src/shared/ui/badge/index.ts b/src/shared/ui/badge/index.ts new file mode 100644 index 00000000..a4b8b972 --- /dev/null +++ b/src/shared/ui/badge/index.ts @@ -0,0 +1 @@ +export { default as Badge } from './Badge'; diff --git a/src/shared/ui/button/ApplyListButton.tsx b/src/shared/ui/button/ApplyListButton.tsx new file mode 100644 index 00000000..f7abb42e --- /dev/null +++ b/src/shared/ui/button/ApplyListButton.tsx @@ -0,0 +1,78 @@ +'use client'; + +import EmptyHeartIcon from '@/components/icons/EmptyHeartIcon'; +import FullHeartIcon from '@/components/icons/FullHeartIcon'; +import Button from './Button'; + +interface ApplyListButtonProps { + nowEnrollmentCount: number; + text: string; + type?: 'button' | 'reset' | 'submit'; + children?: React.ReactNode; + disabled?: boolean; + bookmarked: boolean; + hostUserCheck: boolean; + style?: React.CSSProperties; + /** @deprecated style 사용 권장 */ + addStyle?: React.CSSProperties; + onClick?: (event: React.MouseEvent) => void; + bookmarkOnClick?: (event: React.MouseEvent) => void; +} + +/** + * 신청 목록 버튼 (북마크 아이콘 + 메인 버튼). + * Button 컴포넌트를 내부에서 재사용. + */ +const ApplyListButton = ({ + text = '다음', + type = 'submit', + disabled = false, + children, + style, + addStyle, + onClick, + bookmarkOnClick, + nowEnrollmentCount, + bookmarked = false, + hostUserCheck, +}: ApplyListButtonProps) => { + return ( +
+ {!hostUserCheck && ( + + )} + +
+ ); +}; + +export default ApplyListButton; diff --git a/src/shared/ui/button/Button.test.tsx b/src/shared/ui/button/Button.test.tsx new file mode 100644 index 00000000..41ce3b0e --- /dev/null +++ b/src/shared/ui/button/Button.test.tsx @@ -0,0 +1,42 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Button from './Button'; + +describe('Button', () => { + it('텍스트를 렌더링한다', () => { + render(); + expect(screen.getByTestId('child')).toBeInTheDocument(); + }); +}); diff --git a/src/shared/ui/button/Button.tsx b/src/shared/ui/button/Button.tsx new file mode 100644 index 00000000..edb26c52 --- /dev/null +++ b/src/shared/ui/button/Button.tsx @@ -0,0 +1,55 @@ +'use client'; + +interface ButtonProps { + text: string; + type?: 'button' | 'reset' | 'submit'; + children?: React.ReactNode; + disabled?: boolean; + style?: React.CSSProperties; + /** @deprecated style 사용 권장 */ + addStyle?: React.CSSProperties; + className?: string; + onClick?: (event: React.MouseEvent) => void; +} + +/** + * 기본 버튼 컴포넌트. + * FilterButton, ApplyListButton은 이 컴포넌트를 내부적으로 조합합니다. + */ +const Button = ({ + text = '다음', + type = 'submit', + disabled = false, + children, + style, + addStyle, + className = '', + onClick, +}: ButtonProps) => { + return ( + + ); +}; + +export default Button; diff --git a/src/shared/ui/button/CloseButton.test.tsx b/src/shared/ui/button/CloseButton.test.tsx new file mode 100644 index 00000000..9a3ea47d --- /dev/null +++ b/src/shared/ui/button/CloseButton.test.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { axe } from 'jest-axe'; +import CloseButton from './CloseButton'; + +describe('CloseButton', () => { + it('닫기 버튼이 렌더링된다', () => { + render( {}} />); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('onClick 핸들러가 호출된다', async () => { + const handleClick = vi.fn(); + render(); + await userEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + // Phase 1.5: 접근성 + it('기본 aria-label이 text prop과 동일하다', () => { + render( {}} />); + expect(screen.getByRole('button', { name: '닫기' })).toBeInTheDocument(); + }); + + it('aria-label prop을 통해 접근성 이름을 커스텀할 수 있다', () => { + render( {}} aria-label="모달 닫기" text="닫기" />); + expect(screen.getByRole('button', { name: '모달 닫기' })).toBeInTheDocument(); + }); + + it('접근성 위반이 없어야 한다', async () => { + const { container } = render( {}} />); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/src/shared/ui/button/CloseButton.tsx b/src/shared/ui/button/CloseButton.tsx new file mode 100644 index 00000000..12d827fb --- /dev/null +++ b/src/shared/ui/button/CloseButton.tsx @@ -0,0 +1,36 @@ +'use client'; + +interface CloseButtonProps { + onClick: () => void; + text?: string; + 'aria-label'?: string; +} + +/** + * 닫기 버튼. + * 기존 API(setIsOpen)에서 onClick으로 변경하여 재사용성 향상. + * + * Accessibility notes (Phase 1.5): + * - aria-label 기본값을 text prop과 동일하게 설정 (아이콘 전용 버전 대비) + */ +const CloseButton = ({ onClick, text = '닫기', 'aria-label': ariaLabel }: CloseButtonProps) => { + return ( + + ); +}; + +export default CloseButton; diff --git a/src/shared/ui/button/EditAndDeleteButton.test.tsx b/src/shared/ui/button/EditAndDeleteButton.test.tsx new file mode 100644 index 00000000..22d39dc2 --- /dev/null +++ b/src/shared/ui/button/EditAndDeleteButton.test.tsx @@ -0,0 +1,46 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import EditAndDeleteButton from './EditAndDeleteButton'; + +describe('EditAndDeleteButton', () => { + const defaultProps = { + isOpen: true, + editClickHandler: vi.fn(), + deleteClickHandler: vi.fn(), + }; + + it('수정하기 버튼이 렌더링된다', () => { + render(); + expect(screen.getByText('수정하기')).toBeInTheDocument(); + }); + + it('삭제하기 버튼이 렌더링된다', () => { + render(); + expect(screen.getByText('삭제하기')).toBeInTheDocument(); + }); + + it('deleteText prop으로 삭제 버튼 텍스트를 변경할 수 있다', () => { + render(); + expect(screen.getByText('참가 취소')).toBeInTheDocument(); + }); + + it('isMyApplyTrip이 true이면 수정 버튼이 숨겨진다', () => { + render(); + expect(screen.queryByText('수정하기')).not.toBeInTheDocument(); + }); + + it('editClickHandler가 호출된다', async () => { + const editClickHandler = vi.fn(); + render(); + await userEvent.click(screen.getByText('수정하기')); + expect(editClickHandler).toHaveBeenCalledTimes(1); + }); + + it('deleteClickHandler가 호출된다', async () => { + const deleteClickHandler = vi.fn(); + render(); + await userEvent.click(screen.getByText('삭제하기')); + expect(deleteClickHandler).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/ui/button/EditAndDeleteButton.tsx b/src/shared/ui/button/EditAndDeleteButton.tsx new file mode 100644 index 00000000..0bf91bcb --- /dev/null +++ b/src/shared/ui/button/EditAndDeleteButton.tsx @@ -0,0 +1,63 @@ +'use client'; + +interface EditAndDeleteButtonProps { + isOpen: boolean; + isMyApplyTrip?: boolean; + editClickHandler: (event: React.MouseEvent) => void; + deleteClickHandler: (event: React.MouseEvent) => void; + deleteText?: string; +} + +/** + * 수정/삭제 팝업 버튼. + * isMyApplyTrip=true이면 수정 버튼 없이 단일 액션 버튼으로 동작. + */ +const EditAndDeleteButton = ({ + isOpen, + isMyApplyTrip = false, + editClickHandler, + deleteClickHandler, + deleteText = '삭제하기', +}: EditAndDeleteButtonProps) => { + return ( +
+ {!isMyApplyTrip && ( + + )} + +
+ ); +}; + +export default EditAndDeleteButton; diff --git a/src/shared/ui/button/FilterButton.test.tsx b/src/shared/ui/button/FilterButton.test.tsx new file mode 100644 index 00000000..57c530f5 --- /dev/null +++ b/src/shared/ui/button/FilterButton.test.tsx @@ -0,0 +1,35 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import FilterButton from './FilterButton'; + +describe('FilterButton', () => { + it('텍스트를 렌더링한다', () => { + render(); + expect(screen.getByText('필터 적용')).toBeInTheDocument(); + }); + + it('리셋 버튼이 렌더링된다', () => { + render(); + // 리셋 버튼(ResetIcon)과 필터 버튼 2개 + expect(screen.getAllByRole('button')).toHaveLength(2); + }); + + it('onClick 핸들러가 호출된다', async () => { + const handleClick = vi.fn(); + render(); + const buttons = screen.getAllByRole('button'); + // 두 번째 버튼이 메인 필터 버튼 + await userEvent.click(buttons[1]); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('initializeOnClick 핸들러가 호출된다', async () => { + const handleInitialize = vi.fn(); + render(); + const buttons = screen.getAllByRole('button'); + // 첫 번째 버튼이 리셋 버튼 + await userEvent.click(buttons[0]); + expect(handleInitialize).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/ui/button/FilterButton.tsx b/src/shared/ui/button/FilterButton.tsx new file mode 100644 index 00000000..a068b1c0 --- /dev/null +++ b/src/shared/ui/button/FilterButton.tsx @@ -0,0 +1,52 @@ +'use client'; + +import ResetIcon from '@/components/icons/ResetIcon'; +import Button from './Button'; + +interface FilterButtonProps { + text: string; + type?: 'button' | 'reset' | 'submit'; + children?: React.ReactNode; + disabled?: boolean; + style?: React.CSSProperties; + onClick?: (event: React.MouseEvent) => void; + initializeOnClick?: (event: React.MouseEvent) => void; +} + +/** + * 필터 버튼 (리셋 아이콘 + 메인 버튼). + * Button 컴포넌트를 내부에서 재사용. + * 기존 오타 intializeOnClick → initializeOnClick 수정. + */ +const FilterButton = ({ + text = '다음', + type = 'submit', + disabled = false, + children, + style, + onClick, + initializeOnClick, +}: FilterButtonProps) => { + return ( +
+ + +
+ ); +}; + +export default FilterButton; diff --git a/src/shared/ui/button/ReportButton.tsx b/src/shared/ui/button/ReportButton.tsx new file mode 100644 index 00000000..208e6954 --- /dev/null +++ b/src/shared/ui/button/ReportButton.tsx @@ -0,0 +1,38 @@ +'use client'; + +interface ReportButtonProps { + isOpen: boolean; + reportClickHandler: (event: React.MouseEvent) => void; + reportText?: string; +} + +const ReportButton = ({ + isOpen, + reportClickHandler, + reportText = '신고하기', +}: ReportButtonProps) => { + return ( +
+ +
+ ); +}; + +export default ReportButton; diff --git a/src/shared/ui/button/index.ts b/src/shared/ui/button/index.ts new file mode 100644 index 00000000..d8e862ab --- /dev/null +++ b/src/shared/ui/button/index.ts @@ -0,0 +1,6 @@ +export { default as Button } from './Button'; +export { default as CloseButton } from './CloseButton'; +export { default as FilterButton } from './FilterButton'; +export { default as EditAndDeleteButton } from './EditAndDeleteButton'; +export { default as ReportButton } from './ReportButton'; +export { default as ApplyListButton } from './ApplyListButton'; diff --git a/src/shared/ui/input/CodeInput.test.tsx b/src/shared/ui/input/CodeInput.test.tsx new file mode 100644 index 00000000..babc1575 --- /dev/null +++ b/src/shared/ui/input/CodeInput.test.tsx @@ -0,0 +1,58 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import React from 'react'; +import CodeInput from './CodeInput'; + +describe('CodeInput', () => { + const createRefs = () => { + const refs = React.createRef<(HTMLInputElement | null)[]>() as React.MutableRefObject<(HTMLInputElement | null)[]>; + refs.current = Array(6).fill(null); + return refs; + }; + + it('renders 6 input cells', () => { + const refs = createRefs(); + render( {}} />); + const inputs = screen.getAllByRole('spinbutton'); // type="number" inputs + expect(inputs).toHaveLength(6); + }); + + it('calls onValueChange when input value changes', () => { + const refs = createRefs(); + const onValueChange = vi.fn(); + render(); + const inputs = screen.getAllByRole('spinbutton'); + fireEvent.input(inputs[0], { target: { value: '1' } }); + expect(onValueChange).toHaveBeenCalled(); + }); + + it('ignores non-numeric input', () => { + const refs = createRefs(); + const onValueChange = vi.fn(); + render(); + const inputs = screen.getAllByRole('spinbutton'); + // Simulate non-numeric input + fireEvent.input(inputs[0], { target: { value: 'a' } }); + // After input handler runs, value should be cleared + // (DOM manipulation done directly on the ref, so we check via refs) + // The handler calls updateValues regardless + expect(onValueChange).toHaveBeenCalled(); + }); + + it('shows a bar placeholder in each cell', () => { + const refs = createRefs(); + render( {}} />); + // Each cell has an .input-bar element + const bars = document.querySelectorAll('.input-bar'); + expect(bars).toHaveLength(6); + }); + + // Phase 1.5: 접근성 + it('각 셀에 "n번째 숫자" aria-label이 있다', () => { + const refs = createRefs(); + render( {}} />); + for (let i = 1; i <= 6; i++) { + expect(screen.getByLabelText(`${i}번째 숫자`)).toBeInTheDocument(); + } + }); +}); diff --git a/src/shared/ui/input/CodeInput.tsx b/src/shared/ui/input/CodeInput.tsx new file mode 100644 index 00000000..cdab1d28 --- /dev/null +++ b/src/shared/ui/input/CodeInput.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { + FocusEvent, + FocusEventHandler, + FormEvent, + KeyboardEvent, + MouseEvent, + RefObject, + useCallback, + useState, +} from 'react'; + +interface CodeInputProps extends React.InputHTMLAttributes { + refs: RefObject<(HTMLInputElement | null)[]>; + onValueChange: (values: string[]) => void; +} + +/** + * 6자리 인증 코드 입력 컴포넌트 (OTP 스타일). + * - 숫자만 입력 가능 + * - 각 셀에 입력 시 다음 셀로 자동 포커스 + * - Backspace로 이전 셀로 이동 및 삭제 + * - 붙여넣기 지원 + * - 빈 셀에는 bar placeholder 표시 + * + * Refactoring notes: + * - Emotion → Tailwind (레이아웃/구조) + inline style (상태 기반 색상) + * - input-bar 숨김 CSS → globals.css .code-input-cell 클래스로 분리 + * - 숫자 스피너 제거 → globals.css .code-number-input 클래스 + */ +const CodeInput = ({ refs, onBlur, onFocus, onValueChange, ...props }: CodeInputProps) => { + const [focused, setFocused] = useState(-1); + + const bgColor = focused >= 0 + ? 'rgba(252, 255, 250, 1)' // palette.greenVariant + : props.value === '' + ? 'rgba(245, 245, 245, 1)' // palette.검색창 + : 'rgba(240, 240, 240, 1)'; // palette.비강조4 + + const borderColor = focused >= 0 ? 'rgba(62, 141, 0, 1)' : bgColor; // palette.keycolor + + const isValidIndex = (index: number) => index >= 0 && index < 6; + + const updateValues = () => { + if (!refs.current) return; + const newValues = refs.current.map((input) => input?.value || ''); + onValueChange(newValues); + }; + + const handleFocus = useCallback( + (event: FocusEvent, index: number) => { + event.stopPropagation(); + setFocused(index); + onFocus?.(event); + }, + [onFocus] + ); + + const handleBlur: FocusEventHandler = (event) => { + setFocused(-1); + onBlur?.(event); + }; + + const handleInput = (e: FormEvent, index: number) => { + if (!refs.current) return; + const currentInput = refs.current[index]; + // 숫자가 아니면 클리어 + if (currentInput?.value && isNaN(parseInt(currentInput.value))) { + currentInput.value = ''; + return; + } + // 두 자리 이상이면 마지막 문자만 유지 + if (currentInput && currentInput.value.length > 1) { + currentInput.value = currentInput.value[currentInput.value.length - 1]; + } + // 다음 셀로 이동 + if (currentInput?.value.length === 1 && isValidIndex(index + 1)) { + refs.current[index + 1]?.focus({ preventScroll: true }); + } + updateValues(); + }; + + const clickContainer = (e: MouseEvent) => { + e.stopPropagation(); + const firstEmptyRef = refs.current?.find((ref) => ref?.value === ''); + if (firstEmptyRef) { + firstEmptyRef.focus({ preventScroll: true }); + } else { + refs.current[refs.current.length - 1]?.focus({ preventScroll: true }); + } + }; + + const handleKeyDown = (index: number, e: KeyboardEvent) => { + if (!refs.current) return; + const currentInput = refs.current[index]; + if (e.key === 'Backspace' && currentInput?.value === '') { + e.preventDefault(); + if (isValidIndex(index - 1)) { + const prevInput = refs.current[index - 1]; + if (prevInput) prevInput.value = ''; + prevInput?.focus({ preventScroll: true }); + } + } + updateValues(); + }; + + const handlePaste = (index: number, e: React.ClipboardEvent) => { + e.preventDefault(); + const pastedData = e.clipboardData.getData('text').slice(0, 6 - index); + if (!refs.current || !pastedData) return; + [...pastedData].forEach((char, i) => { + const targetIndex = index + i; + if (isValidIndex(targetIndex)) { + const input = refs.current[targetIndex]; + if (input) input.value = char; + } + }); + updateValues(); + refs.current[Math.min(pastedData.length + index, 5)]?.focus({ preventScroll: true }); + }; + + return ( +
+ {[...Array(6)].map((_, index) => ( +
+ {/* .code-input-cell: globals.css에서 input-bar 숨김 CSS 트리거 */} +
+ handleFocus(e, index)} + {...props} + type="number" + id={String(index)} + ref={(el) => { + if (refs.current) refs.current[index] = el; + }} + onInput={(e) => handleInput(e, index)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={(e) => handlePaste(index, e)} + className="code-number-input w-full h-full text-center border-none font-semibold outline-none block p-0 text-[30px] leading-4 text-black bg-transparent placeholder:opacity-0" + /> + {/* bar: 빈 셀임을 나타내는 placeholder 바 */} +
+
+
+ ))} +
+ ); +}; + +export default CodeInput; diff --git a/src/shared/ui/input/CommentInput.test.tsx b/src/shared/ui/input/CommentInput.test.tsx new file mode 100644 index 00000000..40b7fbb6 --- /dev/null +++ b/src/shared/ui/input/CommentInput.test.tsx @@ -0,0 +1,36 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import React from 'react'; +import CommentInput from './CommentInput'; + +describe('CommentInput', () => { + it('renders a textarea element', () => { + render( {}} value="" onChange={() => {}} />); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('renders a submit button', () => { + render( {}} value="" onChange={() => {}} />); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('applies placeholder text', () => { + render( + {}} value="" onChange={() => {}} placeholder="댓글을 입력하세요" /> + ); + expect(screen.getByPlaceholderText('댓글을 입력하세요')).toBeInTheDocument(); + }); + + it('calls onChange when user types', () => { + const onChange = vi.fn(); + render( {}} value="" onChange={onChange} />); + fireEvent.change(screen.getByRole('textbox'), { target: { value: '안녕' } }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('forwards ref to the textarea element', () => { + const ref = React.createRef(); + render( {}} value="" onChange={() => {}} ref={ref} />); + expect(ref.current).toBeInstanceOf(HTMLTextAreaElement); + }); +}); diff --git a/src/shared/ui/input/CommentInput.tsx b/src/shared/ui/input/CommentInput.tsx new file mode 100644 index 00000000..ee0777d4 --- /dev/null +++ b/src/shared/ui/input/CommentInput.tsx @@ -0,0 +1,66 @@ +'use client'; + +import UpArrowIcon from '@/components/icons/UpArrowIcon'; +import { forwardRef, useEffect, useState } from 'react'; + +interface CommentInputProps extends React.TextareaHTMLAttributes { + setReset: () => void; +} + +/** + * 댓글 입력 컴포넌트. + * - forwardRef로 부모에서 textarea DOM 접근 가능 + * - 포커스 상태에 따라 테두리/배경 색상 변경 + * - 값이 있을 때만 submit 버튼이 활성화 (keycolor 배경) + * - blur 후 값이 비어있으면 setReset() 호출 + * + * Refactoring notes: + * - Emotion styled-components → Tailwind (구조) + inline style (상태 색상) + * - Bug fix: 원본 Input styled.textarea에 height: 32px 두 번 중복 선언 → 한 번만 + */ +const CommentInput = forwardRef( + ({ setReset, placeholder, value, onChange }, ref) => { + const [focused, setFocused] = useState(false); + + useEffect(() => { + if (!focused && value === '') { + setReset(); + } + }, [focused, value]); + + const canSubmit = value !== ''; + + return ( +
+