feat: integrate HeroUI component library
Implemented HeroUI migration plan with the following changes: Stage 0: Environment Setup - Installed @heroui/react@2.8.7, @heroui/theme@2.4.26, framer-motion@12.29.2 - Configured Tailwind with HeroUI plugin - Added HeroUI content paths to Tailwind config Stage 1: Provider Architecture - Created Providers.tsx wrapper combining HeroUIProvider and AppProvider - Updated app/layout.tsx to use new Providers component - Preserved all AppContext functionality (theme, language, translations) Stage 2: Component Migrations - TabNavigation: Migrated to HeroUI Tabs with keyboard navigation support - TopBar: Migrated buttons to HeroUI Button components - LanguageSwitch: Migrated to HeroUI Dropdown for better UX - ThemeSwitch: Migrated to HeroUI Button (isIconOnly variant) - MintSwapPanel: Migrated to HeroUI Tabs and Button components Benefits: - Enhanced accessibility with ARIA attributes and keyboard navigation - Smooth animations and transitions via Framer Motion - Consistent component API across the application - Maintained all existing design tokens and color system - Preserved dark mode functionality Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { AppProvider } from "@/contexts/AppContext";
|
||||
import { Providers } from "@/components/Providers";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -28,7 +28,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.variable} ${jetbrainsMono.variable} ${inter.className}`}>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@heroui/react";
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
const { language, setLanguage } = useApp();
|
||||
|
||||
const toggleLanguage = () => {
|
||||
setLanguage(language === "zh" ? "en" : "zh");
|
||||
const languages = [
|
||||
{ key: "zh", label: "中文" },
|
||||
{ key: "en", label: "English" },
|
||||
];
|
||||
|
||||
const handleSelectionChange = (key: React.Key) => {
|
||||
setLanguage(key as "zh" | "en");
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="bg-bg-surface dark:bg-gray-800 rounded-lg border border-border-normal dark:border-gray-700 px-3 py-2 flex items-center justify-center h-10 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-text-primary dark:text-white">
|
||||
{language === "zh" ? "中" : "EN"}
|
||||
</span>
|
||||
</button>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
variant="bordered"
|
||||
className="bg-bg-surface dark:bg-gray-800 border-border-normal dark:border-gray-700 min-w-10 h-10 px-3 rounded-lg"
|
||||
>
|
||||
<span className="text-sm font-medium text-text-primary dark:text-white">
|
||||
{language === "zh" ? "中" : "EN"}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="Language selection"
|
||||
selectedKeys={new Set([language])}
|
||||
selectionMode="single"
|
||||
onSelectionChange={(keys) => {
|
||||
const key = Array.from(keys)[0];
|
||||
if (key) handleSelectionChange(key);
|
||||
}}
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<DropdownItem key={lang.key}>{lang.label}</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { Tabs, Tab, Button } from "@heroui/react";
|
||||
|
||||
export default function MintSwapPanel() {
|
||||
const { t } = useApp();
|
||||
@@ -10,56 +11,42 @@ export default function MintSwapPanel() {
|
||||
const [activeAction, setActiveAction] = useState<"deposit" | "withdraw">("deposit");
|
||||
|
||||
return (
|
||||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 flex flex-col">
|
||||
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 flex flex-col overflow-hidden">
|
||||
{/* Mint/Swap Tabs */}
|
||||
<div className="flex border-b border-border-gray dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setActiveMode("mint")}
|
||||
className={`flex-1 h-[53px] flex items-center justify-center text-body-small font-bold rounded-tl-3xl transition-colors ${
|
||||
activeMode === "mint"
|
||||
? "bg-bg-subtle dark:bg-gray-700 border-b-2 border-text-primary dark:border-blue-500 text-[#0f172b] dark:text-white"
|
||||
: "text-text-tertiary dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{t("mintSwap.mint")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveMode("swap")}
|
||||
className={`flex-1 h-[53px] flex items-center justify-center text-body-small font-bold transition-colors ${
|
||||
activeMode === "swap"
|
||||
? "bg-bg-subtle dark:bg-gray-700 border-b-2 border-text-primary dark:border-blue-500 text-[#0f172b] dark:text-white"
|
||||
: "text-text-tertiary dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{t("mintSwap.swap")}
|
||||
</button>
|
||||
</div>
|
||||
<Tabs
|
||||
selectedKey={activeMode}
|
||||
onSelectionChange={(key) => setActiveMode(key as "mint" | "swap")}
|
||||
variant="underlined"
|
||||
classNames={{
|
||||
base: "w-full",
|
||||
tabList: "w-full gap-0 p-0 rounded-none border-b border-border-gray dark:border-gray-700",
|
||||
cursor: "bg-text-primary dark:bg-blue-500 h-[2px]",
|
||||
tab: "h-[53px] flex-1 rounded-none data-[selected=true]:bg-bg-subtle dark:data-[selected=true]:bg-gray-700",
|
||||
tabContent: "text-body-small font-bold text-text-tertiary dark:text-gray-400 group-data-[selected=true]:text-[#0f172b] dark:group-data-[selected=true]:text-white",
|
||||
}}
|
||||
>
|
||||
<Tab key="mint" title={t("mintSwap.mint")} />
|
||||
<Tab key="swap" title={t("mintSwap.swap")} />
|
||||
</Tabs>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
{/* Deposit/Withdraw Toggle */}
|
||||
<div className="bg-[#f9fafb] dark:bg-gray-700 rounded-xl p-1 flex gap-0">
|
||||
<button
|
||||
onClick={() => setActiveAction("deposit")}
|
||||
className={`flex-1 h-8 px-4 rounded-lg text-body-small transition-all ${
|
||||
activeAction === "deposit"
|
||||
? "bg-bg-surface dark:bg-gray-600 font-bold text-text-primary dark:text-white shadow-sm"
|
||||
: "font-medium text-text-tertiary dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{t("mintSwap.deposit")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveAction("withdraw")}
|
||||
className={`flex-1 h-8 px-4 rounded-lg text-body-small transition-all ${
|
||||
activeAction === "withdraw"
|
||||
? "bg-bg-surface dark:bg-gray-600 font-bold text-text-primary dark:text-white shadow-sm"
|
||||
: "font-medium text-text-tertiary dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{t("mintSwap.withdraw")}
|
||||
</button>
|
||||
</div>
|
||||
<Tabs
|
||||
selectedKey={activeAction}
|
||||
onSelectionChange={(key) => setActiveAction(key as "deposit" | "withdraw")}
|
||||
variant="solid"
|
||||
classNames={{
|
||||
base: "w-full",
|
||||
tabList: "bg-[#f9fafb] dark:bg-gray-700 rounded-xl p-1 gap-0 w-full",
|
||||
cursor: "bg-bg-surface dark:bg-gray-600 shadow-sm",
|
||||
tab: "h-8 px-4",
|
||||
tabContent: "text-body-small font-medium text-text-tertiary dark:text-gray-400 group-data-[selected=true]:font-bold group-data-[selected=true]:text-text-primary dark:group-data-[selected=true]:text-white",
|
||||
}}
|
||||
>
|
||||
<Tab key="deposit" title={t("mintSwap.deposit")} />
|
||||
<Tab key="withdraw" title={t("mintSwap.withdraw")} />
|
||||
</Tabs>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -166,15 +153,13 @@ export default function MintSwapPanel() {
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
className="rounded-xl h-12 flex items-center justify-center gap-2 bg-[#9ca1af] dark:bg-gray-600"
|
||||
disabled
|
||||
<Button
|
||||
isDisabled
|
||||
className="rounded-xl h-12 bg-[#9ca1af] dark:bg-gray-600 text-lg font-bold text-white"
|
||||
endContent={<Image src="/icon8.svg" alt="" width={20} height={20} />}
|
||||
>
|
||||
<span className="text-lg font-bold text-white leading-7">
|
||||
{t("mintSwap.approveDeposit")}
|
||||
</span>
|
||||
<Image src="/icon8.svg" alt="" width={20} height={20} />
|
||||
</button>
|
||||
{t("mintSwap.approveDeposit")}
|
||||
</Button>
|
||||
|
||||
{/* Terms */}
|
||||
<div className="flex flex-col gap-0 text-center">
|
||||
|
||||
13
components/Providers.tsx
Normal file
13
components/Providers.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { HeroUIProvider } from "@heroui/react";
|
||||
import { AppProvider } from "@/contexts/AppContext";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<HeroUIProvider>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</HeroUIProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, Tab } from "@heroui/react";
|
||||
|
||||
interface Tab {
|
||||
interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TabNavigationProps {
|
||||
tabs: Tab[];
|
||||
tabs: TabItem[];
|
||||
defaultActiveId?: string;
|
||||
onTabChange?: (tabId: string) => void;
|
||||
}
|
||||
@@ -18,38 +18,26 @@ export default function TabNavigation({
|
||||
defaultActiveId,
|
||||
onTabChange,
|
||||
}: TabNavigationProps) {
|
||||
const [activeTab, setActiveTab] = useState(defaultActiveId || tabs[0]?.id);
|
||||
|
||||
const handleTabClick = (tabId: string) => {
|
||||
setActiveTab(tabId);
|
||||
onTabChange?.(tabId);
|
||||
const handleSelectionChange = (key: React.Key) => {
|
||||
onTabChange?.(key.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-8">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
className="flex flex-col gap-2 items-start"
|
||||
>
|
||||
<span
|
||||
className={`text-sm font-bold leading-[150%] ${
|
||||
isActive ? "text-text-primary dark:text-white" : "text-text-tertiary dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
<div
|
||||
className={`self-stretch border-t-2 -mt-[2px] ${
|
||||
isActive ? "border-text-primary dark:border-white" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Tabs
|
||||
selectedKey={defaultActiveId || tabs[0]?.id}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
variant="underlined"
|
||||
classNames={{
|
||||
base: "w-auto",
|
||||
tabList: "gap-8 w-auto p-0",
|
||||
cursor: "bg-text-primary dark:bg-white",
|
||||
tab: "px-0 h-auto",
|
||||
tabContent: "text-sm font-bold text-text-tertiary dark:text-gray-400 group-data-[selected=true]:text-text-primary dark:group-data-[selected=true]:text-white",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<Tab key={tab.id} title={tab.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
import { Button } from "@heroui/react";
|
||||
|
||||
export default function ThemeSwitch() {
|
||||
const { theme, setTheme } = useApp();
|
||||
@@ -10,37 +11,43 @@ export default function ThemeSwitch() {
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="bg-bg-surface dark:bg-gray-800 rounded-lg border border-border-normal dark:border-gray-700 px-3 py-2 flex items-center justify-center h-10 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="bordered"
|
||||
onPress={toggleTheme}
|
||||
className="bg-bg-surface dark:bg-gray-800 border-border-normal dark:border-gray-700 min-w-10 h-10 rounded-lg"
|
||||
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M10 15C12.7614 15 15 12.7614 15 10C15 7.23858 12.7614 5 10 5C7.23858 5 5 7.23858 5 10C5 12.7614 7.23858 15 10 15Z"
|
||||
stroke="#111827"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-text-primary dark:text-white"
|
||||
/>
|
||||
<path
|
||||
d="M10 1V3M10 17V19M19 10H17M3 10H1M16.07 16.07L14.64 14.64M5.36 5.36L3.93 3.93M16.07 3.93L14.64 5.36M5.36 14.64L3.93 16.07"
|
||||
stroke="#111827"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
className="text-text-primary dark:text-white"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M17 10.79C16.8427 12.4922 16.1039 14.0754 14.9116 15.2662C13.7192 16.4571 12.1503 17.1913 10.4501 17.3464C8.74989 17.5016 7.04992 17.0676 5.63182 16.1159C4.21372 15.1642 3.15973 13.7534 2.63564 12.1102C2.11155 10.467 2.14637 8.68739 2.73477 7.06725C3.32317 5.44711 4.43113 4.07931 5.88616 3.18637C7.3412 2.29343 9.05859 1.93047 10.7542 2.15507C12.4498 2.37967 13.9989 3.17747 15.16 4.41"
|
||||
stroke="#111827"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-text-primary dark:text-white"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Button } from "@heroui/react";
|
||||
import Breadcrumb from "./Breadcrumb";
|
||||
import LanguageSwitch from "./LanguageSwitch";
|
||||
import ThemeSwitch from "./ThemeSwitch";
|
||||
@@ -26,24 +29,30 @@ export default function TopBar() {
|
||||
<ThemeSwitch />
|
||||
|
||||
{/* Wallet Button */}
|
||||
<button className="bg-bg-surface rounded-lg border border-border-normal px-2 py-2 flex items-center justify-center h-10">
|
||||
<Image src="/icon-wallet.svg" alt="Wallet" width={20} height={20} />
|
||||
<Image
|
||||
src="/icon-notification.svg"
|
||||
alt="Notification"
|
||||
width={14}
|
||||
height={14}
|
||||
className="ml-1"
|
||||
/>
|
||||
</button>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="bordered"
|
||||
className="bg-bg-surface border-border-normal min-w-10 h-10 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Image src="/icon-wallet.svg" alt="Wallet" width={20} height={20} />
|
||||
<Image
|
||||
src="/icon-notification.svg"
|
||||
alt="Notification"
|
||||
width={14}
|
||||
height={14}
|
||||
className="ml-1"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{/* Address Button */}
|
||||
<button className="bg-text-primary rounded-lg px-4 py-2 flex items-center justify-center gap-2 h-10 hover:bg-gray-800 transition-colors">
|
||||
<Image src="/icon-copy.svg" alt="Copy" width={16} height={16} />
|
||||
<span className="text-white text-sm font-bold font-jetbrains">
|
||||
0x12...4F82
|
||||
</span>
|
||||
</button>
|
||||
<Button
|
||||
className="bg-text-primary text-white font-bold font-jetbrains text-sm h-10 px-4 rounded-lg"
|
||||
startContent={<Image src="/icon-copy.svg" alt="Copy" width={16} height={16} />}
|
||||
>
|
||||
0x12...4F82
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
3244
package-lock.json
generated
3244
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,9 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.8",
|
||||
"@heroui/theme": "^2.4.26",
|
||||
"framer-motion": "^12.29.2",
|
||||
"next": "^15.1.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import { heroui } from "@heroui/theme";
|
||||
|
||||
export default {
|
||||
darkMode: "class",
|
||||
@@ -6,6 +7,7 @@ export default {
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -40,5 +42,5 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [heroui()],
|
||||
} satisfies Config;
|
||||
|
||||
Reference in New Issue
Block a user