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.
489 lines
19 KiB
489 lines
19 KiB
/* ── Trigger ────────────────────────────────────────────────────────────── */
|
|
|
|
[data-drawer-trigger] {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--space-2);
|
|
min-block-size: var(--control-height-md);
|
|
padding-inline: var(--space-3);
|
|
border: var(--border-width) solid var(--color-border-default);
|
|
border-radius: var(--radius-md);
|
|
background: var(--color-surface-raised);
|
|
color: var(--color-content-primary);
|
|
font: inherit;
|
|
font-weight: var(--font-weight-medium);
|
|
cursor: pointer;
|
|
}
|
|
|
|
[data-drawer-trigger]:hover {
|
|
background: var(--color-surface-overlay);
|
|
}
|
|
|
|
[data-drawer-trigger]:focus-visible {
|
|
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
outline-offset: var(--focus-ring-offset);
|
|
}
|
|
|
|
[data-drawer-trigger]:disabled,
|
|
[data-drawer-trigger][data-disabled] {
|
|
cursor: default;
|
|
opacity: var(--opacity-disabled);
|
|
}
|
|
|
|
/* ── Overlay ────────────────────────────────────────────────────────────── */
|
|
|
|
/* Default: hidden. Only `variant='overlay'` paints the backdrop — the
|
|
* `'inline'` and `'persistent'` variants don't have a backdrop concept.
|
|
* The wrapper skips Overlay entirely in those modes; this rule guards
|
|
* the case where a consumer renders `<Drawer.Overlay />` explicitly
|
|
* inside the compound API with a non-overlay variant. */
|
|
[data-drawer-overlay] {
|
|
display: none;
|
|
}
|
|
|
|
[data-drawer-overlay][data-variant='overlay'] {
|
|
display: block;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: color-mix(
|
|
in srgb,
|
|
var(--drawer-overlay-bg) var(--drawer-overlay-opacity),
|
|
transparent
|
|
);
|
|
backdrop-filter: blur(var(--drawer-overlay-blur));
|
|
z-index: var(--drawer-overlay-z);
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Overlay enter/exit motion comes from the motion system (preset `fade` via
|
|
* `data-animation-style`). The overlay is `display:none` for inline/persistent,
|
|
* so the fade only shows for `variant='overlay'`. Timing preserved via override. */
|
|
[data-drawer-overlay] {
|
|
--motion-duration-enter: var(--duration-normal);
|
|
--motion-duration-exit: var(--duration-normal);
|
|
}
|
|
|
|
/* ── Content (variant-shared base) ──────────────────────────────────────── */
|
|
|
|
[data-drawer-content] {
|
|
--_drawer-size-width: var(--drawer-content-width-md);
|
|
--_drawer-size-height: var(--drawer-content-height-md);
|
|
--_drawer-panel-width: var(--_drawer-content-width-override, var(--_drawer-size-width));
|
|
--_drawer-panel-height: var(--_drawer-content-height-override, var(--_drawer-size-height));
|
|
--_drawer-padding: var(--drawer-content-padding-md);
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--drawer-stack-gap);
|
|
padding: var(--_drawer-padding);
|
|
overflow: auto;
|
|
min-inline-size: var(--_drawer-content-min-width-override, auto);
|
|
max-inline-size: var(--_drawer-content-max-width-override, none);
|
|
min-block-size: var(--_drawer-content-min-height-override, auto);
|
|
max-block-size: var(--_drawer-content-max-height-override, none);
|
|
/* Elevation bundle (surface · border · shadow) from the `modal` plane via
|
|
`data-depth` (A1, Decisión 8). Keeps its own radius/color; the
|
|
[data-dragging] box-shadow override below still wins while dragging. */
|
|
border-radius: var(--drawer-content-radius);
|
|
color: var(--drawer-content-color);
|
|
outline: none;
|
|
}
|
|
|
|
/* Drag-state visual feedback. While the gesture is in flight (`data-dragging`
|
|
* is set on Content by the runtime from the gesture state), the panel lifts
|
|
* with a stronger shadow and gains a subtle outline ring. Animation is short
|
|
* (no jank) and additive — does not alter layout. */
|
|
[data-drawer-content][data-dragging] {
|
|
box-shadow: var(--drawer-content-shadow-dragging);
|
|
outline: var(--drawer-content-ring-dragging-width) solid var(--drawer-content-ring-dragging-color);
|
|
outline-offset: var(--drawer-content-ring-dragging-offset);
|
|
}
|
|
|
|
/* Reserved slot for exit tint on the leading edge — colored when the
|
|
* morfo's prewrite has written `data-last-action` and `data-state` flips
|
|
* to 'closed'. The leading edge depends on side: top→bottom, right→left,
|
|
* etc. (the edge facing the user before closing). Only meaningful in
|
|
* overlay/inline variants where the drawer animates out. */
|
|
[data-drawer-content][data-side='right'] {
|
|
border-left: var(--border-width-thick) solid transparent;
|
|
}
|
|
[data-drawer-content][data-side='left'] {
|
|
border-right: var(--border-width-thick) solid transparent;
|
|
}
|
|
[data-drawer-content][data-side='top'] {
|
|
border-bottom: var(--border-width-thick) solid transparent;
|
|
}
|
|
[data-drawer-content][data-side='bottom'] {
|
|
border-top: var(--border-width-thick) solid transparent;
|
|
}
|
|
|
|
/* ── Content sizing (variant-shared) ────────────────────────────────────── */
|
|
|
|
[data-drawer-content][data-size='sm'] {
|
|
--_drawer-size-width: var(--drawer-content-width-sm);
|
|
--_drawer-size-height: var(--drawer-content-height-sm);
|
|
--_drawer-padding: var(--drawer-content-padding-sm);
|
|
}
|
|
|
|
[data-drawer-content][data-size='md'] {
|
|
--_drawer-size-width: var(--drawer-content-width-md);
|
|
--_drawer-size-height: var(--drawer-content-height-md);
|
|
--_drawer-padding: var(--drawer-content-padding-md);
|
|
}
|
|
|
|
[data-drawer-content][data-size='lg'] {
|
|
--_drawer-size-width: var(--drawer-content-width-lg);
|
|
--_drawer-size-height: var(--drawer-content-height-lg);
|
|
--_drawer-padding: var(--drawer-content-padding-lg);
|
|
}
|
|
|
|
[data-drawer-content][data-size='full'] {
|
|
--_drawer-padding: var(--drawer-content-padding-full);
|
|
}
|
|
|
|
/* ── Variant 'overlay' — fixed positioning + backdrop stack ─────────────── */
|
|
|
|
[data-drawer-content][data-variant='overlay'] {
|
|
position: fixed;
|
|
z-index: calc(var(--drawer-overlay-z) + 1);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='overlay'][data-side='right'] {
|
|
top: var(--drawer-content-inset);
|
|
right: var(--drawer-content-inset);
|
|
bottom: var(--drawer-content-inset);
|
|
width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width));
|
|
}
|
|
|
|
[data-drawer-content][data-variant='overlay'][data-side='left'] {
|
|
top: var(--drawer-content-inset);
|
|
left: var(--drawer-content-inset);
|
|
bottom: var(--drawer-content-inset);
|
|
width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width));
|
|
}
|
|
|
|
[data-drawer-content][data-variant='overlay'][data-side='top'] {
|
|
top: var(--drawer-content-inset);
|
|
left: var(--drawer-content-inset);
|
|
right: var(--drawer-content-inset);
|
|
height: min(
|
|
calc(100dvh - (var(--drawer-content-inset) * 2)),
|
|
var(--_drawer-panel-height)
|
|
);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='overlay'][data-side='bottom'] {
|
|
right: var(--drawer-content-inset);
|
|
bottom: var(--drawer-content-inset);
|
|
left: var(--drawer-content-inset);
|
|
height: min(
|
|
calc(100dvh - (var(--drawer-content-inset) * 2)),
|
|
var(--_drawer-panel-height)
|
|
);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='overlay'][data-size='full'] {
|
|
inset: var(--drawer-content-inset);
|
|
width: var(--_drawer-content-width-override, auto);
|
|
height: var(--_drawer-content-height-override, auto);
|
|
}
|
|
|
|
/* ── Variant 'inline' — fixed positioning, no backdrop, lower stack ─────── */
|
|
|
|
/* Same geometry as overlay but a separate z-index band so it doesn't
|
|
* fight a real modal that may also be on screen. The drawer sits over
|
|
* content (background remains interactive — soma drops the focus trap
|
|
* and dismissal layers in this variant). */
|
|
[data-drawer-content][data-variant='inline'] {
|
|
position: fixed;
|
|
z-index: var(--drawer-inline-z);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='inline'][data-side='right'] {
|
|
top: var(--drawer-content-inset);
|
|
right: var(--drawer-content-inset);
|
|
bottom: var(--drawer-content-inset);
|
|
width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width));
|
|
}
|
|
|
|
[data-drawer-content][data-variant='inline'][data-side='left'] {
|
|
top: var(--drawer-content-inset);
|
|
left: var(--drawer-content-inset);
|
|
bottom: var(--drawer-content-inset);
|
|
width: min(calc(100vw - (var(--drawer-content-inset) * 2)), var(--_drawer-panel-width));
|
|
}
|
|
|
|
[data-drawer-content][data-variant='inline'][data-side='top'] {
|
|
top: var(--drawer-content-inset);
|
|
left: var(--drawer-content-inset);
|
|
right: var(--drawer-content-inset);
|
|
height: min(
|
|
calc(100dvh - (var(--drawer-content-inset) * 2)),
|
|
var(--_drawer-panel-height)
|
|
);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='inline'][data-side='bottom'] {
|
|
right: var(--drawer-content-inset);
|
|
bottom: var(--drawer-content-inset);
|
|
left: var(--drawer-content-inset);
|
|
height: min(
|
|
calc(100dvh - (var(--drawer-content-inset) * 2)),
|
|
var(--_drawer-panel-height)
|
|
);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='inline'][data-size='full'] {
|
|
inset: var(--drawer-content-inset);
|
|
width: var(--_drawer-content-width-override, auto);
|
|
height: var(--_drawer-content-height-override, auto);
|
|
}
|
|
|
|
/* ── Variant 'persistent' — in flow, no insets ──────────────────────────── */
|
|
|
|
/* Persistent drawer takes its slot in a parent flex/grid layout (the
|
|
* consumer's responsibility). The recipe just sizes the panel along
|
|
* the drawer's main axis; the cross axis fills the parent (`100%`). */
|
|
[data-drawer-content][data-variant='persistent'] {
|
|
position: relative;
|
|
border-radius: 0;
|
|
}
|
|
|
|
[data-drawer-content][data-variant='persistent'][data-side='left'],
|
|
[data-drawer-content][data-variant='persistent'][data-side='right'] {
|
|
width: var(--_drawer-panel-width);
|
|
block-size: 100%;
|
|
}
|
|
|
|
[data-drawer-content][data-variant='persistent'][data-side='top'],
|
|
[data-drawer-content][data-variant='persistent'][data-side='bottom'] {
|
|
inline-size: 100%;
|
|
height: var(--_drawer-panel-height);
|
|
}
|
|
|
|
[data-drawer-content][data-variant='persistent'][data-size='full'] {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* ── Presence motion (overlay + inline) ─────────────────────────────────
|
|
* Driven by the motion system: the eidos wrapper writes
|
|
* `data-animation-style='slide-full'` and the registry's side-aware preset
|
|
* (full slide, keyed on `data-side`) animates enter/exit. The panel is INSET
|
|
* by `--drawer-content-inset`, so it must travel its own size PLUS that gap to
|
|
* fully clear the edge — `--motion-slide-leave` feeds the preset that extra.
|
|
* See `eidos-motion.md` (momento --state). */
|
|
[data-drawer-content] {
|
|
--motion-slide-leave: var(--drawer-content-inset);
|
|
--motion-duration-enter: var(--duration-moderate);
|
|
--motion-duration-exit: var(--duration-normal);
|
|
}
|
|
|
|
/* `persistent` is always present — it opts out of the presence animation. */
|
|
[data-drawer-content][data-variant='persistent'] {
|
|
animation: none !important;
|
|
}
|
|
|
|
/* ── data-last-action exit tinting (overlay + inline only) ──────────────── */
|
|
|
|
[data-drawer-content][data-state='closed'][data-side='right'][data-last-action='saved'] { border-left-color: var(--color-fulfill-element); }
|
|
[data-drawer-content][data-state='closed'][data-side='left'][data-last-action='saved'] { border-right-color: var(--color-fulfill-element); }
|
|
[data-drawer-content][data-state='closed'][data-side='top'][data-last-action='saved'] { border-bottom-color: var(--color-fulfill-element); }
|
|
[data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='saved'] { border-top-color: var(--color-fulfill-element); }
|
|
|
|
[data-drawer-content][data-state='closed'][data-side='right'][data-last-action='failed'] { border-left-color: var(--color-threat-element); }
|
|
[data-drawer-content][data-state='closed'][data-side='left'][data-last-action='failed'] { border-right-color: var(--color-threat-element); }
|
|
[data-drawer-content][data-state='closed'][data-side='top'][data-last-action='failed'] { border-bottom-color: var(--color-threat-element); }
|
|
[data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='failed'] { border-top-color: var(--color-threat-element); }
|
|
|
|
[data-drawer-content][data-state='closed'][data-side='right'][data-last-action='cancelled'],
|
|
[data-drawer-content][data-state='closed'][data-side='right'][data-last-action='dismissed'],
|
|
[data-drawer-content][data-state='closed'][data-side='right'][data-last-action='dismissed-outside'] { border-left-color: var(--color-neutral-element); }
|
|
|
|
[data-drawer-content][data-state='closed'][data-side='left'][data-last-action='cancelled'],
|
|
[data-drawer-content][data-state='closed'][data-side='left'][data-last-action='dismissed'],
|
|
[data-drawer-content][data-state='closed'][data-side='left'][data-last-action='dismissed-outside'] { border-right-color: var(--color-neutral-element); }
|
|
|
|
[data-drawer-content][data-state='closed'][data-side='top'][data-last-action='cancelled'],
|
|
[data-drawer-content][data-state='closed'][data-side='top'][data-last-action='dismissed'],
|
|
[data-drawer-content][data-state='closed'][data-side='top'][data-last-action='dismissed-outside'] { border-bottom-color: var(--color-neutral-element); }
|
|
|
|
[data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='cancelled'],
|
|
[data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed'],
|
|
[data-drawer-content][data-state='closed'][data-side='bottom'][data-last-action='dismissed-outside'] { border-top-color: var(--color-neutral-element); }
|
|
|
|
/* ── Handle (canon part — air didn't ship a handle) ─────────────────────── */
|
|
|
|
/* The handle is a small grab bar at the leading edge of the drawer
|
|
* (typically the only edge meant to be dragged). Orientation flips
|
|
* with `data-side`: horizontal bar for top/bottom (sheet pattern),
|
|
* vertical bar for left/right. Snap-point cycling handled by soma. */
|
|
[data-drawer-handle] {
|
|
flex: 0 0 auto;
|
|
background: var(--drawer-handle-bg);
|
|
border-radius: var(--drawer-handle-radius);
|
|
cursor: grab;
|
|
touch-action: none;
|
|
transition:
|
|
background var(--drawer-handle-transition-duration) var(--drawer-handle-transition-ease),
|
|
transform var(--drawer-handle-transition-duration) var(--drawer-handle-transition-ease);
|
|
}
|
|
|
|
[data-drawer-handle]:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
/* Active feedback while the gesture is live. `data-dragging` lives on
|
|
* Content (set by the runtime from the Gesture state) — descendant
|
|
* selector reaches into the handle. Brighter color + small bump signals
|
|
* that the handle is the load-bearing element of the gesture. */
|
|
[data-drawer-content][data-dragging] [data-drawer-handle] {
|
|
background: var(--drawer-handle-bg-active);
|
|
cursor: grabbing;
|
|
}
|
|
|
|
/* Cursor persistence during drag: the Gesture layer captures the pointer,
|
|
* which removes the handle from the element-under-cursor → `:active` is
|
|
* lost and the cursor reverts to default. Hold `grabbing` on the entire
|
|
* Content while data-dragging is on, AND on `body` via `:has()` so that
|
|
* drags extending past the drawer edges still show the right cursor. */
|
|
[data-drawer-content][data-dragging] {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
body:has([data-drawer-content][data-dragging]) {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
/* Bump axis depends on which dimension is the "thickness". For top/bottom
|
|
* drawers the handle is horizontal — scale Y; for left/right it's vertical
|
|
* — scale X. */
|
|
[data-drawer-content][data-dragging][data-side='top'] [data-drawer-handle],
|
|
[data-drawer-content][data-dragging][data-side='bottom'] [data-drawer-handle] {
|
|
transform: scaleY(var(--drawer-handle-active-scale));
|
|
}
|
|
|
|
[data-drawer-content][data-dragging][data-side='left'] [data-drawer-handle],
|
|
[data-drawer-content][data-dragging][data-side='right'] [data-drawer-handle] {
|
|
transform: translateY(-50%) scaleX(var(--drawer-handle-active-scale));
|
|
}
|
|
|
|
/* For top/bottom drawers: horizontal grabber centered on the leading edge */
|
|
[data-drawer-content][data-side='top'] [data-drawer-handle],
|
|
[data-drawer-content][data-side='bottom'] [data-drawer-handle] {
|
|
width: var(--drawer-handle-length);
|
|
height: var(--drawer-handle-thickness);
|
|
margin-inline: auto;
|
|
}
|
|
|
|
/* For top: handle sits at the bottom edge (the "pulled" edge)
|
|
* For bottom: at the top edge */
|
|
[data-drawer-content][data-side='top'] [data-drawer-handle] {
|
|
order: 999;
|
|
margin-block-start: auto;
|
|
}
|
|
[data-drawer-content][data-side='bottom'] [data-drawer-handle] {
|
|
order: -1;
|
|
margin-block-end: auto;
|
|
}
|
|
|
|
/* For left/right drawers: vertical grabber */
|
|
[data-drawer-content][data-side='left'] [data-drawer-handle],
|
|
[data-drawer-content][data-side='right'] [data-drawer-handle] {
|
|
width: var(--drawer-handle-thickness);
|
|
height: var(--drawer-handle-length);
|
|
margin-block: auto;
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
[data-drawer-content][data-side='left'] [data-drawer-handle] {
|
|
right: var(--space-2);
|
|
}
|
|
[data-drawer-content][data-side='right'] [data-drawer-handle] {
|
|
left: var(--space-2);
|
|
}
|
|
|
|
/* ── Header / Footer / Title / Description ──────────────────────────────── */
|
|
|
|
[data-drawer-header] {
|
|
display: flex;
|
|
align-items: start;
|
|
justify-content: space-between;
|
|
gap: var(--drawer-header-gap);
|
|
}
|
|
|
|
[data-drawer-footer] {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: end;
|
|
gap: var(--drawer-footer-gap);
|
|
margin-top: var(--drawer-footer-margin-top);
|
|
}
|
|
|
|
[data-drawer-title] {
|
|
margin: 0;
|
|
font-family: var(--drawer-title-font-family);
|
|
font-size: var(--drawer-title-font-size);
|
|
font-weight: var(--drawer-title-font-weight);
|
|
line-height: var(--drawer-title-line-height);
|
|
color: var(--drawer-title-color);
|
|
text-wrap: balance;
|
|
}
|
|
|
|
[data-drawer-description] {
|
|
margin: 0;
|
|
font-family: var(--drawer-description-font-family);
|
|
font-size: var(--drawer-description-font-size);
|
|
line-height: var(--drawer-description-line-height);
|
|
color: var(--drawer-description-color);
|
|
}
|
|
|
|
/* ── Close button ───────────────────────────────────────────────────────── */
|
|
|
|
[data-drawer-close] {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
min-inline-size: var(--drawer-close-size);
|
|
block-size: var(--drawer-close-size);
|
|
padding-inline: var(--drawer-close-px);
|
|
border: var(--drawer-close-border-width) solid var(--drawer-close-border);
|
|
border-radius: var(--drawer-close-radius);
|
|
background: var(--drawer-close-bg);
|
|
color: var(--drawer-close-color);
|
|
cursor: pointer;
|
|
transition:
|
|
background var(--drawer-close-transition-duration) var(--drawer-close-transition-ease),
|
|
color var(--drawer-close-transition-duration) var(--drawer-close-transition-ease),
|
|
border-color var(--drawer-close-transition-duration) var(--drawer-close-transition-ease),
|
|
box-shadow var(--drawer-close-transition-duration) var(--drawer-close-transition-ease);
|
|
}
|
|
|
|
[data-drawer-close][data-icon-only] {
|
|
inline-size: var(--drawer-close-size);
|
|
padding-inline: 0;
|
|
}
|
|
|
|
[data-drawer-close]:hover {
|
|
background: var(--drawer-close-bg-hover);
|
|
color: var(--drawer-close-color-hover);
|
|
}
|
|
|
|
[data-drawer-close]:focus-visible {
|
|
outline: none;
|
|
box-shadow:
|
|
0 0 0 var(--drawer-close-focus-ring-offset) var(--drawer-content-bg),
|
|
0 0 0 calc(var(--drawer-close-focus-ring-offset) + var(--drawer-close-focus-ring-width))
|
|
var(--drawer-close-focus-ring-color);
|
|
}
|
|
|
|
[data-drawer-close]:disabled,
|
|
[data-drawer-close][data-disabled] {
|
|
cursor: default;
|
|
opacity: var(--drawer-close-disabled-opacity);
|
|
}
|