@enigmax/primitives 0.17.0 → 0.19.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.
Files changed (97) hide show
  1. package/dist/button-CaXaqG_K.d.ts +63 -0
  2. package/dist/chunk-2QFTRNAZ.js +88 -0
  3. package/dist/chunk-6BGBYUSZ.js +114 -0
  4. package/dist/chunk-AU3H5WIY.js +107 -0
  5. package/dist/{chunk-UZFEEFMF.js → chunk-D5A2ZMAG.js} +34 -1
  6. package/dist/chunk-DTWZDONY.js +99 -0
  7. package/dist/chunk-F25CGNQC.js +19 -0
  8. package/dist/chunk-FWVWX67R.js +462 -0
  9. package/dist/chunk-HC2ME5PU.js +168 -0
  10. package/dist/chunk-HS3X3XCW.js +43 -0
  11. package/dist/chunk-IXVMRVD4.js +96 -0
  12. package/dist/chunk-KJINGUQN.js +188 -0
  13. package/dist/chunk-MMQPZGSU.js +161 -0
  14. package/dist/chunk-OCMI7R6H.js +79 -0
  15. package/dist/chunk-QQFNAKMY.js +47 -0
  16. package/dist/chunk-QYMUIW5I.js +28 -0
  17. package/dist/chunk-R4ZAEE7V.js +249 -0
  18. package/dist/chunk-S653GLSF.js +17 -0
  19. package/dist/chunk-SNYUBXWQ.js +149 -0
  20. package/dist/chunk-U3V4EHOB.js +41 -0
  21. package/dist/chunk-UOSSNUSC.js +309 -0
  22. package/dist/chunk-XNNQRA35.js +31 -0
  23. package/dist/chunk-XQHCZAPJ.js +102 -0
  24. package/dist/chunk-ZCUFYBPB.js +154 -0
  25. package/dist/chunk-ZWR2EXHQ.js +55 -0
  26. package/dist/flags-BBJc9unY.d.ts +133 -0
  27. package/dist/index-dTdAbOWl.d.ts +144 -0
  28. package/dist/index.d.ts +18 -603
  29. package/dist/index.js +11 -2
  30. package/dist/input-BwXjFenq.d.ts +77 -0
  31. package/dist/marquee-CJ3Uwy3E.d.ts +81 -0
  32. package/dist/network-D2LsBG_k.d.ts +39 -0
  33. package/dist/next/index.d.ts +22 -4
  34. package/dist/next/index.js +25 -4
  35. package/dist/notifications-BpVV6sel.d.ts +70 -0
  36. package/dist/palette-D7iuQh_T.d.ts +86 -0
  37. package/dist/password-3DRQYAYQ.js +2 -0
  38. package/dist/password-C8lG4Zm9.d.ts +71 -0
  39. package/dist/password-FB2CUEKJ.js +1 -0
  40. package/dist/react/button.d.ts +75 -0
  41. package/dist/react/button.js +4 -0
  42. package/dist/react/flag.d.ts +37 -0
  43. package/dist/react/flag.js +3 -0
  44. package/dist/react/index.d.ts +29 -312
  45. package/dist/react/index.js +24 -3
  46. package/dist/react/input.d.ts +4 -0
  47. package/dist/react/input.js +3 -0
  48. package/dist/react/marquee.d.ts +44 -0
  49. package/dist/react/marquee.js +3 -0
  50. package/dist/react/network.d.ts +20 -0
  51. package/dist/react/network.js +3 -0
  52. package/dist/react/notifications.d.ts +17 -0
  53. package/dist/react/notifications.js +3 -0
  54. package/dist/react/palette.d.ts +231 -0
  55. package/dist/react/palette.js +5 -0
  56. package/dist/react/relative-time.d.ts +21 -0
  57. package/dist/react/relative-time.js +3 -0
  58. package/dist/react/search.d.ts +30 -0
  59. package/dist/react/search.js +3 -0
  60. package/dist/react/slot.d.ts +47 -0
  61. package/dist/react/slot.js +2 -0
  62. package/dist/react/toast.d.ts +40 -0
  63. package/dist/react/toast.js +4 -0
  64. package/dist/react-router/index.d.ts +22 -4
  65. package/dist/react-router/index.js +25 -4
  66. package/dist/relative-time-YpRTG7YH.d.ts +106 -0
  67. package/dist/search/index.d.ts +2 -2
  68. package/dist/search/index.js +1 -1
  69. package/dist/{search-CsO3L1Lw.d.ts → search-DXxY8SEH.d.ts} +15 -1
  70. package/dist/search-UQEXAPQB.js +50 -0
  71. package/package.json +54 -3
  72. package/recipes/input/styles.css +60 -20
  73. package/recipes/palette/styles.css +243 -0
  74. package/registry.json +186 -18
  75. package/src/core/flags.ts +88 -42
  76. package/src/core/input-icons.ts +34 -0
  77. package/src/core/input.ts +5 -21
  78. package/src/core/palette.ts +0 -0
  79. package/src/core/search.ts +60 -0
  80. package/src/index.ts +19 -3
  81. package/src/react/button.tsx +70 -4
  82. package/src/react/flag.tsx +18 -6
  83. package/src/react/index.ts +50 -5
  84. package/src/react/input/icon.tsx +17 -0
  85. package/src/react/input/index.tsx +307 -0
  86. package/src/react/input/password.tsx +174 -0
  87. package/src/react/input/search.tsx +82 -0
  88. package/src/react/input/types.ts +146 -0
  89. package/src/react/input/write-value.ts +18 -0
  90. package/src/react/palette/context.ts +60 -0
  91. package/src/react/palette/index.tsx +66 -0
  92. package/src/react/palette/root.tsx +661 -0
  93. package/src/react/slot.tsx +91 -0
  94. package/src/react/use-button.ts +3 -1
  95. package/dist/chunk-4GLYRF4B.js +0 -1259
  96. package/dist/chunk-JFNND6P4.js +0 -749
  97. package/src/react/input.tsx +0 -429
