refactor: 권한별 Route Group 폴더 구조 분리#5
Conversation
shadcn@latest init으로 Tailwind v4 호환 base-nova 스타일 초기화. clsx + tailwind-merge 기반 cn() 유틸 및 components.json 설정 추가. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
npx shadcn@latest add --all로 55개 UI 컴포넌트 설치. Button, Input, Badge, Card, Dialog, Select 등 기본 컴포넌트 포함. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shadcn 기본 neutral 색상을 프로젝트 톤(#b46d55 rose, #c7a56a gold)으로 교체. 기존 셸 변수(--bg, --panel 등)를 --shell- 접두사로 리네이밍하여 충돌 방지. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- status-badge → shadcn Badge 기반 - form-submit-button → shadcn Button 기반 - auth-forms → shadcn Input/Label/Button/Card 적용 - match-detail-modal → shadcn Dialog 적용 (createPortal 제거) - candidate-card → shadcn Card/Badge 적용 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
account-panel, back-nav-button, gender-toggle-field, home-account-shell, lazy-home-account-shell에서 #ead8cf, #b46d59 등 하드코딩 hex를 --border, --primary 등 CSS 변수와 shadcn Button/Card로 통일. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ui-components.md에 shadcn/ui 사용 규칙 반영 (render prop, CSS 변수 체계). 셸 변수 --shell- 접두사 가이드 추가. 기획서 파일 추가. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UI 구현 시 shadcn 컴포넌트 우선 사용 원칙 명시. 리뷰 시 shadcn 사용 여부, hex 하드코딩, cn() 사용 등 확인 항목 추가. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 수동 button 탭 전환 → Tabs/TabsList/TabsTrigger/TabsContent - 메시지/안내 div → Alert/AlertDescription - useState 제거 (Tabs 자체 상태 관리) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shadcn/ui 마이그레이션 시 의도치 않게 테라코타(#b46d55)로 변경된 primary, ring, sidebar-primary 색상을 원래 로즈 핑크로 되돌림 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Button base에 cursor-pointer 추가 - default variant hover: [a]:hover → hover로 수정 (일반 버튼에도 적용) - 오버레이 닫기 버튼에 cursor-pointer 추가 - 커스텀 button에 cursor-pointer, focus-visible 링 추가 - template literal className → cn() 변환 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shadcn 시맨틱 토큰, Shell 토큰, Sidebar/Chart 토큰, 컬러 팔레트 요약, 사용 규칙, 변경 이력 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pretendard 패키지 설치, next/font/local로 로드 - globals.css에 h1~h4 태그 오버라이딩 (크기/행간/자간/굵기) - Geist 제거, --font-body CSS 변수로 Pretendard 연결 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- docs/typography.md: 헤딩/본문 스케일, font-weight, 색상 조합, 말줄임, 링크, 금지사항 - docs/ui-tokens.md: spacing, border-radius, shadow, z-index, transition, 아이콘 규격 - .claude/rules/ui-components.md: 타이포그래피/UI 토큰 섹션 추가, 리뷰 체크리스트 확장 (16항목) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(public), (member), (admin), (super-admin) 4개 그룹으로 페이지 분류. 로직 변경 없이 폴더 이동만 수행. URL 변경 없음. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
auth/callback, dashboard/loading, profiles/[id]/loading을 각 그룹으로 이동. page-patterns.md 라우팅 구조를 Route Group 기준으로 갱신. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR implements a major UI system overhaul: introducing shadcn/ui component library with 60+ styled components, migrating routes to permission-based groups ( Changes
Sequence Diagram(s)The changes do not introduce new runtime interaction flows or significantly alter existing control flow in a way that would benefit from sequence visualization. Route restructuring is configuration-based, UI components are presentational primitives, and refactoring is largely mechanical (replacing elements with styled equivalents). Conditions for diagram generation are not met. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Rationale: This is a substantial, multi-faceted refactoring affecting 100+ files across distinct domains (60+ new UI components, route restructuring, design token system, existing component updates). While the UI component additions follow repetitive patterns (reducing individual review effort per component), the scope is very large and includes logic-dense areas (Dialog/Sidebar state management, Chart integration, responsive behaviors). Additionally, the design token system, CSS variable restructuring, and cross-cutting updates across existing components require careful verification for consistency and unintended side effects. Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
d6dda34 to
e28fb20
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (15)
docs/color-system.md-108-137 (1)
108-137:⚠️ Potential issue | 🟡 MinorSpecify a language for the fenced block (markdownlint MD040).
At Line 108, add a language (e.g.
text) to keep docs lint-clean.✏️ Proposed fix
-``` +```text 브랜드 코어 ├── 로즈 핑크 `#f43f5e` (primary, ring, sidebar-primary) ... └── 레드 `#ef4444` (destructive, 위험)</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@docs/color-system.mdaround lines 108 - 137, The fenced code block in
docs/color-system.md lacks a language specifier (markdownlint MD040); update the
opening triple-backtick for the color palette block (the block showing "브랜드 코어",
"배경 계열 (라이트)", etc.) to include a language token such as text (e.g., changetotext) so the linter recognizes the block language and the doc stays
lint-clean.</details> </blockquote></details> <details> <summary>docs/typography.md-23-27 (1)</summary><blockquote> `23-27`: _⚠️ Potential issue_ | _🟡 Minor_ **Add a language tag to fenced code blocks for lint compliance.** Use an explicit language (e.g., `tsx` or `text`) to satisfy markdownlint MD040. <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@docs/typography.mdaround lines 23 - 27, The fenced code block containing
the example HTML/JSX snippet (매칭 대시보드
and소개 이력
) is missing an explicit language tag; update the opening fence to include a language (e.g., ```tsx or ```text) so the block complies with markdownlint MD040 and lints correctly. ````docs/typography.md-56-58 (1)
56-58:⚠️ Potential issue | 🟡 MinorHeading override guidance is internally contradictory.
Line [56]–Line [58] says heading size override is allowed, but Line [175]–Line [180] marks that exact pattern as forbidden. Please unify to one rule to avoid ambiguous implementation.
Also applies to: 175-180
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/typography.md` around lines 56 - 58, The documentation currently contradicts itself: the snippet under the "Tailwind로 크기 덮어쓰기도 가능 (예외적 상황)" heading that shows <h2 className="text-2xl">고정 크기 제목</h2> allows overriding heading sizes, while a later section (the one that marks that same pattern as forbidden) forbids it; pick one policy and make both sections consistent by either removing the permissive example and updating the "예외적 상황" text to explain why overrides are disallowed, or by updating the forbidden section to permit controlled Tailwind overrides and describing constraints; update the text surrounding the <h2 className="text-2xl"> example and the later forbidden rule so they convey the same unified rule and rationale..claude/rules/page-patterns.md-31-53 (1)
31-53:⚠️ Potential issue | 🟡 MinorSpecify a language for the fenced tree block (MD040).
Please change the unlabeled code fence to something like
```textfor markdownlint compliance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/rules/page-patterns.md around lines 31 - 53, The fenced tree block in page-patterns.md is unlabeled which breaks MD040; update the opening code fence to include a language/token (e.g., change the backticks before the tree to include "text" so it reads ```text) to make the block explicitly labeled and satisfy markdownlint; locate the unnamed fenced block that contains the app/ tree and replace its delimiter accordingly..claude/rules/page-patterns.md-35-40 (1)
35-40:⚠️ Potential issue | 🟡 MinorPublic route group example is missing
/auth/callback.The route tree under
(public)does not include/auth/callback, which is part of the stated target structure for this refactor. Please add it so the rules match the implemented routing model.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/rules/page-patterns.md around lines 35 - 40, The (public) route group listing is missing the /auth/callback route—add an entry for auth/callback/page.tsx under the (public) tree so the documented route structure matches the implemented routing model; update the list that currently shows page.tsx, login/page.tsx, auth/continue/page.tsx, and pending/page.tsx to include auth/callback/page.tsx alongside those entries.components/ui/kbd.tsx-16-24 (1)
16-24:⚠️ Potential issue | 🟡 MinorType mismatch:
KbdGroupacceptsdivprops but renders akbdelement.The component is typed with
React.ComponentProps<"div">but renders a<kbd>element. This mismatch could lead to type errors or unexpected behavior if div-specific attributes are passed.Suggested fix
-function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { +function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) { return ( <kbd data-slot="kbd-group" className={cn("inline-flex items-center gap-1", className)} {...props} /> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/kbd.tsx` around lines 16 - 24, KbdGroup is typed as React.ComponentProps<"div"> but renders a <kbd>, causing a props/type mismatch; change the prop type to React.ComponentProps<"kbd"> (or React.ComponentPropsWithoutRef<"kbd">) and update the function signature for KbdGroup to use that type so only valid kbd attributes are accepted while preserving className and ...props forwarding.components/ui/slider.tsx-16-24 (1)
16-24:⚠️ Potential issue | 🟡 MinorMismatch between value prop and rendered thumbs count.
The component passes
valueanddefaultValuedirectly toSliderPrimitive.Root(lines 30-31), but computes_valuesindependently for rendering the thumbs (line 47). The fallback logic in_valuescreates a mismatch:
- If
value={50}(single number) is passed,SliderPrimitive.Rootreceivesvalue={50}- But
_valuesbecomes[0, 100], rendering 2 thumbs instead of 1- The underlying component state and rendered UI are out of sync
Apply the suggested fix to handle single-value cases explicitly:
Suggested improvement
const _values = React.useMemo( () => Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue + : typeof value === "number" + ? [value] + : typeof defaultValue === "number" + ? [defaultValue] : [min, max], [value, defaultValue, min, max] )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/slider.tsx` around lines 16 - 24, The bug is that _values computes a fallback array [min, max] and can render two thumbs while SliderPrimitive.Root receives the raw value/defaultValue (a single number), causing UI/state mismatch; fix it by normalizing incoming value and defaultValue to arrays consistently and using that normalized array everywhere: update the logic around _values (and the props passed to SliderPrimitive.Root) to treat a single-number value or defaultValue as a single-element array (e.g., if typeof value === 'number' => [value]) and only fall back to [min, max] when neither value nor defaultValue is provided, ensuring the rendered thumbs count matches the value shape; adjust references to _values, value, defaultValue, and SliderPrimitive.Root so they use the same normalized array form.components/ui/carousel.tsx-96-105 (1)
96-105:⚠️ Potential issue | 🟡 MinorMissing cleanup for
reInitevent listener.The effect subscribes to both
reInitandselectevents but only cleans up theselectlistener. This could cause memory leaks or stale callbacks if the component remounts.🐛 Proposed fix
React.useEffect(() => { if (!api) return onSelect(api) api.on("reInit", onSelect) api.on("select", onSelect) return () => { + api?.off("reInit", onSelect) api?.off("select", onSelect) } }, [api, onSelect])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/carousel.tsx` around lines 96 - 105, The effect in React.useEffect attaches both "reInit" and "select" listeners on api using onSelect but only removes the "select" handler; update the cleanup to also detach the "reInit" listener by calling api.off("reInit", onSelect) (or api?.off for safety) alongside the existing api.off("select", onSelect) so both subscriptions are removed when the effect unmounts or dependencies change.components/ui/toggle-group.tsx-43-47 (1)
43-47:⚠️ Potential issue | 🟡 MinorFix the gap utility syntax and CSS variable units.
The class
gap-[--spacing(var(--gap))]has two issues:
--spacing()is a Tailwind v4 function that expects numeric literals (e.g.,--spacing(4)), not CSS variables. Using--spacing(var(--gap))is invalid.- The
--gapCSS variable is set to a unitless number, but CSS gap requires units.Consider this fix:
Suggested change
- style={{ "--gap": spacing } as React.CSSProperties} + style={{ "--gap": `${spacing}px` } as React.CSSProperties} className={cn( - "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch", + "group/toggle-group flex w-fit flex-row items-center gap-[var(--gap)] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",Or use Tailwind v4's native CSS variable syntax:
gap-(--gap)(also requires units in the CSS variable).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/toggle-group.tsx` around lines 43 - 47, Replace the invalid Tailwind gap utility and make the CSS variable include units: change the class string token "gap-[--spacing(var(--gap))]" to "gap-[var(--gap)]" (or Tailwind v4 syntax "gap-(--gap)") and update the inline style that sets "--gap" so it contains units (e.g., change style={{ "--gap": spacing }} to style={{ "--gap": typeof spacing === "number" ? `${spacing}px` : spacing }}), keeping the React.CSSProperties cast; this ensures the gap uses a valid CSS variable value with units and a valid Tailwind utility.components/ui/empty.tsx-71-80 (1)
71-80:⚠️ Potential issue | 🟡 MinorRender
EmptyDescriptionas<p>to match its declared prop type and semantics.
React.ComponentProps<"p">is declared, but adivis rendered.💡 Suggested fix
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { return ( - <div + <p data-slot="empty-description" className={cn( "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className )} {...props} - /> + /> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/empty.tsx` around lines 71 - 80, The EmptyDescription component currently declares props as React.ComponentProps<"p"> but renders a <div>, so change the rendered element in the EmptyDescription function from a div to a p, preserving the data-slot="empty-description", className composition via cn(..., className), and spreading {...props} so semantics and prop typing match; update only the JSX tag used by the EmptyDescription function.components/ui/command.tsx-50-66 (1)
50-66:⚠️ Potential issue | 🟡 Minor
DialogHeaderis rendered outsideDialogContent, breaking the modal structure.The
DialogHeader(lines 52-55) is placed as a sibling toDialogContent, but it should be rendered inside the content for proper DOM structure and accessibility. While thesr-onlyclass hides it visually, placing it outside the portal means it may not be associated with the dialog correctly.🛠️ Suggested fix
return ( <Dialog {...props}> - <DialogHeader className="sr-only"> - <DialogTitle>{title}</DialogTitle> - <DialogDescription>{description}</DialogDescription> - </DialogHeader> <DialogContent className={cn( "top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0", className )} showCloseButton={showCloseButton} > + <DialogHeader className="sr-only"> + <DialogTitle>{title}</DialogTitle> + <DialogDescription>{description}</DialogDescription> + </DialogHeader> {children} </DialogContent> </Dialog> )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/command.tsx` around lines 50 - 66, DialogHeader is currently rendered as a sibling of DialogContent which breaks modal semantics and accessibility; move the DialogHeader (including DialogTitle and DialogDescription) inside the DialogContent component so it is part of the dialog portal and associated with the dialog element. Update the return in the component that renders Dialog, placing the DialogHeader (with its sr-only class) immediately inside DialogContent (before children) and keep existing props like className and showCloseButton unchanged; ensure DialogTitle and DialogDescription remain children of DialogHeader so screen readers can associate them with the Dialog.components/ui/breadcrumb.tsx-99-114 (1)
99-114:⚠️ Potential issue | 🟡 Minor
aria-hidden="true"hides the screen reader text.The
<span className="sr-only">More</span>on line 112 is intended for screen readers, but it's nested inside a parent witharia-hidden="true"(line 103), which hides the entire subtree from assistive technologies. The "More" label will not be announced.♿ Suggested fix — move sr-only text outside aria-hidden container
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) { return ( - <span - data-slot="breadcrumb-ellipsis" - role="presentation" - aria-hidden="true" - className={cn( - "flex size-5 items-center justify-center [&>svg]:size-4", - className - )} - {...props} - > - <MoreHorizontalIcon - /> + <> <span className="sr-only">More</span> - </span> + <span + data-slot="breadcrumb-ellipsis" + role="presentation" + aria-hidden="true" + className={cn( + "flex size-5 items-center justify-center [&>svg]:size-4", + className + )} + {...props} + > + <MoreHorizontalIcon /> + </span> + </> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/breadcrumb.tsx` around lines 99 - 114, The "More" screen-reader text is inside a container with aria-hidden="true" so it will be ignored; update the JSX so the aria-hidden is applied only to the icon element (MoreHorizontalIcon) or the inner element with data-slot="breadcrumb-ellipsis", and move the <span className="sr-only">More</span> outside that aria-hidden subtree (as a sibling) so assistive tech can announce it; locate the return block that renders the element with data-slot="breadcrumb-ellipsis" and MoreHorizontalIcon and adjust those two elements accordingly.components/ui/pagination.tsx-101-120 (1)
101-120:⚠️ Potential issue | 🟡 MinorSame accessibility issue:
aria-hiddenhides screen reader text.The
<span className="sr-only">More pages</span>is nested inside a span witharia-hidden(shorthand foraria-hidden="true"), which prevents assistive technologies from announcing it.♿ Suggested fix
function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) { return ( - <span - aria-hidden - data-slot="pagination-ellipsis" - className={cn( - "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4", - className - )} - {...props} - > - <MoreHorizontalIcon - /> + <> <span className="sr-only">More pages</span> - </span> + <span + aria-hidden="true" + data-slot="pagination-ellipsis" + className={cn( + "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + > + <MoreHorizontalIcon /> + </span> + </> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/pagination.tsx` around lines 101 - 120, PaginationEllipsis currently sets aria-hidden on the wrapper span which hides the nested screen-reader text; remove the aria-hidden attribute from the outer span in PaginationEllipsis so the "sr-only" label can be announced, and instead mark the decorative icon as hidden from assistive tech (set aria-hidden or equivalent on MoreHorizontalIcon and/or make it focusable={false}) so the visual icon is ignored but the "More pages" sr-only text remains reachable.components/ui/field.tsx-193-199 (1)
193-199:⚠️ Potential issue | 🟡 MinorDeduplication maps
undefinedmessages to a single entry, and use===for strict comparison.Two issues:
- Line 194 uses
error?.messageas the Map key. If multiple errors haveundefinedmessages, they collapse into one entry—likely intended, but worth noting.- Line 197 uses
==instead of===. Use strict equality for consistency.Proposed fix
- if (uniqueErrors?.length == 1) { + if (uniqueErrors?.length === 1) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/field.tsx` around lines 193 - 199, The dedupe logic uses error?.message as the Map key which collapses multiple errors with undefined messages into a single entry and it uses loose equality when checking count; update the Map creation to use a key that preserves distinct undefined-message errors (for example use error?.message when defined, otherwise a per-item unique marker such as including the index or a Symbol) so undefined messages don't all collapse, and change the length check from == to ===; refer to the variables uniqueErrors and errors and the Map(...) creation and the subsequent if (uniqueErrors?.length == 1) check to make these changes.components/ui/sidebar.tsx-600-636 (1)
600-636:⚠️ Potential issue | 🟡 MinorPotential SSR hydration mismatch with random skeleton width.
useState(() => Math.floor(Math.random() * 40) + 50}%})initializes differently on server vs client, causing a hydration mismatch warning in React. Since this is a"use client"component, it may work in Next.js App Router, but the initial server render will differ from client.Consider using
useEffectto set the random width after mount, or use a deterministic value based on an index prop.Proposed fix using useEffect
function SidebarMenuSkeleton({ className, showIcon = false, ...props }: React.ComponentProps<"div"> & { showIcon?: boolean }) { - // Random width between 50 to 90%. - const [width] = React.useState(() => { - return `${Math.floor(Math.random() * 40) + 50}%` - }) + const [width, setWidth] = React.useState("70%") + + React.useEffect(() => { + // Random width between 50 to 90%. + setWidth(`${Math.floor(Math.random() * 40) + 50}%`) + }, [])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/sidebar.tsx` around lines 600 - 636, SidebarMenuSkeleton currently seeds a random width in useState during render which can differ between server and client and cause React hydration mismatches; change it to set the random width only after mount (e.g., move the random assignment into a useEffect that calls setWidth) or accept a deterministic prop (e.g., index or width) so the initial render is identical on server and client. Locate the SidebarMenuSkeleton function and replace the current useState initializer for width with a stable initial value (or undefined) and populate the actual random width inside useEffect (or derive it from the provided prop) so the component renders the same on server and client and updates only on the client after mount.
🧹 Nitpick comments (21)
hooks/use-mobile.ts (1)
6-14: Usemql.matchesand avoid initial false-negative state.Line 6 initializes state to
undefinedand Line 18 coerces it tofalsevia!!isMobile, causing a false-negative on first render until the effect runs and corrects the value. Additionally, Lines 11 and 14 should read frommql.matches(the subscribed query's actual state) instead of recomputing withwindow.innerWidth.♻️ Proposed refactor
export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) + const [isMobile, setIsMobile] = React.useState<boolean>(() => { + if (typeof window === "undefined") return false + return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches + }) React.useEffect(() => { const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + const onChange = (event: MediaQueryListEvent) => { + setIsMobile(event.matches) } mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + setIsMobile(mql.matches) return () => mql.removeEventListener("change", onChange) }, []) - return !!isMobile + return isMobile }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-mobile.ts` around lines 6 - 14, The hook currently initializes isMobile to undefined and then uses window.innerWidth causing a false-negative until the effect runs; fix by deriving the initial state and updates from the MediaQueryList.matches value instead of recomputing window.innerWidth: create the mql with window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) (guarded for SSR), initialize useState with mql.matches when window exists, use onChange = () => setIsMobile(mql.matches) (or read the event.matches when handling the listener) and replace any window.innerWidth checks with mql.matches; keep using setIsMobile and the existing MOBILE_BREAKPOINT and mql variable names so the change is localized to the useEffect and state initialization.components/ui/collapsible.tsx (1)
5-19: Minor: Simplify props destructuring.The
{ ...props }pattern in function parameters is redundant when you're just spreading them back. You can usepropsdirectly.✨ Suggested simplification
-function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { +function Collapsible(props: CollapsiblePrimitive.Root.Props) { return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} /> } -function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) { +function CollapsibleTrigger(props: CollapsiblePrimitive.Trigger.Props) { return ( <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} /> ) } -function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) { +function CollapsibleContent(props: CollapsiblePrimitive.Panel.Props) { return ( <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} /> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/collapsible.tsx` around lines 5 - 19, The parameter destructuring "{ ...props }" is redundant in Collapsible, CollapsibleTrigger, and CollapsibleContent; change each function signature to accept the props object directly (e.g., use "props: CollapsiblePrimitive.Root.Props" for Collapsible, "props: CollapsiblePrimitive.Trigger.Props" for CollapsibleTrigger, and "props: CollapsiblePrimitive.Panel.Props" for CollapsibleContent) and keep the existing JSX that spreads props into the underlying CollapsiblePrimitive components..claude/plans/route-group-permission.md (1)
25-25: Add language specifier to fenced code block.The directory structure code block is missing a language identifier. While
plaintextortextwould work, an empty language is flagged by markdownlint.📝 Suggested fix
-``` +```text app/ ├── layout.tsx # 유지🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/plans/route-group-permission.md at line 25, The fenced code block containing the directory structure (the block starting with "app/") lacks a language specifier; update its opening backticks to include a language (e.g., use ```text or ```plaintext) so markdownlint stops flagging it — locate the code block in .claude/plans/route-group-permission.md and change the opening ``` to ```text (or ```plaintext) while leaving the block contents unchanged.package.json (1)
32-32: Consider movingshadcntodevDependencies.The
shadcnpackage is a CLI tool used for scaffolding and adding components during development. It's not used at runtime, so it would be more appropriate indevDependenciesto reduce production bundle concerns.🔧 Suggested change
"dependencies": { ... - "shadcn": "^4.1.2", ... }, "devDependencies": { + "shadcn": "^4.1.2", ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 32, Move the "shadcn" dependency from runtime dependencies to devDependencies in package.json: open package.json, remove the "shadcn": "^4.1.2" entry from the top-level "dependencies" object and add the same entry under "devDependencies" (create "devDependencies" block if absent), then run your package manager's install (npm/yarn/pnpm) to update lockfile so the change takes effect; ensure any CI or build scripts that expect shadcn remain able to access it from devDependencies.components/profile-past-match-records.tsx (1)
54-67: Consider using semantic color tokens instead ofslate-*here.This keeps the new design system easier to theme globally.
🎨 Suggested refactor
-<Badge variant="secondary" className="rounded-full bg-slate-200 text-slate-600"> +<Badge variant="secondary" className="rounded-full bg-muted text-muted-foreground"> ... - className="h-auto px-1 py-0 text-[11px] font-medium text-slate-400 hover:text-slate-700" + className="h-auto px-1 py-0 text-[11px] font-medium text-muted-foreground hover:text-foreground"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/profile-past-match-records.tsx` around lines 54 - 67, Replace the hard-coded slate-* utility classes in the ProfilePastMatchRecords UI (Badge, the date <span>, and the Button className) with semantic design-token classes so the component follows the new theme system; specifically update the Badge's className (currently "rounded-full bg-slate-200 text-slate-600") to use semantic tokens like "rounded-full bg-[color-bg-muted] text-[color-text-muted]" (or your project's equivalent token names), change the date span "text-xs font-medium text-slate-500" to a semantic token such as "text-xs font-medium text-[color-text-secondary]", and replace the Button's "text-[11px] font-medium text-slate-400 hover:text-slate-700" with token-based classes like "text-[11px] font-medium text-[color-text-subtle] hover:text-[color-text-default]"; also ensure any referenced token names match your tailwind/config mapping so theming works across the app.components/gender-toggle-field.tsx (1)
46-60: Extract duplicated option class to reduce drift risk.The class string for both gender option labels is identical; pulling it into a shared constant will make future style changes safer.
♻️ Suggested refactor
+const optionClass = + "flex min-h-12 items-center justify-center rounded-2xl border border-border bg-card/90 px-4 text-sm font-semibold text-secondary-foreground transition peer-checked:border-accent peer-checked:bg-gradient-to-r peer-checked:from-accent peer-checked:to-primary peer-checked:text-foreground group-hover:-translate-y-0.5"; ... - <span className="flex min-h-12 items-center justify-center rounded-2xl border border-border bg-card/90 px-4 text-sm font-semibold text-secondary-foreground transition peer-checked:border-accent peer-checked:bg-gradient-to-r peer-checked:from-accent peer-checked:to-primary peer-checked:text-foreground group-hover:-translate-y-0.5"> + <span className={optionClass}> 남 </span> ... - <span className="flex min-h-12 items-center justify-center rounded-2xl border border-border bg-card/90 px-4 text-sm font-semibold text-secondary-foreground transition peer-checked:border-accent peer-checked:bg-gradient-to-r peer-checked:from-accent peer-checked:to-primary peer-checked:text-foreground group-hover:-translate-y-0.5"> + <span className={optionClass}> 여 </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/gender-toggle-field.tsx` around lines 46 - 60, In components/gender-toggle-field.tsx the long duplicated class string used on both option <span> elements should be pulled into a shared constant (e.g., optionClass or GENDER_OPTION_CLASS) so style updates aren’t missed; define the constant near the top of the GenderToggleField component and replace the duplicated className values on both spans with that constant, leaving the input props (name, value, defaultChecked, required) unchanged..claude/rules/page-patterns.md (1)
58-59: Update example file-path comments to grouped App Router paths.Several example headers still point to pre-group paths (e.g.,
app/profiles/[id]/page.tsx). For consistency with the new permission route groups, these should use grouped paths.Also applies to: 90-91, 110-111, 139-140
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/rules/page-patterns.md around lines 58 - 59, The example header paths still use pre-grouped App Router paths like "app/profiles/[id]/page.tsx"; update these example file-path comments to the new grouped App Router form (e.g., include the appropriate route group wrapper such as "(profiles)" so the example becomes "app/(profiles)/[id]/page.tsx"), and apply the same replacement to the other occurrences noted (the other example headers referenced in the comment). Locate and replace the literal example strings in .claude/rules/page-patterns.md (instances of "app/profiles/[id]/page.tsx" and the other similar examples) so all sample paths reflect the grouped App Router convention.components/ui/slider.tsx (1)
47-53: Consider using a stable key instead of array index.Using
indexas a key for Thumb elements may cause issues if the slider values are reordered dynamically. If the slider supports drag-to-reorder or dynamic value changes that could swap positions, consider using a more stable key.However, for typical range slider usage where thumb positions are fixed, this is acceptable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/slider.tsx` around lines 47 - 53, Use stable keys for the SliderPrimitive.Thumb elements instead of the array index: when rendering thumbs from _values, derive a stable identifier (e.g., the actual numeric value _values[index], or an associated id if values are objects) and use that as the key for SliderPrimitive.Thumb so reordering or dynamic updates won’t cause incorrect reconciliation; update the map call that currently uses key={index} to use key based on the value/id.components/match-detail-modal.tsx (2)
5-5: Consider migrating totypes/domain.ts.The import uses
@/lib/typeswhich is being phased out according to coding guidelines. When updating this component, consider migrating totypes/domain.tsfor domain types.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/match-detail-modal.tsx` at line 5, Update the import of domain types in MatchDetailModal: replace the current import line that brings in Candidate, MatchOutcome, and TimelineEvent from "@/lib/types" with an import from the new domain types module (types/domain.ts); locate the import statement in components/match-detail-modal.tsx where Candidate, MatchOutcome, and TimelineEvent are referenced and change the path so the component uses the canonical domain types file.
201-206: Redundantopencondition since early return already handles falsyevent.Line 201 returns
nullwheneventis falsy, soBoolean(event)on line 206 will always betruewhen the Dialog renders. This is harmless but slightly redundant.🔧 Simplify by removing the redundant condition
return ( - <Dialog open={Boolean(event)} onOpenChange={(open) => { if (!open) onClose(); }}> + <Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/match-detail-modal.tsx` around lines 201 - 206, The Dialog's open prop is redundant because you already early-return when event is falsy; replace open={Boolean(event)} with a constant true (open={true}) or simply open so the Dialog always mounts when event exists. Update the JSX in components/match-detail-modal.tsx where Dialog is rendered (the Dialog component and its onOpenChange prop remain unchanged) to remove the Boolean(event) expression.components/ui/select.tsx (1)
164-167: Minor: Unusual formatting on icon elements.The
ChevronUpIconandChevronDownIconhave empty space between the opening tag and closing bracket. This is functional but inconsistent with the rest of the file's formatting.🔧 Tidy up icon formatting
> - <ChevronUpIcon - /> + <ChevronUpIcon /> </SelectPrimitive.ScrollUpArrow>> - <ChevronDownIcon - /> + <ChevronDownIcon /> </SelectPrimitive.ScrollDownArrow>Also applies to: 183-186
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/select.tsx` around lines 164 - 167, The icon elements have extra whitespace/newlines between the tag name and the self-closing slash which is inconsistent with the file; update the instances of ChevronUpIcon and ChevronDownIcon inside SelectPrimitive.ScrollUpArrow and SelectPrimitive.ScrollDownArrow so they are formatted as single-line self-closing JSX tags (e.g., <ChevronUpIcon />) matching the rest of the file's style; ensure both occurrences (the up arrow block and the down arrow block) are corrected.components/ui/progress.tsx (1)
19-24: Auto-includedProgressTrackmay cause unexpected duplication.The
Progresscomponent renders both{children}and a hardcoded<ProgressTrack><ProgressIndicator /></ProgressTrack>. If a consumer passes a customProgressTrackas children, they'll get duplicate tracks.Consider either:
- Document that children should only be
ProgressLabel/ProgressValue- Or remove the auto-included track and require consumers to compose explicitly
♻️ Option: Make composition explicit
function Progress({ className, children, value, ...props }: ProgressPrimitive.Root.Props) { return ( <ProgressPrimitive.Root value={value} data-slot="progress" className={cn("flex flex-wrap gap-3", className)} {...props} > {children} - <ProgressTrack> - <ProgressIndicator /> - </ProgressTrack> </ProgressPrimitive.Root> ) }Then consumers would use:
<Progress value={50}> <ProgressLabel>Loading...</ProgressLabel> <ProgressValue /> <ProgressTrack> <ProgressIndicator /> </ProgressTrack> </Progress>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/progress.tsx` around lines 19 - 24, The Progress component currently renders both {children} and a hardcoded ProgressTrack/ProgressIndicator which causes duplicate tracks if a consumer passes a custom ProgressTrack; update Progress to remove the auto-included <ProgressTrack><ProgressIndicator/></ProgressTrack> and require consumers to compose their own track/indicator (document that children may include ProgressLabel, ProgressValue, and an explicit ProgressTrack containing ProgressIndicator), or alternatively add a prop (e.g., autoTrack = false) to opt into the included track; locate the logic in the Progress component where children are rendered and remove or gate the hardcoded ProgressTrack/ProgressIndicator to prevent duplication.components/ui/scroll-area.tsx (1)
19-26: Consider exposing scrollbar configuration for flexibility.The
ScrollAreacomponent hardcodes a single verticalScrollBar. If horizontal scrolling or dual-axis scrolling is needed, consumers would need to modify this component or create custom compositions.This is acceptable for simple use cases, but consider exposing an
orientationprop or allowing children to include customScrollBarelements if more flexibility is needed later.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/scroll-area.tsx` around lines 19 - 26, The ScrollArea currently always renders a single vertical ScrollBar which prevents horizontal or dual-axis setups; update the ScrollArea component to accept an optional prop (e.g., orientation: 'vertical' | 'horizontal' | 'both' = 'vertical') and use it to conditionally render ScrollBar instances (e.g., render <ScrollBar orientation="vertical" /> and/or <ScrollBar orientation="horizontal" />), or alternatively accept an optional prop like customScrollBars: ReactNode to allow consumers to pass their own ScrollBar elements; modify the render around ScrollAreaPrimitive.Viewport / ScrollBar so the default behavior stays the same but consumers can enable horizontal or custom scrollbars via the new prop (refer to ScrollArea component, ScrollBar, and ScrollAreaPrimitive.Viewport to locate the changes).components/ui/menubar.tsx (1)
38-52: Consider delegating menubar layers to menubar-native primitives instead of menu-based wrappers.
MenubarPrimitive(which provides roving-focus and menubar keyboard semantics) at the root is composed withDropdownMenu*wrappers that delegate toMenuPrimitivefrom@base-ui/react/menu. This creates a semantic mismatch: menubar-specific keyboard navigation (e.g., arrow keys, roving focus) may not function as expected when descendants use generic menu primitives instead of menubar-native ones.Consider refactoring
MenubarMenu,MenubarGroup, andMenubarPortalto use menubar-native primitives from@base-ui/react/menubar(similar to howMenubarCheckboxItemandMenubarRadioItemuse directMenuPrimitivewrappers for their semantics) rather than delegating to menu-basedDropdownMenu*components.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/menubar.tsx` around lines 38 - 52, The menubar wrappers MenubarMenu, MenubarGroup, and MenubarPortal currently delegate to DropdownMenu*, causing a semantic mismatch with the MenubarPrimitive root; replace those wrappers to use the menubar-native primitives from `@base-ui/react/menubar` (e.g., MenubarMenuPrimitive / MenubarGroupPrimitive / MenubarPortalPrimitive or equivalent exports) so the component tree honors roving-focus and menubar keyboard semantics; follow the pattern used by MenubarCheckboxItem and MenubarRadioItem which wrap MenuPrimitive directly, and update imports and JSX for MenubarMenu, MenubarGroup, and MenubarPortal to render the menubar-native primitives instead of DropdownMenu, DropdownMenuGroup, and DropdownMenuPortal.components/ui/table.tsx (1)
1-1: Consider removing"use client"directive.These table components are stateless wrappers that don't use client-side hooks or browser APIs. They could function as Server Components, reducing client bundle size. The
"use client"directive is only needed if consumers require client-side interactivity, which can be handled by wrapping at the usage site.Based on learnings: "Use Server Components by default in Next.js App Router; add
"use client"only to leaf components to minimize client bundle size."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/table.tsx` at line 1, Remove the top-level "use client" directive from this file so the table components become Server Components; confirm that none of the exported components (e.g., Table, TableHeader, TableBody, TableRow, TableCell or similarly named stateless wrappers in this file) use client-side hooks or browser APIs, and if any consumer requires client-side interactivity add "use client" at the usage site or to a small interactive wrapper component instead.components/ui/carousel.tsx (1)
78-89: Consider adding ArrowUp/ArrowDown for vertical orientation.Keyboard navigation only handles
ArrowLeft/ArrowRight. For vertical carousels, users might intuitively expectArrowUp/ArrowDownto navigate slides.♻️ Optional enhancement
const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { - if (event.key === "ArrowLeft") { + if (event.key === "ArrowLeft" || (orientation === "vertical" && event.key === "ArrowUp")) { event.preventDefault() scrollPrev() - } else if (event.key === "ArrowRight") { + } else if (event.key === "ArrowRight" || (orientation === "vertical" && event.key === "ArrowDown")) { event.preventDefault() scrollNext() } }, - [scrollPrev, scrollNext] + [scrollPrev, scrollNext, orientation] )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/carousel.tsx` around lines 78 - 89, handleKeyDown currently only maps ArrowLeft/ArrowRight to scrollPrev/scrollNext; add handling for ArrowUp/ArrowDown when the carousel is vertical so vertical carousels respond to expected keys. Update the handleKeyDown callback to check the carousel orientation (e.g., a prop or state like orientation === "vertical") and call scrollPrev on ArrowUp and scrollNext on ArrowDown while preserving the existing ArrowLeft/ArrowRight behavior; reference the handleKeyDown function and the scrollPrev/scrollNext callbacks so the new branches integrate with the existing keyboard handling.components/ui/dropdown-menu.tsx (1)
136-145:data-slotprop is not applied to the popup element.The
data-slot="dropdown-menu-sub-content"passed on line 137 will be spread ontoDropdownMenuContent's outer element, butDropdownMenuContentinternally setsdata-slot="dropdown-menu-content"on theMenuPrimitive.Popup(line 43). The submenu-specific slot identifier won't reach the actual popup element.Consider adding explicit support for a
data-slotoverride inDropdownMenuContentif distinguishing submenus is needed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/dropdown-menu.tsx` around lines 136 - 145, The DropdownMenuContent component currently forces data-slot="dropdown-menu-content" on MenuPrimitive.Popup, so a passed prop data-slot (e.g. "dropdown-menu-sub-content") from the parent never reaches the actual popup; update DropdownMenuContent to accept and forward an overriding data-slot prop to MenuPrimitive.Popup (preserve the default "dropdown-menu-content" when no override provided) and ensure the prop is applied to the outer popup element instead of hard-coding the attribute in the component implementation.components/ui/context-menu.tsx (1)
145-152: Samedata-slotoverride issue as dropdown menu.The
data-slot="context-menu-sub-content"on line 147 won't reach the actual popup element sinceContextMenuContentsets its owndata-slotinternally.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/context-menu.tsx` around lines 145 - 152, The data-slot attribute on the ContextMenuContent JSX won’t be applied because ContextMenuContent renders its own element and overrides data-slot; change to use the component’s asChild prop and wrap it with a native element (e.g., <div> or <span>) that sets data-slot="context-menu-sub-content" so the attribute lands on the real popup node (modify the ContextMenuContent usage in components/ui/context-menu.tsx to use asChild and a wrapped element with the data-slot, preserving className, side and spreading {...props} onto the wrapper).components/ui/input-group.tsx (1)
57-61: Click handler assumes single input per group.The
querySelector("input")will only focus the first input element if multiple exist within the group. This is likely fine for typical usage patterns, but worth noting if complex multi-input groups are planned.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/input-group.tsx` around lines 57 - 61, The onClick handler in the InputGroup component currently uses parentElement?.querySelector("input") which only targets the first input and fails for multi-input groups; update the handler (the arrow function passed to onClick) to collect inputs via parentElement?.querySelectorAll("input") and then pick the appropriate element to focus (e.g., the first visible, enabled input: filter out inputs with type="hidden" and disabled, then call focus() on the first match) to ensure the correct input is focused when multiple inputs exist.components/ui/combobox.tsx (1)
215-230: Redundant type intersection inComboboxChipsprops.
React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips>andComboboxPrimitive.Chips.Propslikely overlap significantly. Consider using just one to avoid type complexity.Suggested simplification
function ComboboxChips({ className, ...props -}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & - ComboboxPrimitive.Chips.Props) { +}: ComboboxPrimitive.Chips.Props & { className?: string }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/combobox.tsx` around lines 215 - 230, The props type for ComboboxChips is overly complex due to the intersection of React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> and ComboboxPrimitive.Chips.Props; simplify by using a single source of truth (preferably React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips>) for the function signature in ComboboxChips, removing the redundant ComboboxPrimitive.Chips.Props part and keeping the rest of the component implementation unchanged so types remain accurate and less noisy.components/ui/chart.tsx (1)
94-115:dangerouslySetInnerHTMLusage is acceptable here but document the trust boundary.The generated CSS is constructed from
ChartConfigkeys and color values. SinceChartConfigis typically defined by developers (not user input), XSS risk is minimal. However, if config keys or colors ever derive from untrusted sources, this could inject arbitrary CSS or escape the style block.Consider adding a brief comment documenting that
configkeys and color values must be trusted/sanitized if sourced externally.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/ui/chart.tsx` around lines 94 - 115, Add a short inline comment near the style block that uses dangerouslySetInnerHTML (the block that builds CSS from THEMES, id and colorConfig) documenting the trust boundary: state that ChartConfig keys and color values must be developer-controlled or sanitized before use, and if they can be derived from external/untrusted sources they must be validated/escaped to prevent CSS/HTML injection; reference the variables THEMES, id and colorConfig in the comment so future readers know which inputs require trust or sanitization.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 837e1a03-cd25-457b-bbf8-06a5e51cbbd1
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (106)
.claude/plans/route-group-permission.md.claude/plans/shadcn-ui-migration.md.claude/rules/page-patterns.md.claude/rules/ui-components.mdapp/(admin)/candidates/new/page.tsxapp/(admin)/profiles/[id]/edit/page.tsxapp/(admin)/profiles/[id]/loading.tsxapp/(admin)/profiles/[id]/page.tsxapp/(member)/dashboard/loading.tsxapp/(member)/dashboard/page.tsxapp/(member)/timeline/page.tsxapp/(public)/auth/callback/route.tsapp/(public)/auth/continue/page.tsxapp/(public)/login/page.tsxapp/(public)/page.tsxapp/(public)/pending/page.tsxapp/(super-admin)/admin/page.tsxapp/admin/page.tsxapp/candidates/new/page.tsxapp/globals.cssapp/layout.tsxcomponents.jsoncomponents/account-panel.tsxcomponents/auth-forms.tsxcomponents/back-nav-button.tsxcomponents/candidate-card.tsxcomponents/dashboard-flow-board.tsxcomponents/dashboard-logout-button.tsxcomponents/form-submit-button.tsxcomponents/gender-toggle-field.tsxcomponents/home-account-shell.tsxcomponents/lazy-home-account-shell.tsxcomponents/manager-dashboard.tsxcomponents/match-detail-modal.tsxcomponents/match-history-list-modal.tsxcomponents/operator-desk-controls.tsxcomponents/pending-invite-view.tsxcomponents/photo-upload-field.tsxcomponents/profile-interactive-gallery.tsxcomponents/profile-match-kanban.tsxcomponents/profile-past-match-records.tsxcomponents/splash-intro.tsxcomponents/status-badge.tsxcomponents/ui/accordion.tsxcomponents/ui/alert-dialog.tsxcomponents/ui/alert.tsxcomponents/ui/aspect-ratio.tsxcomponents/ui/avatar.tsxcomponents/ui/badge.tsxcomponents/ui/breadcrumb.tsxcomponents/ui/button-group.tsxcomponents/ui/button.tsxcomponents/ui/calendar.tsxcomponents/ui/card.tsxcomponents/ui/carousel.tsxcomponents/ui/chart.tsxcomponents/ui/checkbox.tsxcomponents/ui/collapsible.tsxcomponents/ui/combobox.tsxcomponents/ui/command.tsxcomponents/ui/context-menu.tsxcomponents/ui/dialog.tsxcomponents/ui/direction.tsxcomponents/ui/drawer.tsxcomponents/ui/dropdown-menu.tsxcomponents/ui/empty.tsxcomponents/ui/field.tsxcomponents/ui/hover-card.tsxcomponents/ui/input-group.tsxcomponents/ui/input-otp.tsxcomponents/ui/input.tsxcomponents/ui/item.tsxcomponents/ui/kbd.tsxcomponents/ui/label.tsxcomponents/ui/menubar.tsxcomponents/ui/native-select.tsxcomponents/ui/navigation-menu.tsxcomponents/ui/pagination.tsxcomponents/ui/popover.tsxcomponents/ui/progress.tsxcomponents/ui/radio-group.tsxcomponents/ui/resizable.tsxcomponents/ui/scroll-area.tsxcomponents/ui/select.tsxcomponents/ui/separator.tsxcomponents/ui/sheet.tsxcomponents/ui/sidebar.tsxcomponents/ui/skeleton.tsxcomponents/ui/slider.tsxcomponents/ui/sonner.tsxcomponents/ui/spinner.tsxcomponents/ui/switch.tsxcomponents/ui/table.tsxcomponents/ui/tabs.tsxcomponents/ui/textarea.tsxcomponents/ui/toggle-group.tsxcomponents/ui/toggle.tsxcomponents/ui/tooltip.tsxdocs/color-system.mddocs/typography.mddocs/ui-tokens.mdhooks/use-mobile.tslib/cn.tslib/utils.tspackage.jsontsconfig.tsbuildinfo
💤 Files with no reviewable changes (2)
- app/candidates/new/page.tsx
- app/admin/page.tsx
| import { rejectMembership, updateMembershipRole } from "@/lib/admin-actions"; | ||
| import { getApprovedMemberships, getPendingMemberships } from "@/lib/data"; | ||
| import { requireMembershipRole } from "@/lib/permissions"; | ||
| import type { AppRole, Membership } from "@/lib/types"; |
There was a problem hiding this comment.
Use the domain model from types/domain.ts instead of legacy lib/types.
This new page introduces fresh dependencies on legacy @/lib/types and snake_case member fields, which conflicts with the ongoing domain-model migration and increases shape inconsistency in the app layer.
Suggested direction
-import type { AppRole, Membership } from "@/lib/types";
+import type { AppRole, Membership } from "@/types/domain";Then align usages to camelCase domain fields (e.g., userId, createdAt, fullName) in this page.
As per coding guidelines: **/*.{ts,tsx}: Use types/domain.ts for all new code; define domain types with camelCase fields (legacy lib/types.ts is being phased out).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(super-admin)/admin/page.tsx at line 8, The file imports legacy types
(AppRole, Membership) from "@/lib/types" and uses snake_case member fields;
replace that import to use the domain model from "types/domain.ts" and update
all usages in this module (e.g., any references to Membership, AppRole, and
member properties) to the camelCase domain fields such as userId, createdAt,
fullName (and other camelCased members) so the page aligns with the new domain
types and removes dependency on legacy "@/lib/types".
| import { cn } from "@/lib/utils"; | ||
|
|
||
| const pretendard = localFont({ | ||
| src: "../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2", |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🌐 Web query:
For Next.js 15, is using next/font/localwithsrcpointing to../node_modules/...officially supported/recommended in production, or is colocating fonts in app source (e.g.,app/fonts) the recommended approach?
💡 Result:
Colocating fonts in the app source (e.g., app/fonts) is the recommended approach for Next.js 15. Using next/font/local with src pointing to ../node_modules/... is not officially recommended or shown in examples, and may cause issues in production due to build-time resolution and deployment constraints.
Citations:
- 1: https://beta.nextjs.org/docs/optimizing/fonts
- 2: https://nextjs.im/docs/15/pages/api-reference/components/font
- 3: https://medium.com/frontendweb/how-to-use-local-fonts-in-next-js-15-a-beginners-guide-b787c34511af
- 4: https://medium.com/%40sharmanachatra/how-to-add-custom-fonts-in-next-js-15-step-by-step-guide-0934ce1858d0
- 5: https://nextjs.org/learn/dashboard-app/optimizing-fonts-images
Move font to project-local path instead of sourcing from ../node_modules/.
Next.js 15 recommends colocating fonts in the app source (e.g., app/fonts/) rather than referencing ../node_modules/. Using node_modules-relative paths can cause build-time resolution and deployment issues in production.
Suggested adjustment
- src: "../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2",
+ src: "./fonts/PretendardVariable.woff2",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/layout.tsx` at line 7, The font is referenced from node_modules using
src:
"../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2" in
app/layout.tsx which can break builds; copy the PretendardVariable.woff2 file
into a project-local directory (e.g., app/fonts/) and update the font reference
in app/layout.tsx (where you configure the font) to use the new local path
(e.g., "/fonts/PretendardVariable.woff2" or import from app/fonts) so Next.js
can resolve it at build and runtime.
| <form className="mt-6 grid gap-4" action={signInWithPassword}> | ||
| <FieldShell label="id"> | ||
| <Input | ||
| className="h-12 rounded-xl" | ||
| name="username" | ||
| placeholder="junseong" | ||
| required | ||
| minLength={4} | ||
| maxLength={20} | ||
| pattern="[a-z0-9._-]{4,20}" | ||
| /> | ||
| </FieldShell> | ||
| <FieldShell label="password"> | ||
| <Input | ||
| className="h-12 rounded-xl" | ||
| name="password" | ||
| type="password" | ||
| placeholder="비밀번호 입력" | ||
| required | ||
| /> | ||
| </FieldShell> | ||
|
|
||
| {tab === "signin" ? ( | ||
| <form className="mt-6 grid gap-4" action={signInWithPassword}> | ||
| <FieldShell label="id"> | ||
| <input | ||
| className={inputClassName} | ||
| name="username" | ||
| placeholder="junseong" | ||
| required | ||
| minLength={4} | ||
| maxLength={20} | ||
| pattern="[a-z0-9._-]{4,20}" | ||
| /> | ||
| </FieldShell> | ||
| <FieldShell label="password"> | ||
| <input | ||
| className={inputClassName} | ||
| name="password" | ||
| type="password" | ||
| placeholder="비밀번호 입력" | ||
| required | ||
| /> | ||
| </FieldShell> | ||
| <Button className="mt-2 h-12 rounded-full text-sm font-semibold" type="submit"> | ||
| 로그인 | ||
| </Button> | ||
| </form> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether form components currently include RHF/Zod usage.
fd -i 'form' components -e tsx -e ts | xargs rg -n --no-heading 'useForm|zodResolver|from "zod"|from '\''zod'\'''Repository: saechimdaeki/project-cupid
Length of output: 224
🏁 Script executed:
cat -n components/auth-forms.tsx | head -160Repository: saechimdaeki/project-cupid
Length of output: 6656
Implement React Hook Form + Zod validation for both auth forms.
The sign-in and sign-up forms currently use only native HTML validation. Project guidelines require components/**/*form*.{ts,tsx} to implement React Hook Form with Zod validation alongside Server Actions. Server Actions are already in place (signInWithPassword, signUpWithPassword); add RHF + Zod schemas for robust client and server validation.
Affects lines 64-89 (sign-in) and 109-138 (sign-up).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/auth-forms.tsx` around lines 64 - 89, Replace the native HTML
validation with React Hook Form + Zod: create zod schemas (e.g., signInSchema
and signUpSchema) matching the same rules (username pattern, min/max lengths,
password requirements), wire them into useForm via the zod resolver (useForm({
resolver: zodResolver(signInSchema) }) and similarly for signUpSchema), replace
native input attributes with RHF register/controlled inputs (use register for
"username" and "password" or Controller if needed) and call the existing Server
Actions (signInWithPassword and signUpWithPassword) from the form submit
handlers (handleSubmit) passing validated values; ensure FieldShell/Input/Button
components receive RHF props (error display from formState.errors) so
client-side validation uses Zod while server actions remain unchanged.
| style={ | ||
| { | ||
| "--ratio": ratio, | ||
| } as React.CSSProperties | ||
| } | ||
| className={cn("relative aspect-(--ratio)", className)} | ||
| {...props} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify call sites that pass inline style to AspectRatio (higher risk of override).
rg -nP --type=tsx -C2 '<AspectRatio\b[^>]*\bstyle='Repository: saechimdaeki/project-cupid
Length of output: 97
🏁 Script executed:
#!/bin/bash
# Find the aspect-ratio file and check its current state
find . -name "aspect-ratio.tsx" -o -name "aspect-ratio.ts" | head -20Repository: saechimdaeki/project-cupid
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Search for AspectRatio component usage (adjust for file type detection)
rg "AspectRatio" --type typescript --type typescriptreact -C2 | head -100Repository: saechimdaeki/project-cupid
Length of output: 104
🏁 Script executed:
#!/bin/bash
# Alternative: search by filename patterns
fd "aspect-ratio" -e tsx -e tsRepository: saechimdaeki/project-cupid
Length of output: 100
🏁 Script executed:
#!/bin/bash
# Read the aspect-ratio.tsx file to see current implementation
cat -n ./components/ui/aspect-ratio.tsxRepository: saechimdaeki/project-cupid
Length of output: 626
🏁 Script executed:
#!/bin/bash
# Search for AspectRatio usage without type filters
rg "AspectRatio" -C3 | head -150Repository: saechimdaeki/project-cupid
Length of output: 502
🏁 Script executed:
#!/bin/bash
# More comprehensive search for AspectRatio usage - search all files
rg "AspectRatio" --no-heading -B2 -A2Repository: saechimdaeki/project-cupid
Length of output: 425
🏁 Script executed:
#!/bin/bash
# Check if there are any imports of AspectRatio
rg "from.*aspect-ratio|import.*AspectRatio" -B1 -A1Repository: saechimdaeki/project-cupid
Length of output: 50383
🏁 Script executed:
#!/bin/bash
# List all tsx/ts files that might use AspectRatio
fd "\.(tsx|ts)$" | xargs rg "AspectRatio" 2>/dev/null | head -50Repository: saechimdaeki/project-cupid
Length of output: 177
🏁 Script executed:
#!/bin/bash
# Broader search for AspectRatio - look in all files for any reference
find . -type f \( -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" \) -not -path "./node_modules/*" | xargs grep -l "AspectRatio" 2>/dev/nullRepository: saechimdaeki/project-cupid
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Check if there's a tailwind.config file to understand custom class definitions
find . -type f -name "tailwind.config.*" | head -5Repository: saechimdaeki/project-cupid
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for any imports of AspectRatio in the codebase
rg "import.*AspectRatio|from.*aspect-ratio" --no-headingRepository: saechimdaeki/project-cupid
Length of output: 50383
Prevent accidental style override from caller props.
In React JSX, spreading ...props after an explicit style attribute means a caller-provided style prop will override the inline styles entirely, which can drop the --ratio CSS custom property. While this component currently has no callers, the fix is a defensive best practice.
💡 Proposed fix
function AspectRatio({
ratio,
className,
+ style,
...props
}: React.ComponentProps<"div"> & { ratio: number }) {
return (
<div
data-slot="aspect-ratio"
- style={
- {
- "--ratio": ratio,
- } as React.CSSProperties
- }
+ style={{ ...(style as React.CSSProperties), "--ratio": ratio } as React.CSSProperties}
className={cn("relative aspect-(--ratio)", className)}
{...props}
/>
)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| style={ | |
| { | |
| "--ratio": ratio, | |
| } as React.CSSProperties | |
| } | |
| className={cn("relative aspect-(--ratio)", className)} | |
| {...props} | |
| function AspectRatio({ | |
| ratio, | |
| className, | |
| style, | |
| ...props | |
| }: React.ComponentProps<"div"> & { ratio: number }) { | |
| return ( | |
| <div | |
| data-slot="aspect-ratio" | |
| style={{ ...(style || {}), "--ratio": ratio } as React.CSSProperties} | |
| className={cn("relative aspect-(--ratio)", className)} | |
| {...props} | |
| /> | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/aspect-ratio.tsx` around lines 11 - 17, The inline style
setting the CSS variable "--ratio" on the AspectRatio component can be
accidentally overridden because props are spread after the explicit style;
change the prop handling to merge any incoming style with the required "--ratio"
instead of letting incoming style replace it (e.g., extract style from props and
set style = { ...style, ["--ratio"]: ratio } cast as React.CSSProperties) and
continue to spread the remaining props (rest) so caller styles are preserved but
the "--ratio" custom property remains enforced; keep the className merge using
cn("relative aspect-(--ratio)", className) and spread the rest of props (not the
original props object) into the element.
| function BreadcrumbLink({ | ||
| className, | ||
| render, | ||
| ...props | ||
| }: useRender.ComponentProps<"a">) { | ||
| return useRender({ | ||
| defaultTagName: "a", | ||
| props: mergeProps<"a">( | ||
| { | ||
| className: cn("transition-colors hover:text-foreground", className), | ||
| }, | ||
| props | ||
| ), | ||
| render, | ||
| state: { | ||
| slot: "breadcrumb-link", | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check `@base-ui/react` useRender documentation and usage patterns
ast-grep --pattern 'useRender({
$$$
state: {
$$$
}
$$$
})'Repository: saechimdaeki/project-cupid
Length of output: 52
🏁 Script executed:
# First, examine the breadcrumb.tsx file to understand context and imports
cat -n components/ui/breadcrumb.tsxRepository: saechimdaeki/project-cupid
Length of output: 3530
🏁 Script executed:
# Search for useRender imports to find where the hook comes from
rg -i "useRender|import.*useRender" components/ui/Repository: saechimdaeki/project-cupid
Length of output: 1622
🏁 Script executed:
# Look for useRender hook definition or implementation in the codebase
fd useRender -t fRepository: saechimdaeki/project-cupid
Length of output: 52
🏁 Script executed:
# Search for other usages of useRender with state parameter
rg -B2 -A5 "state:\s*{" --type ts --type tsx | head -100Repository: saechimdaeki/project-cupid
Length of output: 153
🏁 Script executed:
# Search for useRender calls with state parameter across files
rg -A 10 "useRender\(\{" components/ui/ | grep -A 10 "state:"Repository: saechimdaeki/project-cupid
Length of output: 2506
🏁 Script executed:
# Check sidebar.tsx for useRender usage patterns
sed -n '1,100p' components/ui/sidebar.tsx | grep -A 15 "useRender"Repository: saechimdaeki/project-cupid
Length of output: 586
🏁 Script executed:
# Search for `@base-ui/react` types or documentation in node_modules or package info
fd package.json | xargs grep -l "@base-ui/react"Repository: saechimdaeki/project-cupid
Length of output: 82
🏁 Script executed:
# Check package.json for `@base-ui/react` version and dependencies
cat package.json | grep -A 50 "dependencies"Repository: saechimdaeki/project-cupid
Length of output: 1088
🏁 Script executed:
# Search for any tests or documentation about useRender in the codebase
fd -e test.ts -e spec.ts -e .md | xargs grep -l "useRender\|state.*slot" 2>/dev/nullRepository: saechimdaeki/project-cupid
Length of output: 54
🏁 Script executed:
# Check if there's a types directory or any type definitions related to useRender
fd -i "types\|types.ts" | head -20Repository: saechimdaeki/project-cupid
Length of output: 52
🌐 Web query:
@base-ui/react useRender hook state parameter data-slot attribute
💡 Result:
The @base-ui/react useRender hook enables a render prop in custom components for polymorphism and composition. It accepts a state parameter of type State, which is passed to the render callback function: render={(props, state) => <Element {...props} data-state={state.someState ? 'on' : 'off'} />}. This allows custom rendering based on component state. The state parameter, combined with stateAttributesMapping (type StateAttributesMapping), automatically generates data-* attributes on the rendered element from state properties. For example, boolean state keys like disabled produce data-disabled attributes (data-disabled="" when true). Custom mapping via stateAttributesMapping overrides defaults for converting state to data attributes. There is no data-slot attribute directly on useRender elements. Base UI components use data-* attributes for state styling (e.g., data-open, data-disabled), not data-slot. data-slot appears in web component contexts (HTML ), but Base UI uses React render props, not native slots. MUI's older Base UI had slots/slotProps props, but current @base-ui/react (successor) uses render with useRender. Example usage: import {useRender} from '@base-ui/react/use-render'; function CustomButton({render, disabled, ...props}) { const state = {disabled}; return useRender({ defaultTagName: 'button', render, props, state, // stateAttributesMapping: customMapping // optional }); } In render callback: access state for conditional logic/attributes. Official docs confirm state for render prop and data attribute generation.
Citations:
- 1: https://base-ui.com/react/utils/use-render
- 2:
useRendercan crash during React reconciliation mui/base-ui#4039 - 3: [useRender] Add public hook mui/base-ui#1418
- 4: [useRender] TypeScript Error with data-* Attributes in useRender.ElementProps mui/base-ui#2370
- 5: [useRender] Add
defaultTagNameparameter mui/base-ui#2527 - 6: https://base-ui.com/react/handbook/styling
🏁 Script executed:
# Look for any comments or implementation details in files that use useRender
rg -B 5 "state:" components/ui/ | grep -E "//|/\*"Repository: saechimdaeki/project-cupid
Length of output: 52
🏁 Script executed:
# Check if there's any documentation files in the repo
fd -e md -e txt | xargs grep -l "useRender\|base-ui" 2>/dev/nullRepository: saechimdaeki/project-cupid
Length of output: 54
Add stateAttributesMapping to convert state.slot to data-slot attribute.
The useRender hook's state parameter does not automatically convert state properties to data-* attributes. The slot: "breadcrumb-link" in the state object will not render as data-slot="breadcrumb-link" without explicit stateAttributesMapping. Either add stateAttributesMapping: { slot: 'slot' } to the useRender call, or use direct JSX like the other breadcrumb components (Breadcrumb, BreadcrumbList, etc.).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/breadcrumb.tsx` around lines 42 - 60, BreadcrumbLink's
useRender call sets state: { slot: "breadcrumb-link" } but doesn't map that
state to a data attribute, so add a stateAttributesMapping to the useRender
invocation (e.g., stateAttributesMapping: { slot: "slot" }) so state.slot
becomes data-slot="breadcrumb-link"; update the useRender call inside the
BreadcrumbLink function (the one that currently passes defaultTagName, props via
mergeProps, render, and state) to include stateAttributesMapping: { slot: "slot"
} so the component outputs data-slot correctly.
| const ref = React.useRef<HTMLButtonElement>(null) | ||
| React.useEffect(() => { | ||
| if (modifiers.focused) ref.current?.focus() | ||
| }, [modifiers.focused]) | ||
|
|
There was a problem hiding this comment.
Consider adding ref to Button.
The ref is created but never passed to the Button component. The focus effect won't work as intended since ref.current will always be null.
🐛 Proposed fix
function CalendarDayButton({
className,
day,
modifiers,
locale,
...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
+ ref={ref}
variant="ghost"
size="icon"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/calendar.tsx` around lines 192 - 196, The focus ref is created
with React.useRef and used in a React.useEffect watching modifiers.focused, but
it is never passed to the Button, so ref.current remains null; fix by forwarding
the ref to the rendered Button element (pass ref={ref} to the Button instance)
or use the Button's forwardedRef prop if it expects a different prop name,
ensuring the ref variable declared in this file is the one attached so the
useEffect can call ref.current.focus() when modifiers.focused changes.
| import { cva, type VariantProps } from "class-variance-authority" | ||
|
|
||
| import { cn } from "@/lib/utils" | ||
|
|
||
| function Empty({ className, ...props }: React.ComponentProps<"div">) { | ||
| return ( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify React namespace usage vs import in this file
rg -n 'React\.' components/ui/empty.tsx
rg -n '^import .*react' components/ui/empty.tsxRepository: saechimdaeki/project-cupid
Length of output: 544
🏁 Script executed:
cat -n components/ui/empty.tsx | head -100Repository: saechimdaeki/project-cupid
Length of output: 3114
Add missing React type import.
File uses React.ComponentProps in multiple function signatures (lines 5, 18, 47, 58, 71, 84) but doesn't import React, causing TypeScript compilation to fail.
Suggested fix
+import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { cva, type VariantProps } from "class-variance-authority" | |
| import { cn } from "@/lib/utils" | |
| function Empty({ className, ...props }: React.ComponentProps<"div">) { | |
| return ( | |
| import type * as React from "react" | |
| import { cva, type VariantProps } from "class-variance-authority" | |
| import { cn } from "@/lib/utils" | |
| function Empty({ className, ...props }: React.ComponentProps<"div">) { | |
| return ( |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/empty.tsx` around lines 1 - 6, Add the missing React type
import at the top of the file so usages of React.ComponentProps in the component
signatures (e.g., Empty and the other functions using React.ComponentProps)
compile; insert an import for React (or an "import type React from 'react'")
near the existing imports so all component props typed as
React.ComponentProps<"div"> (and similar) resolve correctly.
| import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu" | ||
| import { cva } from "class-variance-authority" | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify React namespace usage and whether a react import exists
rg -n 'React\.' components/ui/navigation-menu.tsx
rg -n '^import .*react' components/ui/navigation-menu.tsxRepository: saechimdaeki/project-cupid
Length of output: 434
Add missing React type import for React.ComponentPropsWithRef usages.
The file uses React.ComponentPropsWithRef on lines 32, 48, and 143 but doesn't import React. Add the type import:
Suggested fix
+import type * as React from "react"
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import { cva } from "class-variance-authority"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu" | |
| import { cva } from "class-variance-authority" | |
| import type * as React from "react" | |
| import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu" | |
| import { cva } from "class-variance-authority" | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/navigation-menu.tsx` around lines 1 - 3, This file uses
React.ComponentPropsWithRef in several component prop typings but doesn't import
React; add a type-only import and update usages: add "import type {
ComponentPropsWithRef } from 'react'" at the top, then replace occurrences of
React.ComponentPropsWithRef with ComponentPropsWithRef (the type import) for the
components that reference it (the props typed via ComponentPropsWithRef used
alongside NavigationMenuPrimitive and your NavigationMenu items), ensuring
TypeScript resolves the type without importing runtime React.
| <NavigationMenuPrimitive.Content | ||
| data-slot="navigation-menu-content" | ||
| className={cn( | ||
| "data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Tailwind CSS v4, for data-attribute value selectors, is data-activation-direction=left:valid syntax, or should it bedata-[activation-direction=left]:?
💡 Result:
In Tailwind CSS v4, the valid syntax for data-attribute value selectors is data-[attribute=value]:, such as data-[activation-direction=left]:. The shorthand data-activation-direction=left: is not valid syntax.
Citations:
- 1: Normalize attribute selectors for
data-*andaria-*modifiers tailwindlabs/tailwindcss#14040 - 2: Add dynamic
data-*variant tailwindlabs/tailwindcss#9559 - 3: [v4] - Styling according to data-attribute resolves in empty classes tailwindlabs/tailwindcss#15933
- 4: Data attributes with named groups tailwindlabs/tailwindcss#18113
- 5: How to combine named groups with data attributes tailwindlabs/tailwindcss#11768
- 6: https://stackoverflow.com/questions/71394904/how-to-target-elements-with-a-specific-data-attribute-with-tailwind-css/71492464
Fix malformed Tailwind data-attribute variants in transition classes.
The syntax data-activation-direction=left: is invalid in Tailwind CSS v4 and will be purged during build. Use the proper bracket notation data-[activation-direction=left]: instead. This affects all motion-related variants in the className string.
Suggested fix
- "data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] ...
+ "data-ending-style:data-[activation-direction=left]:translate-x-[50%] data-ending-style:data-[activation-direction=right]:translate-x-[-50%] data-starting-style:data-[activation-direction=left]:translate-x-[-50%] data-starting-style:data-[activation-direction=right]:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] ...📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95", | |
| "data-ending-style:data-[activation-direction=left]:translate-x-[50%] data-ending-style:data-[activation-direction=right]:translate-x-[-50%] data-starting-style:data-[activation-direction=left]:translate-x-[-50%] data-starting-style:data-[activation-direction=right]:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/navigation-menu.tsx` at line 87, The long className string in
the NavigationMenu component contains malformed Tailwind variants like
data-activation-direction=left: which will be purged; update every occurrence to
use bracket notation (e.g. change data-activation-direction=left: →
data-[activation-direction=left]: and data-activation-direction=right: →
data-[activation-direction=right]:) and similarly convert any other
data-attribute variants (e.g. data-[motion=from-end],
data-[slot=navigation-menu-link], data-starting-style/data-ending-style) to the
correct data-[...=...] or data-[...]-style bracket form so Tailwind v4
recognizes them in the className string used by the NavigationMenu element.
| function Tabs({ | ||
| className, | ||
| orientation = "horizontal", | ||
| ...props | ||
| }: TabsPrimitive.Root.Props) { | ||
| return ( | ||
| <TabsPrimitive.Root | ||
| data-slot="tabs" | ||
| data-orientation={orientation} | ||
| className={cn( | ||
| "group/tabs flex gap-2 data-horizontal:flex-col", | ||
| className | ||
| )} | ||
| {...props} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify orientation is destructured and whether it is forwarded to Root
rg -n -C3 'function Tabs\(|orientation\s*=' components/ui/tabs.tsx
rg -n -C4 '<TabsPrimitive\.Root|data-orientation|orientation=' components/ui/tabs.tsxRepository: saechimdaeki/project-cupid
Length of output: 743
Forward orientation prop to TabsPrimitive.Root.
orientation is destructured from props but only used for the data-orientation attribute. The underlying component needs the actual orientation prop to function correctly.
Suggested fix
<TabsPrimitive.Root
data-slot="tabs"
+ orientation={orientation}
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function Tabs({ | |
| className, | |
| orientation = "horizontal", | |
| ...props | |
| }: TabsPrimitive.Root.Props) { | |
| return ( | |
| <TabsPrimitive.Root | |
| data-slot="tabs" | |
| data-orientation={orientation} | |
| className={cn( | |
| "group/tabs flex gap-2 data-horizontal:flex-col", | |
| className | |
| )} | |
| {...props} | |
| /> | |
| function Tabs({ | |
| className, | |
| orientation = "horizontal", | |
| ...props | |
| }: TabsPrimitive.Root.Props) { | |
| return ( | |
| <TabsPrimitive.Root | |
| data-slot="tabs" | |
| orientation={orientation} | |
| data-orientation={orientation} | |
| className={cn( | |
| "group/tabs flex gap-2 data-horizontal:flex-col", | |
| className | |
| )} | |
| {...props} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/ui/tabs.tsx` around lines 8 - 22, The Tabs component destructures
orientation but only sets data-orientation; forward the actual orientation prop
into TabsPrimitive.Root so the underlying component receives it (update Tabs
function to pass orientation={orientation} into TabsPrimitive.Root alongside
data-orientation and existing props); ensure you modify the Tabs function
signature/JSX where TabsPrimitive.Root is rendered to include the orientation
prop.
Summary
(public)— 인증 불필요:/,/login,/auth/continue,/auth/callback,/pending(member)— 승인된 멤버:/dashboard,/timeline(admin)— 관리자(admin, super_admin):/candidates/new,/profiles/[id],/profiles/[id]/edit(super-admin)— 최고 관리자:/adminpage-patterns.mdrules 문서를 새 구조에 맞게 업데이트Test plan
npm run typecheck통과 확인🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Style
Refactor
Documentation