Skip to content

feat(ui): migrate core components to shadcn primitives (3/3) - #593

Merged
bordalix merged 32 commits into
masterfrom
wt-component-migration-20260430
May 18, 2026
Merged

bordalix merged 32 commits into
masterfrom
wt-component-migration-20260430

Conversation

@sahilc0

@sahilc0 sahilc0 commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Stack

⚠️ This PR is part of a stacked series. Base branch is wt-shadcn-ui-20260430, not master.

# PR Status
1 #582 - Design tokens + Tailwind v4 Pending review
2 #590 - shadcn/ui primitives Pending review
3 This PR - Component migrations 👈

Summary

Migrate Modal, Checkbox, Select, and Toggle to use shadcn/ui primitives with improved animations and accessibility.

Modal

  • Replace static overlay with Framer Motion AnimatePresence for enter/exit animations
  • Add open, onOpenChange, onExitComplete props for controlled usage
  • Fade + scale animation with consistent easing (EASE_OUT_QUINT_TUPLE)

Checkbox

  • Migrate to shadcn Checkbox primitive
  • Proper checked state visualization with primary color
  • Maintain haptic feedback on interaction

Select

  • Migrate to shadcn RadioGroup
  • Preserve keyboard navigation (arrow keys)
  • Remove legacy FlexRow wrapper

Toggle

  • Use shadcn Switch with new lg size variant
  • iOS-like depth with three-layer shadow on thumb
  • 44px minimum tap target for mobile accessibility

Switch (ui primitive)

  • Add lg size variant (24×44px track, 20px thumb)
  • Add inner track shadow for depth
  • Proper thumb positioning with 2px inset

Updated Usage Sites

  • Burn.tsx, Reissue.tsx: Use onExitComplete for async operation coordination
  • Backup.tsx, Announcement.tsx: Use controlled Modal props

Test Plan

  • Modal: Settings → Backup → "View private key" — verify enter/exit animations
  • Checkbox: Settings → Reset wallet — verify check state persists
  • Select: Settings → Theme — verify keyboard nav and selection
  • Toggle: Settings → various toggles — verify size and haptics
  • Unit tests pass (bun run test)

Files Changed

  • src/components/Modal.tsx
  • src/components/Checkbox.tsx
  • src/components/Select.tsx
  • src/components/Toggle.tsx
  • src/components/ui/switch.tsx
  • src/components/ui/dialog.tsx
  • src/components/Announcement.tsx
  • src/screens/Apps/Assets/Burn.tsx
  • src/screens/Apps/Assets/Reissue.tsx
  • src/screens/Settings/Backup.tsx
  • src/test/components/Checkbox.test.tsx

Summary by CodeRabbit

  • New Features

    • Added Base UI React library
  • Improvements

    • Animated, controlled modals and smoother drawer interactions
    • Unified component variants and className support for buttons, inputs, selects, toggles, switches, and checkboxes
    • Larger switch size, improved tap targets, keyboard handling, and other UX/accessibility refinements
    • Confirmation flows now wait for modal exit before performing actions
    • Mobile amount button opens native keyboard on mobile
  • Tests

    • Added visual checkbox behavior test
  • Chores

    • Dependency and tooling updates; removed some unused CSS variables

Review Change Stack

sahilc0 added 7 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
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.
- 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
Migrate Modal, Checkbox, Select, and Toggle to use shadcn/ui primitives
with improved animations and accessibility:

Modal:
- Replace static overlay with Framer Motion AnimatePresence
- Add open/onOpenChange/onExitComplete props for controlled usage
- Add enter/exit animations (fade + scale)
- Use EASE_OUT_QUINT_TUPLE for consistent easing

Checkbox:
- Migrate to shadcn Checkbox primitive
- Add proper checked state visualization
- Maintain haptic feedback on interaction

Select:
- Migrate to shadcn RadioGroup
- Preserve keyboard navigation (arrow keys)
- Remove legacy FlexRow wrapper

Toggle:
- Use shadcn Switch with new lg size variant
- Switch gets iOS-like depth with three-layer shadow
- 44px minimum tap target for mobile

Updated usage sites:
- Burn.tsx, Reissue.tsx: Use onExitComplete for async coordination
- Backup.tsx, Announcement.tsx: Use controlled Modal props
@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Converts Modal to a controlled, animated component and modernizes UI primitives (Button/CVA, Checkbox, Switch/Toggle, Select, SheetModal). Asset confirm flows defer async work until modal exit. Updates deps/config, exports MAX_DECIMALS, adjusts CSS, and adds a Checkbox behavior test.

Changes

