From 5eff0f661fcac834b5f0ad3ebc068a843c1de04d Mon Sep 17 00:00:00 2001 From: Munkherdene Date: Sat, 22 Aug 2026 22:28:41 +0800 Subject: [PATCH] feat(web): use real Seed icon catalog (@seed-design/react-icon) matching iOS iconography --- bun.lock | 3 + package.json | 9 +- src/ds/TabBar.tsx | 51 ++-------- src/ds/icons.tsx | 232 ++++++++++++---------------------------------- 4 files changed, 76 insertions(+), 219 deletions(-) diff --git a/bun.lock b/bun.lock index 37fa9d3..790de8c 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@seed-design/css": "^2.5.0", "@seed-design/react": "^2.3.0", + "@seed-design/react-icon": "^0.7.4", "@seed-design/tailwind4-theme": "^2.3.0", "@tanstack/react-query": "^5.59.0", "next": "^15.1.0", @@ -351,6 +352,8 @@ "@seed-design/react-floating": ["@seed-design/react-floating@1.0.1", "", { "dependencies": { "@floating-ui/react": "^0.27.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ijlrVyRndiPp4x71cjTq6nT9qOMc/xPM5zSLp5+DM86k5KQ3FB0UEmfoZE/mGrspFZilr/KXP3ctf9CblXqcnQ=="], + "@seed-design/react-icon": ["@seed-design/react-icon@0.7.4", "", { "peerDependencies": { "react": "17.x || 18.x || 19.x" } }, "sha512-jjfKw3mvIYsTlgdxb4pjWTpT+uW8RZns/gcfhOjWuegZEXuZyAy4kPpiNbeNdnbrNoLWTCt8Rtt6UCGr/Zo/EQ=="], + "@seed-design/react-image": ["@seed-design/react-image@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-layout-effect": "^1.1.1", "@seed-design/dom-utils": "^2.0.1", "@seed-design/react-primitive": "^2.0.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZJBr0rW/xIzCcgkSpdGMv9uNEgnaYIfsOrp4YWOtj/ZSK+FSE6o5JPiC5FO0kbpPMVske91AOK92z9a6tAeSWg=="], "@seed-design/react-menu": ["@seed-design/react-menu@2.0.2", "", { "dependencies": { "@floating-ui/react": "^0.27.0", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-focus-scope": "^1.1.8", "@seed-design/dom-utils": "^2.0.1", "@seed-design/react-dismissible-layer": "^1.0.2", "@seed-design/react-primitive": "^2.0.1", "@seed-design/react-use-controllable-state": "^2.0.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-d6hHwY03qCxA6qs664Tv6S9fBiz6zyEu/tDkNYZ4RMbp7JxtoJ8n4Q/SONlBwVWPeddiZdLP6uSe4YRzfyDMWg=="], diff --git a/package.json b/package.json index 6f40e7d..3d31fd4 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,14 @@ "e2e": "playwright test" }, "dependencies": { + "@seed-design/css": "^2.5.0", + "@seed-design/react": "^2.3.0", + "@seed-design/react-icon": "^0.7.4", + "@seed-design/tailwind4-theme": "^2.3.0", + "@tanstack/react-query": "^5.59.0", "next": "^15.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "@seed-design/react": "^2.3.0", - "@seed-design/css": "^2.5.0", - "@seed-design/tailwind4-theme": "^2.3.0", - "@tanstack/react-query": "^5.59.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/src/ds/TabBar.tsx b/src/ds/TabBar.tsx index a021a09..0b3a296 100644 --- a/src/ds/TabBar.tsx +++ b/src/ds/TabBar.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import Link from "next/link"; -import { Icon } from "@seed-design/react"; +import { Icon, type IconName } from "./icons"; import { t } from "@/i18n/common"; export type TabKey = "home" | "accounting" | "planner" | "assets" | "profile"; @@ -15,47 +15,12 @@ const HREF: Record = { profile: "/profile", }; -// `@seed-design/react`'s `Icon` takes a raw `svg` node rather than a named -// icon catalog (there's no bundled icon-name set in the installed -// @seed-design/react/css versions — verified under node_modules), so these -// are small hand-drawn stand-ins mirroring the iOS tab glyphs -// (houseFill / horizline3VerticalFill / checkmarkCalendarFill / cardFill / -// personFill from HomeTabBar.swift). Swap for the real Seed icon set once -// it's available in this app. -// Seed's `` uses a Radix `Slot` internally, which clones -// its size/color props onto a *single* element — so each entry here must be -// one root `` node, not a fragment of bare paths. -function tabSvg(children: React.ReactNode): React.ReactNode { - return ( - - {children} - - ); -} - -const TAB_ICON: Record = { - home: tabSvg(), - accounting: tabSvg( - <> - - - - , - ), - planner: tabSvg( - <> - - - - , - ), - assets: tabSvg(), - profile: tabSvg( - <> - - - , - ), +const TAB_ICON: Record = { + home: "home", + accounting: "list", + planner: "calendar-check", + assets: "card", + profile: "user", }; const TAB_ORDER: TabKey[] = ["home", "accounting", "planner", "assets", "profile"]; @@ -82,7 +47,7 @@ export function TabBar({ active }: TabBarProps) { className="flex flex-1 flex-col items-center gap-1 p-1 md:flex-none md:flex-row md:justify-start md:gap-3 md:rounded-xl md:px-3 md:py-2" style={{ color: isActive ? "var(--seed-color-fg-neutral)" : "var(--seed-color-fg-placeholder)" }} > - + {t.tabs[key]} ); diff --git a/src/ds/icons.tsx b/src/ds/icons.tsx index 85b08f9..90d94bc 100644 --- a/src/ds/icons.tsx +++ b/src/ds/icons.tsx @@ -1,8 +1,10 @@ -import type { SVGProps } from "react"; +import type { CSSProperties, ComponentType } from "react"; +import * as Seed from "@seed-design/react-icon"; -// Clean line-icon set (stroke, currentColor) — a dependency-free stand-in for -// the Seed multicolor catalog, which has no web build. 24x24, 1.75 stroke, -// round caps/joins. Color via `color` / currentColor; size via `size`. +// Real Seed icon catalog (same iconography as the iOS app's SeedIcon), mapped +// from our semantic keys to Seed's filled variants. Keeping this indirection +// means every `` call site renders authentic Seed icons and +// we can retune the mapping in one place. export type IconName = | "utensils" @@ -38,174 +40,60 @@ export type IconName = | "hand-coins" | "bank"; -const PATHS: Record = { - utensils: ( - <> - - - - ), - cart: ( - <> - - - - - ), - coffee: ( - <> - - - - - ), - car: ( - <> - - - - - - ), - card: ( - <> - - - - ), - shield: , - phone: ( - <> - - - - ), - sparkles: ( - <> - - - - ), - bag: ( - <> - - - - ), - monitor: ( - <> - - - - ), - gamepad: ( - <> - - - - ), - dumbbell: ( - <> - - - ), - scissors: ( - <> - - - - ), - repeat: ( - <> - - - - ), - wallet: ( - <> - - - - - ), - receipt: ( - <> - - - - ), - wrench: , - home: ( - <> - - - - ), - list: , - "calendar-check": ( - <> - - - - ), - layers: ( - <> - - - - ), - user: ( - <> - - - - ), - plus: , - "chevron-right": , - "chevron-left": , - eye: ( - <> - - - - ), - "eye-off": ( - <> - - - - ), - "arrow-up-right": , - "arrow-down-left": , - trending: , - "hand-coins": ( - <> - - - - ), - bank: ( - <> - - - ), +type SeedIcon = ComponentType<{ size?: number; color?: string; style?: CSSProperties; "aria-hidden"?: boolean }>; + +const S = Seed as unknown as Record; +const pick = (...names: string[]): SeedIcon => { + for (const n of names) if (S[n]) return S[n]; + return S.IconQuestionCheckFill ?? S.IconBillFill; }; -export function Icon({ name, size = 22, ...rest }: { name: IconName; size?: number } & Omit, "name">) { - return ( - - {PATHS[name]} - - ); +const MAP: Record = { + utensils: pick("IconRestaurantFill"), + cart: pick("IconCartFill"), + coffee: pick("IconCafeFill", "IconRestaurantFill"), + car: pick("IconCarFill"), + card: pick("IconPaymentFill", "IconListCardFill"), + shield: pick("IconLockFill"), + phone: pick("IconMobileFill"), + sparkles: pick("IconReviewStarFill"), + bag: pick("IconShoppingBagFill", "IconCartFill"), + monitor: pick("IconLaptopFill", "IconMonitorFill"), + gamepad: pick("IconPlayFill"), + dumbbell: pick("IconHeartFill"), + scissors: pick("IconReviewStarFill"), + repeat: pick("IconRetryFill"), + wallet: pick("IconMoneyWonFill", "IconPriceWonFill"), + receipt: pick("IconBillFill"), + wrench: pick("IconSettingFill", "IconToolboxFill"), + home: pick("IconHouseFill"), + list: pick("IconMenuFill", "IconListFill"), + "calendar-check": pick("IconCalendarFill"), + layers: pick("IconListCardFill"), + user: pick("IconProfileFill"), + plus: pick("IconAddFill"), + "chevron-right": pick("IconChevronRightFill"), + "chevron-left": pick("IconChevronLeftFill"), + eye: pick("IconViewFill", "IconChartFill"), + "eye-off": pick("IconViewOffFill", "IconChartFill"), + "arrow-up-right": pick("IconArrowUpwardFill", "IconArrowFill"), + "arrow-down-left": pick("IconArrowDownwardFill", "IconArrowFill"), + trending: pick("IconChartFill"), + "hand-coins": pick("IconMoneyWonFill", "IconPriceWonFill"), + bank: pick("IconPaymentFill", "IconListCardFill"), +}; + +export function Icon({ + name, + size = 22, + color = "currentColor", + style, +}: { + name: IconName; + size?: number; + color?: string; + style?: CSSProperties; +}) { + const C = MAP[name]; + return ; }