@enigmax/primitives 0.15.0 → 0.17.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-53DQY6LP.js → chunk-4GLYRF4B.js} +99 -4
- package/dist/{chunk-U2KYYMBJ.js → chunk-JFNND6P4.js} +27 -2
- package/dist/index.d.ts +107 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +3 -3
- package/dist/react/index.d.ts +30 -4
- package/dist/react/index.js +2 -2
- package/dist/react-router/index.d.ts +2 -2
- package/dist/react-router/index.js +3 -3
- package/package.json +3 -1
- package/recipes/logo-marquee/index.tsx +18 -2
- package/registry.json +62 -0
- package/src/core/flags.ts +220 -0
- package/src/index.ts +18 -0
- package/src/react/flag.tsx +58 -0
- package/src/react/index.ts +12 -0
|
@@ -346,11 +346,11 @@ function createInput(input, options = {}) {
|
|
|
346
346
|
return typeof opts.reveal === "object" ? opts.reveal : {};
|
|
347
347
|
}
|
|
348
348
|
function revealAction() {
|
|
349
|
-
const
|
|
349
|
+
const config2 = revealConfig();
|
|
350
350
|
return {
|
|
351
351
|
name: "reveal",
|
|
352
|
-
label: (current) => current.revealed ?
|
|
353
|
-
icon: (current) => current.revealed ?
|
|
352
|
+
label: (current) => current.revealed ? config2.hideLabel ?? "Hide password" : config2.showLabel ?? "Show password",
|
|
353
|
+
icon: (current) => current.revealed ? config2.hideIcon ?? EYE_OFF : config2.showIcon ?? EYE,
|
|
354
354
|
pressed: (current) => current.revealed,
|
|
355
355
|
onSelect: () => setRevealed(!revealed)
|
|
356
356
|
};
|
|
@@ -1161,4 +1161,99 @@ function relativeTimeAttributes(view, options = {}) {
|
|
|
1161
1161
|
return out;
|
|
1162
1162
|
}
|
|
1163
1163
|
|
|
1164
|
-
|
|
1164
|
+
// src/core/flags.ts
|
|
1165
|
+
var FLAG_ICONS_VERSION = "7.5.0";
|
|
1166
|
+
var CIRCLE_FLAGS_REF = "gh-pages";
|
|
1167
|
+
var DEFAULTS2 = {
|
|
1168
|
+
shape: "rect",
|
|
1169
|
+
format: "svg",
|
|
1170
|
+
source: "cdn",
|
|
1171
|
+
basePath: "/flags",
|
|
1172
|
+
size: 16
|
|
1173
|
+
};
|
|
1174
|
+
var config = { ...DEFAULTS2 };
|
|
1175
|
+
function configureFlags(patch) {
|
|
1176
|
+
config = { ...config, ...patch };
|
|
1177
|
+
return config;
|
|
1178
|
+
}
|
|
1179
|
+
function flagConfig() {
|
|
1180
|
+
return config;
|
|
1181
|
+
}
|
|
1182
|
+
function resetFlagConfig() {
|
|
1183
|
+
config = { ...DEFAULTS2 };
|
|
1184
|
+
return config;
|
|
1185
|
+
}
|
|
1186
|
+
var FLAG_RATIO = { rect: 4 / 3, square: 1, circle: 1 };
|
|
1187
|
+
var EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
|
|
1188
|
+
var LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
|
|
1189
|
+
var FILE_CODE = /^[a-z]{2,}(?:[-_][a-z0-9]+)*$/;
|
|
1190
|
+
function normalizeFlagCode(value) {
|
|
1191
|
+
if (!value) return null;
|
|
1192
|
+
const raw = value.trim();
|
|
1193
|
+
if (!raw) return null;
|
|
1194
|
+
if (EMOJI_FLAG.test(raw)) {
|
|
1195
|
+
const letters = [...raw].map((char) => String.fromCharCode((char.codePointAt(0) ?? 0) - 127462 + 97));
|
|
1196
|
+
return letters.join("");
|
|
1197
|
+
}
|
|
1198
|
+
const locale = LOCALE_TAG.exec(raw);
|
|
1199
|
+
if (locale) return locale[2].toLowerCase();
|
|
1200
|
+
const code = raw.toLowerCase();
|
|
1201
|
+
return FILE_CODE.test(code) ? code : null;
|
|
1202
|
+
}
|
|
1203
|
+
function cdnUrl(code, shape) {
|
|
1204
|
+
if (shape === "circle") return `https://cdn.jsdelivr.net/gh/HatScripts/circle-flags@${CIRCLE_FLAGS_REF}/flags/${code}.svg`;
|
|
1205
|
+
const size = shape === "square" ? "1x1" : "4x3";
|
|
1206
|
+
return `https://cdn.jsdelivr.net/npm/flag-icons@${FLAG_ICONS_VERSION}/flags/${size}/${code}.svg`;
|
|
1207
|
+
}
|
|
1208
|
+
var warnedAboutFormat = false;
|
|
1209
|
+
function flagSrc(code, options = {}) {
|
|
1210
|
+
const settings = { ...config, ...options };
|
|
1211
|
+
const normalized = normalizeFlagCode(code);
|
|
1212
|
+
if (!normalized) return null;
|
|
1213
|
+
const remoteCdn = settings.source === "cdn";
|
|
1214
|
+
let format2 = settings.format;
|
|
1215
|
+
if (remoteCdn && format2 !== "svg") {
|
|
1216
|
+
if (!warnedAboutFormat && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
1217
|
+
warnedAboutFormat = true;
|
|
1218
|
+
console.warn(`[enigma/flags] The flag CDNs publish SVG only, so "${format2}" was served as SVG. Download a local set for raster formats: enigma add flags --flags local --flag-formats ${format2}`);
|
|
1219
|
+
}
|
|
1220
|
+
format2 = "svg";
|
|
1221
|
+
}
|
|
1222
|
+
if (remoteCdn) return cdnUrl(normalized, settings.shape);
|
|
1223
|
+
const base = (settings.source === "local" ? settings.basePath : settings.source).replace(/\/+$/, "");
|
|
1224
|
+
return `${base}/${settings.shape}/${normalized}.${format2}`;
|
|
1225
|
+
}
|
|
1226
|
+
function flagView(code, options = {}) {
|
|
1227
|
+
const settings = { ...config, ...options };
|
|
1228
|
+
const normalized = normalizeFlagCode(code);
|
|
1229
|
+
const src = flagSrc(normalized, options);
|
|
1230
|
+
if (!normalized || !src) return null;
|
|
1231
|
+
const height = Math.max(1, Math.round(settings.size));
|
|
1232
|
+
return {
|
|
1233
|
+
code: normalized,
|
|
1234
|
+
src,
|
|
1235
|
+
shape: settings.shape,
|
|
1236
|
+
format: settings.source === "cdn" ? "svg" : settings.format,
|
|
1237
|
+
width: Math.round(height * FLAG_RATIO[settings.shape]),
|
|
1238
|
+
height
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
function flagAttributes(code, options = {}) {
|
|
1242
|
+
const view = flagView(code, options);
|
|
1243
|
+
if (!view) return null;
|
|
1244
|
+
const label = options.label?.trim();
|
|
1245
|
+
return {
|
|
1246
|
+
src: view.src,
|
|
1247
|
+
alt: label ?? "",
|
|
1248
|
+
width: view.width,
|
|
1249
|
+
height: view.height,
|
|
1250
|
+
loading: "lazy",
|
|
1251
|
+
decoding: "async",
|
|
1252
|
+
"data-enigma-flag": "",
|
|
1253
|
+
"data-flag-code": view.code,
|
|
1254
|
+
"data-flag-shape": view.shape,
|
|
1255
|
+
...label ? {} : { "aria-hidden": "true" }
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export { CIRCLE_FLAGS_REF, FLAG_ICONS_VERSION, FLAG_RATIO, INPUT_ICON_PATHS, SERVER_NETWORK_STATE, configureFlags, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, flagAttributes, flagConfig, flagSrc, flagView, generatePassword, iconMarkup, normalizeDate, normalizeFlagCode, parseDuration, relativeTimeAttributes, relativeTimeView, resetFlagConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createNotifications, createButton, createMarquee, createInput, createNetworkMonitor, SERVER_NETWORK_STATE, relativeTimeView, relativeTimeAttributes } from './chunk-
|
|
1
|
+
import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createNotifications, createButton, createMarquee, createInput, createNetworkMonitor, SERVER_NETWORK_STATE, relativeTimeView, relativeTimeAttributes, flagView } from './chunk-4GLYRF4B.js';
|
|
2
2
|
import { createSearch } from './chunk-UZFEEFMF.js';
|
|
3
3
|
import { forwardRef, createElement, useRef, useState, useId, useCallback, useEffect, useMemo, useSyncExternalStore, useLayoutEffect } from 'react';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -720,5 +720,30 @@ function RelativeTime({
|
|
|
720
720
|
const { prefix: prefixAttribute, ...attributes } = relativeTimeAttributes(view, options);
|
|
721
721
|
return createElement("relative-time", { ...attributes, ...marks, ...rest, ref: applyPrefix }, view.label);
|
|
722
722
|
}
|
|
723
|
+
function Flag({ code, label, shape, format, source, basePath, size, fallback = null, ...rest }) {
|
|
724
|
+
const options = { shape, format, source, basePath, size };
|
|
725
|
+
for (const key of Object.keys(options)) {
|
|
726
|
+
if (options[key] === void 0) delete options[key];
|
|
727
|
+
}
|
|
728
|
+
const view = flagView(code, options);
|
|
729
|
+
if (!view) return fallback;
|
|
730
|
+
const name = label?.trim();
|
|
731
|
+
return /* @__PURE__ */ jsx(
|
|
732
|
+
"img",
|
|
733
|
+
{
|
|
734
|
+
src: view.src,
|
|
735
|
+
alt: name ?? "",
|
|
736
|
+
width: view.width,
|
|
737
|
+
height: view.height,
|
|
738
|
+
loading: "lazy",
|
|
739
|
+
decoding: "async",
|
|
740
|
+
"aria-hidden": name ? void 0 : true,
|
|
741
|
+
"data-enigma-flag": "",
|
|
742
|
+
"data-flag-code": view.code,
|
|
743
|
+
"data-flag-shape": view.shape,
|
|
744
|
+
...rest
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
}
|
|
723
748
|
|
|
724
|
-
export { Button, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch };
|
|
749
|
+
export { Button, Flag, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch };
|
package/dist/index.d.ts
CHANGED
|
@@ -514,4 +514,110 @@ declare function relativeTimeView(value: string | number | Date | null | undefin
|
|
|
514
514
|
*/
|
|
515
515
|
declare function relativeTimeAttributes(view: RelativeTimeView, options?: RelativeTimeOptions): Record<string, string>;
|
|
516
516
|
|
|
517
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Country flags as ASSETS, which is the only form of them that renders everywhere.
|
|
519
|
+
*
|
|
520
|
+
* The emoji flag is the thing this replaces. It is a pair of regional-indicator code
|
|
521
|
+
* points, and the glyph for that pair exists only where the platform ships one: Windows
|
|
522
|
+
* has never shipped country flags in Segoe UI Emoji, so `\u{1F1EA}\u{1F1F8}` renders as
|
|
523
|
+
* the letters "ES" for the largest desktop platform there is - and a language picker built
|
|
524
|
+
* out of them looks broken to those readers, with nothing in the code to explain why.
|
|
525
|
+
* There is no font stack that fixes it, so the fix is not a font: it is an image.
|
|
526
|
+
*
|
|
527
|
+
* Two upstream sets, both SVG, both installable or served from a CDN:
|
|
528
|
+
*
|
|
529
|
+
* - `rect` / `square` - lipis/flag-icons (`flags/4x3`, `flags/1x1`), the classic
|
|
530
|
+
* rectangular set, 271 flags plus subdivisions.
|
|
531
|
+
* - `circle` - HatScripts/circle-flags (`flags/*.svg` on its gh-pages branch), 445 round
|
|
532
|
+
* flags, which is what a flag next to an avatar or inside a chip usually wants.
|
|
533
|
+
*
|
|
534
|
+
* Nothing here touches the DOM, so it renders on a server.
|
|
535
|
+
*/
|
|
536
|
+
type FlagShape = "rect" | "square" | "circle";
|
|
537
|
+
/**
|
|
538
|
+
* `svg` is the only format the two upstreams publish, so it is the only one a CDN can
|
|
539
|
+
* serve. `png` and `webp` exist for a LOCAL set (`enigma add flags --flags local
|
|
540
|
+
* --flag-formats webp`), which rasterises what it downloads, and for a mirror of your own.
|
|
541
|
+
*/
|
|
542
|
+
type FlagFormat = "svg" | "png" | "webp";
|
|
543
|
+
/**
|
|
544
|
+
* Where the files come from.
|
|
545
|
+
*
|
|
546
|
+
* - `"cdn"` - jsDelivr, straight from the two upstream projects. Nothing to install.
|
|
547
|
+
* - `"local"` - files under `basePath` in your own public directory.
|
|
548
|
+
* - any URL - a mirror of your own with the same layout as the local one
|
|
549
|
+
* (`<base>/<shape>/<code>.<format>`), which is exactly the tree the downloader writes.
|
|
550
|
+
*/
|
|
551
|
+
type FlagSource = "cdn" | "local" | (string & {});
|
|
552
|
+
/** Pinned so an upgrade upstream is a decision here, never a surprise in production. */
|
|
553
|
+
declare const FLAG_ICONS_VERSION = "7.5.0";
|
|
554
|
+
/** circle-flags publishes from a branch, not a tag; gh-pages is its default branch. */
|
|
555
|
+
declare const CIRCLE_FLAGS_REF = "gh-pages";
|
|
556
|
+
interface FlagOptions {
|
|
557
|
+
shape?: FlagShape;
|
|
558
|
+
format?: FlagFormat;
|
|
559
|
+
source?: FlagSource;
|
|
560
|
+
/** Where a local set is served from. Default `/flags`. */
|
|
561
|
+
basePath?: string;
|
|
562
|
+
/** Rendered HEIGHT in px. The width follows the shape's ratio. Default 16. */
|
|
563
|
+
size?: number;
|
|
564
|
+
}
|
|
565
|
+
interface FlagConfig extends Required<Omit<FlagOptions, "size">> {
|
|
566
|
+
size: number;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Set the defaults for every flag on the page, once, at startup.
|
|
570
|
+
*
|
|
571
|
+
* This is what makes the CDN/local decision a one-line change rather than a prop on every
|
|
572
|
+
* call site: `configureFlags({ source: "local" })` moves the whole app onto the files the
|
|
573
|
+
* downloader wrote, and nothing else in the codebase mentions where a flag comes from.
|
|
574
|
+
*/
|
|
575
|
+
declare function configureFlags(patch: Partial<FlagConfig>): FlagConfig;
|
|
576
|
+
declare function flagConfig(): FlagConfig;
|
|
577
|
+
/** Test seam and a way back to the shipped defaults. */
|
|
578
|
+
declare function resetFlagConfig(): FlagConfig;
|
|
579
|
+
/** Width / height of each shape. The circle set is square; only `rect` is 4:3. */
|
|
580
|
+
declare const FLAG_RATIO: Record<FlagShape, number>;
|
|
581
|
+
/**
|
|
582
|
+
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
583
|
+
*
|
|
584
|
+
* Null rather than a guess: a code this cannot resolve would otherwise become a 404 image
|
|
585
|
+
* with no alt text, and a broken flag beside a country name is worse than no flag at all.
|
|
586
|
+
* An emoji flag is accepted and converted, so migrating an existing picker is a rename of
|
|
587
|
+
* the component and nothing else.
|
|
588
|
+
*/
|
|
589
|
+
declare function normalizeFlagCode(value: string | null | undefined): string | null;
|
|
590
|
+
/** Everything a renderer needs, computed in one pass. Null when the code is unusable. */
|
|
591
|
+
interface FlagView {
|
|
592
|
+
/** The upstream file name, e.g. `es`, `gb-eng`. */
|
|
593
|
+
code: string;
|
|
594
|
+
src: string;
|
|
595
|
+
shape: FlagShape;
|
|
596
|
+
format: FlagFormat;
|
|
597
|
+
width: number;
|
|
598
|
+
height: number;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The URL for one flag. Null when the code cannot be resolved.
|
|
602
|
+
*
|
|
603
|
+
* A raster format asked of the CDN falls back to SVG rather than producing a 404: neither
|
|
604
|
+
* upstream publishes anything but SVG, so `format: "webp"` is only meaningful against a
|
|
605
|
+
* local set or a mirror. It says so once, in development, instead of silently disagreeing
|
|
606
|
+
* with what the call site asked for.
|
|
607
|
+
*/
|
|
608
|
+
declare function flagSrc(code: string | null | undefined, options?: FlagOptions): string | null;
|
|
609
|
+
/** `flagSrc` plus the resolved geometry, for a renderer that has to size the box. */
|
|
610
|
+
declare function flagView(code: string | null | undefined, options?: FlagOptions): FlagView | null;
|
|
611
|
+
/**
|
|
612
|
+
* Attributes for a plain `<img>`, for vanilla, Astro and any template language.
|
|
613
|
+
*
|
|
614
|
+
* `label` is the accessible name and there is no default for it: a flag with no label is
|
|
615
|
+
* DECORATIVE (empty alt, `aria-hidden`), which is correct beside a country name and is the
|
|
616
|
+
* common case. Inventing one from the code would put "ES" or "es" into a screen reader,
|
|
617
|
+
* and a country name guessed in the reader's wrong language is worse than silence.
|
|
618
|
+
*/
|
|
619
|
+
declare function flagAttributes(code: string | null | undefined, options?: FlagOptions & {
|
|
620
|
+
label?: string;
|
|
621
|
+
}): Record<string, string | number> | null;
|
|
622
|
+
|
|
623
|
+
export { type ButtonCooldown, type ButtonElement, type ButtonInstance, type ButtonOptions, type ButtonState, CIRCLE_FLAGS_REF, type EstimateOptions, FLAG_ICONS_VERSION, FLAG_RATIO, type FlagConfig, type FlagFormat, type FlagOptions, type FlagShape, type FlagSource, type FlagView, type GeneratePasswordOptions, INPUT_ICON_PATHS, type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeHover, type MarqueeInstance, type MarqueeOptions, type NetworkMonitor, type NetworkState, type Notification, type NotificationAction, type NotificationInput, type NotificationTone, type Notifications, type NotificationsOptions, type PasswordAlphabet, type PasswordScore, type PasswordStrengthReport, type PromiseMessages, type RelativeTimeFormat, type RelativeTimeOptions, type RelativeTimePrecision, type RelativeTimeStyle, type RelativeTimeTense, type RelativeTimeView, SERVER_NETWORK_STATE, configureFlags, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, flagAttributes, flagConfig, flagSrc, flagView, generatePassword, iconMarkup, normalizeDate, normalizeFlagCode, parseDuration, relativeTimeAttributes, relativeTimeView, resetFlagConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { INPUT_ICON_PATHS, SERVER_NETWORK_STATE, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, generatePassword, iconMarkup, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView } from './chunk-
|
|
1
|
+
export { CIRCLE_FLAGS_REF, FLAG_ICONS_VERSION, FLAG_RATIO, INPUT_ICON_PATHS, SERVER_NETWORK_STATE, configureFlags, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, flagAttributes, flagConfig, flagSrc, flagView, generatePassword, iconMarkup, normalizeDate, normalizeFlagCode, parseDuration, relativeTimeAttributes, relativeTimeView, resetFlagConfig } from './chunk-4GLYRF4B.js';
|
|
2
2
|
export { createSearch } from './chunk-UZFEEFMF.js';
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ButtonProps } from '../react/index.js';
|
|
3
|
-
export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, RelativeTime, RelativeTimeProps, ToastControls, ToastPosition, Toaster, ToasterProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseNotificationsResult, UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../react/index.js';
|
|
4
|
-
export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, NetworkState, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
|
|
3
|
+
export { BreachChecker, BreachState, BreachStatus, FieldAction, Flag, FlagProps, Input, InputProps, PasswordStrength, PasswordStrengthProps, RelativeTime, RelativeTimeProps, ToastControls, ToastPosition, Toaster, ToasterProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseNotificationsResult, UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../react/index.js';
|
|
4
|
+
export { ButtonOptions, ButtonState, EstimateOptions, FlagFormat, FlagOptions, FlagShape, FlagSource, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, NetworkState, PasswordScore, PasswordStrengthReport, configureFlags, estimatePasswordStrength, flagAttributes, flagSrc, flagView, generatePassword, normalizeFlagCode } from '../index.js';
|
|
5
5
|
export { F as FuseConstructor, b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
6
6
|
|
|
7
7
|
/**
|
package/dist/next/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Button } from '../chunk-
|
|
3
|
-
export { Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-
|
|
4
|
-
export { estimatePasswordStrength, generatePassword } from '../chunk-
|
|
2
|
+
import { Button } from '../chunk-JFNND6P4.js';
|
|
3
|
+
export { Flag, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-JFNND6P4.js';
|
|
4
|
+
export { configureFlags, estimatePasswordStrength, flagAttributes, flagSrc, flagView, generatePassword, normalizeFlagCode } from '../chunk-4GLYRF4B.js';
|
|
5
5
|
import '../chunk-UZFEEFMF.js';
|
|
6
6
|
import Link from 'next/link';
|
|
7
7
|
import { forwardRef, createElement } from 'react';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ComponentPropsWithoutRef, ElementType, ReactNode, RefObject, CSSProperties, HTMLAttributes } from 'react';
|
|
3
|
-
import { ButtonOptions, ButtonState, MarqueeOptions, InputOptions, GeneratePasswordOptions, EstimateOptions, PasswordStrengthReport, Notifications, Notification, NotificationsOptions, NetworkState, RelativeTimeOptions } from '../index.js';
|
|
4
|
-
export { InputAction, InputIcon, MarqueeHover, MarqueeInstance, PasswordScore, estimatePasswordStrength, generatePassword } from '../index.js';
|
|
2
|
+
import { ComponentPropsWithoutRef, ElementType, ReactNode, RefObject, CSSProperties, HTMLAttributes, ImgHTMLAttributes } from 'react';
|
|
3
|
+
import { ButtonOptions, ButtonState, MarqueeOptions, InputOptions, GeneratePasswordOptions, EstimateOptions, PasswordStrengthReport, Notifications, Notification, NotificationsOptions, NetworkState, RelativeTimeOptions, FlagShape, FlagFormat, FlagSource } from '../index.js';
|
|
4
|
+
export { FlagOptions, InputAction, InputIcon, MarqueeHover, MarqueeInstance, PasswordScore, configureFlags, estimatePasswordStrength, flagAttributes, flagSrc, flagView, generatePassword, normalizeFlagCode } from '../index.js';
|
|
5
5
|
import { b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
6
6
|
export { F as FuseConstructor } from '../search-CsO3L1Lw.js';
|
|
7
7
|
|
|
@@ -373,4 +373,30 @@ interface RelativeTimeProps extends RelativeTimeOptions, Omit<HTMLAttributes<HTM
|
|
|
373
373
|
}
|
|
374
374
|
declare function RelativeTime({ date, fallback, capitalize, capitalizeFirst, now, format, tense, precision, threshold, prefix, formatStyle, locale, timeZone, second, minute, hour, weekday, day, month, year, timeZoneName, noTitle, numericBeyondThreshold, ...rest }: RelativeTimeProps): ReactNode;
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
interface FlagProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "width" | "height"> {
|
|
377
|
+
/** `es`, `ES`, `en-GB`, `gb-eng`, or the emoji flag you are replacing. */
|
|
378
|
+
code: string | null | undefined;
|
|
379
|
+
/**
|
|
380
|
+
* The accessible name. Leave it out and the flag is decorative, which is what it is
|
|
381
|
+
* beside a country name - the reader hears the name once instead of twice.
|
|
382
|
+
*/
|
|
383
|
+
label?: string;
|
|
384
|
+
shape?: FlagShape;
|
|
385
|
+
format?: FlagFormat;
|
|
386
|
+
source?: FlagSource;
|
|
387
|
+
basePath?: string;
|
|
388
|
+
/** Rendered height in px. The width follows the shape. Default 16. */
|
|
389
|
+
size?: number;
|
|
390
|
+
/** Rendered when the code resolves to nothing. Nothing, by default. */
|
|
391
|
+
fallback?: ReactNode;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* `<Flag code="es" />` - a country flag as an image, at the size you asked for, from the
|
|
395
|
+
* source the app configured once.
|
|
396
|
+
*
|
|
397
|
+
* The defaults come from `configureFlags`, so moving the whole application from the CDN to
|
|
398
|
+
* a downloaded set is one line at startup and no change at any call site.
|
|
399
|
+
*/
|
|
400
|
+
declare function Flag({ code, label, shape, format, source, basePath, size, fallback, ...rest }: FlagProps): ReactNode;
|
|
401
|
+
|
|
402
|
+
export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, Flag, FlagFormat, type FlagProps, FlagShape, FlagSource, GeneratePasswordOptions, Input, InputOptions, type InputProps, MarqueeOptions, NetworkState, PasswordStrength, type PasswordStrengthProps, PasswordStrengthReport, RelativeTime, type RelativeTimeProps, SearchMatch, SearchOptions, type ToastControls, type ToastPosition, Toaster, type ToasterProps, type UseButtonResult, type UseInputResult, type UseMarqueeResult, type UseNotificationsResult, type UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch };
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export { Button, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-
|
|
3
|
-
export { estimatePasswordStrength, generatePassword } from '../chunk-
|
|
2
|
+
export { Button, Flag, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-JFNND6P4.js';
|
|
3
|
+
export { configureFlags, estimatePasswordStrength, flagAttributes, flagSrc, flagView, generatePassword, normalizeFlagCode } from '../chunk-4GLYRF4B.js';
|
|
4
4
|
import '../chunk-UZFEEFMF.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ButtonProps } from '../react/index.js';
|
|
3
|
-
export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, RelativeTime, RelativeTimeProps, ToastControls, ToastPosition, Toaster, ToasterProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseNotificationsResult, UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../react/index.js';
|
|
4
|
-
export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, NetworkState, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
|
|
3
|
+
export { BreachChecker, BreachState, BreachStatus, FieldAction, Flag, FlagProps, Input, InputProps, PasswordStrength, PasswordStrengthProps, RelativeTime, RelativeTimeProps, ToastControls, ToastPosition, Toaster, ToasterProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseNotificationsResult, UseSearchResult, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../react/index.js';
|
|
4
|
+
export { ButtonOptions, ButtonState, EstimateOptions, FlagFormat, FlagOptions, FlagShape, FlagSource, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, NetworkState, PasswordScore, PasswordStrengthReport, configureFlags, estimatePasswordStrength, flagAttributes, flagSrc, flagView, generatePassword, normalizeFlagCode } from '../index.js';
|
|
5
5
|
export { F as FuseConstructor, b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
6
6
|
|
|
7
7
|
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLElement>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Button } from '../chunk-
|
|
3
|
-
export { Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-
|
|
4
|
-
export { estimatePasswordStrength, generatePassword } from '../chunk-
|
|
2
|
+
import { Button } from '../chunk-JFNND6P4.js';
|
|
3
|
+
export { Flag, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-JFNND6P4.js';
|
|
4
|
+
export { configureFlags, estimatePasswordStrength, flagAttributes, flagSrc, flagView, generatePassword, normalizeFlagCode } from '../chunk-4GLYRF4B.js';
|
|
5
5
|
import '../chunk-UZFEEFMF.js';
|
|
6
6
|
import { Link } from 'react-router';
|
|
7
7
|
import { forwardRef, createElement } from 'react';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Headless components: the behaviour, the timing and the accessibility, with no styles of their own. Framework-agnostic core plus thin adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -92,6 +92,8 @@
|
|
|
92
92
|
"keywords": [
|
|
93
93
|
"carousel",
|
|
94
94
|
"enigma",
|
|
95
|
+
"flag-icons",
|
|
96
|
+
"flags",
|
|
95
97
|
"fuse",
|
|
96
98
|
"headless",
|
|
97
99
|
"input",
|
|
@@ -14,9 +14,22 @@ import { useMarquee, type MarqueeHover } from "@enigmax/primitives/react";
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export type LogoItem =
|
|
17
|
+
/**
|
|
18
|
+
* A URL, which is the whole entry for most logos:
|
|
19
|
+
* `logos={["/logos/claude.svg", "/logos/openai.svg"]}`.
|
|
20
|
+
*
|
|
21
|
+
* It carries no `alt` - an alt guessed from a file name is a fabricated one, and a
|
|
22
|
+
* wrong alt is worse than an empty one. Use the object form where a mark needs a name.
|
|
23
|
+
*/
|
|
24
|
+
| string
|
|
17
25
|
| { kind: "img"; src: string; alt?: string; /** Override the shared height for one that reads small. */ height?: number; }
|
|
18
26
|
| { kind: "node"; node: ReactNode; key?: string; };
|
|
19
27
|
|
|
28
|
+
/** The object form of whatever was passed. A bare URL is an image with no alt. */
|
|
29
|
+
function normalize(item: LogoItem): Exclude<LogoItem, string> {
|
|
30
|
+
return typeof item === "string" ? { kind: "img", src: item } : item;
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export interface LogoMarqueeProps {
|
|
21
34
|
logos: LogoItem[];
|
|
22
35
|
/**
|
|
@@ -61,7 +74,9 @@ export function LogoMarquee({
|
|
|
61
74
|
}: LogoMarqueeProps) {
|
|
62
75
|
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed, hover, reverse });
|
|
63
76
|
|
|
64
|
-
const render = (
|
|
77
|
+
const render = (raw: LogoItem, index: number): ReactNode => {
|
|
78
|
+
const item = normalize(raw);
|
|
79
|
+
return (
|
|
65
80
|
<span className="logo-marquee__item" key={item.kind === "node" ? item.key ?? index : index}>
|
|
66
81
|
{item.kind === "img"
|
|
67
82
|
? <img
|
|
@@ -77,7 +92,8 @@ export function LogoMarquee({
|
|
|
77
92
|
/>
|
|
78
93
|
: item.node}
|
|
79
94
|
</span>
|
|
80
|
-
|
|
95
|
+
);
|
|
96
|
+
};
|
|
81
97
|
|
|
82
98
|
return (
|
|
83
99
|
<div
|
package/registry.json
CHANGED
|
@@ -417,6 +417,68 @@
|
|
|
417
417
|
],
|
|
418
418
|
"docs": "docs/notes/primitives.md#button"
|
|
419
419
|
},
|
|
420
|
+
{
|
|
421
|
+
"name": "flags",
|
|
422
|
+
"title": "Country flags",
|
|
423
|
+
"description": "A flag as an image, from the CDN or from files you downloaded, in the rectangular set or the round one. Emoji flags are the thing this replaces: Windows ships no glyph for them, so a picker built out of emoji renders as bare letters for most desktop readers.",
|
|
424
|
+
"targets": [
|
|
425
|
+
"vanilla",
|
|
426
|
+
"astro",
|
|
427
|
+
"react"
|
|
428
|
+
],
|
|
429
|
+
"entry": {
|
|
430
|
+
"vanilla": "@enigmax/primitives",
|
|
431
|
+
"astro": "@enigmax/primitives",
|
|
432
|
+
"react": "@enigmax/primitives/react"
|
|
433
|
+
},
|
|
434
|
+
"exports": {
|
|
435
|
+
"vanilla": [
|
|
436
|
+
"flagAttributes",
|
|
437
|
+
"flagSrc",
|
|
438
|
+
"configureFlags"
|
|
439
|
+
],
|
|
440
|
+
"astro": [
|
|
441
|
+
"flagAttributes",
|
|
442
|
+
"flagSrc",
|
|
443
|
+
"configureFlags"
|
|
444
|
+
],
|
|
445
|
+
"react": [
|
|
446
|
+
"Flag",
|
|
447
|
+
"configureFlags"
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
"files": [
|
|
451
|
+
{
|
|
452
|
+
"path": "src/core/flags.ts",
|
|
453
|
+
"dest": "flags/flags.ts",
|
|
454
|
+
"targets": [
|
|
455
|
+
"vanilla",
|
|
456
|
+
"astro",
|
|
457
|
+
"react"
|
|
458
|
+
]
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"path": "src/react/flag.tsx",
|
|
462
|
+
"dest": "flags/index.tsx",
|
|
463
|
+
"targets": [
|
|
464
|
+
"react"
|
|
465
|
+
],
|
|
466
|
+
"rewrite": {
|
|
467
|
+
"@/core/flags": "./flags"
|
|
468
|
+
},
|
|
469
|
+
"main": true
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
"styles": false,
|
|
473
|
+
"themeHooks": [
|
|
474
|
+
"[data-enigma-flag]",
|
|
475
|
+
"[data-enigma-flag][data-flag-shape=rect]",
|
|
476
|
+
"[data-enigma-flag][data-flag-shape=square]",
|
|
477
|
+
"[data-enigma-flag][data-flag-shape=circle]"
|
|
478
|
+
],
|
|
479
|
+
"assets": "flags",
|
|
480
|
+
"docs": "docs/notes/primitives.md#flags"
|
|
481
|
+
},
|
|
420
482
|
{
|
|
421
483
|
"name": "notifications",
|
|
422
484
|
"title": "Notification queue",
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
* Two upstream sets, both SVG, both installable or served from a CDN:
|
|
12
|
+
*
|
|
13
|
+
* - `rect` / `square` - lipis/flag-icons (`flags/4x3`, `flags/1x1`), the classic
|
|
14
|
+
* rectangular set, 271 flags plus subdivisions.
|
|
15
|
+
* - `circle` - HatScripts/circle-flags (`flags/*.svg` on its gh-pages branch), 445 round
|
|
16
|
+
* flags, which is what a flag next to an avatar or inside a chip usually wants.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here touches the DOM, so it renders on a server.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type FlagShape = "rect" | "square" | "circle";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `svg` is the only format the two upstreams publish, so it is the only one a CDN can
|
|
25
|
+
* serve. `png` and `webp` exist for a LOCAL set (`enigma add flags --flags local
|
|
26
|
+
* --flag-formats webp`), which rasterises what it downloads, and for a mirror of your own.
|
|
27
|
+
*/
|
|
28
|
+
export type FlagFormat = "svg" | "png" | "webp";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Where the files come from.
|
|
32
|
+
*
|
|
33
|
+
* - `"cdn"` - jsDelivr, straight from the two upstream projects. Nothing to install.
|
|
34
|
+
* - `"local"` - files under `basePath` in your own public directory.
|
|
35
|
+
* - any URL - a mirror of your own with the same layout as the local one
|
|
36
|
+
* (`<base>/<shape>/<code>.<format>`), which is exactly the tree the downloader writes.
|
|
37
|
+
*/
|
|
38
|
+
export type FlagSource = "cdn" | "local" | (string & {});
|
|
39
|
+
|
|
40
|
+
/** Pinned so an upgrade upstream is a decision here, never a surprise in production. */
|
|
41
|
+
export const FLAG_ICONS_VERSION = "7.5.0";
|
|
42
|
+
/** circle-flags publishes from a branch, not a tag; gh-pages is its default branch. */
|
|
43
|
+
export const CIRCLE_FLAGS_REF = "gh-pages";
|
|
44
|
+
|
|
45
|
+
export interface FlagOptions {
|
|
46
|
+
shape?: FlagShape;
|
|
47
|
+
format?: FlagFormat;
|
|
48
|
+
source?: FlagSource;
|
|
49
|
+
/** Where a local set is served from. Default `/flags`. */
|
|
50
|
+
basePath?: string;
|
|
51
|
+
/** Rendered HEIGHT in px. The width follows the shape's ratio. Default 16. */
|
|
52
|
+
size?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FlagConfig extends Required<Omit<FlagOptions, "size">> {
|
|
56
|
+
size: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULTS: FlagConfig = {
|
|
60
|
+
shape: "rect",
|
|
61
|
+
format: "svg",
|
|
62
|
+
source: "cdn",
|
|
63
|
+
basePath: "/flags",
|
|
64
|
+
size: 16
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let config: FlagConfig = { ...DEFAULTS };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the defaults for every flag on the page, once, at startup.
|
|
71
|
+
*
|
|
72
|
+
* This is what makes the CDN/local decision a one-line change rather than a prop on every
|
|
73
|
+
* call site: `configureFlags({ source: "local" })` moves the whole app onto the files the
|
|
74
|
+
* downloader wrote, and nothing else in the codebase mentions where a flag comes from.
|
|
75
|
+
*/
|
|
76
|
+
export function configureFlags(patch: Partial<FlagConfig>): FlagConfig {
|
|
77
|
+
config = { ...config, ...patch };
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function flagConfig(): FlagConfig {
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Test seam and a way back to the shipped defaults. */
|
|
86
|
+
export function resetFlagConfig(): FlagConfig {
|
|
87
|
+
config = { ...DEFAULTS };
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Width / height of each shape. The circle set is square; only `rect` is 4:3. */
|
|
92
|
+
export const FLAG_RATIO: Record<FlagShape, number> = { rect: 4 / 3, square: 1, circle: 1 };
|
|
93
|
+
|
|
94
|
+
const EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
|
|
95
|
+
/**
|
|
96
|
+
* A BCP 47 tag whose region is spelled the way the standard spells it: UPPERCASE. That
|
|
97
|
+
* casing is the whole discriminator, and it has to be read before the value is lowercased -
|
|
98
|
+
* `es-ES` is Spanish-in-Spain and means the flag `es`, while `es-ct` is a subdivision code
|
|
99
|
+
* both sets publish (Catalonia) and means the file `es-ct`. Lowercasing first makes those
|
|
100
|
+
* two the same string, and then one of them is always wrong.
|
|
101
|
+
*/
|
|
102
|
+
const LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
|
|
103
|
+
/** What the two sets actually name their files: `es`, `gb-eng`, `au-nsw`, `easter_island`. */
|
|
104
|
+
const FILE_CODE = /^[a-z]{2,}(?:[-_][a-z0-9]+)*$/;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
108
|
+
*
|
|
109
|
+
* Null rather than a guess: a code this cannot resolve would otherwise become a 404 image
|
|
110
|
+
* with no alt text, and a broken flag beside a country name is worse than no flag at all.
|
|
111
|
+
* An emoji flag is accepted and converted, so migrating an existing picker is a rename of
|
|
112
|
+
* the component and nothing else.
|
|
113
|
+
*/
|
|
114
|
+
export function normalizeFlagCode(value: string | null | undefined): string | null {
|
|
115
|
+
if (!value) return null;
|
|
116
|
+
const raw = value.trim();
|
|
117
|
+
if (!raw) return null;
|
|
118
|
+
|
|
119
|
+
if (EMOJI_FLAG.test(raw)) {
|
|
120
|
+
const letters = [...raw].map((char) => String.fromCharCode((char.codePointAt(0) ?? 0) - 0x1f1e6 + 97));
|
|
121
|
+
return letters.join("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const locale = LOCALE_TAG.exec(raw);
|
|
125
|
+
if (locale) return locale[2].toLowerCase();
|
|
126
|
+
|
|
127
|
+
const code = raw.toLowerCase();
|
|
128
|
+
return FILE_CODE.test(code) ? code : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Everything a renderer needs, computed in one pass. Null when the code is unusable. */
|
|
132
|
+
export interface FlagView {
|
|
133
|
+
/** The upstream file name, e.g. `es`, `gb-eng`. */
|
|
134
|
+
code: string;
|
|
135
|
+
src: string;
|
|
136
|
+
shape: FlagShape;
|
|
137
|
+
format: FlagFormat;
|
|
138
|
+
width: number;
|
|
139
|
+
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`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One warning per process, not one per flag: a list of 200 would print 200 times. */
|
|
149
|
+
let warnedAboutFormat = false;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The URL for one flag. Null when the code cannot be resolved.
|
|
153
|
+
*
|
|
154
|
+
* A raster format asked of the CDN falls back to SVG rather than producing a 404: neither
|
|
155
|
+
* upstream publishes anything but SVG, so `format: "webp"` is only meaningful against a
|
|
156
|
+
* local set or a mirror. It says so once, in development, instead of silently disagreeing
|
|
157
|
+
* with what the call site asked for.
|
|
158
|
+
*/
|
|
159
|
+
export function flagSrc(code: string | null | undefined, options: FlagOptions = {}): string | null {
|
|
160
|
+
const settings = { ...config, ...options };
|
|
161
|
+
const normalized = normalizeFlagCode(code);
|
|
162
|
+
if (!normalized) return null;
|
|
163
|
+
|
|
164
|
+
const remoteCdn = settings.source === "cdn";
|
|
165
|
+
let format = settings.format;
|
|
166
|
+
if (remoteCdn && format !== "svg") {
|
|
167
|
+
if (!warnedAboutFormat && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
168
|
+
warnedAboutFormat = true;
|
|
169
|
+
console.warn(`[enigma/flags] The flag CDNs publish SVG only, so "${format}" was served as SVG. Download a local set for raster formats: enigma add flags --flags local --flag-formats ${format}`);
|
|
170
|
+
}
|
|
171
|
+
format = "svg";
|
|
172
|
+
}
|
|
173
|
+
if (remoteCdn) return cdnUrl(normalized, settings.shape);
|
|
174
|
+
|
|
175
|
+
const base = (settings.source === "local" ? settings.basePath : settings.source).replace(/\/+$/, "");
|
|
176
|
+
return `${base}/${settings.shape}/${normalized}.${format}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** `flagSrc` plus the resolved geometry, for a renderer that has to size the box. */
|
|
180
|
+
export function flagView(code: string | null | undefined, options: FlagOptions = {}): FlagView | null {
|
|
181
|
+
const settings = { ...config, ...options };
|
|
182
|
+
const normalized = normalizeFlagCode(code);
|
|
183
|
+
const src = flagSrc(normalized, options);
|
|
184
|
+
if (!normalized || !src) return null;
|
|
185
|
+
const height = Math.max(1, Math.round(settings.size));
|
|
186
|
+
return {
|
|
187
|
+
code: normalized,
|
|
188
|
+
src,
|
|
189
|
+
shape: settings.shape,
|
|
190
|
+
format: settings.source === "cdn" ? "svg" : settings.format,
|
|
191
|
+
width: Math.round(height * FLAG_RATIO[settings.shape]),
|
|
192
|
+
height
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Attributes for a plain `<img>`, for vanilla, Astro and any template language.
|
|
198
|
+
*
|
|
199
|
+
* `label` is the accessible name and there is no default for it: a flag with no label is
|
|
200
|
+
* DECORATIVE (empty alt, `aria-hidden`), which is correct beside a country name and is the
|
|
201
|
+
* common case. Inventing one from the code would put "ES" or "es" into a screen reader,
|
|
202
|
+
* and a country name guessed in the reader's wrong language is worse than silence.
|
|
203
|
+
*/
|
|
204
|
+
export function flagAttributes(code: string | null | undefined, options: FlagOptions & { label?: string; } = {}): Record<string, string | number> | null {
|
|
205
|
+
const view = flagView(code, options);
|
|
206
|
+
if (!view) return null;
|
|
207
|
+
const label = options.label?.trim();
|
|
208
|
+
return {
|
|
209
|
+
src: view.src,
|
|
210
|
+
alt: label ?? "",
|
|
211
|
+
width: view.width,
|
|
212
|
+
height: view.height,
|
|
213
|
+
loading: "lazy",
|
|
214
|
+
decoding: "async",
|
|
215
|
+
"data-enigma-flag": "",
|
|
216
|
+
"data-flag-code": view.code,
|
|
217
|
+
"data-flag-shape": view.shape,
|
|
218
|
+
...(label ? {} : { "aria-hidden": "true" })
|
|
219
|
+
};
|
|
220
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,3 +27,21 @@ export {
|
|
|
27
27
|
type RelativeTimePrecision,
|
|
28
28
|
type RelativeTimeStyle
|
|
29
29
|
} from "@/core/relative-time";
|
|
30
|
+
export {
|
|
31
|
+
flagSrc,
|
|
32
|
+
flagView,
|
|
33
|
+
flagAttributes,
|
|
34
|
+
normalizeFlagCode,
|
|
35
|
+
configureFlags,
|
|
36
|
+
flagConfig,
|
|
37
|
+
resetFlagConfig,
|
|
38
|
+
FLAG_RATIO,
|
|
39
|
+
FLAG_ICONS_VERSION,
|
|
40
|
+
CIRCLE_FLAGS_REF,
|
|
41
|
+
type FlagOptions,
|
|
42
|
+
type FlagConfig,
|
|
43
|
+
type FlagView,
|
|
44
|
+
type FlagShape,
|
|
45
|
+
type FlagFormat,
|
|
46
|
+
type FlagSource
|
|
47
|
+
} from "@/core/flags";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ImgHTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
import { flagView, type FlagOptions, type FlagShape, type FlagFormat, type FlagSource } from "@/core/flags";
|
|
5
|
+
|
|
6
|
+
export interface FlagProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "width" | "height"> {
|
|
7
|
+
/** `es`, `ES`, `en-GB`, `gb-eng`, or the emoji flag you are replacing. */
|
|
8
|
+
code: string | null | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The accessible name. Leave it out and the flag is decorative, which is what it is
|
|
11
|
+
* beside a country name - the reader hears the name once instead of twice.
|
|
12
|
+
*/
|
|
13
|
+
label?: string;
|
|
14
|
+
shape?: FlagShape;
|
|
15
|
+
format?: FlagFormat;
|
|
16
|
+
source?: FlagSource;
|
|
17
|
+
basePath?: string;
|
|
18
|
+
/** Rendered height in px. The width follows the shape. Default 16. */
|
|
19
|
+
size?: number;
|
|
20
|
+
/** Rendered when the code resolves to nothing. Nothing, by default. */
|
|
21
|
+
fallback?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `<Flag code="es" />` - a country flag as an image, at the size you asked for, from the
|
|
26
|
+
* source the app configured once.
|
|
27
|
+
*
|
|
28
|
+
* The defaults come from `configureFlags`, so moving the whole application from the CDN to
|
|
29
|
+
* a downloaded set is one line at startup and no change at any call site.
|
|
30
|
+
*/
|
|
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 };
|
|
33
|
+
// An explicit `undefined` prop must not beat the configured default, so the unset ones
|
|
34
|
+
// are dropped rather than spread over the config.
|
|
35
|
+
for (const key of Object.keys(options) as (keyof FlagOptions)[]) {
|
|
36
|
+
if (options[key] === undefined) delete options[key];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const view = flagView(code, options);
|
|
40
|
+
if (!view) return fallback;
|
|
41
|
+
|
|
42
|
+
const name = label?.trim();
|
|
43
|
+
return (
|
|
44
|
+
<img
|
|
45
|
+
src={view.src}
|
|
46
|
+
alt={name ?? ""}
|
|
47
|
+
width={view.width}
|
|
48
|
+
height={view.height}
|
|
49
|
+
loading="lazy"
|
|
50
|
+
decoding="async"
|
|
51
|
+
aria-hidden={name ? undefined : true}
|
|
52
|
+
data-enigma-flag=""
|
|
53
|
+
data-flag-code={view.code}
|
|
54
|
+
data-flag-shape={view.shape}
|
|
55
|
+
{...rest}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -33,3 +33,15 @@ export { Toaster, type ToasterProps, type ToastPosition, type ToastControls } fr
|
|
|
33
33
|
export { useNetworkState } from "@/react/use-network-state";
|
|
34
34
|
export { type NetworkState } from "@/core/network";
|
|
35
35
|
export { RelativeTime, type RelativeTimeProps } from "@/react/relative-time";
|
|
36
|
+
export { Flag, type FlagProps } from "@/react/flag";
|
|
37
|
+
export {
|
|
38
|
+
flagSrc,
|
|
39
|
+
flagView,
|
|
40
|
+
flagAttributes,
|
|
41
|
+
normalizeFlagCode,
|
|
42
|
+
configureFlags,
|
|
43
|
+
type FlagOptions,
|
|
44
|
+
type FlagShape,
|
|
45
|
+
type FlagFormat,
|
|
46
|
+
type FlagSource
|
|
47
|
+
} from "@/core/flags";
|