Repository navigation
feat(ui): migrate core components to shadcn primitives (3/3) - #593
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughConverts 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. ChangesUI Component Modernization and Controlled Modal Refactoring
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying wallet-bitcoin with
|
| 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 |
Deploying wallet-mutinynet with
|
| 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 |
Deploying tmp-boltz-upstream-mainnet-arkade-wallet with
|
| 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 |
ghost
left a comment
There was a problem hiding this comment.
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:
onCheckedChangeis effectively dead code — it can never fire via pointer events. This is confusing.- 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'sdisabledstate 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
pendingConfirmref 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 callingprocessBurn(). - SheetModal → Drawer migration (
SheetModal.tsx): Clean swap from react-spring-bottom-sheet to vaul. ThehandleOpenChangeonly fires haptic + close on!open— correct. - Switch
lgvariant (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.
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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winScope 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
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
package.jsonsrc/components/Announcement.tsxsrc/components/Button.tsxsrc/components/Checkbox.tsxsrc/components/Input.tsxsrc/components/Modal.tsxsrc/components/Select.tsxsrc/components/SheetModal.tsxsrc/components/Toggle.tsxsrc/components/ui/switch.tsxsrc/index.csssrc/lib/assets.tssrc/screens/Apps/Assets/Burn.tsxsrc/screens/Apps/Assets/Reissue.tsxsrc/screens/Settings/Backup.tsxsrc/test/components/Checkbox.test.tsxvitest.config.ts
💤 Files with no reviewable changes (1)
- src/index.css
| <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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/index.css (1)
173-173: 💤 Low valueVerify copy button background color change is intentional.
The background was changed from
var(--neutral-50)tovar(--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
📒 Files selected for processing (2)
src/index.csssrc/screens/Wallet/Receive/QrCode.tsx
There was a problem hiding this comment.
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 winReplace hardcoded decimal limit with
MAX_DECIMALSconstant.Lines 261 and 265 still hardcode the value
8for the decimals input validation and constraints. Since the rest of the validation logic (lines 91-92, 187-188) now uses theMAX_DECIMALSconstant, these should be updated for consistency:
- Line 261: Change
max='8'tomax={String(MAX_DECIMALS)}- Line 265: Change
v <= 8tov <= MAX_DECIMALSThis 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
📒 Files selected for processing (2)
src/lib/assets.tssrc/screens/Apps/Assets/Mint.tsx
ghost
left a comment
There was a problem hiding this comment.
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): ThependingConfirmref +onExitCompleteis 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 newvariantprop with CVA is well done. - Announcement.tsx
handleTryIt: Correctly callsclose()directly (not via exit animation) because navigation may unmount the component beforeonExitCompletefires. 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): AddingoptionsandonChangeto the dep array fixes a real stale-closure bug.
No protocol-critical concerns. Ship it. 🚢
Deploying wallet-staging with
|
| 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/screens/Apps/Assets/Mint.tsx (1)
256-256: 💤 Low valueConsider using MAX_DECIMALS for the input max attribute.
The
max='8'attribute is hardcoded. For consistency with the validation logic, consider usingmax={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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
package.jsonsrc/components/Input.tsxsrc/lib/assets.tssrc/screens/Apps/Assets/Burn.tsxsrc/screens/Apps/Assets/Mint.tsxsrc/screens/Apps/Assets/Reissue.tsxsrc/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
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/screens/Apps/Assets/Mint.tsx (1)
24-24: ⚡ Quick winUse
MAX_DECIMALSfor the inputmaxprop 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
package.jsonsrc/components/ui/command.tsxsrc/components/ui/drawer.tsxsrc/screens/Apps/Assets/Mint.tsxtsconfig.json
💤 Files with no reviewable changes (1)
- tsconfig.json
Stack
wt-shadcn-ui-20260430, notmaster.Summary
Migrate Modal, Checkbox, Select, and Toggle to use shadcn/ui primitives with improved animations and accessibility.
Modal
open,onOpenChange,onExitCompleteprops for controlled usageEASE_OUT_QUINT_TUPLE)Checkbox
Select
Toggle
lgsize variantSwitch (ui primitive)
lgsize variant (24×44px track, 20px thumb)Updated Usage Sites
Burn.tsx,Reissue.tsx: UseonExitCompletefor async operation coordinationBackup.tsx,Announcement.tsx: Use controlled Modal propsTest Plan
bun run test)Files Changed
src/components/Modal.tsxsrc/components/Checkbox.tsxsrc/components/Select.tsxsrc/components/Toggle.tsxsrc/components/ui/switch.tsxsrc/components/ui/dialog.tsxsrc/components/Announcement.tsxsrc/screens/Apps/Assets/Burn.tsxsrc/screens/Apps/Assets/Reissue.tsxsrc/screens/Settings/Backup.tsxsrc/test/components/Checkbox.test.tsxSummary by CodeRabbit
New Features
Improvements
Tests
Chores