Skip to content

feat(shadcn): #53 restyle search Select in Tailwind#60

Closed
fray-cloud wants to merge 6 commits into
devfrom
shadcn/53-select
Closed

feat(shadcn): #53 restyle search Select in Tailwind#60
fray-cloud wants to merge 6 commits into
devfrom
shadcn/53-select

Conversation

@fray-cloud

@fray-cloud fray-cloud commented Apr 13, 2026

Copy link
Copy Markdown
Owner

Summary

Stacked on #59. Scope adjusted from "Radix Select + rhf Controller" to Tailwind-styled native <select> while preserving the existing rhf register flow.

Why not Radix

Radix Select rejects empty-string SelectItem values, and every init* 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 daisyUI select-bordered) is fully met by a Tailwind-styled native select.

Changes

  • 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 + <option> children. Form submit behavior is identical.
  • nx.json picks up the analytics: true line that the 22-6-0-enable-analytics-prompt migration injects on first run (unrelated to shadcn; merged in-flight).

Closes #53.

Test plan

  • npx nx affected -t lint test build --exclude web-e2e green

🤖 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:

  • Add reusable shadcn-style UI primitives (Button, Card, Avatar, Progress, Skeleton) and a Tailwind class-merging utility for consistent styling across the web app.

Enhancements:

  • Restyle the search select, animal cards, header, home contents CTA, count list, and infinite scroll loader to use the new UI components and token-based Tailwind classes instead of daisyUI styles.
  • Enable class-based dark mode and token-driven color and radius theming in Tailwind and global CSS to support the new design system.
  • Clean up the new-component barrel exports to reflect the current surface area and remove the unused sidebar export.

Build:

  • Add Radix UI, class-variance-authority, clsx, tailwind-merge, and tailwindcss-animate dependencies and configure the Tailwind plugin stack.
  • Update nx.json to include the analytics flag injected by Nx migrations.

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
@vercel

vercel Bot commented Apr 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
animal-project Error Error Apr 13, 2026 8:48am

@sourcery-ai

sourcery-ai Bot commented Apr 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces 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.

  • Replace daisyUI form-control/label-text wrapper with a simple flex-column label and muted text span
  • Replace select/select-bordered/select-xs classes with explicit Tailwind classes using border-input, bg-background, and focus ring-ring styles
  • Adopt cn() helper for composing select classes
  • Keep children and register(name) usage intact so existing RHF register flow and empty-string sentinel values are unchanged
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).
  • Convert AnimalCard layout from daisyUI card/table classes to Card + CardContent with simplified table styling
  • Update Header to use the new Button component instead of a daisyUI ghost button, and adjust container layout classes
  • Update home Contents call-to-action to use Button variant="link" instead of a daisyUI link button
  • Update search form submit control from a daisyUI button to the new Button component with size="sm"
  • Update SearchView loader from a native with daisyUI classes to the new Progress component
  • Refactor Count list avatars and loading states from daisyUI avatar/skeleton classes to Avatar and Skeleton components and add 'use client' where needed
apps/web/src/new-site/search/card/AnimalCard.tsx
apps/web/src/new-component/header.tsx
apps/web/src/new-site/home/Contents.tsx
apps/web/src/new-site/search/Form.tsx
apps/web/src/new-site/search/SearchView.tsx
apps/web/src/new-site/home/Count.tsx Adjust exports and dependencies to support the new UI layer and infrastructure tweaks.
  • Clean up new-component index barrel to export form and header only, removing sidebar export tied to a deleted file
  • Delete the legacy sidebar component file
  • Add Radix UI avatar/progress/slot, class-variance-authority, clsx, tailwind-merge, tailwindcss-animate, and lucide-react dependencies
  • Enable Nx analytics flag that was added by a migration
apps/web/src/new-component/index.ts
apps/web/src/new-component/sidebar.tsx
package.json
package-lock.json
nx.json
Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@nx-cloud

nx-cloud Bot commented Apr 13, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 171851e

Command Status Duration Result
nx build web ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-13 08:49:54 UTC

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • In ui/card.tsx, CardTitle and CardDescription are typed as HTMLHeadingElement / HTMLParagraphElement but render divs, 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +20 to +22
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" />}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
loader={<Progress key={uuidv4()} className="h-2" />}
loader={<Progress key={uuidv4()} value={100} className="h-2" />}

Comment on lines +32 to +33
src={`/logo/${result.data?.sido.orgCd}.png`}
alt="logo"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@fray-cloud

Copy link
Copy Markdown
Owner Author

Superseded by #61 — full #48#54 chain lands via #61.

@fray-cloud fray-cloud closed this Apr 13, 2026
@fray-cloud fray-cloud deleted the shadcn/53-select branch April 13, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant