@djangocfg/ui-core 2.1.571 → 2.1.573

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.571",
3
+ "version": "2.1.573",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -130,7 +130,7 @@
130
130
  "check:contrast": "node scripts/check-preset-contrast.mjs"
131
131
  },
132
132
  "peerDependencies": {
133
- "@djangocfg/i18n": "^2.1.571",
133
+ "@djangocfg/i18n": "^2.1.573",
134
134
  "consola": "^3.4.2",
135
135
  "lucide-react": "^0.545.0",
136
136
  "moment": "^2.30.1",
@@ -206,9 +206,9 @@
206
206
  "vaul": "1.1.2"
207
207
  },
208
208
  "devDependencies": {
209
- "@djangocfg/eslint-config": "^2.1.571",
210
- "@djangocfg/i18n": "^2.1.571",
211
- "@djangocfg/typescript-config": "^2.1.571",
209
+ "@djangocfg/eslint-config": "^2.1.573",
210
+ "@djangocfg/i18n": "^2.1.573",
211
+ "@djangocfg/typescript-config": "^2.1.573",
212
212
  "@storybook/react-vite": "^10.5.0",
213
213
  "@types/node": "^24.13.3",
214
214
  "@types/react": "19.2.15",
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import moment from "moment";
5
4
  import { cva, type VariantProps } from "class-variance-authority";
6
5
  import { cn } from "../../../lib/utils";
7
6
 
@@ -31,13 +30,16 @@ const relativeTimeCardVariants = cva(
31
30
  interface RelativeTimeCardProps
32
31
  extends React.HTMLAttributes<HTMLSpanElement>,
33
32
  VariantProps<typeof relativeTimeCardVariants> {
34
- /** Date value — accepts Date, string in any format, number (timestamp), or Moment */
35
- date: Date | string | number | moment.Moment;
36
- /** Input format hint if date is a string (e.g. "DD.MM.YYYY HH:mm"). Auto-detected if omitted. */
33
+ /** Date value — a `Date`, an ISO 8601 string, or a timestamp in milliseconds */
34
+ date: DateInput;
35
+ /**
36
+ * @deprecated Ignored. Dates are parsed as ISO 8601; arbitrary input patterns
37
+ * went away with moment. Kept so existing call sites still type-check.
38
+ */
37
39
  inputFormat?: string;
38
40
  /** Display format: relative | auto | datetime | custom */
39
41
  format?: "relative" | "auto" | "datetime" | "custom";
40
- /** Custom output format string (moment format). Used when format="custom" */
42
+ /** Output pattern for `format="custom"`. Supports `YYYY MM DD HH mm ss`. */
41
43
  outputFormat?: string;
42
44
  /** Update interval in ms for relative format. @default 60000 */
43
45
  updateInterval?: number;
@@ -51,65 +53,117 @@ interface RelativeTimeCardProps
51
53
  utc?: boolean;
52
54
  }
53
55
 
54
- function parseDate(
55
- value: Date | string | number | moment.Moment,
56
- inputFormat?: string,
57
- useUtc: boolean = true
58
- ): moment.Moment {
59
- if (moment.isMoment(value)) {
60
- return useUtc ? value.utc() : value.local();
61
- }
62
-
63
- const m = useUtc ? moment.utc : moment;
64
-
65
- if (value instanceof Date) {
66
- return m(value);
67
- }
68
-
69
- if (typeof value === "number") {
70
- return m(value);
71
- }
72
-
56
+ /* Built on `Intl`, not moment.
57
+ *
58
+ * moment bundles 137 locale files and this module was the last thing importing
59
+ * it here. Because the package entry is `export *` under a file-level
60
+ * `'use client'`, that cost reached every consumer of the barrel, not just the
61
+ * ones rendering a date: measured on the MLS Caribbean property pages
62
+ * 2026-10-17, two 805 KB chunks on every listing, for a component those pages
63
+ * never render.
64
+ *
65
+ * `Intl.RelativeTimeFormat` and `Intl.DateTimeFormat` are in every browser this
66
+ * package targets, and they localise from the `locale` prop rather than from a
67
+ * global `moment.locale()` call — which was shared mutable state, so two cards
68
+ * with different locales fought over it.
69
+ *
70
+ * Two deliberate narrowings, both of moment-only behaviour:
71
+ * - `inputFormat` no longer parses arbitrary patterns. `Date` accepts ISO
72
+ * 8601; anything else was already locale-dependent guesswork.
73
+ * - `outputFormat` accepts the moment-style tokens this component actually
74
+ * shipped with (`YYYY MM DD HH mm ss`), not the full moment grammar.
75
+ */
76
+
77
+ type DateInput = Date | string | number;
78
+
79
+ /** A timestamp with no zone designator is UTC here, as the APIs emit it. */
80
+ function toDate(value: DateInput, useUtc: boolean): Date {
81
+ if (value instanceof Date) return value;
82
+ if (typeof value === "number") return new Date(value);
73
83
  if (typeof value === "string") {
74
- if (inputFormat) {
75
- return m(value, inputFormat, true);
76
- }
77
- return m(value);
84
+ const iso =
85
+ useUtc && !/(?:Z|[+-]\d{2}:?\d{2})$/.test(value) ? `${value}Z` : value;
86
+ return new Date(iso);
78
87
  }
79
-
80
- return m();
88
+ return new Date();
81
89
  }
82
90
 
83
- function getRelativeTime(date: moment.Moment, locale: string = "en"): string {
84
- moment.locale(locale);
85
- return date.fromNow();
91
+ const RELATIVE_UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
92
+ ["year", 365 * 24 * 60 * 60],
93
+ ["month", 30 * 24 * 60 * 60],
94
+ ["day", 24 * 60 * 60],
95
+ ["hour", 60 * 60],
96
+ ["minute", 60],
97
+ ["second", 1],
98
+ ];
99
+
100
+ function getRelativeTime(date: Date, locale: string = "en"): string {
101
+ const seconds = Math.round((date.getTime() - Date.now()) / 1000);
102
+ const absolute = Math.abs(seconds);
103
+ const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
104
+
105
+ for (const [unit, size] of RELATIVE_UNITS) {
106
+ if (absolute >= size || unit === "second") {
107
+ return formatter.format(Math.round(seconds / size), unit);
108
+ }
109
+ }
110
+ return formatter.format(0, "second");
86
111
  }
87
112
 
88
- function getAutoFormat(date: moment.Moment, locale: string = "en"): string {
89
- moment.locale(locale);
90
- const now = moment();
91
- const diffHours = now.diff(date, "hours");
92
- const diffDays = now.diff(date, "days");
113
+ function getAutoFormat(date: Date, locale: string = "en"): string {
114
+ const diffHours = (Date.now() - date.getTime()) / 3_600_000;
115
+ const diffDays = diffHours / 24;
93
116
 
94
117
  if (diffHours < 24) {
95
- return date.format("HH:mm");
118
+ return new Intl.DateTimeFormat(locale, {
119
+ hour: "2-digit",
120
+ minute: "2-digit",
121
+ hour12: false,
122
+ }).format(date);
96
123
  }
97
124
  if (diffDays < 7) {
98
- return date.format("ddd");
125
+ return new Intl.DateTimeFormat(locale, { weekday: "short" }).format(date);
99
126
  }
100
127
  if (diffDays < 365) {
101
- return date.format("MMM D");
128
+ return new Intl.DateTimeFormat(locale, {
129
+ month: "short",
130
+ day: "numeric",
131
+ }).format(date);
102
132
  }
103
- return date.format("MMM D, YYYY");
133
+ return new Intl.DateTimeFormat(locale, {
134
+ year: "numeric",
135
+ month: "short",
136
+ day: "numeric",
137
+ }).format(date);
104
138
  }
105
139
 
106
- function getDateTimeFormat(date: moment.Moment, locale: string = "en"): string {
107
- moment.locale(locale);
108
- return date.format("MMM D, YYYY HH:mm");
140
+ function getDateTimeFormat(date: Date, locale: string = "en"): string {
141
+ return new Intl.DateTimeFormat(locale, {
142
+ year: "numeric",
143
+ month: "short",
144
+ day: "numeric",
145
+ hour: "2-digit",
146
+ minute: "2-digit",
147
+ hour12: false,
148
+ }).format(date);
149
+ }
150
+
151
+ /** The moment-style tokens this component shipped with, and only those. */
152
+ function applyCustomFormat(date: Date, pattern: string): string {
153
+ const pad = (n: number) => String(n).padStart(2, "0");
154
+ const tokens: Record<string, string> = {
155
+ YYYY: String(date.getFullYear()),
156
+ MM: pad(date.getMonth() + 1),
157
+ DD: pad(date.getDate()),
158
+ HH: pad(date.getHours()),
159
+ mm: pad(date.getMinutes()),
160
+ ss: pad(date.getSeconds()),
161
+ };
162
+ return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => tokens[t] ?? t);
109
163
  }
110
164
 
111
165
  function formatDate(
112
- date: moment.Moment,
166
+ date: Date,
113
167
  format: "relative" | "auto" | "datetime" | "custom",
114
168
  locale: string,
115
169
  outputFormat?: string
@@ -122,8 +176,7 @@ function formatDate(
122
176
  case "datetime":
123
177
  return getDateTimeFormat(date, locale);
124
178
  case "custom":
125
- moment.locale(locale);
126
- return date.format(outputFormat || "YYYY-MM-DD HH:mm");
179
+ return applyCustomFormat(date, outputFormat || "YYYY-MM-DD HH:mm");
127
180
  }
128
181
  }
129
182
 
@@ -149,10 +202,7 @@ const RelativeTimeCard = React.forwardRef<
149
202
  },
150
203
  ref
151
204
  ) => {
152
- const dateObj = React.useMemo(
153
- () => parseDate(date, inputFormat, utc),
154
- [date, inputFormat, utc]
155
- );
205
+ const dateObj = React.useMemo(() => toDate(date, utc), [date, utc]);
156
206
 
157
207
  const [display, setDisplay] = React.useState(() =>
158
208
  formatDate(dateObj, format, locale, outputFormat)
@@ -167,8 +217,16 @@ const RelativeTimeCard = React.forwardRef<
167
217
  return () => clearInterval(id);
168
218
  }, [dateObj, format, locale, outputFormat, updateInterval]);
169
219
 
170
- const isoString = dateObj.toISOString();
171
- const title = dateObj.format("LLLL");
220
+ // An invalid date would throw from `toISOString`, where moment returned
221
+ // null and rendered "Invalid date". Both attributes are omitted instead.
222
+ const valid = !Number.isNaN(dateObj.getTime());
223
+ const isoString = valid ? dateObj.toISOString() : undefined;
224
+ const title = valid
225
+ ? new Intl.DateTimeFormat(locale, {
226
+ dateStyle: "full",
227
+ timeStyle: "short",
228
+ }).format(dateObj)
229
+ : undefined;
172
230
 
173
231
  return (
174
232
  <time
@@ -286,8 +286,8 @@ export { Primitive, dispatchDiscreteCustomEvent } from './specialized/primitive'
286
286
  export type { PrimitivePropsWithRef } from './specialized/primitive';
287
287
  export { ImageWithFallback } from './specialized/image-with-fallback';
288
288
  export type { ImageWithFallbackProps } from './specialized/image-with-fallback';
289
- export { Flag, LanguageFlag, LANGUAGE_TO_COUNTRY, getLanguageCountryCode, FLAG_COMPONENTS } from './specialized/flag';
290
- export type { FlagProps, LanguageFlagProps, FlagSvgComponent } from './specialized/flag';
289
+ export { Flag, LanguageFlag, LANGUAGE_TO_COUNTRY, getLanguageCountryCode, FLAG_LOADERS } from './specialized/flag';
290
+ export type { FlagProps, LanguageFlagProps, FlagLoader } from './specialized/flag';
291
291
 
292
292
  // ─────────────────────────────────────────────────────────────────────────────
293
293
  // Effects
@@ -4,13 +4,11 @@ import * as React from 'react';
4
4
 
5
5
  import { cn } from '../../../lib/utils';
6
6
 
7
- import { FLAG_COMPONENTS } from './flag-map';
7
+ import { FLAG_LOADERS } from './flag-map';
8
8
  import { getLanguageCountryCode } from './language-to-country';
9
9
 
10
10
  /**
11
- * Presentational attributes only. The underlying `country-flag-icons`
12
- * components type their handlers against an element that cannot exist, so no
13
- * event handler passed here could be given an honest signature.
11
+ * Presentational attributes only.
14
12
  */
15
13
  export interface FlagProps {
16
14
  /** ISO 3166-1 alpha-2 country code (e.g. `'US'`, `'JP'`). Case-insensitive. */
@@ -23,24 +21,49 @@ export interface FlagProps {
23
21
  title?: string;
24
22
  }
25
23
 
24
+ /** Loaded SVG strings, shared by every `Flag` on the page. */
25
+ const loaded = new Map<string, string>();
26
+
26
27
  /**
27
- * SVG country flag (3:2 aspect). Renders nothing for unknown country codes
28
- * — call sites should reserve space if they want a stable layout.
28
+ * SVG country flag (3:2 aspect), loaded on demand — one small chunk per code.
29
+ * Renders an empty box of the same size until it arrives, and nothing for an
30
+ * unknown code.
29
31
  */
30
- export function Flag({ countryCode, rounded, className, ...props }: FlagProps) {
32
+ export function Flag({ countryCode, rounded, className, width, height, title }: FlagProps) {
31
33
  const code = countryCode?.toUpperCase();
32
- const Component = code ? FLAG_COMPONENTS[code] : undefined;
33
- if (!Component) return null;
34
+ const load = code ? FLAG_LOADERS[code] : undefined;
35
+ const [svg, setSvg] = React.useState(() => (code ? loaded.get(code) : undefined));
36
+
37
+ React.useEffect(() => {
38
+ if (!code || !load) return;
39
+ const cached = loaded.get(code);
40
+ if (cached) {
41
+ setSvg(cached);
42
+ return;
43
+ }
44
+ let live = true;
45
+ load().then((m) => {
46
+ loaded.set(code, m.default);
47
+ if (live) setSvg(m.default);
48
+ });
49
+ return () => {
50
+ live = false;
51
+ };
52
+ }, [code, load]);
53
+
54
+ if (!load) return null;
34
55
  return (
35
- <Component
56
+ <span
36
57
  className={cn(
37
- 'inline-block shrink-0 select-none',
58
+ 'inline-block shrink-0 select-none [&>svg]:block [&>svg]:h-full [&>svg]:w-full',
38
59
  rounded && 'overflow-hidden rounded-[2px]',
39
60
  className,
40
61
  )}
62
+ style={{ width, height }}
63
+ title={title}
41
64
  aria-hidden="true"
42
- focusable={false}
43
- {...props}
65
+ // Static SVG from `country-flag-icons`, never user input.
66
+ dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
44
67
  />
45
68
  );
46
69
  }
@@ -0,0 +1,5 @@
1
+ // `country-flag-icons` ships no types for its per-flag string modules.
2
+ declare module 'country-flag-icons/string/3x2/*' {
3
+ const svg: string;
4
+ export default svg;
5
+ }
@@ -1,28 +1,275 @@
1
+ /// <reference path="./country-flag-strings.d.ts" />
1
2
  /**
2
- * Static map: ISO 3166-1 alpha-2 country code → SVG React component.
3
+ * ISO 3166-1 alpha-2 country code → a loader for that one flag's SVG.
3
4
  *
4
- * Re-exports every flag from `country-flag-icons` (~270 entries) so the
5
- * `Flag` component covers every consumer — locale switchers, phone-input
6
- * country picker, country select, etc. Tree-shaking keeps unused flags out
7
- * of the final bundle when consumers reach for `<Flag countryCode>`
8
- * directly with a constant code.
5
+ * One dynamic import per flag, so a page pays for the flags it shows. The
6
+ * previous static map spread `import * as Flags` into an object, which no
7
+ * bundler can tree-shake: every app that reached `Flag` (a locale switcher
8
+ * with four languages) shipped all ~270 flags, ~250 KB raw.
9
+ *
10
+ * Generated from `country-flag-icons`' own `exports` (`./string/3x2/*`);
11
+ * regenerate when that package adds a code. Subdivision flags (`GB_ENG`,
12
+ * `ES_CT`, …) have no per-file module there, so they are not covered.
9
13
  */
10
14
 
11
- import * as Flags from 'country-flag-icons/react/3x2';
12
-
13
- /**
14
- * `country-flag-icons` declares its components against `HTMLSVGElement` — its
15
- * own `HTMLElement & SVGElement` intersection, which no DOM node satisfies. Its
16
- * event-handler props are consequently uncallable with a real SVG event, so
17
- * `FlagProps` exposes only the presentational attributes `Flag` forwards; a
18
- * handler would have to be typed against an element that cannot exist.
19
- */
20
- export type FlagSvgComponent = (typeof Flags)[keyof typeof Flags];
15
+ export type FlagLoader = () => Promise<{ default: string }>;
21
16
 
22
- /**
23
- * Keyed by country code rather than indexed off the namespace object, because
24
- * the lookup code is a runtime string and a module namespace admits only its
25
- * literal export names.
26
- */
27
- export const FLAG_COMPONENTS: Record<string, FlagSvgComponent> =
28
- Object.fromEntries(Object.entries(Flags));
17
+ export const FLAG_LOADERS: Record<string, FlagLoader> = {
18
+ AC: () => import('country-flag-icons/string/3x2/AC'),
19
+ AD: () => import('country-flag-icons/string/3x2/AD'),
20
+ AE: () => import('country-flag-icons/string/3x2/AE'),
21
+ AF: () => import('country-flag-icons/string/3x2/AF'),
22
+ AG: () => import('country-flag-icons/string/3x2/AG'),
23
+ AI: () => import('country-flag-icons/string/3x2/AI'),
24
+ AL: () => import('country-flag-icons/string/3x2/AL'),
25
+ AM: () => import('country-flag-icons/string/3x2/AM'),
26
+ AO: () => import('country-flag-icons/string/3x2/AO'),
27
+ AQ: () => import('country-flag-icons/string/3x2/AQ'),
28
+ AR: () => import('country-flag-icons/string/3x2/AR'),
29
+ AS: () => import('country-flag-icons/string/3x2/AS'),
30
+ AT: () => import('country-flag-icons/string/3x2/AT'),
31
+ AU: () => import('country-flag-icons/string/3x2/AU'),
32
+ AW: () => import('country-flag-icons/string/3x2/AW'),
33
+ AX: () => import('country-flag-icons/string/3x2/AX'),
34
+ AZ: () => import('country-flag-icons/string/3x2/AZ'),
35
+ BA: () => import('country-flag-icons/string/3x2/BA'),
36
+ BB: () => import('country-flag-icons/string/3x2/BB'),
37
+ BD: () => import('country-flag-icons/string/3x2/BD'),
38
+ BE: () => import('country-flag-icons/string/3x2/BE'),
39
+ BF: () => import('country-flag-icons/string/3x2/BF'),
40
+ BG: () => import('country-flag-icons/string/3x2/BG'),
41
+ BH: () => import('country-flag-icons/string/3x2/BH'),
42
+ BI: () => import('country-flag-icons/string/3x2/BI'),
43
+ BJ: () => import('country-flag-icons/string/3x2/BJ'),
44
+ BL: () => import('country-flag-icons/string/3x2/BL'),
45
+ BM: () => import('country-flag-icons/string/3x2/BM'),
46
+ BN: () => import('country-flag-icons/string/3x2/BN'),
47
+ BO: () => import('country-flag-icons/string/3x2/BO'),
48
+ BQ: () => import('country-flag-icons/string/3x2/BQ'),
49
+ BR: () => import('country-flag-icons/string/3x2/BR'),
50
+ BS: () => import('country-flag-icons/string/3x2/BS'),
51
+ BT: () => import('country-flag-icons/string/3x2/BT'),
52
+ BV: () => import('country-flag-icons/string/3x2/BV'),
53
+ BW: () => import('country-flag-icons/string/3x2/BW'),
54
+ BY: () => import('country-flag-icons/string/3x2/BY'),
55
+ BZ: () => import('country-flag-icons/string/3x2/BZ'),
56
+ CA: () => import('country-flag-icons/string/3x2/CA'),
57
+ CC: () => import('country-flag-icons/string/3x2/CC'),
58
+ CD: () => import('country-flag-icons/string/3x2/CD'),
59
+ CF: () => import('country-flag-icons/string/3x2/CF'),
60
+ CG: () => import('country-flag-icons/string/3x2/CG'),
61
+ CH: () => import('country-flag-icons/string/3x2/CH'),
62
+ CI: () => import('country-flag-icons/string/3x2/CI'),
63
+ CK: () => import('country-flag-icons/string/3x2/CK'),
64
+ CL: () => import('country-flag-icons/string/3x2/CL'),
65
+ CM: () => import('country-flag-icons/string/3x2/CM'),
66
+ CN: () => import('country-flag-icons/string/3x2/CN'),
67
+ CO: () => import('country-flag-icons/string/3x2/CO'),
68
+ CR: () => import('country-flag-icons/string/3x2/CR'),
69
+ CU: () => import('country-flag-icons/string/3x2/CU'),
70
+ CV: () => import('country-flag-icons/string/3x2/CV'),
71
+ CW: () => import('country-flag-icons/string/3x2/CW'),
72
+ CX: () => import('country-flag-icons/string/3x2/CX'),
73
+ CY: () => import('country-flag-icons/string/3x2/CY'),
74
+ CZ: () => import('country-flag-icons/string/3x2/CZ'),
75
+ DE: () => import('country-flag-icons/string/3x2/DE'),
76
+ DJ: () => import('country-flag-icons/string/3x2/DJ'),
77
+ DK: () => import('country-flag-icons/string/3x2/DK'),
78
+ DM: () => import('country-flag-icons/string/3x2/DM'),
79
+ DO: () => import('country-flag-icons/string/3x2/DO'),
80
+ DZ: () => import('country-flag-icons/string/3x2/DZ'),
81
+ EC: () => import('country-flag-icons/string/3x2/EC'),
82
+ EE: () => import('country-flag-icons/string/3x2/EE'),
83
+ EG: () => import('country-flag-icons/string/3x2/EG'),
84
+ EH: () => import('country-flag-icons/string/3x2/EH'),
85
+ ER: () => import('country-flag-icons/string/3x2/ER'),
86
+ ES: () => import('country-flag-icons/string/3x2/ES'),
87
+ ET: () => import('country-flag-icons/string/3x2/ET'),
88
+ EU: () => import('country-flag-icons/string/3x2/EU'),
89
+ FI: () => import('country-flag-icons/string/3x2/FI'),
90
+ FJ: () => import('country-flag-icons/string/3x2/FJ'),
91
+ FK: () => import('country-flag-icons/string/3x2/FK'),
92
+ FM: () => import('country-flag-icons/string/3x2/FM'),
93
+ FO: () => import('country-flag-icons/string/3x2/FO'),
94
+ FR: () => import('country-flag-icons/string/3x2/FR'),
95
+ GA: () => import('country-flag-icons/string/3x2/GA'),
96
+ GB: () => import('country-flag-icons/string/3x2/GB'),
97
+ GD: () => import('country-flag-icons/string/3x2/GD'),
98
+ GE: () => import('country-flag-icons/string/3x2/GE'),
99
+ GF: () => import('country-flag-icons/string/3x2/GF'),
100
+ GG: () => import('country-flag-icons/string/3x2/GG'),
101
+ GH: () => import('country-flag-icons/string/3x2/GH'),
102
+ GI: () => import('country-flag-icons/string/3x2/GI'),
103
+ GL: () => import('country-flag-icons/string/3x2/GL'),
104
+ GM: () => import('country-flag-icons/string/3x2/GM'),
105
+ GN: () => import('country-flag-icons/string/3x2/GN'),
106
+ GP: () => import('country-flag-icons/string/3x2/GP'),
107
+ GQ: () => import('country-flag-icons/string/3x2/GQ'),
108
+ GR: () => import('country-flag-icons/string/3x2/GR'),
109
+ GS: () => import('country-flag-icons/string/3x2/GS'),
110
+ GT: () => import('country-flag-icons/string/3x2/GT'),
111
+ GU: () => import('country-flag-icons/string/3x2/GU'),
112
+ GW: () => import('country-flag-icons/string/3x2/GW'),
113
+ GY: () => import('country-flag-icons/string/3x2/GY'),
114
+ HK: () => import('country-flag-icons/string/3x2/HK'),
115
+ HM: () => import('country-flag-icons/string/3x2/HM'),
116
+ HN: () => import('country-flag-icons/string/3x2/HN'),
117
+ HR: () => import('country-flag-icons/string/3x2/HR'),
118
+ HT: () => import('country-flag-icons/string/3x2/HT'),
119
+ HU: () => import('country-flag-icons/string/3x2/HU'),
120
+ IC: () => import('country-flag-icons/string/3x2/IC'),
121
+ ID: () => import('country-flag-icons/string/3x2/ID'),
122
+ IE: () => import('country-flag-icons/string/3x2/IE'),
123
+ IL: () => import('country-flag-icons/string/3x2/IL'),
124
+ IM: () => import('country-flag-icons/string/3x2/IM'),
125
+ IN: () => import('country-flag-icons/string/3x2/IN'),
126
+ IO: () => import('country-flag-icons/string/3x2/IO'),
127
+ IQ: () => import('country-flag-icons/string/3x2/IQ'),
128
+ IR: () => import('country-flag-icons/string/3x2/IR'),
129
+ IS: () => import('country-flag-icons/string/3x2/IS'),
130
+ IT: () => import('country-flag-icons/string/3x2/IT'),
131
+ JE: () => import('country-flag-icons/string/3x2/JE'),
132
+ JM: () => import('country-flag-icons/string/3x2/JM'),
133
+ JO: () => import('country-flag-icons/string/3x2/JO'),
134
+ JP: () => import('country-flag-icons/string/3x2/JP'),
135
+ KE: () => import('country-flag-icons/string/3x2/KE'),
136
+ KG: () => import('country-flag-icons/string/3x2/KG'),
137
+ KH: () => import('country-flag-icons/string/3x2/KH'),
138
+ KI: () => import('country-flag-icons/string/3x2/KI'),
139
+ KM: () => import('country-flag-icons/string/3x2/KM'),
140
+ KN: () => import('country-flag-icons/string/3x2/KN'),
141
+ KP: () => import('country-flag-icons/string/3x2/KP'),
142
+ KR: () => import('country-flag-icons/string/3x2/KR'),
143
+ KW: () => import('country-flag-icons/string/3x2/KW'),
144
+ KY: () => import('country-flag-icons/string/3x2/KY'),
145
+ KZ: () => import('country-flag-icons/string/3x2/KZ'),
146
+ LA: () => import('country-flag-icons/string/3x2/LA'),
147
+ LB: () => import('country-flag-icons/string/3x2/LB'),
148
+ LC: () => import('country-flag-icons/string/3x2/LC'),
149
+ LI: () => import('country-flag-icons/string/3x2/LI'),
150
+ LK: () => import('country-flag-icons/string/3x2/LK'),
151
+ LR: () => import('country-flag-icons/string/3x2/LR'),
152
+ LS: () => import('country-flag-icons/string/3x2/LS'),
153
+ LT: () => import('country-flag-icons/string/3x2/LT'),
154
+ LU: () => import('country-flag-icons/string/3x2/LU'),
155
+ LV: () => import('country-flag-icons/string/3x2/LV'),
156
+ LY: () => import('country-flag-icons/string/3x2/LY'),
157
+ MA: () => import('country-flag-icons/string/3x2/MA'),
158
+ MC: () => import('country-flag-icons/string/3x2/MC'),
159
+ MD: () => import('country-flag-icons/string/3x2/MD'),
160
+ ME: () => import('country-flag-icons/string/3x2/ME'),
161
+ MF: () => import('country-flag-icons/string/3x2/MF'),
162
+ MG: () => import('country-flag-icons/string/3x2/MG'),
163
+ MH: () => import('country-flag-icons/string/3x2/MH'),
164
+ MK: () => import('country-flag-icons/string/3x2/MK'),
165
+ ML: () => import('country-flag-icons/string/3x2/ML'),
166
+ MM: () => import('country-flag-icons/string/3x2/MM'),
167
+ MN: () => import('country-flag-icons/string/3x2/MN'),
168
+ MO: () => import('country-flag-icons/string/3x2/MO'),
169
+ MP: () => import('country-flag-icons/string/3x2/MP'),
170
+ MQ: () => import('country-flag-icons/string/3x2/MQ'),
171
+ MR: () => import('country-flag-icons/string/3x2/MR'),
172
+ MS: () => import('country-flag-icons/string/3x2/MS'),
173
+ MT: () => import('country-flag-icons/string/3x2/MT'),
174
+ MU: () => import('country-flag-icons/string/3x2/MU'),
175
+ MV: () => import('country-flag-icons/string/3x2/MV'),
176
+ MW: () => import('country-flag-icons/string/3x2/MW'),
177
+ MX: () => import('country-flag-icons/string/3x2/MX'),
178
+ MY: () => import('country-flag-icons/string/3x2/MY'),
179
+ MZ: () => import('country-flag-icons/string/3x2/MZ'),
180
+ NA: () => import('country-flag-icons/string/3x2/NA'),
181
+ NC: () => import('country-flag-icons/string/3x2/NC'),
182
+ NE: () => import('country-flag-icons/string/3x2/NE'),
183
+ NF: () => import('country-flag-icons/string/3x2/NF'),
184
+ NG: () => import('country-flag-icons/string/3x2/NG'),
185
+ NI: () => import('country-flag-icons/string/3x2/NI'),
186
+ NL: () => import('country-flag-icons/string/3x2/NL'),
187
+ NO: () => import('country-flag-icons/string/3x2/NO'),
188
+ NP: () => import('country-flag-icons/string/3x2/NP'),
189
+ NR: () => import('country-flag-icons/string/3x2/NR'),
190
+ NU: () => import('country-flag-icons/string/3x2/NU'),
191
+ NZ: () => import('country-flag-icons/string/3x2/NZ'),
192
+ OM: () => import('country-flag-icons/string/3x2/OM'),
193
+ PA: () => import('country-flag-icons/string/3x2/PA'),
194
+ PE: () => import('country-flag-icons/string/3x2/PE'),
195
+ PF: () => import('country-flag-icons/string/3x2/PF'),
196
+ PG: () => import('country-flag-icons/string/3x2/PG'),
197
+ PH: () => import('country-flag-icons/string/3x2/PH'),
198
+ PK: () => import('country-flag-icons/string/3x2/PK'),
199
+ PL: () => import('country-flag-icons/string/3x2/PL'),
200
+ PM: () => import('country-flag-icons/string/3x2/PM'),
201
+ PN: () => import('country-flag-icons/string/3x2/PN'),
202
+ PR: () => import('country-flag-icons/string/3x2/PR'),
203
+ PS: () => import('country-flag-icons/string/3x2/PS'),
204
+ PT: () => import('country-flag-icons/string/3x2/PT'),
205
+ PW: () => import('country-flag-icons/string/3x2/PW'),
206
+ PY: () => import('country-flag-icons/string/3x2/PY'),
207
+ QA: () => import('country-flag-icons/string/3x2/QA'),
208
+ RE: () => import('country-flag-icons/string/3x2/RE'),
209
+ RO: () => import('country-flag-icons/string/3x2/RO'),
210
+ RS: () => import('country-flag-icons/string/3x2/RS'),
211
+ RU: () => import('country-flag-icons/string/3x2/RU'),
212
+ RW: () => import('country-flag-icons/string/3x2/RW'),
213
+ SA: () => import('country-flag-icons/string/3x2/SA'),
214
+ SB: () => import('country-flag-icons/string/3x2/SB'),
215
+ SC: () => import('country-flag-icons/string/3x2/SC'),
216
+ SD: () => import('country-flag-icons/string/3x2/SD'),
217
+ SE: () => import('country-flag-icons/string/3x2/SE'),
218
+ SG: () => import('country-flag-icons/string/3x2/SG'),
219
+ SH: () => import('country-flag-icons/string/3x2/SH'),
220
+ SI: () => import('country-flag-icons/string/3x2/SI'),
221
+ SJ: () => import('country-flag-icons/string/3x2/SJ'),
222
+ SK: () => import('country-flag-icons/string/3x2/SK'),
223
+ SL: () => import('country-flag-icons/string/3x2/SL'),
224
+ SM: () => import('country-flag-icons/string/3x2/SM'),
225
+ SN: () => import('country-flag-icons/string/3x2/SN'),
226
+ SO: () => import('country-flag-icons/string/3x2/SO'),
227
+ SR: () => import('country-flag-icons/string/3x2/SR'),
228
+ SS: () => import('country-flag-icons/string/3x2/SS'),
229
+ ST: () => import('country-flag-icons/string/3x2/ST'),
230
+ SV: () => import('country-flag-icons/string/3x2/SV'),
231
+ SX: () => import('country-flag-icons/string/3x2/SX'),
232
+ SY: () => import('country-flag-icons/string/3x2/SY'),
233
+ SZ: () => import('country-flag-icons/string/3x2/SZ'),
234
+ TA: () => import('country-flag-icons/string/3x2/TA'),
235
+ TC: () => import('country-flag-icons/string/3x2/TC'),
236
+ TD: () => import('country-flag-icons/string/3x2/TD'),
237
+ TF: () => import('country-flag-icons/string/3x2/TF'),
238
+ TG: () => import('country-flag-icons/string/3x2/TG'),
239
+ TH: () => import('country-flag-icons/string/3x2/TH'),
240
+ TJ: () => import('country-flag-icons/string/3x2/TJ'),
241
+ TK: () => import('country-flag-icons/string/3x2/TK'),
242
+ TL: () => import('country-flag-icons/string/3x2/TL'),
243
+ TM: () => import('country-flag-icons/string/3x2/TM'),
244
+ TN: () => import('country-flag-icons/string/3x2/TN'),
245
+ TO: () => import('country-flag-icons/string/3x2/TO'),
246
+ TR: () => import('country-flag-icons/string/3x2/TR'),
247
+ TT: () => import('country-flag-icons/string/3x2/TT'),
248
+ TV: () => import('country-flag-icons/string/3x2/TV'),
249
+ TW: () => import('country-flag-icons/string/3x2/TW'),
250
+ TZ: () => import('country-flag-icons/string/3x2/TZ'),
251
+ UA: () => import('country-flag-icons/string/3x2/UA'),
252
+ UG: () => import('country-flag-icons/string/3x2/UG'),
253
+ UM: () => import('country-flag-icons/string/3x2/UM'),
254
+ US: () => import('country-flag-icons/string/3x2/US'),
255
+ UY: () => import('country-flag-icons/string/3x2/UY'),
256
+ UZ: () => import('country-flag-icons/string/3x2/UZ'),
257
+ VA: () => import('country-flag-icons/string/3x2/VA'),
258
+ VC: () => import('country-flag-icons/string/3x2/VC'),
259
+ VE: () => import('country-flag-icons/string/3x2/VE'),
260
+ VG: () => import('country-flag-icons/string/3x2/VG'),
261
+ VI: () => import('country-flag-icons/string/3x2/VI'),
262
+ VN: () => import('country-flag-icons/string/3x2/VN'),
263
+ VU: () => import('country-flag-icons/string/3x2/VU'),
264
+ WF: () => import('country-flag-icons/string/3x2/WF'),
265
+ WS: () => import('country-flag-icons/string/3x2/WS'),
266
+ XA: () => import('country-flag-icons/string/3x2/XA'),
267
+ XC: () => import('country-flag-icons/string/3x2/XC'),
268
+ XK: () => import('country-flag-icons/string/3x2/XK'),
269
+ XO: () => import('country-flag-icons/string/3x2/XO'),
270
+ YE: () => import('country-flag-icons/string/3x2/YE'),
271
+ YT: () => import('country-flag-icons/string/3x2/YT'),
272
+ ZA: () => import('country-flag-icons/string/3x2/ZA'),
273
+ ZM: () => import('country-flag-icons/string/3x2/ZM'),
274
+ ZW: () => import('country-flag-icons/string/3x2/ZW'),
275
+ };
@@ -1,5 +1,5 @@
1
1
  export { Flag, LanguageFlag } from './Flag';
2
2
  export type { FlagProps, LanguageFlagProps } from './Flag';
3
3
  export { LANGUAGE_TO_COUNTRY, getLanguageCountryCode } from './language-to-country';
4
- export { FLAG_COMPONENTS } from './flag-map';
5
- export type { FlagSvgComponent } from './flag-map';
4
+ export { FLAG_LOADERS } from './flag-map';
5
+ export type { FlagLoader } from './flag-map';
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import moment from 'moment';
4
3
  import { useCallback, useEffect, useRef, useState } from 'react';
5
4
 
6
5
  export interface CountdownState {
@@ -64,10 +63,22 @@ export const useCountdown = (targetDate: string | null): CountdownState => {
64
63
  useEffect(() => {
65
64
  if (!targetDate) return;
66
65
 
67
- const target = moment.utc(targetDate).local();
66
+ // Plain `Date` arithmetic, not moment: the hook only ever needed the gap in
67
+ // seconds, and the conversion moment did (`.utc().local()`) is what a Date
68
+ // already is internally — an absolute instant, rendered in local time only
69
+ // when formatted. Importing moment for this shipped its 137 locale files to
70
+ // every consumer of this package's barrel export.
71
+ //
72
+ // A timestamp with no zone designator means UTC here, so append the `Z`
73
+ // that `Date` would otherwise read as local time.
74
+ const iso = /(?:Z|[+-]\d{2}:?\d{2})$/.test(targetDate)
75
+ ? targetDate
76
+ : `${targetDate}Z`;
77
+ const targetMs = new Date(iso).getTime();
78
+ if (Number.isNaN(targetMs)) return;
68
79
 
69
80
  const tick = () => {
70
- const diff = target.diff(moment(), 'seconds');
81
+ const diff = Math.round((targetMs - Date.now()) / 1000);
71
82
  setCountdown(secondsToState(diff));
72
83
  };
73
84