@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.
@@ -0,0 +1,465 @@
1
+ import { defaultQueue } from './chunk-QYMUIW5I.js';
2
+ import { useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/react/toast-styles.ts
6
+ var TOAST_STYLES = `
7
+ [data-enigma-toaster] {
8
+ --enigma-toast-width: 356px;
9
+ --enigma-toast-edge: 24px;
10
+ --enigma-toast-gap: 14px;
11
+ --enigma-toast-peek: 14px;
12
+ --enigma-toast-scale-step: 0.06;
13
+ --enigma-toast-radius: 12px;
14
+ --enigma-toast-enter: 420ms;
15
+ --enigma-toast-exit: 350ms;
16
+
17
+ --enigma-toast-bg: #101010;
18
+ --enigma-toast-border: #2a2a2a;
19
+ --enigma-toast-text: #f5f5f5;
20
+ --enigma-toast-muted: #a3a3a3;
21
+ --enigma-toast-shadow: 0 6px 22px rgb(0 0 0 / 34%);
22
+
23
+ --enigma-toast-success: hsl(150 80% 72%);
24
+ --enigma-toast-success-bg: hsl(150 38% 9%);
25
+ --enigma-toast-success-border: hsl(147 55% 18%);
26
+ --enigma-toast-error: hsl(358 100% 82%);
27
+ --enigma-toast-error-bg: hsl(358 42% 12%);
28
+ --enigma-toast-error-border: hsl(357 55% 24%);
29
+ --enigma-toast-info: hsl(216 87% 74%);
30
+ --enigma-toast-info-bg: hsl(215 45% 11%);
31
+ --enigma-toast-info-border: hsl(223 50% 24%);
32
+ --enigma-toast-warning: hsl(46 87% 70%);
33
+ --enigma-toast-warning-bg: hsl(40 55% 9%);
34
+ --enigma-toast-warning-border: hsl(40 55% 20%);
35
+
36
+ position: fixed;
37
+ z-index: 999999;
38
+ width: var(--enigma-toast-width);
39
+ margin: 0;
40
+ padding: 0;
41
+ list-style: none;
42
+ outline: none;
43
+ font-family: inherit;
44
+ font-size: 13px;
45
+ /* The stack is a positioning context: every toast is absolute inside it, which is what
46
+ lets the ones behind be clipped to the front one's height. */
47
+ transition: transform 350ms ease;
48
+ }
49
+
50
+ @media (prefers-color-scheme: light) {
51
+ [data-enigma-toaster] {
52
+ --enigma-toast-bg: #ffffff;
53
+ --enigma-toast-border: #e5e5e5;
54
+ --enigma-toast-text: #171717;
55
+ --enigma-toast-muted: #737373;
56
+ --enigma-toast-shadow: 0 6px 22px rgb(0 0 0 / 12%);
57
+ --enigma-toast-success-bg: hsl(143 85% 96%);
58
+ --enigma-toast-success-border: hsl(145 92% 87%);
59
+ --enigma-toast-success: hsl(140 100% 27%);
60
+ --enigma-toast-error-bg: hsl(359 100% 97%);
61
+ --enigma-toast-error-border: hsl(359 100% 94%);
62
+ --enigma-toast-error: hsl(360 100% 45%);
63
+ --enigma-toast-info-bg: hsl(208 100% 97%);
64
+ --enigma-toast-info-border: hsl(221 91% 93%);
65
+ --enigma-toast-info: hsl(210 92% 45%);
66
+ --enigma-toast-warning-bg: hsl(49 100% 97%);
67
+ --enigma-toast-warning-border: hsl(49 91% 84%);
68
+ --enigma-toast-warning: hsl(31 92% 45%);
69
+ }
70
+ }
71
+
72
+ [data-enigma-toaster][data-position^="top"] { top: var(--enigma-toast-edge); }
73
+ [data-enigma-toaster][data-position^="bottom"] { bottom: var(--enigma-toast-edge); }
74
+ [data-enigma-toaster][data-position$="-right"] { right: var(--enigma-toast-edge); }
75
+ [data-enigma-toaster][data-position$="-left"] { left: var(--enigma-toast-edge); }
76
+ [data-enigma-toaster][data-position$="-center"] { left: 50%; transform: translateX(-50%); }
77
+ /* The whole stack lifts a little when it fans out, so the front toast does not sit on the
78
+ exact pixel it did a moment ago. */
79
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] { transform: translateY(-6px); }
80
+ [data-enigma-toaster][data-expanded][data-position^="top"] { transform: translateY(6px); }
81
+ [data-enigma-toaster][data-expanded][data-position="bottom-center"] { transform: translateX(-50%) translateY(-6px); }
82
+ [data-enigma-toaster][data-expanded][data-position="top-center"] { transform: translateX(-50%) translateY(6px); }
83
+
84
+ [data-enigma-toast] {
85
+ position: absolute;
86
+ left: 0;
87
+ right: 0;
88
+ width: 100%;
89
+ box-sizing: border-box;
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 10px;
93
+ padding: 15px 16px;
94
+ color: var(--enigma-toast-text);
95
+ background: var(--enigma-toast-bg);
96
+ border: 1px solid var(--enigma-toast-border);
97
+ border-radius: var(--enigma-toast-radius);
98
+ box-shadow: var(--enigma-toast-shadow);
99
+ overflow-wrap: anywhere;
100
+ touch-action: none;
101
+ outline: none;
102
+ z-index: var(--enigma-toast-z, 1);
103
+ transition:
104
+ transform var(--enigma-toast-enter) cubic-bezier(0.21, 1.02, 0.73, 1),
105
+ opacity var(--enigma-toast-exit),
106
+ height 350ms;
107
+ }
108
+
109
+ [data-enigma-toaster][data-position^="bottom"] [data-enigma-toast] { bottom: 0; transform-origin: bottom center; }
110
+ [data-enigma-toaster][data-position^="top"] [data-enigma-toast] { top: 0; transform-origin: top center; }
111
+
112
+ /* Enter from off screen, in the direction the stack is pinned to. */
113
+ [data-enigma-toaster][data-position^="bottom"] [data-enigma-toast] { transform: translateY(110%); opacity: 0; }
114
+ [data-enigma-toaster][data-position^="top"] [data-enigma-toast] { transform: translateY(-110%); opacity: 0; }
115
+ /* Scoped to the toaster so it OUTRANKS the enter rules above, which are scoped the same
116
+ way: at equal specificity the later rule wins, and a mounted toast that lost this one sat
117
+ at its entry offset forever - visible, and 110% of its own height off the bottom. */
118
+ [data-enigma-toaster] [data-enigma-toast][data-mounted] { opacity: 1; transform: translateY(0); }
119
+
120
+ /* Collapsed: the ones behind are clipped to the FRONT toast's height and scaled back, so
121
+ the stack reads as depth rather than as a list. Their content is hidden but present -
122
+ removing it would make the box jump when the stack expands. */
123
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast][data-mounted]:not([data-front]) {
124
+ height: var(--enigma-toast-front-height);
125
+ transform: translateY(calc(var(--enigma-toast-before) * var(--enigma-toast-peek) * -1))
126
+ scale(calc(1 - var(--enigma-toast-before) * var(--enigma-toast-scale-step)));
127
+ }
128
+ [data-enigma-toaster][data-position^="top"]:not([data-expanded]) [data-enigma-toast][data-mounted]:not([data-front]) {
129
+ transform: translateY(calc(var(--enigma-toast-before) * var(--enigma-toast-peek)))
130
+ scale(calc(1 - var(--enigma-toast-before) * var(--enigma-toast-scale-step)));
131
+ }
132
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast]:not([data-front]) > * { opacity: 0; }
133
+ [data-enigma-toast] > * { transition: opacity 350ms; }
134
+
135
+ /* Expanded: each toast is lifted by the summed height of the ones in front of it. */
136
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] [data-enigma-toast][data-mounted] {
137
+ transform: translateY(calc(var(--enigma-toast-offset) * -1));
138
+ }
139
+ [data-enigma-toaster][data-expanded][data-position^="top"] [data-enigma-toast][data-mounted] {
140
+ transform: translateY(var(--enigma-toast-offset));
141
+ }
142
+
143
+ /* Past the visible count it is still in the DOM, still counted, and not on screen. */
144
+ [data-enigma-toast][data-hidden] { opacity: 0; pointer-events: none; }
145
+
146
+ [data-enigma-toast][data-state="leaving"] { opacity: 0; }
147
+ [data-enigma-toaster]:not([data-expanded]) [data-enigma-toast][data-state="leaving"] { transform: translateY(150%) scale(0.9); }
148
+ [data-enigma-toaster][data-expanded][data-position^="bottom"] [data-enigma-toast][data-state="leaving"] {
149
+ transform: translateY(calc(var(--enigma-toast-offset) * -1 + 110%));
150
+ }
151
+
152
+ /* While a finger is down the toast tracks it exactly: a transition here would lag behind the
153
+ pointer, which reads as a broken gesture rather than a smooth one. */
154
+ [data-enigma-toast][data-swiping] {
155
+ transition: opacity 200ms;
156
+ transform: translateX(var(--enigma-toast-swipe, 0px));
157
+ }
158
+
159
+ [data-enigma-toast]:focus-visible { box-shadow: var(--enigma-toast-shadow), 0 0 0 2px var(--enigma-toast-info); }
160
+
161
+ [data-enigma-toast][data-tone="success"] {
162
+ color: var(--enigma-toast-success);
163
+ background: var(--enigma-toast-success-bg);
164
+ border-color: var(--enigma-toast-success-border);
165
+ }
166
+ [data-enigma-toast][data-tone="error"] {
167
+ color: var(--enigma-toast-error);
168
+ background: var(--enigma-toast-error-bg);
169
+ border-color: var(--enigma-toast-error-border);
170
+ }
171
+ [data-enigma-toast][data-tone="info"] {
172
+ color: var(--enigma-toast-info);
173
+ background: var(--enigma-toast-info-bg);
174
+ border-color: var(--enigma-toast-info-border);
175
+ }
176
+ [data-enigma-toast][data-tone="warning"] {
177
+ color: var(--enigma-toast-warning);
178
+ background: var(--enigma-toast-warning-bg);
179
+ border-color: var(--enigma-toast-warning-border);
180
+ }
181
+
182
+ [data-enigma-toast-icon] { display: flex; flex-shrink: 0; width: 20px; height: 20px; }
183
+ [data-enigma-toast-icon] svg { width: 20px; height: 20px; display: block; }
184
+ [data-enigma-toast-icon][data-tone="loading"] svg { animation: enigma-toast-spin 900ms linear infinite; }
185
+ @keyframes enigma-toast-spin { to { transform: rotate(360deg); } }
186
+
187
+ [data-enigma-toast-content] { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
188
+ [data-enigma-toast-title] { margin: 0; font-weight: 600; line-height: 1.4; }
189
+ [data-enigma-toast-body] { margin: 0; font-size: 12px; line-height: 1.45; opacity: 0.75; }
190
+
191
+ [data-enigma-toast-action] {
192
+ flex-shrink: 0;
193
+ padding: 5px 10px;
194
+ font: inherit;
195
+ font-size: 12px;
196
+ font-weight: 600;
197
+ color: var(--enigma-toast-bg);
198
+ background: currentColor;
199
+ border: 0;
200
+ border-radius: 6px;
201
+ cursor: pointer;
202
+ }
203
+
204
+ [data-enigma-toast-close] {
205
+ flex-shrink: 0;
206
+ display: grid;
207
+ place-items: center;
208
+ width: 20px;
209
+ height: 20px;
210
+ padding: 0;
211
+ color: inherit;
212
+ background: none;
213
+ border: 0;
214
+ border-radius: 5px;
215
+ cursor: pointer;
216
+ /* Shown on hover or focus, so it never covers the message while it is being read - but
217
+ always reachable by keyboard, which display:none would take away. */
218
+ opacity: 0;
219
+ transition: opacity 120ms ease-out;
220
+ }
221
+ [data-enigma-toast]:hover [data-enigma-toast-close],
222
+ [data-enigma-toast-close]:focus-visible { opacity: 0.7; }
223
+ [data-enigma-toast-close]:hover { opacity: 1; }
224
+
225
+ @media (max-width: 600px) {
226
+ [data-enigma-toaster] { right: 14px; left: 14px; width: auto; }
227
+ }
228
+
229
+ @media (prefers-reduced-motion: reduce) {
230
+ /* The movement goes, the state change stays: a toast still appears and still leaves. */
231
+ [data-enigma-toast], [data-enigma-toast] > * { transition: opacity 200ms !important; }
232
+ [data-enigma-toast][data-mounted] { transform: none !important; }
233
+ }
234
+ `;
235
+ var VISIBLE = 3;
236
+ var injected = false;
237
+ function injectStyles() {
238
+ if (injected || typeof document === "undefined") return;
239
+ injected = true;
240
+ if (document.querySelector("[data-enigma-toast-styles]")) return;
241
+ const element = document.createElement("style");
242
+ element.setAttribute("data-enigma-toast-styles", "");
243
+ element.textContent = TOAST_STYLES;
244
+ document.head.prepend(element);
245
+ }
246
+ var ICONS = {
247
+ success: /* @__PURE__ */ jsx("path", { d: "M10 18a8 8 0 100-16 8 8 0 000 16zm3.86-9.81a.75.75 0 00-1.22-.88l-3.48 4.79-1.88-1.88a.75.75 0 10-1.06 1.06l2.5 2.5a.75.75 0 001.14-.09l4-5.5z" }),
248
+ error: /* @__PURE__ */ jsx("path", { d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" }),
249
+ warning: /* @__PURE__ */ jsx("path", { d: "M9.4 3a1.5 1.5 0 012.6 0l7.36 12.75A1.5 1.5 0 0118.06 18H3.94a1.5 1.5 0 01-1.3-2.25L10 3zm.85 4.5a.75.75 0 00-1.5 0v4a.75.75 0 001.5 0v-4zM10 15a.9.9 0 100-1.8.9.9 0 000 1.8z" }),
250
+ info: /* @__PURE__ */ jsx("path", { d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.25a.25.25 0 01.25.3l-.46 2.07A1.75 1.75 0 0010.75 15H11a.75.75 0 000-1.5h-.25a.25.25 0 01-.25-.3l.46-2.07A1.75 1.75 0 009.25 9H9z" })
251
+ };
252
+ function ToneIcon({ tone }) {
253
+ if (!tone || tone === "default") return null;
254
+ if (tone === "loading") {
255
+ return /* @__PURE__ */ jsx("span", { "data-enigma-toast-icon": "", "data-tone": "loading", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.22-8.56", opacity: "0.85" }) }) });
256
+ }
257
+ const glyph = ICONS[tone];
258
+ if (!glyph) return null;
259
+ return /* @__PURE__ */ jsx("span", { "data-enigma-toast-icon": "", "data-tone": tone, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: glyph }) });
260
+ }
261
+ function swipeAxis(position) {
262
+ if (position.endsWith("-center")) return { axis: "y", sign: position.startsWith("top") ? -1 : 1 };
263
+ return { axis: "x", sign: position.endsWith("-left") ? -1 : 1 };
264
+ }
265
+ function Toaster({
266
+ queue = defaultQueue,
267
+ position = "bottom-right",
268
+ exitDuration = 350,
269
+ swipeThreshold = 60,
270
+ visibleCount = VISIBLE,
271
+ expand = false,
272
+ styles: withStyles = true,
273
+ children,
274
+ label = "Notifications",
275
+ className,
276
+ style
277
+ }) {
278
+ const [rendered, setRendered] = useState(
279
+ () => queue.items.map((notification) => ({ notification, leaving: false }))
280
+ );
281
+ const timers = useRef(/* @__PURE__ */ new Map());
282
+ const [hovered, setHovered] = useState(false);
283
+ const [heights, setHeights] = useState({});
284
+ useLayoutEffect(() => {
285
+ if (withStyles) injectStyles();
286
+ }, [withStyles]);
287
+ useEffect(() => {
288
+ const sync = (items) => {
289
+ setRendered((current) => {
290
+ const live = new Map(items.map((item) => [item.id, item]));
291
+ const kept = current.map((entry) => {
292
+ const still = live.get(entry.notification.id);
293
+ if (still) return { notification: still, leaving: false };
294
+ if (entry.leaving) return entry;
295
+ const timer = setTimeout(() => {
296
+ timers.current.delete(entry.notification.id);
297
+ setRendered((later) => later.filter((row) => row.notification.id !== entry.notification.id));
298
+ }, exitDuration);
299
+ timers.current.set(entry.notification.id, timer);
300
+ return { ...entry, leaving: true };
301
+ });
302
+ const seen = new Set(kept.map((entry) => entry.notification.id));
303
+ const added = items.filter((item) => !seen.has(item.id)).map((item) => ({ notification: item, leaving: false }));
304
+ return [...kept, ...added];
305
+ });
306
+ };
307
+ sync(queue.items);
308
+ return queue.subscribe(sync);
309
+ }, [queue, exitDuration]);
310
+ useEffect(() => {
311
+ const pending = timers.current;
312
+ return () => {
313
+ for (const timer of pending.values()) clearTimeout(timer);
314
+ pending.clear();
315
+ };
316
+ }, []);
317
+ if (!rendered.length) return null;
318
+ const expanded = expand || hovered;
319
+ const stack = [...rendered].reverse();
320
+ const frontHeight = heights[stack[0]?.notification.id ?? ""] ?? 0;
321
+ return /* @__PURE__ */ jsx(
322
+ "section",
323
+ {
324
+ "aria-label": label,
325
+ "data-enigma-toaster": "",
326
+ "data-position": position,
327
+ "data-expanded": expanded ? "" : void 0,
328
+ style: { ...style, "--enigma-toast-front-height": `${frontHeight}px` },
329
+ className,
330
+ onPointerEnter: () => {
331
+ queue.pause();
332
+ setHovered(true);
333
+ },
334
+ onPointerLeave: () => {
335
+ queue.resume();
336
+ setHovered(false);
337
+ },
338
+ onFocusCapture: () => {
339
+ queue.pause();
340
+ setHovered(true);
341
+ },
342
+ onBlurCapture: () => {
343
+ queue.resume();
344
+ setHovered(false);
345
+ },
346
+ children: stack.map((entry, index) => /* @__PURE__ */ jsx(
347
+ Toast,
348
+ {
349
+ entry,
350
+ index,
351
+ hidden: index >= visibleCount,
352
+ offset: stack.slice(0, index).reduce((total, row) => total + (heights[row.notification.id] ?? 0) + 14, 0),
353
+ onMeasure: (height) => setHeights((current) => current[entry.notification.id] === height ? current : { ...current, [entry.notification.id]: height }),
354
+ position,
355
+ swipeThreshold,
356
+ onDismiss: () => queue.dismiss(entry.notification.id),
357
+ render: children
358
+ },
359
+ entry.notification.id
360
+ ))
361
+ }
362
+ );
363
+ }
364
+ function Toast({ entry, index, hidden, offset: stackOffset, onMeasure, position, swipeThreshold, onDismiss, render }) {
365
+ const { notification, leaving } = entry;
366
+ const [offset, setOffset] = useState(0);
367
+ const [swiping, setSwiping] = useState(false);
368
+ const [mounted, setMounted] = useState(false);
369
+ const node = useRef(null);
370
+ const start = useRef(0);
371
+ const { axis, sign } = swipeAxis(position);
372
+ useLayoutEffect(() => {
373
+ const element = node.current;
374
+ if (!element) return;
375
+ const report = () => onMeasure(element.getBoundingClientRect().height);
376
+ report();
377
+ if (typeof ResizeObserver === "undefined") return;
378
+ const observer = new ResizeObserver(report);
379
+ observer.observe(element);
380
+ return () => observer.disconnect();
381
+ }, [notification.title, notification.body]);
382
+ useEffect(() => {
383
+ const frame = requestAnimationFrame(() => setMounted(true));
384
+ return () => cancelAnimationFrame(frame);
385
+ }, []);
386
+ const onPointerDown = useCallback((event) => {
387
+ if (event.target.closest("button")) return;
388
+ if (event.pointerType === "mouse" && event.button !== 0) return;
389
+ event.currentTarget.setPointerCapture(event.pointerId);
390
+ start.current = axis === "x" ? event.clientX : event.clientY;
391
+ setSwiping(true);
392
+ }, [axis]);
393
+ const onPointerMove = useCallback((event) => {
394
+ if (!swiping) return;
395
+ const delta = (axis === "x" ? event.clientX : event.clientY) - start.current;
396
+ setOffset(sign > 0 ? Math.max(0, delta) : Math.min(0, delta));
397
+ }, [swiping, axis, sign]);
398
+ const onPointerUp = useCallback(() => {
399
+ if (!swiping) return;
400
+ setSwiping(false);
401
+ if (Math.abs(offset) >= swipeThreshold) {
402
+ onDismiss();
403
+ return;
404
+ }
405
+ setOffset(0);
406
+ }, [swiping, offset, swipeThreshold, onDismiss]);
407
+ const controls = { dismiss: onDismiss };
408
+ return /* @__PURE__ */ jsx(
409
+ "article",
410
+ {
411
+ ref: (element) => {
412
+ node.current = element;
413
+ },
414
+ "data-enigma-toast": "",
415
+ "data-tone": notification.tone,
416
+ "data-state": leaving ? "leaving" : "open",
417
+ "data-index": index,
418
+ "data-front": index === 0 ? "" : void 0,
419
+ "data-mounted": mounted && !leaving ? "" : void 0,
420
+ "data-hidden": hidden ? "" : void 0,
421
+ "data-swiping": swiping ? "" : void 0,
422
+ role: notification.tone === "error" ? "alert" : "status",
423
+ "aria-live": notification.tone === "error" ? "assertive" : "polite",
424
+ tabIndex: 0,
425
+ onKeyDown: (event) => {
426
+ if (event.key === "Escape") onDismiss();
427
+ },
428
+ onPointerDown,
429
+ onPointerMove,
430
+ onPointerUp,
431
+ onPointerCancel: onPointerUp,
432
+ style: {
433
+ // Read by the theme, so every transform stays the stylesheet's decision.
434
+ "--enigma-toast-swipe": `${offset}px`,
435
+ "--enigma-toast-index": index,
436
+ "--enigma-toast-before": index,
437
+ "--enigma-toast-offset": `${stackOffset}px`,
438
+ // The front toast is on top, and each one behind it a layer lower.
439
+ "--enigma-toast-z": 100 - index
440
+ },
441
+ children: render ? render(notification, controls) : /* @__PURE__ */ jsxs(Fragment, { children: [
442
+ /* @__PURE__ */ jsx(ToneIcon, { tone: notification.tone }),
443
+ /* @__PURE__ */ jsxs("span", { "data-enigma-toast-content": "", children: [
444
+ /* @__PURE__ */ jsx("p", { "data-enigma-toast-title": "", children: notification.title }),
445
+ notification.body && /* @__PURE__ */ jsx("p", { "data-enigma-toast-body": "", children: notification.body })
446
+ ] }),
447
+ notification.action && /* @__PURE__ */ jsx(
448
+ "button",
449
+ {
450
+ type: "button",
451
+ "data-enigma-toast-action": "",
452
+ onClick: () => {
453
+ notification.action?.onSelect();
454
+ if (notification.action?.dismiss !== false) onDismiss();
455
+ },
456
+ children: notification.action.label
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-toast-close": "", "aria-label": "Dismiss", onClick: onDismiss, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
460
+ ] })
461
+ }
462
+ );
463
+ }
464
+
465
+ export { Toaster };
@@ -22,9 +22,56 @@ function resetFlagConfig() {
22
22
  var FLAG_RATIO = { rect: 4 / 3, square: 1, circle: 1 };
23
23
  var EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
24
24
  var LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
25
- var FILE_CODE = /^[a-z]{2,}(?:[-_][a-z0-9]+)*$/;
25
+ var FILE_CODE = /^[a-z]{2,}[-_][a-z0-9_-]+$/;
26
26
  var COUNTRY_CODE = /^[a-z]{2}$/;
27
- function normalizeFlagCode(value) {
27
+ function fold(value) {
28
+ return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/['‘’ʼ`´]/g, "").toLowerCase().replace(/&/g, " and ").replace(/\bst\.?\s/g, "saint ").replace(/^the\s+/, "").replace(/[.,]/g, "").replace(/\s+/g, " ").trim();
29
+ }
30
+ var nameIndexes = /* @__PURE__ */ new Map();
31
+ function canonicalRegion(code) {
32
+ try {
33
+ return new Intl.Locale(`und-${code}`).region?.toLowerCase() ?? code.toLowerCase();
34
+ } catch {
35
+ return code.toLowerCase();
36
+ }
37
+ }
38
+ function unknownRegionName(names) {
39
+ return names.of("ZZ");
40
+ }
41
+ function regionsByName(locale) {
42
+ const cached = nameIndexes.get(locale);
43
+ if (cached !== void 0) return cached;
44
+ let index = null;
45
+ try {
46
+ const names = new Intl.DisplayNames([locale], { type: "region" });
47
+ const unknown = unknownRegionName(names);
48
+ index = /* @__PURE__ */ new Map();
49
+ for (let first = 65; first <= 90; first++) {
50
+ for (let second = 65; second <= 90; second++) {
51
+ const code = String.fromCharCode(first, second);
52
+ const name = names.of(code);
53
+ if (!name || name === code || name === unknown) continue;
54
+ const key = fold(name);
55
+ if (!index.has(key)) index.set(key, canonicalRegion(code));
56
+ }
57
+ }
58
+ } catch {
59
+ index = null;
60
+ }
61
+ nameIndexes.set(locale, index);
62
+ return index;
63
+ }
64
+ function isKnownRegion(code) {
65
+ try {
66
+ const names = new Intl.DisplayNames(["en"], { type: "region" });
67
+ const upper = code.toUpperCase();
68
+ const name = names.of(upper);
69
+ return Boolean(name) && name !== upper && name !== unknownRegionName(names);
70
+ } catch {
71
+ return true;
72
+ }
73
+ }
74
+ function normalizeFlagCode(value, locale) {
28
75
  if (!value) return null;
29
76
  const raw = value.trim();
30
77
  if (!raw) return null;
@@ -32,10 +79,18 @@ function normalizeFlagCode(value) {
32
79
  const letters = [...raw].map((char) => String.fromCharCode((char.codePointAt(0) ?? 0) - 127462 + 97));
33
80
  return letters.join("");
34
81
  }
35
- const locale = LOCALE_TAG.exec(raw);
36
- if (locale) return locale[2].toLowerCase();
37
- const code = raw.toLowerCase();
38
- return FILE_CODE.test(code) ? code : null;
82
+ const tag = LOCALE_TAG.exec(raw);
83
+ if (tag) return tag[2].toLowerCase();
84
+ const lower = raw.toLowerCase();
85
+ if (COUNTRY_CODE.test(lower)) return isKnownRegion(lower) ? canonicalRegion(lower) : null;
86
+ if (FILE_CODE.test(lower)) return lower;
87
+ const folded = fold(raw);
88
+ for (const candidate of [locale, documentLocale(), "en", runtimeLocale()]) {
89
+ if (!candidate) continue;
90
+ const found = regionsByName(candidate)?.get(folded);
91
+ if (found) return found;
92
+ }
93
+ return null;
39
94
  }
40
95
  function flagName(code, locale) {
41
96
  const normalized = normalizeFlagCode(code);
@@ -49,6 +104,13 @@ function flagName(code, locale) {
49
104
  return null;
50
105
  }
51
106
  }
107
+ function runtimeLocale() {
108
+ try {
109
+ return Intl.DateTimeFormat().resolvedOptions().locale;
110
+ } catch {
111
+ return void 0;
112
+ }
113
+ }
52
114
  function documentLocale() {
53
115
  if (typeof document === "undefined") return void 0;
54
116
  return document.documentElement.lang || void 0;
@@ -1,4 +1,4 @@
1
- import { flagView } from './chunk-T4F3BWR5.js';
1
+ import { flagView } from './chunk-LMZJBOZG.js';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  function Flag({ code, label, decorative = false, locale, shape, format, source, basePath, size, fallback = null, ...rest }) {
@@ -71,12 +71,20 @@ declare const FLAG_RATIO: Record<FlagShape, number>;
71
71
  /**
72
72
  * Anything a codebase calls a country into the file name the sets use, or null.
73
73
  *
74
- * Null rather than a guess: a code this cannot resolve would otherwise become a 404 image
75
- * with no alt text, and a broken flag beside a country name is worse than no flag at all.
76
- * An emoji flag is accepted and converted, so migrating an existing picker is a rename of
77
- * the component and nothing else.
74
+ * What it accepts: a code (`es`, `ES`), a locale tag (`en-GB`, `es_ES`), a subdivision or
75
+ * named file (`gb-eng`, `au-nsw`, `easter_island`), the emoji flag you are replacing, and
76
+ * the country's NAME - in English or in the page's own language, accents optional, so
77
+ * `Spain`, `España` and `espana` all resolve. A name is what a database column or an API
78
+ * payload usually holds, and requiring the caller to convert it first is how a flag ends up
79
+ * not being rendered at all.
80
+ *
81
+ * Null rather than a guess, and that is the point of the strictness: `banana` used to pass
82
+ * as a file code and render as a 404 with no alt text, which is worse than no flag. A
83
+ * two-letter code is checked against the runtime's own region list, a bare word is resolved
84
+ * as a name or refused, and only a separator-carrying file name is taken on trust, because
85
+ * nothing can check those without shipping the whole directory listing.
78
86
  */
79
- declare function normalizeFlagCode(value: string | null | undefined): string | null;
87
+ declare function normalizeFlagCode(value: string | null | undefined, locale?: string): string | null;
80
88
  /**
81
89
  * The country's name, in the reader's language, or null.
82
90
  *
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { E as EstimateOptions, G as GeneratePasswordOptions, P as PasswordAlphab
6
6
  export { N as Notification, a as NotificationAction, b as NotificationInput, c as NotificationTone, d as Notifications, e as NotificationsOptions, P as PromiseMessages, f as createNotifications } from './notifications-BpVV6sel.js';
7
7
  export { N as NetworkMonitor, a as NetworkState, S as SERVER_NETWORK_STATE, c as createNetworkMonitor } from './network-D2LsBG_k.js';
8
8
  export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePrecision, c as RelativeTimeStyle, d as RelativeTimeTense, e as RelativeTimeView, f as ensureZone, n as normalizeDate, p as parseDuration, r as relativeTimeAttributes, g as relativeTimeView } from './relative-time-YpRTG7YH.js';
9
- export { F as FLAG_CDN, a as FLAG_RATIO, b as FlagConfig, c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, g as FlagView, h as configureFlags, i as flagAttributes, j as flagConfig, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode, r as resetFlagConfig } from './flags-fLwIl2hG.js';
9
+ export { F as FLAG_CDN, a as FLAG_RATIO, b as FlagConfig, c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, g as FlagView, h as configureFlags, i as flagAttributes, j as flagConfig, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode, r as resetFlagConfig } from './flags-BQU1V9rE.js';
10
10
  export { P as PaletteKey, a as PositionedRow, R as RecentEntry, b as RecentStore, c as RecentStoreOptions, d as RowGroup, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from './palette-D7iuQh_T.js';
11
11
 
12
12
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createInput } from './chunk-KJINGUQN.js';
2
2
  export { estimatePasswordStrength, generatePassword } from './chunk-HC2ME5PU.js';
3
- export { FLAG_CDN, FLAG_RATIO, configureFlags, flagAttributes, flagConfig, flagName, flagSrc, flagView, normalizeFlagCode, resetFlagConfig } from './chunk-T4F3BWR5.js';
3
+ export { FLAG_CDN, FLAG_RATIO, configureFlags, flagAttributes, flagConfig, flagName, flagSrc, flagView, normalizeFlagCode, resetFlagConfig } from './chunk-LMZJBOZG.js';
4
4
  export { createMarquee } from './chunk-UOSSNUSC.js';
5
5
  export { SERVER_NETWORK_STATE, createNetworkMonitor } from './chunk-2QFTRNAZ.js';
6
6
  export { createNotifications } from './chunk-SNYUBXWQ.js';
@@ -18,7 +18,7 @@ export { RelativeTime, RelativeTimeProps } from '../react/relative-time.js';
18
18
  export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePrecision, c as RelativeTimeStyle, d as RelativeTimeTense } from '../relative-time-YpRTG7YH.js';
19
19
  export { N as Notification, a as NotificationAction, b as NotificationInput, c as NotificationTone, e as NotificationsOptions } from '../notifications-BpVV6sel.js';
20
20
  export { Flag, FlagProps } from '../react/flag.js';
21
- export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-fLwIl2hG.js';
21
+ export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-BQU1V9rE.js';
22
22
  export { PaletteContent, PaletteContextValue, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteListProps, PaletteRoot, PaletteRootProps, PaletteRow, PaletteSection, PaletteTrigger, SearchPalette, SearchPaletteProps, usePaletteContext } from '../react/palette.js';
23
23
  export { R as RecentEntry, b as RecentStore, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from '../palette-D7iuQh_T.js';
24
24
 
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  export { useInput } from '../chunk-U3V4EHOB.js';
3
3
  export { PasswordStrength } from '../chunk-XQHCZAPJ.js';
4
- export { Flag } from '../chunk-XPKWJD5O.js';
5
- export { Toaster } from '../chunk-ZCUFYBPB.js';
4
+ export { Flag } from '../chunk-NPHRS2Q7.js';
5
+ export { Toaster } from '../chunk-JCCL7XKC.js';
6
6
  export { useMarquee } from '../chunk-ZWR2EXHQ.js';
7
7
  export { useSearch } from '../chunk-QQFNAKMY.js';
8
8
  export { useNetworkState } from '../chunk-S653GLSF.js';
@@ -10,7 +10,7 @@ export { createNotificationQueue, defaultQueue, useNotifications } from '../chun
10
10
  export { RelativeTime } from '../chunk-OCMI7R6H.js';
11
11
  import '../chunk-KJINGUQN.js';
12
12
  export { estimatePasswordStrength, generatePassword } from '../chunk-HC2ME5PU.js';
13
- export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-T4F3BWR5.js';
13
+ export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-LMZJBOZG.js';
14
14
  import '../chunk-UOSSNUSC.js';
15
15
  import '../chunk-2QFTRNAZ.js';
16
16
  import '../chunk-SNYUBXWQ.js';
@@ -1,5 +1,5 @@
1
1
  import { ImgHTMLAttributes, ReactNode } from 'react';
2
- import { e as FlagShape, c as FlagFormat, f as FlagSource } from '../flags-fLwIl2hG.js';
2
+ import { e as FlagShape, c as FlagFormat, f as FlagSource } from '../flags-BQU1V9rE.js';
3
3
 
4
4
  interface FlagProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "width" | "height"> {
5
5
  /** `es`, `ES`, `en-GB`, `gb-eng`, or the emoji flag you are replacing. */
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- export { Flag } from '../chunk-XPKWJD5O.js';
3
- import '../chunk-T4F3BWR5.js';
2
+ export { Flag } from '../chunk-NPHRS2Q7.js';
3
+ import '../chunk-LMZJBOZG.js';