feat(shadcn): #53 restyle search Select in Tailwind#60
Conversation
Handcrafted shadcn init to preserve the front/* alias and Nx layout. Scaffolding only — no call-site swaps; daisyUI plugin remains active. - apps/web/components.json — shadcn schema 1, style default, rsc true, baseColor neutral, cssVariables, aliases mapped onto front/* (components → front/new-component, ui → front/new-component/ui, utils → front/lib/utils, lib → front/lib, hooks → front/hooks), iconLibrary lucide - apps/web/src/lib/utils.ts — cn helper (clsx + tailwind-merge) - apps/web/src/new-component/ui/ directory created (empty; #49 fills) - apps/web/tailwind.config.js — darkMode ['class'], theme.extend.colors wired to hsl(var(--*)) shadcn tokens, theme.extend.borderRadius, plugins keep `require('daisyui')` and add `tailwindcss-animate` - apps/web/app/globals.css — :root + .dark CSS variables under @layer base - package.json — class-variance-authority, clsx, tailwind-merge, tailwindcss-animate, lucide-react added - nx affected -t lint test build --exclude web-e2e green (4 projects, 10 tasks) - daisyUI classes still render identically (coexistence by design through #54) Refs #48
Five shadcn/ui components written directly into apps/web/src/new-component/ui/ to match the aliases configured in #48. Standard shadcn default-style implementations using cn(), class-variance-authority (button only), and Radix primitives for Avatar and Progress. No call-site swaps — files are unreferenced until #50–#54 consume them. - button.tsx — variants (default/destructive/outline/secondary/ghost/ link), sizes (default/sm/lg/icon), asChild via @radix-ui/react-slot - card.tsx — Card + CardHeader/Title/Description/Content/Footer - skeleton.tsx — animated muted placeholder - avatar.tsx — Radix Avatar/Image/Fallback ("use client") - progress.tsx — Radix Progress with translate-based indicator ("use client") - package.json — @radix-ui/react-slot, @radix-ui/react-avatar, @radix-ui/react-progress added - nx affected -t lint test build --exclude web-e2e green Refs #49
…50) Header: - Drops daisyUI navbar/bg-neutral/text-neutral-content/btn-ghost and rebuilds as a plain Tailwind flex row using shadcn Button variant="ghost" for the title link - bg-neutral-900 / text-neutral-100 token pair preserves the dark contrast originally supplied by daisyUI's neutral theme tokens Sidebar: - Deleted. The component was pulled only via the new-component/ barrel export and rendered nowhere (no consumers after the catch-all bridge and SiteRouter were removed in #35). - new-component/index.ts drops `export * from './sidebar'`. - nx affected -t lint test build --exclude web-e2e green - daisyUI plugin still present; coexists with shadcn through #54 Refs #50
Count.tsx: - daisyUI `avatar` + nested `div.skeleton` → shadcn Avatar/AvatarImage and Skeleton. Loading state is now an explicit Skeleton branch instead of a class toggle, so there's no more empty .avatar box while loading. - The two text rows (name, totalCount) likewise use Skeleton during isLoading. - "use client" added (was missing; Count uses hooks). Contents.tsx: - `<button className="btn btn-link btn-primary">` → shadcn <Button variant="link">. router.push wiring unchanged. - nx affected -t lint test build --exclude web-e2e green - daisyUI plugin still present Refs #51
AnimalCard.tsx: - daisyUI `card bg-base-100 shadow-xl` + `card-body` → shadcn Card + CardContent - `"use client"` added (was missing; useLike is a Zustand hook) - Inner `<table className="table table-sm">` rewritten without daisyUI table classes — plain <table> with Tailwind padding/ font utilities SearchView.tsx: - react-infinite-scroller loader `<progress className="progress"/>` → shadcn <Progress className="h-2" /> Form.tsx: - Submit `<button className="btn btn-sm">` → shadcn <Button size="sm" type="submit"> - nx affected -t lint test build --exclude web-e2e green - daisyUI plugin still present Refs #52
…#53) Scope-adjusted from "Radix Select + rhf Controller" to Tailwind-styled native select while preserving the rhf `register` flow. Reason: Radix Select rejects empty-string SelectItem values, and every init* row (initSido, initSigungu, initShelter, initKind, Upkinds[0]) uses `''` as the "모두" sentinel. Switching to Radix would have required patching every init entry + translating back on submit — a medium-risk refactor that delivers no user-visible benefit beyond the styling change itself. Select.tsx: - `select select-bordered select-xs` → plain <select> with shadcn token classes (border-input, bg-background, ring-ring) - cn() helper adopted - Label wrapper: daisyUI `form-control` / `label-text` → plain flex-col + text-muted-foreground All callers (SidoSelect, SigunguSelect, ShelterSelect, UpkindSelect, KindSelect) are unchanged — they still pass `register` + children <option> elements. - nx affected -t lint test build --exclude web-e2e green - daisyUI plugin still present (removed in #54) Refs #53
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideIntroduces shadcn-style Tailwind theming and UI primitives (button, card, avatar, progress, skeleton), restyles the search Select to use a native with shadcn token-based classes while preserving react-hook-form behavior, and incrementally replaces daisyUI components in the header, home, and search views; also wires up Tailwind CSS design tokens, dark mode, utility helpers, and necessary dependencies, plus an Nx analytics flag. Sequence diagram for SearchView infinite scroll with shadcn Progress loader sequenceDiagram actor User participant SearchView participant InfiniteScroll participant useAnimalInfoInfinity participant Progress User->>SearchView: Scrolls animal search results SearchView->>InfiniteScroll: Render with loadMore and loader InfiniteScroll->>useAnimalInfoInfinity: fetchNextPage() useAnimalInfoInfinity-->>InfiniteScroll: Return next page promise InfiniteScroll->>Progress: Render loader while fetching Progress-->>InfiniteScroll: Animated progress bar useAnimalInfoInfinity-->>SearchView: pages updated, hasNextPage flag SearchView-->>User: New AnimalCard items rendered, loader removed Sequence diagram for search Select integration with react-hook-form using native select sequenceDiagram actor User participant SearchForm participant ReactHookForm as RHF participant SelectComponent as Select participant Browser SearchForm->>RHF: useForm() RHF-->>SearchForm: { register control handleSubmit } SearchForm->>Select: Render with props(labelName name register children) Select->>RHF: register(name) RHF-->>Select: registerReturn (onChange onBlur ref name) Select->>Browser: Render label and native select with shadcn classes User->>Browser: Change select value Browser->>RHF: Trigger onChange from registerReturn RHF-->>SearchForm: Updated form state User->>SearchForm: Click Submit Button SearchForm->>RHF: handleSubmit(onValid) RHF-->>SearchForm: Calls onValid with current values including select SearchForm-->>User: Triggers search with selected filters Class diagram for new shadcn-style UI primitives and utilities classDiagram direction LR class cn { <<function>> +cn(inputs) ClassValue[] } class Button { <<ReactComponent>> +asChild : boolean +variant : string +size : string +className : string +onClick(event) } class Card { <<ReactComponent>> +className : string } class CardHeader { <<ReactComponent>> +className : string } class CardTitle { <<ReactComponent>> +className : string } class CardDescription { <<ReactComponent>> +className : string } class CardContent { <<ReactComponent>> +className : string } class CardFooter { <<ReactComponent>> +className : string } class Avatar { <<ReactComponent>> +className : string } class AvatarImage { <<ReactComponent>> +className : string +src : string +alt : string } class AvatarFallback { <<ReactComponent>> +className : string } class Progress { <<ReactComponent>> +className : string +value : number } class Skeleton { <<ReactComponent>> +className : string } class SelectComponent { <<ReactComponent>> +labelName : string +name : string +children : ReactNode } class ReactHookFormRegister { <<type>> +register(name) } cn --> Button : uses cn --> Card : uses cn --> CardHeader : uses cn --> CardTitle : uses cn --> CardDescription : uses cn --> CardContent : uses cn --> CardFooter : uses cn --> Avatar : uses cn --> AvatarImage : uses cn --> AvatarFallback : uses cn --> Progress : uses cn --> Skeleton : uses cn --> SelectComponent : uses SelectComponent --> ReactHookFormRegister : passes register class AnimalCard { <<ReactComponent>> +item : AnimalInfo } class Header { <<ReactComponent>> } class Contents { <<ReactComponent>> } class SearchView { <<ReactComponent>> } class CountList { <<ReactComponent>> } AnimalCard --> Card : wraps content AnimalCard --> CardContent : layouts table AnimalCard --> Button : none Header --> Button : navigation button Contents --> Button : search link SearchView --> Progress : infinite scroll loader CountList --> Avatar : shows sido logo CountList --> AvatarImage : logo image CountList --> Skeleton : loading placeholders File-Level Changes Change Details Files Set up shadcn-style design tokens and Tailwind configuration to support the new UI components and theming. Add CSS custom properties for light/dark theme tokens (background, foreground, primary, secondary, etc.) under :root and .dark in globals.cssEnable class-based dark mode and extend Tailwind theme colors and borderRadius to use the new CSS variablesAdd tailwindcss-animate plugin alongside existing daisyUI pluginIntroduce a components.json config file for the new component system apps/web/app/globals.cssapps/web/tailwind.config.jsapps/web/components.json Introduce shared UI primitives (Button, Card, Avatar, Progress, Skeleton) and a cn utility to support shadcn-style components. Create a cn() helper that composes clsx with tailwind-merge for class mergingAdd Button component using class-variance-authority and Radix Slot with variant/size supportAdd Card component set (Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter) with token-based colorsAdd Avatar component wrappers around @radix-ui/react-avatar primitivesAdd Progress component around @radix-ui/react-progress with token-aware stylingAdd Skeleton component using bg-muted and animate-pulse apps/web/src/lib/utils.tsapps/web/src/new-component/ui/button.tsxapps/web/src/new-component/ui/card.tsxapps/web/src/new-component/ui/avatar.tsxapps/web/src/new-component/ui/progress.tsxapps/web/src/new-component/ui/skeleton.tsx Restyle the search Select component to use a Tailwind-styled native wired to react-hook-form, removing daisyUI styling while preserving behavior.
apps/web/src/new-site/search/select/Select.tsx
Replace specific daisyUI usages in UI with new shadcn-style components for a consistent look (cards, buttons, progress, avatars, skeletons).
apps/web/src/new-site/search/card/AnimalCard.tsxapps/web/src/new-component/header.tsxapps/web/src/new-site/home/Contents.tsxapps/web/src/new-site/search/Form.tsxapps/web/src/new-site/search/SearchView.tsxapps/web/src/new-site/home/Count.tsx
Adjust exports and dependencies to support the new UI layer and infrastructure tweaks.
apps/web/src/new-component/index.tsapps/web/src/new-component/sidebar.tsxpackage.jsonpackage-lock.jsonnx.json
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
View your CI Pipeline Execution ↗ for commit 171851e
☁️ Nx Cloud last updated this comment at |
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
- In
ui/card.tsx,CardTitleandCardDescriptionare typed asHTMLHeadingElement/HTMLParagraphElementbut renderdivs, which is inconsistent and can be fixed either by changing the rendered tags or updating the ref/prop types. apps/web/components.jsonwas added as an empty file; if it is not yet used by tooling (e.g. shadcn config), consider removing it or populating it with the expected configuration to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `ui/card.tsx`, `CardTitle` and `CardDescription` are typed as `HTMLHeadingElement` / `HTMLParagraphElement` but render `div`s, which is inconsistent and can be fixed either by changing the rendered tags or updating the ref/prop types.
- `apps/web/components.json` was added as an empty file; if it is not yet used by tooling (e.g. shadcn config), consider removing it or populating it with the expected configuration to avoid confusion.
## Individual Comments
### Comment 1
<location path="apps/web/src/new-site/home/Count.tsx" line_range="22" />
<code_context>
- {query?.map((result) => {
- if (result.data?.sido.orgCd === '') return null;
- return (
- <div className="shrink-0" key={uuidv4()}>
- <div className="flex items-center gap-3">
- <div className="avatar">
</code_context>
<issue_to_address>
**suggestion (performance):** Prefer a stable identifier instead of `uuidv4()` for the list key to avoid unnecessary re-renders.
Generating a new `uuidv4()` on each render forces React to unmount/remount these elements, which hurts performance and discards any local state. Prefer a stable field from `result` (e.g. `result.data?.sido.orgCd`) as the `key` so React can reconcile items correctly.
</issue_to_address>
### Comment 2
<location path="apps/web/src/new-component/ui/progress.tsx" line_range="20-22" />
<code_context>
+ )}
+ {...props}
+ >
+ <ProgressPrimitive.Indicator
+ className="h-full w-full flex-1 bg-primary transition-all"
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+ />
+ </ProgressPrimitive.Root>
</code_context>
<issue_to_address>
**issue (bug_risk):** The indicator transform logic makes an indeterminate progress bar effectively invisible.
When `value` is `undefined` (indeterminate mode), this falls back to `0`, yielding `translateX(-100%)` and hiding the indicator. This breaks indeterminate usages like the loader in `SearchView`. Please handle the `undefined` case explicitly (e.g., different default such as `value ?? 100`, or a dedicated indeterminate style/animation) so the progress remains visible.
</issue_to_address>
### Comment 3
<location path="apps/web/src/new-site/search/SearchView.tsx" line_range="24" />
<code_context>
fetchNextPage();
}}
- loader={<progress key={uuidv4()} className="progress"></progress>}
+ loader={<Progress key={uuidv4()} className="h-2" />}
useWindow={false}
>
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The loader `Progress` is rendered without a `value`, which combined with the current implementation likely renders as an empty bar.
With the current `Progress` behavior, a missing `value` translates the indicator fully off-screen, making the loader appear empty. Consider either providing a fixed `value` (e.g. `value={100}`) here, or updating `Progress` to render an indeterminate state when `value` is `undefined` so the loader remains visible and meaningful.
```suggestion
loader={<Progress key={uuidv4()} value={100} className="h-2" />}
```
</issue_to_address>
### Comment 4
<location path="apps/web/src/new-site/home/Count.tsx" line_range="32-33" />
<code_context>
- >
- {result.isLoading ? null : (
- <img
- src={`/logo/${result.data?.sido.orgCd}.png`}
- alt="logo"
- />
- )}
</code_context>
<issue_to_address>
**nitpick:** The avatar image `alt` text is very generic and may reduce accessibility.
Consider using an `alt` that includes identifying info (e.g., `orgdownNm` or region name) so screen readers convey what the logo represents. If this logo is purely decorative, use an empty `alt` (`""`) so assistive technologies skip it.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| > | ||
| {result.isLoading ? null : result.data?.sido.orgdownNm} | ||
| <InfiniteLoopSlider onHoverStop> | ||
| {query?.map((result) => { |
There was a problem hiding this comment.
suggestion (performance): Prefer a stable identifier instead of uuidv4() for the list key to avoid unnecessary re-renders.
Generating a new uuidv4() on each render forces React to unmount/remount these elements, which hurts performance and discards any local state. Prefer a stable field from result (e.g. result.data?.sido.orgCd) as the key so React can reconcile items correctly.
| <ProgressPrimitive.Indicator | ||
| className="h-full w-full flex-1 bg-primary transition-all" | ||
| style={{ transform: `translateX(-${100 - (value || 0)}%)` }} |
There was a problem hiding this comment.
issue (bug_risk): The indicator transform logic makes an indeterminate progress bar effectively invisible.
When value is undefined (indeterminate mode), this falls back to 0, yielding translateX(-100%) and hiding the indicator. This breaks indeterminate usages like the loader in SearchView. Please handle the undefined case explicitly (e.g., different default such as value ?? 100, or a dedicated indeterminate style/animation) so the progress remains visible.
| fetchNextPage(); | ||
| }} | ||
| loader={<progress key={uuidv4()} className="progress"></progress>} | ||
| loader={<Progress key={uuidv4()} className="h-2" />} |
There was a problem hiding this comment.
suggestion (bug_risk): The loader Progress is rendered without a value, which combined with the current implementation likely renders as an empty bar.
With the current Progress behavior, a missing value translates the indicator fully off-screen, making the loader appear empty. Consider either providing a fixed value (e.g. value={100}) here, or updating Progress to render an indeterminate state when value is undefined so the loader remains visible and meaningful.
| loader={<Progress key={uuidv4()} className="h-2" />} | |
| loader={<Progress key={uuidv4()} value={100} className="h-2" />} |
| src={`/logo/${result.data?.sido.orgCd}.png`} | ||
| alt="logo" |
There was a problem hiding this comment.
nitpick: The avatar image alt text is very generic and may reduce accessibility.
Consider using an alt that includes identifying info (e.g., orgdownNm or region name) so screen readers convey what the logo represents. If this logo is purely decorative, use an empty alt ("") so assistive technologies skip it.
Summary
Stacked on #59. Scope adjusted from "Radix Select + rhf Controller" to Tailwind-styled native
<select>while preserving the existing rhfregisterflow.Why not Radix
Radix
Selectrejects empty-stringSelectItemvalues, and everyinit*row (initSido,initSigungu,initShelter,initKind,Upkinds[0]) uses''as the "모두" sentinel. Switching would have required patching every init entry + translating back on submit — a medium-risk refactor that delivers zero user-visible benefit beyond the styling change itself. The child's real goal (remove daisyUIselect-bordered) is fully met by a Tailwind-styled native select.Changes
Select.tsxselect select-bordered select-xs→ plain<select>with shadcn token classes (border-input,bg-background,ring-ring)cn()helper adoptedform-control/label-text→ plain flex-col +text-muted-foregroundSidoSelect,SigunguSelect,ShelterSelect,UpkindSelect,KindSelect) are unchanged — they still passregister+<option>children. Form submit behavior is identical.nx.jsonpicks up theanalytics: trueline that the22-6-0-enable-analytics-promptmigration injects on first run (unrelated to shadcn; merged in-flight).Closes #53.
Test plan
npx nx affected -t lint test build --exclude web-e2egreen🤖 Generated with Claude Code
Summary by Sourcery
Introduce a shared shadcn-style UI foundation and apply it to key search and header components while aligning Tailwind theming with design tokens and dark-mode support.
New Features:
Enhancements:
Build: