feat(web): use real Seed icon catalog (@seed-design/react-icon) matching iOS iconography
This commit is contained in:
parent
0010b7b5fa
commit
5eff0f661f
4 changed files with 76 additions and 219 deletions
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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<TabKey, string> = {
|
|||
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 `<Icon svg={...} />` uses a Radix `Slot` internally, which clones
|
||||
// its size/color props onto a *single* element — so each entry here must be
|
||||
// one root `<svg>` node, not a fragment of bare paths.
|
||||
function tabSvg(children: React.ReactNode): React.ReactNode {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const TAB_ICON: Record<TabKey, React.ReactNode> = {
|
||||
home: tabSvg(<path d="M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" />),
|
||||
accounting: tabSvg(
|
||||
<>
|
||||
<rect x="3" y="5" width="18" height="3" rx="1" />
|
||||
<rect x="3" y="10.5" width="18" height="3" rx="1" />
|
||||
<rect x="3" y="16" width="18" height="3" rx="1" />
|
||||
</>,
|
||||
),
|
||||
planner: tabSvg(
|
||||
<>
|
||||
<rect x="3" y="4" width="18" height="17" rx="2" />
|
||||
<path d="M3 9h18" />
|
||||
<path d="m8 14 2.5 2.5L16 11" />
|
||||
</>,
|
||||
),
|
||||
assets: tabSvg(<rect x="2.5" y="5" width="19" height="14" rx="2" />),
|
||||
profile: tabSvg(
|
||||
<>
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 20c1.5-4 5-6 8-6s6.5 2 8 6" />
|
||||
</>,
|
||||
),
|
||||
const TAB_ICON: Record<TabKey, IconName> = {
|
||||
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)" }}
|
||||
>
|
||||
<Icon svg={TAB_ICON[key]} size="24px" />
|
||||
<Icon name={TAB_ICON[key]} size={24} />
|
||||
<span className="text-xs md:text-sm">{t.tabs[key]}</span>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
232
src/ds/icons.tsx
232
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 `<Icon name="…" />` 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<IconName, React.ReactNode> = {
|
||||
utensils: (
|
||||
<>
|
||||
<path d="M4 3v6a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V3M6 11v10" />
|
||||
<path d="M17 3c-1.5 0-3 1.5-3 4.5S15.5 12 17 12v9" />
|
||||
</>
|
||||
),
|
||||
cart: (
|
||||
<>
|
||||
<circle cx="9" cy="20" r="1.4" />
|
||||
<circle cx="18" cy="20" r="1.4" />
|
||||
<path d="M2 3h2.2l2 12.4a1.5 1.5 0 0 0 1.5 1.2h9.1a1.5 1.5 0 0 0 1.5-1.2L20.5 7H5.2" />
|
||||
</>
|
||||
),
|
||||
coffee: (
|
||||
<>
|
||||
<path d="M4 8h13v5a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5V8Z" />
|
||||
<path d="M17 9h2.5a2.5 2.5 0 0 1 0 5H17" />
|
||||
<path d="M8 2c-.6.8-.6 1.7 0 2.5M12 2c-.6.8-.6 1.7 0 2.5" />
|
||||
</>
|
||||
),
|
||||
car: (
|
||||
<>
|
||||
<path d="M5 12l1.5-4.5A2 2 0 0 1 8.4 6h7.2a2 2 0 0 1 1.9 1.5L19 12" />
|
||||
<path d="M3 12h18v4a1 1 0 0 1-1 1h-1.5M5.5 17H4a1 1 0 0 1-1-1v-4" />
|
||||
<path d="M6.5 17v1.5M17.5 17v1.5" />
|
||||
<circle cx="7" cy="14.5" r=".6" /><circle cx="17" cy="14.5" r=".6" />
|
||||
</>
|
||||
),
|
||||
card: (
|
||||
<>
|
||||
<rect x="2.5" y="5" width="19" height="14" rx="2.5" />
|
||||
<path d="M2.5 9.5h19" />
|
||||
</>
|
||||
),
|
||||
shield: <path d="M12 3l7 2.5v5.5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V5.5L12 3Z" />,
|
||||
phone: (
|
||||
<>
|
||||
<rect x="6.5" y="2.5" width="11" height="19" rx="2.5" />
|
||||
<path d="M11 18.5h2" />
|
||||
</>
|
||||
),
|
||||
sparkles: (
|
||||
<>
|
||||
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3Z" />
|
||||
<path d="M18 14l.8 2.2L21 17l-2.2.8L18 20l-.8-2.2L15 17l2.2-.8L18 14Z" />
|
||||
</>
|
||||
),
|
||||
bag: (
|
||||
<>
|
||||
<path d="M5.5 8h13l-.9 11a2 2 0 0 1-2 1.8H8.4a2 2 0 0 1-2-1.8L5.5 8Z" />
|
||||
<path d="M9 8V6.5a3 3 0 0 1 6 0V8" />
|
||||
</>
|
||||
),
|
||||
monitor: (
|
||||
<>
|
||||
<rect x="3" y="4" width="18" height="12" rx="2" />
|
||||
<path d="M9 20h6M12 16v4" />
|
||||
</>
|
||||
),
|
||||
gamepad: (
|
||||
<>
|
||||
<path d="M7 9h10a4 4 0 0 1 4 4v.5a3.5 3.5 0 0 1-6.3 2.1L14 15h-4l-.7.6A3.5 3.5 0 0 1 3 13.5V13a4 4 0 0 1 4-4Z" />
|
||||
<path d="M7.5 12v2M6.5 13h2M15.5 12.5h.01M17.5 14.5h.01" />
|
||||
</>
|
||||
),
|
||||
dumbbell: (
|
||||
<>
|
||||
<path d="M3 9v6M6 7v10M18 7v10M21 9v6M6 12h12" />
|
||||
</>
|
||||
),
|
||||
scissors: (
|
||||
<>
|
||||
<circle cx="6" cy="6" r="2.2" /><circle cx="6" cy="18" r="2.2" />
|
||||
<path d="M8 7.5 20 18M8 16.5 20 6" />
|
||||
</>
|
||||
),
|
||||
repeat: (
|
||||
<>
|
||||
<path d="M3 12V9a3 3 0 0 1 3-3h11M14 3l3 3-3 3" />
|
||||
<path d="M21 12v3a3 3 0 0 1-3 3H7M10 21l-3-3 3-3" />
|
||||
</>
|
||||
),
|
||||
wallet: (
|
||||
<>
|
||||
<path d="M3.5 7.5A2 2 0 0 1 5.5 5.5h11a1.5 1.5 0 0 1 0 3H4" />
|
||||
<rect x="3" y="7.5" width="18" height="11.5" rx="2.5" />
|
||||
<circle cx="16.5" cy="13.5" r="1.1" />
|
||||
</>
|
||||
),
|
||||
receipt: (
|
||||
<>
|
||||
<path d="M6 3h12v18l-2-1.3-2 1.3-2-1.3-2 1.3-2-1.3L6 21V3Z" />
|
||||
<path d="M9 8h6M9 12h6" />
|
||||
</>
|
||||
),
|
||||
wrench: <path d="M14.5 6.5a3.5 3.5 0 0 0 4.4 4.4l-8.9 8.9a2 2 0 0 1-2.8-2.8l8.9-8.9a3.5 3.5 0 0 0-1.6-1.6Z" />,
|
||||
home: (
|
||||
<>
|
||||
<path d="M4 11 12 4l8 7" />
|
||||
<path d="M6 10v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-9" />
|
||||
</>
|
||||
),
|
||||
list: <path d="M4 7h16M4 12h16M4 17h16" />,
|
||||
"calendar-check": (
|
||||
<>
|
||||
<rect x="3.5" y="5" width="17" height="16" rx="2.5" />
|
||||
<path d="M3.5 9.5h17M8 3v4M16 3v4M8.5 15l2.2 2.2L15.5 12" />
|
||||
</>
|
||||
),
|
||||
layers: (
|
||||
<>
|
||||
<path d="M12 3 3 8l9 5 9-5-9-5Z" />
|
||||
<path d="M3 13l9 5 9-5M3 18l9 5 9-5" opacity=".5" />
|
||||
</>
|
||||
),
|
||||
user: (
|
||||
<>
|
||||
<circle cx="12" cy="8" r="3.5" />
|
||||
<path d="M5 20a7 7 0 0 1 14 0" />
|
||||
</>
|
||||
),
|
||||
plus: <path d="M12 5v14M5 12h14" />,
|
||||
"chevron-right": <path d="m9 5 7 7-7 7" />,
|
||||
"chevron-left": <path d="m15 5-7 7 7 7" />,
|
||||
eye: (
|
||||
<>
|
||||
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
|
||||
<circle cx="12" cy="12" r="2.6" />
|
||||
</>
|
||||
),
|
||||
"eye-off": (
|
||||
<>
|
||||
<path d="M4 4l16 16" />
|
||||
<path d="M9.5 9.6A2.6 2.6 0 0 0 12 14.5a2.6 2.6 0 0 0 2.4-1.6M6.5 6.8C4 8.4 2.5 12 2.5 12s3.5 6.5 9.5 6.5c1.5 0 2.8-.4 4-1M16.5 15.2C19 13.6 21.5 12 21.5 12s-3-5.5-8-6.4" />
|
||||
</>
|
||||
),
|
||||
"arrow-up-right": <path d="M7 17 17 7M9 7h8v8" />,
|
||||
"arrow-down-left": <path d="M17 7 7 17M15 17H7V9" />,
|
||||
trending: <path d="m3 16 5-5 4 4 8-8M15 7h6v6" />,
|
||||
"hand-coins": (
|
||||
<>
|
||||
<circle cx="8" cy="6" r="3" />
|
||||
<path d="M13 9h4.5a2 2 0 0 1 0 4H14M3 20l3-3h6a2 2 0 0 0 2-2" />
|
||||
</>
|
||||
),
|
||||
bank: (
|
||||
<>
|
||||
<path d="M3 9.5 12 4l9 5.5M4.5 9.5V18M19.5 9.5V18M8 10v6M12 10v6M16 10v6M3 20h18" />
|
||||
</>
|
||||
),
|
||||
type SeedIcon = ComponentType<{ size?: number; color?: string; style?: CSSProperties; "aria-hidden"?: boolean }>;
|
||||
|
||||
const S = Seed as unknown as Record<string, SeedIcon>;
|
||||
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<SVGProps<SVGSVGElement>, "name">) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.75}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
{...rest}
|
||||
>
|
||||
{PATHS[name]}
|
||||
</svg>
|
||||
);
|
||||
const MAP: Record<IconName, SeedIcon> = {
|
||||
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 <C size={size} color={color} style={style} aria-hidden />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue