From 2b324d083bab7ace102eeb5378cfa2ede8e4a855 Mon Sep 17 00:00:00 2001 From: syc0123 Date: Wed, 4 Mar 2026 10:11:48 +0900 Subject: [PATCH] feat: Improve V2Select multi-select dropdown item display - Enhanced the display of selected items in the V2Select component to show labels in a comma-separated format, improving user visibility without needing to open the dropdown. - Implemented tooltip functionality that activates only when the text is truncated, allowing users to see all selected items at a glance. - Updated the DropdownSelect component to ensure consistent behavior across all screens using this component. - Added necessary imports and state management for tooltip visibility and text truncation detection. Made-with: Cursor --- .../MST[계획서]-다중선택-라벨표시.md | 146 ++++++ .../MST[맥락노트]-다중선택-라벨표시.md | 89 ++++ .../MST[체크리스트]-다중선택-라벨표시.md | 52 ++ frontend/components/ui/tooltip.tsx | 57 +++ frontend/components/v2/V2Select.tsx | 194 ++++---- frontend/package-lock.json | 463 ++++++++++++++++++ frontend/package.json | 1 + 7 files changed, 919 insertions(+), 83 deletions(-) create mode 100644 docs/ycshin-node/MST[계획서]-다중선택-라벨표시.md create mode 100644 docs/ycshin-node/MST[맥락노트]-다중선택-라벨표시.md create mode 100644 docs/ycshin-node/MST[체크리스트]-다중선택-라벨표시.md create mode 100644 frontend/components/ui/tooltip.tsx diff --git a/docs/ycshin-node/MST[계획서]-다중선택-라벨표시.md b/docs/ycshin-node/MST[계획서]-다중선택-라벨표시.md new file mode 100644 index 00000000..01761b9c --- /dev/null +++ b/docs/ycshin-node/MST[계획서]-다중선택-라벨표시.md @@ -0,0 +1,146 @@ +# [계획서] V2Select 다중 선택 드롭다운 - 선택 항목 표시 개선 + +> 관련 문서: [맥락노트](./MST[맥락노트]-v2select-multiselect-tooltip.md) | [체크리스트](./MST[체크리스트]-v2select-multiselect-tooltip.md) + +## 개요 + +모든 화면에서 다중 선택 가능한 드롭다운(`V2Select` - `DropdownSelect`)의 선택 항목 표시 방식을 개선합니다. + +--- + +## 현재 동작 + +- 다중 선택 시 `"3개 선택됨"` 같은 텍스트만 표시 +- 어떤 항목이 선택되었는지 드롭다운을 열어야만 확인 가능 + +### 현재 코드 (V2Select.tsx - DropdownSelect, 174~178행) + +```tsx +{selectedLabels.length > 0 + ? multiple + ? `${selectedLabels.length}개 선택됨` + : selectedLabels[0] + : placeholder} +``` + +--- + +## 변경 후 동작 + +### 1. 선택된 항목 라벨을 쉼표로 연결하여 한 줄로 표시 + +- 예: `"구매품, 판매품, 재고품"` +- `truncate` (text-overflow: ellipsis)로 필드 너비를 넘으면 말줄임(`...`) 처리 +- 무조건 한 줄 표시, 넘치면 `...`으로 숨김 + +### 2. 텍스트가 말줄임(`...`) 처리될 때만 호버 툴팁 표시 + +- 필드 너비를 넘어서 `...`으로 잘릴 때만 툴팁 활성화 +- 필드 내에 전부 보이면 툴팁 불필요 +- 툴팁 내용은 세로 나열로 각 항목을 한눈에 확인 가능 +- 툴팁은 딜레이 없이 즉시 표시 + +--- + +## 시각적 동작 예시 + +| 상태 | 필드 내 표시 | 호버 시 툴팁 | +|------|-------------|-------------| +| 미선택 | `선택` (placeholder) | 없음 | +| 1개 선택 | `구매품` | 없음 | +| 3개 선택 (필드 내 수용) | `구매품, 판매품, 재고품` | 없음 (잘리지 않으므로) | +| 5개 선택 (필드 넘침) | `구매품, 판매품, 재고품, 외...` | 구매품 / 판매품 / 재고품 / 외주품 / 반제품 (세로 나열) | + +--- + +## 변경 대상 + +- **파일**: `frontend/components/v2/V2Select.tsx` +- **컴포넌트**: `DropdownSelect` 내부 표시 텍스트 부분 (170~178행) +- **적용 범위**: `DropdownSelect`를 사용하는 모든 화면 (품목정보, 기타 모든 모달 포함) +- **변경 규모**: 약 30줄 내외 소규모 변경 + +--- + +## 코드 설계 + +### 추가 import + +```tsx +import { useRef, useEffect, useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +``` + +### 말줄임 감지 로직 + +```tsx +// 텍스트가 잘리는지(truncated) 감지 +const textRef = useRef(null); +const [isTruncated, setIsTruncated] = useState(false); + +useEffect(() => { + const el = textRef.current; + if (el) { + setIsTruncated(el.scrollWidth > el.clientWidth); + } +}, [selectedLabels]); +``` + +### 수정 코드 (DropdownSelect 내부, 170~178행 대체) + +```tsx +const displayText = selectedLabels.length > 0 + ? (multiple ? selectedLabels.join(", ") : selectedLabels[0]) + : placeholder; + +const isPlaceholder = selectedLabels.length === 0; + +// 렌더링 부분 +{isTruncated && multiple ? ( + + + + + {displayText} + + + +
+ {selectedLabels.map((label, i) => ( +
{label}
+ ))} +
+
+
+
+) : ( + + {displayText} + +)} +``` + +--- + +## 설계 원칙 + +- 기존 단일 선택 동작은 변경하지 않음 +- `DropdownSelect` 공통 컴포넌트 수정이므로 모든 화면에 자동 적용 +- 무조건 한 줄 표시, 넘치면 `...`으로 말줄임 +- 툴팁은 텍스트가 실제로 잘릴 때(`scrollWidth > clientWidth`)만 표시 +- 툴팁 내용은 세로 나열로 각 항목 확인 용이 +- 툴팁 딜레이 없음 (`delayDuration={0}`) +- shadcn 표준 Tooltip 컴포넌트 사용으로 프로젝트 일관성 유지 diff --git a/docs/ycshin-node/MST[맥락노트]-다중선택-라벨표시.md b/docs/ycshin-node/MST[맥락노트]-다중선택-라벨표시.md new file mode 100644 index 00000000..81b536fe --- /dev/null +++ b/docs/ycshin-node/MST[맥락노트]-다중선택-라벨표시.md @@ -0,0 +1,89 @@ +# [맥락노트] V2Select 다중 선택 드롭다운 - 선택 항목 표시 개선 + +> 관련 문서: [계획서](./MST[계획서]-v2select-multiselect-tooltip.md) | [체크리스트](./MST[체크리스트]-v2select-multiselect-tooltip.md) + +--- + +## 왜 이 작업을 하는가 + +- 사용자가 수정 모달에서 다중 선택 드롭다운을 사용할 때 `"3개 선택됨"` 만 보임 +- 드롭다운을 다시 열어봐야만 무엇이 선택됐는지 확인 가능 → UX 불편 +- 선택 항목을 직접 보여주고, 넘치면 툴팁으로 확인할 수 있게 개선 + +--- + +## 핵심 결정 사항과 근거 + +### 1. "n개 선택됨" → 라벨 쉼표 나열 + +- **결정**: `"구매품, 판매품, 재고품"` 형태로 표시 +- **근거**: 사용자가 드롭다운을 열지 않아도 선택 내용을 바로 확인 가능 + +### 2. 무조건 한 줄, 넘치면 말줄임(`...`) + +- **결정**: 여러 줄 줄바꿈 없이 한 줄 고정, `truncate`로 오버플로우 처리 +- **근거**: 드롭다운 필드 높이가 고정되어 있어 여러 줄 표시 시 레이아웃이 깨짐 + +### 3. 텍스트가 잘릴 때만 툴팁 표시 + +- **결정**: `scrollWidth > clientWidth` 비교로 실제 잘림 여부 감지 후 툴팁 활성화 +- **근거**: 전부 보이는데 툴팁이 뜨면 오히려 방해. 필요할 때만 보여야 함 +- **대안 검토**: "2개 이상이면 항상 툴팁" → 기각 (불필요한 툴팁 발생) + +### 4. 툴팁 내용은 세로 나열 + +- **결정**: 툴팁 안에서 항목을 줄바꿈으로 세로 나열 +- **근거**: 가로 나열 시 툴팁도 길어져서 읽기 어려움. 세로가 한눈에 파악하기 좋음 + +### 5. 툴팁 딜레이 0ms + +- **결정**: `delayDuration={0}` 즉시 표시 +- **근거**: 사용자가 "무엇을 선택했는지" 확인하려는 의도적 행동이므로 즉시 응답해야 함 + +### 6. Radix Tooltip 대신 커스텀 호버 툴팁 사용 + +- **결정**: Radix Tooltip을 사용하지 않고 `onMouseEnter`/`onMouseLeave`로 직접 제어 +- **근거**: Radix Tooltip + Popover 조합은 이벤트 충돌 발생. 내부 배치든 외부 래핑이든 Popover가 호버를 가로챔 +- **시도 1**: Tooltip을 Button 안에 배치 → Popover가 이벤트 가로챔 (실패) +- **시도 2**: Radix 공식 패턴 (TooltipTrigger > PopoverTrigger > Button 체이닝) → 여전히 동작 안 함 (실패) +- **최종**: wrapper div에 마우스 이벤트 + 절대 위치 div로 툴팁 렌더링 (성공) +- **추가**: Popover 열릴 때 `setHoverTooltip(false)`로 툴팁 자동 숨김 + +### 7. DropdownSelect 공통 컴포넌트 수정 + +- **결정**: 특정 화면이 아닌 `DropdownSelect` 자체를 수정 +- **근거**: 품목정보뿐 아니라 모든 화면에서 동일한 문제가 있으므로 공통 해결 + +--- + +## 관련 파일 위치 + +| 구분 | 파일 경로 | 설명 | +|------|----------|------| +| 수정 대상 | `frontend/components/v2/V2Select.tsx` | DropdownSelect 컴포넌트 (170~178행) | +| 타입 정의 | `frontend/types/v2-components.ts` | V2SelectProps, SelectOption 타입 | +| UI 컴포넌트 | `frontend/components/ui/tooltip.tsx` | shadcn Tooltip 컴포넌트 | +| 렌더러 | `frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx` | V2Select를 레지스트리에 연결 | +| 수정 모달 | `frontend/components/screen/EditModal.tsx` | 공통 편집 모달 | + +--- + +## 기술 참고 + +### truncate 감지 방식 + +``` +scrollWidth: 텍스트의 실제 전체 너비 (보이지 않는 부분 포함) +clientWidth: 요소의 보이는 너비 + +scrollWidth > clientWidth → 텍스트가 잘리고 있음 (... 표시 중) +``` + +### selectedLabels 계산 흐름 + +``` +value (string[]) → selectedValues → safeOptions에서 label 매칭 → selectedLabels (string[]) +``` + +- `selectedLabels`는 이미 `DropdownSelect` 내부에서 `useMemo`로 계산됨 (126~130행) +- 추가 데이터 fetching 불필요, 기존 값을 `.join(", ")`로 결합하면 됨 diff --git a/docs/ycshin-node/MST[체크리스트]-다중선택-라벨표시.md b/docs/ycshin-node/MST[체크리스트]-다중선택-라벨표시.md new file mode 100644 index 00000000..0a22eab9 --- /dev/null +++ b/docs/ycshin-node/MST[체크리스트]-다중선택-라벨표시.md @@ -0,0 +1,52 @@ +# [체크리스트] V2Select 다중 선택 드롭다운 - 선택 항목 표시 개선 + +> 관련 문서: [계획서](./MST[계획서]-v2select-multiselect-tooltip.md) | [맥락노트](./MST[맥락노트]-v2select-multiselect-tooltip.md) + +--- + +## 공정 상태 + +- 전체 진행률: **100%** (완료) +- 현재 단계: 전체 완료 + +--- + +## 구현 체크리스트 + +### 1단계: 코드 수정 + +- [x] `V2Select.tsx`에 Tooltip 관련 import 추가 +- [x] `DropdownSelect` 내부에 `textRef`, `isTruncated` 상태 추가 +- [x] `useEffect`로 `scrollWidth > clientWidth` 감지 로직 추가 +- [x] 표시 텍스트를 `selectedLabels.join(", ")`로 변경 +- [x] `isTruncated && multiple` 조건으로 Tooltip 래핑 +- [x] 툴팁 내용을 세로 나열 (`space-y-0.5`)로 구성 +- [x] `delayDuration={0}` 설정 +- [x] Radix Tooltip → 커스텀 호버 툴팁으로 변경 (onMouseEnter/onMouseLeave + 절대 위치 div) + +### 2단계: 검증 + +- [x] 단일 선택 모드: 기존 동작 변화 없음 확인 +- [x] 다중 선택 1개: 라벨 정상 표시, 툴팁 없음 +- [x] 다중 선택 3개 (필드 내 수용): 쉼표 나열 표시, 툴팁 없음 +- [x] 다중 선택 5개+ (필드 넘침): 말줄임 표시, 호버 시 툴팁 세로 나열 +- [x] 품목정보 수정 모달에서 동작 확인 +- [x] 다른 화면의 다중 선택 드롭다운에서도 동작 확인 + +### 3단계: 정리 + +- [x] 린트 에러 없음 확인 +- [x] 이 체크리스트 완료 표시 업데이트 + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-03-04 | 설계 문서 작성 완료 | +| 2026-03-04 | 맥락노트, 체크리스트 작성 완료 | +| 2026-03-04 | 파일명 MST 접두사 적용 | +| 2026-03-04 | 1단계 코드 수정 완료 (V2Select.tsx) | +| 2026-03-04 | Radix Tooltip이 Popover와 충돌 → 커스텀 호버 툴팁으로 변경 | +| 2026-03-04 | 사용자 검증 완료, 전체 작업 완료 | diff --git a/frontend/components/ui/tooltip.tsx b/frontend/components/ui/tooltip.tsx new file mode 100644 index 00000000..ec65c1e4 --- /dev/null +++ b/frontend/components/ui/tooltip.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import { Tooltip as TooltipPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 64d52f11..7631bc74 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -12,7 +12,7 @@ * - swap: 스왑 선택 (좌우 이동) */ -import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Checkbox } from "@/components/ui/checkbox"; @@ -57,6 +57,9 @@ const DropdownSelect = forwardRef { const [open, setOpen] = useState(false); + const textRef = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + const [hoverTooltip, setHoverTooltip] = useState(false); // 현재 선택된 값 존재 여부 const hasValue = useMemo(() => { @@ -129,6 +132,13 @@ const DropdownSelect = forwardRef { + const el = textRef.current; + if (el) { + setIsTruncated(el.scrollWidth > el.clientWidth); + } + }, [selectedLabels]); + const handleSelect = useCallback((selectedValue: string) => { if (multiple) { const newValues = selectedValues.includes(selectedValue) @@ -148,91 +158,109 @@ const DropdownSelect = forwardRef 0 + ? (multiple ? selectedLabels.join(", ") : selectedLabels[0]) + : placeholder; + const isPlaceholder = selectedLabels.length === 0; + return ( - - - {/* Button에 style로 직접 height 전달 (Popover도 DOM 체인 끊김) */} - + + + { + if (!search) return 1; + const option = safeOptions.find((o) => o.value === itemValue); + const label = (option?.label || option?.value || "").toLowerCase(); + if (label.includes(search.toLowerCase())) return 1; + return 0; + }} + > + {searchable && } + + 검색 결과가 없습니다. + + {safeOptions.map((option) => { + const displayLabel = option.label || option.value || "(빈 값)"; + return ( + handleSelect(option.value)} + > + + {displayLabel} + + ); + })} + + + + + + {hoverTooltip && !open && ( +
+
+ {selectedLabels.map((label, i) => ( +
{label}
+ ))}
- - - - { - if (!search) return 1; - const option = safeOptions.find((o) => o.value === itemValue); - const label = (option?.label || option?.value || "").toLowerCase(); - if (label.includes(search.toLowerCase())) return 1; - return 0; - }} - > - {searchable && } - - 검색 결과가 없습니다. - - {safeOptions.map((option) => { - const displayLabel = option.label || option.value || "(빈 값)"; - return ( - handleSelect(option.value)} - > - - {displayLabel} - - ); - })} - - - - - +
+ )} + ); }); DropdownSelect.displayName = "DropdownSelect"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a360f556..15c164c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -72,6 +72,7 @@ "mammoth": "^1.11.0", "next": "^15.4.8", "qrcode": "^1.5.4", + "radix-ui": "^1.4.3", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dnd": "^16.0.1", @@ -1492,6 +1493,29 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", @@ -1574,6 +1598,29 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-avatar": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", @@ -1892,6 +1939,65 @@ } } }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1973,6 +2079,38 @@ } } }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", @@ -2009,6 +2147,70 @@ } } }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", @@ -2333,6 +2535,39 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -2410,6 +2645,157 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -13127,6 +13513,83 @@ "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", "license": "ISC" }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8a31c255..68e79db4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -81,6 +81,7 @@ "mammoth": "^1.11.0", "next": "^15.4.8", "qrcode": "^1.5.4", + "radix-ui": "^1.4.3", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dnd": "^16.0.1",