@enigmax/primitives 0.16.0 → 0.18.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/button-CaXaqG_K.d.ts +63 -0
- package/dist/chunk-2QFTRNAZ.js +88 -0
- package/dist/chunk-6BGBYUSZ.js +114 -0
- package/dist/chunk-AU3H5WIY.js +107 -0
- package/dist/chunk-F25CGNQC.js +19 -0
- package/dist/chunk-HC2ME5PU.js +168 -0
- package/dist/chunk-HS3X3XCW.js +43 -0
- package/dist/chunk-IIT6U7LM.js +249 -0
- package/dist/chunk-IXVMRVD4.js +96 -0
- package/dist/chunk-KJINGUQN.js +188 -0
- package/dist/chunk-LKA2UG4P.js +542 -0
- package/dist/chunk-MMQPZGSU.js +161 -0
- package/dist/chunk-OCMI7R6H.js +79 -0
- package/dist/chunk-QYMUIW5I.js +28 -0
- package/dist/chunk-S653GLSF.js +17 -0
- package/dist/chunk-SNYUBXWQ.js +149 -0
- package/dist/chunk-U3V4EHOB.js +41 -0
- package/dist/chunk-UOSSNUSC.js +309 -0
- package/dist/chunk-WSB655JB.js +47 -0
- package/dist/chunk-XNNQRA35.js +31 -0
- package/dist/chunk-XQHCZAPJ.js +102 -0
- package/dist/chunk-ZCUFYBPB.js +154 -0
- package/dist/chunk-ZWR2EXHQ.js +55 -0
- package/dist/flags-BBJc9unY.d.ts +133 -0
- package/dist/index-D-ARvWpQ.d.ts +306 -0
- package/dist/index-DNXPtY9X.d.ts +144 -0
- package/dist/index.d.ts +16 -496
- package/dist/index.js +9 -1
- package/dist/input-BwXjFenq.d.ts +77 -0
- package/dist/marquee-CJ3Uwy3E.d.ts +81 -0
- package/dist/network-D2LsBG_k.d.ts +39 -0
- package/dist/next/index.d.ts +20 -3
- package/dist/next/index.js +23 -3
- package/dist/notifications-BpVV6sel.d.ts +70 -0
- package/dist/password-3DRQYAYQ.js +2 -0
- package/dist/password-C8lG4Zm9.d.ts +71 -0
- package/dist/password-FB2CUEKJ.js +1 -0
- package/dist/react/button.d.ts +75 -0
- package/dist/react/button.js +4 -0
- package/dist/react/flag.d.ts +37 -0
- package/dist/react/flag.js +3 -0
- package/dist/react/index.d.ts +28 -286
- package/dist/react/index.js +22 -2
- package/dist/react/input.d.ts +4 -0
- package/dist/react/input.js +3 -0
- package/dist/react/marquee.d.ts +44 -0
- package/dist/react/marquee.js +3 -0
- package/dist/react/network.d.ts +20 -0
- package/dist/react/network.js +3 -0
- package/dist/react/notifications.d.ts +17 -0
- package/dist/react/notifications.js +3 -0
- package/dist/react/palette.d.ts +3 -0
- package/dist/react/palette.js +4 -0
- package/dist/react/relative-time.d.ts +21 -0
- package/dist/react/relative-time.js +3 -0
- package/dist/react/search.d.ts +30 -0
- package/dist/react/search.js +3 -0
- package/dist/react/slot.d.ts +47 -0
- package/dist/react/slot.js +2 -0
- package/dist/react/toast.d.ts +40 -0
- package/dist/react/toast.js +4 -0
- package/dist/react-router/index.d.ts +20 -3
- package/dist/react-router/index.js +23 -3
- package/dist/relative-time-YpRTG7YH.d.ts +106 -0
- package/dist/search-P4OHCRXD.js +50 -0
- package/package.json +53 -1
- package/registry.json +235 -16
- package/src/core/flags.ts +266 -0
- package/src/core/input-icons.ts +34 -0
- package/src/core/input.ts +5 -21
- package/src/core/palette.ts +0 -0
- package/src/index.ts +18 -0
- package/src/react/button.tsx +70 -4
- package/src/react/flag.tsx +70 -0
- package/src/react/index.ts +52 -5
- package/src/react/input/icon.tsx +17 -0
- package/src/react/input/index.tsx +307 -0
- package/src/react/input/password.tsx +174 -0
- package/src/react/input/search.tsx +82 -0
- package/src/react/input/types.ts +146 -0
- package/src/react/input/write-value.ts +18 -0
- package/src/react/palette/context.ts +60 -0
- package/src/react/palette/index.tsx +66 -0
- package/src/react/palette/root.tsx +621 -0
- package/src/react/slot.tsx +91 -0
- package/src/react/use-button.ts +3 -1
- package/dist/chunk-53DQY6LP.js +0 -1164
- package/dist/chunk-U2KYYMBJ.js +0 -724
- package/src/react/input.tsx +0 -429
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { estimatePasswordStrength } from './chunk-HC2ME5PU.js';
|
|
2
|
+
import { useMemo, useRef, useEffect, useState } from 'react';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var SCORE_LABELS = ["Very weak", "Weak", "Fair", "Strong", "Very strong"];
|
|
6
|
+
function PasswordExtras({
|
|
7
|
+
id,
|
|
8
|
+
value,
|
|
9
|
+
strength,
|
|
10
|
+
onStrengthChange,
|
|
11
|
+
onScore,
|
|
12
|
+
breach,
|
|
13
|
+
breachDelay,
|
|
14
|
+
onBreachChange,
|
|
15
|
+
classNames
|
|
16
|
+
}) {
|
|
17
|
+
const estimateOptions = typeof strength === "object" ? strength : void 0;
|
|
18
|
+
const userInputs = estimateOptions?.userInputs;
|
|
19
|
+
const report = useMemo(
|
|
20
|
+
() => strength === false ? null : estimatePasswordStrength(value, { userInputs }),
|
|
21
|
+
// The array is compared by identity, so a literal would re-estimate every render.
|
|
22
|
+
[strength, value, userInputs]
|
|
23
|
+
);
|
|
24
|
+
const strengthListener = useRef(onStrengthChange);
|
|
25
|
+
strengthListener.current = onStrengthChange;
|
|
26
|
+
const scoreListener = useRef(onScore);
|
|
27
|
+
scoreListener.current = onScore;
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!report) return;
|
|
30
|
+
strengthListener.current?.(report);
|
|
31
|
+
scoreListener.current(report.empty ? null : report.score);
|
|
32
|
+
}, [report]);
|
|
33
|
+
const [breachState, setBreachState] = useState({ status: "idle", count: 0, error: null });
|
|
34
|
+
const breachListener = useRef(onBreachChange);
|
|
35
|
+
breachListener.current = onBreachChange;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!breach || !value) {
|
|
38
|
+
setBreachState({ status: "idle", count: 0, error: null });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
setBreachState({ status: "checking", count: 0, error: null });
|
|
44
|
+
breach(value, { signal: controller.signal }).then((result) => {
|
|
45
|
+
if (controller.signal.aborted) return;
|
|
46
|
+
setBreachState({ status: result.breached ? "breached" : "safe", count: result.count, error: null });
|
|
47
|
+
}).catch((error) => {
|
|
48
|
+
if (controller.signal.aborted) return;
|
|
49
|
+
setBreachState({ status: "error", count: 0, error });
|
|
50
|
+
});
|
|
51
|
+
}, breachDelay);
|
|
52
|
+
return () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
controller.abort();
|
|
55
|
+
};
|
|
56
|
+
}, [breach, value, breachDelay]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
breachListener.current(breachState);
|
|
59
|
+
}, [breachState]);
|
|
60
|
+
if (!report) return null;
|
|
61
|
+
return /* @__PURE__ */ jsx(PasswordStrength, { id, report, classNames });
|
|
62
|
+
}
|
|
63
|
+
function PasswordStrength({
|
|
64
|
+
value = "",
|
|
65
|
+
report,
|
|
66
|
+
userInputs,
|
|
67
|
+
segments = 5,
|
|
68
|
+
labels = SCORE_LABELS,
|
|
69
|
+
showWarning = true,
|
|
70
|
+
classNames,
|
|
71
|
+
...props
|
|
72
|
+
}) {
|
|
73
|
+
const computed = useMemo(
|
|
74
|
+
() => report ?? estimatePasswordStrength(value, { userInputs }),
|
|
75
|
+
[report, value, userInputs]
|
|
76
|
+
);
|
|
77
|
+
const score = computed.score;
|
|
78
|
+
return /* @__PURE__ */ jsxs(
|
|
79
|
+
"div",
|
|
80
|
+
{
|
|
81
|
+
...props,
|
|
82
|
+
"data-enigma-password-strength": "",
|
|
83
|
+
"data-score": computed.empty ? void 0 : score,
|
|
84
|
+
"data-empty": computed.empty ? "" : void 0,
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsx("div", { "data-enigma-password-strength-track": "", "aria-hidden": "true", className: classNames?.track, children: Array.from({ length: segments }, (unused, index) => /* @__PURE__ */ jsx(
|
|
87
|
+
"span",
|
|
88
|
+
{
|
|
89
|
+
"data-enigma-password-strength-segment": "",
|
|
90
|
+
className: classNames?.segment,
|
|
91
|
+
"data-filled": !computed.empty && index <= score ? "" : void 0
|
|
92
|
+
},
|
|
93
|
+
index
|
|
94
|
+
)) }),
|
|
95
|
+
/* @__PURE__ */ jsx("p", { "data-enigma-password-strength-label": "", role: "status", "aria-live": "polite", className: classNames?.label, children: computed.empty ? "" : labels[score] ?? "" }),
|
|
96
|
+
showWarning && !computed.empty && computed.warnings.length > 0 && /* @__PURE__ */ jsx("p", { "data-enigma-password-strength-warning": "", className: classNames?.warning, children: computed.warnings[0] })
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { PasswordExtras, PasswordStrength };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { defaultQueue } from './chunk-QYMUIW5I.js';
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
function swipeAxis(position) {
|
|
6
|
+
if (position.endsWith("-center")) return { axis: "y", sign: position.startsWith("top") ? -1 : 1 };
|
|
7
|
+
return { axis: "x", sign: position.endsWith("-left") ? -1 : 1 };
|
|
8
|
+
}
|
|
9
|
+
function Toaster({
|
|
10
|
+
queue = defaultQueue,
|
|
11
|
+
position = "bottom-right",
|
|
12
|
+
exitDuration = 200,
|
|
13
|
+
swipeThreshold = 60,
|
|
14
|
+
children,
|
|
15
|
+
label = "Notifications",
|
|
16
|
+
className,
|
|
17
|
+
style
|
|
18
|
+
}) {
|
|
19
|
+
const [rendered, setRendered] = useState(
|
|
20
|
+
() => queue.items.map((notification) => ({ notification, leaving: false }))
|
|
21
|
+
);
|
|
22
|
+
const timers = useRef(/* @__PURE__ */ new Map());
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const sync = (items) => {
|
|
25
|
+
setRendered((current) => {
|
|
26
|
+
const live = new Map(items.map((item) => [item.id, item]));
|
|
27
|
+
const kept = current.map((entry) => {
|
|
28
|
+
const still = live.get(entry.notification.id);
|
|
29
|
+
if (still) return { notification: still, leaving: false };
|
|
30
|
+
if (entry.leaving) return entry;
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
timers.current.delete(entry.notification.id);
|
|
33
|
+
setRendered((later) => later.filter((row) => row.notification.id !== entry.notification.id));
|
|
34
|
+
}, exitDuration);
|
|
35
|
+
timers.current.set(entry.notification.id, timer);
|
|
36
|
+
return { ...entry, leaving: true };
|
|
37
|
+
});
|
|
38
|
+
const seen = new Set(kept.map((entry) => entry.notification.id));
|
|
39
|
+
const added = items.filter((item) => !seen.has(item.id)).map((item) => ({ notification: item, leaving: false }));
|
|
40
|
+
return [...kept, ...added];
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
sync(queue.items);
|
|
44
|
+
return queue.subscribe(sync);
|
|
45
|
+
}, [queue, exitDuration]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const pending = timers.current;
|
|
48
|
+
return () => {
|
|
49
|
+
for (const timer of pending.values()) clearTimeout(timer);
|
|
50
|
+
pending.clear();
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
if (!rendered.length) return null;
|
|
54
|
+
return /* @__PURE__ */ jsx(
|
|
55
|
+
"section",
|
|
56
|
+
{
|
|
57
|
+
"aria-label": label,
|
|
58
|
+
"data-enigma-toaster": "",
|
|
59
|
+
"data-position": position,
|
|
60
|
+
className,
|
|
61
|
+
style,
|
|
62
|
+
onPointerEnter: () => queue.pause(),
|
|
63
|
+
onPointerLeave: () => queue.resume(),
|
|
64
|
+
onFocusCapture: () => queue.pause(),
|
|
65
|
+
onBlurCapture: () => queue.resume(),
|
|
66
|
+
children: rendered.map((entry, index) => /* @__PURE__ */ jsx(
|
|
67
|
+
Toast,
|
|
68
|
+
{
|
|
69
|
+
entry,
|
|
70
|
+
index: rendered.length - 1 - index,
|
|
71
|
+
position,
|
|
72
|
+
swipeThreshold,
|
|
73
|
+
onDismiss: () => queue.dismiss(entry.notification.id),
|
|
74
|
+
render: children
|
|
75
|
+
},
|
|
76
|
+
entry.notification.id
|
|
77
|
+
))
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function Toast({ entry, index, position, swipeThreshold, onDismiss, render }) {
|
|
82
|
+
const { notification, leaving } = entry;
|
|
83
|
+
const [offset, setOffset] = useState(0);
|
|
84
|
+
const [swiping, setSwiping] = useState(false);
|
|
85
|
+
const start = useRef(0);
|
|
86
|
+
const { axis, sign } = swipeAxis(position);
|
|
87
|
+
const onPointerDown = useCallback((event) => {
|
|
88
|
+
if (event.target.closest("button")) return;
|
|
89
|
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
90
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
91
|
+
start.current = axis === "x" ? event.clientX : event.clientY;
|
|
92
|
+
setSwiping(true);
|
|
93
|
+
}, [axis]);
|
|
94
|
+
const onPointerMove = useCallback((event) => {
|
|
95
|
+
if (!swiping) return;
|
|
96
|
+
const delta = (axis === "x" ? event.clientX : event.clientY) - start.current;
|
|
97
|
+
setOffset(sign > 0 ? Math.max(0, delta) : Math.min(0, delta));
|
|
98
|
+
}, [swiping, axis, sign]);
|
|
99
|
+
const onPointerUp = useCallback(() => {
|
|
100
|
+
if (!swiping) return;
|
|
101
|
+
setSwiping(false);
|
|
102
|
+
if (Math.abs(offset) >= swipeThreshold) {
|
|
103
|
+
onDismiss();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setOffset(0);
|
|
107
|
+
}, [swiping, offset, swipeThreshold, onDismiss]);
|
|
108
|
+
const controls = { dismiss: onDismiss };
|
|
109
|
+
return /* @__PURE__ */ jsx(
|
|
110
|
+
"article",
|
|
111
|
+
{
|
|
112
|
+
"data-enigma-toast": "",
|
|
113
|
+
"data-tone": notification.tone,
|
|
114
|
+
"data-state": leaving ? "leaving" : "open",
|
|
115
|
+
"data-index": index,
|
|
116
|
+
"data-front": index === 0 ? "" : void 0,
|
|
117
|
+
"data-swiping": swiping ? "" : void 0,
|
|
118
|
+
role: notification.tone === "error" ? "alert" : "status",
|
|
119
|
+
"aria-live": notification.tone === "error" ? "assertive" : "polite",
|
|
120
|
+
tabIndex: 0,
|
|
121
|
+
onKeyDown: (event) => {
|
|
122
|
+
if (event.key === "Escape") onDismiss();
|
|
123
|
+
},
|
|
124
|
+
onPointerDown,
|
|
125
|
+
onPointerMove,
|
|
126
|
+
onPointerUp,
|
|
127
|
+
onPointerCancel: onPointerUp,
|
|
128
|
+
style: {
|
|
129
|
+
// Read by the theme, so the transform stays the stylesheet's decision.
|
|
130
|
+
"--enigma-toast-swipe": `${offset}px`,
|
|
131
|
+
"--enigma-toast-index": index
|
|
132
|
+
},
|
|
133
|
+
children: render ? render(notification, controls) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
134
|
+
/* @__PURE__ */ jsx("p", { "data-enigma-toast-title": "", children: notification.title }),
|
|
135
|
+
notification.body && /* @__PURE__ */ jsx("p", { "data-enigma-toast-body": "", children: notification.body }),
|
|
136
|
+
notification.action && /* @__PURE__ */ jsx(
|
|
137
|
+
"button",
|
|
138
|
+
{
|
|
139
|
+
type: "button",
|
|
140
|
+
"data-enigma-toast-action": "",
|
|
141
|
+
onClick: () => {
|
|
142
|
+
notification.action?.onSelect();
|
|
143
|
+
if (notification.action?.dismiss !== false) onDismiss();
|
|
144
|
+
},
|
|
145
|
+
children: notification.action.label
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
/* @__PURE__ */ jsx("button", { type: "button", "data-enigma-toast-close": "", "aria-label": "Dismiss", onClick: onDismiss, children: "\xD7" })
|
|
149
|
+
] })
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { Toaster };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createMarquee } from './chunk-UOSSNUSC.js';
|
|
2
|
+
import { useRef, useState, useEffect, useLayoutEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
|
+
function useMarquee(options = {}) {
|
|
6
|
+
const laneRef = useRef(null);
|
|
7
|
+
const trackRef = useRef(null);
|
|
8
|
+
const instanceRef = useRef(null);
|
|
9
|
+
const optionsRef = useRef(options);
|
|
10
|
+
optionsRef.current = options;
|
|
11
|
+
const [copies, setCopies] = useState(2);
|
|
12
|
+
const [dragging, setDragging] = useState(false);
|
|
13
|
+
const [period, setPeriod] = useState(0);
|
|
14
|
+
useIsomorphicLayoutEffect(() => {
|
|
15
|
+
const lane = laneRef.current;
|
|
16
|
+
const track = trackRef.current;
|
|
17
|
+
if (!lane || !track) return;
|
|
18
|
+
const instance = createMarquee(lane, track, {
|
|
19
|
+
...optionsRef.current,
|
|
20
|
+
// React owns the DOM; cloning behind its back would be undone on render.
|
|
21
|
+
copies: "external",
|
|
22
|
+
onCopyCountChange: setCopies,
|
|
23
|
+
onMeasure: (measured) => setPeriod(measured),
|
|
24
|
+
onItemClick: (event) => optionsRef.current.onItemClick?.(event)
|
|
25
|
+
});
|
|
26
|
+
instanceRef.current = instance;
|
|
27
|
+
const syncDragging = () => setDragging(instance.dragging);
|
|
28
|
+
lane.addEventListener("pointerdown", syncDragging);
|
|
29
|
+
window.addEventListener("pointerup", syncDragging);
|
|
30
|
+
window.addEventListener("pointercancel", syncDragging);
|
|
31
|
+
return () => {
|
|
32
|
+
lane.removeEventListener("pointerdown", syncDragging);
|
|
33
|
+
window.removeEventListener("pointerup", syncDragging);
|
|
34
|
+
window.removeEventListener("pointercancel", syncDragging);
|
|
35
|
+
instance.destroy();
|
|
36
|
+
instanceRef.current = null;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
useIsomorphicLayoutEffect(() => {
|
|
40
|
+
instanceRef.current?.update(options);
|
|
41
|
+
}, [options.speed, options.reverse, options.vertical, options.draggable, options.hover, options.decay]);
|
|
42
|
+
useIsomorphicLayoutEffect(() => {
|
|
43
|
+
instanceRef.current?.measure();
|
|
44
|
+
}, [copies]);
|
|
45
|
+
return {
|
|
46
|
+
laneRef,
|
|
47
|
+
trackRef,
|
|
48
|
+
copies,
|
|
49
|
+
dragging,
|
|
50
|
+
period,
|
|
51
|
+
measure: () => instanceRef.current?.measure()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { useMarquee };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Country flags as ASSETS, which is the only form of them that renders everywhere.
|
|
3
|
+
*
|
|
4
|
+
* The emoji flag is the thing this replaces. It is a pair of regional-indicator code
|
|
5
|
+
* points, and the glyph for that pair exists only where the platform ships one: Windows
|
|
6
|
+
* has never shipped country flags in Segoe UI Emoji, so `\u{1F1EA}\u{1F1F8}` renders as
|
|
7
|
+
* the letters "ES" for the largest desktop platform there is - and a language picker built
|
|
8
|
+
* out of them looks broken to those readers, with nothing in the code to explain why.
|
|
9
|
+
* There is no font stack that fixes it, so the fix is not a font: it is an image.
|
|
10
|
+
*
|
|
11
|
+
* Three sets, all SVG, all served by enigma itself:
|
|
12
|
+
*
|
|
13
|
+
* - `rect` (4:3) and `square` (1:1) - the classic rectangular artwork.
|
|
14
|
+
* - `circle` - round, which is what a flag next to an avatar or inside a chip wants.
|
|
15
|
+
*
|
|
16
|
+
* They are served from enigma's own tree rather than from anyone else's, because a
|
|
17
|
+
* component whose default source is a third party is a component that breaks when that
|
|
18
|
+
* third party moves, renames a branch, or goes away.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here touches the DOM, so it renders on a server.
|
|
21
|
+
*/
|
|
22
|
+
type FlagShape = "rect" | "square" | "circle";
|
|
23
|
+
/**
|
|
24
|
+
* `svg` is what the sets are stored as, so it is what a remote source can serve. `png` and
|
|
25
|
+
* `webp` exist for a LOCAL set (`enigma add flags --flags local --flag-formats webp`), which
|
|
26
|
+
* rasterises what it downloads, and for a mirror of your own.
|
|
27
|
+
*/
|
|
28
|
+
type FlagFormat = "svg" | "png" | "webp";
|
|
29
|
+
/**
|
|
30
|
+
* Where the files come from.
|
|
31
|
+
*
|
|
32
|
+
* - `"cdn"` - enigma's own artwork, over a CDN. Nothing to install.
|
|
33
|
+
* - `"local"` - files under `basePath` in your own public directory.
|
|
34
|
+
* - any URL - a mirror of your own with the same layout
|
|
35
|
+
* (`<base>/<shape>/<code>.<format>`), which is exactly the tree the downloader writes.
|
|
36
|
+
*/
|
|
37
|
+
type FlagSource = "cdn" | "local" | (string & {});
|
|
38
|
+
/** enigma's own artwork, over a CDN. Every source shares this layout. */
|
|
39
|
+
declare const FLAG_CDN = "https://cdn.jsdelivr.net/gh/FJRG2007/enigma@main/assets/flags";
|
|
40
|
+
interface FlagOptions {
|
|
41
|
+
shape?: FlagShape;
|
|
42
|
+
format?: FlagFormat;
|
|
43
|
+
source?: FlagSource;
|
|
44
|
+
/** Where a local set is served from. Default `/flags`. */
|
|
45
|
+
basePath?: string;
|
|
46
|
+
/** Rendered HEIGHT in px. The width follows the shape's ratio. Default 16. */
|
|
47
|
+
size?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Language the automatic name is written in. Left out, it follows the document's own
|
|
50
|
+
* `lang`, which is what a translated page wants.
|
|
51
|
+
*/
|
|
52
|
+
locale?: string;
|
|
53
|
+
}
|
|
54
|
+
interface FlagConfig extends Required<Omit<FlagOptions, "size" | "locale">> {
|
|
55
|
+
size: number;
|
|
56
|
+
locale?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Set the defaults for every flag on the page, once, at startup.
|
|
60
|
+
*
|
|
61
|
+
* This is what makes the CDN/local decision a one-line change rather than a prop on every
|
|
62
|
+
* call site: `configureFlags({ source: "local" })` moves the whole app onto the files the
|
|
63
|
+
* downloader wrote, and nothing else in the codebase mentions where a flag comes from.
|
|
64
|
+
*/
|
|
65
|
+
declare function configureFlags(patch: Partial<FlagConfig>): FlagConfig;
|
|
66
|
+
declare function flagConfig(): FlagConfig;
|
|
67
|
+
/** Test seam and a way back to the shipped defaults. */
|
|
68
|
+
declare function resetFlagConfig(): FlagConfig;
|
|
69
|
+
/** Width / height of each shape. The circle set is square; only `rect` is 4:3. */
|
|
70
|
+
declare const FLAG_RATIO: Record<FlagShape, number>;
|
|
71
|
+
/**
|
|
72
|
+
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
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.
|
|
78
|
+
*/
|
|
79
|
+
declare function normalizeFlagCode(value: string | null | undefined): string | null;
|
|
80
|
+
/**
|
|
81
|
+
* The country's name, in the reader's language, or null.
|
|
82
|
+
*
|
|
83
|
+
* `Intl.DisplayNames` is the whole implementation: the names are in the runtime already,
|
|
84
|
+
* translated, and maintained by whoever maintains the platform - so this ships no table of
|
|
85
|
+
* 250 country names to go stale, and a Spanish page says "España" without being told.
|
|
86
|
+
*
|
|
87
|
+
* Null for anything it cannot name, and that is deliberate for SUBDIVISIONS: `gb-eng` is
|
|
88
|
+
* England, and falling back to the region (`gb`, "United Kingdom") would put a confidently
|
|
89
|
+
* wrong name on the flag. A name nobody can source is worse than no name.
|
|
90
|
+
*/
|
|
91
|
+
declare function flagName(code: string | null | undefined, locale?: string): string | null;
|
|
92
|
+
/** Everything a renderer needs, computed in one pass. Null when the code is unusable. */
|
|
93
|
+
interface FlagView {
|
|
94
|
+
/** The file name, e.g. `es`, `gb-eng`. */
|
|
95
|
+
code: string;
|
|
96
|
+
src: string;
|
|
97
|
+
shape: FlagShape;
|
|
98
|
+
format: FlagFormat;
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
/** The automatic name, when there is one. */
|
|
102
|
+
name: string | null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The URL for one flag. Null when the code cannot be resolved.
|
|
106
|
+
*
|
|
107
|
+
* A raster format asked of the CDN falls back to SVG rather than producing a 404: the sets
|
|
108
|
+
* are stored as SVG, so `format: "webp"` is only meaningful against a local set or a mirror.
|
|
109
|
+
* It says so once, in development, instead of silently disagreeing with the call site.
|
|
110
|
+
*/
|
|
111
|
+
declare function flagSrc(code: string | null | undefined, options?: FlagOptions): string | null;
|
|
112
|
+
/** `flagSrc` plus the resolved geometry and name, for a renderer that has to size the box. */
|
|
113
|
+
declare function flagView(code: string | null | undefined, options?: FlagOptions): FlagView | null;
|
|
114
|
+
interface FlagAttributeOptions extends FlagOptions {
|
|
115
|
+
/** Your own accessible name, which wins over the automatic one. */
|
|
116
|
+
label?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Beside a country name that is already on screen, the flag is decoration and repeating
|
|
119
|
+
* the name only makes a screen reader say it twice.
|
|
120
|
+
*/
|
|
121
|
+
decorative?: boolean;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Attributes for a plain `<img>`, for vanilla, Astro and any template language.
|
|
125
|
+
*
|
|
126
|
+
* The accessible name is AUTOMATIC: the country's own name, in the reader's language, from
|
|
127
|
+
* `Intl.DisplayNames`. `label` replaces it and `decorative` drops it. Nothing is ever
|
|
128
|
+
* invented - a subdivision the platform cannot name, or a runtime without `Intl`, renders
|
|
129
|
+
* as decoration rather than putting a bare "ES" into a screen reader.
|
|
130
|
+
*/
|
|
131
|
+
declare function flagAttributes(code: string | null | undefined, options?: FlagAttributeOptions): Record<string, string | number> | null;
|
|
132
|
+
|
|
133
|
+
export { FLAG_CDN as F, FLAG_RATIO as a, type FlagConfig as b, type FlagFormat as c, type FlagOptions as d, type FlagShape as e, type FlagSource as f, type FlagView as g, configureFlags as h, flagAttributes as i, flagConfig as j, flagName as k, flagSrc as l, flagView as m, normalizeFlagCode as n, resetFlagConfig as r };
|