You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
6.5 KiB
175 lines
6.5 KiB
/*
|
|
* ContextMenu recipe.
|
|
*
|
|
* Functionally identical to DropdownMenu — same panel chrome, same
|
|
* item row treatment. Only differences:
|
|
*
|
|
* - Trigger is a `<span>` wrapper (NOT a button) that intercepts
|
|
* `contextmenu`. The recipe gives it no chrome by default; the
|
|
* wrapper is meant to feel inert until invoked.
|
|
* - Anchoring is to the cursor position rather than the trigger's
|
|
* bounding box — soma's floating layer handles this. The visual
|
|
* panel reads identical to DropdownMenu's.
|
|
*
|
|
* To avoid recipe duplication we lean on attribute selectors that
|
|
* cover both `data-dropdown-menu-*` and `data-context-menu-*` for the
|
|
* shared parts (Content, Item, Group, Separator, etc.). The Trigger
|
|
* gets its own rule since its semantics differ.
|
|
*/
|
|
|
|
/* ── Panel (Content / SubContent) ──────────────────────────────────── */
|
|
|
|
[data-context-menu-content],
|
|
[data-context-menu-sub-content] {
|
|
min-inline-size: var(--context-menu-content-min-width, 12rem);
|
|
max-block-size: var(--context-menu-content-max-height, min(60vh, 22rem));
|
|
overflow-y: auto;
|
|
padding: var(--context-menu-content-padding, var(--space-1));
|
|
border: var(--context-menu-content-border-width, 1px) solid
|
|
var(--context-menu-content-border-color, var(--color-border-default));
|
|
border-radius: var(--context-menu-content-radius, var(--radius-md));
|
|
background: var(--context-menu-content-bg, var(--color-surface-overlay));
|
|
box-shadow: var(--context-menu-content-shadow, var(--shadow-overlay));
|
|
font-family: var(--style-label-font-family, var(--font-ui));
|
|
font-size: var(--context-menu-content-font-size, var(--font-size-sm));
|
|
color: var(--color-content-primary);
|
|
}
|
|
|
|
[data-context-menu-content]:focus-visible,
|
|
[data-context-menu-sub-content]:focus-visible {
|
|
outline: none;
|
|
}
|
|
|
|
/* ── Item rows ─────────────────────────────────────────────────────── */
|
|
|
|
[data-context-menu-item],
|
|
[data-context-menu-checkbox-item],
|
|
[data-context-menu-radio-item],
|
|
[data-context-menu-sub-trigger] {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--context-menu-item-gap, var(--space-2));
|
|
min-block-size: var(--context-menu-item-height, 2rem);
|
|
padding-block: var(--context-menu-item-py, var(--space-1));
|
|
padding-inline: var(--context-menu-item-px, var(--space-2));
|
|
border-radius: var(--context-menu-item-radius, var(--radius-sm));
|
|
color: var(--color-content-primary);
|
|
line-height: var(--leading-ui);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition:
|
|
background var(--duration-fast) var(--ease-default),
|
|
color var(--duration-fast) var(--ease-default);
|
|
}
|
|
|
|
[data-context-menu-item]:hover:not([data-disabled]),
|
|
[data-context-menu-checkbox-item]:hover:not([data-disabled]),
|
|
[data-context-menu-radio-item]:hover:not([data-disabled]),
|
|
[data-context-menu-sub-trigger]:hover:not([data-disabled]),
|
|
[data-context-menu-item]:focus-visible,
|
|
[data-context-menu-checkbox-item]:focus-visible,
|
|
[data-context-menu-radio-item]:focus-visible,
|
|
[data-context-menu-sub-trigger]:focus-visible,
|
|
[data-context-menu-sub-trigger][data-state='open'] {
|
|
outline: none;
|
|
background: var(--context-menu-item-bg-hover, var(--color-primary-element));
|
|
color: var(--context-menu-item-fg-hover, var(--color-content-primary));
|
|
}
|
|
|
|
[data-context-menu-item][data-disabled],
|
|
[data-context-menu-checkbox-item][data-disabled],
|
|
[data-context-menu-radio-item][data-disabled],
|
|
[data-context-menu-sub-trigger][data-disabled] {
|
|
cursor: default;
|
|
opacity: var(--context-menu-item-disabled-opacity, 0.55);
|
|
}
|
|
|
|
/* Indicator slot + ::before painting for checkbox-item / radio-item shared
|
|
with dropdown-menu via `lib/menu-indicator.css` (imported by eidos
|
|
entrypoint before this recipe). Override the private vars below if a
|
|
per-component tweak is needed. */
|
|
|
|
[data-context-menu-sub-trigger]::after {
|
|
content: '';
|
|
margin-inline-start: auto;
|
|
inline-size: 0.625rem;
|
|
block-size: 0.625rem;
|
|
border-inline-end: 2px solid currentColor;
|
|
border-block-start: 2px solid currentColor;
|
|
transform: rotate(45deg);
|
|
opacity: 0.65;
|
|
}
|
|
|
|
/* ── Group / GroupHeading / Separator ─────────────────────────────── */
|
|
|
|
[data-context-menu-group] {
|
|
display: contents;
|
|
}
|
|
|
|
[data-context-menu-group-heading] {
|
|
padding-block: var(--context-menu-heading-py, var(--space-1));
|
|
padding-inline: var(--context-menu-heading-px, var(--space-2));
|
|
color: var(--color-content-muted);
|
|
font-size: var(--context-menu-heading-font-size, var(--font-size-xs));
|
|
font-weight: var(--style-label-font-weight, 500);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em; /* literal: micro-tracking for caps */
|
|
}
|
|
|
|
[data-context-menu-separator] {
|
|
margin-block: var(--context-menu-separator-my, var(--space-1));
|
|
block-size: 1px;
|
|
background: var(--context-menu-separator-bg, var(--color-border-subtle));
|
|
}
|
|
|
|
/* ── Trigger ──────────────────────────────────────────────────────── */
|
|
/*
|
|
* Default: no visual treatment. The Trigger is meant to feel inert
|
|
* until invoked. Consumers wrap whatever content they want
|
|
* right-clickable; the recipe stays out of the way.
|
|
*
|
|
* `data-state='open'` (set by soma when the menu is open) gets a
|
|
* subtle outline as a courtesy affordance — the user often forgets
|
|
* which area summoned the menu.
|
|
*/
|
|
|
|
[data-context-menu-trigger] {
|
|
display: inline-block;
|
|
}
|
|
|
|
[data-context-menu-trigger][data-state='open'] {
|
|
outline: 1px dashed var(--color-border-default);
|
|
outline-offset: 2px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
/* ── Arrow ────────────────────────────────────────────────────────── */
|
|
|
|
[data-context-menu-arrow] {
|
|
fill: var(--context-menu-content-bg, var(--color-surface-overlay));
|
|
}
|
|
|
|
/* ── State transitions ────────────────────────────────────────────── */
|
|
|
|
[data-context-menu-content][data-state='closed'],
|
|
[data-context-menu-sub-content][data-state='closed'] {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
[data-context-menu-content][data-state='open'],
|
|
[data-context-menu-sub-content][data-state='open'] {
|
|
animation: context-menu-enter var(--duration-fast) var(--ease-out);
|
|
}
|
|
|
|
@keyframes context-menu-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.96);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
}
|