@enigmax/primitives 0.19.1 → 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.
- package/dist/chunk-JCCL7XKC.js +465 -0
- package/dist/{chunk-T4F3BWR5.js → chunk-LMZJBOZG.js} +68 -6
- package/dist/{chunk-XPKWJD5O.js → chunk-NPHRS2Q7.js} +1 -1
- package/dist/{flags-fLwIl2hG.d.ts → flags-BQU1V9rE.d.ts} +13 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +3 -3
- package/dist/react/flag.d.ts +1 -1
- package/dist/react/flag.js +2 -2
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +3 -3
- package/dist/react/toast.d.ts +14 -13
- package/dist/react/toast.js +2 -2
- package/dist/react-router/index.d.ts +1 -1
- package/dist/react-router/index.js +3 -3
- package/package.json +1 -1
- package/recipes/toast/styles.css +173 -100
- package/registry.json +9 -4
- package/src/core/flags.ts +155 -11
- package/src/react/toast-styles.ts +247 -0
- package/src/react/toaster.tsx +174 -21
- package/dist/chunk-ZCUFYBPB.js +0 -154
package/src/core/flags.ts
CHANGED
|
@@ -106,20 +106,141 @@ const EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
|
|
|
106
106
|
* two the same string, and then one of them is always wrong.
|
|
107
107
|
*/
|
|
108
108
|
const LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
|
|
109
|
-
/**
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* A SUBDIVISION or a named file, which is the only shape that cannot be checked against
|
|
111
|
+
* anything: `gb-eng`, `au-nsw`, `es-ct`, `easter_island`. It must carry a separator - that
|
|
112
|
+
* is what distinguishes a file name from an ordinary word, and without it `banana` was
|
|
113
|
+
* accepted as a code and rendered as a 404.
|
|
114
|
+
*/
|
|
115
|
+
const FILE_CODE = /^[a-z]{2,}[-_][a-z0-9_-]+$/;
|
|
111
116
|
/** A plain country: exactly the shape `Intl.DisplayNames` can put a name to. */
|
|
112
117
|
const COUNTRY_CODE = /^[a-z]{2}$/;
|
|
113
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Accent-, case- and apostrophe-insensitive, so `espana`, `España` and `ESPAÑA` are one
|
|
121
|
+
* string - and so are `Cote d'Ivoire` and `Côte d’Ivoire`.
|
|
122
|
+
*
|
|
123
|
+
* The apostrophe matters more than it looks: CLDR writes the typographic one (U+2019) and
|
|
124
|
+
* every keyboard, database and API writes the straight one, so a lookup that kept them
|
|
125
|
+
* apart failed on exactly the countries whose names contain one.
|
|
126
|
+
*/
|
|
127
|
+
function fold(value: string): string {
|
|
128
|
+
return value
|
|
129
|
+
.normalize("NFD")
|
|
130
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
131
|
+
.replace(/['‘’ʼ`´]/g, "")
|
|
132
|
+
.toLowerCase()
|
|
133
|
+
// Three spellings of the same name, and all three are mechanical - CLDR writes
|
|
134
|
+
// "St. Lucia" and "Trinidad & Tobago", while a form, a database and an API write
|
|
135
|
+
// "Saint Lucia" and "Trinidad and Tobago". Both sides of the lookup go through this,
|
|
136
|
+
// so it is a normalization rather than a table of alternative names.
|
|
137
|
+
.replace(/&/g, " and ")
|
|
138
|
+
.replace(/\bst\.?\s/g, "saint ")
|
|
139
|
+
.replace(/^the\s+/, "")
|
|
140
|
+
.replace(/[.,]/g, "")
|
|
141
|
+
.replace(/\s+/g, " ")
|
|
142
|
+
.trim();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Every two-letter region the runtime can name, folded, mapped back to its code.
|
|
147
|
+
*
|
|
148
|
+
* Built by asking `Intl.DisplayNames` for all 676 letter pairs and keeping the ones it has a
|
|
149
|
+
* name for - which is also how an unknown code is detected, since it echoes the input back.
|
|
150
|
+
* The runtime already holds this data, translated and maintained; shipping a table of 250
|
|
151
|
+
* country names would be a copy of it that goes stale.
|
|
152
|
+
*
|
|
153
|
+
* Memoized per locale and built only when something that is not a code arrives, so a page
|
|
154
|
+
* that passes codes never pays for it.
|
|
155
|
+
*/
|
|
156
|
+
const nameIndexes = new Map<string, Map<string, string> | null>();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The code a region is FILED under.
|
|
160
|
+
*
|
|
161
|
+
* `Intl.Locale` canonicalizes a deprecated or alias subtag, which is the difference between
|
|
162
|
+
* a flag and a 404: `UK` is not the ISO code for the United Kingdom (`GB` is), `AN` became
|
|
163
|
+
* `CW`, `SU` became `RU`. All three have a name, so a name lookup that skipped this step
|
|
164
|
+
* resolved "United Kingdom" to `uk` and asked for a file that does not exist.
|
|
165
|
+
*/
|
|
166
|
+
function canonicalRegion(code: string): string {
|
|
167
|
+
try {
|
|
168
|
+
return new Intl.Locale(`und-${code}`).region?.toLowerCase() ?? code.toLowerCase();
|
|
169
|
+
} catch {
|
|
170
|
+
return code.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The name the runtime uses for a region it does not know.
|
|
176
|
+
*
|
|
177
|
+
* `ZZ` IS that region by definition, so its name is the sentinel: any code the runtime
|
|
178
|
+
* cannot place comes back either echoing the code (`QQ`) or carrying this exact string, and
|
|
179
|
+
* both have to be refused. Comparing against `of("ZZ")` rather than the English words keeps
|
|
180
|
+
* that true in every language.
|
|
181
|
+
*/
|
|
182
|
+
function unknownRegionName(names: Intl.DisplayNames): string | undefined {
|
|
183
|
+
return names.of("ZZ");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function regionsByName(locale: string): Map<string, string> | null {
|
|
187
|
+
const cached = nameIndexes.get(locale);
|
|
188
|
+
if (cached !== undefined) return cached;
|
|
189
|
+
|
|
190
|
+
let index: Map<string, string> | null = null;
|
|
191
|
+
try {
|
|
192
|
+
const names = new Intl.DisplayNames([locale], { type: "region" });
|
|
193
|
+
const unknown = unknownRegionName(names);
|
|
194
|
+
index = new Map();
|
|
195
|
+
for (let first = 65; first <= 90; first++) {
|
|
196
|
+
for (let second = 65; second <= 90; second++) {
|
|
197
|
+
const code = String.fromCharCode(first, second);
|
|
198
|
+
const name = names.of(code);
|
|
199
|
+
if (!name || name === code || name === unknown) continue;
|
|
200
|
+
const key = fold(name);
|
|
201
|
+
// First wins, and the code is canonical, so an alias can neither overwrite
|
|
202
|
+
// the real entry nor become one.
|
|
203
|
+
if (!index.has(key)) index.set(key, canonicalRegion(code));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
index = null; // No Intl, or no data for this locale.
|
|
208
|
+
}
|
|
209
|
+
nameIndexes.set(locale, index);
|
|
210
|
+
return index;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether the runtime knows this two-letter code as a region at all. */
|
|
214
|
+
function isKnownRegion(code: string): boolean {
|
|
215
|
+
try {
|
|
216
|
+
const names = new Intl.DisplayNames(["en"], { type: "region" });
|
|
217
|
+
const upper = code.toUpperCase();
|
|
218
|
+
const name = names.of(upper);
|
|
219
|
+
return Boolean(name) && name !== upper && name !== unknownRegionName(names);
|
|
220
|
+
} catch {
|
|
221
|
+
// Without Intl there is nothing to check against, and refusing every code would be
|
|
222
|
+
// worse than passing one through: two letters is the shape of a flag file.
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
114
227
|
/**
|
|
115
228
|
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
116
229
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
230
|
+
* What it accepts: a code (`es`, `ES`), a locale tag (`en-GB`, `es_ES`), a subdivision or
|
|
231
|
+
* named file (`gb-eng`, `au-nsw`, `easter_island`), the emoji flag you are replacing, and
|
|
232
|
+
* the country's NAME - in English or in the page's own language, accents optional, so
|
|
233
|
+
* `Spain`, `España` and `espana` all resolve. A name is what a database column or an API
|
|
234
|
+
* payload usually holds, and requiring the caller to convert it first is how a flag ends up
|
|
235
|
+
* not being rendered at all.
|
|
236
|
+
*
|
|
237
|
+
* Null rather than a guess, and that is the point of the strictness: `banana` used to pass
|
|
238
|
+
* as a file code and render as a 404 with no alt text, which is worse than no flag. A
|
|
239
|
+
* two-letter code is checked against the runtime's own region list, a bare word is resolved
|
|
240
|
+
* as a name or refused, and only a separator-carrying file name is taken on trust, because
|
|
241
|
+
* nothing can check those without shipping the whole directory listing.
|
|
121
242
|
*/
|
|
122
|
-
export function normalizeFlagCode(value: string | null | undefined): string | null {
|
|
243
|
+
export function normalizeFlagCode(value: string | null | undefined, locale?: string): string | null {
|
|
123
244
|
if (!value) return null;
|
|
124
245
|
const raw = value.trim();
|
|
125
246
|
if (!raw) return null;
|
|
@@ -129,11 +250,25 @@ export function normalizeFlagCode(value: string | null | undefined): string | nu
|
|
|
129
250
|
return letters.join("");
|
|
130
251
|
}
|
|
131
252
|
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
253
|
+
const tag = LOCALE_TAG.exec(raw);
|
|
254
|
+
if (tag) return tag[2].toLowerCase();
|
|
255
|
+
|
|
256
|
+
const lower = raw.toLowerCase();
|
|
257
|
+
// Canonical, so `uk` reaches the file the United Kingdom is actually filed under.
|
|
258
|
+
if (COUNTRY_CODE.test(lower)) return isKnownRegion(lower) ? canonicalRegion(lower) : null;
|
|
259
|
+
if (FILE_CODE.test(lower)) return lower;
|
|
134
260
|
|
|
135
|
-
|
|
136
|
-
|
|
261
|
+
// A name, then, in this order: the locale you asked for, the page's own, English, and
|
|
262
|
+
// finally the runtime's. English is always tried because it is what an API or a database
|
|
263
|
+
// column holds even on a translated page; the runtime's is last so it can rescue a name
|
|
264
|
+
// nothing else placed without ever overriding a match that was more specific.
|
|
265
|
+
const folded = fold(raw);
|
|
266
|
+
for (const candidate of [locale, documentLocale(), "en", runtimeLocale()]) {
|
|
267
|
+
if (!candidate) continue;
|
|
268
|
+
const found = regionsByName(candidate)?.get(folded);
|
|
269
|
+
if (found) return found;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
137
272
|
}
|
|
138
273
|
|
|
139
274
|
/**
|
|
@@ -161,6 +296,15 @@ export function flagName(code: string | null | undefined, locale?: string): stri
|
|
|
161
296
|
}
|
|
162
297
|
}
|
|
163
298
|
|
|
299
|
+
/** Whatever language this runtime is set to - the browser's, or the server's. */
|
|
300
|
+
function runtimeLocale(): string | undefined {
|
|
301
|
+
try {
|
|
302
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
164
308
|
/** The page's own language, which is what a translated page wants the name written in. */
|
|
165
309
|
function documentLocale(): string | undefined {
|
|
166
310
|
if (typeof document === "undefined") return undefined;
|
|
@@ -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
|
+
`;
|