|
|
/* ── 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 `<span data-popover-arrow>` 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 `<Popover.Close>` 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); }
|