/* ── Trigger ────────────────────────────────────────────────────────────── * Air relied on the Button recipe via .air-button class. Eidos applies a * baseline button envelope here. Consumes Capa 2 directly (same as Dialog * trigger). Consumers can override per-instance via inline tokens. */ [data-popover-trigger] { display: inline-flex; align-items: center; justify-content: center; height: var(--control-height-md); padding-inline: var(--space-3); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-raised); color: var(--color-content-primary); font: inherit; font-size: var(--popover-trigger-font-size); cursor: pointer; transition: background var(--duration-fast) var(--ease-default), border-color var(--duration-fast) var(--ease-default); } [data-popover-trigger]:hover:not([disabled]):not([data-disabled]) { background: var(--color-surface-overlay); border-color: var(--color-border-strong); } [data-popover-trigger][data-state='open'] { background: var(--color-surface-overlay); border-color: var(--color-border-strong); } [data-popover-trigger]:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset); } /* ── Overlay (optional, modal popovers only) ─────────────────────────── */ [data-popover-overlay] { position: fixed; inset: 0; background: var(--popover-overlay-bg); z-index: var(--popover-overlay-z); } /* The popover's floating wrapper is `position: fixed` but has no explicit * z-index unless the content provides one via `getComputedStyle().zIndex`, * which only works for positioned elements. When modal=true, the overlay * (z=60) would otherwise paint over the wrapper, hiding the panel and * making it unclickable. Soma exposes a public floating wrapper marker for * cross-layer styling, so Eidos does not depend on Soma internals here. */ [data-floating-wrapper]:has(> [data-popover-content]) { z-index: var(--popover-content-z); } [data-popover-overlay][data-state='open'] { animation: popover-overlay-in var(--duration-normal) var(--ease-out) both; } [data-popover-overlay][data-state='closed'] { animation: popover-overlay-out var(--duration-fast) var(--ease-default) both; } @keyframes popover-overlay-in { from { opacity: 0; } } @keyframes popover-overlay-out { to { opacity: 0; } } /* ── Content ──────────────────────────────────────────────────────────── */ /* `size` controls PHYSICAL panel size (padding + width). Typography * stays constant — size and font-size are independent concerns. Override * `--popover-font-size` at recipe / theme level to change body type. */ [data-popover-content] { --_popover-py: var(--popover-padding-y-md); --_popover-px: var(--popover-padding-x-md); --_popover-base-width: var(--popover-min-width-md); --_popover-width: var( --_popover-content-width-override, var(--_popover-match-anchor-width, var(--_popover-base-width)) ); --_popover-max-width: var(--_popover-content-max-width-override, var(--popover-max-width)); --_popover-max-height: var( --_popover-content-max-height-override, var(--soma-popover-content-available-height, calc(100dvh - var(--space-4))) ); display: grid; box-sizing: border-box; gap: var(--popover-gap); inline-size: min(var(--_popover-width), var(--_popover-max-width), calc(100vw - var(--space-4))); min-inline-size: var(--_popover-content-min-width-override, auto); max-inline-size: min(var(--_popover-max-width), calc(100vw - var(--space-4))); block-size: var(--_popover-content-height-override, auto); min-block-size: var(--_popover-content-min-height-override, auto); max-block-size: var(--_popover-max-height); padding: var(--_popover-py) var(--_popover-px); overflow: auto; border: var(--popover-border-width) solid var(--popover-border); border-radius: var(--popover-radius); background: var(--popover-bg); color: var(--popover-color); box-shadow: var(--popover-shadow); transform-origin: var(--soma-popover-content-transform-origin, center); font-family: var(--popover-font-family); font-size: var(--popover-font-size); font-weight: var(--popover-font-weight); line-height: var(--popover-line-height); outline: none; } [data-popover-content][data-size='sm'] { --_popover-py: var(--popover-padding-y-sm); --_popover-px: var(--popover-padding-x-sm); --_popover-base-width: var(--popover-min-width-sm); } [data-popover-content][data-size='lg'] { --_popover-py: var(--popover-padding-y-lg); --_popover-px: var(--popover-padding-x-lg); --_popover-base-width: var(--popover-min-width-lg); } [data-popover-content][data-size='xl'] { --_popover-py: var(--popover-padding-y-xl); --_popover-px: var(--popover-padding-x-xl); --_popover-base-width: var(--popover-min-width-xl); } /* ── Content entrance/exit per anchored side ───────────────────────────── * Soma's Floating layer writes `data-side` (top/right/bottom/left) on * Content based on the resolved placement. The popover slides into place * FROM the trigger edge (i.e., 'top' side = popover above the trigger, * slides UP from below). * * Air calls this 'revelation'. ~180ms entrance / 120ms exit. Fade + small * translate (4px) + scale (0.985 → 1). */ [data-popover-content][data-state='open'][data-side='top'] { animation: popover-in-top var(--duration-normal) var(--ease-out) both; } [data-popover-content][data-state='closed'][data-side='top'] { animation: popover-out-top var(--duration-fast) var(--ease-default) both; } [data-popover-content][data-state='open'][data-side='bottom'] { animation: popover-in-bottom var(--duration-normal) var(--ease-out) both; } [data-popover-content][data-state='closed'][data-side='bottom'] { animation: popover-out-bottom var(--duration-fast) var(--ease-default) both; } [data-popover-content][data-state='open'][data-side='right'] { animation: popover-in-right var(--duration-normal) var(--ease-out) both; } [data-popover-content][data-state='closed'][data-side='right'] { animation: popover-out-right var(--duration-fast) var(--ease-default) both; } [data-popover-content][data-state='open'][data-side='left'] { animation: popover-in-left var(--duration-normal) var(--ease-out) both; } [data-popover-content][data-state='closed'][data-side='left'] { animation: popover-out-left var(--duration-fast) var(--ease-default) both; } @keyframes popover-in-top { from { opacity: 0; transform: translateY(4px) scale(0.985); } } @keyframes popover-out-top { to { opacity: 0; transform: translateY(4px) scale(0.985); } } @keyframes popover-in-bottom { from { opacity: 0; transform: translateY(-4px) scale(0.985); } } @keyframes popover-out-bottom { to { opacity: 0; transform: translateY(-4px) scale(0.985); } } @keyframes popover-in-right { from { opacity: 0; transform: translateX(-4px) scale(0.985); } } @keyframes popover-out-right { to { opacity: 0; transform: translateX(-4px) scale(0.985); } } @keyframes popover-in-left { from { opacity: 0; transform: translateX(4px) scale(0.985); } } @keyframes popover-out-left { to { opacity: 0; transform: translateX(4px) scale(0.985); } } /* ── Arrow (optional) ───────────────────────────────────────────────────── * Soma's FloatingArrow renders `` with inline * positioning styles + an SVG triangle inside. The fill and the outline are * split so the border is drawn only on the two exposed sides; drawing a stroke * on the polygon itself also paints the base edge and makes the arrow look * detached from the panel. * * The SVG uses `viewBox="0 0 30 10"` with `preserveAspectRatio="none"`, so * stroke widths declared in user units get scaled to sub-pixel values (the * X scale is 10/30 = 0.33). `vector-effect: non-scaling-stroke` keeps the * stroke at device-pixel width regardless of the viewBox transform — without * it the border outline of the arrow vanishes against the page background. * * `overflow: visible` on the SVG lets the stroke extend slightly past the * viewBox bounds; otherwise the half of the stroke that sits outside gets * clipped. */ [data-popover-arrow] svg { display: block; overflow: visible; } [data-popover-arrow] polygon { fill: var(--popover-bg); stroke: none; } [data-popover-arrow] path { stroke: var(--popover-border); stroke-width: var(--popover-border-width); stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; } /* ── Close button ────────────────────────────────────────────────────── * Default sizing is auto-fit (text labels like "Close" or "Dismiss" fit * naturally). For icon-only close buttons (× glyph), the consumer sets * `data-icon-only` on `` to switch to a fixed square. */ [data-popover-close] { display: inline-flex; align-items: center; justify-content: center; min-block-size: var(--popover-close-size); padding-inline: var(--space-3); border: var(--popover-close-border-width) solid var(--popover-close-border); border-radius: var(--popover-close-radius); background: var(--popover-close-bg); color: var(--popover-close-color); box-shadow: none; font: inherit; font-size: var(--popover-close-font-size); cursor: pointer; transition: background var(--popover-transition-duration) var(--popover-transition-ease), border-color var(--popover-transition-duration) var(--popover-transition-ease), color var(--popover-transition-duration) var(--popover-transition-ease), box-shadow var(--popover-transition-duration) var(--popover-transition-ease); } [data-popover-close][data-icon-only] { inline-size: var(--popover-close-size); block-size: var(--popover-close-size); padding: 0; } [data-popover-close]:hover { background: var(--popover-close-bg-hover); border-color: var(--popover-close-border-hover); } [data-popover-close]:focus-visible { outline: none; box-shadow: var(--focus-ring); } /* ── data-last-action exit tinting (mirrors Dialog/Drawer) ────────────── * Prewrite writes `data-last-action` BEFORE state flips to 'closed'. * Tint the trigger-facing edge (opposite to data-side) during the exit * animation so the user gets a brief tinted flash by causa of the close. */ [data-popover-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom: 2px solid var(--color-fulfill-element); } [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top: 2px solid var(--color-fulfill-element); } [data-popover-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left: 2px solid var(--color-fulfill-element); } [data-popover-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right: 2px solid var(--color-fulfill-element); } [data-popover-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom: 2px solid var(--color-threat-element); } [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top: 2px solid var(--color-threat-element); } [data-popover-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left: 2px solid var(--color-threat-element); } [data-popover-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right: 2px solid var(--color-threat-element); } [data-popover-content][data-state='closed'][data-side='top'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed'], [data-popover-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom: 2px solid var(--color-neutral-element); } [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed'], [data-popover-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top: 2px solid var(--color-neutral-element); } [data-popover-content][data-state='closed'][data-side='right'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed'], [data-popover-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left: 2px solid var(--color-neutral-element); } [data-popover-content][data-state='closed'][data-side='left'][data-last-action='cancelled'], [data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed'], [data-popover-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right: 2px solid var(--color-neutral-element); }