diff --git a/web/src/components/ui/checkbox/Checkbox.stories.tsx b/web/src/components/ui/checkbox/Checkbox.stories.tsx
index 12efb235b..987456018 100644
--- a/web/src/components/ui/checkbox/Checkbox.stories.tsx
+++ b/web/src/components/ui/checkbox/Checkbox.stories.tsx
@@ -86,7 +86,7 @@ export const DisabledChecked: Story = {
// Example with event handler
export const WithOnChange: Story = {
args: {
- onCheckedChange: (checked: boolean) => console.log('Checked:', checked)
+ onCheckedChange: (checked: boolean) => alert(`Checked: ${checked}`)
}
};
diff --git a/web/src/components/ui/dropdown/Dropdown.stories.tsx b/web/src/components/ui/dropdown/Dropdown.stories.tsx
index c4dbb507a..6d68a8d05 100644
--- a/web/src/components/ui/dropdown/Dropdown.stories.tsx
+++ b/web/src/components/ui/dropdown/Dropdown.stories.tsx
@@ -35,20 +35,20 @@ export const Basic: Story = {
{
id: '1',
label: 'Profile',
- onClick: () => console.log('Profile clicked'),
+ onClick: () => alert('Profile clicked'),
loading: false,
icon:
},
{
id: '2',
label: 'Settings',
- onClick: () => console.log('Settings clicked'),
+ onClick: () => alert('Settings clicked'),
shortcut: '⌘S'
},
{
id: '3',
label: 'Logout',
- onClick: () => console.log('Logout clicked'),
+ onClick: () => alert('Logout clicked'),
items: [
{
id: '3-1',
@@ -75,21 +75,21 @@ export const WithIconsAndShortcuts: Story = {
label: 'Profile',
icon: '👤',
shortcut: '⌘P',
- onClick: () => console.log('Profile clicked')
+ onClick: () => alert('Profile clicked')
},
{
id: '2',
label: 'Settings',
icon: '⚙️',
shortcut: '⌘S',
- onClick: () => console.log('Settings clicked')
+ onClick: () => alert('Settings clicked')
},
{
id: '3',
label: 'Logout',
icon: '🚪',
shortcut: '⌘L',
- onClick: () => console.log('Logout clicked')
+ onClick: () => alert('Logout clicked')
}
],
children:
@@ -108,12 +108,12 @@ export const WithNestedItems: Story = {
{
id: '1-1',
label: 'Option 1',
- onClick: () => console.log('Option 1 clicked')
+ onClick: () => alert('Option 1 clicked')
},
{
id: '1-2',
label: 'Option 2',
- onClick: () => console.log('Option 2 clicked')
+ onClick: () => alert('Option 2 clicked')
}
]
},
@@ -124,12 +124,12 @@ export const WithNestedItems: Story = {
{
id: '2-1',
label: 'Sub Option 1',
- onClick: () => console.log('Sub Option 1 clicked')
+ onClick: () => alert('Sub Option 1 clicked')
},
{
id: '2-2',
label: 'Sub Option 2',
- onClick: () => console.log('Sub Option 2 clicked')
+ onClick: () => alert('Sub Option 2 clicked')
}
]
}
@@ -145,18 +145,18 @@ export const WithDisabledItems: Story = {
{
id: '1',
label: 'Available Option',
- onClick: () => console.log('Available clicked')
+ onClick: () => alert('Available clicked')
},
{
id: '2',
label: 'Disabled Option',
disabled: true,
- onClick: () => console.log('Should not be called')
+ onClick: () => alert('Should not be called')
},
{
id: '3',
label: 'Another Available',
- onClick: () => console.log('Another clicked')
+ onClick: () => alert('Another clicked')
}
],
children:
@@ -173,12 +173,12 @@ export const CustomWidth: Story = {
{
id: '1',
label: 'This is a very long menu item that might need wrapping',
- onClick: () => console.log('Long item clicked')
+ onClick: () => alert('Long item clicked')
},
{
id: '2',
label: 'Short item',
- onClick: () => console.log('Short item clicked')
+ onClick: () => alert('Short item clicked')
}
],
children:
@@ -192,29 +192,29 @@ export const WithLoadingItems: Story = {
{
id: '1',
label: 'Normal Item',
- onClick: () => console.log('Normal clicked')
+ onClick: () => alert('Normal clicked')
},
{
id: '2',
label: 'Loading Item',
loading: true,
- onClick: () => console.log('Loading clicked')
+ onClick: () => alert('Loading clicked')
},
{
id: '3',
label: 'Another Normal',
- onClick: () => console.log('Another clicked')
+ onClick: () => alert('Another clicked')
},
{ type: 'divider' },
{
id: '4',
label: 'Option 4',
- onClick: () => console.log('Option 4 clicked')
+ onClick: () => alert('Option 4 clicked')
},
{
id: '5',
label: 'Option 5',
- onClick: () => console.log('Option 5 clicked')
+ onClick: () => alert('Option 5 clicked')
}
],
children:
@@ -230,17 +230,17 @@ export const WithSelectionSingle: Story = {
id: '1',
label: 'Option 1',
selected: false,
- onClick: () => console.log('Option 1 clicked')
+ onClick: () => alert('Option 1 clicked')
},
{
id: '2',
label: 'Option 2',
- onClick: () => console.log('Option 2 clicked')
+ onClick: () => alert('Option 2 clicked')
},
{
id: '3',
label: 'Option 3 - Selected',
- onClick: () => console.log('Option 3 clicked'),
+ onClick: () => alert('Option 3 clicked'),
selected: true
}
],
@@ -257,32 +257,32 @@ export const WithSelectionMultiple: Story = {
id: '1',
label: 'Option 1',
selected: selectedIds.has('1'),
- onClick: () => console.log('Option 1 clicked')
+ onClick: () => alert('Option 1 clicked')
},
{
id: '2',
label: 'Option 2',
selected: selectedIds.has('2'),
- onClick: () => console.log('Option 2 clicked')
+ onClick: () => alert('Option 2 clicked')
},
{
id: '3',
label: 'Option 3',
selected: selectedIds.has('3'),
- onClick: () => console.log('Option 3 clicked')
+ onClick: () => alert('Option 3 clicked')
},
{ type: 'divider' as const },
{
id: '4',
label: 'Option 4',
selected: selectedIds.has('4'),
- onClick: () => console.log('Option 4 clicked')
+ onClick: () => alert('Option 4 clicked')
},
{
id: '5',
label: 'Option 5',
selected: selectedIds.has('5'),
- onClick: () => console.log('Option 5 clicked')
+ onClick: () => alert('Option 5 clicked')
}
];
@@ -319,14 +319,14 @@ export const WithSecondaryLabel: Story = {
id: '1',
label: 'Profile Settings',
secondaryLabel: 'User preferences',
- onClick: () => console.log('Profile clicked'),
+ onClick: () => alert('Profile clicked'),
icon:
},
{
id: '2',
label: 'Storage',
secondaryLabel: '45GB used',
- onClick: () => console.log('Storage clicked'),
+ onClick: () => alert('Storage clicked'),
icon:
},
{ type: 'divider' },
@@ -334,7 +334,7 @@ export const WithSecondaryLabel: Story = {
id: '3',
label: 'Subscription',
secondaryLabel: 'Pro Plan',
- onClick: () => console.log('Subscription clicked'),
+ onClick: () => alert('Subscription clicked'),
icon:
}
],
@@ -354,7 +354,7 @@ export const WithSearchHeader: Story = {
label: 'Profile Settings',
searchLabel: 'profile settings user preferences account',
secondaryLabel: 'User preferences',
- onClick: () => console.log('Profile clicked'),
+ onClick: () => alert('Profile clicked'),
icon:
},
{
@@ -362,7 +362,7 @@ export const WithSearchHeader: Story = {
label: 'Storage Options',
searchLabel: 'storage disk space memory',
secondaryLabel: 'Manage storage space',
- onClick: () => console.log('Storage clicked'),
+ onClick: () => alert('Storage clicked'),
icon:
},
{
@@ -370,7 +370,7 @@ export const WithSearchHeader: Story = {
label: 'Favorites',
searchLabel: 'favorites starred items bookmarks',
secondaryLabel: 'View starred items',
- onClick: () => console.log('Favorites clicked'),
+ onClick: () => alert('Favorites clicked'),
icon:
},
{ type: 'divider' },
@@ -378,12 +378,12 @@ export const WithSearchHeader: Story = {
id: '4',
label: 'Logout',
- onClick: () => console.log('Logout clicked')
+ onClick: () => alert('Logout clicked')
},
{
id: '5',
label: 'Invite User',
- onClick: () => console.log('Invite User clicked')
+ onClick: () => alert('Invite User clicked')
}
],
children:
@@ -405,7 +405,7 @@ export const WithLongText: Story = {
label,
secondaryLabel,
searchLabel: label + ' ' + secondaryLabel,
- onClick: () => console.log('Long text clicked'),
+ onClick: () => alert('Long text clicked'),
truncate: true
};
})
@@ -413,3 +413,105 @@ export const WithLongText: Story = {
children:
}
};
+
+// Example with links
+export const WithLinks: Story = {
+ args: {
+ menuHeader: 'Navigation Links',
+ items: [
+ {
+ id: '1',
+ label: 'Documentation',
+ link: '/docs',
+ icon: ,
+ onClick: () => alert('Documentation clicked')
+ },
+ {
+ id: '2',
+ label: 'GitHub Repository',
+ link: 'https://github.com/example/repo',
+ icon: ,
+ secondaryLabel: 'External Link'
+ },
+ { type: 'divider' },
+ {
+ id: '3',
+ label: 'Settings Page',
+ link: '/settings',
+ icon:
+ },
+ {
+ id: '4',
+ label: 'Help Center',
+ link: '/help',
+ secondaryLabel: 'Get Support'
+ }
+ ],
+ children:
+ }
+};
+
+// Interactive example with links and multiple selection
+export const WithLinksAndMultipleSelection: Story = {
+ render: () => {
+ const [selectedIds, setSelectedIds] = React.useState>(new Set(['2']));
+
+ const items: DropdownItems = [
+ {
+ id: '1',
+ label: 'Documentation Home',
+ link: '/docs',
+ selected: selectedIds.has('1'),
+ icon: ,
+ secondaryLabel: 'Main documentation'
+ },
+ {
+ id: '2',
+ label: 'API Reference',
+ link: '/docs/api',
+ selected: selectedIds.has('2'),
+ icon: ,
+ secondaryLabel: 'API documentation'
+ },
+ { type: 'divider' as const },
+ {
+ id: '3',
+ label: 'Tutorials',
+ link: '/docs/tutorials',
+ selected: selectedIds.has('3'),
+ icon: ,
+ secondaryLabel: 'Learn step by step'
+ },
+ {
+ id: '4',
+ label: 'Examples',
+ link: '/docs/examples',
+ selected: selectedIds.has('4'),
+ secondaryLabel: 'Code examples'
+ }
+ ];
+
+ const handleSelect = (itemId: string) => {
+ setSelectedIds((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(itemId)) {
+ newSet.delete(itemId);
+ } else {
+ newSet.add(itemId);
+ }
+ return newSet;
+ });
+ };
+
+ return (
+ Documentation Sections}
+ />
+ );
+ }
+};
diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx
index 77084da2b..e04166625 100644
--- a/web/src/components/ui/dropdown/Dropdown.tsx
+++ b/web/src/components/ui/dropdown/Dropdown.tsx
@@ -1,10 +1,11 @@
-import { DropdownMenuLabel, DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
+import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import React, { useMemo } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup, //Do I need this?
DropdownMenuItem,
+ DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
@@ -13,7 +14,8 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuCheckboxItemSingle,
- DropdownMenuCheckboxItemMultiple
+ DropdownMenuCheckboxItemMultiple,
+ DropdownMenuLink
} from './DropdownBase';
import { CircleSpinnerLoader } from '../loaders/CircleSpinnerLoader';
import { useMemoizedFn } from 'ahooks';
@@ -35,6 +37,8 @@ export interface DropdownItem {
loading?: boolean;
selected?: boolean;
items?: DropdownItems;
+ link?: string;
+ linkIcon?: 'arrow-right' | 'arrow-external' | 'caret-right';
}
export interface DropdownDivider {
@@ -233,7 +237,9 @@ const DropdownItem: React.FC<
onSelect,
selectType,
secondaryLabel,
- truncate
+ truncate,
+ link,
+ linkIcon
}) => {
const onClickItem = useMemoizedFn((e: React.MouseEvent) => {
if (onClick) onClick();
@@ -252,6 +258,13 @@ const DropdownItem: React.FC<
{secondaryLabel && {secondaryLabel}}
{shortcut && {shortcut}}
+ {link && (
+
+ )}
>
);
diff --git a/web/src/components/ui/dropdown/DropdownBase.tsx b/web/src/components/ui/dropdown/DropdownBase.tsx
index 1fee44d9b..b7943ea90 100644
--- a/web/src/components/ui/dropdown/DropdownBase.tsx
+++ b/web/src/components/ui/dropdown/DropdownBase.tsx
@@ -2,10 +2,19 @@
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
-import { Check3 as Check, ChevronRight } from '../icons/NucleoIconOutlined';
+import {
+ ArrowRight,
+ ArrowUpRight,
+ CaretRight,
+ Check3 as Check,
+ ChevronRight
+} from '../icons/NucleoIconOutlined';
import { cn } from '@/lib/classMerge';
import { Checkbox } from '../checkbox/Checkbox';
+import { Button } from '../buttons/Button';
+import { useRouter } from 'next/router';
+import Link from 'next/link';
const DropdownMenu = DropdownMenuPrimitive.Root;
@@ -83,6 +92,7 @@ const DropdownMenuItem = React.forwardRef<
'focus:bg-item-hover focus:text-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
inset && 'pl-8',
truncate && 'overflow-hidden',
+ 'group',
className
)}
onClick={(e) => {
@@ -100,7 +110,8 @@ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const itemClass = cn(
'focus:bg-item-hover focus:text-foreground',
'relative flex cursor-pointer items-center rounded-sm py-1.5 text-sm transition-colors outline-none select-none',
- 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
+ 'gap-1.5'
);
const DropdownMenuCheckboxItemSingle = React.forwardRef<
@@ -180,7 +191,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => (
));
@@ -209,6 +220,33 @@ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes = ({ className, link, linkIcon = 'arrow-right', ...props }) => {
+ const icon = React.useMemo(() => {
+ if (linkIcon === 'arrow-right') return ;
+ if (linkIcon === 'arrow-external') return ;
+ if (linkIcon === 'caret-right') return ;
+ }, [linkIcon]);
+
+ const isExternal = link?.startsWith('http');
+
+ return (
+
+
+
+ );
+};
+DropdownMenuLink.displayName = 'DropdownMenuLink';
+
export {
DropdownMenu,
DropdownMenuTrigger,
@@ -223,5 +261,6 @@ export {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
- DropdownMenuCheckboxItemMultiple
+ DropdownMenuCheckboxItemMultiple,
+ DropdownMenuLink
};
diff --git a/web/src/components/ui/dropdown/SearchDropdown.stories.tsx b/web/src/components/ui/dropdown/SearchDropdown.stories.tsx
index 9e8741821..ed9800ba2 100644
--- a/web/src/components/ui/dropdown/SearchDropdown.stories.tsx
+++ b/web/src/components/ui/dropdown/SearchDropdown.stories.tsx
@@ -50,7 +50,7 @@ export const Default: Story = {
args: {
items: items,
open: true,
- onSelect: (item) => console.log('Selected:', item.value)
+ onSelect: (item) => alert(`Selected: ${item.value}`)
}
};
@@ -66,7 +66,7 @@ export const WithDisabledItems: Story = {
}
],
open: true,
- onSelect: (item) => console.log('Selected:', item)
+ onSelect: (item) => alert(`Selected: ${item.value}`)
}
};
@@ -76,6 +76,6 @@ export const CustomStyling: Story = {
items: items,
open: true,
className: 'bg-slate-100 rounded-lg shadow-lg',
- onSelect: (item) => console.log('Selected:', item)
+ onSelect: (item) => alert(`Selected: ${item.value}`)
}
};