diff options
| author | Adam <[email protected]> | 2026-02-02 07:02:40 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-02 14:24:22 -0600 |
| commit | ea1aba4192fd356603e807144edf202328008ee6 (patch) | |
| tree | e8d57fb47b8d288f884cbcab9d9b7ea13d0f4ded /packages/ui/src | |
| parent | b9aad20be651050880bf2bc3b4c857f16a970402 (diff) | |
| download | opencode-ea1aba4192fd356603e807144edf202328008ee6.tar.gz opencode-ea1aba4192fd356603e807144edf202328008ee6.zip | |
feat(app): project context menu on right-click
Diffstat (limited to 'packages/ui/src')
| -rw-r--r-- | packages/ui/src/components/context-menu.css | 134 | ||||
| -rw-r--r-- | packages/ui/src/components/context-menu.tsx | 308 | ||||
| -rw-r--r-- | packages/ui/src/styles/index.css | 1 |
3 files changed, 443 insertions, 0 deletions
diff --git a/packages/ui/src/components/context-menu.css b/packages/ui/src/components/context-menu.css new file mode 100644 index 000000000..1e366dccd --- /dev/null +++ b/packages/ui/src/components/context-menu.css @@ -0,0 +1,134 @@ +[data-component="context-menu-content"], +[data-component="context-menu-sub-content"] { + min-width: 8rem; + overflow: hidden; + border: none; + border-radius: var(--radius-md); + box-shadow: var(--shadow-xs-border); + background-clip: padding-box; + background-color: var(--surface-raised-stronger-non-alpha); + padding: 4px; + z-index: 100; + transform-origin: var(--kb-menu-content-transform-origin); + + &:focus-within, + &:focus { + outline: none; + } + + animation: contextMenuContentHide var(--transition-duration) var(--transition-easing) forwards; + + @starting-style { + animation: none; + } + + &[data-expanded] { + pointer-events: auto; + animation: contextMenuContentShow var(--transition-duration) var(--transition-easing) forwards; + } +} + +[data-component="context-menu-content"], +[data-component="context-menu-sub-content"] { + [data-slot="context-menu-item"], + [data-slot="context-menu-checkbox-item"], + [data-slot="context-menu-radio-item"], + [data-slot="context-menu-sub-trigger"] { + position: relative; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-radius: var(--radius-sm); + cursor: default; + outline: none; + + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + color: var(--text-strong); + + transition-property: background-color, color; + transition-duration: var(--transition-duration); + transition-timing-function: var(--transition-easing); + user-select: none; + + &:hover { + background-color: var(--surface-raised-base-hover); + } + + &[data-disabled] { + color: var(--text-weak); + pointer-events: none; + } + } + + [data-slot="context-menu-sub-trigger"] { + &[data-expanded] { + background: var(--surface-raised-base-hover); + outline: none; + border: none; + } + } + + [data-slot="context-menu-item-indicator"] { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + } + + [data-slot="context-menu-item-label"] { + flex: 1; + } + + [data-slot="context-menu-item-description"] { + font-size: var(--font-size-x-small); + color: var(--text-weak); + } + + [data-slot="context-menu-separator"] { + height: 1px; + margin: 4px -4px; + border-top-color: var(--border-weak-base); + } + + [data-slot="context-menu-group-label"] { + padding: 4px 8px; + font-family: var(--font-family-sans); + font-size: var(--font-size-x-small); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + color: var(--text-weak); + } + + [data-slot="context-menu-arrow"] { + fill: var(--surface-raised-stronger-non-alpha); + } +} + +@keyframes contextMenuContentShow { + from { + opacity: 0; + transform: scaleY(0.95); + } + to { + opacity: 1; + transform: scaleY(1); + } +} + +@keyframes contextMenuContentHide { + from { + opacity: 1; + transform: scaleY(1); + } + to { + opacity: 0; + transform: scaleY(0.95); + } +} diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx new file mode 100644 index 000000000..afdaff7b8 --- /dev/null +++ b/packages/ui/src/components/context-menu.tsx @@ -0,0 +1,308 @@ +import { ContextMenu as Kobalte } from "@kobalte/core/context-menu" +import { splitProps } from "solid-js" +import type { ComponentProps, ParentProps } from "solid-js" + +export interface ContextMenuProps extends ComponentProps<typeof Kobalte> {} +export interface ContextMenuTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {} +export interface ContextMenuIconProps extends ComponentProps<typeof Kobalte.Icon> {} +export interface ContextMenuPortalProps extends ComponentProps<typeof Kobalte.Portal> {} +export interface ContextMenuContentProps extends ComponentProps<typeof Kobalte.Content> {} +export interface ContextMenuArrowProps extends ComponentProps<typeof Kobalte.Arrow> {} +export interface ContextMenuSeparatorProps extends ComponentProps<typeof Kobalte.Separator> {} +export interface ContextMenuGroupProps extends ComponentProps<typeof Kobalte.Group> {} +export interface ContextMenuGroupLabelProps extends ComponentProps<typeof Kobalte.GroupLabel> {} +export interface ContextMenuItemProps extends ComponentProps<typeof Kobalte.Item> {} +export interface ContextMenuItemLabelProps extends ComponentProps<typeof Kobalte.ItemLabel> {} +export interface ContextMenuItemDescriptionProps extends ComponentProps<typeof Kobalte.ItemDescription> {} +export interface ContextMenuItemIndicatorProps extends ComponentProps<typeof Kobalte.ItemIndicator> {} +export interface ContextMenuRadioGroupProps extends ComponentProps<typeof Kobalte.RadioGroup> {} +export interface ContextMenuRadioItemProps extends ComponentProps<typeof Kobalte.RadioItem> {} +export interface ContextMenuCheckboxItemProps extends ComponentProps<typeof Kobalte.CheckboxItem> {} +export interface ContextMenuSubProps extends ComponentProps<typeof Kobalte.Sub> {} +export interface ContextMenuSubTriggerProps extends ComponentProps<typeof Kobalte.SubTrigger> {} +export interface ContextMenuSubContentProps extends ComponentProps<typeof Kobalte.SubContent> {} + +function ContextMenuRoot(props: ContextMenuProps) { + return <Kobalte {...props} data-component="context-menu" /> +} + +function ContextMenuTrigger(props: ParentProps<ContextMenuTriggerProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Trigger + {...rest} + data-slot="context-menu-trigger" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.Trigger> + ) +} + +function ContextMenuIcon(props: ParentProps<ContextMenuIconProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Icon + {...rest} + data-slot="context-menu-icon" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.Icon> + ) +} + +function ContextMenuPortal(props: ContextMenuPortalProps) { + return <Kobalte.Portal {...props} /> +} + +function ContextMenuContent(props: ParentProps<ContextMenuContentProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Content + {...rest} + data-component="context-menu-content" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.Content> + ) +} + +function ContextMenuArrow(props: ContextMenuArrowProps) { + const [local, rest] = splitProps(props, ["class", "classList"]) + return ( + <Kobalte.Arrow + {...rest} + data-slot="context-menu-arrow" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + /> + ) +} + +function ContextMenuSeparator(props: ContextMenuSeparatorProps) { + const [local, rest] = splitProps(props, ["class", "classList"]) + return ( + <Kobalte.Separator + {...rest} + data-slot="context-menu-separator" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + /> + ) +} + +function ContextMenuGroup(props: ParentProps<ContextMenuGroupProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Group + {...rest} + data-slot="context-menu-group" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.Group> + ) +} + +function ContextMenuGroupLabel(props: ParentProps<ContextMenuGroupLabelProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.GroupLabel + {...rest} + data-slot="context-menu-group-label" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.GroupLabel> + ) +} + +function ContextMenuItem(props: ParentProps<ContextMenuItemProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.Item + {...rest} + data-slot="context-menu-item" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.Item> + ) +} + +function ContextMenuItemLabel(props: ParentProps<ContextMenuItemLabelProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.ItemLabel + {...rest} + data-slot="context-menu-item-label" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.ItemLabel> + ) +} + +function ContextMenuItemDescription(props: ParentProps<ContextMenuItemDescriptionProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.ItemDescription + {...rest} + data-slot="context-menu-item-description" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.ItemDescription> + ) +} + +function ContextMenuItemIndicator(props: ParentProps<ContextMenuItemIndicatorProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.ItemIndicator + {...rest} + data-slot="context-menu-item-indicator" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.ItemIndicator> + ) +} + +function ContextMenuRadioGroup(props: ParentProps<ContextMenuRadioGroupProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.RadioGroup + {...rest} + data-slot="context-menu-radio-group" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.RadioGroup> + ) +} + +function ContextMenuRadioItem(props: ParentProps<ContextMenuRadioItemProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.RadioItem + {...rest} + data-slot="context-menu-radio-item" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.RadioItem> + ) +} + +function ContextMenuCheckboxItem(props: ParentProps<ContextMenuCheckboxItemProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.CheckboxItem + {...rest} + data-slot="context-menu-checkbox-item" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.CheckboxItem> + ) +} + +function ContextMenuSub(props: ContextMenuSubProps) { + return <Kobalte.Sub {...props} /> +} + +function ContextMenuSubTrigger(props: ParentProps<ContextMenuSubTriggerProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.SubTrigger + {...rest} + data-slot="context-menu-sub-trigger" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.SubTrigger> + ) +} + +function ContextMenuSubContent(props: ParentProps<ContextMenuSubContentProps>) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + <Kobalte.SubContent + {...rest} + data-component="context-menu-sub-content" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + {local.children} + </Kobalte.SubContent> + ) +} + +export const ContextMenu = Object.assign(ContextMenuRoot, { + Trigger: ContextMenuTrigger, + Icon: ContextMenuIcon, + Portal: ContextMenuPortal, + Content: ContextMenuContent, + Arrow: ContextMenuArrow, + Separator: ContextMenuSeparator, + Group: ContextMenuGroup, + GroupLabel: ContextMenuGroupLabel, + Item: ContextMenuItem, + ItemLabel: ContextMenuItemLabel, + ItemDescription: ContextMenuItemDescription, + ItemIndicator: ContextMenuItemIndicator, + RadioGroup: ContextMenuRadioGroup, + RadioItem: ContextMenuRadioItem, + CheckboxItem: ContextMenuCheckboxItem, + Sub: ContextMenuSub, + SubTrigger: ContextMenuSubTrigger, + SubContent: ContextMenuSubContent, +}) diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 2a8171f98..d5939b2b3 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -16,6 +16,7 @@ @import "../components/collapsible.css" layer(components); @import "../components/diff.css" layer(components); @import "../components/diff-changes.css" layer(components); +@import "../components/context-menu.css" layer(components); @import "../components/dropdown-menu.css" layer(components); @import "../components/dialog.css" layer(components); @import "../components/file-icon.css" layer(components); |
