/* * ContextMenu recipe. * * Functionally identical to DropdownMenu — same panel chrome, same * item row treatment. Only differences: * * - Trigger is a `` 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; } /* Event-driven item cascade rhythm — the Content wrapper marks itself `[data-stagger]`; each direct-child item carries `data-animation-style` and gets its structural index from `:nth-child`. `var(--motion-stagger)` is the canonical 20ms (0 = parallel). Same mechanism as DropdownMenu; the appearance flows from the `open` (emerge) event — no JS writes the index. Applies to the root panel AND fly-out SubContent (both mark themselves `[data-stagger]`). */ [data-context-menu-content], [data-context-menu-sub-content] { --motion-stagger-each: var(--motion-stagger); } /* ── 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: var(--border-width-medium) solid currentColor; border-block-start: var(--border-width-medium) solid currentColor; transform: rotate(45deg); opacity: var(--opacity-muted); } /* ── 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: var(--tracking-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; } }