@enigmax/primitives 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,165 +1,238 @@
1
1
  /*
2
- * Ember - the default toast theme.
2
+ * The toast theme.
3
3
  *
4
- * Every colour, every distance and every curve is in this file. `<Toaster />` renders the
5
- * structure and the state; nothing below is something the package decided for you, so
6
- * editing this file is the supported way to make it yours.
4
+ * GENERATED from src/react/toast-styles.ts by scripts/sync-recipes.mjs - edit that file, not
5
+ * this one. It exists as a stylesheet as well as a string because <Toaster /> injects the
6
+ * theme itself, and this is the same look for anyone who would rather import it, copy it
7
+ * with `enigma add toast --copy`, or fork it outright.
7
8
  *
8
- * The custom properties at the top are the whole API. Override them on `:root` and you have
9
- * a different theme without touching a selector.
9
+ * Every value is a custom property on [data-enigma-toaster]: override them there, or on
10
+ * :root, and you have a different toast without touching a selector.
10
11
  */
11
-
12
- :root {
13
- --enigma-toast-width: 22rem;
14
- --enigma-toast-gap: 0.625rem;
15
- --enigma-toast-edge: 1.25rem;
16
- --enigma-toast-radius: 0.75rem;
17
- --enigma-toast-exit: 200ms;
12
+ [data-enigma-toaster] {
13
+ --enigma-toast-width: 356px;
14
+ --enigma-toast-edge: 24px;
15
+ --enigma-toast-gap: 14px;
16
+ --enigma-toast-peek: 14px;
17
+ --enigma-toast-scale-step: 0.06;
18
+ --enigma-toast-radius: 12px;
19
+ --enigma-toast-enter: 420ms;
20
+ --enigma-toast-exit: 350ms;
18
21
 
19
22
  --enigma-toast-bg: #101010;
20
23
  --enigma-toast-border: #2a2a2a;
21
24
  --enigma-toast-text: #f5f5f5;
22
25
  --enigma-toast-muted: #a3a3a3;
23
- --enigma-toast-shadow: 0 8px 30px rgb(0 0 0 / 45%);
26
+ --enigma-toast-shadow: 0 6px 22px rgb(0 0 0 / 34%);
27
+
28
+ --enigma-toast-success: hsl(150 80% 72%);
29
+ --enigma-toast-success-bg: hsl(150 38% 9%);
30
+ --enigma-toast-success-border: hsl(147 55% 18%);
31
+ --enigma-toast-error: hsl(358 100% 82%);
32
+ --enigma-toast-error-bg: hsl(358 42% 12%);
33
+ --enigma-toast-error-border: hsl(357 55% 24%);
34
+ --enigma-toast-info: hsl(216 87% 74%);
35
+ --enigma-toast-info-bg: hsl(215 45% 11%);
36
+ --enigma-toast-info-border: hsl(223 50% 24%);
37
+ --enigma-toast-warning: hsl(46 87% 70%);
38
+ --enigma-toast-warning-bg: hsl(40 55% 9%);
39
+ --enigma-toast-warning-border: hsl(40 55% 20%);
24
40
 
25
- --enigma-toast-info: #60a5fa;
26
- --enigma-toast-success: #4ade80;
27
- --enigma-toast-warning: #fbbf24;
28
- --enigma-toast-error: #f87171;
29
- --enigma-toast-loading: var(--enigma-toast-muted);
41
+ position: fixed;
42
+ z-index: 999999;
43
+ width: var(--enigma-toast-width);
44
+ margin: 0;
45
+ padding: 0;
46
+ list-style: none;
47
+ outline: none;
48
+ font-family: inherit;
49
+ font-size: 13px;
50
+ /* The stack is a positioning context: every toast is absolute inside it, which is what
51
+ lets the ones behind be clipped to the front one's height. */
52
+ transition: transform 350ms ease;
30
53
  }
31
54
 
32
55
  @media (prefers-color-scheme: light) {
33
- :root {
56
+ [data-enigma-toaster] {
34
57
  --enigma-toast-bg: #ffffff;
35
58
  --enigma-toast-border: #e5e5e5;
36
59
  --enigma-toast-text: #171717;
37
60
  --enigma-toast-muted: #737373;
38
- --enigma-toast-shadow: 0 8px 30px rgb(0 0 0 / 12%);
61
+ --enigma-toast-shadow: 0 6px 22px rgb(0 0 0 / 12%);
62
+ --enigma-toast-success-bg: hsl(143 85% 96%);
63
+ --enigma-toast-success-border: hsl(145 92% 87%);
64
+ --enigma-toast-success: hsl(140 100% 27%);
65
+ --enigma-toast-error-bg: hsl(359 100% 97%);
66
+ --enigma-toast-error-border: hsl(359 100% 94%);
67
+ --enigma-toast-error: hsl(360 100% 45%);
68
+ --enigma-toast-info-bg: hsl(208 100% 97%);
69
+ --enigma-toast-info-border: hsl(221 91% 93%);
70
+ --enigma-toast-info: hsl(210 92% 45%);
71
+ --enigma-toast-warning-bg: hsl(49 100% 97%);
72
+ --enigma-toast-warning-border: hsl(49 91% 84%);
73
+ --enigma-toast-warning: hsl(31 92% 45%);
39
74
  }
40
75
  }
41
76
 
42
- [data-enigma-toaster] {
43
- position: fixed;
44
- z-index: 9999;
45
- display: flex;
46
- flex-direction: column;
47
- gap: var(--enigma-toast-gap);
48
- width: min(var(--enigma-toast-width), calc(100vw - var(--enigma-toast-edge) * 2));
49
- /* The region is a layout box, not a wall: clicks pass through the gaps between toasts. */
50
- pointer-events: none;
51
- margin: 0;
52
- padding: 0;
53
- }
54
-
55
- [data-enigma-toaster][data-position^="top"] { top: var(--enigma-toast-edge); flex-direction: column-reverse; }
77
+ [data-enigma-toaster][data-position^="top"] { top: var(--enigma-toast-edge); }
56
78
  [data-enigma-toaster][data-position^="bottom"] { bottom: var(--enigma-toast-edge); }
57
- [data-enigma-toaster][data-position$="-left"] { left: var(--enigma-toast-edge); }
58
79
  [data-enigma-toaster][data-position$="-right"] { right: var(--enigma-toast-edge); }
80
+ [data-enigma-toaster][data-position$="-left"] { left: var(--enigma-toast-edge); }
59
81
  [data-enigma-toaster][data-position$="-center"] { left: 50%; transform: translateX(-50%); }
82
+ /* The whole stack lifts a little when it fans out, so the front toast does not sit on the
83
+ exact pixel it did a moment ago. */
84
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] { transform: translateY(-6px); }
85
+ [data-enigma-toaster][data-expanded][data-position^="top"] { transform: translateY(6px); }
86
+ [data-enigma-toaster][data-expanded][data-position="bottom-center"] { transform: translateX(-50%) translateY(-6px); }
87
+ [data-enigma-toaster][data-expanded][data-position="top-center"] { transform: translateX(-50%) translateY(6px); }
60
88
 
61
89
  [data-enigma-toast] {
62
- pointer-events: auto;
63
- position: relative;
64
- display: grid;
65
- grid-template-columns: 1fr auto;
90
+ position: absolute;
91
+ left: 0;
92
+ right: 0;
93
+ width: 100%;
94
+ box-sizing: border-box;
95
+ display: flex;
66
96
  align-items: center;
67
- gap: 0.25rem 0.75rem;
68
- padding: 0.875rem 1rem;
97
+ gap: 10px;
98
+ padding: 15px 16px;
69
99
  color: var(--enigma-toast-text);
70
100
  background: var(--enigma-toast-bg);
71
101
  border: 1px solid var(--enigma-toast-border);
72
102
  border-radius: var(--enigma-toast-radius);
73
103
  box-shadow: var(--enigma-toast-shadow);
74
- /* The swipe offset the component publishes. Zero unless a finger is on it. */
75
- translate: var(--enigma-toast-swipe, 0) 0;
76
- transition: translate 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease-out;
77
- touch-action: pan-y;
104
+ overflow-wrap: anywhere;
105
+ touch-action: none;
78
106
  outline: none;
107
+ z-index: var(--enigma-toast-z, 1);
108
+ transition:
109
+ transform var(--enigma-toast-enter) cubic-bezier(0.21, 1.02, 0.73, 1),
110
+ opacity var(--enigma-toast-exit),
111
+ height 350ms;
79
112
  }
80
113
 
81
- [data-enigma-toaster][data-position$="-center"] [data-enigma-toast] { translate: 0 var(--enigma-toast-swipe, 0); }
114
+ [data-enigma-toaster][data-position^="bottom"] [data-enigma-toast] { bottom: 0; transform-origin: bottom center; }
115
+ [data-enigma-toaster][data-position^="top"] [data-enigma-toast] { top: 0; transform-origin: top center; }
116
+
117
+ /* Enter from off screen, in the direction the stack is pinned to. */
118
+ [data-enigma-toaster][data-position^="bottom"] [data-enigma-toast] { transform: translateY(110%); opacity: 0; }
119
+ [data-enigma-toaster][data-position^="top"] [data-enigma-toast] { transform: translateY(-110%); opacity: 0; }
120
+ /* Scoped to the toaster so it OUTRANKS the enter rules above, which are scoped the same
121
+ way: at equal specificity the later rule wins, and a mounted toast that lost this one sat
122
+ at its entry offset forever - visible, and 110% of its own height off the bottom. */
123
+ [data-enigma-toaster] [data-enigma-toast][data-mounted] { opacity: 1; transform: translateY(0); }
124
+
125
+ /* Collapsed: the ones behind are clipped to the FRONT toast's height and scaled back, so
126
+ the stack reads as depth rather than as a list. Their content is hidden but present -
127
+ removing it would make the box jump when the stack expands. */
128
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast][data-mounted]:not([data-front]) {
129
+ height: var(--enigma-toast-front-height);
130
+ transform: translateY(calc(var(--enigma-toast-before) * var(--enigma-toast-peek) * -1))
131
+ scale(calc(1 - var(--enigma-toast-before) * var(--enigma-toast-scale-step)));
132
+ }
133
+ [data-enigma-toaster][data-position^="top"]:not([data-expanded]) [data-enigma-toast][data-mounted]:not([data-front]) {
134
+ transform: translateY(calc(var(--enigma-toast-before) * var(--enigma-toast-peek)))
135
+ scale(calc(1 - var(--enigma-toast-before) * var(--enigma-toast-scale-step)));
136
+ }
137
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast]:not([data-front]) > * { opacity: 0; }
138
+ [data-enigma-toast] > * { transition: opacity 350ms; }
82
139
 