UI Component Modernization and Controlled Modal Refactoring

Layer / File(s) Summary
Modal controlled API and Framer Motion implementation
src/components/Modal.tsx
Modal implements typed ModalProps with controlled open, onOpenChange, and onExitComplete; uses AnimatePresence and Framer Motion for overlay/content animations and applies blur side-effects when closed.
Modal adoption in Announcement and Backup
src/components/Announcement.tsx, src/screens/Settings/Backup.tsx
Announcement and Backup switch to controlled Modal usage with local useState and onOpenChange/onExitComplete wiring.
Deferred confirmation pattern for asset operations
src/screens/Apps/Assets/Burn.tsx, src/screens/Apps/Assets/Reissue.tsx
Burn/Reissue defer the async operation until the confirmation modal's exit completes using pendingConfirm refs and processBurn/processReissue triggered from onExitComplete; confirmation UIs converted to controlled Modal.
Checkbox replaced with shadcn component + test
src/components/Checkbox.tsx, src/test/components/Checkbox.test.tsx
Replaces custom checkbox with shadcn Checkbox, links label via useId, uses controlled checked and onCheckedChange with hapticLight(), and adds a test asserting data-checked toggling.
Toggle, Select, and Switch styling updates
src/components/Toggle.tsx, src/components/Select.tsx, src/components/ui/switch.tsx
Toggle now renders Switch (adds 'lg' size). Select refactored to RadioGroup/RadioGroupItem with document keydown listener and handleChange invoking hapticSubtle(). Switch Root/Thumb classes reworked for tap-target, shadows, and size variants.
SheetModal migrated from react-spring to Drawer
src/components/SheetModal.tsx
Replaces react-spring-bottom-sheet with app Drawer/DrawerContent, uses onOpenChange to call hapticLight() + onClose() on close, and applies Tailwind safe-area padding.
Button variant system using CVA
src/components/Button.tsx
Adds buttonVariants (CVA), supports variant prop with fallback to legacy boolean props, accepts className, composes classes with cn, and exports buttonVariants.
Input className prop support
src/components/Input.tsx
Adds optional className prop to Input and merges it with base 'input' class via cn.
Dependencies, test config, asset precision, and CSS tweaks
package.json, vitest.config.ts, src/lib/assets.ts, src/index.css, src/screens/Wallet/Receive/QrCode.tsx, src/screens/Apps/Assets/Mint.tsx, src/components/ui/command.tsx, src/components/ui/drawer.tsx, tsconfig.json
Adds @base-ui/react dependency and replaces cmdk/vaul with cmdk-base/vaul-base, reorders shadcn in devDeps, configures Vitest alias and exclude patterns, exports MAX_DECIMALS and uses it as defaults/validation, removes several :root CSS vars and tweaks .button.copy bg, and conditions QrCode Add-amount on mobile and Mint validations on MAX_DECIMALS.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ConfirmButton
  participant Modal
  participant Processor
  User->>ConfirmButton: click Confirm
  ConfirmButton->>Modal: set open=false
  Modal->>Processor: onExitComplete triggers processing
  Processor->>Processor: perform async operation
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • pietro909
  • bordalix
  • Kukks
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(ui): migrate core components to shadcn primitives (3/3)' directly and clearly summarizes the main change—migrating core UI components to shadcn/ui primitives, which is the primary focus of the changeset across Modal, Checkbox, Select, Toggle, and related files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-component-migration-20260430

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sahilc0
sahilc0 requested review from bordalix and pietro909 May 1, 2026 19:38
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 1, 2026 •

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 249708f
Status: ✅  Deploy successful!
Preview URL: https://a39e2ddd.wallet-bitcoin.pages.dev
Branch Preview URL: https://wt-component-migration-20260.wallet-bitcoin.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 1, 2026 •

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 249708f
Status: ✅  Deploy successful!
Preview URL: https://7ce39241.arkade-wallet.pages.dev
Branch Preview URL: https://wt-component-migration-20260.arkade-wallet.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 1, 2026 •

Copy link
Copy Markdown

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

Latest commit: 249708f
Status: ✅  Deploy successful!
Preview URL: https://e7e7a03c.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev
Branch Preview URL: https://wt-component-migration-20260.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev

View logs

@ghost ghost 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.

Code Review — feat(ui): migrate core components to shadcn primitives (3/3)

Verdict: Request changes — one likely bug, one fragile pattern, a few nits. No protocol-critical concerns (pure UI migration, no signing/VTXO/forfeit logic touched).


🐛 BUG — Announcement close may never fire after "Try it"

src/components/Announcement.tsx:113-114

