@enigmax/primitives 0.17.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 -602
- 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 -312
- 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 +52 -2
- package/registry.json +175 -18
- package/src/core/flags.ts +88 -42
- 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 +2 -2
- package/src/react/button.tsx +70 -4
- package/src/react/flag.tsx +18 -6
- package/src/react/index.ts +40 -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-4GLYRF4B.js +0 -1259
- package/dist/chunk-JFNND6P4.js +0 -749
- package/src/react/input.tsx +0 -429
package/src/core/flags.ts
CHANGED
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* out of them looks broken to those readers, with nothing in the code to explain why.
|
|
9
9
|
* There is no font stack that fixes it, so the fix is not a font: it is an image.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Three sets, all SVG, all served by enigma itself:
|
|
12
12
|
*
|
|
13
|
-
* - `rect`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
17
19
|
*
|
|
18
20
|
* Nothing here touches the DOM, so it renders on a server.
|
|
19
21
|
*/
|
|
@@ -21,26 +23,24 @@
|
|
|
21
23
|
export type FlagShape = "rect" | "square" | "circle";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* `svg` is the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* `svg` is what the sets are stored as, so it is what a remote source can serve. `png` and
|
|
27
|
+
* `webp` exist for a LOCAL set (`enigma add flags --flags local --flag-formats webp`), which
|
|
28
|
+
* rasterises what it downloads, and for a mirror of your own.
|
|
27
29
|
*/
|
|
28
30
|
export type FlagFormat = "svg" | "png" | "webp";
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Where the files come from.
|
|
32
34
|
*
|
|
33
|
-
* - `"cdn"` -
|
|
35
|
+
* - `"cdn"` - enigma's own artwork, over a CDN. Nothing to install.
|
|
34
36
|
* - `"local"` - files under `basePath` in your own public directory.
|
|
35
|
-
* - any URL - a mirror of your own with the same layout
|
|
37
|
+
* - any URL - a mirror of your own with the same layout
|
|
36
38
|
* (`<base>/<shape>/<code>.<format>`), which is exactly the tree the downloader writes.
|
|
37
39
|
*/
|
|
38
40
|
export type FlagSource = "cdn" | "local" | (string & {});
|
|
39
41
|
|
|
40
|
-
/**
|
|
41
|
-
export const
|
|
42
|
-
/** circle-flags publishes from a branch, not a tag; gh-pages is its default branch. */
|
|
43
|
-
export const CIRCLE_FLAGS_REF = "gh-pages";
|
|
42
|
+
/** enigma's own artwork, over a CDN. Every source shares this layout. */
|
|
43
|
+
export const FLAG_CDN = "https://cdn.jsdelivr.net/gh/FJRG2007/enigma@main/assets/flags";
|
|
44
44
|
|
|
45
45
|
export interface FlagOptions {
|
|
46
46
|
shape?: FlagShape;
|
|
@@ -50,10 +50,16 @@ export interface FlagOptions {
|
|
|
50
50
|
basePath?: string;
|
|
51
51
|
/** Rendered HEIGHT in px. The width follows the shape's ratio. Default 16. */
|
|
52
52
|
size?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Language the automatic name is written in. Left out, it follows the document's own
|
|
55
|
+
* `lang`, which is what a translated page wants.
|
|
56
|
+
*/
|
|
57
|
+
locale?: string;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
export interface FlagConfig extends Required<Omit<FlagOptions, "size">> {
|
|
60
|
+
export interface FlagConfig extends Required<Omit<FlagOptions, "size" | "locale">> {
|
|
56
61
|
size: number;
|
|
62
|
+
locale?: string;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
const DEFAULTS: FlagConfig = {
|
|
@@ -96,12 +102,14 @@ const EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
|
|
|
96
102
|
* A BCP 47 tag whose region is spelled the way the standard spells it: UPPERCASE. That
|
|
97
103
|
* casing is the whole discriminator, and it has to be read before the value is lowercased -
|
|
98
104
|
* `es-ES` is Spanish-in-Spain and means the flag `es`, while `es-ct` is a subdivision code
|
|
99
|
-
*
|
|
105
|
+
* the sets publish (Catalonia) and means the file `es-ct`. Lowercasing first makes those
|
|
100
106
|
* two the same string, and then one of them is always wrong.
|
|
101
107
|
*/
|
|
102
108
|
const LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
|
|
103
|
-
/** What the
|
|
109
|
+
/** What the sets actually name their files: `es`, `gb-eng`, `au-nsw`, `easter_island`. */
|
|
104
110
|
const FILE_CODE = /^[a-z]{2,}(?:[-_][a-z0-9]+)*$/;
|
|
111
|
+
/** A plain country: exactly the shape `Intl.DisplayNames` can put a name to. */
|
|
112
|
+
const COUNTRY_CODE = /^[a-z]{2}$/;
|
|
105
113
|
|
|
106
114
|
/**
|
|
107
115
|
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
@@ -128,21 +136,48 @@ export function normalizeFlagCode(value: string | null | undefined): string | nu
|
|
|
128
136
|
return FILE_CODE.test(code) ? code : null;
|
|
129
137
|
}
|
|
130
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The country's name, in the reader's language, or null.
|
|
141
|
+
*
|
|
142
|
+
* `Intl.DisplayNames` is the whole implementation: the names are in the runtime already,
|
|
143
|
+
* translated, and maintained by whoever maintains the platform - so this ships no table of
|
|
144
|
+
* 250 country names to go stale, and a Spanish page says "España" without being told.
|
|
145
|
+
*
|
|
146
|
+
* Null for anything it cannot name, and that is deliberate for SUBDIVISIONS: `gb-eng` is
|
|
147
|
+
* England, and falling back to the region (`gb`, "United Kingdom") would put a confidently
|
|
148
|
+
* wrong name on the flag. A name nobody can source is worse than no name.
|
|
149
|
+
*/
|
|
150
|
+
export function flagName(code: string | null | undefined, locale?: string): string | null {
|
|
151
|
+
const normalized = normalizeFlagCode(code);
|
|
152
|
+
if (!normalized || !COUNTRY_CODE.test(normalized)) return null;
|
|
153
|
+
const region = normalized.toUpperCase();
|
|
154
|
+
try {
|
|
155
|
+
const names = new Intl.DisplayNames([locale ?? documentLocale() ?? "en"], { type: "region" });
|
|
156
|
+
const name = names.of(region);
|
|
157
|
+
// It echoes the input back when it has no name for the code.
|
|
158
|
+
return !name || name === region ? null : name;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The page's own language, which is what a translated page wants the name written in. */
|
|
165
|
+
function documentLocale(): string | undefined {
|
|
166
|
+
if (typeof document === "undefined") return undefined;
|
|
167
|
+
return document.documentElement.lang || undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
131
170
|
/** Everything a renderer needs, computed in one pass. Null when the code is unusable. */
|
|
132
171
|
export interface FlagView {
|
|
133
|
-
/** The
|
|
172
|
+
/** The file name, e.g. `es`, `gb-eng`. */
|
|
134
173
|
code: string;
|
|
135
174
|
src: string;
|
|
136
175
|
shape: FlagShape;
|
|
137
176
|
format: FlagFormat;
|
|
138
177
|
width: number;
|
|
139
178
|
height: number;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
function cdnUrl(code: string, shape: FlagShape): string {
|
|
143
|
-
if (shape === "circle") return `https://cdn.jsdelivr.net/gh/HatScripts/circle-flags@${CIRCLE_FLAGS_REF}/flags/${code}.svg`;
|
|
144
|
-
const size = shape === "square" ? "1x1" : "4x3";
|
|
145
|
-
return `https://cdn.jsdelivr.net/npm/flag-icons@${FLAG_ICONS_VERSION}/flags/${size}/${code}.svg`;
|
|
179
|
+
/** The automatic name, when there is one. */
|
|
180
|
+
name: string | null;
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
/** One warning per process, not one per flag: a list of 200 would print 200 times. */
|
|
@@ -151,32 +186,32 @@ let warnedAboutFormat = false;
|
|
|
151
186
|
/**
|
|
152
187
|
* The URL for one flag. Null when the code cannot be resolved.
|
|
153
188
|
*
|
|
154
|
-
* A raster format asked of the CDN falls back to SVG rather than producing a 404:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* with what the call site asked for.
|
|
189
|
+
* A raster format asked of the CDN falls back to SVG rather than producing a 404: the sets
|
|
190
|
+
* are stored as SVG, so `format: "webp"` is only meaningful against a local set or a mirror.
|
|
191
|
+
* It says so once, in development, instead of silently disagreeing with the call site.
|
|
158
192
|
*/
|
|
159
193
|
export function flagSrc(code: string | null | undefined, options: FlagOptions = {}): string | null {
|
|
160
194
|
const settings = { ...config, ...options };
|
|
161
195
|
const normalized = normalizeFlagCode(code);
|
|
162
196
|
if (!normalized) return null;
|
|
163
197
|
|
|
164
|
-
const
|
|
198
|
+
const remote = settings.source === "cdn";
|
|
165
199
|
let format = settings.format;
|
|
166
|
-
if (
|
|
200
|
+
if (remote && format !== "svg") {
|
|
167
201
|
if (!warnedAboutFormat && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
168
202
|
warnedAboutFormat = true;
|
|
169
|
-
console.warn(`[enigma/flags] The
|
|
203
|
+
console.warn(`[enigma/flags] The flags are served as SVG, so "${format}" was served as SVG. Download a local set for raster formats: enigma add flags --flags local --flag-formats ${format}`);
|
|
170
204
|
}
|
|
171
205
|
format = "svg";
|
|
172
206
|
}
|
|
173
|
-
if (remoteCdn) return cdnUrl(normalized, settings.shape);
|
|
174
207
|
|
|
175
|
-
|
|
208
|
+
// One layout everywhere - `<base>/<shape>/<code>.<format>` - so a mirror is the same
|
|
209
|
+
// string with a different host, and the downloader writes exactly what this reads.
|
|
210
|
+
const base = remote ? FLAG_CDN : (settings.source === "local" ? settings.basePath : settings.source).replace(/\/+$/, "");
|
|
176
211
|
return `${base}/${settings.shape}/${normalized}.${format}`;
|
|
177
212
|
}
|
|
178
213
|
|
|
179
|
-
/** `flagSrc` plus the resolved geometry, for a renderer that has to size the box. */
|
|
214
|
+
/** `flagSrc` plus the resolved geometry and name, for a renderer that has to size the box. */
|
|
180
215
|
export function flagView(code: string | null | undefined, options: FlagOptions = {}): FlagView | null {
|
|
181
216
|
const settings = { ...config, ...options };
|
|
182
217
|
const normalized = normalizeFlagCode(code);
|
|
@@ -189,25 +224,36 @@ export function flagView(code: string | null | undefined, options: FlagOptions =
|
|
|
189
224
|
shape: settings.shape,
|
|
190
225
|
format: settings.source === "cdn" ? "svg" : settings.format,
|
|
191
226
|
width: Math.round(height * FLAG_RATIO[settings.shape]),
|
|
192
|
-
height
|
|
227
|
+
height,
|
|
228
|
+
name: flagName(normalized, settings.locale)
|
|
193
229
|
};
|
|
194
230
|
}
|
|
195
231
|
|
|
232
|
+
export interface FlagAttributeOptions extends FlagOptions {
|
|
233
|
+
/** Your own accessible name, which wins over the automatic one. */
|
|
234
|
+
label?: string;
|
|
235
|
+
/**
|
|
236
|
+
* Beside a country name that is already on screen, the flag is decoration and repeating
|
|
237
|
+
* the name only makes a screen reader say it twice.
|
|
238
|
+
*/
|
|
239
|
+
decorative?: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
196
242
|
/**
|
|
197
243
|
* Attributes for a plain `<img>`, for vanilla, Astro and any template language.
|
|
198
244
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
245
|
+
* The accessible name is AUTOMATIC: the country's own name, in the reader's language, from
|
|
246
|
+
* `Intl.DisplayNames`. `label` replaces it and `decorative` drops it. Nothing is ever
|
|
247
|
+
* invented - a subdivision the platform cannot name, or a runtime without `Intl`, renders
|
|
248
|
+
* as decoration rather than putting a bare "ES" into a screen reader.
|
|
203
249
|
*/
|
|
204
|
-
export function flagAttributes(code: string | null | undefined, options:
|
|
250
|
+
export function flagAttributes(code: string | null | undefined, options: FlagAttributeOptions = {}): Record<string, string | number> | null {
|
|
205
251
|
const view = flagView(code, options);
|
|
206
252
|
if (!view) return null;
|
|
207
|
-
const label = options.label?.trim();
|
|
253
|
+
const label = options.decorative ? "" : options.label?.trim() || view.name || "";
|
|
208
254
|
return {
|
|
209
255
|
src: view.src,
|
|
210
|
-
alt: label
|
|
256
|
+
alt: label,
|
|
211
257
|
width: view.width,
|
|
212
258
|
height: view.height,
|
|
213
259
|
loading: "lazy",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icon shapes both renderers draw.
|
|
3
|
+
*
|
|
4
|
+
* Their own module because the React field imports NOTHING else from the vanilla adapter:
|
|
5
|
+
* pulling `createInput` in for two arrays of path data put the whole imperative renderer
|
|
6
|
+
* into every React bundle, for a component that never calls it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The built-in glyphs, as path data.
|
|
11
|
+
*
|
|
12
|
+
* Path data rather than markup because there are two renderers: this file writes an SVG
|
|
13
|
+
* string into a button it created, and the React component builds elements. Keeping the
|
|
14
|
+
* shapes here means one definition, and a theme that replaces an icon replaces it in both.
|
|
15
|
+
* Everything is stroked with `currentColor` at 1em, so an icon inherits the field's text.
|
|
16
|
+
*/
|
|
17
|
+
export const INPUT_ICON_PATHS = {
|
|
18
|
+
eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],
|
|
19
|
+
eyeOff: [
|
|
20
|
+
"M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9",
|
|
21
|
+
"m2 2 20 20",
|
|
22
|
+
"M9.9 9.9a3 3 0 0 0 4.2 4.2"
|
|
23
|
+
],
|
|
24
|
+
generate: ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"],
|
|
25
|
+
/** Clears a search field. The platform draws one only in WebKit, and not on mobile. */
|
|
26
|
+
clear: ["m6 6 12 12", "m18 6-12 12"],
|
|
27
|
+
search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14Z", "m20 20-4.2-4.2"]
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/** The same shapes as a standalone SVG string, for the DOM renderer below. */
|
|
31
|
+
export function iconMarkup(paths: readonly string[]): string {
|
|
32
|
+
const body = paths.map((path) => `<path d="${path}"/>`).join("");
|
|
33
|
+
return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
34
|
+
}
|
package/src/core/input.ts
CHANGED
|
@@ -73,28 +73,12 @@ export interface InputInstance {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* The
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* string into a button it created, and the React component builds elements. Keeping the
|
|
80
|
-
* shapes here means one definition, and a theme that replaces an icon replaces it in both.
|
|
81
|
-
* Everything is stroked with `currentColor` at 1em, so an icon inherits the field's text.
|
|
76
|
+
* The icons live in their own module so the React field can take them without taking this
|
|
77
|
+
* whole adapter with it. Re-exported here because they were part of this module's API
|
|
78
|
+
* before the split, and moving an export is a breaking change for no gain.
|
|
82
79
|
*/
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
eyeOff: [
|
|
86
|
-
"M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9",
|
|
87
|
-
"m2 2 20 20",
|
|
88
|
-
"M9.9 9.9a3 3 0 0 0 4.2 4.2"
|
|
89
|
-
],
|
|
90
|
-
generate: ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"]
|
|
91
|
-
} as const;
|
|
92
|
-
|
|
93
|
-
/** The same shapes as a standalone SVG string, for the DOM renderer below. */
|
|
94
|
-
export function iconMarkup(paths: readonly string[]): string {
|
|
95
|
-
const body = paths.map((path) => `<path d="${path}"/>`).join("");
|
|
96
|
-
return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
97
|
-
}
|
|
80
|
+
export { INPUT_ICON_PATHS, iconMarkup } from "@/core/input-icons";
|
|
81
|
+
import { INPUT_ICON_PATHS, iconMarkup } from "@/core/input-icons";
|
|
98
82
|
|
|
99
83
|
const EYE = iconMarkup(INPUT_ICON_PATHS.eye);
|
|
100
84
|
const EYE_OFF = iconMarkup(INPUT_ICON_PATHS.eyeOff);
|
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -31,13 +31,13 @@ export {
|
|
|
31
31
|
flagSrc,
|
|
32
32
|
flagView,
|
|
33
33
|
flagAttributes,
|
|
34
|
+
flagName,
|
|
34
35
|
normalizeFlagCode,
|
|
35
36
|
configureFlags,
|
|
36
37
|
flagConfig,
|
|
37
38
|
resetFlagConfig,
|
|
38
39
|
FLAG_RATIO,
|
|
39
|
-
|
|
40
|
-
CIRCLE_FLAGS_REF,
|
|
40
|
+
FLAG_CDN,
|
|
41
41
|
type FlagOptions,
|
|
42
42
|
type FlagConfig,
|
|
43
43
|
type FlagView,
|
package/src/react/button.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Slot } from "@/react/slot";
|
|
3
4
|
import { useButton } from "@/react/use-button";
|
|
4
5
|
import { getLinkComponent } from "@/react/link";
|
|
5
6
|
import type { ButtonOptions, ButtonState } from "@/core/button";
|
|
6
|
-
import { createElement, forwardRef, type ComponentPropsWithoutRef, type ElementType, type ReactNode } from "react";
|
|
7
|
+
import { createElement, forwardRef, isValidElement, type ComponentPropsWithoutRef, type ElementType, type ReactNode } from "react";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* `<Button>` - the component, for the ninety percent.
|
|
@@ -51,6 +52,49 @@ export interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRe
|
|
|
51
52
|
pending?: ReactNode;
|
|
52
53
|
/** `type` on a real button. Ignored on a link, which has none. */
|
|
53
54
|
type?: "button" | "submit" | "reset";
|
|
55
|
+
/**
|
|
56
|
+
* Show the shortcut as a key badge after the label, the way Stripe writes
|
|
57
|
+
* "Create invoice n".
|
|
58
|
+
*
|
|
59
|
+
* `"auto"` (the default) shows it when there is a shortcut AND the label renders text:
|
|
60
|
+
* beside a lone glyph on an icon-only button the badge is noise, and it would double the
|
|
61
|
+
* width of the smallest control on the screen. `true` and `false` decide it outright.
|
|
62
|
+
*/
|
|
63
|
+
shortcutHint?: boolean | "auto";
|
|
64
|
+
/** Render the badge yourself. Return null to drop it for one button. */
|
|
65
|
+
renderShortcut?: (key: string) => ReactNode;
|
|
66
|
+
/**
|
|
67
|
+
* Put the behaviour on YOUR element instead of ours - a `motion.button`, your design
|
|
68
|
+
* system's button, a router Link, anything.
|
|
69
|
+
*
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <Button asChild cooldown={30_000} onPress={resend}>
|
|
72
|
+
* <motion.button whileTap={{ scale: 0.98 }}>Resend</motion.button>
|
|
73
|
+
* </Button>
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* The child owns its own markup, so the shortcut badge is not injected - the child is
|
|
77
|
+
* rendered exactly as written. Read `state.shortcut` from `useButton` to place your own.
|
|
78
|
+
*/
|
|
79
|
+
asChild?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether a label renders any TEXT.
|
|
84
|
+
*
|
|
85
|
+
* Walked rather than assumed: `<Button shortcut="s"><Icon /></Button>` and
|
|
86
|
+
* `<Button shortcut="s"><Icon /> Save</Button>` differ only in a string buried in the tree,
|
|
87
|
+
* and that string is the whole difference between a hint and a decoration. An element whose
|
|
88
|
+
* text arrives from somewhere this cannot see reports false, so `auto` errs towards the
|
|
89
|
+
* quieter button; `shortcutHint` overrides it in one word.
|
|
90
|
+
*/
|
|
91
|
+
function hasText(node: ReactNode): boolean {
|
|
92
|
+
if (node === null || node === undefined || typeof node === "boolean") return false;
|
|
93
|
+
if (typeof node === "string") return node.trim().length > 0;
|
|
94
|
+
if (typeof node === "number") return true;
|
|
95
|
+
if (Array.isArray(node)) return node.some(hasText);
|
|
96
|
+
if (isValidElement(node)) return hasText((node.props as { children?: ReactNode; }).children);
|
|
97
|
+
return false;
|
|
54
98
|
}
|
|
55
99
|
|
|
56
100
|
export const Button = forwardRef<HTMLElement, ButtonProps>(function Button({
|
|
@@ -66,6 +110,9 @@ export const Button = forwardRef<HTMLElement, ButtonProps>(function Button({
|
|
|
66
110
|
onClick,
|
|
67
111
|
onChange,
|
|
68
112
|
type = "button",
|
|
113
|
+
shortcutHint = "auto",
|
|
114
|
+
renderShortcut,
|
|
115
|
+
asChild = false,
|
|
69
116
|
...rest
|
|
70
117
|
}, ref) {
|
|
71
118
|
const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress: onPress ?? onClick, onChange });
|
|
@@ -75,17 +122,36 @@ export const Button = forwardRef<HTMLElement, ButtonProps>(function Button({
|
|
|
75
122
|
// to setLinkComponent - and a plain <a> when nothing was. The package cannot import a
|
|
76
123
|
// router itself, so registering one is what turns every href into a client navigation
|
|
77
124
|
// without a word at the call site.
|
|
78
|
-
const Tag: ElementType = as ?? (state.element === "a" ? getLinkComponent() : "button");
|
|
125
|
+
const Tag: ElementType = asChild ? Slot : as ?? (state.element === "a" ? getLinkComponent() : "button");
|
|
79
126
|
|
|
80
127
|
// `type="button"` is the default on purpose. A bare <button> inside a form submits it,
|
|
81
|
-
// so an action button that forgot it posts the form instead of doing its job.
|
|
128
|
+
// so an action button that forgot it posts the form instead of doing its job. Never
|
|
129
|
+
// injected through a slot: the child may be an anchor or a div, where `type` is either
|
|
130
|
+
// invalid or means something else entirely.
|
|
82
131
|
const native = Tag === "button" ? { type } : {};
|
|
83
132
|
|
|
84
133
|
const label = typeof children === "function" ? children(state) : children;
|
|
134
|
+
const content = state.loading && pending !== undefined ? pending : label;
|
|
135
|
+
|
|
136
|
+
// Not while it is working: the label has already been replaced by whatever `pending`
|
|
137
|
+
// says, and a key that does nothing right now is not a hint.
|
|
138
|
+
// Never through a slot: it takes exactly one child, and a second one would throw.
|
|
139
|
+
const showHint = !asChild && Boolean(state.shortcut) && !state.loading &&
|
|
140
|
+
(shortcutHint === "auto" ? hasText(content) : shortcutHint);
|
|
141
|
+
const hint = showHint && state.shortcut
|
|
142
|
+
? renderShortcut?.(state.shortcut) ?? createElement(
|
|
143
|
+
"kbd",
|
|
144
|
+
// Hidden from the accessible name - the button already carries
|
|
145
|
+
// `aria-keyshortcuts`, and "Save n" is not what anyone wants read out.
|
|
146
|
+
{ "data-enigma-button-key": "", "aria-hidden": true },
|
|
147
|
+
state.shortcut
|
|
148
|
+
)
|
|
149
|
+
: null;
|
|
85
150
|
|
|
86
151
|
return createElement(
|
|
87
152
|
Tag,
|
|
88
153
|
{ ref, ...button.props, ...native, ...rest },
|
|
89
|
-
|
|
154
|
+
content,
|
|
155
|
+
hint
|
|
90
156
|
);
|
|
91
157
|
});
|
package/src/react/flag.tsx
CHANGED
|
@@ -7,10 +7,17 @@ export interface FlagProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "sr
|
|
|
7
7
|
/** `es`, `ES`, `en-GB`, `gb-eng`, or the emoji flag you are replacing. */
|
|
8
8
|
code: string | null | undefined;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Your own accessible name. Left out, the country's own name is used, in the reader's
|
|
11
|
+
* language - so a flag standing on its own is never a silent image.
|
|
12
12
|
*/
|
|
13
13
|
label?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Beside a country name that is already on screen the flag is decoration, and repeating
|
|
16
|
+
* the name only makes a screen reader say it twice.
|
|
17
|
+
*/
|
|
18
|
+
decorative?: boolean;
|
|
19
|
+
/** Language the automatic name is written in. Follows the document's `lang` otherwise. */
|
|
20
|
+
locale?: string;
|
|
14
21
|
shape?: FlagShape;
|
|
15
22
|
format?: FlagFormat;
|
|
16
23
|
source?: FlagSource;
|
|
@@ -28,8 +35,8 @@ export interface FlagProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "sr
|
|
|
28
35
|
* The defaults come from `configureFlags`, so moving the whole application from the CDN to
|
|
29
36
|
* a downloaded set is one line at startup and no change at any call site.
|
|
30
37
|
*/
|
|
31
|
-
export function Flag({ code, label, shape, format, source, basePath, size, fallback = null, ...rest }: FlagProps): ReactNode {
|
|
32
|
-
const options: FlagOptions = { shape, format, source, basePath, size };
|
|
38
|
+
export function Flag({ code, label, decorative = false, locale, shape, format, source, basePath, size, fallback = null, ...rest }: FlagProps): ReactNode {
|
|
39
|
+
const options: FlagOptions = { shape, format, source, basePath, size, locale };
|
|
33
40
|
// An explicit `undefined` prop must not beat the configured default, so the unset ones
|
|
34
41
|
// are dropped rather than spread over the config.
|
|
35
42
|
for (const key of Object.keys(options) as (keyof FlagOptions)[]) {
|
|
@@ -39,16 +46,21 @@ export function Flag({ code, label, shape, format, source, basePath, size, fallb
|
|
|
39
46
|
const view = flagView(code, options);
|
|
40
47
|
if (!view) return fallback;
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
// Automatic unless told otherwise: the country's own name, in the reader's language.
|
|
50
|
+
const name = decorative ? "" : label?.trim() || view.name || "";
|
|
43
51
|
return (
|
|
44
52
|
<img
|
|
45
53
|
src={view.src}
|
|
46
|
-
alt={name
|
|
54
|
+
alt={name}
|
|
47
55
|
width={view.width}
|
|
48
56
|
height={view.height}
|
|
49
57
|
loading="lazy"
|
|
50
58
|
decoding="async"
|
|
51
59
|
aria-hidden={name ? undefined : true}
|
|
60
|
+
// The name comes from the reader's own runtime, so a server render and its
|
|
61
|
+
// hydration can legitimately disagree on the language it is written in. Same
|
|
62
|
+
// escape hatch, and same reason, as the relative timestamp.
|
|
63
|
+
suppressHydrationWarning
|
|
52
64
|
data-enigma-flag=""
|
|
53
65
|
data-flag-code={view.code}
|
|
54
66
|
data-flag-shape={view.shape}
|
package/src/react/index.ts
CHANGED
|
@@ -10,16 +10,23 @@ export { useButton, type UseButtonResult } from "@/react/use-button";
|
|
|
10
10
|
export { Button, type ButtonProps } from "@/react/button";
|
|
11
11
|
export { setLinkComponent, getLinkComponent } from "@/react/link";
|
|
12
12
|
export { type ButtonOptions, type ButtonState } from "@/core/button";
|
|
13
|
+
export { Input } from "@/react/input";
|
|
13
14
|
export {
|
|
14
|
-
Input,
|
|
15
|
-
PasswordStrength,
|
|
16
15
|
type InputProps,
|
|
17
|
-
type
|
|
16
|
+
type InputBaseProps,
|
|
17
|
+
type InputType,
|
|
18
18
|
type FieldAction,
|
|
19
19
|
type BreachChecker,
|
|
20
20
|
type BreachState,
|
|
21
|
-
type BreachStatus
|
|
22
|
-
|
|
21
|
+
type BreachStatus,
|
|
22
|
+
type PasswordOnlyProps,
|
|
23
|
+
type SearchOnlyProps,
|
|
24
|
+
type PlainOnlyProps
|
|
25
|
+
} from "@/react/input/types";
|
|
26
|
+
// The meter as a standalone component, for a form that renders it somewhere else. Re-exported
|
|
27
|
+
// from the chunk rather than through `Input`, so importing only `Input` leaves it behind.
|
|
28
|
+
export { PasswordStrength, type PasswordStrengthProps } from "@/react/input/password";
|
|
29
|
+
export { Slot, mergeSlotProps, type SlotProps } from "@/react/slot";
|
|
23
30
|
export {
|
|
24
31
|
generatePassword,
|
|
25
32
|
estimatePasswordStrength,
|
|
@@ -38,6 +45,7 @@ export {
|
|
|
38
45
|
flagSrc,
|
|
39
46
|
flagView,
|
|
40
47
|
flagAttributes,
|
|
48
|
+
flagName,
|
|
41
49
|
normalizeFlagCode,
|
|
42
50
|
configureFlags,
|
|
43
51
|
type FlagOptions,
|
|
@@ -45,3 +53,30 @@ export {
|
|
|
45
53
|
type FlagFormat,
|
|
46
54
|
type FlagSource
|
|
47
55
|
} from "@/core/flags";
|
|
56
|
+
export {
|
|
57
|
+
SearchPalette,
|
|
58
|
+
PaletteRoot,
|
|
59
|
+
PaletteTrigger,
|
|
60
|
+
PaletteContent,
|
|
61
|
+
PaletteField,
|
|
62
|
+
PaletteList,
|
|
63
|
+
PaletteItem,
|
|
64
|
+
PaletteFooter,
|
|
65
|
+
usePaletteContext,
|
|
66
|
+
type SearchPaletteProps,
|
|
67
|
+
type PaletteRootProps,
|
|
68
|
+
type PaletteListProps,
|
|
69
|
+
type PaletteSection,
|
|
70
|
+
type PaletteRow,
|
|
71
|
+
type PaletteContextValue
|
|
72
|
+
} from "@/react/palette";
|
|
73
|
+
export {
|
|
74
|
+
createRecentStore,
|
|
75
|
+
recentKey,
|
|
76
|
+
groupRows,
|
|
77
|
+
moveActive,
|
|
78
|
+
shortcutLabel,
|
|
79
|
+
isPaletteShortcut,
|
|
80
|
+
type RecentEntry,
|
|
81
|
+
type RecentStore
|
|
82
|
+
} from "@/core/palette";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The built-in glyphs, drawn from the shared path data.
|
|
5
|
+
*
|
|
6
|
+
* Stroked with `currentColor` at 1em, so an icon inherits the field's text colour and size
|
|
7
|
+
* and needs no styling of its own. Replacing one is a `FieldAction` with the same `name`.
|
|
8
|
+
*/
|
|
9
|
+
const ICON_PROPS = {
|
|
10
|
+
viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "none",
|
|
11
|
+
stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
|
|
12
|
+
"aria-hidden": true
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export function Icon({ paths }: { paths: readonly string[]; }): ReactNode {
|
|
16
|
+
return <svg {...ICON_PROPS}>{paths.map((path) => <path key={path} d={path} />)}</svg>;
|
|
17
|
+
}
|