83
- /* While a finger is down the toast tracks it exactly: a transition here would lag behind
84
- the pointer, which reads as a broken gesture rather than a smooth one. */
85
- [data-enigma-toast][data-swiping] { transition: none; cursor: grabbing; user-select: none; }
140
+ /* Expanded: each toast is lifted by the summed height of the ones in front of it. */
141
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] [data-enigma-toast][data-mounted] {
142
+ transform: translateY(calc(var(--enigma-toast-offset) * -1));
143
+ }
144
+ [data-enigma-toaster][data-expanded][data-position^="top"] [data-enigma-toast][data-mounted] {
145
+ transform: translateY(var(--enigma-toast-offset));
146
+ }
147
+
148
+ /* Past the visible count it is still in the DOM, still counted, and not on screen. */
149
+ [data-enigma-toast][data-hidden] { opacity: 0; pointer-events: none; }
150
+
151
+ [data-enigma-toast][data-state="leaving"] { opacity: 0; }
152
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast][data-state="leaving"] { transform: translateY(150%) scale(0.9); }
153
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] [data-enigma-toast][data-state="leaving"] {
154
+ transform: translateY(calc(var(--enigma-toast-offset) * -1 + 110%));
155
+ }
156
+
157
+ /* While a finger is down the toast tracks it exactly: a transition here would lag behind the
158
+ pointer, which reads as a broken gesture rather than a smooth one. */
159
+ [data-enigma-toast][data-swiping] {
160
+ transition: opacity 200ms;
161
+ transform: translateX(var(--enigma-toast-swipe, 0px));
162
+ }
86
163
 
