Add v2 Dropdown menu component
Implement the Dropdown Menu family from the Probo Radix UI Figma on top of Base UI's Menu primitive, exported as flat Dropdown* parts: Dropdown (root), DropdownTrigger, DropdownPopup, DropdownItem (with shortcut and accent/error color), DropdownCheckboxItem, DropdownRadioGroup/RadioItem, DropdownGroup/GroupLabel, DropdownSeparator, and DropdownSubmenu/SubmenuTrigger. The popup carries size (1-2), variant (solid/soft highlight), and highContrast to its items through a small context. Styling uses tailwind-variants/lite with data-[highlighted] selectors; item shortcuts inherit the contrast color on highlight. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
119
packages/ui/src/v2/Dropdown/Dropdown.stories.tsx
Normal file
119
packages/ui/src/v2/Dropdown/Dropdown.stories.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "../Button/Button";
|
||||
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { DropdownCheckboxItem } from "./DropdownCheckboxItem";
|
||||
import { DropdownGroup } from "./DropdownGroup";
|
||||
import { DropdownGroupLabel } from "./DropdownGroupLabel";
|
||||
import { DropdownItem } from "./DropdownItem";
|
||||
import { DropdownPopup } from "./DropdownPopup";
|
||||
import { DropdownRadioGroup } from "./DropdownRadioGroup";
|
||||
import { DropdownRadioItem } from "./DropdownRadioItem";
|
||||
import { DropdownSeparator } from "./DropdownSeparator";
|
||||
import { DropdownSubmenu } from "./DropdownSubmenu";
|
||||
import { DropdownSubmenuTrigger } from "./DropdownSubmenuTrigger";
|
||||
import { DropdownTrigger } from "./DropdownTrigger";
|
||||
|
||||
export default {
|
||||
title: "v2/Dropdown",
|
||||
component: Dropdown,
|
||||
};
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger render={<Button variant="soft" color="neutral">Open menu</Button>} />
|
||||
<DropdownPopup>
|
||||
<DropdownItem shortcut="⌘ E">Edit</DropdownItem>
|
||||
<DropdownItem shortcut="⌘ D">Duplicate</DropdownItem>
|
||||
<DropdownSeparator />
|
||||
<DropdownSubmenu>
|
||||
<DropdownSubmenuTrigger>More</DropdownSubmenuTrigger>
|
||||
<DropdownPopup side="right" sideOffset={4}>
|
||||
<DropdownItem>Move</DropdownItem>
|
||||
<DropdownItem>Archive</DropdownItem>
|
||||
</DropdownPopup>
|
||||
</DropdownSubmenu>
|
||||
<DropdownSeparator />
|
||||
<DropdownItem color="error" shortcut="⌘ ⌫">Delete</DropdownItem>
|
||||
</DropdownPopup>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function Soft() {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger render={<Button variant="soft" color="neutral">Soft highlight</Button>} />
|
||||
<DropdownPopup variant="soft">
|
||||
<DropdownItem shortcut="⌘ E">Edit</DropdownItem>
|
||||
<DropdownItem shortcut="⌘ D">Duplicate</DropdownItem>
|
||||
<DropdownSeparator />
|
||||
<DropdownItem color="error">Delete</DropdownItem>
|
||||
</DropdownPopup>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function Size1() {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger render={<Button size={1} variant="soft" color="neutral">Small</Button>} />
|
||||
<DropdownPopup size={1}>
|
||||
<DropdownItem shortcut="⌘ E">Edit</DropdownItem>
|
||||
<DropdownItem shortcut="⌘ D">Duplicate</DropdownItem>
|
||||
</DropdownPopup>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithGroupLabel() {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger render={<Button variant="soft" color="neutral">Account</Button>} />
|
||||
<DropdownPopup>
|
||||
<DropdownGroup>
|
||||
<DropdownGroupLabel>Account</DropdownGroupLabel>
|
||||
<DropdownItem>Profile</DropdownItem>
|
||||
<DropdownItem>Settings</DropdownItem>
|
||||
</DropdownGroup>
|
||||
</DropdownPopup>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckboxAndRadio() {
|
||||
const [showGrid, setShowGrid] = useState(true);
|
||||
const [view, setView] = useState("comfortable");
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownTrigger render={<Button variant="soft" color="neutral">View</Button>} />
|
||||
<DropdownPopup>
|
||||
<DropdownCheckboxItem checked={showGrid} onCheckedChange={setShowGrid}>
|
||||
Show grid
|
||||
</DropdownCheckboxItem>
|
||||
<DropdownSeparator />
|
||||
<DropdownRadioGroup value={view} onValueChange={setView}>
|
||||
<DropdownRadioItem value="comfortable">Comfortable</DropdownRadioItem>
|
||||
<DropdownRadioItem value="compact">Compact</DropdownRadioItem>
|
||||
</DropdownRadioGroup>
|
||||
</DropdownPopup>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
25
packages/ui/src/v2/Dropdown/Dropdown.tsx
Normal file
25
packages/ui/src/v2/Dropdown/Dropdown.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
// Root of the dropdown menu (Radix "Dropdown Menu"). Controlled the same way as
|
||||
// Base UI's Menu: `open` / `onOpenChange`, or left uncontrolled. See
|
||||
// contrib/claude/ui.md.
|
||||
export type DropdownProps = ComponentProps<typeof Menu.Root>;
|
||||
|
||||
export function Dropdown(props: DropdownProps) {
|
||||
return <Menu.Root {...props} />;
|
||||
}
|
||||
49
packages/ui/src/v2/Dropdown/DropdownCheckboxItem.tsx
Normal file
49
packages/ui/src/v2/Dropdown/DropdownCheckboxItem.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import { CheckIcon } from "@phosphor-icons/react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { useDropdownContext } from "./context";
|
||||
import { dropdownItem, dropdownItemIndicator, dropdownItemLabel, dropdownItemShortcut } from "./variants";
|
||||
|
||||
export type DropdownCheckboxItemProps
|
||||
= & Omit<ComponentProps<typeof Menu.CheckboxItem>, "className">
|
||||
& {
|
||||
className?: string;
|
||||
shortcut?: ReactNode;
|
||||
};
|
||||
|
||||
// A menu item with a checkmark indicator. Controlled with `checked` /
|
||||
// `onCheckedChange`, like Base UI's CheckboxItem.
|
||||
export function DropdownCheckboxItem(props: DropdownCheckboxItemProps) {
|
||||
const { className, shortcut, children, ...rest } = props;
|
||||
const { size, variant, highContrast } = useDropdownContext();
|
||||
|
||||
return (
|
||||
<Menu.CheckboxItem
|
||||
className={dropdownItem({ size, variant, color: "accent", highContrast, className })}
|
||||
{...rest}
|
||||
>
|
||||
<span className={dropdownItemIndicator()}>
|
||||
<Menu.CheckboxItemIndicator>
|
||||
<CheckIcon weight="bold" />
|
||||
</Menu.CheckboxItemIndicator>
|
||||
</span>
|
||||
<span className={dropdownItemLabel()}>{children}</span>
|
||||
{shortcut != null && <span className={dropdownItemShortcut()}>{shortcut}</span>}
|
||||
</Menu.CheckboxItem>
|
||||
);
|
||||
}
|
||||
23
packages/ui/src/v2/Dropdown/DropdownGroup.tsx
Normal file
23
packages/ui/src/v2/Dropdown/DropdownGroup.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
// Groups related items, optionally labelled with a DropdownGroupLabel.
|
||||
export type DropdownGroupProps = ComponentProps<typeof Menu.Group>;
|
||||
|
||||
export function DropdownGroup(props: DropdownGroupProps) {
|
||||
return <Menu.Group {...props} />;
|
||||
}
|
||||
29
packages/ui/src/v2/Dropdown/DropdownGroupLabel.tsx
Normal file
29
packages/ui/src/v2/Dropdown/DropdownGroupLabel.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { dropdownGroupLabel } from "./variants";
|
||||
|
||||
export type DropdownGroupLabelProps = Omit<ComponentProps<typeof Menu.GroupLabel>, "className"> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// A non-interactive heading for a DropdownGroup.
|
||||
export function DropdownGroupLabel(props: DropdownGroupLabelProps) {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return <Menu.GroupLabel className={dropdownGroupLabel({ className })} {...rest} />;
|
||||
}
|
||||
47
packages/ui/src/v2/Dropdown/DropdownItem.tsx
Normal file
47
packages/ui/src/v2/Dropdown/DropdownItem.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { useDropdownContext } from "./context";
|
||||
import { dropdownItem, dropdownItemLabel, dropdownItemShortcut } from "./variants";
|
||||
|
||||
export type DropdownItemProps
|
||||
= & Omit<ComponentProps<typeof Menu.Item>, "color" | "className">
|
||||
& {
|
||||
className?: string;
|
||||
color?: "accent" | "error";
|
||||
iconStart?: ReactNode;
|
||||
// Trailing keyboard shortcut hint (e.g. "⌘ E").
|
||||
shortcut?: ReactNode;
|
||||
};
|
||||
|
||||
// A single actionable menu item. Inherits size/variant/highContrast from the
|
||||
// popup; set `color="error"` for destructive actions.
|
||||
export function DropdownItem(props: DropdownItemProps) {
|
||||
const { className, color = "accent", iconStart, shortcut, children, ...rest } = props;
|
||||
const { size, variant, highContrast } = useDropdownContext();
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
className={dropdownItem({ size, variant, color, highContrast, className })}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
<span className={dropdownItemLabel()}>{children}</span>
|
||||
{shortcut != null && <span className={dropdownItemShortcut()}>{shortcut}</span>}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
55
packages/ui/src/v2/Dropdown/DropdownPopup.tsx
Normal file
55
packages/ui/src/v2/Dropdown/DropdownPopup.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { DropdownProvider } from "./context";
|
||||
import { dropdownPopup } from "./variants";
|
||||
|
||||
export type DropdownPopupProps
|
||||
= & Omit<ComponentProps<typeof Menu.Popup>, "className">
|
||||
& {
|
||||
className?: string;
|
||||
// Menu-level look applied to every item.
|
||||
size?: 1 | 2;
|
||||
variant?: "solid" | "soft";
|
||||
highContrast?: boolean;
|
||||
// Positioner placement passthrough.
|
||||
side?: ComponentProps<typeof Menu.Positioner>["side"];
|
||||
align?: ComponentProps<typeof Menu.Positioner>["align"];
|
||||
sideOffset?: ComponentProps<typeof Menu.Positioner>["sideOffset"];
|
||||
};
|
||||
|
||||
// Portal + positioner + styled popup. Provides the menu-level look to its items.
|
||||
export function DropdownPopup(props: DropdownPopupProps) {
|
||||
const {
|
||||
className, children,
|
||||
size = 2, variant = "solid", highContrast = false,
|
||||
side = "bottom", align = "start", sideOffset = 4,
|
||||
...popupProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner side={side} align={align} sideOffset={sideOffset}>
|
||||
<Menu.Popup className={dropdownPopup({ className })} {...popupProps}>
|
||||
<DropdownProvider value={{ size, variant, highContrast }}>
|
||||
{children}
|
||||
</DropdownProvider>
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
);
|
||||
}
|
||||
24
packages/ui/src/v2/Dropdown/DropdownRadioGroup.tsx
Normal file
24
packages/ui/src/v2/Dropdown/DropdownRadioGroup.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
// Groups radio items with a shared value (Base UI's RadioGroup), controlled
|
||||
// with `value` / `onValueChange`.
|
||||
export type DropdownRadioGroupProps = ComponentProps<typeof Menu.RadioGroup>;
|
||||
|
||||
export function DropdownRadioGroup(props: DropdownRadioGroupProps) {
|
||||
return <Menu.RadioGroup {...props} />;
|
||||
}
|
||||
45
packages/ui/src/v2/Dropdown/DropdownRadioItem.tsx
Normal file
45
packages/ui/src/v2/Dropdown/DropdownRadioItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { useDropdownContext } from "./context";
|
||||
import { dropdownItem, dropdownItemIndicator, dropdownItemLabel, dropdownItemShortcut } from "./variants";
|
||||
|
||||
export type DropdownRadioItemProps
|
||||
= & Omit<ComponentProps<typeof Menu.RadioItem>, "className">
|
||||
& {
|
||||
className?: string;
|
||||
shortcut?: ReactNode;
|
||||
};
|
||||
|
||||
// A menu item with a radio dot indicator. Used inside a DropdownRadioGroup.
|
||||
export function DropdownRadioItem(props: DropdownRadioItemProps) {
|
||||
const { className, shortcut, children, ...rest } = props;
|
||||
const { size, variant, highContrast } = useDropdownContext();
|
||||
|
||||
return (
|
||||
<Menu.RadioItem
|
||||
className={dropdownItem({ size, variant, color: "accent", highContrast, className })}
|
||||
{...rest}
|
||||
>
|
||||
<span className={dropdownItemIndicator()}>
|
||||
<Menu.RadioItemIndicator className="size-1.5 rounded-full bg-current" />
|
||||
</span>
|
||||
<span className={dropdownItemLabel()}>{children}</span>
|
||||
{shortcut != null && <span className={dropdownItemShortcut()}>{shortcut}</span>}
|
||||
</Menu.RadioItem>
|
||||
);
|
||||
}
|
||||
29
packages/ui/src/v2/Dropdown/DropdownSeparator.tsx
Normal file
29
packages/ui/src/v2/Dropdown/DropdownSeparator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { dropdownSeparator } from "./variants";
|
||||
|
||||
export type DropdownSeparatorProps = Omit<ComponentProps<typeof Menu.Separator>, "className"> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// A thin divider between groups of items.
|
||||
export function DropdownSeparator(props: DropdownSeparatorProps) {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return <Menu.Separator className={dropdownSeparator({ className })} {...rest} />;
|
||||
}
|
||||
24
packages/ui/src/v2/Dropdown/DropdownSubmenu.tsx
Normal file
24
packages/ui/src/v2/Dropdown/DropdownSubmenu.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
// Wraps a nested menu. Place a DropdownSubmenuTrigger and a DropdownPopup
|
||||
// inside. Mirrors Base UI's SubmenuRoot.
|
||||
export type DropdownSubmenuProps = ComponentProps<typeof Menu.SubmenuRoot>;
|
||||
|
||||
export function DropdownSubmenu(props: DropdownSubmenuProps) {
|
||||
return <Menu.SubmenuRoot {...props} />;
|
||||
}
|
||||
46
packages/ui/src/v2/Dropdown/DropdownSubmenuTrigger.tsx
Normal file
46
packages/ui/src/v2/Dropdown/DropdownSubmenuTrigger.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import { CaretRightIcon } from "@phosphor-icons/react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { useDropdownContext } from "./context";
|
||||
import { dropdownItem, dropdownItemLabel, dropdownSubmenuCaret } from "./variants";
|
||||
|
||||
export type DropdownSubmenuTriggerProps
|
||||
= & Omit<ComponentProps<typeof Menu.SubmenuTrigger>, "className">
|
||||
& {
|
||||
className?: string;
|
||||
iconStart?: ReactNode;
|
||||
};
|
||||
|
||||
// The item that opens a submenu; renders a trailing caret.
|
||||
export function DropdownSubmenuTrigger(props: DropdownSubmenuTriggerProps) {
|
||||
const { className, iconStart, children, ...rest } = props;
|
||||
const { size, variant, highContrast } = useDropdownContext();
|
||||
|
||||
return (
|
||||
<Menu.SubmenuTrigger
|
||||
className={dropdownItem({ size, variant, color: "accent", highContrast, className })}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
<span className={dropdownItemLabel()}>{children}</span>
|
||||
<span className={dropdownSubmenuCaret()}>
|
||||
<CaretRightIcon />
|
||||
</span>
|
||||
</Menu.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
23
packages/ui/src/v2/Dropdown/DropdownTrigger.tsx
Normal file
23
packages/ui/src/v2/Dropdown/DropdownTrigger.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
// The element that opens the menu. Compose a Button via `render` to style it.
|
||||
export type DropdownTriggerProps = ComponentProps<typeof Menu.Trigger>;
|
||||
|
||||
export function DropdownTrigger(props: DropdownTriggerProps) {
|
||||
return <Menu.Trigger {...props} />;
|
||||
}
|
||||
35
packages/ui/src/v2/Dropdown/context.ts
Normal file
35
packages/ui/src/v2/Dropdown/context.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
// Menu-level look (set on the popup) shared with every item, so item styling
|
||||
// stays consistent without threading props through each one.
|
||||
export type DropdownContextValue = {
|
||||
size: 1 | 2;
|
||||
variant: "solid" | "soft";
|
||||
highContrast: boolean;
|
||||
};
|
||||
|
||||
const DropdownContext = createContext<DropdownContextValue>({
|
||||
size: 2,
|
||||
variant: "solid",
|
||||
highContrast: false,
|
||||
});
|
||||
|
||||
export const DropdownProvider = DropdownContext.Provider;
|
||||
|
||||
export function useDropdownContext() {
|
||||
return useContext(DropdownContext);
|
||||
}
|
||||
93
packages/ui/src/v2/Dropdown/variants.ts
Normal file
93
packages/ui/src/v2/Dropdown/variants.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Dropdown menu (Radix "Dropdown Menu" over Base UI's Menu). Accent-colored
|
||||
// menu surface with grouped, highlightable items.
|
||||
|
||||
export const dropdownPopup = tv({
|
||||
base: [
|
||||
"min-w-40 origin-[var(--transform-origin)] rounded-3 border border-sand-6 bg-sand-1 p-2 shadow-5 outline-none",
|
||||
"transition-[transform,opacity] data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
|
||||
"data-[ending-style]:scale-95 data-[ending-style]:opacity-0",
|
||||
],
|
||||
});
|
||||
|
||||
export const dropdownItem = tv({
|
||||
base: [
|
||||
"group flex cursor-pointer items-center gap-2 rounded-2 outline-none select-none",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
1: "h-7 px-2 text-1",
|
||||
2: "h-8 px-3 text-2",
|
||||
},
|
||||
// Highlight treatment resolves in the compound variants below.
|
||||
variant: {
|
||||
solid: "",
|
||||
soft: "",
|
||||
},
|
||||
color: {
|
||||
accent: "text-sand-12",
|
||||
error: "text-red-11",
|
||||
},
|
||||
highContrast: {
|
||||
true: "",
|
||||
false: "",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
// accent
|
||||
{ variant: "solid", color: "accent", class: "data-[highlighted]:bg-gold-9 data-[highlighted]:text-white" },
|
||||
{ variant: "soft", color: "accent", class: "data-[highlighted]:bg-gold-4 data-[highlighted]:text-gold-12" },
|
||||
{ color: "accent", highContrast: true, class: "text-sand-12" },
|
||||
// error
|
||||
{ variant: "solid", color: "error", class: "data-[highlighted]:bg-red-9 data-[highlighted]:text-white" },
|
||||
{ variant: "soft", color: "error", class: "data-[highlighted]:bg-red-4 data-[highlighted]:text-red-12" },
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 2,
|
||||
variant: "solid",
|
||||
color: "accent",
|
||||
highContrast: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const dropdownItemLabel = tv({
|
||||
base: "min-w-0 flex-1 truncate",
|
||||
});
|
||||
|
||||
// Keyboard shortcut hint: trailing and muted, but inherits the item's color
|
||||
// once the item is highlighted (so it tracks the contrast text).
|
||||
export const dropdownItemShortcut = tv({
|
||||
base: "shrink-0 text-sand-11 group-data-[highlighted]:text-inherit",
|
||||
});
|
||||
|
||||
export const dropdownItemIndicator = tv({
|
||||
base: "flex size-4 shrink-0 items-center justify-center [&_svg]:size-4",
|
||||
});
|
||||
|
||||
export const dropdownSubmenuCaret = tv({
|
||||
base: "size-4 shrink-0 [&_svg]:size-4",
|
||||
});
|
||||
|
||||
export const dropdownSeparator = tv({
|
||||
base: "-mx-2 my-1 h-px bg-sand-6",
|
||||
});
|
||||
|
||||
export const dropdownGroupLabel = tv({
|
||||
base: "px-3 py-1 text-1 font-medium text-sand-11",
|
||||
});
|
||||
Reference in New Issue
Block a user