Old code called close() synchronously in handleTryIt. New code calls setOpen(false) and relies on onExitComplete={close} to eventually fire close.

// Old (guaranteed):
close()

// New (deferred):
setOpen(false)  // → animation → onExitComplete → close()

If navigate() triggers a re-render that unmounts the Announcement component before the exit animation completes, onExitComplete never fires and close() never runs. The parent still thinks the announcement is active → it reappears when the user returns.

Fix: Call close() directly in handleTryIt (like before), OR use a ref-based pattern like Burn/Reissue to ensure close fires on exit complete while also handling the unmount case. At minimum, add close as a fallback in a cleanup effect:

useEffect(() => {
  return () => { if (!open) close() }
}, [open, close])

⚠️ FRAGILE — Checkbox dual-handler pattern

src/components/Checkbox.tsx:21-28

The outer <div> has onClick={() => handleChange(!checked)} and the inner <ShadcnCheckbox> has onCheckedChange={handleChange}. The pointer-events-none class on the checkbox prevents mouse-driven double-fire, but:

  1. onCheckedChange is effectively dead code — it can never fire via pointer events. This is confusing.
  2. Assistive tech (screen readers, Switch Access) may activate the checkbox directly, bypassing pointer-events-none. If that happens, both handlers fire → onChange() called twice → Reset page's disabled state toggles twice (noop — checkbox looks checked but button stays disabled).

Fix: Remove onCheckedChange from the ShadcnCheckbox since the div's onClick handles everything:

<ShadcnCheckbox
  checked={checked}
  className='size-6 rounded-md pointer-events-none data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground'
/>

Or, go the other direction — remove the div's onClick and let the ShadcnCheckbox handle its own state (remove pointer-events-none, use a <label> wrapper).


📝 NITS

1. Modal overlay click-to-dismiss is new behavior
src/components/Modal.tsx:32 — The overlay's onClick={() => onOpenChange?.(false)} lets users dismiss the Burn/Reissue confirmation dialogs by clicking outside. The old Modal didn't support this. Safe (cancels the operation), but intentional? Worth noting for QA.

2. Select: redundant RadioGroupItem
src/components/Select.tsx:42-44 — The RadioGroupItem is visually hidden (opacity-0, absolute, -m-px, size-px). The label's onClick calls event.preventDefault() + handleChange() manually, so the radio never fires its own change event. The RadioGroup's onValueChange also fires from handleChange. This works but the RadioGroupItem is essentially vestigial — consider whether it adds accessibility value (it does provide ARIA semantics, so probably keep it, but worth a comment).

3. Select useEffect deps fix is good
src/components/Select.tsx:29 — Adding options and onChange to the dep array is a legit bugfix 👍. Stale closures were possible before.

4. Button backward compat is clean
src/components/Button.tsx:63-64 — The resolvedVariant fallback from boolean props to CVA variants preserves backward compatibility. Good approach.


✅ Good patterns

  • Burn/Reissue pendingConfirm ref pattern (Burn.tsx:33,77-83, Reissue.tsx:34,76-82): Clean way to coordinate animation completion with async operations. No double-fire risk since the ref is reset before calling processBurn().
  • SheetModal → Drawer migration (SheetModal.tsx): Clean swap from react-spring-bottom-sheet to vaul. The handleOpenChange only fires haptic + close on !open — correct.
  • Switch lg variant (switch.tsx): Well-structured with proper thumb positioning and 44px tap target.

Test coverage

Only Checkbox has a new test. Modal animation, Select keyboard nav with RadioGroup, and Toggle Switch migration have no tests. Acceptable for UI-only changes, but the Announcement close bug above would be caught by an integration test.


TL;DR: Fix the Announcement close race condition (real bug), clean up the Checkbox dual-handler (fragile), and verify the overlay click-to-dismiss is intentional. Everything else looks solid.

sahilc0 and others added 12 commits May 4, 2026 11:28
Restore direct announcement close behavior for the Try action so parent state clears even if navigation unmounts before exit animation completion.

Update the migrated checkbox wrapper to use a label-bound control path and guard same-state events, avoiding duplicate onChange calls across pointer and assistive activation paths.
…omponent-migration-20260430

# Conflicts:
#	bun.lock
#	src/screens/Apps/Assets/Burn.tsx
#	src/screens/Apps/Assets/Reissue.tsx

@ghost ghost 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.

Follow-up Review — new commits since last review

Reviewed 66da6dc (review fixes), 51dfff8 (merge from base), e8ffb38 (lockfile restore).

✅ Previous findings addressed