87
164
  [data-enigma-toast]:focus-visible { box-shadow: var(--enigma-toast-shadow), 0 0 0 2px var(--enigma-toast-info); }
88
165
 
89
- /* A left border is the tone. It reads at a glance and survives any background. */
90
- [data-enigma-toast]::before {
91
- content: "";
92
- position: absolute;
93
- inset: 0 auto 0 0;
94
- width: 3px;
95
- border-radius: var(--enigma-toast-radius) 0 0 var(--enigma-toast-radius);
96
- background: var(--enigma-toast-tone, transparent);
166
+ [data-enigma-toast][data-tone="success"] {
167
+ color: var(--enigma-toast-success);
168
+ background: var(--enigma-toast-success-bg);
169
+ border-color: var(--enigma-toast-success-border);
170
+ }
171
+ [data-enigma-toast][data-tone="error"] {
172
+ color: var(--enigma-toast-error);
173
+ background: var(--enigma-toast-error-bg);
174
+ border-color: var(--enigma-toast-error-border);
175
+ }
176
+ [data-enigma-toast][data-tone="info"] {
177
+ color: var(--enigma-toast-info);
178
+ background: var(--enigma-toast-info-bg);
179
+ border-color: var(--enigma-toast-info-border);
180
+ }
181
+ [data-enigma-toast][data-tone="warning"] {
182
+ color: var(--enigma-toast-warning);
183
+ background: var(--enigma-toast-warning-bg);
184
+ border-color: var(--enigma-toast-warning-border);
97
185
  }
98
186
 
99
- [data-enigma-toast][data-tone="info"] { --enigma-toast-tone: var(--enigma-toast-info); }
100
- [data-enigma-toast][data-tone="success"] { --enigma-toast-tone: var(--enigma-toast-success); }
101
- [data-enigma-toast][data-tone="warning"] { --enigma-toast-tone: var(--enigma-toast-warning); }
102
- [data-enigma-toast][data-tone="error"] { --enigma-toast-tone: var(--enigma-toast-error); }
103
- [data-enigma-toast][data-tone="loading"] { --enigma-toast-tone: var(--enigma-toast-loading); }
187
+ [data-enigma-toast-icon] { display: flex; flex-shrink: 0; width: 20px; height: 20px; }
188
+ [data-enigma-toast-icon] svg { width: 20px; height: 20px; display: block; }
189
+ [data-enigma-toast-icon][data-tone="loading"] svg { animation: enigma-toast-spin 900ms linear infinite; }
190
+ @keyframes enigma-toast-spin { to { transform: rotate(360deg); } }
104
191
 
105
- [data-enigma-toast-title] { grid-column: 1; margin: 0; font-size: 0.875rem; font-weight: 600; line-height: 1.35; }
106
- [data-enigma-toast-body] { grid-column: 1; margin: 0; font-size: 0.8125rem; color: var(--enigma-toast-muted); line-height: 1.45; }
192
+ [data-enigma-toast-content] { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
193
+ [data-enigma-toast-title] { margin: 0; font-weight: 600; line-height: 1.4; }
194
+ [data-enigma-toast-body] { margin: 0; font-size: 12px; line-height: 1.45; opacity: 0.75; }
107
195
 
108
196
  [data-enigma-toast-action] {
109
- grid-column: 2;
110
- grid-row: 1 / -1;
111
- padding: 0.3125rem 0.625rem;
197
+ flex-shrink: 0;
198
+ padding: 5px 10px;
112
199
  font: inherit;
113
- font-size: 0.75rem;
200
+ font-size: 12px;
114
201
  font-weight: 600;
115
202
  color: var(--enigma-toast-bg);
116
- background: var(--enigma-toast-text);
203
+ background: currentColor;
117
204
  border: 0;
118
- border-radius: 0.375rem;
205
+ border-radius: 6px;
119
206
  cursor: pointer;
120
207
  }
121
208
 
122
209
  [data-enigma-toast-close] {
123
- position: absolute;
124
- top: 0.375rem;
125
- right: 0.5rem;
126
- padding: 0 0.25rem;
127
- font-size: 0.875rem;
128
- line-height: 1;
129
- color: var(--enigma-toast-muted);
210
+ flex-shrink: 0;
211
+ display: grid;
212
+ place-items: center;
213
+ width: 20px;
214
+ height: 20px;
215
+ padding: 0;
216
+ color: inherit;
130
217
  background: none;
131
218
  border: 0;
219
+ border-radius: 5px;
132
220
  cursor: pointer;
133
221
  /* Shown on hover or focus, so it never covers the message while it is being read - but
134
- always reachable by keyboard, which `display: none` would take away. */
222
+ always reachable by keyboard, which display:none would take away. */
135
223
  opacity: 0;
136
224
  transition: opacity 120ms ease-out;
137
225
  }
138
-
139
226
  [data-enigma-toast]:hover [data-enigma-toast-close],
140
- [data-enigma-toast-close]:focus-visible { opacity: 1; }
141
-
142
- /* Enter from the edge it is pinned to, leave the same way. */
143
- @keyframes enigma-toast-in-right { from { opacity: 0; translate: 100% 0; } to { opacity: 1; translate: 0 0; } }
144
- @keyframes enigma-toast-in-left { from { opacity: 0; translate: -100% 0; } to { opacity: 1; translate: 0 0; } }
145
- @keyframes enigma-toast-in-top { from { opacity: 0; translate: 0 -100%; } to { opacity: 1; translate: 0 0; } }
146
- @keyframes enigma-toast-in-bottom { from { opacity: 0; translate: 0 100%; } to { opacity: 1; translate: 0 0; } }
147
- @keyframes enigma-toast-out { to { opacity: 0; scale: 0.94; } }
148
-
149
- [data-position$="-right"] [data-enigma-toast][data-state="open"] { animation: enigma-toast-in-right 260ms cubic-bezier(0.22, 1, 0.36, 1); }
150
- [data-position$="-left"] [data-enigma-toast][data-state="open"] { animation: enigma-toast-in-left 260ms cubic-bezier(0.22, 1, 0.36, 1); }
151
- [data-position="top-center"] [data-enigma-toast][data-state="open"] { animation: enigma-toast-in-top 260ms cubic-bezier(0.22, 1, 0.36, 1); }
152
- [data-position="bottom-center"] [data-enigma-toast][data-state="open"] { animation: enigma-toast-in-bottom 260ms cubic-bezier(0.22, 1, 0.36, 1); }
227
+ [data-enigma-toast-close]:focus-visible { opacity: 0.7; }
228
+ [data-enigma-toast-close]:hover { opacity: 1; }
153
229
 
154
- /* Matches the component's exitDuration: the node is held for exactly this long, so a
155
- longer animation would be cut off mid-way. */
156
- [data-enigma-toast][data-state="leaving"] {
157
- animation: enigma-toast-out var(--enigma-toast-exit) ease-in forwards;
158
- pointer-events: none;
230
+ @media (max-width: 600px) {
231
+ [data-enigma-toaster] { right: 14px; left: 14px; width: auto; }
159
232
  }
160
233
 
161
234
  @media (prefers-reduced-motion: reduce) {
162
235
  /* The movement goes, the state change stays: a toast still appears and still leaves. */
163
- [data-enigma-toast] { animation: none !important; transition: opacity 120ms ease-out; }
164
- [data-enigma-toast][data-state="leaving"] { opacity: 0; }
236
+ [data-enigma-toast], [data-enigma-toast] > * { transition: opacity 200ms !important; }
237
+ [data-enigma-toast][data-mounted] { transform: none !important; }
165
238
  }
package/registry.json CHANGED
@@ -711,12 +711,12 @@
711
711
  {
712
712
  "name": "toast",
713
713
  "title": "Toast",
714
- "description": "The notification queue with a renderer on top: stacking, swipe to dismiss, a pointer that stops the clock, and a toast that is still on screen while it animates out. Ships the Ember theme, which is a stylesheet you own.",
714
+ "description": "The notification queue with a renderer on top: the newest toast in front, the older ones stacked behind it and fanning out when the pointer arrives, swipe to dismiss, a pointer that stops the clock, and a toast that is still on screen while it animates out. It brings its own theme - a toast at the edge of the screen is the one component a forgotten stylesheet ruins rather than merely undresses.",
715
715
  "targets": [
716
716
  "react"
717
717
  ],
718
718
  "entry": {
719
- "react": "@enigmax/primitives/react"
719
+ "react": "@enigmax/primitives/react/toast"
720
720
  },
721
721
  "exports": {
722
722
  "react": [
@@ -774,15 +774,20 @@
774
774
  }
775
775
  }
776
776
  ],
777
- "styles": false,
777
+ "styles": true,
778
778
  "themeHooks": [
779
779
  "[data-enigma-toaster]",
780
780
  "[data-enigma-toaster][data-position]",
781
+ "[data-enigma-toaster][data-expanded]",
781
782
  "[data-enigma-toast]",
782
783
  "[data-enigma-toast][data-tone=error]",
784
+ "[data-enigma-toast][data-front]",
785
+ "[data-enigma-toast][data-mounted]",
783
786
  "[data-enigma-toast][data-state=leaving]",
784
787
  "[data-enigma-toast][data-swiping]",
785
- "[data-enigma-toast][data-front]",
788
+ "[data-enigma-toast][data-hidden]",
789
+ "[data-enigma-toast-icon]",
790
+ "[data-enigma-toast-content]",
786
791
  "[data-enigma-toast-title]",
787
792
  "[data-enigma-toast-body]",
788
793
  "[data-enigma-toast-action]",
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The toast theme, as a string, because `<Toaster />` injects it.
3
+ *
4
+ * A toast is the one component where "bring your own CSS" is the wrong default: it is
5
+ * rendered into a portal at the edge of the screen, it stacks, and it animates - so a
6
+ * consumer who forgets the stylesheet does not get an unstyled toast, they get a pile of
7
+ * text in the corner. `<Toaster styles={false} />` turns it off for anyone who wants their
8
+ * own, and the injected sheet is PREPENDED to `<head>` so any stylesheet in the document
9
+ * outranks it by source order without needing a single `!important`.
10
+ *
11
+ * This module is the source of truth for `recipes/toast/styles.css` too, which
12
+ * `scripts/sync-recipes.mjs` writes from it - one look, whether it is injected or copied.
13
+ *
14
+ * The geometry is the stacked-card pattern: the newest toast is the front one, older ones
15
+ * sit behind it scaled down and clipped to the front one's height, and hovering the stack
16
+ * fans them out by the summed height of the ones in front. Every number below is one of
17
+ * those two states.
18
+ */
19
+ export const TOAST_STYLES = `
20
+ [data-enigma-toaster] {
21
+ --enigma-toast-width: 356px;
22
+ --enigma-toast-edge: 24px;
23
+ --enigma-toast-gap: 14px;
24
+ --enigma-toast-peek: 14px;
25
+ --enigma-toast-scale-step: 0.06;
26
+ --enigma-toast-radius: 12px;
27
+ --enigma-toast-enter: 420ms;
28
+ --enigma-toast-exit: 350ms;
29
+
30
+ --enigma-toast-bg: #101010;
31
+ --enigma-toast-border: #2a2a2a;
32
+ --enigma-toast-text: #f5f5f5;
33
+ --enigma-toast-muted: #a3a3a3;
34
+ --enigma-toast-shadow: 0 6px 22px rgb(0 0 0 / 34%);
35
+
36
+ --enigma-toast-success: hsl(150 80% 72%);
37
+ --enigma-toast-success-bg: hsl(150 38% 9%);
38
+ --enigma-toast-success-border: hsl(147 55% 18%);
39
+ --enigma-toast-error: hsl(358 100% 82%);
40
+ --enigma-toast-error-bg: hsl(358 42% 12%);
41
+ --enigma-toast-error-border: hsl(357 55% 24%);
42
+ --enigma-toast-info: hsl(216 87% 74%);
43
+ --enigma-toast-info-bg: hsl(215 45% 11%);
44
+ --enigma-toast-info-border: hsl(223 50% 24%);
45
+ --enigma-toast-warning: hsl(46 87% 70%);
46
+ --enigma-toast-warning-bg: hsl(40 55% 9%);
47
+ --enigma-toast-warning-border: hsl(40 55% 20%);
48
+
49
+ position: fixed;
50
+ z-index: 999999;
51
+ width: var(--enigma-toast-width);
52
+ margin: 0;
53
+ padding: 0;
54
+ list-style: none;
55
+ outline: none;
56
+ font-family: inherit;
57
+ font-size: 13px;
58
+ /* The stack is a positioning context: every toast is absolute inside it, which is what
59
+ lets the ones behind be clipped to the front one's height. */
60
+ transition: transform 350ms ease;
61
+ }
62
+
63
+ @media (prefers-color-scheme: light) {
64
+ [data-enigma-toaster] {
65
+ --enigma-toast-bg: #ffffff;
66
+ --enigma-toast-border: #e5e5e5;
67
+ --enigma-toast-text: #171717;
68
+ --enigma-toast-muted: #737373;
69
+ --enigma-toast-shadow: 0 6px 22px rgb(0 0 0 / 12%);
70
+ --enigma-toast-success-bg: hsl(143 85% 96%);
71
+ --enigma-toast-success-border: hsl(145 92% 87%);
72
+ --enigma-toast-success: hsl(140 100% 27%);
73
+ --enigma-toast-error-bg: hsl(359 100% 97%);
74
+ --enigma-toast-error-border: hsl(359 100% 94%);
75
+ --enigma-toast-error: hsl(360 100% 45%);
76
+ --enigma-toast-info-bg: hsl(208 100% 97%);
77
+ --enigma-toast-info-border: hsl(221 91% 93%);
78
+ --enigma-toast-info: hsl(210 92% 45%);
79
+ --enigma-toast-warning-bg: hsl(49 100% 97%);
80
+ --enigma-toast-warning-border: hsl(49 91% 84%);
81
+ --enigma-toast-warning: hsl(31 92% 45%);
82
+ }
83
+ }
84
+
85
+ [data-enigma-toaster][data-position^="top"] { top: var(--enigma-toast-edge); }
86
+ [data-enigma-toaster][data-position^="bottom"] { bottom: var(--enigma-toast-edge); }
87
+ [data-enigma-toaster][data-position$="-right"] { right: var(--enigma-toast-edge); }
88
+ [data-enigma-toaster][data-position$="-left"] { left: var(--enigma-toast-edge); }
89
+ [data-enigma-toaster][data-position$="-center"] { left: 50%; transform: translateX(-50%); }
90
+ /* The whole stack lifts a little when it fans out, so the front toast does not sit on the
91
+ exact pixel it did a moment ago. */
92
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] { transform: translateY(-6px); }
93
+ [data-enigma-toaster][data-expanded][data-position^="top"] { transform: translateY(6px); }
94
+ [data-enigma-toaster][data-expanded][data-position="bottom-center"] { transform: translateX(-50%) translateY(-6px); }
95
+ [data-enigma-toaster][data-expanded][data-position="top-center"] { transform: translateX(-50%) translateY(6px); }
96
+
97
+ [data-enigma-toast] {
98
+ position: absolute;
99
+ left: 0;
100
+ right: 0;
101
+ width: 100%;
102
+ box-sizing: border-box;
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 10px;
106
+ padding: 15px 16px;
107
+ color: var(--enigma-toast-text);
108
+ background: var(--enigma-toast-bg);
109
+ border: 1px solid var(--enigma-toast-border);
110
+ border-radius: var(--enigma-toast-radius);
111
+ box-shadow: var(--enigma-toast-shadow);
112
+ overflow-wrap: anywhere;
113
+ touch-action: none;
114
+ outline: none;
115
+ z-index: var(--enigma-toast-z, 1);
116
+ transition:
117
+ transform var(--enigma-toast-enter) cubic-bezier(0.21, 1.02, 0.73, 1),
118
+ opacity var(--enigma-toast-exit),
119
+ height 350ms;
120
+ }
121
+
122
+ [data-enigma-toaster][data-position^="bottom"] [data-enigma-toast] { bottom: 0; transform-origin: bottom center; }
123
+ [data-enigma-toaster][data-position^="top"] [data-enigma-toast] { top: 0; transform-origin: top center; }
124
+
125
+ /* Enter from off screen, in the direction the stack is pinned to. */
126
+ [data-enigma-toaster][data-position^="bottom"] [data-enigma-toast] { transform: translateY(110%); opacity: 0; }
127
+ [data-enigma-toaster][data-position^="top"] [data-enigma-toast] { transform: translateY(-110%); opacity: 0; }
128
+ /* Scoped to the toaster so it OUTRANKS the enter rules above, which are scoped the same
129
+ way: at equal specificity the later rule wins, and a mounted toast that lost this one sat
130
+ at its entry offset forever - visible, and 110% of its own height off the bottom. */
131
+ [data-enigma-toaster] [data-enigma-toast][data-mounted] { opacity: 1; transform: translateY(0); }
132
+
133
+ /* Collapsed: the ones behind are clipped to the FRONT toast's height and scaled back, so
134
+ the stack reads as depth rather than as a list. Their content is hidden but present -
135
+ removing it would make the box jump when the stack expands. */
136
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast][data-mounted]:not([data-front]) {
137
+ height: var(--enigma-toast-front-height);
138
+ transform: translateY(calc(var(--enigma-toast-before) * var(--enigma-toast-peek) * -1))
139
+ scale(calc(1 - var(--enigma-toast-before) * var(--enigma-toast-scale-step)));
140
+ }
141
+ [data-enigma-toaster][data-position^="top"]:not([data-expanded]) [data-enigma-toast][data-mounted]:not([data-front]) {
142
+ transform: translateY(calc(var(--enigma-toast-before) * var(--enigma-toast-peek)))
143
+ scale(calc(1 - var(--enigma-toast-before) * var(--enigma-toast-scale-step)));
144
+ }
145
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast]:not([data-front]) > * { opacity: 0; }
146
+ [data-enigma-toast] > * { transition: opacity 350ms; }
147
+
148
+ /* Expanded: each toast is lifted by the summed height of the ones in front of it. */
149
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] [data-enigma-toast][data-mounted] {
150
+ transform: translateY(calc(var(--enigma-toast-offset) * -1));
151
+ }
152
+ [data-enigma-toaster][data-expanded][data-position^="top"] [data-enigma-toast][data-mounted] {
153
+ transform: translateY(var(--enigma-toast-offset));
154
+ }
155
+
156
+ /* Past the visible count it is still in the DOM, still counted, and not on screen. */
157
+ [data-enigma-toast][data-hidden] { opacity: 0; pointer-events: none; }
158
+
159
+ [data-enigma-toast][data-state="leaving"] { opacity: 0; }
160
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast][data-state="leaving"] { transform: translateY(150%) scale(0.9); }
161
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] [data-enigma-toast][data-state="leaving"] {
162
+ transform: translateY(calc(var(--enigma-toast-offset) * -1 + 110%));
163
+ }
164
+
165
+ /* While a finger is down the toast tracks it exactly: a transition here would lag behind the
166
+ pointer, which reads as a broken gesture rather than a smooth one. */
167
+ [data-enigma-toast][data-swiping] {
168
+ transition: opacity 200ms;
169
+ transform: translateX(var(--enigma-toast-swipe, 0px));
170
+ }
171
+
172
+ [data-enigma-toast]:focus-visible { box-shadow: var(--enigma-toast-shadow), 0 0 0 2px var(--enigma-toast-info); }
173
+
174
+ [data-enigma-toast][data-tone="success"] {
175
+ color: var(--enigma-toast-success);
176
+ background: var(--enigma-toast-success-bg);
177
+ border-color: var(--enigma-toast-success-border);
178
+ }
179
+ [data-enigma-toast][data-tone="error"] {
180
+ color: var(--enigma-toast-error);
181
+ background: var(--enigma-toast-error-bg);
182
+ border-color: var(--enigma-toast-error-border);
183
+ }
184
+ [data-enigma-toast][data-tone="info"] {
185
+ color: var(--enigma-toast-info);
186
+ background: var(--enigma-toast-info-bg);
187
+ border-color: var(--enigma-toast-info-border);
188
+ }
189
+ [data-enigma-toast][data-tone="warning"] {
190
+ color: var(--enigma-toast-warning);
191
+ background: var(--enigma-toast-warning-bg);
192
+ border-color: var(--enigma-toast-warning-border);
193
+ }
194
+
195
+ [data-enigma-toast-icon] { display: flex; flex-shrink: 0; width: 20px; height: 20px; }
196
+ [data-enigma-toast-icon] svg { width: 20px; height: 20px; display: block; }
197
+ [data-enigma-toast-icon][data-tone="loading"] svg { animation: enigma-toast-spin 900ms linear infinite; }
198
+ @keyframes enigma-toast-spin { to { transform: rotate(360deg); } }
199
+
200
+ [data-enigma-toast-content] { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
201
+ [data-enigma-toast-title] { margin: 0; font-weight: 600; line-height: 1.4; }
202
+ [data-enigma-toast-body] { margin: 0; font-size: 12px; line-height: 1.45; opacity: 0.75; }
203
+
204
+ [data-enigma-toast-action] {
205
+ flex-shrink: 0;
206
+ padding: 5px 10px;
207
+ font: inherit;
208
+ font-size: 12px;
209
+ font-weight: 600;
210
+ color: var(--enigma-toast-bg);
211
+ background: currentColor;
212
+ border: 0;
213
+ border-radius: 6px;
214
+ cursor: pointer;
215
+ }
216
+
217
+ [data-enigma-toast-close] {
218
+ flex-shrink: 0;
219
+ display: grid;
220
+ place-items: center;
221
+ width: 20px;
222
+ height: 20px;
223
+ padding: 0;
224
+ color: inherit;
225
+ background: none;
226
+ border: 0;
227
+ border-radius: 5px;
228
+ cursor: pointer;
229
+ /* Shown on hover or focus, so it never covers the message while it is being read - but
230
+ always reachable by keyboard, which display:none would take away. */
231
+ opacity: 0;
232
+ transition: opacity 120ms ease-out;
233
+ }
234
+ [data-enigma-toast]:hover [data-enigma-toast-close],
235
+ [data-enigma-toast-close]:focus-visible { opacity: 0.7; }
236
+ [data-enigma-toast-close]:hover { opacity: 1; }
237
+
238
+ @media (max-width: 600px) {
239
+ [data-enigma-toaster] { right: 14px; left: 14px; width: auto; }
240
+ }
241
+
242
+ @media (prefers-reduced-motion: reduce) {
243
+ /* The movement goes, the state change stays: a toast still appears and still leaves. */
244
+ [data-enigma-toast], [data-enigma-toast] > * { transition: opacity 200ms !important; }
245
+ [data-enigma-toast][data-mounted] { transform: none !important; }
246
+ }
247
+ `;