From 83d73100736a3d326e0961c416ed30399276e421 Mon Sep 17 00:00:00 2001 From: mayrang Date: Fri, 20 Mar 2026 23:53:15 +0900 Subject: [PATCH 1/8] =?UTF-8?q?Refactor:=20Phase=201=20shared=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EA=B5=AC=EC=B6=95=20+=20Phase=201.5=20?= =?UTF-8?q?=EC=9B=B9=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Phase 0: 기반 구축 - Vitest + happy-dom 테스트 환경 설정 - Playwright E2E 설정 - Tailwind v4 설치 (Emotion 공존) - FSD 디렉토리 스캐폴딩 (shared/entities/features/widgets) - CLAUDE.md 리팩토링 가이드라인 문서 작성 ## Phase 1: shared/ui 레이어 구축 (113개 테스트) - Button 그룹: Button/FilterButton/ApplyListButton/CloseButton/EditAndDeleteButton/ReportButton - ButtonContainer 3중복 제거, 컴포지션 패턴 적용 - CloseButton API: setIsOpen → onClick으로 변경 - Badge/Select/Tag/Text 그룹 - Input 그룹: RemoveButton/InputField/StateInputField/ValidationInputField/TextareaField/CodeInput/CommentInput - Toast 그룹: BaseToast 추출, ErrorToast FSD 위반 제거 (스토어 어댑터 패턴) - Modal 그룹: ModalDimmed/BaseModal/BottomSheetModal 추출, window.innerWidth → Tailwind 대체 - Profile: RoundedImage - Emotion → Tailwind 전환, 하위 호환 re-export 유지 - 버그 수정 7건 (opacity:0px, clip-path camelCase, white-space 문자열 등) ## Phase 1.5: 웹 접근성 보강 (134개 테스트) - jest-axe 도입, setup.ts 전역 등록 - RemoveButton: aria-label="삭제" - CloseButton: aria-label prop 추가 - Select: role=combobox/listbox/option, aria-expanded, aria-haspopup, aria-selected - StateInputField: aria-describedby 에러메시지 연결 (errorMessageId prop) - CodeInput: 각 셀 aria-label="{n}번째 숫자" - BaseModal/BottomSheetModal: role=dialog, aria-modal, focus trap, Escape 닫기 ## 기타 수정 - 개발 서버 포트 9999 → 8080 (백엔드 CORS 허용 목록 대응) - next.config.js rewrites 프록시 추가 (CORS 우회) - src/api/index.ts: MAX_RETRY_COUNT 50 → 5, 클라이언트/서버 baseURL 분기 - globals.css CSS 리셋 전체 @layer base로 이동 (Tailwind utilities 우선순위 보장) - src/pages/ 디렉토리 제거 (Next.js Pages Router 충돌 방지) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 204 + docs/decisions/001-tailwind.md | 28 + docs/decisions/002-fsd.md | 42 + docs/decisions/003-testing.md | 36 + docs/progress.md | 104 + docs/refactoring/_template.md | 100 + docs/refactoring/phase-0.md | 173 + docs/refactoring/phase-1.5.md | 242 ++ docs/refactoring/phase-1.md | 273 ++ e2e/.gitkeep | 0 next.config.js | 8 + package-lock.json | 3475 +++++++++++++++-- package.json | 24 +- playwright.config.ts | 31 + postcss.config.js | 5 + src/api/index.ts | 6 +- src/app/globals.css | 378 +- src/components/designSystem/Badge.tsx | 96 +- .../designSystem/Buttons/ApplyListButton.tsx | 122 +- .../designSystem/Buttons/Button.tsx | 78 +- .../designSystem/Buttons/CloseButton.tsx | 43 +- .../Buttons/EditAndDeleteButton.tsx | 101 +- .../designSystem/Buttons/FilterButton.tsx | 110 +- .../designSystem/Buttons/ReportButton.tsx | 66 +- src/components/designSystem/Select.tsx | 272 +- .../designSystem/input/CodeInput.tsx | 223 +- .../designSystem/input/CommentInput.tsx | 85 +- .../designSystem/input/InputField.tsx | 98 +- .../designSystem/input/RemoveButton.tsx | 30 +- .../designSystem/input/StateInputField.tsx | 161 +- .../designSystem/input/TextareaField.tsx | 244 +- .../input/ValidationInputField.tsx | 62 +- .../designSystem/modal/CheckingModal.tsx | 186 +- .../designSystem/modal/EditAndDeleteModal.tsx | 132 +- .../designSystem/modal/ImageModal.tsx | 70 +- .../designSystem/modal/NoticeModal.tsx | 200 +- .../designSystem/modal/ReportModal.tsx | 107 +- .../designSystem/modal/ResultModal.tsx | 174 +- .../designSystem/profile/RoundedImage.tsx | 31 +- .../designSystem/tag/BoxLayoutTag.tsx | 83 +- .../designSystem/tag/SearchFilterTag.tsx | 112 +- src/components/designSystem/text/InfoText.tsx | 93 +- .../designSystem/text/TextButton.tsx | 96 +- .../toastMessage/WarningToast.tsx | 82 +- .../designSystem/toastMessage/errorToast.tsx | 85 +- .../designSystem/toastMessage/resultToast.tsx | 80 +- .../designSystem/toastMessage/tripToast.tsx | 75 +- src/entities/.gitkeep | 0 src/entities/index.ts | 2 + src/features/.gitkeep | 0 src/features/index.ts | 2 + src/mocks/http.ts | 2 +- src/shared/api/.gitkeep | 0 src/shared/constants/.gitkeep | 0 src/shared/hooks/.gitkeep | 0 src/shared/index.ts | 2 + src/shared/lib/.gitkeep | 0 src/shared/types/.gitkeep | 0 src/shared/ui/.gitkeep | 0 src/shared/ui/badge/Badge.test.tsx | 25 + src/shared/ui/badge/Badge.tsx | 60 + src/shared/ui/badge/index.ts | 1 + src/shared/ui/button/ApplyListButton.tsx | 75 + src/shared/ui/button/Button.test.tsx | 42 + src/shared/ui/button/Button.tsx | 52 + src/shared/ui/button/CloseButton.test.tsx | 36 + src/shared/ui/button/CloseButton.tsx | 36 + .../ui/button/EditAndDeleteButton.test.tsx | 46 + src/shared/ui/button/EditAndDeleteButton.tsx | 63 + src/shared/ui/button/FilterButton.test.tsx | 35 + src/shared/ui/button/FilterButton.tsx | 52 + src/shared/ui/button/ReportButton.tsx | 38 + src/shared/ui/button/index.ts | 6 + src/shared/ui/input/CodeInput.test.tsx | 58 + src/shared/ui/input/CodeInput.tsx | 161 + src/shared/ui/input/CommentInput.test.tsx | 36 + src/shared/ui/input/CommentInput.tsx | 66 + src/shared/ui/input/InputField.test.tsx | 86 + src/shared/ui/input/InputField.tsx | 77 + src/shared/ui/input/RemoveButton.test.tsx | 35 + src/shared/ui/input/RemoveButton.tsx | 18 + src/shared/ui/input/StateInputField.test.tsx | 85 + src/shared/ui/input/StateInputField.tsx | 116 + src/shared/ui/input/TextareaField.test.tsx | 35 + src/shared/ui/input/TextareaField.tsx | 124 + .../ui/input/ValidationInputField.test.tsx | 41 + src/shared/ui/input/ValidationInputField.tsx | 64 + src/shared/ui/input/index.ts | 7 + src/shared/ui/modal/BaseModal.tsx | 119 + src/shared/ui/modal/BottomSheetModal.tsx | 108 + src/shared/ui/modal/CheckingModal.test.tsx | 76 + src/shared/ui/modal/CheckingModal.tsx | 60 + .../ui/modal/EditAndDeleteModal.test.tsx | 54 + src/shared/ui/modal/EditAndDeleteModal.tsx | 46 + src/shared/ui/modal/ImageModal.test.tsx | 29 + src/shared/ui/modal/ImageModal.tsx | 42 + src/shared/ui/modal/ModalDimmed.tsx | 19 + src/shared/ui/modal/NoticeModal.test.tsx | 48 + src/shared/ui/modal/NoticeModal.tsx | 58 + src/shared/ui/modal/ReportModal.test.tsx | 39 + src/shared/ui/modal/ReportModal.tsx | 32 + src/shared/ui/modal/ResultModal.test.tsx | 53 + src/shared/ui/modal/ResultModal.tsx | 42 + src/shared/ui/modal/index.ts | 9 + src/shared/ui/profile/RoundedImage.test.tsx | 29 + src/shared/ui/profile/RoundedImage.tsx | 30 + src/shared/ui/profile/index.ts | 1 + src/shared/ui/select/Select.test.tsx | 68 + src/shared/ui/select/Select.tsx | 138 + src/shared/ui/select/index.ts | 1 + src/shared/ui/tag/BoxLayoutTag.test.tsx | 25 + src/shared/ui/tag/BoxLayoutTag.tsx | 55 + src/shared/ui/tag/SearchFilterTag.test.tsx | 35 + src/shared/ui/tag/SearchFilterTag.tsx | 74 + src/shared/ui/tag/index.ts | 2 + src/shared/ui/text/InfoText.test.tsx | 28 + src/shared/ui/text/InfoText.tsx | 53 + src/shared/ui/text/TextButton.test.tsx | 36 + src/shared/ui/text/TextButton.tsx | 71 + src/shared/ui/text/index.ts | 2 + src/shared/ui/toast/BaseToast.test.tsx | 60 + src/shared/ui/toast/BaseToast.tsx | 68 + src/shared/ui/toast/ErrorToast.test.tsx | 45 + src/shared/ui/toast/ErrorToast.tsx | 54 + src/shared/ui/toast/ResultToast.tsx | 47 + src/shared/ui/toast/TripToast.test.tsx | 48 + src/shared/ui/toast/TripToast.tsx | 64 + src/shared/ui/toast/WarningToast.tsx | 50 + src/shared/ui/toast/index.ts | 5 + src/test/setup.ts | 5 + src/widgets/.gitkeep | 0 src/widgets/index.ts | 2 + tailwind.config.ts | 3 + vitest.config.mts | 30 + 134 files changed, 8311 insertions(+), 3762 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/decisions/001-tailwind.md create mode 100644 docs/decisions/002-fsd.md create mode 100644 docs/decisions/003-testing.md create mode 100644 docs/progress.md create mode 100644 docs/refactoring/_template.md create mode 100644 docs/refactoring/phase-0.md create mode 100644 docs/refactoring/phase-1.5.md create mode 100644 docs/refactoring/phase-1.md create mode 100644 e2e/.gitkeep create mode 100644 playwright.config.ts create mode 100644 postcss.config.js create mode 100644 src/entities/.gitkeep create mode 100644 src/entities/index.ts create mode 100644 src/features/.gitkeep create mode 100644 src/features/index.ts create mode 100644 src/shared/api/.gitkeep create mode 100644 src/shared/constants/.gitkeep create mode 100644 src/shared/hooks/.gitkeep create mode 100644 src/shared/index.ts create mode 100644 src/shared/lib/.gitkeep create mode 100644 src/shared/types/.gitkeep create mode 100644 src/shared/ui/.gitkeep create mode 100644 src/shared/ui/badge/Badge.test.tsx create mode 100644 src/shared/ui/badge/Badge.tsx create mode 100644 src/shared/ui/badge/index.ts create mode 100644 src/shared/ui/button/ApplyListButton.tsx create mode 100644 src/shared/ui/button/Button.test.tsx create mode 100644 src/shared/ui/button/Button.tsx create mode 100644 src/shared/ui/button/CloseButton.test.tsx create mode 100644 src/shared/ui/button/CloseButton.tsx create mode 100644 src/shared/ui/button/EditAndDeleteButton.test.tsx create mode 100644 src/shared/ui/button/EditAndDeleteButton.tsx create mode 100644 src/shared/ui/button/FilterButton.test.tsx create mode 100644 src/shared/ui/button/FilterButton.tsx create mode 100644 src/shared/ui/button/ReportButton.tsx create mode 100644 src/shared/ui/button/index.ts create mode 100644 src/shared/ui/input/CodeInput.test.tsx create mode 100644 src/shared/ui/input/CodeInput.tsx create mode 100644 src/shared/ui/input/CommentInput.test.tsx create mode 100644 src/shared/ui/input/CommentInput.tsx create mode 100644 src/shared/ui/input/InputField.test.tsx create mode 100644 src/shared/ui/input/InputField.tsx create mode 100644 src/shared/ui/input/RemoveButton.test.tsx create mode 100644 src/shared/ui/input/RemoveButton.tsx create mode 100644 src/shared/ui/input/StateInputField.test.tsx create mode 100644 src/shared/ui/input/StateInputField.tsx create mode 100644 src/shared/ui/input/TextareaField.test.tsx create mode 100644 src/shared/ui/input/TextareaField.tsx create mode 100644 src/shared/ui/input/ValidationInputField.test.tsx create mode 100644 src/shared/ui/input/ValidationInputField.tsx create mode 100644 src/shared/ui/input/index.ts create mode 100644 src/shared/ui/modal/BaseModal.tsx create mode 100644 src/shared/ui/modal/BottomSheetModal.tsx create mode 100644 src/shared/ui/modal/CheckingModal.test.tsx create mode 100644 src/shared/ui/modal/CheckingModal.tsx create mode 100644 src/shared/ui/modal/EditAndDeleteModal.test.tsx create mode 100644 src/shared/ui/modal/EditAndDeleteModal.tsx create mode 100644 src/shared/ui/modal/ImageModal.test.tsx create mode 100644 src/shared/ui/modal/ImageModal.tsx create mode 100644 src/shared/ui/modal/ModalDimmed.tsx create mode 100644 src/shared/ui/modal/NoticeModal.test.tsx create mode 100644 src/shared/ui/modal/NoticeModal.tsx create mode 100644 src/shared/ui/modal/ReportModal.test.tsx create mode 100644 src/shared/ui/modal/ReportModal.tsx create mode 100644 src/shared/ui/modal/ResultModal.test.tsx create mode 100644 src/shared/ui/modal/ResultModal.tsx create mode 100644 src/shared/ui/modal/index.ts create mode 100644 src/shared/ui/profile/RoundedImage.test.tsx create mode 100644 src/shared/ui/profile/RoundedImage.tsx create mode 100644 src/shared/ui/profile/index.ts create mode 100644 src/shared/ui/select/Select.test.tsx create mode 100644 src/shared/ui/select/Select.tsx create mode 100644 src/shared/ui/select/index.ts create mode 100644 src/shared/ui/tag/BoxLayoutTag.test.tsx create mode 100644 src/shared/ui/tag/BoxLayoutTag.tsx create mode 100644 src/shared/ui/tag/SearchFilterTag.test.tsx create mode 100644 src/shared/ui/tag/SearchFilterTag.tsx create mode 100644 src/shared/ui/tag/index.ts create mode 100644 src/shared/ui/text/InfoText.test.tsx create mode 100644 src/shared/ui/text/InfoText.tsx create mode 100644 src/shared/ui/text/TextButton.test.tsx create mode 100644 src/shared/ui/text/TextButton.tsx create mode 100644 src/shared/ui/text/index.ts create mode 100644 src/shared/ui/toast/BaseToast.test.tsx create mode 100644 src/shared/ui/toast/BaseToast.tsx create mode 100644 src/shared/ui/toast/ErrorToast.test.tsx create mode 100644 src/shared/ui/toast/ErrorToast.tsx create mode 100644 src/shared/ui/toast/ResultToast.tsx create mode 100644 src/shared/ui/toast/TripToast.test.tsx create mode 100644 src/shared/ui/toast/TripToast.tsx create mode 100644 src/shared/ui/toast/WarningToast.tsx create mode 100644 src/shared/ui/toast/index.ts create mode 100644 src/test/setup.ts create mode 100644 src/widgets/.gitkeep create mode 100644 src/widgets/index.ts create mode 100644 tailwind.config.ts create mode 100644 vitest.config.mts 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..5f5a4794 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,14 @@ const nextConfig = { experimental: { scrollRestoration: true, }, + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.API_BASE_URL}/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/designSystem/Badge.tsx b/src/components/designSystem/Badge.tsx index f7a06065..0318c819 100644 --- a/src/components/designSystem/Badge.tsx +++ b/src/components/designSystem/Badge.tsx @@ -1,93 +1,3 @@ -"use client"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; - -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; -} -// 사용 방식 -{ - /* */ -} -// Dday에 쓰임. - -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 ? palette.비강조2 : backgroundColor; - const textColor = isClose ? "white" : color; - - return ( - <> - {isDueDate ? ( - - {isClose ? ( - "마감" - ) : ( - <> - {text} D-{daysLeft} - - )} - - ) : ( - - {text} - - )} - - ); -}; - -const BadgeContainer = styled.div` - font-size: 12px; - padding: 4px 10px; - display: flex; - align-items: center; - justify-content: center; - min-width: max-content; - line-height: 14px; - gap: 10px; - text-align: center; -`; - -export default Badge; +// 이 파일은 하위 호환성을 위해 유지됩니다. +// 실제 구현은 @/shared/ui/badge/Badge 에 있습니다. +export { default } from '@/shared/ui/badge/Badge'; diff --git a/src/components/designSystem/Buttons/ApplyListButton.tsx b/src/components/designSystem/Buttons/ApplyListButton.tsx index 7fb160f0..c7ed58a4 100644 --- a/src/components/designSystem/Buttons/ApplyListButton.tsx +++ b/src/components/designSystem/Buttons/ApplyListButton.tsx @@ -1,119 +1,3 @@ -"use client"; -import EmptyHeartIcon from "@/components/icons/EmptyHeartIcon"; -import FullHeartIcon from "@/components/icons/FullHeartIcon"; -import { palette } from "@/styles/palette"; -import styled from "@emotion/styled"; -import "./Button.css"; - -interface ApplyListButtonProps { - nowEnrollmentCount: number; - text: string; - addStyle?: { - backgroundColor?: string; - color?: string; - boxShadow?: string; - weight?: "regular" | "medium" | "semiBold" | "bold"; - }; - type?: "button" | "reset" | "submit" | undefined; - children?: React.ReactNode; - disabled?: boolean; - bookmarked: boolean; - onClick?: (event: React.MouseEvent) => void; - bookmarkOnClick?: (event: React.MouseEvent) => void; - hostUserCheck: boolean; -} -// 사용 방식 -{ - /* - )} - - {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/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..f433714a --- /dev/null +++ b/src/shared/ui/button/ApplyListButton.tsx @@ -0,0 +1,75 @@ +'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; + onClick?: (event: React.MouseEvent) => void; + bookmarkOnClick?: (event: React.MouseEvent) => void; +} + +/** + * 신청 목록 버튼 (북마크 아이콘 + 메인 버튼). + * Button 컴포넌트를 내부에서 재사용. + */ +const ApplyListButton = ({ + text = '다음', + type = 'submit', + disabled = false, + children, + style, + 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..40734d2f --- /dev/null +++ b/src/shared/ui/button/Button.tsx @@ -0,0 +1,52 @@ +'use client'; + +interface ButtonProps { + text: string; + type?: 'button' | 'reset' | 'submit'; + children?: React.ReactNode; + disabled?: boolean; + style?: React.CSSProperties; + className?: string; + onClick?: (event: React.MouseEvent) => void; +} + +/** + * 기본 버튼 컴포넌트. + * FilterButton, ApplyListButton은 이 컴포넌트를 내부적으로 조합합니다. + */ +const Button = ({ + text = '다음', + type = 'submit', + disabled = false, + children, + style, + 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 ( +
+