1. Announcement close race (BUG) — Fixed correctly. handleTryIt now calls close() directly (line 113), matching the old behavior. Parent state clears immediately regardless of whether the exit animation completes before unmount. Good.

2. Checkbox dual-handler (FRAGILE) — Fixed well. Changed from the awkward div onClick + pointer-events-none combo to a proper <label htmlFor> + <ShadcnCheckbox id> pattern. pointer-events-none removed, so the ShadcnCheckbox is the single source of truth for interaction. The if (nextChecked === checked) return guard at Checkbox.tsx:16 prevents the edge case of duplicate onChange calls. This is the idiomatic Radix/shadcn approach. 👍

⚠️ Note: Cloudflare builds failing

All three Cloudflare Pages deploys are failing. The e8ffb38 commit looks like an attempt to fix this (restores bun.lock), but the checks haven't re-run on that commit yet. Base branch PR #590 builds pass, so the issue is in this PR's build. Confirm builds pass before merging.

No new issues found. Previous concerns resolved. Approving.

Base automatically changed from wt-shadcn-ui-20260430 to master May 12, 2026 09:06

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/components/Select.tsx (1)

16-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope keyboard handling to the component instead of document.

Line 25 attaches a global listener, so ArrowUp/ArrowDown can mutate this select while users interact elsewhere. Also, Line 20 and Line 22 bypass handleChange, so keyboard navigation skips haptic feedback.

Suggested fix
-import { useEffect } from 'react'
@@
-  useEffect(() => {
-    const handleKeyDown = (event: { key: string; keyCode: number }) => {
-      const selectedIndex = options.indexOf(selected)
-      if (event.key === 'ArrowUp' || event.keyCode === 38) {
-        if (selectedIndex > 0) onChange(options[selectedIndex - 1])
-      } else if (event.key === 'ArrowDown' || event.keyCode === 40) {
-        if (selectedIndex < options.length - 1) onChange(options[selectedIndex + 1])
-      }
-    }
-    document.addEventListener('keydown', handleKeyDown)
-
-    return () => {
-      document.removeEventListener('keydown', handleKeyDown)
-    }
-  }, [selected, options, onChange])
-
   const handleChange = (value: string) => {
     hapticSubtle()
     onChange(value)
   }
+
+  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
+    const selectedIndex = options.indexOf(selected)
+    if (event.key === 'ArrowUp' && selectedIndex > 0) {
+      event.preventDefault()
+      handleChange(options[selectedIndex - 1])
+    } else if (event.key === 'ArrowDown' && selectedIndex < options.length - 1) {
+      event.preventDefault()
+      handleChange(options[selectedIndex + 1])
+    }
+  }
@@
-    <RadioGroup value={selected} onValueChange={handleChange} className='flex flex-col !gap-0 !p-0 !m-0'>
+    <RadioGroup
+      value={selected}
+      onValueChange={handleChange}
+      onKeyDown={handleKeyDown}
+      className='flex flex-col !gap-0 !p-0 !m-0'
+    >

Also applies to: 31-34, 37-37

🤖 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/Select.tsx` around lines 16 - 29, The effect currently
registers handleKeyDown on document, letting ArrowUp/ArrowDown affect this
Select from anywhere and bypassing the component's handleChange (which triggers
haptic feedback); change to attach the keydown listener to the component's root
element via a ref (e.g., rootRef) and scope focus so the handler only runs when
the component is focused, and inside the handler call the component's existing
handleChange (or the wrapper that triggers haptics) instead of directly calling
onChange; ensure you create and use a ref for the root element, add/remove the
listener on that element in useEffect, and apply the same scoping/fix to the
other similar keyboard handlers mentioned (the effects at the other locations).
🤖 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/Modal.tsx`:
- Around line 29-74: The modal currently renders raw motion.div nodes
(AnimatePresence/motion.div) and lacks dialog semantics and focus management;
replace the overlay/content motion.divs with your UI dialog primitives (e.g.,
Dialog/DialogOverlay/DialogContent or the project’s ui/dialog components) and
apply Framer Motion to those primitives (use asChild or wrap motion on the
primitive nodes) so animations remain but accessibility is restored. Ensure the
Dialog root is controlled by the same open/onOpenChange props, add role="dialog"
and aria-modal="true" on the dialog content node, enable focus trapping and
keyboard close (Escape) via the dialog primitive, and keep onExitComplete wired
to AnimatePresence as before so existing animation lifecycle is preserved.

