From 67f300c35d3a290790948046eb3835ab6a83073e Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Sat, 20 Jun 2026 02:27:38 +0800 Subject: [PATCH] feat(DropdownMenu): support custom icon nodes + external-link items Items could only take a Material Symbols icon NAME, so a brand logo (e.g. GitHub's mark, which Material Symbols doesn't ship) couldn't be used. Allow `icon` to be a ReactNode as well, rendered in place of ``. Add `external?: boolean` on an item to render a trailing open-in-new glyph as the affordance for items that open in a new tab (performing the navigation stays the caller's job in onSelect). Backward-compatible: string icons render exactly as before, and items without `external` are unchanged. Adds a CustomIconAndExternal story. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- .../dropdown-menu/DropdownMenu.stories.tsx | 32 ++++++++++++++ .../navigation/dropdown-menu/DropdownMenu.tsx | 42 +++++++++++++++---- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 9255dd2be..2145e6bb2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mirrorstack-ai/web-ui-kit", "packageManager": "pnpm@10.29.3", - "version": "0.5.11", + "version": "0.5.12", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { diff --git a/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx b/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx index 51429069d..7d9bc24c7 100644 --- a/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx +++ b/src/components/ui/navigation/dropdown-menu/DropdownMenu.stories.tsx @@ -32,6 +32,38 @@ export const Playground: Story = { }, }; +/** + * An item's `icon` can be a custom node (e.g. a brand-logo SVG that Material + * Symbols doesn't ship) instead of an icon name — size it to ~the item + * icon size and use `currentColor`. `external: true` adds a trailing + * open-in-new glyph for items that open in a new tab. + */ +export const CustomIconAndExternal: Story = { + args: { + trigger: ( + + ), + notchRadius: 8, + notchInverseRadius: 6, + items: [ + { + id: "repo", + label: "Repository", + external: true, + icon: ( + + + + ), + }, + { id: "docs", label: "Docs", icon: "menu_book", external: true }, + { type: "separator" as const }, + { id: "settings", label: "Settings", icon: "settings" }, + ], + onSelect: (item) => console.log("Selected:", item.id), + }, +}; + /** * The notch head tuned to a small trigger: `notchWidth`/`notchHeight` size the * tab to the icon button, and `notchRadius`/`notchInverseRadius` round the head diff --git a/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx b/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx index e6966add4..07444d072 100644 --- a/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx +++ b/src/components/ui/navigation/dropdown-menu/DropdownMenu.tsx @@ -35,7 +35,14 @@ export const meta: ComponentMeta = { export interface DropdownMenuItem { id: string; label: string; - icon?: string; + /** Leading icon: a Material Symbols name, or a custom node (e.g. a brand-logo + * SVG that Material Symbols can't provide). A custom node should size itself + * to ~`size` and use `currentColor` so it inherits the muted icon colour. */ + icon?: string | ReactNode; + /** Marks an item that opens externally (a new tab): renders a trailing + * open-in-new glyph as the affordance. Performing the navigation is still the + * caller's job in `onSelect`. */ + external?: boolean; /** `"danger"` is a deprecated alias for `"error"`. */ variant?: "default" | "error" | "danger"; disabled?: boolean; @@ -283,17 +290,34 @@ export function DropdownMenu({ if (!item.disabled) setActiveIndex(index); }} > - {item.icon && ( + {item.icon != null && + (typeof item.icon === "string" ? ( + + ) : ( + + {item.icon} + + ))} + {item.label} + {item.external && ( )} - {item.label} ); })}