@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.
@@ -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 config = revealConfig();
349
+ const config2 = revealConfig();
350
350
  return {
351
351
  name: "reveal",
352
- label: (current) => current.revealed ? config.hideLabel ?? "Hide password" : config.showLabel ?? "Show password",
353
- icon: (current) => current.revealed ? config.hideIcon ?? EYE_OFF : config.showIcon ?? EYE,
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
- export { INPUT_ICON_PATHS, SERVER_NETWORK_STATE, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, generatePassword, iconMarkup, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView };
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-53DQY6LP.js';
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
- export { type ButtonCooldown, type ButtonElement, type ButtonInstance, type ButtonOptions, type ButtonState, type EstimateOptions, 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, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, generatePassword, iconMarkup, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView };
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-53DQY6LP.js';
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';
@@ -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
  /**
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { Button } from '../chunk-U2KYYMBJ.js';
3
- export { Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-U2KYYMBJ.js';
4
- export { estimatePasswordStrength, generatePassword } from '../chunk-53DQY6LP.js';
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';
@@ -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
- export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, 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 };
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 };
@@ -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-U2KYYMBJ.js';
3
- export { estimatePasswordStrength, generatePassword } from '../chunk-53DQY6LP.js';
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-U2KYYMBJ.js';
3
- export { Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch } from '../chunk-U2KYYMBJ.js';
4
- export { estimatePasswordStrength, generatePassword } from '../chunk-53DQY6LP.js';
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.15.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 = (item: LogoItem, index: number): ReactNode => (
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
+ }
@@ -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";