In `@src/lib/assets.ts`:
- Line 1: The Mint UI validation still hardcodes an 8-decimal limit; update the
check in the Mint component (e.g., in src/screens/Apps/Assets/Mint.tsx) to use
the shared constant MAX_DECIMALS instead of 8 (replace the condition that
currently reads "... > 8" with "... > MAX_DECIMALS" or at minimum use 18), and
also update the decimals input element's max attribute from '8' to
String(MAX_DECIMALS) (or '18') so the UI matches the expanded MAX_DECIMALS = 18
defined in assets.ts.

In `@vitest.config.ts`:
- Line 3: The config files (vitest.config.ts, vite.config.ts,
vite.worker.config.ts) are using the CommonJS __dirname which is undefined in
ESM; replace any use of __dirname by deriving the directory from import.meta.url
(e.g., import fileURLToPath from 'url' and compute const __dirname =
path.dirname(fileURLToPath(import.meta.url'))) or by switching to
path.resolve(process.cwd(), ...) for workspace-relative paths; update references
in vitest.config.ts (where path is imported) and the equivalent occurrences in
vite.config.ts and vite.worker.config.ts (look for uses of __dirname) so the
configs load under ESM without runtime errors.

---

Outside diff comments:
In `@src/components/Select.tsx`:
- Around line 16-29: The effect currently registers handleKeyDown on document,
letting ArrowUp/ArrowDown affect this Select from anywhere and bypassing the
component's handleChange (which triggers haptic feedback); change to attach the
keydown listener to the component's root element via a ref (e.g., rootRef) and
scope focus so the handler only runs when the component is focused, and inside
the handler call the component's existing handleChange (or the wrapper that
triggers haptics) instead of directly calling onChange; ensure you create and
use a ref for the root element, add/remove the listener on that element in
useEffect, and apply the same scoping/fix to the other similar keyboard handlers
mentioned (the effects at the other locations).
🪄 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: f05bd630-96eb-4872-9b9c-e38193384864

📥 Commits

Reviewing files that changed from the base of the PR and between 30bdae1 and b00bdfd.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • package.json
  • src/components/Announcement.tsx
  • src/components/Button.tsx
  • src/components/Checkbox.tsx
  • src/components/Input.tsx
  • src/components/Modal.tsx
  • src/components/Select.tsx
  • src/components/SheetModal.tsx
  • src/components/Toggle.tsx
  • src/components/ui/switch.tsx
  • src/index.css
  • src/lib/assets.ts
  • src/screens/Apps/Assets/Burn.tsx
  • src/screens/Apps/Assets/Reissue.tsx
  • src/screens/Settings/Backup.tsx
  • src/test/components/Checkbox.test.tsx
  • vitest.config.ts
💤 Files with no reviewable changes (1)
  • src/index.css

Comment thread src/components/Modal.tsx
Comment on lines +29 to +74
<AnimatePresence onExitComplete={onExitComplete}>
{open ? (
<motion.div
key='modal-overlay'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: EASE_OUT_QUINT_TUPLE }}
onClick={() => onOpenChange?.(false)}
style={{
position: 'fixed',
inset: 0,
zIndex: 50,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
}}
/>
) : null}
{open ? (
<motion.div
key='modal-content'
initial={{ opacity: 0, x: '-50%', y: '-50%', scale: 0.95 }}
animate={{ opacity: 1, x: '-50%', y: '-50%', scale: 1 }}
exit={{ opacity: 0, x: '-50%', y: '-50%', scale: 0.95 }}
transition={{ duration: 0.2, ease: EASE_OUT_QUINT_TUPLE }}
style={{
position: 'fixed',
top: '50%',
left: '50%',
zIndex: 51,
width: '100%',
maxWidth: 'min(22rem, 90%)',
}}
>
<div
style={{
padding: '1.5rem',
borderRadius: '0.75rem',
border: '1px solid var(--neutral-100)',
backgroundColor: 'var(--background-color)',
}}
>
{children}
</div>
</motion.div>
) : null}
</AnimatePresence>

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 | 🏗️ Heavy lift

Animated modal currently lacks dialog semantics and focus management.

The new structure is visually correct, but there’s no semantic role="dialog"/aria-modal, focus trap, or keyboard-close path in this component. That’s an accessibility regression risk for keyboard/screen-reader users.

Use your ui/dialog primitive as the accessibility foundation and layer Framer Motion animation onto its overlay/content nodes.

