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.
335 lines
12 KiB
335 lines
12 KiB
/**
|
|
* Eidos — Toast component recipe.
|
|
*
|
|
* Translated from `air/components/toast/toast.css` with substantive
|
|
* remapping: air's part naming and intent vocabulary differ from the
|
|
* Toast morfo. Air's token model still maps to the same conceptual parts, so
|
|
* we keep the values and rebind the selectors.
|
|
*
|
|
* Selector remap:
|
|
* air `[data-toast-toaster]` → eidos `[data-toast-viewport]`
|
|
* air `[data-toast]` (the card) → eidos `[data-toast-item]`
|
|
* air `[data-toast-message]` → DROPPED (soma has Title + Description
|
|
* directly; no separate "message" part)
|
|
* air `.air-toast-status` → DROPPED (no status icon part)
|
|
* air `.air-toast-main` → DROPPED (no content wrapper part)
|
|
*
|
|
* Intent vocabulary remap (air `data-type` ↔ morfo `data-intent`):
|
|
* air `error` → eidos `threat`
|
|
* air `warning` → eidos `risk`
|
|
* air `success` → eidos `fulfill`
|
|
* air `info` → eidos `neutral`
|
|
* no air equiv → eidos `affirm` (added below as success-tinted)
|
|
*
|
|
* `data-position` on viewport is a-la-carte: the morfo doesn't declare
|
|
* it, but a consumer can add it to enable air's positioning rules.
|
|
*/
|
|
|
|
/* ── Viewport ─────────────────────────────────────────────────────────── */
|
|
|
|
[data-toast-viewport] {
|
|
position: fixed;
|
|
z-index: var(--toast-toaster-z);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--toast-toaster-gap);
|
|
inline-size: min(
|
|
calc(100vw - (var(--toast-toaster-inset) * 2)),
|
|
var(--toast-toaster-width)
|
|
);
|
|
pointer-events: none;
|
|
}
|
|
|
|
[data-toast-viewport][data-position='top-left'] {
|
|
top: var(--toast-toaster-inset);
|
|
left: var(--toast-toaster-inset);
|
|
}
|
|
|
|
[data-toast-viewport][data-position='top-center'] {
|
|
top: var(--toast-toaster-inset);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
[data-toast-viewport][data-position='top-right'] {
|
|
top: var(--toast-toaster-inset);
|
|
right: var(--toast-toaster-inset);
|
|
}
|
|
|
|
[data-toast-viewport][data-position='bottom-left'] {
|
|
bottom: var(--toast-toaster-inset);
|
|
left: var(--toast-toaster-inset);
|
|
}
|
|
|
|
[data-toast-viewport][data-position='bottom-center'] {
|
|
bottom: var(--toast-toaster-inset);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
[data-toast-viewport][data-position='bottom-right'] {
|
|
right: var(--toast-toaster-inset);
|
|
bottom: var(--toast-toaster-inset);
|
|
}
|
|
|
|
/* ── Item (the card itself) ──────────────────────────────────────────── */
|
|
|
|
[data-toast-item] {
|
|
--_toast-surface: var(--toast-default-surface);
|
|
--_toast-border: var(--toast-default-border);
|
|
--_toast-accent: var(--toast-default-accent);
|
|
--_toast-status-bg: var(--toast-default-status-bg);
|
|
--_toast-status-color: var(--toast-default-status-color);
|
|
--_toast-action-color: var(--toast-default-action-color);
|
|
--_toast-action-hover-bg: var(--toast-default-action-hover-bg);
|
|
|
|
inline-size: min(100%, var(--toast-provider-max-width));
|
|
min-inline-size: min(100%, var(--toast-provider-min-width));
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
align-items: start;
|
|
gap: var(--toast-provider-gap);
|
|
padding: var(--toast-provider-py) var(--toast-provider-px);
|
|
border: var(--toast-provider-border-width) solid var(--_toast-border);
|
|
border-inline-start: var(--toast-accent-width) solid var(--_toast-accent);
|
|
border-radius: var(--toast-provider-radius);
|
|
background: var(--_toast-surface);
|
|
box-shadow: var(--toast-provider-shadow);
|
|
color: var(--toast-message-color);
|
|
pointer-events: auto;
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
transition:
|
|
opacity var(--toast-transition-duration) var(--toast-transition-ease),
|
|
transform var(--toast-transition-duration) var(--toast-transition-ease);
|
|
}
|
|
|
|
/* Auto-collapse the 3-column grid based on which side parts are present.
|
|
* Status icon on the leading side, Close on the trailing side; if either
|
|
* is missing, the main column expands. */
|
|
[data-toast-item]:not(:has([data-toast-status])):not(:has([data-toast-close])) {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
[data-toast-item]:not(:has([data-toast-status])):has([data-toast-close]) {
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
}
|
|
[data-toast-item]:has([data-toast-status]):not(:has([data-toast-close])) {
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|
}
|
|
|
|
/* Toast intent recipes — each maps to its doctrinal palette directly. */
|
|
[data-toast-item][data-intent='affirm'] {
|
|
--_toast-surface: var(--toast-affirm-surface);
|
|
--_toast-border: var(--toast-affirm-border);
|
|
--_toast-accent: var(--toast-affirm-accent);
|
|
--_toast-status-bg: var(--toast-affirm-status-bg);
|
|
--_toast-status-color: var(--toast-affirm-status-color);
|
|
--_toast-action-color: var(--toast-affirm-action-color);
|
|
--_toast-action-hover-bg: var(--toast-affirm-action-hover-bg);
|
|
}
|
|
|
|
[data-toast-item][data-intent='fulfill'] {
|
|
--_toast-surface: var(--toast-fulfill-surface);
|
|
--_toast-border: var(--toast-fulfill-border);
|
|
--_toast-accent: var(--toast-fulfill-accent);
|
|
--_toast-status-bg: var(--toast-fulfill-status-bg);
|
|
--_toast-status-color: var(--toast-fulfill-status-color);
|
|
--_toast-action-color: var(--toast-fulfill-action-color);
|
|
--_toast-action-hover-bg: var(--toast-fulfill-action-hover-bg);
|
|
}
|
|
|
|
[data-toast-item][data-intent='risk'] {
|
|
--_toast-surface: var(--toast-risk-surface);
|
|
--_toast-border: var(--toast-risk-border);
|
|
--_toast-accent: var(--toast-risk-accent);
|
|
--_toast-status-bg: var(--toast-risk-status-bg);
|
|
--_toast-status-color: var(--toast-risk-status-color);
|
|
--_toast-action-color: var(--toast-risk-action-color);
|
|
--_toast-action-hover-bg: var(--toast-risk-action-hover-bg);
|
|
}
|
|
|
|
[data-toast-item][data-intent='threat'] {
|
|
--_toast-surface: var(--toast-threat-surface);
|
|
--_toast-border: var(--toast-threat-border);
|
|
--_toast-accent: var(--toast-threat-accent);
|
|
--_toast-status-bg: var(--toast-threat-status-bg);
|
|
--_toast-status-color: var(--toast-threat-status-color);
|
|
--_toast-action-color: var(--toast-threat-action-color);
|
|
--_toast-action-hover-bg: var(--toast-threat-action-hover-bg);
|
|
}
|
|
|
|
[data-toast-item][data-intent='loss'] {
|
|
--_toast-surface: var(--toast-loss-surface);
|
|
--_toast-border: var(--toast-loss-border);
|
|
--_toast-accent: var(--toast-loss-accent);
|
|
--_toast-status-bg: var(--toast-loss-status-bg);
|
|
--_toast-status-color: var(--toast-loss-status-color);
|
|
--_toast-action-color: var(--toast-loss-action-color);
|
|
--_toast-action-hover-bg: var(--toast-loss-action-hover-bg);
|
|
}
|
|
|
|
/* neutral default — uses neutral palette (no override needed unless
|
|
* the recipe wants a specific neutral toast styling). */
|
|
|
|
[data-toast-item][data-state='closed'] {
|
|
opacity: 0;
|
|
transform: translateY(var(--toast-closed-offset)) scale(var(--toast-closed-scale));
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Loading state — subtle pulse on the accent stripe so the user reads
|
|
* "still working" without the toast competing for attention. */
|
|
[data-toast-item][data-loading] {
|
|
border-inline-start-color: var(--_toast-status-color);
|
|
animation: toast-loading-pulse 1.4s ease-in-out infinite;
|
|
}
|
|
@keyframes toast-loading-pulse {
|
|
0%, 100% { border-inline-start-color: var(--_toast-status-color); }
|
|
50% { border-inline-start-color: var(--_toast-accent); }
|
|
}
|
|
|
|
/* Swipe states — track the cursor / touch as the user pulls to dismiss.
|
|
* The Item's transform is updated by soma; the recipe just darkens the
|
|
* accent so the user gets stronger feedback as they near the threshold. */
|
|
[data-toast-item][data-swipe='start'],
|
|
[data-toast-item][data-swipe='move'] {
|
|
transition: none;
|
|
user-select: none;
|
|
cursor: grabbing;
|
|
}
|
|
[data-toast-item][data-swipe='cancel'] {
|
|
transition:
|
|
transform calc(var(--toast-transition-duration) * 1.5) var(--toast-transition-ease),
|
|
opacity var(--toast-transition-duration) var(--toast-transition-ease);
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
[data-toast-item][data-swipe='end'] {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Status icon disc — air's `air-toast-status` continuation. The wrapper
|
|
* reads `data-intent` from the Item and tints the disc via the cascade
|
|
* variables already set on Item. */
|
|
[data-toast-status] {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
inline-size: var(--toast-status-size);
|
|
block-size: var(--toast-status-size);
|
|
border-radius: var(--toast-status-radius);
|
|
background: var(--_toast-status-bg);
|
|
color: var(--_toast-status-color);
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
/* Loading spinner — when the Item carries `data-loading`, the status
|
|
* disc spins. Uses an inline border trick so it doesn't depend on an
|
|
* extra child element. */
|
|
[data-toast-item][data-loading] [data-toast-status] {
|
|
background: transparent;
|
|
border: 2px solid color-mix(in srgb, var(--_toast-status-color) 30%, transparent);
|
|
border-block-start-color: var(--_toast-status-color);
|
|
animation: toast-spinner 0.9s linear infinite;
|
|
}
|
|
@keyframes toast-spinner {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Title + description should NOT be column 1 when there's no status —
|
|
* but the auto-collapse rule above already collapses to a single column,
|
|
* so the main slot just fills. We add a wrapper rule in case a consumer
|
|
* uses an explicit `<div data-toast-main>` to group title+description. */
|
|
[data-toast-main] {
|
|
display: grid;
|
|
gap: var(--toast-provider-stack-gap);
|
|
min-inline-size: 0;
|
|
}
|
|
|
|
/* ── Title / Description / Action / Close ──────────────────────────── */
|
|
|
|
[data-toast-title] {
|
|
margin: 0;
|
|
font-family: var(--toast-title-font-family);
|
|
font-size: var(--toast-title-font-size);
|
|
font-weight: var(--toast-title-font-weight);
|
|
line-height: var(--toast-title-line-height);
|
|
color: var(--toast-title-color);
|
|
}
|
|
|
|
[data-toast-description] {
|
|
margin: 0;
|
|
font-family: var(--toast-description-font-family);
|
|
font-size: var(--toast-description-font-size);
|
|
line-height: var(--toast-description-line-height);
|
|
color: var(--toast-description-color);
|
|
}
|
|
|
|
[data-toast-action] {
|
|
justify-self: start;
|
|
margin-top: var(--toast-action-margin-top);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--toast-action-gap);
|
|
padding: var(--toast-action-py) var(--toast-action-px);
|
|
border: var(--toast-action-border-width) solid currentColor;
|
|
border-radius: var(--toast-action-radius);
|
|
background: transparent;
|
|
color: var(--_toast-action-color);
|
|
font-family: var(--toast-action-font-family);
|
|
font-size: var(--toast-action-font-size);
|
|
font-weight: var(--toast-action-font-weight);
|
|
line-height: var(--toast-action-line-height);
|
|
cursor: pointer;
|
|
transition:
|
|
background var(--toast-transition-duration) var(--toast-transition-ease),
|
|
color var(--toast-transition-duration) var(--toast-transition-ease),
|
|
box-shadow var(--toast-transition-duration) var(--toast-transition-ease);
|
|
}
|
|
|
|
[data-toast-action]:hover {
|
|
background: var(--_toast-action-hover-bg);
|
|
}
|
|
|
|
[data-toast-action]:focus-visible {
|
|
outline: none;
|
|
box-shadow:
|
|
0 0 0 var(--toast-action-focus-ring-offset) var(--_toast-surface),
|
|
0 0 0 calc(var(--toast-action-focus-ring-offset) + var(--toast-action-focus-ring-width))
|
|
var(--toast-action-focus-ring-color);
|
|
}
|
|
|
|
[data-toast-close] {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
inline-size: var(--toast-close-size);
|
|
block-size: var(--toast-close-size);
|
|
flex: 0 0 auto;
|
|
padding: 0;
|
|
border: var(--toast-close-border-width) solid var(--toast-close-border);
|
|
border-radius: var(--toast-close-radius);
|
|
background: var(--toast-close-bg);
|
|
color: var(--toast-close-color);
|
|
cursor: pointer;
|
|
transition:
|
|
background var(--toast-transition-duration) var(--toast-transition-ease),
|
|
border-color var(--toast-transition-duration) var(--toast-transition-ease),
|
|
color var(--toast-transition-duration) var(--toast-transition-ease),
|
|
box-shadow var(--toast-transition-duration) var(--toast-transition-ease);
|
|
}
|
|
|
|
[data-toast-close]:hover {
|
|
background: var(--toast-close-hover-bg);
|
|
border-color: var(--toast-close-hover-border);
|
|
color: var(--toast-close-hover-color);
|
|
}
|
|
|
|
[data-toast-close]:focus-visible {
|
|
outline: none;
|
|
box-shadow:
|
|
0 0 0 var(--toast-close-focus-ring-offset) var(--_toast-surface),
|
|
0 0 0 calc(var(--toast-close-focus-ring-offset) + var(--toast-close-focus-ring-width))
|
|
var(--toast-close-focus-ring-color);
|
|
}
|