Skip to content

feat(ui): add shadcn/ui primitives#590

Merged
bordalix merged 27 commits into
masterfrom
wt-shadcn-ui-20260430
May 12, 2026
Merged

feat(ui): add shadcn/ui primitives#590
bordalix merged 27 commits into
masterfrom
wt-shadcn-ui-20260430

Conversation

@sahilc0
Copy link
Copy Markdown
Contributor

@sahilc0 sahilc0 commented Apr 30, 2026

Summary

Add 55 shadcn/ui components as primitives for future component migrations.

Merge order

This PR is part of a chain:

  1. feat(design-system): add design tokens and Tailwind v4 integration #582 — Tailwind v4 + design tokens (base for this PR)
  2. This PR — shadcn/ui primitives
  3. feat(toast): migrate to sonner library #589 — Sonner toast migration (independent, can merge anytime)

Merge #582 first, then this PR.

Changes

  • Add @/* path alias in tsconfig.json and vite.config.ts
  • Add shadcn theme tokens to app.css (mapped to existing arkade tokens)
  • Add components.json configuration
  • Add 55 shadcn components in src/components/ui/
  • Add use-mobile.ts hook

Components added

Button, Input, Dialog, Drawer, Sheet, Select, Checkbox, Switch, Tabs, Card, Table, Accordion, Alert, Avatar, Badge, Breadcrumb, Calendar, Carousel, Chart, Collapsible, Combobox, Command, ContextMenu, DropdownMenu, HoverCard, InputOTP, Label, Menubar, NavigationMenu, Pagination, Popover, Progress, RadioGroup, Resizable, ScrollArea, Separator, Sidebar, Skeleton, Slider, Sonner, Spinner, Textarea, Toggle, ToggleGroup, Tooltip, and more.

Test plan

  • Build passes
  • 179 unit tests pass
  • No existing code modified — primitives available for future use

Summary by CodeRabbit

  • New Features

    • Added 60+ reusable UI components (dialogs, menus, forms, navigation, data viz, carousel, etc.).
    • Comprehensive theming with CSS variables, light/dark palettes and design tokens.
    • Mobile detection hook for responsive behavior.
  • Chores

    • Updated build & TypeScript configs with path aliasing.
    • Added UI/styling packages to dependencies.
    • Added component library configuration for consistent UI generation.

Review Change Stack

@sahilc0 sahilc0 requested review from bordalix and pietro909 April 30, 2026 18:19
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Walkthrough

Adds shadcn config, Tailwind/theme tokens, TS/Vite path aliases, new dependencies, a mobile-detection hook and a large set of reusable UI primitives (buttons, inputs, navigation, overlays, charts, layout, sidebar, carousel, etc.), plus CSS variable palettes and theming.

Shadcn UI primitives + config

Layer / File(s) Summary
Tooling & Aliases
tsconfig.json, vite.config.ts
Adds @ path alias and updates TS JSX setting.
Packages
package.json
Adds UI libs and font dependency; shadcn devDependency.
Theme & Global Styles
src/app.css, components.json
Imports shadcn/tailwind, font, animations; injects @theme and CSS variable palettes (light/dark).
Utilities
src/lib/utils.ts, src/hooks/use-mobile.ts
Reorders clsx import; adds useIsMobile hook (768px breakpoint).
Core primitives / layout
src/components/ui/card.tsx, table.tsx, skeleton.tsx, empty.tsx, aspect-ratio.tsx
Adds presentational layout primitives.
Typography / labels
label.tsx, kbd.tsx
Adds label and keyboard hint components.
Buttons & groups
button.tsx, button-group.tsx, badge.tsx
Button variants and group utilities.
Forms / inputs
input.tsx, textarea.tsx, select.tsx, native-select.tsx, input-otp.tsx, checkbox.tsx, radio-group.tsx, toggle.tsx, switch.tsx, slider.tsx
Adds many input controls and wrappers.
Input-group / field helpers
input-group.tsx, field.tsx
Composed input-group and form field building blocks (addons, error rendering).
Navigation / menus
breadcrumb.tsx, pagination.tsx, menubar.tsx, navigation-menu.tsx, context-menu.tsx, dropdown-menu.tsx, command.tsx
Adds menus, command palette, breadcrumb and pagination components.
Overlays / panels
dialog.tsx, alert-dialog.tsx, popover.tsx, hover-card.tsx, sheet.tsx, drawer.tsx, popover.tsx
Adds modals, dialogs, popovers, sheets, drawers, alert dialogs.
Sidebar & related
sidebar.tsx
Implements SidebarProvider, sidebar UI, persistence, mobile behavior and shortcuts.
Display / navigation widgets
tabs.tsx, accordion.tsx, collapsible.tsx, popover.tsx, tooltip.tsx, hover-card.tsx
Adds interaction primitives.
Media / motion
carousel.tsx, scroll-area.tsx, resizable.tsx, spinner.tsx
Carousel, scrollbars, resizable panels, and spinner.
Charts & visualization
chart.tsx
Chart context, tooltip and legend components (Recharts).
Toasts
sonner.tsx
Adds sonner Toaster wrapper with theme-aware icons and styles.
Exports & wiring
many src/components/ui/*.tsx
Exports numerous named UI components across modules.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs:

  • arkade-os/wallet#589: related — also adds/changes sonner Toaster usage and toasts integration.
  • arkade-os/wallet#582: related — modifies theming/tokens and Tailwind build surface that this PR consumes.

Suggested reviewers:

  • pietro909
  • bordalix
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wt-shadcn-ui-20260430

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 30, 2026

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0386dbc
Status: ✅  Deploy successful!
Preview URL: https://13a3b14f.wallet-bitcoin.pages.dev
Branch Preview URL: https://wt-shadcn-ui-20260430.wallet-bitcoin.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 30, 2026

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0386dbc
Status: ✅  Deploy successful!
Preview URL: https://c6277e5d.arkade-wallet.pages.dev
Branch Preview URL: https://wt-shadcn-ui-20260430.arkade-wallet.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 30, 2026

Deploying tmp-boltz-upstream-mainnet-arkade-wallet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0386dbc
Status: ✅  Deploy successful!
Preview URL: https://57da46ad.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev
Branch Preview URL: https://wt-shadcn-ui-20260430.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev

View logs

sahilc0 added 2 commits April 30, 2026 13:21
Introduces a proper design token system with full color ramps and migrates
all opacity-based --darkXX tokens to solid --neutral-XXX colors.

New files:
- src/tokens.css: Single source of truth for colors, typography, shadows
- src/app.css: Tailwind v4 @theme configuration mapping tokens to utilities
- src/lib/utils.ts: cn() utility combining clsx + tailwind-merge

Token architecture:
- Color ramps (50-950): purple, green, red, orange, yellow, neutral
- Neutral ramp uses color-mix(in oklab) for automatic light/dark adaptation
- Dark mode selector: html.palette-dark (consistent with existing app)
- Typography tokens: font families, sizes, line heights, letter spacing
- Shadow/elevation tokens: sm, md, lg, xl with three-layer depth

Migration:
- All --darkXX opacity tokens replaced with --neutral-XXX solid colors
- Semantic aliases (--purple, --green, etc.) now reference ramps
- Legacy aliases added for backward compat (--background-color, --heading-font)
- 50+ component files updated to use new token system

Dependencies added:
- tailwindcss@^4.2.2, @tailwindcss/vite@^4.2.2
- clsx@^2.1.1, tailwind-merge@^3.5.0, class-variance-authority@^0.7.1
@sahilc0 sahilc0 force-pushed the wt-design-tokens-tailwind-20260429 branch from 27759e6 to f1eccc9 Compare April 30, 2026 18:21
Add 55 shadcn/ui components for future component migrations.

Setup:
- Add @/* path alias in tsconfig.json and vite.config.ts
- Add shadcn theme tokens to app.css
- Add components.json configuration

Components include: Button, Input, Dialog, Drawer, Sheet, Select,
Checkbox, Switch, Tabs, Card, Table, and 45 more primitives.

No existing code modified - primitives are available in src/components/ui/
for future use.
@sahilc0 sahilc0 force-pushed the wt-shadcn-ui-20260430 branch from 40e5f47 to 7472fbd Compare April 30, 2026 18:22
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Review: feat(ui): add shadcn/ui primitives

No protocol-critical code touched (no VTXO, signing, forfeit, round, or exit paths). This is purely a UI primitives drop. That said, there are several integration issues that will bite you at runtime.


🔴 Must Fix

1. Dark mode will not work on shadcn components

src/app.css adds two competing dark mode mechanisms:

  • Existing (line ~4 of base): @variant dark (&:where(html.palette-dark, html.palette-dark *));
  • New (line ~5 of diff): @custom-variant dark (&:is(.dark *));

The app toggles dark mode via html.palette-dark class (see src/providers/config.tsx:104-107), but all shadcn CSS tokens are scoped under .dark selector. The :root / .dark token block in app.css defines --background, --foreground, etc. under .dark {} — a class that is never added by the existing theme provider.

Result: Every shadcn component will be permanently stuck in light mode colors. The @custom-variant dark also redefines the Tailwind dark: variant, potentially breaking existing components that rely on the palette-dark selector.

Fix: Either (a) update the theme provider to also toggle .dark class on <html>, or (b) change the .dark {} selector in app.css to html.palette-dark {} to match the existing system.

2. sonner.tsx will crash at runtime

src/components/ui/sonner.tsx imports useTheme from next-themes:

import { useTheme } from 'next-themes'

This hook requires a <ThemeProvider> from next-themes wrapping the app. No such provider exists in this codebase. The first time any code renders <Toaster />, it will throw.

Fix: Either wrap the app in a next-themes ThemeProvider, or remove the useTheme dependency and wire it to the existing useConfig() theme state instead.

3. @layer base overrides existing global styles

src/app.css adds:

@layer base {
  * { @apply border-border outline-ring/50; }
  body { @apply bg-background text-foreground; }
  html { @apply font-sans; }
}

This applies border-color to every element, overrides the body background/text color with shadcn tokens (which won't match the existing palette in dark mode — see issue #1), and forces font-sans (Geist Variable) on <html>, replacing whatever font the app currently uses.

These are not inert primitives — they are global style overrides that will visually change the existing app.

Fix: Remove the @layer base block entirely. Shadcn components should use their own classes; global style reset should be a separate, deliberate PR with visual QA.


🟡 Should Fix

4. shadcn CLI belongs in devDependencies

package.json lists "shadcn": "^4.6.0" in dependencies. This is a CLI scaffolding tool (like create-react-app). It should be in devDependencies to avoid shipping it in production bundles.

5. recharts exact pin is inconsistent

"recharts": "3.8.0" — every other dependency uses ^ ranges. If this is intentional (known regression in 3.8.1+), add a comment. Otherwise use "^3.8.0".


🟢 Observations (non-blocking)

6. 13 new runtime dependencies for unused code — None of the 55 new components are imported by existing code. This is fine as a staging approach, but the dependency footprint is real: recharts, embla-carousel-react, react-day-picker, cmdk, date-fns, input-otp, react-resizable-panels, vaul, @base-ui/react, @fontsource-variable/geist, sonner, tw-animate-css, next-themes. Consider adding components (and their deps) incrementally as they're actually used.

7. No tests for new components — The 179 passing tests are all pre-existing. Acceptable for vendored primitives, but worth noting.

8. utils.ts change is cosmetic — Import reorder (clsx, type ClassValuetype ClassValue, clsx). No functional change. Fine.


Summary

The core issue is the dark mode integration gap (issues #1, #2, #3). The shadcn token system assumes a .dark class toggle that doesn't exist here, and the @layer base block will visually change the running app today. These need to be resolved before merge — either adapt shadcn's output to the existing palette-dark system, or introduce .dark class toggling alongside it.

Everything else is minor. No security or protocol concerns.

sahilc0 added 2 commits May 1, 2026 11:35
- Dark mode: change .dark selector to html.palette-dark in app.css and chart.tsx
- sonner.tsx: replace next-themes useTheme with ConfigContext (fixes crash)
- Remove body/html overrides from @layer base (keeps border rule)
- Move shadcn CLI to devDependencies
- Remove unused next-themes dependency
- Use ^3.8.0 for recharts (consistency)
- Fix eslint array-index-key warnings in chart.tsx and field.tsx
- Fix prettier formatting in 5 shadcn components
@bordalix
Copy link
Copy Markdown
Collaborator

bordalix commented May 5, 2026

@arkanaai review latest changes

Base automatically changed from wt-design-tokens-tailwind-20260429 to master May 7, 2026 09:12
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review: all feedback addressed ✅

Checked the changes since my previous review. All five issues are resolved:

# Issue Status
1 Dark mode .dark vs palette-dark mismatch ✅ Fixed — unified to html.palette-dark in app.css and chart.tsx
2 sonner.tsx crash (next-themes import) ✅ Fixed — now uses ConfigContext + Themes from existing codebase
3 @layer base global style overrides ✅ Fixed — removed entirely
4 shadcn CLI in production deps ✅ Fixed — moved to devDependencies
5 recharts exact pin ✅ Fixed — now ^3.8.0

Also removed next-themes and @base-ui/react from dependencies. Clean.

No protocol-critical code touched. LGTM for merge (after #582 lands).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

6-45: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add @base-ui/react to dependencies.

src/components/ui/direction.tsx imports DirectionProvider and useDirection from @base-ui/react/direction-provider, but this package is not declared in package.json. This will cause a module-not-found error at build time or runtime.

Suggested fix
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
+    "@base-ui/react": "^1.4.1",
     "cmdk": "^1.1.1",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 6 - 45, The project imports DirectionProvider and
useDirection from '@base-ui/react/direction-provider' in
src/components/ui/direction.tsx but the package is missing from package.json;
add "@base-ui/react" to the dependencies (pick a compatible version or use a
careted latest), update package.json accordingly, run your package manager
install (npm/yarn/pnpm) to restore modules, and re-run the build to ensure
DirectionProvider and useDirection resolve at runtime.
♻️ Duplicate comments (2)
src/components/ui/spinner.tsx (1)

4-5: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same missing React import as skeleton.tsx.

React.ComponentProps<'svg'> requires React in scope; the same fix applies:

+import * as React from 'react'
 import { cn } from '@/lib/utils'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/spinner.tsx` around lines 4 - 5, The Spinner component uses
React.ComponentProps<'svg'> but React is not imported; add an import for React
at the top of the file (e.g., import React from 'react') so Spinner({ className,
...props }: React.ComponentProps<'svg'>) compiles; update the file that defines
Spinner (and ensure imports for Loader2Icon and cn remain untouched).
src/components/ui/direction.tsx (1)

1-1: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

@base-ui/react is not declared as a dependency.

This import will fail at build/runtime since @base-ui/react is absent from package.json. See the corresponding comment on package.json for the fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/direction.tsx` at line 1, The file re-exports
DirectionProvider and useDirection from '@base-ui/react' but that package is not
declared in package.json; add '@base-ui/react' to dependencies in package.json
(using the appropriate version constraint), run npm/yarn install to update
lockfile, and then verify the import in src/components/ui/direction.tsx
(symbols: DirectionProvider, useDirection) resolves correctly in both dev and
build.
🟡 Minor comments (15)
src/components/ui/aspect-ratio.tsx-7-13 (1)

7-13: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve --ratio when caller passes style

Spreading ...props after style allows props.style to replace the internal --ratio variable, which can break sizing.

Suggested fix
-function AspectRatio({ ratio, className, ...props }: React.ComponentProps<'div'> & { ratio: number }) {
+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' as '--ratio']: ratio }}
       className={cn('relative aspect-(--ratio)', className)}
       {...props}
     />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/aspect-ratio.tsx` around lines 7 - 13, The component
currently sets style={{ '--ratio': ratio } as React.CSSProperties} and then
spreads {...props}, allowing props.style to overwrite --ratio; fix by merging
styles so the internal CSS variable wins: build the final style as an object
that spreads props.style first and then sets '--ratio': ratio (e.g., finalStyle
= { ...props.style, '--ratio': ratio } as React.CSSProperties), pass that
finalStyle to the element and still spread the rest of props; update references
in the AspectRatio component where style and {...props} are used so callers
cannot override the internal --ratio.
src/components/ui/pagination.tsx-39-43 (1)

39-43: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

data-active={isActive} renders data-active="false" when isActive is false — attribute remains present

React serialises boolean false as the string "false" for data-* attributes, so the attribute is present in the DOM. Any CSS [data-active] presence check or Tailwind data-active: variant will incorrectly match inactive links. Use || undefined to omit the attribute when not active:

🐛 Proposed fix
-        <a aria-current={isActive ? 'page' : undefined} data-slot='pagination-link' data-active={isActive} {...props} />
+        <a aria-current={isActive ? 'page' : undefined} data-slot='pagination-link' data-active={isActive || undefined} {...props} />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/pagination.tsx` around lines 39 - 43, The anchor in the
render prop currently sets data-active={isActive}, which produces
data-active="false" when isActive is false; change it to omit the attribute when
inactive by assigning data-active={isActive || undefined} (or
data-active={isActive ? true : undefined}) in the render JSX for the anchor so
the data-active attribute is only present when isActive is truthy.
src/components/ui/sonner.tsx-24-31 (1)

24-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

React.CSSProperties is used without importing the React namespace

import { useContext } from 'react' only brings in the named export; React.CSSProperties in the type assertion is unresolved and would fail tsc --noEmit. Vite/esbuild silently strips the annotation, so the runtime build passes.

🛡️ Proposed fix
+'use client'
+
+import * as React from 'react'
 import { useContext } from 'react'

Or inline the type annotation without the namespace:

-      } as React.CSSProperties
+      } as React.CSSProperties  // keep and add namespace import above
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/sonner.tsx` around lines 24 - 31, The style object uses the
type assertion React.CSSProperties but the React namespace isn't imported,
causing TypeScript errors; fix by either importing the React namespace (add
import React from 'react') so React.CSSProperties is resolved, or remove the
namespace and import the type directly (import { CSSProperties } from 'react')
and change the assertion to CSSProperties on the style object in
src/components/ui/sonner.tsx (the inline style block with '--normal-bg',
'--normal-text', etc.).
src/components/ui/pagination.tsx-72-83 (1)

72-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sr-only text inside an aria-hidden container is never read by assistive technology

aria-hidden propagates to all descendants, so the <span className='sr-only'>More pages</span> on Line 81 is invisible to screen readers. Either remove the dead span, or move aria-hidden onto the icon only so the text is announced:

🐛 Option A — remove the unreachable sr-only span
 <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>
🐛 Option B — announce "More pages" to screen readers (move aria-hidden to the icon only)
 <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 />
+  <MoreHorizontalIcon aria-hidden />
   <span className='sr-only'>More pages</span>
 </span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/pagination.tsx` around lines 72 - 83, The wrapper span in
PaginationEllipsis currently has aria-hidden which hides its child <span
className='sr-only'>More pages</span> from screen readers; remove aria-hidden
from the outer span in PaginationEllipsis and instead apply aria-hidden to the
visual icon (MoreHorizontalIcon) so the decorative icon is ignored but the
sr-only text is announced; keep the existing data-slot, className and props
spread on the wrapper and ensure the MoreHorizontalIcon receives aria-hidden (or
a surrounding element around the icon) so accessibility is preserved.
src/components/ui/empty.tsx-64-74 (1)

64-74: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

EmptyDescription is typed as React.ComponentProps<'p'> but renders a <div> — element/type mismatch

The prop type advertises HTMLParagraphElement semantics (including the ref shape), but the rendered element is an HTMLDivElement. Any consumer passing a ref would receive a mistyped DOM node. Semantically, a description/body-copy slot should also be a <p>.

🛡️ Proposed fix — render `

` to match the declared type and improve semantics

 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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/empty.tsx` around lines 64 - 74, The component
EmptyDescription is typed as React.ComponentProps<'p'> but renders a <div>,
causing an element/type mismatch; change the rendered element in the
EmptyDescription function from a div to a p so the DOM node matches the declared
type and semantics, keep the data-slot='empty-description', preserve className
merging via cn and spread {...props} onto the <p> so refs and paragraph-specific
props work correctly with the existing EmptyDescription signature.
src/components/ui/input-group.tsx-54-59 (1)

54-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

InputGroupAddon click-to-focus skips <textarea> controls

querySelector('input') only finds <input> elements. When the group contains an InputGroupTextarea, clicking the addon silently does nothing because no input element exists. Extend the selector to cover textarea as well:

🐛 Proposed fix
-        e.currentTarget.parentElement?.querySelector('input')?.focus()
+        e.currentTarget.parentElement?.querySelector('input, textarea')?.focus()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/input-group.tsx` around lines 54 - 59, The click handler on
the InputGroupAddon currently focuses only input elements using
querySelector('input'), so clicking the addon does nothing when the group
contains a textarea; update the selector in the onClick handler (the arrow
function bound to onClick in src/components/ui/input-group.tsx / the
InputGroupAddon click handler) to target both inputs and textareas (e.g.,
querySelector('input, textarea')) so clicking the addon focuses the appropriate
control.
src/components/ui/command.tsx-119-119 (1)

119-119: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix incorrect SVG descendant selector: data-selected:*:[svg]:text-foreground should use [&_svg] pattern

The selector data-selected:*:[svg]:text-foreground does not target SVG descendants as intended. In Tailwind v4, the *: variant generates > * (matching any direct child), and chaining it with [svg] produces an ineffective selector. The correct pattern used elsewhere in the same component is:

Fix
- data-selected:*:[svg]:text-foreground
+ data-selected:[&_svg]:text-foreground

The [&_svg] arbitrary selector pattern is used consistently throughout the codebase for SVG descendants and should be applied here for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/command.tsx` at line 119, The class string in
src/components/ui/command.tsx contains an incorrect SVG descendant selector
token `data-selected:*:[svg]:text-foreground`; replace that token with the
correct Tailwind arbitrary descendant pattern `[&_svg]:text-foreground` in the
same class string (the one starting with "group/command-item relative flex ...")
so SVG descendants receive the intended styles when `data-selected` is present.
src/app.css-317-323 (1)

317-323: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Radius scale values diverge significantly between @theme blocks.

The new @theme inline block recomputes radii from --radius: 0.625rem, producing values that contradict the earlier @theme definitions:

Token Earlier @theme @theme inline result
--radius-sm 0.25rem 0.375rem (+50%)
--radius-lg 1rem 0.625rem (−38%)
--radius-xl 1.5rem 0.875rem (−42%)

rounded-lg and rounded-xl utilities will render noticeably tighter corners than the design token intent. Align --radius with the existing scale, or remove the duplicate radius definitions from @theme inline and retain the earlier hardcoded ones.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.css` around lines 317 - 323, The inline `@theme` block redefines
--radius (and thus --radius-sm, --radius-md, --radius-lg, --radius-xl, etc.)
producing values that conflict with the original `@theme` tokens; either align the
inline --radius to the original token base or remove the duplicated radius
calculations. Update the `@theme` inline block to use the same --radius base value
as the original theme (so --radius-sm, --radius-lg, --radius-xl compute
identically) or delete the inline definitions for --radius, --radius-sm,
--radius-md, --radius-lg, --radius-xl, --radius-2xl, --radius-3xl, --radius-4xl
and rely on the earlier hardcoded tokens to keep rounded-lg/rounded-xl semantics
consistent.
src/components/ui/select.tsx-126-153 (1)

126-153: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scroll arrow buttons are missing a sticky positioning class.

top-0 / bottom-0 alone have no visual effect inside the overflow-y-auto popup — the buttons will scroll out of view with the list content. Add sticky so they stay anchored:

🐛 Proposed fix
-        "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
+        "sticky top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
-        "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
+        "sticky bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/select.tsx` around lines 126 - 153, The scroll arrow
buttons in SelectScrollUpButton and SelectScrollDownButton use top-0/bottom-0
but lack sticky positioning so they scroll away; update the className strings in
both components (SelectPrimitive.ScrollUpArrow and
SelectPrimitive.ScrollDownArrow) to include the "sticky" utility alongside the
existing "top-0" and "bottom-0" classes so the buttons remain anchored while the
popup content scrolls.
src/components/ui/field.tsx-104-115 (1)

104-115: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

FieldTitle shares data-slot='field-label' with FieldLabel — confirm intent.

Both FieldLabel (Line 93) and FieldTitle (Line 107) emit data-slot='field-label'. Any CSS selector targeting [data-slot=field-label] — including *:data-[slot=field-label]:flex-auto in fieldVariants — will apply to both. If this is intentional for layout purposes, a comment explaining the shared slot would help future maintainers. If FieldTitle is meant to be independently targetable, it should use its own slot name (e.g., data-slot='field-title').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/field.tsx` around lines 104 - 115, FieldTitle and
FieldLabel both emit data-slot='field-label', which causes shared CSS selectors
like the fieldVariants rule (*:data-[slot=field-label]:flex-auto) to hit both;
decide whether they should share styling or be distinct: if they should be
distinct, change FieldTitle's data-slot to 'field-title' and update any
selectors in fieldVariants (and other styles) that should target the new slot
name; if sharing is intentional, add a brief inline comment above the FieldTitle
component explaining why it uses the same data-slot as FieldLabel to avoid
future confusion.
src/components/ui/input-otp.tsx-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Suspicious cn-input-otp class — verify intent.

The cn-input-otp class on the containerClassName is unusual: the cn- prefix shadows the cn() utility name used throughout this file, and no other component in this PR uses a similar namespace prefix. data-slot='input-otp' already provides a reliable CSS hook for this container. Please confirm whether this is an intentional named class (used somewhere in the project CSS) or a typo.

🧹 If unintentional, remove the prefix
-      containerClassName={cn('cn-input-otp flex items-center has-disabled:opacity-50', containerClassName)}
+      containerClassName={cn('flex items-center has-disabled:opacity-50', containerClassName)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/input-otp.tsx` at line 17, The class string passed to
containerClassName includes a suspicious token 'cn-input-otp' which likely
collides with the cn() utility name or is a typo; verify whether 'cn-input-otp'
is intentionally referenced in project CSS, and if not remove it from the
containerClassName value so the component uses only the intended classes (e.g.,
'flex items-center has-disabled:opacity-50' plus any containerClassName prop)
and rely on data-slot='input-otp' for styling; update the JSX where
containerClassName is set (the cn(...) call in this component) accordingly.
src/components/ui/field.tsx-41-43 (1)

41-43: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

data-[slot=checkbox-group]:gap-3 never matches — dead selector.

FieldGroup always renders with data-slot='field-group', so the condition data-[slot=checkbox-group] on itself can never be true. Compare to FieldSet which correctly uses has-[>[data-slot=checkbox-group]]:gap-3 to detect a child with that slot. The intended behaviour (tighter gap when containing a checkbox group) should use the same has- pattern.

🐛 Proposed fix
-        'group/field-group `@container/field-group` flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
+        'group/field-group `@container/field-group` flex w-full flex-col gap-5 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 *:data-[slot=field-group]:gap-4',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/field.tsx` around lines 41 - 43, The selector
'data-[slot=checkbox-group]:gap-3' in FieldGroup is dead because FieldGroup
itself renders with data-slot='field-group'; replace that self-targeting
selector with the parent-has-child variant used in FieldSet (e.g. use the
has-[>[data-slot=checkbox-group]]:gap-3 pattern) so the container detects a
child with data-slot="checkbox-group" and applies the tighter gap; update the
class string in the FieldGroup component where className is composed.
src/components/ui/navigation-menu.tsx-95-95 (1)

95-95: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

easing-[ease] is not a valid Tailwind utility — likely a typo for ease-[ease].

easing- is not a Tailwind prefix; the correct utility for transition-timing-function is ease-*. As written, the class is silently ignored, so the intended animation timing for the ending-style state is never applied.

🔧 Proposed fix
-<NavigationMenuPrimitive.Popup className='data-[ending-style]:easing-[ease] xs:w-(--popup-width) ...'>
+<NavigationMenuPrimitive.Popup className='data-[ending-style]:ease-[ease] xs:w-(--popup-width) ...'>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/navigation-menu.tsx` at line 95, The tailwind class string
on NavigationMenuPrimitive.Popup uses the invalid utility "easing-[ease]" so the
ending-style timing isn't applied; update the className for the Popup
(NavigationMenuPrimitive.Popup) to replace "easing-[ease]" with the correct
Tailwind utility "ease-[ease]" so the transition-timing-function is applied for
the data-[ending-style] state.
src/components/ui/item.tsx-9-20 (1)

9-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

role="list" on ItemGroup without a corresponding role="listitem" on Item children.

The ARIA list role requires its owned elements to have role="listitem". Item renders a div (or custom element via render) and the useRender state object only sets data-* attributes — it does not inject role. Screen readers may not associate Item nodes as list members, breaking list navigation.

Consider adding role="listitem" either directly in Item's mergeProps call or as a documented requirement for consumers.

🔧 Proposed fix
 function Item({
   ...
 }: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) {
   return useRender({
     defaultTagName: 'div',
     props: mergeProps<'div'>(
       {
+        role: 'listitem',
         className: cn(itemVariants({ variant, size, className })),
       },
       props,
     ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/item.tsx` around lines 9 - 20, The ItemGroup component uses
role="list" but Item children lack role="listitem"; update the Item component
(the function that uses useRender and mergeProps/render logic) to include
role="listitem" in the merged props (e.g., add role: 'listitem' in the
mergeProps/returned props or when spreading state.attrs) so every rendered Item
element is exposed as a list item to assistive tech; alternatively, if you
prefer not to alter props, document in the Item component's API that consumers
must pass role="listitem" when rendering inside ItemGroup.
src/components/ui/breadcrumb.tsx-69-81 (1)

69-81: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sr-only text inside aria-hidden="true" is unreachable by screen readers.

aria-hidden="true" on the outer <span> hides the entire subtree from the accessibility tree, including the <span className='sr-only'>More</span> child. The "More" label is never announced. Either remove the sr-only span (if the intent is to fully hide the ellipsis from AT), or remove aria-hidden and keep the sr-only text (if "More" should be read aloud).

🔧 Proposed fix (remove dead sr-only 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 className='sr-only'>More</span>
 </span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/breadcrumb.tsx` around lines 69 - 81, The outer span in
BreadcrumbEllipsis has aria-hidden="true" which hides its subtree, making the
inner <span className='sr-only'>More</span> unreachable; decide intent and fix
accordingly—if the ellipsis is purely decorative, remove the inaccessible
sr-only child from the BreadcrumbEllipsis JSX and keep aria-hidden; if the
ellipsis should be announced, remove aria-hidden from the outer span and retain
the sr-only label so screen readers can read "More". Ensure the change is made
inside the BreadcrumbEllipsis component.
🧹 Nitpick comments (9)
src/components/ui/spinner.tsx (1)

5-5: 💤 Low value

Spread {...props} after role/aria-label lets callers silently override accessibility attributes.

This is intentional in many component libraries for composability, but worth being explicit about. If the intent is to always enforce role="status", move those attributes after {...props}:

♻️ Proposed fix
-  return <Loader2Icon role='status' aria-label='Loading' className={cn('size-4 animate-spin', className)} {...props} />
+  return <Loader2Icon {...props} role='status' aria-label='Loading' className={cn('size-4 animate-spin', className)} />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/spinner.tsx` at line 5, The Spinner component spreads
{...props} before setting role and aria-label which allows callers to override
those accessibility attributes; to enforce them always, move role='status' and
aria-label='Loading' after the {...props} spread in the JSX returned by the
Spinner (the line rendering Loader2Icon) so Loader2Icon always receives role and
aria-label last and cannot be overridden by props while keeping className
composition via cn('size-4 animate-spin', className).
src/hooks/use-mobile.ts (1)

9-14: ⚡ Quick win

Minor inconsistency between matchMedia and window.innerWidth.

matchMedia measures viewport width including the scrollbar, while window.innerWidth excludes it. This means the listener fires on breakpoint crossing (correct threshold) but onChange re-reads a slightly different width. For consistency, read from mql.matches inside onChange instead of re-querying window.innerWidth.

♻️ Proposed fix
-    const onChange = () => {
-      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
-    }
+    const onChange = () => {
+      setIsMobile(mql.matches)
+    }
     mql.addEventListener('change', onChange)
-    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    setIsMobile(mql.matches)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/use-mobile.ts` around lines 9 - 14, The onChange handler is
inconsistent because it reads window.innerWidth while the listener is attached
to mql; update onChange to use mql.matches to determine the breakpoint state and
pass that value to setIsMobile so the listener and initial state use the same
source (refer to mql, onChange, setIsMobile, and MOBILE_BREAKPOINT).
src/components/ui/hover-card.tsx (1)

25-43: 💤 Low value

data-slot on Portal is a no-op and may be misleading.

PreviewCardPrimitive.Portal (like all Base UI portals) renders children directly into document.body without a wrapping DOM node, so data-slot='hover-card-portal' won't appear in the DOM. Compare: SelectContent's SelectPrimitive.Portal (line 57 in select.tsx) correctly omits data-slot. The meaningful data-slot is already on the Popup at line 34.

🛠️ Proposed fix
-    <PreviewCardPrimitive.Portal data-slot='hover-card-portal'>
+    <PreviewCardPrimitive.Portal>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/hover-card.tsx` around lines 25 - 43, Remove the misleading
no-op data-slot on PreviewCardPrimitive.Portal: delete the
data-slot='hover-card-portal' attribute from the PreviewCardPrimitive.Portal
element and leave the meaningful data-slot on PreviewCardPrimitive.Popup (which
is the rendered DOM node). This ensures only the actual rendered Popup carries
the data-slot; update any related tests/docs if they referenced the portal
data-slot.
src/components/ui/calendar.tsx (1)

134-167: ⚡ Quick win

getDefaultClassNames() is called on every render of every day button.

CalendarDayButton is instantiated for each visible day cell (up to ~42 per month). Calling getDefaultClassNames() fresh on every render is unnecessary since the result is static. Move it to a module-level constant or cache it in the parent:

♻️ Proposed fix
+const defaultDayButtonClassNames = getDefaultClassNames()
+
 function CalendarDayButton({
   className,
   day,
   modifiers,
   locale,
   ...props
 }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
-  const defaultClassNames = getDefaultClassNames()
-
   const ref = React.useRef<HTMLButtonElement>(null)
   ...
       className={cn(
         '...',
-        defaultClassNames.day,
+        defaultDayButtonClassNames.day,
         className,
       )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/calendar.tsx` around lines 134 - 167, CalendarDayButton
unnecessarily calls getDefaultClassNames() on every render for each day cell;
move the static result out of the component to avoid repeated computation by
either creating a module-level constant (e.g., DEFAULT_CLASS_NAMES =
getDefaultClassNames()) and use that inside CalendarDayButton, or compute it
once in the parent and pass it into CalendarDayButton as a prop (or at minimum
wrap the call in React.useMemo with an empty dependency array). Update
references to defaultClassNames in CalendarDayButton to use the new
DEFAULT_CLASS_NAMES (or the prop name) and remove the inline call to
getDefaultClassNames().
src/components/ui/alert-dialog.tsx (1)

125-142: ⚡ Quick win

Redundant cn(className) calls in AlertDialogAction and AlertDialogCancel.

cn(singleString) returns the string unchanged — it has no effect here. Either pass className directly or add actual base classes that justify the cn() call.

♻️ Proposed fix
 function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
-  return <Button data-slot='alert-dialog-action' className={cn(className)} {...props} />
+  return <Button data-slot='alert-dialog-action' className={className} {...props} />
 }

 function AlertDialogCancel({
   className,
   variant = 'outline',
   size = 'default',
   ...props
 }: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
   return (
     <AlertDialogPrimitive.Close
       data-slot='alert-dialog-cancel'
-      className={cn(className)}
+      className={className}
       render={<Button variant={variant} size={size} />}
       {...props}
     />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/alert-dialog.tsx` around lines 125 - 142, The cn(className)
calls in AlertDialogAction and AlertDialogCancel are redundant because cn with a
single string returns it unchanged; update these components to either pass
className directly to the underlying elements (replace cn(className) with
className) or provide actual base classes to cn (e.g., cn('base-classes',
className)) so the helper is meaningful; change the return in AlertDialogAction
and the className prop on AlertDialogPrimitive.Close (and keep the render prop
using Button) accordingly, referencing AlertDialogAction, AlertDialogCancel,
Button, AlertDialogPrimitive.Close, and cn.
src/components/ui/combobox.tsx (1)

181-195: ⚡ Quick win

ComboboxChips props type is inconsistently double-wrapped.

Every other component in this file uses only ComboboxPrimitive.X.Props, but ComboboxChips intersects it with React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips>. This creates a redundant intersection that can introduce conflicting or duplicated prop definitions. ComboboxPrimitive.Chips.Props alone is sufficient and consistent with the rest.

♻️ Proposed fix
 function ComboboxChips({
   className,
   ...props
-}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props) {
+}: ComboboxPrimitive.Chips.Props) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/combobox.tsx` around lines 181 - 195, The ComboboxChips
props are unnecessarily typed as an intersection of
React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> and
ComboboxPrimitive.Chips.Props, causing redundant/Conflicting types; change the
component signature to accept only ComboboxPrimitive.Chips.Props (i.e., update
the ComboboxChips parameter type to use ComboboxPrimitive.Chips.Props directly)
and remove the React.ComponentPropsWithRef<> wrapper so the typing matches the
other components in this file.
src/components/ui/progress.tsx (1)

1-1: 💤 Low value

'use client' directive is a no-op in this Vite project.

The project uses Vite without RSC support, so 'use client' has no effect and is inconsistent with all other newly added src/components/ui/* files, which omit it. It's likely a copy-paste artifact from a Next.js-based shadcn template.

🧹 Proposed fix
-'use client'
-
 import { Progress as ProgressPrimitive } from '@base-ui/react/progress'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/progress.tsx` at line 1, Remove the stray `'use client'`
directive from the top of src/components/ui/progress.tsx (it is a no-op in this
Vite project and inconsistent with other components); simply delete that line so
the Progress component file matches the rest of the ui components and avoids
Next.js-specific artifacts.
src/components/ui/menubar.tsx (1)

73-76: ⚡ Quick win

MenubarContent is missing close animation classes present on MenubarSubContent.

MenubarSubContent (line 233) includes data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95, but MenubarContent (line 74) omits them. The menu will appear without an exit animation when dismissed.

✨ Proposed fix
  className={cn(
-   'min-w-36 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95',
+   'min-w-36 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
    className,
  )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/menubar.tsx` around lines 73 - 76, MenubarContent is
missing the exit animation classes that MenubarSubContent uses; update the
className in the MenubarContent component to include the same data-closed
classes (data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95)
so the top-level menu gets a closing animation like MenubarSubContent; locate
the className invocation in the MenubarContent JSX and append those data-closed
classes to the existing animation-related class list.
src/components/ui/sheet.tsx (1)

39-74: ⚡ Quick win

sidebar.tsx hides the close button via CSS instead of showCloseButton={false} — fragile.

sidebar.tsx (line 171) suppresses the SheetContent close button using [&>button]:hidden. This works today because SheetPrimitive.Close renders a Button as a direct child, but the selector will silently break if SheetContent's internal DOM structure changes. The showCloseButton prop exists precisely for this use case.

In sidebar.tsx, replace:

- className='w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden'
+ className='w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground'
  ...
+ showCloseButton={false}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/sheet.tsx` around lines 39 - 74, The sidebar hides the
close button via a fragile CSS selector instead of using the SheetContent prop;
update the sidebar usage to pass showCloseButton={false} to the SheetContent
component and remove the `[&>button]:hidden` rule from the sidebar styles so the
visibility is controlled by the SheetContent prop (SheetContent /
showCloseButton and SheetPrimitive.Close are the relevant symbols to modify).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f62289a-512b-433c-822b-9b1872ff0c12

📥 Commits

Reviewing files that changed from the base of the PR and between 7066839 and d05532f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (62)
  • components.json
  • package.json
  • src/app.css
  • src/components/ui/accordion.tsx
  • src/components/ui/alert-dialog.tsx
  • src/components/ui/alert.tsx
  • src/components/ui/aspect-ratio.tsx
  • src/components/ui/avatar.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/breadcrumb.tsx
  • src/components/ui/button-group.tsx
  • src/components/ui/button.tsx
  • src/components/ui/calendar.tsx
  • src/components/ui/card.tsx
  • src/components/ui/carousel.tsx
  • src/components/ui/chart.tsx
  • src/components/ui/checkbox.tsx
  • src/components/ui/collapsible.tsx
  • src/components/ui/combobox.tsx
  • src/components/ui/command.tsx
  • src/components/ui/context-menu.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/direction.tsx
  • src/components/ui/drawer.tsx
  • src/components/ui/dropdown-menu.tsx
  • src/components/ui/empty.tsx
  • src/components/ui/field.tsx
  • src/components/ui/hover-card.tsx
  • src/components/ui/input-group.tsx
  • src/components/ui/input-otp.tsx
  • src/components/ui/input.tsx
  • src/components/ui/item.tsx
  • src/components/ui/kbd.tsx
  • src/components/ui/label.tsx
  • src/components/ui/menubar.tsx
  • src/components/ui/native-select.tsx
  • src/components/ui/navigation-menu.tsx
  • src/components/ui/pagination.tsx
  • src/components/ui/popover.tsx
  • src/components/ui/progress.tsx
  • src/components/ui/radio-group.tsx
  • src/components/ui/resizable.tsx
  • src/components/ui/scroll-area.tsx
  • src/components/ui/select.tsx
  • src/components/ui/separator.tsx
  • src/components/ui/sheet.tsx
  • src/components/ui/sidebar.tsx
  • src/components/ui/skeleton.tsx
  • src/components/ui/slider.tsx
  • src/components/ui/sonner.tsx
  • src/components/ui/spinner.tsx
  • src/components/ui/switch.tsx
  • src/components/ui/table.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/toggle-group.tsx
  • src/components/ui/toggle.tsx
  • src/components/ui/tooltip.tsx
  • src/hooks/use-mobile.ts
  • src/lib/utils.ts
  • tsconfig.json
  • vite.config.ts

Comment thread src/app.css
Comment on lines +283 to +316
@theme inline {
--font-heading: var(--font-sans);
--font-sans: 'Geist Variable', sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

@theme inline silently overrides all color utilities to shadcn grayscale defaults.

In Tailwind v4, a later @theme declaration wins over an earlier one for the same variable name. The @theme inline block redefines every semantic color token—--color-primary, --color-ring, --color-border, --color-destructive, etc.—pointing them at new :root CSS variables (--primary, --ring, …) whose values are shadcn's out-of-the-box oklch grayscale palette:

Token Earlier @theme (brand) @theme inline (shadcn default)
--color-primary var(--purple-700) oklch(0.205 0 0) ≈ near-black
--color-ring var(--purple-700) oklch(0.708 0 0) grey
--color-destructive var(--red-700) oklch(0.577 0.245 27.325)

Any existing code using bg-primary, text-primary, ring-ring, etc. will silently switch from the brand palette to these greyscale defaults.

To preserve the brand identity, wire the shadcn :root variables to the arkade tokens:

🐛 Proposed fix — map shadcn runtime variables to arkade tokens in :root
 :root {
-  --background: oklch(1 0 0);
-  --foreground: oklch(0.145 0 0);
   ...
-  --primary: oklch(0.205 0 0);
-  --primary-foreground: oklch(0.985 0 0);
-  --secondary: oklch(0.97 0 0);
-  --secondary-foreground: oklch(0.205 0 0);
   ...
-  --ring: oklch(0.708 0 0);
+  --background: var(--bg);
+  --foreground: var(--fg);
   ...
+  --primary: var(--purple-700);
+  --primary-foreground: `#fff`;
+  --secondary: var(--neutral-100);
+  --secondary-foreground: var(--fg);
   ...
+  --ring: var(--purple-700);
🧰 Tools
🪛 Stylelint (17.10.0)

[error] 283-283: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.css` around lines 283 - 316, The `@theme` inline block is replacing
your brand colors because shadcn reads its own :root vars (e.g. --primary,
--ring, --border, --destructive, --accent, --muted, --secondary, --popover,
--card, --foreground, --background, --chart-1..5, --sidebar-*, etc.); fix this
by adding a :root mapping that points every shadcn runtime variable to your
arkade semantic tokens (for example set --primary: var(--color-primary); --ring:
var(--color-ring); --border: var(--color-border); --destructive:
var(--color-destructive); --input: var(--color-input); --accent:
var(--color-accent); --muted: var(--color-muted); --secondary:
var(--color-secondary); --popover: var(--color-popover); --card:
var(--color-card); --foreground: var(--color-foreground); --background:
var(--color-background); --chart-1: var(--color-chart-1); … and map the
--sidebar-* names to your --color-sidebar-* tokens) so that when `@theme` inline
defines --color-* values it resolves back to your brand palette rather than the
shadcn grayscale defaults.

Comment thread src/components/ui/carousel.tsx
Comment thread src/components/ui/carousel.tsx
Comment thread src/components/ui/chart.tsx
Comment thread src/components/ui/command.tsx
Comment thread src/components/ui/sidebar.tsx Outdated
Comment thread src/components/ui/skeleton.tsx
Comment thread src/components/ui/slider.tsx
Comment thread src/components/ui/tabs.tsx
Comment thread src/components/ui/toggle-group.tsx Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (new commit 39b8f05c)

Single commit since my last approval — carousel keyboard nav fix from CodeRabbit suggestion.

src/components/ui/carousel.tsx:78-90 — Adds ArrowUp/ArrowDown support for vertical carousels, correctly guarded by !isHorizontal. orientation properly added to the useCallback dependency array. Clean.

No protocol-critical code touched. Still LGTM.

bordalix and others added 2 commits May 11, 2026 10:58
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (new commit 12ac4737)

Single commit since last approval — CodeRabbit co-authored fix to toggle-group.tsx.

src/components/ui/toggle-group.tsx:41-44 — Fixes spacing calculation: --gap now properly computed as calc(var(--spacing) * ${spacing}) instead of raw number, and gap-[var(--gap)] replaces the invalid gap-[--spacing(var(--gap))] syntax. Correct fix.

No protocol-critical code touched. Still LGTM.

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (new commit 1d94a6ef)

Single commit since last approval — CodeRabbit co-authored fix to tabs.tsx.

src/components/ui/tabs.tsx:9 — Passes orientation prop to TabsPrimitive.Root. Previously only data-orientation (CSS) was set but the actual prop wasn't forwarded, meaning keyboard navigation (arrow key direction) would default to horizontal regardless of the orientation value. Correct fix.

No protocol-critical code. LGTM. ✅

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (new commit 13a4c725)

separator.tsx — Correct fix. data-[orientation=horizontal]: properly targets the data-orientation="horizontal" attribute rendered by @base-ui/react/separator, replacing the shorthand data-horizontal: which only matches boolean data-horizontal attributes.

Consistent with the same pattern applied to toggle-group.tsx and tabs.tsx in the previous commits.

No protocol-critical code. LGTM. ✅

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (new commit 5f6b51f1)

src/components/ui/kbd.tsx:17KbdGroup now returns <div> instead of <kbd>, correctly matching its React.ComponentProps<'div'> type. Previous version had a semantic mismatch (wrapper typed as div but rendered as kbd). Clean fix.

No protocol-critical code. LGTM. ✅

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

bordalix and others added 2 commits May 11, 2026 11:12
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: João Bordalo <bordalix@users.noreply.github.com>
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (new commit 94158bbe)

src/components/ui/chart.tsx:88-95 — CSS injection hardening in ChartStyle. Good fix:

  • CSS.escape(id) on the attribute selector — prevents selector breakout
  • safeKey = key.replace(/[^a-zA-Z0-9_-]/g, '') — whitelists CSS custom property name chars
  • safeColor = color?.replace(/[;{}]/g, '') — prevents declaration/block escape

Also correctly removes stale ) from the arrow-to-implicit-return conversion (line 101). File compiles clean.

No protocol-critical code. LGTM. ✅

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

Re-review (commits 011111f6..eecd64a1)

Three commits since last approval: carousel cleanup fix, CodeRabbit suggestions batch, lockfile update.

✅ Clean

  • carousel.tsx:103 — Now cleans up reInit listener in the effect teardown, matching the subscription at line 98. Correct.
  • command.tsx:40-47DialogHeader moved inside DialogContent. Required by Radix Dialog — DialogTitle/DialogDescription must be descendants of DialogContent for the aria-labelledby/aria-describedby linkage to work. Correct fix.
  • sidebar.tsx:452hsl(var(--sidebar-border))var(--sidebar-border). Matches Tailwind v4 convention where CSS variables already contain the full color value. Correct.
  • slider.tsx:6 — Default from [min, max] to [min]. A single-thumb slider should default to one value, not a range. Correct.
  • chart.tsx:104 — Missing closing ) for ChartStyle return. Syntax fix.

⚠️ Potential duplicate React keys

chart.tsx:178 and chart.tsx:263 — Both ChartTooltipContent and ChartLegendContent now use key={key} where key = nameKey ?? item.dataKey ?? 'value'.

Problem: if a consumer passes nameKey (a single string applied to all items), every item in the .map() gets the same key. The previous code had index as a fallback to guarantee uniqueness. The new code dropped index entirely.

This won't crash but will produce React warnings and potentially incorrect DOM reconciliation when payload items share the same dataKey or when nameKey is provided.

Suggested fix — append index to guarantee uniqueness:

// chart.tsx, tooltip (~line 178)
key={`${key}-${index}`}

// chart.tsx, legend (~line 263) — restore index param too
.map((item, index) => {
  ...
  key={`${key}-${index}`}

Low severity since this is UI-only and not yet consumed, but worth fixing before these components get used.

No protocol-critical code touched. Overall LGTM — the key issue is minor but easy to fix now.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ui/chart.tsx`:
- Around line 169-178: The current tooltip/legend mapping in the payload
iteration uses a derived key string (computed via
nameKey/item.name/item.dataKey) which can collide across items; update the key
generation in the payload.map used for rendering tooltip/legend items (the block
that computes const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`
and calls getPayloadConfigFromPayload) to produce collision-safe keys by
appending a unique discriminator (e.g., include the loop index, item.id,
item.type, or a hash) so each rendered <div> has a stable unique key (for
example change the key to combine the derived key with index or item-specific
id) and apply the same change to the other mapping that renders legend items
(the similar block around lines 254-263).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ec50588-0fd5-47bb-b69d-6ef7ddf2043e

📥 Commits

Reviewing files that changed from the base of the PR and between 39b8f05 and 0386dbc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • package.json
  • src/components/ui/carousel.tsx
  • src/components/ui/chart.tsx
  • src/components/ui/command.tsx
  • src/components/ui/kbd.tsx
  • src/components/ui/separator.tsx
  • src/components/ui/sidebar.tsx
  • src/components/ui/slider.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/toggle-group.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/ui/separator.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/components/ui/command.tsx
  • src/components/ui/slider.tsx
  • src/components/ui/toggle-group.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/sidebar.tsx
  • package.json
  • src/components/ui/carousel.tsx

Comment on lines +169 to +178
{payload
.filter((item) => item.type !== 'none')
.map((item, index) => {
const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color ?? item.payload?.fill ?? item.color

return (
<div
key={key}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use collision-safe React keys for tooltip/legend items.

Line 178 and Line 262 can collide when multiple items resolve to the same derived key, which can cause React reconciliation glitches and warnings.

💡 Proposed fix
@@
-        {payload
+        {payload
           .filter((item) => item.type !== 'none')
           .map((item, index) => {
             const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`
+            const reactKey = `${item.dataKey ?? key}-${index}`
             const itemConfig = getPayloadConfigFromPayload(config, item, key)
             const indicatorColor = color ?? item.payload?.fill ?? item.color

             return (
               <div
-                key={key}
+                key={reactKey}
                 className={cn(
@@
-      {payload
+      {payload
         .filter((item) => item.type !== 'none')
-        .map((item) => {
+        .map((item, index) => {
           const key = `${nameKey ?? item.dataKey ?? 'value'}`
+          const reactKey = `${item.dataKey ?? key}-${index}`
           const itemConfig = getPayloadConfigFromPayload(config, item, key)

           return (
             <div
-              key={key}
+              key={reactKey}
               className={cn('flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground')}
             >

Also applies to: 254-263

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/chart.tsx` around lines 169 - 178, The current
tooltip/legend mapping in the payload iteration uses a derived key string
(computed via nameKey/item.name/item.dataKey) which can collide across items;
update the key generation in the payload.map used for rendering tooltip/legend
items (the block that computes const key = `${nameKey ?? item.name ??
item.dataKey ?? 'value'}` and calls getPayloadConfigFromPayload) to produce
collision-safe keys by appending a unique discriminator (e.g., include the loop
index, item.id, item.type, or a hash) so each rendered <div> has a stable unique
key (for example change the key to combine the derived key with index or
item-specific id) and apply the same change to the other mapping that renders
legend items (the similar block around lines 254-263).

@bordalix bordalix merged commit 30bdae1 into master May 12, 2026
6 checks passed
@bordalix bordalix deleted the wt-shadcn-ui-20260430 branch May 12, 2026 09:06
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.

2 participants