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.
This commit is contained in:
Maofeng
2026-07-31 16:06:39 +08:00
parent 38d2defeb1
commit d364e5e39f
22 changed files with 120 additions and 49 deletions
+1 -2
View File
@@ -19,8 +19,6 @@
"@base-ui/react": "^1.6.0", "@base-ui/react": "^1.6.0",
"@base-ui/utils": "^0.3.1", "@base-ui/utils": "^0.3.1",
"@floating-ui/utils": "^0.2.12", "@floating-ui/utils": "^0.2.12",
"@hugeicons/core-free-icons": "^4.2.3",
"@hugeicons/react": "^1.1.9",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@lingui/core": "^6", "@lingui/core": "^6",
"@tanstack/react-form": "^1.33.2", "@tanstack/react-form": "^1.33.2",
@@ -29,6 +27,7 @@
"@tanstack/react-router": "^1.170.18", "@tanstack/react-router": "^1.170.18",
"@tanstack/react-start": "^1.168.32", "@tanstack/react-start": "^1.168.32",
"@workspace/blocks": "workspace:*", "@workspace/blocks": "workspace:*",
"@workspace/icons": "workspace:*",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/lexical": "workspace:*", "@workspace/lexical": "workspace:*",
"@workspace/ui": "workspace:*", "@workspace/ui": "workspace:*",
+1 -1
View File
@@ -56,7 +56,7 @@ import {
UserShield01Icon, UserShield01Icon,
Wallet01Icon, Wallet01Icon,
WalletCardsIcon, WalletCardsIcon,
} from "@hugeicons/core-free-icons" } from "@workspace/icons"
import type { NavigationGroup } from "@workspace/blocks/navigation" import type { NavigationGroup } from "@workspace/blocks/navigation"
+1 -1
View File
@@ -3,7 +3,7 @@ import {
Mail01Icon, Mail01Icon,
Message01Icon, Message01Icon,
PackageIcon, PackageIcon,
} from "@hugeicons/core-free-icons" } from "@workspace/icons"
import type { QueryFunctionContext } from "@tanstack/react-query" import type { QueryFunctionContext } from "@tanstack/react-query"
import type { Notification } from "@workspace/blocks/notifications" import type { Notification } from "@workspace/blocks/notifications"
+17 -4
View File
@@ -17,8 +17,6 @@
"@base-ui/react": "^1.6.0", "@base-ui/react": "^1.6.0",
"@base-ui/utils": "^0.3.1", "@base-ui/utils": "^0.3.1",
"@floating-ui/utils": "^0.2.12", "@floating-ui/utils": "^0.2.12",
"@hugeicons/core-free-icons": "^4.2.3",
"@hugeicons/react": "^1.1.9",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@lingui/core": "^6", "@lingui/core": "^6",
"@tanstack/react-form": "^1.33.2", "@tanstack/react-form": "^1.33.2",
@@ -28,6 +26,7 @@
"@tanstack/react-start": "^1.168.32", "@tanstack/react-start": "^1.168.32",
"@workspace/blocks": "workspace:*", "@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/icons": "workspace:*",
"@workspace/lexical": "workspace:*", "@workspace/lexical": "workspace:*",
"@workspace/ui": "workspace:*", "@workspace/ui": "workspace:*",
"country-flag-icons": "^1.6.20", "country-flag-icons": "^1.6.20",
@@ -58,12 +57,11 @@
"@base-ui/react": "^1.6.0", "@base-ui/react": "^1.6.0",
"@base-ui/utils": "^0.3.1", "@base-ui/utils": "^0.3.1",
"@floating-ui/utils": "^0.2.12", "@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-hotkeys": "^0.10.0",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.101.4",
"@tanstack/react-router": "^1.170.18", "@tanstack/react-router": "^1.170.18",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/icons": "workspace:*",
"@workspace/ui": "workspace:*", "@workspace/ui": "workspace:*",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"lucide-react": "^1.27.0", "lucide-react": "^1.27.0",
@@ -109,6 +107,19 @@
"vitest": "^4", "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": { "packages/lexical": {
"name": "@workspace/lexical", "name": "@workspace/lexical",
"version": "0.0.0", "version": "0.0.0",
@@ -793,6 +804,8 @@
"@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"], "@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"],
"@workspace/icons": ["@workspace/icons@workspace:packages/icons"],
"@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"], "@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"],
"@workspace/ui": ["@workspace/ui@workspace:packages/ui"], "@workspace/ui": ["@workspace/ui@workspace:packages/ui"],
+1 -2
View File
@@ -11,11 +11,10 @@
"@base-ui/react": "^1.6.0", "@base-ui/react": "^1.6.0",
"@base-ui/utils": "^0.3.1", "@base-ui/utils": "^0.3.1",
"@floating-ui/utils": "^0.2.12", "@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-hotkeys": "^0.10.0",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.101.4",
"@tanstack/react-router": "^1.170.18", "@tanstack/react-router": "^1.170.18",
"@workspace/icons": "workspace:*",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/ui": "workspace:*", "@workspace/ui": "workspace:*",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -1,5 +1,6 @@
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import * as React from "react" import * as React from "react"
import { Icon } from "@workspace/icons"
import { useMessage } from "@workspace/i18n" import { useMessage } from "@workspace/i18n"
import { import {
Breadcrumb, Breadcrumb,
@@ -27,7 +28,6 @@ import {
type NavigationInfo, type NavigationInfo,
useNavigationStack, useNavigationStack,
} from "../navigation" } from "../navigation"
import { Icon } from "../../components/icon"
import { layoutMessages } from "./messages" import { layoutMessages } from "./messages"
const MAX_VISIBLE_ENTRIES = 3 const MAX_VISIBLE_ENTRIES = 3
@@ -1,4 +1,10 @@
import * as React from "react" import * as React from "react"
import {
BellIcon,
Icon,
MessageMultiple01Icon,
Search01Icon,
} from "@workspace/icons"
import { Button } from "@workspace/ui/components/button" import { Button } from "@workspace/ui/components/button"
import { Kbd } from "@workspace/ui/components/kbd" import { Kbd } from "@workspace/ui/components/kbd"
import { useIsTablet } from "@workspace/ui/hooks/use-breakpoint" 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 { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"
import { ChatPopover, ChatPopoverTrigger, type ChatThread } from "../chats" import { ChatPopover, ChatPopoverTrigger, type ChatThread } from "../chats"
import { Icon } from "../../components/icon"
import { import {
MobileNavigationSheet, MobileNavigationSheet,
MobileNavigationSheetTrigger, MobileNavigationSheetTrigger,
@@ -18,12 +23,6 @@ import {
NotificationCenterTrigger, NotificationCenterTrigger,
type UseNotificationsOptions, type UseNotificationsOptions,
} from "../notifications" } from "../notifications"
import {
BellIcon,
MessageMultiple01Icon,
Search01Icon,
} from "@hugeicons/core-free-icons"
import { AppSidebar } from "./app-sidebar" import { AppSidebar } from "./app-sidebar"
import { import {
headerActionIconClassName, headerActionIconClassName,
@@ -1,3 +1,4 @@
import { ReloadIcon } from "@workspace/icons"
import { useMessage } from "@workspace/i18n" import { useMessage } from "@workspace/i18n"
import { useRefresh } from "../../hooks/use-refresh" import { useRefresh } from "../../hooks/use-refresh"
import { import {
@@ -6,7 +7,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@workspace/ui/components/tooltip" } from "@workspace/ui/components/tooltip"
import { HeaderActionButton } from "./header-action-button" import { HeaderActionButton } from "./header-action-button"
import { ReloadIcon } from "@hugeicons/core-free-icons"
import { cn } from "@workspace/ui/lib/utils" import { cn } from "@workspace/ui/lib/utils"
import { layoutMessages } from "./messages" import { layoutMessages } from "./messages"
@@ -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 { Button, type ButtonProps } from "@workspace/ui/components/button"
import { cn } from "@workspace/ui/lib/utils" import { cn } from "@workspace/ui/lib/utils"
@@ -1,4 +1,12 @@
import { formatForDisplay } from "@tanstack/react-hotkeys" import { formatForDisplay } from "@tanstack/react-hotkeys"
import {
Icon,
type IconData,
KeyboardIcon,
LogoutCircle02Icon,
ShieldKeyIcon,
SwatchIcon,
} from "@workspace/icons"
import { useMessage } from "@workspace/i18n" import { useMessage } from "@workspace/i18n"
import { Button, type ButtonProps } from "@workspace/ui/components/button" import { Button, type ButtonProps } from "@workspace/ui/components/button"
import { import {
@@ -35,13 +43,6 @@ import {
HeaderActionButton, HeaderActionButton,
} from "./header-action-button" } from "./header-action-button"
import { useOnScroll } from "../../hooks/use-on-scroll" 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" import { layoutMessages } from "./messages"
interface UserMenuItem { interface UserMenuItem {
@@ -1,12 +1,10 @@
import { Icon } from "@workspace/icons"
import { import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
} from "@workspace/ui/components/dropdown-menu" } from "@workspace/ui/components/dropdown-menu"
import { Icon } from "../../components/icon"
import { resolveNavigationItemIcon } from "./item-icon" import { resolveNavigationItemIcon } from "./item-icon"
import { NavigationBadge, NavigationLink } from "./navigation-button" import { NavigationBadge, NavigationLink } from "./navigation-button"
import { NavigationLabel } from "./navigation-label" import { NavigationLabel } from "./navigation-label"
@@ -1,4 +1,4 @@
import type { IconData } from "../../components/icon" import type { IconData } from "@workspace/icons"
import type { NavigationItem } from "./types" import type { NavigationItem } from "./types"
@@ -1,4 +1,5 @@
import { useLocation } from "@tanstack/react-router" import { useLocation } from "@tanstack/react-router"
import { Icon, Menu02Icon } from "@workspace/icons"
import { useMessage } from "@workspace/i18n" import { useMessage } from "@workspace/i18n"
import type { ButtonProps } from "@workspace/ui/components/button" import type { ButtonProps } from "@workspace/ui/components/button"
import { Separator } from "@workspace/ui/components/separator" import { Separator } from "@workspace/ui/components/separator"
@@ -31,8 +32,6 @@ import {
type GetNavigationRouteState, type GetNavigationRouteState,
} from "./route-state" } from "./route-state"
import type { NavigationGroup } from "./types" import type { NavigationGroup } from "./types"
import { Icon } from "../../components/icon"
import { Menu02Icon } from "@hugeicons/core-free-icons"
const mobileNavigationSheetHandle = createSheetHandle<void>() const mobileNavigationSheetHandle = createSheetHandle<void>()
@@ -2,12 +2,12 @@ import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render" import { useRender } from "@base-ui/react/use-render"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import * as React from "react" import * as React from "react"
import { Icon, type IconData } from "@workspace/icons"
import { Badge } from "@workspace/ui/components/badge" import { Badge } from "@workspace/ui/components/badge"
import { useRippleRef } from "@workspace/ui/hooks/use-ripple" import { useRippleRef } from "@workspace/ui/hooks/use-ripple"
import { cn } from "@workspace/ui/lib/utils" import { cn } from "@workspace/ui/lib/utils"
import { ChevronRightIcon } from "lucide-react" import { ChevronRightIcon } from "lucide-react"
import { Icon, type IconData } from "../../components/icon"
import { NavigationLabel, TruncatedNavigationLabel } from "./navigation-label" import { NavigationLabel, TruncatedNavigationLabel } from "./navigation-label"
import type { NavigationLabelValue } from "./types" import type { NavigationLabelValue } from "./types"
@@ -1,7 +1,6 @@
import type { IconData } from "@workspace/icons"
import type { MessageDescriptor } from "@workspace/i18n" import type { MessageDescriptor } from "@workspace/i18n"
import type { IconData } from "../../components/icon"
export type NavigationLabelValue = string | MessageDescriptor export type NavigationLabelValue = string | MessageDescriptor
export interface NavigationItem { export interface NavigationItem {
@@ -1,7 +1,7 @@
import { useLocation } from "@tanstack/react-router" import { useLocation } from "@tanstack/react-router"
import * as React from "react" import * as React from "react"
import type { IconData } from "@workspace/icons"
import type { IconData } from "../../components/icon"
import { resolveNavigationItemIcon } from "./item-icon" import { resolveNavigationItemIcon } from "./item-icon"
import type { import type {
NavigationGroup, NavigationGroup,
@@ -1,5 +1,6 @@
import * as React from "react" import * as React from "react"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import { Icon } from "@workspace/icons"
import { useFormatters, useMessage } from "@workspace/i18n" import { useFormatters, useMessage } from "@workspace/i18n"
import { import {
Avatar, Avatar,
@@ -17,7 +18,6 @@ import {
} from "@workspace/ui/components/item" } from "@workspace/ui/components/item"
import { cn } from "@workspace/ui/lib/utils" import { cn } from "@workspace/ui/lib/utils"
import { Icon } from "../../components/icon"
import { notificationMessages } from "./messages" import { notificationMessages } from "./messages"
import type { import type {
@@ -1,4 +1,4 @@
import type { IconData } from "../../components/icon" import type { IconData } from "@workspace/icons"
export type NotificationTone = export type NotificationTone =
| "destructive" | "destructive"
+22
View File
@@ -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 data={Search01Icon} size="lg" />
}
```
`Icon` defaults to the `md` size and inherits the current text color. A
Tailwind size class supplied through `className` overrides the size preset:
```tsx
<Icon data={Search01Icon} className="size-8 text-primary" />
```
Available presets are `xs`, `sm`, `md`, `lg`, and `xl`.
+22
View File
@@ -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"
}
}
@@ -1,29 +1,32 @@
import { type HugeiconsIconProps, HugeiconsIcon } from "@hugeicons/react" 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 IconData = HugeiconsIconProps["icon"]
export type IconSize = keyof typeof sizeValues
export type IconProps = Omit<HugeiconsIconProps, "icon"> & { export type IconProps = Omit<HugeiconsIconProps, "icon" | "size"> & {
data: IconData data: IconData
size?: "xs" | "sm" | "md" | "lg" | "xl" size?: IconSize
} }
const sizeClasses = { const sizeValues = {
xs: "size-3", xs: 12,
sm: "size-4", sm: 16,
md: "size-5", md: 20,
lg: "size-6", lg: 24,
xl: "size-7", xl: 28,
} }
export function Icon({ data, className, size = "md", ...props }: IconProps) { export function Icon({ data, className, size = "md", ...props }: IconProps) {
return ( return (
<HugeiconsIcon <HugeiconsIcon
icon={data} icon={data}
size={sizeValues[size]}
color="currentColor" color="currentColor"
aria-hidden="true" aria-hidden="true"
strokeWidth={1.75} strokeWidth={1.75}
className={cn(sizeClasses[size], className)} className={className}
{...props} {...props}
/> />
) )
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"paths": {
"@workspace/icons": ["./src/index.tsx"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}