From 2bdd37eaaacbb85990b6fa8e3f6340522045cb08 Mon Sep 17 00:00:00 2001 From: aowheel Date: Wed, 11 Mar 2026 19:06:08 +0900 Subject: [PATCH 1/2] feat(frontend): add BottomNavigation component with safe-area support Implement BottomNavigation (capsule-shaped fixed bar) and BottomNavigationItem (icon + label with active/disabled states) using semantic design tokens. Includes Ladle stories for active state and disabled item variations. Closes #58 Co-Authored-By: Claude Opus 4.6 --- .../components/bottom-navigation.stories.tsx | 121 ++++++++++++++++++ .../app/components/ui/bottom-navigation.tsx | 102 +++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 packages/frontend/app/components/bottom-navigation.stories.tsx create mode 100644 packages/frontend/app/components/ui/bottom-navigation.tsx diff --git a/packages/frontend/app/components/bottom-navigation.stories.tsx b/packages/frontend/app/components/bottom-navigation.stories.tsx new file mode 100644 index 0000000..78a8913 --- /dev/null +++ b/packages/frontend/app/components/bottom-navigation.stories.tsx @@ -0,0 +1,121 @@ +import { Gift, QrCode, Scan, Send } from "lucide-react"; +import type * as React from "react"; +import { + MemoryRouter, + Route, + Routes, + type To, + useLocation, +} from "react-router"; + +import "~/app.css"; +import { + BottomNavigation, + BottomNavigationItem, +} from "~/components/ui/bottom-navigation"; + +export default { + title: "Components/BottomNavigation", +}; + +type StoryFrameProps = { + children: React.ReactNode; + initialEntry?: string; +}; + +function StoryFrame({ children, initialEntry = "/send" }: StoryFrameProps) { + return ( + + + {children}} /> + + + ); +} + +function StoryContent({ children }: { children: React.ReactNode }) { + const location = useLocation(); + return ( +
+
+

+ 現在のパス: {location.pathname} +

+
+ {children} +
+ ); +} + +const ICON_SIZE = 20; + +type NavItem = { + icon: React.ReactNode; + label: string; + to: To; +}; + +const navItems: NavItem[] = [ + { icon: , label: "送る", to: "/send" }, + { icon: , label: "スキャン", to: "/scan" }, + { icon: , label: "マイコード", to: "/mycode" }, + { icon: , label: "出品", to: "/listing" }, +]; + +export const Default = () => { + return ( + + + {navItems.map((item) => ( + + ))} + + + ); +}; + +Default.storyName = "Default (1st Active)"; + +export const ThirdActive = () => { + return ( + + + {navItems.map((item) => ( + + ))} + + + ); +}; + +ThirdActive.storyName = "3rd Item Active"; + +export const WithDisabled = () => { + return ( + + + {navItems.map((item, i) => ( + + ))} + + + ); +}; + +WithDisabled.storyName = "With Disabled Item"; diff --git a/packages/frontend/app/components/ui/bottom-navigation.tsx b/packages/frontend/app/components/ui/bottom-navigation.tsx new file mode 100644 index 0000000..484b584 --- /dev/null +++ b/packages/frontend/app/components/ui/bottom-navigation.tsx @@ -0,0 +1,102 @@ +import * as React from "react"; +import { NavLink, type To } from "react-router"; + +import { cn } from "~/lib/utils"; + +export interface BottomNavigationProps + extends React.HTMLAttributes { + children: React.ReactNode; +} + +export const BottomNavigation = React.forwardRef< + HTMLElement, + BottomNavigationProps +>(({ className, children, ...props }, ref) => { + return ( + + ); +}); +BottomNavigation.displayName = "BottomNavigation"; + +export interface BottomNavigationItemProps { + "aria-label"?: string; + className?: string; + disabled?: boolean; + icon: React.ReactNode; + label: string; + to: To; +} + +export const BottomNavigationItem = React.forwardRef< + HTMLAnchorElement, + BottomNavigationItemProps +>( + ( + { + "aria-label": ariaLabel, + className, + disabled = false, + icon, + label, + to, + ...props + }, + ref, + ) => { + if (disabled) { + return ( + + {icon} + {label} + + ); + } + + return ( + + cn( + "flex w-[72px] flex-col items-center gap-2 rounded-full px-8 py-6 text-primary-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + isActive && "bg-alpha-black-25", + className, + ) + } + {...props} + > + {({ isActive }) => ( + <> + {icon} + + {label} + + + )} + + ); + }, +); +BottomNavigationItem.displayName = "BottomNavigationItem"; From 6bcf28fa11bded5776515669e3940016a585c445 Mon Sep 17 00:00:00 2001 From: aowheel Date: Wed, 11 Mar 2026 19:16:31 +0900 Subject: [PATCH 2/2] fix(frontend): address review feedback for BottomNavigation - Remove redundant aria-current on inner span (NavLink handles it) - Remove unused ...props spread (type doesn't extend NavLink props) Co-Authored-By: Claude Opus 4.6 --- .../app/components/ui/bottom-navigation.tsx | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/frontend/app/components/ui/bottom-navigation.tsx b/packages/frontend/app/components/ui/bottom-navigation.tsx index 484b584..bab7ddc 100644 --- a/packages/frontend/app/components/ui/bottom-navigation.tsx +++ b/packages/frontend/app/components/ui/bottom-navigation.tsx @@ -44,15 +44,7 @@ export const BottomNavigationItem = React.forwardRef< BottomNavigationItemProps >( ( - { - "aria-label": ariaLabel, - className, - disabled = false, - icon, - label, - to, - ...props - }, + { "aria-label": ariaLabel, className, disabled = false, icon, label, to }, ref, ) => { if (disabled) { @@ -82,19 +74,9 @@ export const BottomNavigationItem = React.forwardRef< className, ) } - {...props} > - {({ isActive }) => ( - <> - {icon} - - {label} - - - )} + {icon} + {label} ); },