🤖 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/Modal.tsx` around lines 29 - 74, The modal currently renders
raw motion.div nodes (AnimatePresence/motion.div) and lacks dialog semantics and
focus management; replace the overlay/content motion.divs with your UI dialog
primitives (e.g., Dialog/DialogOverlay/DialogContent or the project’s ui/dialog
components) and apply Framer Motion to those primitives (use asChild or wrap
motion on the primitive nodes) so animations remain but accessibility is
restored. Ensure the Dialog root is controlled by the same open/onOpenChange
props, add role="dialog" and aria-modal="true" on the dialog content node,
enable focus trapping and keyboard close (Escape) via the dialog primitive, and
keep onExitComplete wired to AnimatePresence as before so existing animation
lifecycle is preserved.

Comment thread src/lib/assets.ts Resolved
Comment thread vitest.config.ts Resolved

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
src/index.css (1)

173-173: 💤 Low value

Verify copy button background color change is intentional.

The background was changed from var(--neutral-50) to var(--neutral-100), making it slightly darker. Please confirm this aligns with the design system and maintains adequate contrast with the icon color (var(--neutral-300) on line 175).

🤖 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/index.css` at line 173, The copy button background was changed to
var(--neutral-100) from var(--neutral-50) which darkens the button and may
reduce contrast with the icon color var(--neutral-300); verify this change
against the design system and either revert to var(--neutral-50) or update the
icon color/contrast to meet accessibility guidelines, by locating the CSS rule
that sets background: var(--neutral-100) and the related icon color
var(--neutral-300) and adjusting the variable or values accordingly to restore
intended visual and contrast compliance.
🤖 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.

Nitpick comments:
In `@src/index.css`:
- Line 173: The copy button background was changed to var(--neutral-100) from
var(--neutral-50) which darkens the button and may reduce contrast with the icon
color var(--neutral-300); verify this change against the design system and
either revert to var(--neutral-50) or update the icon color/contrast to meet
accessibility guidelines, by locating the CSS rule that sets background:
var(--neutral-100) and the related icon color var(--neutral-300) and adjusting
the variable or values accordingly to restore intended visual and contrast
compliance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: adc13d92-1a76-4555-bfba-c3e6af8f621d

📥 Commits

Reviewing files that changed from the base of the PR and between b00bdfd and 1bb4c9d.

📒 Files selected for processing (2)
  • src/index.css
  • src/screens/Wallet/Receive/QrCode.tsx

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
src/screens/Apps/Assets/Mint.tsx (1)

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

Replace hardcoded decimal limit with MAX_DECIMALS constant.

Lines 261 and 265 still hardcode the value 8 for the decimals input validation and constraints. Since the rest of the validation logic (lines 91-92, 187-188) now uses the MAX_DECIMALS constant, these should be updated for consistency:

  • Line 261: Change max='8' to max={String(MAX_DECIMALS)}
  • Line 265: Change v <= 8 to v <= MAX_DECIMALS

This ensures the UI input constraints match the validation logic and the shared constant.

🔧 Proposed fix
               <Input
                 label='Decimals'
                 type='number'
                 min='0'
-                max='8'
+                max={String(MAX_DECIMALS)}
                 step='1'
                 value={decimals}
                 onChange={(v: number) => {
-                  setDecimals(!isNaN(v) && v >= 0 && v <= 8 ? String(v) : '')
+                  setDecimals(!isNaN(v) && v >= 0 && v <= MAX_DECIMALS ? String(v) : '')
                 }}
                 placeholder='0'
                 testId='asset-decimals'
               />
