diff --git a/packages/demo/src/components/demo/dropdown-menu.tsx b/packages/demo/src/components/demo/dropdown-menu.tsx index 8cc5e5dc..01b14d7d 100644 --- a/packages/demo/src/components/demo/dropdown-menu.tsx +++ b/packages/demo/src/components/demo/dropdown-menu.tsx @@ -37,203 +37,222 @@ export const DropdownMenuDemo = ({ if (variant === "default") { return ( - - - - - - Profile - Settings - Logout - - +
+ + + + + + Profile + Settings + Logout + + +
); } if (variant === "with-separators") { return ( - - - - - - My Account - - - - Profile - - - - Settings - - - - - Logout - - - +
+ + + + + + My Account + + + + Profile + + + + Settings + + + + + Logout + + + +
); } if (variant === "with-checkboxes") { return ( - - - - - - Appearance - - - Status Bar - - - Activity Bar - - - Panel - - - +
+ + + + + + Appearance + + + Status Bar + + + Activity Bar + + + Panel + + + +
); } if (variant === "with-radio") { return ( - - - - - - Panel Position - - - Top - Bottom - Right - - - +
+ + + + + + Panel Position + + + Top + + Bottom + + Right + + + +
); } if (variant === "with-shortcuts") { return ( - - - - - - - New Tab - ⌘T - - - New Window - ⌘N - - - Close Tab - ⌘W - - - - Print - ⌘P - - - +
+ + + + + + + New Tab + ⌘T + + + New Window + ⌘N + + + Close Tab + ⌘W + + + + Print + ⌘P + + + +
); } if (variant === "with-submenu") { return ( - - - - - - Back - Forward - Reload - - - - More Tools - - - Save Page As... - Create Shortcut... - Name Window... - - Developer Tools - - - - Settings - - +
+ + + + + + Back + Forward + Reload + + + + More Tools + + + Save Page As... + Create Shortcut... + Name Window... + + Developer Tools + + + + Settings + + +
); } if (variant === "with-groups") { return ( - - - - - - - File - - New File - ⌘N - - - Open File - ⌘O - - - Save - ⌘S - - - - - Edit - Undo - Redo - Cut - Copy - Paste - - - +
+ + + + + + + File + + New File + ⌘N + + + Open File + ⌘O + + + Save + ⌘S + + + + + Edit + Undo + Redo + Cut + Copy + Paste + + + +
); } diff --git a/packages/demo/src/content/components/dropdown-menu.mdx b/packages/demo/src/content/components/dropdown-menu.mdx index 4a19b570..2d6fcc5c 100644 --- a/packages/demo/src/content/components/dropdown-menu.mdx +++ b/packages/demo/src/content/components/dropdown-menu.mdx @@ -1,38 +1,237 @@ --- layout: "@demo/layouts/mdx-layout.astro" heading: "Dropdown Menu" -description: "Dropdown Menu with sizes and variants" +description: "A menu of actions or options triggered by a button" --- import { DropdownMenuDemo } from "@demo/components/demo/dropdown-menu"; -import { ELEVATION } from "@eqtylab/equality"; -## Variants +## Overview + +A dropdown menu displays a list of actions or options in a floating panel anchored to a trigger. Use it for contextual actions, account menus, view options, and settings. It supports labels, separators, checkboxes, radio groups, keyboard shortcuts, grouping, and nested submenus, and is fully keyboard navigable. + +## Usage + +Import the components: + +```ts +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@eqtylab/equality"; +``` + +Basic usage: -### Default +```jsx + + + + + + Profile + Settings + Logout + + +``` +## Variants + ### With Separators and Labels +Use `DropdownMenuLabel` to title a section and `DropdownMenuSeparator` to divide groups of items. Items accept icons as children alongside a `` label. + +```jsx + + My Account + + + + Profile + + + + Settings + + + + + Logout + + +``` + ### With Checkboxes +Use `DropdownMenuCheckboxItem` for options that toggle on and off independently. Control each item with `checked` and `onCheckedChange`. + +```jsx +const [showStatusBar, setShowStatusBar] = useState(true); + + + Status Bar +; +``` + ### With Radio Items +Use `DropdownMenuRadioGroup` with `DropdownMenuRadioItem` to select a single option from a set. The group is controlled with `value` and `onValueChange`. + +```jsx +const [position, setPosition] = useState("bottom"); + + + Top + Bottom + Right +; +``` + ### With Shortcuts +Use `DropdownMenuShortcut` to display a keyboard shortcut hint aligned to the end of an item. + +```jsx + + New Tab + ⌘T + +``` + ### With Submenu +Use `DropdownMenuSub`, `DropdownMenuSubTrigger`, and `DropdownMenuSubContent` to nest a menu inside an item. The submenu opens on hover or keyboard focus. + +```jsx + + + More Tools + + + Save Page As... + Create Shortcut... + + Developer Tools + + +``` + ### With Groups +Wrap related items in `DropdownMenuGroup` to associate a label with its items for assistive technology. + + +```jsx + + File + + New File + ⌘N + + + Open File + ⌘O + + +``` + +## Slots + +| Name | Description | +| -------------------------- | ------------------------------------------------ | +| `DropdownMenu` | Root component, manages open state | +| `DropdownMenuTrigger` | Element that opens the menu on click | +| `DropdownMenuContent` | Floating panel containing the menu items | +| `DropdownMenuItem` | A single actionable menu item | +| `DropdownMenuCheckboxItem` | A toggleable item with a checkmark indicator | +| `DropdownMenuRadioGroup` | Groups radio items into a single-select set | +| `DropdownMenuRadioItem` | A single-select item within a radio group | +| `DropdownMenuLabel` | Non-interactive section title | +| `DropdownMenuSeparator` | Divider between groups of items | +| `DropdownMenuShortcut` | Keyboard shortcut hint aligned to the item's end | +| `DropdownMenuGroup` | Groups related items for assistive technology | +| `DropdownMenuSub` | Root for a nested submenu | +| `DropdownMenuSubTrigger` | Item that opens a nested submenu | +| `DropdownMenuSubContent` | Floating panel for a nested submenu | + +## Props + +### DropdownMenu + +| Name | Description | Type | Default | Required | +| -------------- | ---------------------------------- | ------------------------- | ------- | -------- | +| `open` | Controls the open state | `boolean` | - | ❌ | +| `defaultOpen` | Open state when initially rendered | `boolean` | - | ❌ | +| `onOpenChange` | Called when the open state changes | `(open: boolean) => void` | - | ❌ | + +### DropdownMenuTrigger + +| Name | Description | Type | Default | Required | +| --------- | -------------------------------------------------------- | --------- | ------- | -------- | +| `asChild` | Merge props onto the child instead of rendering a button | `boolean` | `false` | ❌ | + +### DropdownMenuContent + +| Name | Description | Type | Default | Required | +| ------------ | ----------------------------------- | -------------------------------- | -------- | -------- | +| `align` | Alignment against the trigger | `start`, `center`, `end` | `center` | ❌ | +| `side` | Side of the trigger to render on | `top`, `right`, `bottom`, `left` | `bottom` | ❌ | +| `sideOffset` | Distance in pixels from the trigger | `number` | `4` | ❌ | + +### DropdownMenuItem + +| Name | Description | Type | Default | Required | +| ---------- | ----------------------------------------------------- | ------------ | ------- | -------- | +| `danger` | Styles the item as a destructive action | `boolean` | `false` | ❌ | +| `inset` | Adds left padding to align with items that have icons | `boolean` | `false` | ❌ | +| `disabled` | Prevents interaction and dims the item | `boolean` | `false` | ❌ | +| `onSelect` | Called when the item is selected | `() => void` | - | ❌ | + +### DropdownMenuCheckboxItem + +| Name | Description | Type | Default | Required | +| ----------------- | -------------------------------------- | ---------------------------- | ------- | -------- | +| `checked` | Whether the item is checked | `boolean` | - | ❌ | +| `onCheckedChange` | Called when the checked state changes | `(checked: boolean) => void` | - | ❌ | +| `disabled` | Prevents interaction and dims the item | `boolean` | `false` | ❌ | + +### DropdownMenuRadioGroup + +| Name | Description | Type | Default | Required | +| --------------- | -------------------------------------- | ------------------------- | ------- | -------- | +| `value` | The value of the selected item | `string` | - | ❌ | +| `onValueChange` | Called when the selected value changes | `(value: string) => void` | - | ❌ | + +### DropdownMenuRadioItem + +| Name | Description | Type | Default | Required | +| ---------- | -------------------------------------- | --------- | ------- | -------- | +| `value` | The unique value of the item | `string` | - | ✅ | +| `disabled` | Prevents interaction and dims the item | `boolean` | `false` | ❌ | + +### DropdownMenuLabel & DropdownMenuSubTrigger + +| Name | Description | Type | Default | Required | +| ------- | ----------------------------------------------------- | --------- | ------- | -------- | +| `inset` | Adds left padding to align with items that have icons | `boolean` | `false` | ❌ | diff --git a/packages/tokens/equality-tokens.json b/packages/tokens/equality-tokens.json index 993250b6..b2ea0e4a 100644 --- a/packages/tokens/equality-tokens.json +++ b/packages/tokens/equality-tokens.json @@ -876,6 +876,14 @@ } }, "text": { + "danger": { + "$type": "color", + "$value": { + "colorSpace": "display-p3", + "components": [0.8779, 0.5865, 0.5474], + "hex": "#E0968C" + } + }, "link": { "$type": "color", "$value": { @@ -2926,6 +2934,14 @@ } }, "text": { + "danger": { + "$type": "color", + "$value": { + "colorSpace": "display-p3", + "components": [0.5698, 0.1873, 0.1674], + "hex": "#91302B" + } + }, "link": { "$type": "color", "$value": { diff --git a/packages/tokens/equality-tokens.pinwheel b/packages/tokens/equality-tokens.pinwheel index 336a0b84..3f5dd983 100644 --- a/packages/tokens/equality-tokens.pinwheel +++ b/packages/tokens/equality-tokens.pinwheel @@ -1425,7 +1425,6 @@ 3AF4332D-089A-4B0F-B382-199809A5D0F8 Badge - 1 1 1B345E6F-7BFA-4A29-A8DE-903B2EB193A0 @@ -1611,7 +1610,6 @@ DA9E770F-29ED-4A78-9EDB-DD9F456BB566 Background - 1 1 Primary @@ -1794,7 +1792,6 @@ F4FC786D-8F79-4199-A3D3-35667F6E2DCD Resource - 1 1 Agent @@ -2622,6 +2619,7 @@ 4EC67953-4547-4883-B947-D73CF2D3DF18 Text + 1 1 Primary @@ -2665,6 +2663,17 @@ 2 86B9D7D8-F74A-4CAE-B006-65451BBEDAB9 + + Danger + 3 + 3 + lightanddark + CCD80E1D-24ED-4193-A845-891C968555AF + 5 + CCD80E1D-24ED-4193-A845-891C968555AF + 2 + 86B9D7D8-F74A-4CAE-B006-65451BBEDAB9 + 635BED27-D972-4FC5-BD7E-AE05C08B92FD @@ -2696,7 +2705,6 @@ C2525A18-4A97-4EEC-BE78-A03DF3113E10 Brand - 1 1 Primary diff --git a/packages/ui/src/components/command/command.module.css b/packages/ui/src/components/command/command.module.css index d0c28b54..a88d7762 100644 --- a/packages/ui/src/components/command/command.module.css +++ b/packages/ui/src/components/command/command.module.css @@ -74,7 +74,7 @@ @apply cursor-pointer select-none; @apply [&_svg:not([class*='text-'])]:text-text-secondary [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0; @apply data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50; - @apply data-[selected=true]:bg-mixed-light; + @apply dark:data-[selected=true]:bg-mixed-light data-[selected=true]:bg-mixed-dark; } .command-shortcut { diff --git a/packages/ui/src/components/dropdown-menu/dropdown-menu.module.css b/packages/ui/src/components/dropdown-menu/dropdown-menu.module.css index b4a794b7..572b21cc 100644 --- a/packages/ui/src/components/dropdown-menu/dropdown-menu.module.css +++ b/packages/ui/src/components/dropdown-menu/dropdown-menu.module.css @@ -30,6 +30,25 @@ @apply dropdown-item; } +.dropdown-menu-trigger, +.dropdown-menu-item, +.dropdown-menu-checkbox-item, +.dropdown-menu-radio-item { + @apply hover:not-disabled:text-lilac-700! focus:text-lilac-700!; + @apply hover:not-disabled:bg-lilac-300/50! focus:bg-lilac-300/50!; + @apply hover:not-disabled:shadow-sm; + @apply dark:hover:not-disabled:bg-lilac-600/50! dark:focus:bg-lilac-600/50!; + @apply dark:hover:not-disabled:text-lilac-100! dark:focus:text-lilac-100!; +} + +.dropdown-menu-item-danger { + @apply text-text-danger!; + @apply hover:not-disabled:text-red-700! focus:text-red-700!; + @apply hover:not-disabled:bg-red-300/50! focus:bg-red-300/50!; + @apply dark:hover:not-disabled:bg-red-600/50! dark:focus:bg-red-600/50!; + @apply dark:hover:not-disabled:text-red-100! dark:focus:text-red-100!; +} + .dropdown-menu-item-indicator-checkbox, .dropdown-menu-item-indicator-radio { @apply absolute left-2 size-3.5; diff --git a/packages/ui/src/components/dropdown-menu/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu/dropdown-menu.tsx index 7dc187f9..352d6995 100644 --- a/packages/ui/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu/dropdown-menu.tsx @@ -77,11 +77,17 @@ const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { inset?: boolean; + danger?: boolean; } ->(({ className, inset, ...props }, ref) => ( +>(({ className, inset, danger, ...props }, ref) => ( )); diff --git a/packages/ui/src/components/table/table-components.module.css b/packages/ui/src/components/table/table-components.module.css index 7762c831..e264953d 100644 --- a/packages/ui/src/components/table/table-components.module.css +++ b/packages/ui/src/components/table/table-components.module.css @@ -61,8 +61,8 @@ .table-row--clickable:not([data-state='selected']) { --mix-color: var(--color-brand-primary); - --hover-darken: 50%; - --hover-lighten: 50%; + --hover-darken: 55%; + --hover-lighten: 55%; @apply cursor-pointer; } @@ -70,7 +70,7 @@ .table-row--clickable:not([data-state='selected']):hover:not( :has(button:hover, a:not(.table-row-link):hover) ) { - @apply bg-mixed-light! dark:bg-mixed-dark!; + @apply dark:bg-mixed-dark! bg-mixed-light!; } /* Linked Row (stretched link pattern) */ diff --git a/packages/ui/src/theme/generated/color-vars.css b/packages/ui/src/theme/generated/color-vars.css index 9f517e8f..e4903062 100644 --- a/packages/ui/src/theme/generated/color-vars.css +++ b/packages/ui/src/theme/generated/color-vars.css @@ -245,6 +245,7 @@ --color-teal-700: color(display-p3 0.1475 0.2946 0.2434); --color-teal-800: color(display-p3 0.09443 0.1941 0.159); --color-teal-900: color(display-p3 0.04463 0.1013 0.08089); + --color-text-danger: color(display-p3 0.5698 0.1873 0.1674); --color-text-link: color(display-p3 0.4045 0.2306 0.6077); --color-text-primary: color(display-p3 0 0 0); --color-text-secondary: color(display-p3 0.1792 0.1792 0.1792); @@ -367,6 +368,7 @@ --dark-color-resource-text-token: color(display-p3 0.8033 0.8266 0.7044); --dark-color-resource-text-tool: color(display-p3 0.9358 0.751 0.7228); --dark-color-resource-text-unknown: color(display-p3 0.8698 0.8698 0.8698); + --dark-color-text-danger: color(display-p3 0.8779 0.5865 0.5474); --dark-color-text-link: color(display-p3 0.7319 0.6108 0.8873); --dark-color-text-primary: color(display-p3 1 1 1); --dark-color-text-secondary: color(display-p3 0.7432 0.7432 0.7432); diff --git a/packages/ui/src/theme/generated/dark-tokens.css b/packages/ui/src/theme/generated/dark-tokens.css index f0035dd5..6e01fab5 100644 --- a/packages/ui/src/theme/generated/dark-tokens.css +++ b/packages/ui/src/theme/generated/dark-tokens.css @@ -106,6 +106,7 @@ --color-resource-text-token: color(display-p3 0.8033 0.8266 0.7044); --color-resource-text-tool: color(display-p3 0.9358 0.751 0.7228); --color-resource-text-unknown: color(display-p3 0.8698 0.8698 0.8698); + --color-text-danger: color(display-p3 0.8779 0.5865 0.5474); --color-text-link: color(display-p3 0.7319 0.6108 0.8873); --color-text-primary: color(display-p3 1 1 1); --color-text-secondary: color(display-p3 0.7432 0.7432 0.7432); diff --git a/packages/ui/src/theme/generated/global-dark-tokens.css b/packages/ui/src/theme/generated/global-dark-tokens.css index 595b3fe5..f3b6df24 100644 --- a/packages/ui/src/theme/generated/global-dark-tokens.css +++ b/packages/ui/src/theme/generated/global-dark-tokens.css @@ -106,6 +106,7 @@ html[data-equality-theme='dark'] { --color-resource-text-token: color(display-p3 0.8033 0.8266 0.7044); --color-resource-text-tool: color(display-p3 0.9358 0.751 0.7228); --color-resource-text-unknown: color(display-p3 0.8698 0.8698 0.8698); + --color-text-danger: color(display-p3 0.8779 0.5865 0.5474); --color-text-link: color(display-p3 0.7319 0.6108 0.8873); --color-text-primary: color(display-p3 1 1 1); --color-text-secondary: color(display-p3 0.7432 0.7432 0.7432); diff --git a/packages/ui/src/theme/generated/global-light-tokens.css b/packages/ui/src/theme/generated/global-light-tokens.css index caec31a1..15f1e492 100644 --- a/packages/ui/src/theme/generated/global-light-tokens.css +++ b/packages/ui/src/theme/generated/global-light-tokens.css @@ -249,6 +249,7 @@ html { --color-teal-700: color(display-p3 0.1475 0.2946 0.2434); --color-teal-800: color(display-p3 0.09443 0.1941 0.159); --color-teal-900: color(display-p3 0.04463 0.1013 0.08089); + --color-text-danger: color(display-p3 0.5698 0.1873 0.1674); --color-text-link: color(display-p3 0.4045 0.2306 0.6077); --color-text-primary: color(display-p3 0 0 0); --color-text-secondary: color(display-p3 0.1792 0.1792 0.1792); diff --git a/packages/ui/src/theme/generated/light-tokens.css b/packages/ui/src/theme/generated/light-tokens.css index da66a0c1..a6210c56 100644 --- a/packages/ui/src/theme/generated/light-tokens.css +++ b/packages/ui/src/theme/generated/light-tokens.css @@ -249,6 +249,7 @@ --color-teal-700: color(display-p3 0.1475 0.2946 0.2434); --color-teal-800: color(display-p3 0.09443 0.1941 0.159); --color-teal-900: color(display-p3 0.04463 0.1013 0.08089); + --color-text-danger: color(display-p3 0.5698 0.1873 0.1674); --color-text-link: color(display-p3 0.4045 0.2306 0.6077); --color-text-primary: color(display-p3 0 0 0); --color-text-secondary: color(display-p3 0.1792 0.1792 0.1792); diff --git a/packages/ui/src/theme/generated/tailwind-tokens.css b/packages/ui/src/theme/generated/tailwind-tokens.css index b84fb449..2d1aaf9f 100644 --- a/packages/ui/src/theme/generated/tailwind-tokens.css +++ b/packages/ui/src/theme/generated/tailwind-tokens.css @@ -247,6 +247,7 @@ --color-teal-700: var(--color-teal-700); --color-teal-800: var(--color-teal-800); --color-teal-900: var(--color-teal-900); + --color-text-danger: var(--color-text-danger); --color-text-link: var(--color-text-link); --color-text-primary: var(--color-text-primary); --color-text-secondary: var(--color-text-secondary); diff --git a/packages/ui/src/theme/theme-utilities.css b/packages/ui/src/theme/theme-utilities.css index 8d9d4e70..79111847 100644 --- a/packages/ui/src/theme/theme-utilities.css +++ b/packages/ui/src/theme/theme-utilities.css @@ -39,7 +39,7 @@ @apply text-sm transition-colors; @apply relative rounded-sm; @apply outline-hidden select-none; - @apply hover:not-disabled:bg-mixed-light focus:bg-mixed-light; + @apply dark:hover:not-disabled:bg-mixed-light dark:focus:bg-mixed-light hover:not-disabled:bg-mixed-dark focus:bg-mixed-dark; @apply data-disabled:pointer-events-none data-disabled:opacity-50; }