Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ jobs:
bunx shadcn@latest add http://localhost:8099/r/popover.json http://localhost:8099/r/command.json -y -o
grep -q 'rounded-menu' components/ui/popover.tsx
grep -q 'cmdk' package.json
bunx shadcn@latest add http://localhost:8099/r/dropdown-menu.json -y -o
grep -q 'rounded-menu' components/ui/dropdown-menu.tsx
bun run build
- if: always()
run: docker compose logs
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ npx shadcn@latest add @winlab/button
| `table` | `registry:ui` | Data table with muted headers; add `tabular-nums` and `text-right` to number columns |
| `popover` | `registry:ui` | Frosted menu surface anchored to a trigger; `w-(--anchor-width)` matches the trigger |
| `command` | `registry:ui` | Searchable list for a combobox inside a `popover` |
| `dropdown-menu` | `registry:ui` | Action menu: items, labels, separators and submenus |
| `badge` | `registry:ui` | Status label; variants `default`, `muted`, `outline`, `destructive`, `success`, `warning` |

Text falls back in that order: `font-sans` is Inter, then Noto Sans JP, then Noto Sans TC. `font-mono` puts JetBrains Mono in front of the same CJK fonts. Use Inter's `tabular-nums` for amounts and table figures, not `font-mono`.
Expand Down
45 changes: 45 additions & 0 deletions app/_components/menu-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client"

import { EllipsisIcon } from "lucide-react"

import { Button } from "@/registry/winlab/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuGroup,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/winlab/ui/dropdown-menu"

export function MenuDemo() {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="icon" aria-label="More" />}
>
<EllipsisIcon />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>Receipt</DropdownMenuLabel>
<DropdownMenuItem>Rename</DropdownMenuItem>
<DropdownMenuItem>Download</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Move to</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Travel</DropdownMenuItem>
<DropdownMenuItem>Meals</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PlusIcon } from "lucide-react"

import { ComboboxDemo } from "@/app/_components/combobox-demo"
import { MenuDemo } from "@/app/_components/menu-demo"

import {
AlertDialog,
Expand Down Expand Up @@ -106,6 +107,7 @@ export default function Page() {
<Button size="icon" variant="outline" aria-label="Add">
<PlusIcon />
</Button>
<MenuDemo />
</div>
</section>

Expand Down
17 changes: 17 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,23 @@
"type": "registry:ui"
}
]
},
{
"name": "dropdown-menu",
"type": "registry:ui",
"title": "Dropdown Menu",
"description": "Frosted action menu with labels, separators and one level of submenu.",
"dependencies": [
"@base-ui/react",
"cn",
"lucide-react"
],
"files": [
{
"path": "registry/winlab/ui/dropdown-menu.tsx",
"type": "registry:ui"
}
]
}
]
}
147 changes: 147 additions & 0 deletions registry/winlab/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use client"

import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "cn"
import { ChevronRightIcon } from "lucide-react"

const menuSurface =
"z-50 max-h-(--available-height) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-menu border border-border bg-transparent p-1 text-body text-foreground shadow-md backdrop-blur-md duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95"

const menuRow =
"relative flex h-10 cursor-default items-center gap-2 rounded-control px-3 outline-hidden select-none focus:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0"

function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}

function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}

function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(menuSurface, className)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
)
}

function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}

function DropdownMenuLabel({
className,
...props
}: MenuPrimitive.GroupLabel.Props) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
className={cn("px-3 py-2 text-muted-foreground", className)}
{...props}
/>
)
}

function DropdownMenuItem({ className, ...props }: MenuPrimitive.Item.Props) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
className={cn(menuRow, className)}
{...props}
/>
)
}

function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}

function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
}

function DropdownMenuSubTrigger({
className,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
className={cn(menuRow, "data-popup-open:bg-muted", className)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
)
}

// The offset (p-1 plus the border) lines the first sub item up with its trigger.
function DropdownMenuSubContent({
align = "start",
alignOffset = -5,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn("shadow-lg", className)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
)
}

export {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
}
Loading