feat(app): project context menu on right-click

This commit is contained in:
Adam
2026-02-02 07:02:40 -06:00
parent b9aad20be6
commit ea1aba4192
4 changed files with 521 additions and 29 deletions

View File

@@ -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);
}
}

View File

@@ -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,
})

View File

@@ -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);