🤖 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/screens/Apps/Assets/Mint.tsx` around lines 256 - 270, The decimals Input
is still using the hardcoded limit 8; update the attributes and validation to
use the shared MAX_DECIMALS constant: change the Input prop max from '8' to
String(MAX_DECIMALS) (i.e. max={String(MAX_DECIMALS)}) and update the onChange
check inside setDecimals from v <= 8 to v <= MAX_DECIMALS so the UI constraint
matches the validation logic around decimals and the existing MAX_DECIMALS usage
elsewhere in Mint.tsx.
🤖 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.

Outside diff comments:
In `@src/screens/Apps/Assets/Mint.tsx`:
- Around line 256-270: The decimals Input is still using the hardcoded limit 8;
update the attributes and validation to use the shared MAX_DECIMALS constant:
change the Input prop max from '8' to String(MAX_DECIMALS) (i.e.
max={String(MAX_DECIMALS)}) and update the onChange check inside setDecimals
from v <= 8 to v <= MAX_DECIMALS so the UI constraint matches the validation
logic around decimals and the existing MAX_DECIMALS usage elsewhere in Mint.tsx.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c657cb0b-f286-4ed9-acb9-7aed32409720

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb4c9d and 3a90e51.

📒 Files selected for processing (2)
  • src/lib/assets.ts
  • src/screens/Apps/Assets/Mint.tsx

@ghost ghost 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.

Arkana Code Review — PR #593

Scope: UI component migration to shadcn/ui primitives. Modal, Checkbox, Select, Toggle, SheetModal, Button (CVA). Not protocol-critical — no changes to VTXO handling, transaction signing, forfeit paths, round lifecycle, or exit paths.

Verdict: Approve. Clean migration with good backward compatibility. A few minor items below.


Findings

1. Modal: no focus trap or Escape key handler — src/components/Modal.tsx
The new Modal lacks onKeyDown for Escape and has no focus trap. The old Modal didn't have these either so this isn't a regression, but since you're rebuilding the component anyway, consider adding:

useEffect(() => {
  if (!open) return
  const handleEscape = (e: KeyboardEvent) => {
    if (e.key === 'Escape') onOpenChange?.(false)
  }
  document.addEventListener('keydown', handleEscape)
  return () => document.removeEventListener('keydown', handleEscape)
}, [open, onOpenChange])

Not blocking — file a follow-up if desired.

2. Select: fragile event interception — src/components/Select.tsx:40-42
The onClick with event.preventDefault() on the <label> + explicit handleChange(option) bypasses RadioGroup's native value-change path. This works today but couples you to RadioGroup's internal DOM structure. If a shadcn/radix update changes how RadioGroupItem propagates clicks, this will silently break. Consider using RadioGroup's onValueChange as the sole change handler and dropping the label onClick override.

3. Select: !important overrides — src/components/Select.tsx:38
className='flex flex-col !gap-0 !p-0 !m-0' — fighting RadioGroup's default styles with !important is a code smell. If RadioGroup's defaults change, this breaks. Consider wrapping with a plain div instead or overriding via a custom className on the RadioGroup's slot.

4. QrCode: behavior change in Copy button — src/screens/Wallet/Receive/QrCode.tsx:332-339
handleCopyButton now auto-copies the QR value to clipboard AND opens the copy sheet simultaneously. Previously, the Copy button only opened the sheet. This is a UX change — make sure it's intentional (the toast + sheet opening at the same time could be noisy on some devices).

5. Checkbox: onChange doesn't convey checked state — src/components/Checkbox.tsx:16
onChange() is called without passing the new checked value. Every consumer has to assume it's a toggle. This isn't new (the old code had the same signature), but worth noting for the shadcn migration — consider evolving to onChange(checked: boolean) in a follow-up.

6. Modal: children always evaluated — src/components/Modal.tsx
In Burn.tsx:95 and Reissue.tsx:93, the <Modal> wrapper is always in the JSX tree. When open={false}, Modal's AnimatePresence correctly renders nothing to DOM, but the children (FlexCol tree with Text, Button, etc.) are still created as React elements every render. For simple content like this, the cost is negligible. Just be aware if Modal ever wraps expensive children — in that case, consider lazy-rendering children only when open is true.

What looks good

  • Deferred confirmation pattern (Burn.tsx, Reissue.tsx): The pendingConfirm ref + onExitComplete is a clean way to sequence async operations after modal exit animation. No race conditions.
  • Backward compat in Button.tsx: Supporting both old boolean props (red, secondary, clear) and new variant prop with CVA is well done.
  • Announcement.tsx handleTryIt: Correctly calls close() directly (not via exit animation) because navigation may unmount the component before onExitComplete fires. Good defensive coding.
  • MAX_DECIMALS export (src/lib/assets.ts): Clean extraction, correctly stays at 8, used consistently in Mint.tsx.
  • useEffect dependency fix (Select.tsx:29): Adding options and onChange to the dep array fixes a real stale-closure bug.

No protocol-critical concerns. Ship it. 🚢

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 18, 2026 •

Copy link
Copy Markdown

Deploying wallet-staging with  Cloudflare Pages  Cloudflare Pages

Latest commit: 249708f
Status: ✅  Deploy successful!
Preview URL: https://08eeafab.wallet-staging-95g.pages.dev
Branch Preview URL: https://wt-component-migration-20260.wallet-staging-95g.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/screens/Apps/Assets/Mint.tsx (1)

256-256: 💤 Low value

Consider using MAX_DECIMALS for the input max attribute.

The max='8' attribute is hardcoded. For consistency with the validation logic, consider using max={String(MAX_DECIMALS)} instead.

♻️ Proposed refactor
               <Input
                 min='0'
-                max='8'
+                max={String(MAX_DECIMALS)}
                 step='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 `@src/screens/Apps/Assets/Mint.tsx` at line 256, The input currently hardcodes
max='8'; replace this with the shared constant by using the MAX_DECIMALS value
(e.g., set the input's max to String(MAX_DECIMALS) or {MAX_DECIMALS.toString()})
so the UI max attribute stays consistent with the validation logic; update the
JSX in Mint.tsx where the input element is rendered to reference MAX_DECIMALS
instead of the literal '8'.
🤖 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/screens/Apps/Assets/Mint.tsx`:
- Around line 190-191: The disabled message for the decimals validation is
hardcoded to "Decimals must be 0-8" but the validation uses MAX_DECIMALS; update
the message to reference MAX_DECIMALS (e.g., build the string using
MAX_DECIMALS) so it stays correct if MAX_DECIMALS changes—look for the decimals
validation around the conditional that reads ": decimals === undefined ||
isNaN(decimals) || decimals < 0 || decimals > MAX_DECIMALS" in the Mint
component and replace the literal message with one that includes MAX_DECIMALS.