package/registry.json CHANGED
@@ -155,8 +155,8 @@
155
155
  },
156
156
  {
157
157
  "name": "input",
158
- "title": "Input, with what a password field needs",
159
- "description": "A field you pass props to, with no styles of its own. A password gets its reveal toggle automatically; a generator, a strength meter and a breach check are one prop each and off until you ask. React renders every part of it, so nothing fights your tree.",
158
+ "title": "Input, one field for every type",
159
+ "description": "A field you pass props to, typed by its `type`: password gets the reveal toggle, a generator, a strength meter and a breach check; search gets debouncing, ranking and a clear button; everything else is a plain field with native props. What each type needs is loaded when that type is used, so a form of text fields ships none of it.",
160
160
  "targets": [
161
161
  "vanilla",
162
162
  "astro",
@@ -165,7 +165,7 @@
165
165
  "entry": {
166
166
  "vanilla": "@enigmax/primitives",
167
167
  "astro": "@enigmax/primitives",
168
- "react": "@enigmax/primitives/react"
168
+ "react": "@enigmax/primitives/react/input"
169
169
  },
170
170
  "exports": {
171
171
  "vanilla": [
@@ -181,17 +181,28 @@
181
181
  },
182
182
  "files": [
183
183
  {
184
- "path": "src/core/input.ts",
185
- "dest": "input/input.ts",
184
+ "path": "src/core/input-icons.ts",
185
+ "dest": "input/input-icons.ts",
186
186
  "targets": [
187
187
  "vanilla",
188
188
  "astro",
189
189
  "react"
190
190
  ]
191
191
  },
192
+ {
193
+ "path": "src/core/input.ts",
194
+ "dest": "input/input.ts",
195
+ "targets": [
196
+ "vanilla",
197
+ "astro"
198
+ ],
199
+ "rewrite": {
200
+ "@/core/input-icons": "./input-icons"
201
+ }
202
+ },
192
203
  {
193
204
  "path": "src/core/password.ts",
194
- "dest": "input/password.ts",
205
+ "dest": "input/password-core.ts",
195
206
  "targets": [
196
207
  "vanilla",
197
208
  "astro",
@@ -199,13 +210,56 @@
199
210
  ]
200
211
  },
201
212
  {
202
- "path": "src/react/use-input.ts",
203
- "dest": "input/use-input.ts",
213
+ "path": "src/core/search.ts",
214
+ "dest": "input/search-core.ts",
215
+ "targets": [
216
+ "react"
217
+ ]
218
+ },
219
+ {
220
+ "path": "src/react/input/types.ts",
221
+ "dest": "input/types.ts",
222
+ "targets": [
223
+ "react"
224
+ ],
225
+ "rewrite": {
226
+ "@/core/search": "./search-core",
227
+ "@/core/password": "./password-core"
228
+ }
229
+ },
230
+ {
231
+ "path": "src/react/input/icon.tsx",
232
+ "dest": "input/icon.tsx",
233
+ "targets": [
234
+ "react"
235
+ ]
236
+ },
237
+ {
238
+ "path": "src/react/input/write-value.ts",
239
+ "dest": "input/write-value.ts",
240
+ "targets": [
241
+ "react"
242
+ ]
243
+ },
244
+ {
245
+ "path": "src/react/input/password.tsx",
246
+ "dest": "input/password.tsx",
204
247
  "targets": [
205
248
  "react"
206
249
  ],
207
250
  "rewrite": {
208
- "@/core/input": "./input"
251
+ "@/core/password": "./password-core",
252
+ "@/react/input/types": "./types"
253
+ }
254
+ },
255
+ {
256
+ "path": "src/react/input/search.tsx",
257
+ "dest": "input/search.tsx",
258
+ "targets": [
259
+ "react"
260
+ ],
261
+ "rewrite": {
262
+ "@/core/search": "./search-core"
209
263
  }
210
264
  },
211
265
  {
@@ -219,15 +273,20 @@
219
273
  "style": "css"
220
274
  },
221
275
  {
222
- "path": "src/react/input.tsx",
276
+ "path": "src/react/input/index.tsx",
223
277
  "dest": "input/index.tsx",
224
278
  "main": true,
225
279
  "targets": [
226
280
  "react"
227
281
  ],
228
282
  "rewrite": {
229
- "@/core/input": "./input",
230
- "@/core/password": "./password"
283
+ "@/react/input/icon": "./icon",
284
+ "@/react/input/write-value": "./write-value",
285
+ "@/core/input-icons": "./input-icons",
286
+ "@/react/input/types": "./types",
287
+ "@/react/input/password": "./password",
288
+ "@/react/input/search": "./search",
289
+ "@/core/password": "./password-core"
231
290
  }
232
291
  },
233
292
  {
@@ -262,7 +321,8 @@
262
321
  "[data-enigma-password-strength-segment]",
263
322
  "[data-enigma-password-strength-segment][data-filled]",
264
323
  "[data-enigma-password-strength-label]",
265
- "[data-enigma-password-strength-warning]"
324
+ "[data-enigma-password-strength-warning]",
325
+ "[data-enigma-input-action=clear]"
266
326
  ],
267
327
  "docs": "docs/notes/primitives.md#input",
268
328
  "recipes": [
@@ -273,7 +333,7 @@
273
333
  {
274
334
  "name": "search",
275
335
  "title": "Search as you type",
276
- "description": "Search as you type, fuzzy by default with Fuse.js. Debouncing, ranking and cancellation are the primitive's; the matching is replaceable, and dropping Fuse falls back to a built-in accent-insensitive substring matcher.",
336
+ "description": "The engine behind `<Input type=\"search\">` and the palette: debouncing, ranking and cancellation, fuzzy with Fuse.js or a built-in accent-insensitive substring matcher with no dependency at all.",
277
337
  "targets": [
278
338
  "vanilla",
279
339
  "astro",
@@ -282,7 +342,7 @@
282
342
  "entry": {
283
343
  "vanilla": "@enigmax/primitives/search",
284
344
  "astro": "@enigmax/primitives/search",
285
- "react": "@enigmax/primitives/react"
345
+ "react": "@enigmax/primitives/react/search"
286
346
  },
287
347
  "exports": {
288
348
  "vanilla": [
@@ -413,14 +473,122 @@
413
473
  "[data-loading]",
414
474
  "[data-cooldown]",
415
475
  "[aria-disabled=true]",
416
- "[aria-busy=true]"
476
+ "[aria-busy=true]",
477
+ "[data-enigma-button-key]"
417
478
  ],
418
479
  "docs": "docs/notes/primitives.md#button"
419
480
  },
481
+ {
482
+ "name": "palette",
483
+ "title": "Command palette",
484
+ "description": "Ctrl/Cmd+K, search as you type, what was searched before, groups, and every key a palette is expected to answer to. A dialog made of parts you can compose or take whole, with the focus trap, the scroll lock and the listbox wiring already right.",
485
+ "targets": [
486
+ "react"
487
+ ],
488
+ "entry": {
489
+ "react": "@enigmax/primitives/react/palette"
490
+ },
491
+ "exports": {
492
+ "react": [
493
+ "SearchPalette"
494
+ ]
495
+ },
496
+ "files": [
497
+ {
498
+ "path": "src/core/palette.ts",
499
+ "dest": "palette/palette.ts",
500
+ "targets": [
501
+ "react"
502
+ ]
503
+ },
504
+ {
505
+ "path": "src/core/search.ts",
506
+ "dest": "palette/search-core.ts",
507
+ "targets": [
508
+ "react"
509
+ ]
510
+ },
511
+ {
512
+ "path": "src/react/slot.tsx",
513
+ "dest": "palette/slot.tsx",
514
+ "targets": [
515
+ "react"
516
+ ]
517
+ },
518
+ {
519
+ "path": "recipes/palette/styles.css",
520
+ "dest": "palette/styles.css",
521
+ "targets": [
522
+ "react"
523
+ ],
524
+ "style": "css"
525
+ },
526
+ {
527
+ "path": "src/react/palette/context.ts",
528
+ "dest": "palette/context.ts",
529
+ "targets": [
530
+ "react"
531
+ ],
532
+ "rewrite": {
533
+ "@/core/palette": "./palette",
534
+ "@/core/search": "./search-core"
535
+ }
536
+ },
537
+ {
538
+ "path": "src/react/palette/root.tsx",
539
+ "dest": "palette/root.tsx",
540
+ "targets": [
541
+ "react"
542
+ ],
543
+ "rewrite": {
544
+ "@/core/palette": "./palette",
545
+ "@/core/search": "./search-core",
546
+ "@/react/slot": "./slot",
547
+ "@/react/palette/context": "./context"
548
+ }
549
+ },
550
+ {
551
+ "path": "src/react/palette/index.tsx",
552
+ "dest": "palette/index.tsx",
553
+ "main": true,
554
+ "targets": [
555
+ "react"
556
+ ],
557
+ "rewrite": {
558
+ "@/react/palette/root": "./root",
559
+ "@/react/palette/context": "./context"
560
+ }
561
+ }
562
+ ],
563
+ "styles": false,
564
+ "themeHooks": [
565
+ "[data-enigma-palette-trigger]",
566
+ "[data-enigma-palette-trigger-key]",
567
+ "[data-enigma-palette-overlay]",
568
+ "[data-enigma-palette-content]",
569
+ "[data-enigma-palette-title]",
570
+ "[data-enigma-palette-field]",
571
+ "[data-enigma-palette-list]",
572
+ "[data-enigma-palette-group]",
573
+ "[data-enigma-palette-group-label]",
574
+ "[data-enigma-palette-item]",
575
+ "[data-enigma-palette-item][data-active=true]",
576
+ "[data-enigma-palette-item][data-kind=recent]",
577
+ "[data-enigma-palette-item-label]",
578
+ "[data-enigma-palette-item-description]",
579
+ "[data-enigma-palette-empty]",
580
+ "[data-enigma-palette-footer]",
581
+ "[data-enigma-palette-hint]"
582
+ ],
583
+ "docs": "docs/notes/primitives.md#palette",
584
+ "recipes": [
585
+ "css"
586
+ ]
587
+ },
420
588
  {
421
589
  "name": "flags",
422
590
  "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.",
591
+ "description": "A flag as an image, served by enigma or from files in your project, rectangular, square or round. It names itself in the reader's language, and accepts the emoji flag it replaces as its code - Windows ships no glyph for those, so a picker built out of emoji renders as bare letters for most desktop readers.",
424
592
  "targets": [
425
593
  "vanilla",
426
594
  "astro",
@@ -429,7 +597,7 @@
429
597
  "entry": {
430
598
  "vanilla": "@enigmax/primitives",
431
599
  "astro": "@enigmax/primitives",
432
- "react": "@enigmax/primitives/react"
600
+ "react": "@enigmax/primitives/react/flag"
433
601
  },
434
602
  "exports": {
435
603
  "vanilla": [
package/src/core/flags.ts CHANGED
@@ -8,12 +8,14 @@
8
8
  * out of them looks broken to those readers, with nothing in the code to explain why.
9
9
  * There is no font stack that fixes it, so the fix is not a font: it is an image.
10
10
  *
11
- * Two upstream sets, both SVG, both installable or served from a CDN:
11
+ * Three sets, all SVG, all served by enigma itself:
12
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.
13
+ * - `rect` (4:3) and `square` (1:1) - the classic rectangular artwork.
14
+ * - `circle` - round, which is what a flag next to an avatar or inside a chip wants.
15
+ *
16
+ * They are served from enigma's own tree rather than from anyone else's, because a
17
+ * component whose default source is a third party is a component that breaks when that
18
+ * third party moves, renames a branch, or goes away.
17
19
  *
18
20
  * Nothing here touches the DOM, so it renders on a server.
19
21
  */
@@ -21,26 +23,24 @@
21
23
  export type FlagShape = "rect" | "square" | "circle";
22
24
 
23
25
  /**
24
- * `svg` is the 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.
26
+ * `svg` is what the sets are stored as, so it is what a remote source can serve. `png` and
27
+ * `webp` exist for a LOCAL set (`enigma add flags --flags local --flag-formats webp`), which
28
+ * rasterises what it downloads, and for a mirror of your own.
27
29
  */
28
30
  export type FlagFormat = "svg" | "png" | "webp";
29
31
 
30
32
  /**
31
33
  * Where the files come from.
32
34
  *
33
- * - `"cdn"` - jsDelivr, straight from the two upstream projects. Nothing to install.
35
+ * - `"cdn"` - enigma's own artwork, over a CDN. Nothing to install.
34
36
  * - `"local"` - files under `basePath` in your own public directory.
35
- * - any URL - a mirror of your own with the same layout as the local one
37
+ * - any URL - a mirror of your own with the same layout
36
38
  * (`<base>/<shape>/<code>.<format>`), which is exactly the tree the downloader writes.
37
39
  */
38
40
  export type FlagSource = "cdn" | "local" | (string & {});
39
41
 
40
- /** 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";
42
+ /** enigma's own artwork, over a CDN. Every source shares this layout. */
43
+ export const FLAG_CDN = "https://cdn.jsdelivr.net/gh/FJRG2007/enigma@main/assets/flags";
44
44
 
45
45
  export interface FlagOptions {
46
46
  shape?: FlagShape;
@@ -50,10 +50,16 @@ export interface FlagOptions {
50
50
  basePath?: string;
51
51
  /** Rendered HEIGHT in px. The width follows the shape's ratio. Default 16. */
52
52
  size?: number;
53
+ /**
54
+ * Language the automatic name is written in. Left out, it follows the document's own
55
+ * `lang`, which is what a translated page wants.
56
+ */
57
+ locale?: string;
53
58
  }
54
59
 
55
- export interface FlagConfig extends Required<Omit<FlagOptions, "size">> {
60
+ export interface FlagConfig extends Required<Omit<FlagOptions, "size" | "locale">> {
56
61
  size: number;
62
+ locale?: string;
57
63
  }
58
64
 
59
65
  const DEFAULTS: FlagConfig = {
@@ -96,12 +102,14 @@ const EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
96
102
  * A BCP 47 tag whose region is spelled the way the standard spells it: UPPERCASE. That
97
103
  * casing is the whole discriminator, and it has to be read before the value is lowercased -
98
104
  * `es-ES` is Spanish-in-Spain and means the flag `es`, while `es-ct` is a subdivision code
99
- * both sets publish (Catalonia) and means the file `es-ct`. Lowercasing first makes those
105
+ * the sets publish (Catalonia) and means the file `es-ct`. Lowercasing first makes those
100
106
  * two the same string, and then one of them is always wrong.
101
107
  */
102
108
  const LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
103
- /** What the two sets actually name their files: `es`, `gb-eng`, `au-nsw`, `easter_island`. */
109
+ /** What the sets actually name their files: `es`, `gb-eng`, `au-nsw`, `easter_island`. */
104
110
  const FILE_CODE = /^[a-z]{2,}(?:[-_][a-z0-9]+)*$/;
111
+ /** A plain country: exactly the shape `Intl.DisplayNames` can put a name to. */
112
+ const COUNTRY_CODE = /^[a-z]{2}$/;
105
113
 
106
114
  /**
107
115
  * Anything a codebase calls a country into the file name the sets use, or null.
@@ -128,21 +136,48 @@ export function normalizeFlagCode(value: string | null | undefined): string | nu
128
136
  return FILE_CODE.test(code) ? code : null;
129
137
  }
130
138
 
139
+ /**
140
+ * The country's name, in the reader's language, or null.
141
+ *
142
+ * `Intl.DisplayNames` is the whole implementation: the names are in the runtime already,
143
+ * translated, and maintained by whoever maintains the platform - so this ships no table of
144
+ * 250 country names to go stale, and a Spanish page says "España" without being told.
145
+ *
146
+ * Null for anything it cannot name, and that is deliberate for SUBDIVISIONS: `gb-eng` is
147
+ * England, and falling back to the region (`gb`, "United Kingdom") would put a confidently
148
+ * wrong name on the flag. A name nobody can source is worse than no name.
149
+ */
150
+ export function flagName(code: string | null | undefined, locale?: string): string | null {
151
+ const normalized = normalizeFlagCode(code);
152
+ if (!normalized || !COUNTRY_CODE.test(normalized)) return null;
153
+ const region = normalized.toUpperCase();
154
+ try {
155
+ const names = new Intl.DisplayNames([locale ?? documentLocale() ?? "en"], { type: "region" });
156
+ const name = names.of(region);
157
+ // It echoes the input back when it has no name for the code.
158
+ return !name || name === region ? null : name;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /** The page's own language, which is what a translated page wants the name written in. */
165
+ function documentLocale(): string | undefined {
166
+ if (typeof document === "undefined") return undefined;
167
+ return document.documentElement.lang || undefined;
168
+ }
169
+
131
170
  /** Everything a renderer needs, computed in one pass. Null when the code is unusable. */
132
171
  export interface FlagView {
133
- /** The upstream file name, e.g. `es`, `gb-eng`. */
172
+ /** The file name, e.g. `es`, `gb-eng`. */
134
173
  code: string;
135
174
  src: string;
136
175
  shape: FlagShape;
137
176
  format: FlagFormat;
138
177
  width: number;
139
178
  height: number;
140
- }
141
-
142
- function cdnUrl(code: string, shape: FlagShape): string {
143
- if (shape === "circle") return `https://cdn.jsdelivr.net/gh/HatScripts/circle-flags@${CIRCLE_FLAGS_REF}/flags/${code}.svg`;
144
- const size = shape === "square" ? "1x1" : "4x3";
145
- return `https://cdn.jsdelivr.net/npm/flag-icons@${FLAG_ICONS_VERSION}/flags/${size}/${code}.svg`;
179
+ /** The automatic name, when there is one. */
180
+ name: string | null;
146
181
  }
147
182
 
148
183
  /** One warning per process, not one per flag: a list of 200 would print 200 times. */
@@ -151,32 +186,32 @@ let warnedAboutFormat = false;
151
186
  /**
152
187
  * The URL for one flag. Null when the code cannot be resolved.
153
188
  *
154
- * A raster format asked of the CDN falls back to SVG rather than producing a 404: 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.
189
+ * A raster format asked of the CDN falls back to SVG rather than producing a 404: the sets
190
+ * are stored as SVG, so `format: "webp"` is only meaningful against a local set or a mirror.
191
+ * It says so once, in development, instead of silently disagreeing with the call site.
158
192
  */
159
193
  export function flagSrc(code: string | null | undefined, options: FlagOptions = {}): string | null {
160
194
  const settings = { ...config, ...options };
161
195
  const normalized = normalizeFlagCode(code);
162
196
  if (!normalized) return null;
163
197
 
164
- const remoteCdn = settings.source === "cdn";
198
+ const remote = settings.source === "cdn";
165
199
  let format = settings.format;
166
- if (remoteCdn && format !== "svg") {
200
+ if (remote && format !== "svg") {
167
201
  if (!warnedAboutFormat && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
168
202
  warnedAboutFormat = true;
169
- console.warn(`[enigma/flags] The 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}`);
203
+ console.warn(`[enigma/flags] The flags are served as SVG, so "${format}" was served as SVG. Download a local set for raster formats: enigma add flags --flags local --flag-formats ${format}`);
170
204
  }
171
205
  format = "svg";
172
206
  }
173
- if (remoteCdn) return cdnUrl(normalized, settings.shape);
174
207
 
175
- const base = (settings.source === "local" ? settings.basePath : settings.source).replace(/\/+$/, "");
208
+ // One layout everywhere - `<base>/<shape>/<code>.<format>` - so a mirror is the same
209
+ // string with a different host, and the downloader writes exactly what this reads.
210
+ const base = remote ? FLAG_CDN : (settings.source === "local" ? settings.basePath : settings.source).replace(/\/+$/, "");
176
211
  return `${base}/${settings.shape}/${normalized}.${format}`;
177
212
  }
178
213
 
179
- /** `flagSrc` plus the resolved geometry, for a renderer that has to size the box. */
214
+ /** `flagSrc` plus the resolved geometry and name, for a renderer that has to size the box. */
180
215
  export function flagView(code: string | null | undefined, options: FlagOptions = {}): FlagView | null {
181
216
  const settings = { ...config, ...options };
182
217
  const normalized = normalizeFlagCode(code);
@@ -189,25 +224,36 @@ export function flagView(code: string | null | undefined, options: FlagOptions =
189
224
  shape: settings.shape,
190
225
  format: settings.source === "cdn" ? "svg" : settings.format,
191
226
  width: Math.round(height * FLAG_RATIO[settings.shape]),
192
- height
227
+ height,
228
+ name: flagName(normalized, settings.locale)
193
229
  };
194
230
  }
195
231
 
232
+ export interface FlagAttributeOptions extends FlagOptions {
233
+ /** Your own accessible name, which wins over the automatic one. */
234
+ label?: string;
235
+ /**
236
+ * Beside a country name that is already on screen, the flag is decoration and repeating
237
+ * the name only makes a screen reader say it twice.
238
+ */
239
+ decorative?: boolean;
240
+ }
241
+
196
242
  /**
197
243
  * Attributes for a plain `<img>`, for vanilla, Astro and any template language.
198
244
  *
199
- * `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.
245
+ * The accessible name is AUTOMATIC: the country's own name, in the reader's language, from
246
+ * `Intl.DisplayNames`. `label` replaces it and `decorative` drops it. Nothing is ever
247
+ * invented - a subdivision the platform cannot name, or a runtime without `Intl`, renders
248
+ * as decoration rather than putting a bare "ES" into a screen reader.
203
249
  */
204
- export function flagAttributes(code: string | null | undefined, options: FlagOptions & { label?: string; } = {}): Record<string, string | number> | null {
250
+ export function flagAttributes(code: string | null | undefined, options: FlagAttributeOptions = {}): Record<string, string | number> | null {
205
251
  const view = flagView(code, options);
206
252
  if (!view) return null;
207
- const label = options.label?.trim();
253
+ const label = options.decorative ? "" : options.label?.trim() || view.name || "";
208
254
  return {
209
255
  src: view.src,
210
- alt: label ?? "",
256
+ alt: label,
211
257
  width: view.width,
212
258
  height: view.height,
213
259
  loading: "lazy",
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The icon shapes both renderers draw.
3
+ *
4
+ * Their own module because the React field imports NOTHING else from the vanilla adapter:
5
+ * pulling `createInput` in for two arrays of path data put the whole imperative renderer
6
+ * into every React bundle, for a component that never calls it.
7
+ */
8
+
9
+ /**
10
+ * The built-in glyphs, as path data.
11
+ *
12
+ * Path data rather than markup because there are two renderers: this file writes an SVG
13
+ * string into a button it created, and the React component builds elements. Keeping the
14
+ * shapes here means one definition, and a theme that replaces an icon replaces it in both.
15
+ * Everything is stroked with `currentColor` at 1em, so an icon inherits the field's text.
16
+ */
17
+ export const INPUT_ICON_PATHS = {
18
+ eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],
19
+ eyeOff: [
20
+ "M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9",
21
+ "m2 2 20 20",
22
+ "M9.9 9.9a3 3 0 0 0 4.2 4.2"
23
+ ],
24
+ generate: ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"],
25
+ /** Clears a search field. The platform draws one only in WebKit, and not on mobile. */
26
+ clear: ["m6 6 12 12", "m18 6-12 12"],
27
+ search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14Z", "m20 20-4.2-4.2"]
28
+ } as const;
29
+
30
+ /** The same shapes as a standalone SVG string, for the DOM renderer below. */
31
+ export function iconMarkup(paths: readonly string[]): string {
32
+ const body = paths.map((path) => `<path d="${path}"/>`).join("");
33
+ return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
34
+ }
package/src/core/input.ts CHANGED
@@ -73,28 +73,12 @@ export interface InputInstance {
73
73
  }
74
74
 
75
75
  /**
76
- * The built-in glyphs, as path data.
77
- *
78
- * Path data rather than markup because there are two renderers: this file writes an SVG
79
- * string into a button it created, and the React component builds elements. Keeping the
80
- * shapes here means one definition, and a theme that replaces an icon replaces it in both.
81
- * Everything is stroked with `currentColor` at 1em, so an icon inherits the field's text.
76
+ * The icons live in their own module so the React field can take them without taking this
77
+ * whole adapter with it. Re-exported here because they were part of this module's API
78
+ * before the split, and moving an export is a breaking change for no gain.
82
79
  */
83
- export const INPUT_ICON_PATHS = {
84
- eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],
85
- eyeOff: [
86
- "M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9",
87
- "m2 2 20 20",
88
- "M9.9 9.9a3 3 0 0 0 4.2 4.2"
89
- ],
90
- generate: ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"]
91
- } as const;
92
-
93
- /** The same shapes as a standalone SVG string, for the DOM renderer below. */
94
- export function iconMarkup(paths: readonly string[]): string {
95
- const body = paths.map((path) => `<path d="${path}"/>`).join("");
96
- return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
97
- }
80
+ export { INPUT_ICON_PATHS, iconMarkup } from "@/core/input-icons";
81
+ import { INPUT_ICON_PATHS, iconMarkup } from "@/core/input-icons";
98
82
 
99
83
  const EYE = iconMarkup(INPUT_ICON_PATHS.eye);
100
84
  const EYE_OFF = iconMarkup(INPUT_ICON_PATHS.eyeOff);
Binary file