From d364e5e39fbb84e616623d4ecaa4628c1f4279ba Mon Sep 17 00:00:00 2001 From: Maofeng Date: Fri, 31 Jul 2026 16:06:39 +0800 Subject: [PATCH] feat(icons): extract shared workspace icon package Create @workspace/icons as the single Hugeicons-backed Icon primitive and icon-data export surface, with size presets and inherited current color. Migrate blocks and the web app to the shared package, remove duplicated direct Hugeicons dependencies and the blocks-local Icon component, and update the lockfile. --- apps/web/package.json | 3 +-- apps/web/src/data/navigation.ts | 2 +- apps/web/src/data/notifications.ts | 2 +- bun.lock | 21 +++++++++++++---- packages/blocks/package.json | 3 +-- .../src/blocks/layout/app-breadcrumb.tsx | 2 +- .../blocks/src/blocks/layout/app-layout.tsx | 13 +++++------ .../src/blocks/layout/app-query-indicator.tsx | 2 +- .../blocks/layout/header-action-button.tsx | 2 +- .../blocks/src/blocks/layout/user-menu.tsx | 15 ++++++------ .../blocks/navigation/flyout-navigation.tsx | 4 +--- .../blocks/src/blocks/navigation/item-icon.ts | 2 +- .../blocks/navigation/mobile-navigation.tsx | 3 +-- .../blocks/navigation/navigation-button.tsx | 2 +- .../blocks/src/blocks/navigation/types.ts | 3 +-- .../src/blocks/navigation/use-navigation.tsx | 2 +- .../notifications/notification-item.tsx | 2 +- .../blocks/src/blocks/notifications/types.ts | 2 +- packages/icons/README.md | 22 ++++++++++++++++++ packages/icons/package.json | 22 ++++++++++++++++++ .../icon.tsx => icons/src/index.tsx} | 23 +++++++++++-------- packages/icons/tsconfig.json | 17 ++++++++++++++ 22 files changed, 120 insertions(+), 49 deletions(-) create mode 100644 packages/icons/README.md create mode 100644 packages/icons/package.json rename packages/{blocks/src/components/icon.tsx => icons/src/index.tsx} (55%) create mode 100644 packages/icons/tsconfig.json diff --git a/apps/web/package.json b/apps/web/package.json index 22a2bbf..24b4662 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,8 +19,6 @@ "@base-ui/react": "^1.6.0", "@base-ui/utils": "^0.3.1", "@floating-ui/utils": "^0.2.12", - "@hugeicons/core-free-icons": "^4.2.3", - "@hugeicons/react": "^1.1.9", "@iconify/react": "^6.0.2", "@lingui/core": "^6", "@tanstack/react-form": "^1.33.2", @@ -29,6 +27,7 @@ "@tanstack/react-router": "^1.170.18", "@tanstack/react-start": "^1.168.32", "@workspace/blocks": "workspace:*", + "@workspace/icons": "workspace:*", "@workspace/i18n": "workspace:*", "@workspace/lexical": "workspace:*", "@workspace/ui": "workspace:*", diff --git a/apps/web/src/data/navigation.ts b/apps/web/src/data/navigation.ts index d07ce28..80d80dc 100644 --- a/apps/web/src/data/navigation.ts +++ b/apps/web/src/data/navigation.ts @@ -56,7 +56,7 @@ import { UserShield01Icon, Wallet01Icon, WalletCardsIcon, -} from "@hugeicons/core-free-icons" +} from "@workspace/icons" import type { NavigationGroup } from "@workspace/blocks/navigation" diff --git a/apps/web/src/data/notifications.ts b/apps/web/src/data/notifications.ts index 8a9b972..c50b839 100644 --- a/apps/web/src/data/notifications.ts +++ b/apps/web/src/data/notifications.ts @@ -3,7 +3,7 @@ import { Mail01Icon, Message01Icon, PackageIcon, -} from "@hugeicons/core-free-icons" +} from "@workspace/icons" import type { QueryFunctionContext } from "@tanstack/react-query" import type { Notification } from "@workspace/blocks/notifications" diff --git a/bun.lock b/bun.lock index 27baf12..570ac83 100644 --- a/bun.lock +++ b/bun.lock @@ -17,8 +17,6 @@ "@base-ui/react": "^1.6.0", "@base-ui/utils": "^0.3.1", "@floating-ui/utils": "^0.2.12", - "@hugeicons/core-free-icons": "^4.2.3", - "@hugeicons/react": "^1.1.9", "@iconify/react": "^6.0.2", "@lingui/core": "^6", "@tanstack/react-form": "^1.33.2", @@ -28,6 +26,7 @@ "@tanstack/react-start": "^1.168.32", "@workspace/blocks": "workspace:*", "@workspace/i18n": "workspace:*", + "@workspace/icons": "workspace:*", "@workspace/lexical": "workspace:*", "@workspace/ui": "workspace:*", "country-flag-icons": "^1.6.20", @@ -58,12 +57,11 @@ "@base-ui/react": "^1.6.0", "@base-ui/utils": "^0.3.1", "@floating-ui/utils": "^0.2.12", - "@hugeicons/core-free-icons": "^4.2.3", - "@hugeicons/react": "^1.1.9", "@tanstack/react-hotkeys": "^0.10.0", "@tanstack/react-query": "^5.101.4", "@tanstack/react-router": "^1.170.18", "@workspace/i18n": "workspace:*", + "@workspace/icons": "workspace:*", "@workspace/ui": "workspace:*", "cmdk": "^1.1.1", "lucide-react": "^1.27.0", @@ -109,6 +107,19 @@ "vitest": "^4", }, }, + "packages/icons": { + "name": "@workspace/icons", + "version": "0.0.0", + "dependencies": { + "@hugeicons/core-free-icons": "^4.2.3", + "@hugeicons/react": "^1.1.9", + "react": "^19.2.6", + }, + "devDependencies": { + "@types/react": "^19", + "typescript": "~6", + }, + }, "packages/lexical": { "name": "@workspace/lexical", "version": "0.0.0", @@ -793,6 +804,8 @@ "@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"], + "@workspace/icons": ["@workspace/icons@workspace:packages/icons"], + "@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"], "@workspace/ui": ["@workspace/ui@workspace:packages/ui"], diff --git a/packages/blocks/package.json b/packages/blocks/package.json index e92126a..9ed0226 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -11,11 +11,10 @@ "@base-ui/react": "^1.6.0", "@base-ui/utils": "^0.3.1", "@floating-ui/utils": "^0.2.12", - "@hugeicons/core-free-icons": "^4.2.3", - "@hugeicons/react": "^1.1.9", "@tanstack/react-hotkeys": "^0.10.0", "@tanstack/react-query": "^5.101.4", "@tanstack/react-router": "^1.170.18", + "@workspace/icons": "workspace:*", "@workspace/i18n": "workspace:*", "@workspace/ui": "workspace:*", "cmdk": "^1.1.1", diff --git a/packages/blocks/src/blocks/layout/app-breadcrumb.tsx b/packages/blocks/src/blocks/layout/app-breadcrumb.tsx index 502e43a..ef15d65 100644 --- a/packages/blocks/src/blocks/layout/app-breadcrumb.tsx +++ b/packages/blocks/src/blocks/layout/app-breadcrumb.tsx @@ -1,5 +1,6 @@ import { Link } from "@tanstack/react-router" import * as React from "react" +import { Icon } from "@workspace/icons" import { useMessage } from "@workspace/i18n" import { Breadcrumb, @@ -27,7 +28,6 @@ import { type NavigationInfo, useNavigationStack, } from "../navigation" -import { Icon } from "../../components/icon" import { layoutMessages } from "./messages" const MAX_VISIBLE_ENTRIES = 3 diff --git a/packages/blocks/src/blocks/layout/app-layout.tsx b/packages/blocks/src/blocks/layout/app-layout.tsx index aa57063..75ab56a 100644 --- a/packages/blocks/src/blocks/layout/app-layout.tsx +++ b/packages/blocks/src/blocks/layout/app-layout.tsx @@ -1,4 +1,10 @@ import * as React from "react" +import { + BellIcon, + Icon, + MessageMultiple01Icon, + Search01Icon, +} from "@workspace/icons" import { Button } from "@workspace/ui/components/button" import { Kbd } from "@workspace/ui/components/kbd" import { useIsTablet } from "@workspace/ui/hooks/use-breakpoint" @@ -6,7 +12,6 @@ import { cn } from "@workspace/ui/lib/utils" import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react" import { ChatPopover, ChatPopoverTrigger, type ChatThread } from "../chats" -import { Icon } from "../../components/icon" import { MobileNavigationSheet, MobileNavigationSheetTrigger, @@ -18,12 +23,6 @@ import { NotificationCenterTrigger, type UseNotificationsOptions, } from "../notifications" -import { - BellIcon, - MessageMultiple01Icon, - Search01Icon, -} from "@hugeicons/core-free-icons" - import { AppSidebar } from "./app-sidebar" import { headerActionIconClassName, diff --git a/packages/blocks/src/blocks/layout/app-query-indicator.tsx b/packages/blocks/src/blocks/layout/app-query-indicator.tsx index a6a7aba..14afa01 100644 --- a/packages/blocks/src/blocks/layout/app-query-indicator.tsx +++ b/packages/blocks/src/blocks/layout/app-query-indicator.tsx @@ -1,3 +1,4 @@ +import { ReloadIcon } from "@workspace/icons" import { useMessage } from "@workspace/i18n" import { useRefresh } from "../../hooks/use-refresh" import { @@ -6,7 +7,6 @@ import { TooltipTrigger, } from "@workspace/ui/components/tooltip" import { HeaderActionButton } from "./header-action-button" -import { ReloadIcon } from "@hugeicons/core-free-icons" import { cn } from "@workspace/ui/lib/utils" import { layoutMessages } from "./messages" diff --git a/packages/blocks/src/blocks/layout/header-action-button.tsx b/packages/blocks/src/blocks/layout/header-action-button.tsx index c34e18c..f8a1070 100644 --- a/packages/blocks/src/blocks/layout/header-action-button.tsx +++ b/packages/blocks/src/blocks/layout/header-action-button.tsx @@ -1,4 +1,4 @@ -import { Icon, type IconData } from "../../components/icon" +import { Icon, type IconData } from "@workspace/icons" import { Button, type ButtonProps } from "@workspace/ui/components/button" import { cn } from "@workspace/ui/lib/utils" diff --git a/packages/blocks/src/blocks/layout/user-menu.tsx b/packages/blocks/src/blocks/layout/user-menu.tsx index 4a09438..15cfed1 100644 --- a/packages/blocks/src/blocks/layout/user-menu.tsx +++ b/packages/blocks/src/blocks/layout/user-menu.tsx @@ -1,4 +1,12 @@ import { formatForDisplay } from "@tanstack/react-hotkeys" +import { + Icon, + type IconData, + KeyboardIcon, + LogoutCircle02Icon, + ShieldKeyIcon, + SwatchIcon, +} from "@workspace/icons" import { useMessage } from "@workspace/i18n" import { Button, type ButtonProps } from "@workspace/ui/components/button" import { @@ -35,13 +43,6 @@ import { HeaderActionButton, } from "./header-action-button" import { useOnScroll } from "../../hooks/use-on-scroll" -import { Icon, type IconData } from "../../components/icon" -import { - KeyboardIcon, - LogoutCircle02Icon, - ShieldKeyIcon, - SwatchIcon, -} from "@hugeicons/core-free-icons" import { layoutMessages } from "./messages" interface UserMenuItem { diff --git a/packages/blocks/src/blocks/navigation/flyout-navigation.tsx b/packages/blocks/src/blocks/navigation/flyout-navigation.tsx index 15be945..2c34a4a 100644 --- a/packages/blocks/src/blocks/navigation/flyout-navigation.tsx +++ b/packages/blocks/src/blocks/navigation/flyout-navigation.tsx @@ -1,12 +1,10 @@ +import { Icon } from "@workspace/icons" import { DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "@workspace/ui/components/dropdown-menu" - -import { Icon } from "../../components/icon" - import { resolveNavigationItemIcon } from "./item-icon" import { NavigationBadge, NavigationLink } from "./navigation-button" import { NavigationLabel } from "./navigation-label" diff --git a/packages/blocks/src/blocks/navigation/item-icon.ts b/packages/blocks/src/blocks/navigation/item-icon.ts index 7782382..4e15bda 100644 --- a/packages/blocks/src/blocks/navigation/item-icon.ts +++ b/packages/blocks/src/blocks/navigation/item-icon.ts @@ -1,4 +1,4 @@ -import type { IconData } from "../../components/icon" +import type { IconData } from "@workspace/icons" import type { NavigationItem } from "./types" diff --git a/packages/blocks/src/blocks/navigation/mobile-navigation.tsx b/packages/blocks/src/blocks/navigation/mobile-navigation.tsx index d4bec02..4aba304 100644 --- a/packages/blocks/src/blocks/navigation/mobile-navigation.tsx +++ b/packages/blocks/src/blocks/navigation/mobile-navigation.tsx @@ -1,4 +1,5 @@ import { useLocation } from "@tanstack/react-router" +import { Icon, Menu02Icon } from "@workspace/icons" import { useMessage } from "@workspace/i18n" import type { ButtonProps } from "@workspace/ui/components/button" import { Separator } from "@workspace/ui/components/separator" @@ -31,8 +32,6 @@ import { type GetNavigationRouteState, } from "./route-state" import type { NavigationGroup } from "./types" -import { Icon } from "../../components/icon" -import { Menu02Icon } from "@hugeicons/core-free-icons" const mobileNavigationSheetHandle = createSheetHandle() diff --git a/packages/blocks/src/blocks/navigation/navigation-button.tsx b/packages/blocks/src/blocks/navigation/navigation-button.tsx index f6630cd..b53bd00 100644 --- a/packages/blocks/src/blocks/navigation/navigation-button.tsx +++ b/packages/blocks/src/blocks/navigation/navigation-button.tsx @@ -2,12 +2,12 @@ import { mergeProps } from "@base-ui/react/merge-props" import { useRender } from "@base-ui/react/use-render" import { Link } from "@tanstack/react-router" import * as React from "react" +import { Icon, type IconData } from "@workspace/icons" import { Badge } from "@workspace/ui/components/badge" import { useRippleRef } from "@workspace/ui/hooks/use-ripple" import { cn } from "@workspace/ui/lib/utils" import { ChevronRightIcon } from "lucide-react" -import { Icon, type IconData } from "../../components/icon" import { NavigationLabel, TruncatedNavigationLabel } from "./navigation-label" import type { NavigationLabelValue } from "./types" diff --git a/packages/blocks/src/blocks/navigation/types.ts b/packages/blocks/src/blocks/navigation/types.ts index d1a01d3..f3bd9f1 100644 --- a/packages/blocks/src/blocks/navigation/types.ts +++ b/packages/blocks/src/blocks/navigation/types.ts @@ -1,7 +1,6 @@ +import type { IconData } from "@workspace/icons" import type { MessageDescriptor } from "@workspace/i18n" -import type { IconData } from "../../components/icon" - export type NavigationLabelValue = string | MessageDescriptor export interface NavigationItem { diff --git a/packages/blocks/src/blocks/navigation/use-navigation.tsx b/packages/blocks/src/blocks/navigation/use-navigation.tsx index ce5c772..63927b9 100644 --- a/packages/blocks/src/blocks/navigation/use-navigation.tsx +++ b/packages/blocks/src/blocks/navigation/use-navigation.tsx @@ -1,7 +1,7 @@ import { useLocation } from "@tanstack/react-router" import * as React from "react" +import type { IconData } from "@workspace/icons" -import type { IconData } from "../../components/icon" import { resolveNavigationItemIcon } from "./item-icon" import type { NavigationGroup, diff --git a/packages/blocks/src/blocks/notifications/notification-item.tsx b/packages/blocks/src/blocks/notifications/notification-item.tsx index fdf7181..03428b8 100644 --- a/packages/blocks/src/blocks/notifications/notification-item.tsx +++ b/packages/blocks/src/blocks/notifications/notification-item.tsx @@ -1,5 +1,6 @@ import * as React from "react" import { Link } from "@tanstack/react-router" +import { Icon } from "@workspace/icons" import { useFormatters, useMessage } from "@workspace/i18n" import { Avatar, @@ -17,7 +18,6 @@ import { } from "@workspace/ui/components/item" import { cn } from "@workspace/ui/lib/utils" -import { Icon } from "../../components/icon" import { notificationMessages } from "./messages" import type { diff --git a/packages/blocks/src/blocks/notifications/types.ts b/packages/blocks/src/blocks/notifications/types.ts index c628f63..e42d1e5 100644 --- a/packages/blocks/src/blocks/notifications/types.ts +++ b/packages/blocks/src/blocks/notifications/types.ts @@ -1,4 +1,4 @@ -import type { IconData } from "../../components/icon" +import type { IconData } from "@workspace/icons" export type NotificationTone = | "destructive" diff --git a/packages/icons/README.md b/packages/icons/README.md new file mode 100644 index 0000000..e738eb6 --- /dev/null +++ b/packages/icons/README.md @@ -0,0 +1,22 @@ +# @workspace/icons + +Workspace icon primitives backed by Hugeicons. + +## Usage + +```tsx +import { Icon, Search01Icon } from "@workspace/icons" + +export function SearchButton() { + return +} +``` + +`Icon` defaults to the `md` size and inherits the current text color. A +Tailwind size class supplied through `className` overrides the size preset: + +```tsx + +``` + +Available presets are `xs`, `sm`, `md`, `lg`, and `xl`. diff --git a/packages/icons/package.json b/packages/icons/package.json new file mode 100644 index 0000000..bfb8cb8 --- /dev/null +++ b/packages/icons/package.json @@ -0,0 +1,22 @@ +{ + "name": "@workspace/icons", + "version": "0.0.0", + "type": "module", + "private": true, + "sideEffects": false, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hugeicons/core-free-icons": "^4.2.3", + "@hugeicons/react": "^1.1.9", + "react": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19", + "typescript": "~6" + }, + "exports": { + ".": "./src/index.tsx" + } +} diff --git a/packages/blocks/src/components/icon.tsx b/packages/icons/src/index.tsx similarity index 55% rename from packages/blocks/src/components/icon.tsx rename to packages/icons/src/index.tsx index 5628df8..8008a70 100644 --- a/packages/blocks/src/components/icon.tsx +++ b/packages/icons/src/index.tsx @@ -1,29 +1,32 @@ import { type HugeiconsIconProps, HugeiconsIcon } from "@hugeicons/react" -import { cn } from "@workspace/ui/lib/utils" + +export * from "@hugeicons/core-free-icons" export type IconData = HugeiconsIconProps["icon"] +export type IconSize = keyof typeof sizeValues -export type IconProps = Omit & { +export type IconProps = Omit & { data: IconData - size?: "xs" | "sm" | "md" | "lg" | "xl" + size?: IconSize } -const sizeClasses = { - xs: "size-3", - sm: "size-4", - md: "size-5", - lg: "size-6", - xl: "size-7", +const sizeValues = { + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, } export function Icon({ data, className, size = "md", ...props }: IconProps) { return (