---

Nitpick comments:
In `@src/screens/Apps/Assets/Mint.tsx`:
- Line 256: The input currently hardcodes max='8'; replace this with the shared
constant by using the MAX_DECIMALS value (e.g., set the input's max to
String(MAX_DECIMALS) or {MAX_DECIMALS.toString()}) so the UI max attribute stays
consistent with the validation logic; update the JSX in Mint.tsx where the input
element is rendered to reference MAX_DECIMALS instead of the literal '8'.
🪄 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: 94151189-42d9-4234-b2f7-143935d73f4d

📥 Commits

Reviewing files that changed from the base of the PR and between 3a90e51 and c08eb09.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json
  • src/components/Input.tsx
  • src/lib/assets.ts
  • src/screens/Apps/Assets/Burn.tsx
  • src/screens/Apps/Assets/Mint.tsx
  • src/screens/Apps/Assets/Reissue.tsx
  • src/screens/Wallet/Receive/QrCode.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/screens/Apps/Assets/Burn.tsx
  • src/components/Input.tsx
  • src/screens/Wallet/Receive/QrCode.tsx
  • src/screens/Apps/Assets/Reissue.tsx

Comment thread src/screens/Apps/Assets/Mint.tsx Resolved

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/screens/Apps/Assets/Mint.tsx (1)

24-24: ⚡ Quick win

Use MAX_DECIMALS for the input max prop as well.

You centralized validation/messages with MAX_DECIMALS, but Line 256 still hardcodes '8', which can drift from the constant later.

Suggested fix
-                  max='8'
+                  max={String(MAX_DECIMALS)}
🤖 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/screens/Apps/Assets/Mint.tsx` at line 24, The input's max prop is
hardcoded as '8' but should use the shared constant MAX_DECIMALS to avoid drift;
locate the input element in Mint (where max='8' is used) and replace the
hardcoded '8' with the MAX_DECIMALS value (e.g., use String(MAX_DECIMALS) or
MAX_DECIMALS.toString() if the prop expects a string) so the component uses the
imported MAX_DECIMALS constant consistently with the validation and messages.
🤖 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/screens/Apps/Assets/Mint.tsx`:
- Around line 97-98: The disabled-state check for the Mint button is looser than
the submit-time validation and currently allows non-integer decimals (e.g., 1.5)
whereas the submit handler rejects them; update the Mint button/disabled logic
to use the same validation as the submit path by adding the
Number.isInteger(decimals) check (alongside decimals === undefined, decimals < 0
and decimals > MAX_DECIMALS) so the disabled-state and submit-time checks are
identical (reference MAX_DECIMALS and the decimals validation used in the submit
handler).

---

Nitpick comments:
In `@src/screens/Apps/Assets/Mint.tsx`:
- Line 24: The input's max prop is hardcoded as '8' but should use the shared
constant MAX_DECIMALS to avoid drift; locate the input element in Mint (where
max='8' is used) and replace the hardcoded '8' with the MAX_DECIMALS value
(e.g., use String(MAX_DECIMALS) or MAX_DECIMALS.toString() if the prop expects a
string) so the component uses the imported MAX_DECIMALS constant consistently
with the validation and messages.
🪄 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: 98c74424-cf87-43eb-8d4f-4b7ead28621d

📥 Commits

Reviewing files that changed from the base of the PR and between c08eb09 and 249708f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • package.json
  • src/components/ui/command.tsx
  • src/components/ui/drawer.tsx
  • src/screens/Apps/Assets/Mint.tsx
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • tsconfig.json

Comment thread src/screens/Apps/Assets/Mint.tsx Resolved
@bordalix
bordalix merged commit 98f2ef0 into master May 18, 2026
7 checks passed
@bordalix
bordalix deleted the wt-component-migration-20260430 branch May 18, 2026 15:35
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