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..bab7ddc --- /dev/null +++ b/packages/frontend/app/components/ui/bottom-navigation.tsx @@ -0,0 +1,84 @@ +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 }, + 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, + ) + } + > + {icon} + {label} + + ); + }, +); +BottomNavigationItem.displayName = "BottomNavigationItem";