From 1992421c916507093edabe82f5d27199540d7089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 24 Jun 2026 22:49:59 +0200 Subject: [PATCH] Add v2 Dropdown menu component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- .../ui/src/v2/Dropdown/Dropdown.stories.tsx | 119 ++++++++++++++++++ packages/ui/src/v2/Dropdown/Dropdown.tsx | 25 ++++ .../src/v2/Dropdown/DropdownCheckboxItem.tsx | 49 ++++++++ packages/ui/src/v2/Dropdown/DropdownGroup.tsx | 23 ++++ .../ui/src/v2/Dropdown/DropdownGroupLabel.tsx | 29 +++++ packages/ui/src/v2/Dropdown/DropdownItem.tsx | 47 +++++++ packages/ui/src/v2/Dropdown/DropdownPopup.tsx | 55 ++++++++ .../ui/src/v2/Dropdown/DropdownRadioGroup.tsx | 24 ++++ .../ui/src/v2/Dropdown/DropdownRadioItem.tsx | 45 +++++++ .../ui/src/v2/Dropdown/DropdownSeparator.tsx | 29 +++++ .../ui/src/v2/Dropdown/DropdownSubmenu.tsx | 24 ++++ .../v2/Dropdown/DropdownSubmenuTrigger.tsx | 46 +++++++ .../ui/src/v2/Dropdown/DropdownTrigger.tsx | 23 ++++ packages/ui/src/v2/Dropdown/context.ts | 35 ++++++ packages/ui/src/v2/Dropdown/variants.ts | 93 ++++++++++++++ 15 files changed, 666 insertions(+) create mode 100644 packages/ui/src/v2/Dropdown/Dropdown.stories.tsx create mode 100644 packages/ui/src/v2/Dropdown/Dropdown.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownCheckboxItem.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownGroup.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownGroupLabel.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownItem.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownPopup.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownRadioGroup.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownRadioItem.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownSeparator.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownSubmenu.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownSubmenuTrigger.tsx create mode 100644 packages/ui/src/v2/Dropdown/DropdownTrigger.tsx create mode 100644 packages/ui/src/v2/Dropdown/context.ts create mode 100644 packages/ui/src/v2/Dropdown/variants.ts diff --git a/packages/ui/src/v2/Dropdown/Dropdown.stories.tsx b/packages/ui/src/v2/Dropdown/Dropdown.stories.tsx new file mode 100644 index 000000000..41d8e767f --- /dev/null +++ b/packages/ui/src/v2/Dropdown/Dropdown.stories.tsx @@ -0,0 +1,119 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 ( + + Open menu} /> + + Edit + Duplicate + + + More + + Move + Archive + + + + Delete + + + ); +} + +export function Soft() { + return ( + + Soft highlight} /> + + Edit + Duplicate + + Delete + + + ); +} + +export function Size1() { + return ( + + Small} /> + + Edit + Duplicate + + + ); +} + +export function WithGroupLabel() { + return ( + + Account} /> + + + Account + Profile + Settings + + + + ); +} + +export function CheckboxAndRadio() { + const [showGrid, setShowGrid] = useState(true); + const [view, setView] = useState("comfortable"); + + return ( + + View} /> + + + Show grid + + + + Comfortable + Compact + + + + ); +} diff --git a/packages/ui/src/v2/Dropdown/Dropdown.tsx b/packages/ui/src/v2/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..788a563fd --- /dev/null +++ b/packages/ui/src/v2/Dropdown/Dropdown.tsx @@ -0,0 +1,25 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function Dropdown(props: DropdownProps) { + return ; +} diff --git a/packages/ui/src/v2/Dropdown/DropdownCheckboxItem.tsx b/packages/ui/src/v2/Dropdown/DropdownCheckboxItem.tsx new file mode 100644 index 000000000..8448dfd07 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownCheckboxItem.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "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 ( + + + + + + + {children} + {shortcut != null && {shortcut}} + + ); +} diff --git a/packages/ui/src/v2/Dropdown/DropdownGroup.tsx b/packages/ui/src/v2/Dropdown/DropdownGroup.tsx new file mode 100644 index 000000000..823978f82 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownGroup.tsx @@ -0,0 +1,23 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function DropdownGroup(props: DropdownGroupProps) { + return ; +} diff --git a/packages/ui/src/v2/Dropdown/DropdownGroupLabel.tsx b/packages/ui/src/v2/Dropdown/DropdownGroupLabel.tsx new file mode 100644 index 000000000..d1290e834 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownGroupLabel.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "className"> & { + className?: string; +}; + +// A non-interactive heading for a DropdownGroup. +export function DropdownGroupLabel(props: DropdownGroupLabelProps) { + const { className, ...rest } = props; + + return ; +} diff --git a/packages/ui/src/v2/Dropdown/DropdownItem.tsx b/packages/ui/src/v2/Dropdown/DropdownItem.tsx new file mode 100644 index 000000000..6f2bffd03 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownItem.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "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 ( + + {iconStart} + {children} + {shortcut != null && {shortcut}} + + ); +} diff --git a/packages/ui/src/v2/Dropdown/DropdownPopup.tsx b/packages/ui/src/v2/Dropdown/DropdownPopup.tsx new file mode 100644 index 000000000..663a2f907 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownPopup.tsx @@ -0,0 +1,55 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "className"> + & { + className?: string; + // Menu-level look applied to every item. + size?: 1 | 2; + variant?: "solid" | "soft"; + highContrast?: boolean; + // Positioner placement passthrough. + side?: ComponentProps["side"]; + align?: ComponentProps["align"]; + sideOffset?: ComponentProps["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 ( + + + + + {children} + + + + + ); +} diff --git a/packages/ui/src/v2/Dropdown/DropdownRadioGroup.tsx b/packages/ui/src/v2/Dropdown/DropdownRadioGroup.tsx new file mode 100644 index 000000000..1f442bae2 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownRadioGroup.tsx @@ -0,0 +1,24 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function DropdownRadioGroup(props: DropdownRadioGroupProps) { + return ; +} diff --git a/packages/ui/src/v2/Dropdown/DropdownRadioItem.tsx b/packages/ui/src/v2/Dropdown/DropdownRadioItem.tsx new file mode 100644 index 000000000..f04af2b0e --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownRadioItem.tsx @@ -0,0 +1,45 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "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 ( + + + + + {children} + {shortcut != null && {shortcut}} + + ); +} diff --git a/packages/ui/src/v2/Dropdown/DropdownSeparator.tsx b/packages/ui/src/v2/Dropdown/DropdownSeparator.tsx new file mode 100644 index 000000000..74f754ede --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownSeparator.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "className"> & { + className?: string; +}; + +// A thin divider between groups of items. +export function DropdownSeparator(props: DropdownSeparatorProps) { + const { className, ...rest } = props; + + return ; +} diff --git a/packages/ui/src/v2/Dropdown/DropdownSubmenu.tsx b/packages/ui/src/v2/Dropdown/DropdownSubmenu.tsx new file mode 100644 index 000000000..0f1ca9484 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownSubmenu.tsx @@ -0,0 +1,24 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function DropdownSubmenu(props: DropdownSubmenuProps) { + return ; +} diff --git a/packages/ui/src/v2/Dropdown/DropdownSubmenuTrigger.tsx b/packages/ui/src/v2/Dropdown/DropdownSubmenuTrigger.tsx new file mode 100644 index 000000000..cb84de087 --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownSubmenuTrigger.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "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 ( + + {iconStart} + {children} + + + + + ); +} diff --git a/packages/ui/src/v2/Dropdown/DropdownTrigger.tsx b/packages/ui/src/v2/Dropdown/DropdownTrigger.tsx new file mode 100644 index 000000000..a8992e0fd --- /dev/null +++ b/packages/ui/src/v2/Dropdown/DropdownTrigger.tsx @@ -0,0 +1,23 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function DropdownTrigger(props: DropdownTriggerProps) { + return ; +} diff --git a/packages/ui/src/v2/Dropdown/context.ts b/packages/ui/src/v2/Dropdown/context.ts new file mode 100644 index 000000000..4d299e5fa --- /dev/null +++ b/packages/ui/src/v2/Dropdown/context.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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({ + size: 2, + variant: "solid", + highContrast: false, +}); + +export const DropdownProvider = DropdownContext.Provider; + +export function useDropdownContext() { + return useContext(DropdownContext); +} diff --git a/packages/ui/src/v2/Dropdown/variants.ts b/packages/ui/src/v2/Dropdown/variants.ts new file mode 100644 index 000000000..06921000c --- /dev/null +++ b/packages/ui/src/v2/Dropdown/variants.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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", +});