@enigmax/primitives 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) 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-F25CGNQC.js +19 -0
  6. package/dist/chunk-HC2ME5PU.js +168 -0
  7. package/dist/chunk-HS3X3XCW.js +43 -0
  8. package/dist/chunk-IIT6U7LM.js +249 -0
  9. package/dist/chunk-IXVMRVD4.js +96 -0
  10. package/dist/chunk-KJINGUQN.js +188 -0
  11. package/dist/chunk-LKA2UG4P.js +542 -0
  12. package/dist/chunk-MMQPZGSU.js +161 -0
  13. package/dist/chunk-OCMI7R6H.js +79 -0
  14. package/dist/chunk-QYMUIW5I.js +28 -0
  15. package/dist/chunk-S653GLSF.js +17 -0
  16. package/dist/chunk-SNYUBXWQ.js +149 -0
  17. package/dist/chunk-U3V4EHOB.js +41 -0
  18. package/dist/chunk-UOSSNUSC.js +309 -0
  19. package/dist/chunk-WSB655JB.js +47 -0
  20. package/dist/chunk-XNNQRA35.js +31 -0
  21. package/dist/chunk-XQHCZAPJ.js +102 -0
  22. package/dist/chunk-ZCUFYBPB.js +154 -0
  23. package/dist/chunk-ZWR2EXHQ.js +55 -0
  24. package/dist/flags-BBJc9unY.d.ts +133 -0
  25. package/dist/index-D-ARvWpQ.d.ts +306 -0
  26. package/dist/index-DNXPtY9X.d.ts +144 -0
  27. package/dist/index.d.ts +16 -496
  28. package/dist/index.js +9 -1
  29. package/dist/input-BwXjFenq.d.ts +77 -0
  30. package/dist/marquee-CJ3Uwy3E.d.ts +81 -0
  31. package/dist/network-D2LsBG_k.d.ts +39 -0
  32. package/dist/next/index.d.ts +20 -3
  33. package/dist/next/index.js +23 -3
  34. package/dist/notifications-BpVV6sel.d.ts +70 -0
  35. package/dist/password-3DRQYAYQ.js +2 -0
  36. package/dist/password-C8lG4Zm9.d.ts +71 -0
  37. package/dist/password-FB2CUEKJ.js +1 -0
  38. package/dist/react/button.d.ts +75 -0
  39. package/dist/react/button.js +4 -0
  40. package/dist/react/flag.d.ts +37 -0
  41. package/dist/react/flag.js +3 -0
  42. package/dist/react/index.d.ts +28 -286
  43. package/dist/react/index.js +22 -2
  44. package/dist/react/input.d.ts +4 -0
  45. package/dist/react/input.js +3 -0
  46. package/dist/react/marquee.d.ts +44 -0
  47. package/dist/react/marquee.js +3 -0
  48. package/dist/react/network.d.ts +20 -0
  49. package/dist/react/network.js +3 -0
  50. package/dist/react/notifications.d.ts +17 -0
  51. package/dist/react/notifications.js +3 -0
  52. package/dist/react/palette.d.ts +3 -0
  53. package/dist/react/palette.js +4 -0
  54. package/dist/react/relative-time.d.ts +21 -0
  55. package/dist/react/relative-time.js +3 -0
  56. package/dist/react/search.d.ts +30 -0
  57. package/dist/react/search.js +3 -0
  58. package/dist/react/slot.d.ts +47 -0
  59. package/dist/react/slot.js +2 -0
  60. package/dist/react/toast.d.ts +40 -0
  61. package/dist/react/toast.js +4 -0
  62. package/dist/react-router/index.d.ts +20 -3
  63. package/dist/react-router/index.js +23 -3
  64. package/dist/relative-time-YpRTG7YH.d.ts +106 -0
  65. package/dist/search-P4OHCRXD.js +50 -0
  66. package/package.json +53 -1
  67. package/registry.json +235 -16
  68. package/src/core/flags.ts +266 -0
  69. package/src/core/input-icons.ts +34 -0
  70. package/src/core/input.ts +5 -21
  71. package/src/core/palette.ts +0 -0
  72. package/src/index.ts +18 -0
  73. package/src/react/button.tsx +70 -4
  74. package/src/react/flag.tsx +70 -0
  75. package/src/react/index.ts +52 -5
  76. package/src/react/input/icon.tsx +17 -0
  77. package/src/react/input/index.tsx +307 -0
  78. package/src/react/input/password.tsx +174 -0
  79. package/src/react/input/search.tsx +82 -0
  80. package/src/react/input/types.ts +146 -0
  81. package/src/react/input/write-value.ts +18 -0
  82. package/src/react/palette/context.ts +60 -0
  83. package/src/react/palette/index.tsx +66 -0
  84. package/src/react/palette/root.tsx +621 -0
  85. package/src/react/slot.tsx +91 -0
  86. package/src/react/use-button.ts +3 -1
  87. package/dist/chunk-53DQY6LP.js +0 -1164
  88. package/dist/chunk-U2KYYMBJ.js +0 -724
  89. 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",
204
222
  "targets": [
205
223
  "react"
206
224
  ],
207
225
  "rewrite": {
208
- "@/core/input": "./input"
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",
247
+ "targets": [
248
+ "react"
249
+ ],
250
+ "rewrite": {
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,10 +473,169 @@
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": "src/react/palette/context.ts",
520
+ "dest": "palette/context.ts",
521
+ "targets": [
522
+ "react"
523
+ ],
524
+ "rewrite": {
525
+ "@/core/palette": "./palette",
526
+ "@/core/search": "./search-core"
527
+ }
528
+ },
529
+ {
530
+ "path": "src/react/palette/root.tsx",
531
+ "dest": "palette/root.tsx",
532
+ "targets": [
533
+ "react"
534
+ ],
535
+ "rewrite": {
536
+ "@/core/palette": "./palette",
537
+ "@/core/search": "./search-core",
538
+ "@/react/slot": "./slot",
539
+ "@/react/palette/context": "./context"
540
+ }
541
+ },
542
+ {
543
+ "path": "src/react/palette/index.tsx",
544
+ "dest": "palette/index.tsx",
545
+ "main": true,
546
+ "targets": [
547
+ "react"
548
+ ],
549
+ "rewrite": {
550
+ "@/react/palette/root": "./root",
551
+ "@/react/palette/context": "./context"
552
+ }
553
+ }
554
+ ],
555
+ "styles": false,
556
+ "themeHooks": [
557
+ "[data-enigma-palette-trigger]",
558
+ "[data-enigma-palette-trigger-key]",
559
+ "[data-enigma-palette-overlay]",
560
+ "[data-enigma-palette-content]",
561
+ "[data-enigma-palette-title]",
562
+ "[data-enigma-palette-field]",
563
+ "[data-enigma-palette-list]",
564
+ "[data-enigma-palette-group]",
565
+ "[data-enigma-palette-group-label]",
566
+ "[data-enigma-palette-item]",
567
+ "[data-enigma-palette-item][data-active=true]",
568
+ "[data-enigma-palette-item][data-kind=recent]",
569
+ "[data-enigma-palette-item-label]",
570
+ "[data-enigma-palette-item-description]",
571
+ "[data-enigma-palette-empty]",
572
+ "[data-enigma-palette-footer]",
573
+ "[data-enigma-palette-hint]"
574
+ ],
575
+ "docs": "docs/notes/primitives.md#palette"
576
+ },
577
+ {
578
+ "name": "flags",
579
+ "title": "Country flags",
580
+ "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.",
581
+ "targets": [
582
+ "vanilla",
583
+ "astro",
584
+ "react"
585
+ ],
586
+ "entry": {
587
+ "vanilla": "@enigmax/primitives",
588
+ "astro": "@enigmax/primitives",
589
+ "react": "@enigmax/primitives/react/flag"
590
+ },
591
+ "exports": {
592
+ "vanilla": [
593
+ "flagAttributes",
594
+ "flagSrc",
595
+ "configureFlags"
596
+ ],
597
+ "astro": [
598
+ "flagAttributes",
599
+ "flagSrc",
600
+ "configureFlags"
601
+ ],
602
+ "react": [
603
+ "Flag",
604
+ "configureFlags"
605
+ ]
606
+ },
607
+ "files": [
608
+ {
609
+ "path": "src/core/flags.ts",
610
+ "dest": "flags/flags.ts",
611
+ "targets": [
612
+ "vanilla",
613
+ "astro",
614
+ "react"
615
+ ]
616
+ },
617
+ {
618
+ "path": "src/react/flag.tsx",
619
+ "dest": "flags/index.tsx",
620
+ "targets": [
621
+ "react"
622
+ ],
623
+ "rewrite": {
624
+ "@/core/flags": "./flags"
625
+ },
626
+ "main": true
627
+ }
628
+ ],
629
+ "styles": false,
630
+ "themeHooks": [
631
+ "[data-enigma-flag]",
632
+ "[data-enigma-flag][data-flag-shape=rect]",
633
+ "[data-enigma-flag][data-flag-shape=square]",
634
+ "[data-enigma-flag][data-flag-shape=circle]"
635
+ ],
636
+ "assets": "flags",
637
+ "docs": "docs/notes/primitives.md#flags"
638
+ },
420
639
  {
421
640
  "name": "notifications",
422
641
  "title": "Notification queue",
@@ -0,0 +1,266 @@
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
+ * Three sets, all SVG, all served by enigma itself:
12
+ *
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.
19
+ *
20
+ * Nothing here touches the DOM, so it renders on a server.
21
+ */
22
+
23
+ export type FlagShape = "rect" | "square" | "circle";
24
+
25
+ /**
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.
29
+ */
30
+ export type FlagFormat = "svg" | "png" | "webp";
31
+
32
+ /**
33
+ * Where the files come from.
34
+ *
35
+ * - `"cdn"` - enigma's own artwork, over a CDN. Nothing to install.
36
+ * - `"local"` - files under `basePath` in your own public directory.
37
+ * - any URL - a mirror of your own with the same layout
38
+ * (`<base>/<shape>/<code>.<format>`), which is exactly the tree the downloader writes.
39
+ */
40
+ export type FlagSource = "cdn" | "local" | (string & {});
41
+
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
+
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
+ * 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;
58
+ }
59
+
60
+ export interface FlagConfig extends Required<Omit<FlagOptions, "size" | "locale">> {
61
+ size: number;
62
+ locale?: string;
63
+ }
64
+
65
+ const DEFAULTS: FlagConfig = {
66
+ shape: "rect",
67
+ format: "svg",
68
+ source: "cdn",
69
+ basePath: "/flags",
70
+ size: 16
71
+ };
72
+
73
+ let config: FlagConfig = { ...DEFAULTS };
74
+
75
+ /**
76
+ * Set the defaults for every flag on the page, once, at startup.
77
+ *
78
+ * This is what makes the CDN/local decision a one-line change rather than a prop on every
79
+ * call site: `configureFlags({ source: "local" })` moves the whole app onto the files the
80
+ * downloader wrote, and nothing else in the codebase mentions where a flag comes from.
81
+ */
82
+ export function configureFlags(patch: Partial<FlagConfig>): FlagConfig {
83
+ config = { ...config, ...patch };
84
+ return config;
85
+ }
86
+
87
+ export function flagConfig(): FlagConfig {
88
+ return config;
89
+ }
90
+
91
+ /** Test seam and a way back to the shipped defaults. */
92
+ export function resetFlagConfig(): FlagConfig {
93
+ config = { ...DEFAULTS };
94
+ return config;
95
+ }
96
+
97
+ /** Width / height of each shape. The circle set is square; only `rect` is 4:3. */
98
+ export const FLAG_RATIO: Record<FlagShape, number> = { rect: 4 / 3, square: 1, circle: 1 };
99
+
100
+ const EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
101
+ /**
102
+ * A BCP 47 tag whose region is spelled the way the standard spells it: UPPERCASE. That
103
+ * casing is the whole discriminator, and it has to be read before the value is lowercased -
104
+ * `es-ES` is Spanish-in-Spain and means the flag `es`, while `es-ct` is a subdivision code
105
+ * the sets publish (Catalonia) and means the file `es-ct`. Lowercasing first makes those
106
+ * two the same string, and then one of them is always wrong.
107
+ */
108
+ const LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
109
+ /** What the sets actually name their files: `es`, `gb-eng`, `au-nsw`, `easter_island`. */
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}$/;
113
+
114
+ /**
115
+ * Anything a codebase calls a country into the file name the sets use, or null.
116
+ *
117
+ * Null rather than a guess: a code this cannot resolve would otherwise become a 404 image
118
+ * with no alt text, and a broken flag beside a country name is worse than no flag at all.
119
+ * An emoji flag is accepted and converted, so migrating an existing picker is a rename of
120
+ * the component and nothing else.
121
+ */
122
+ export function normalizeFlagCode(value: string | null | undefined): string | null {
123
+ if (!value) return null;
124
+ const raw = value.trim();
125
+ if (!raw) return null;
126
+
127
+ if (EMOJI_FLAG.test(raw)) {
128
+ const letters = [...raw].map((char) => String.fromCharCode((char.codePointAt(0) ?? 0) - 0x1f1e6 + 97));
129
+ return letters.join("");
130
+ }
131
+
132
+ const locale = LOCALE_TAG.exec(raw);
133
+ if (locale) return locale[2].toLowerCase();
134
+
135
+ const code = raw.toLowerCase();
136
+ return FILE_CODE.test(code) ? code : null;
137
+ }
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
+
170
+ /** Everything a renderer needs, computed in one pass. Null when the code is unusable. */
171
+ export interface FlagView {
172
+ /** The file name, e.g. `es`, `gb-eng`. */
173
+ code: string;
174
+ src: string;
175
+ shape: FlagShape;
176
+ format: FlagFormat;
177
+ width: number;
178
+ height: number;
179
+ /** The automatic name, when there is one. */
180
+ name: string | null;
181
+ }
182
+
183
+ /** One warning per process, not one per flag: a list of 200 would print 200 times. */
184
+ let warnedAboutFormat = false;
185
+
186
+ /**
187
+ * The URL for one flag. Null when the code cannot be resolved.
188
+ *
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.
192
+ */
193
+ export function flagSrc(code: string | null | undefined, options: FlagOptions = {}): string | null {
194
+ const settings = { ...config, ...options };
195
+ const normalized = normalizeFlagCode(code);
196
+ if (!normalized) return null;
197
+
198
+ const remote = settings.source === "cdn";
199
+ let format = settings.format;
200
+ if (remote && format !== "svg") {
201
+ if (!warnedAboutFormat && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
202
+ warnedAboutFormat = true;
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}`);
204
+ }
205
+ format = "svg";
206
+ }
207
+
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(/\/+$/, "");
211
+ return `${base}/${settings.shape}/${normalized}.${format}`;
212
+ }
213
+
214
+ /** `flagSrc` plus the resolved geometry and name, for a renderer that has to size the box. */
215
+ export function flagView(code: string | null | undefined, options: FlagOptions = {}): FlagView | null {
216
+ const settings = { ...config, ...options };
217
+ const normalized = normalizeFlagCode(code);
218
+ const src = flagSrc(normalized, options);
219
+ if (!normalized || !src) return null;
220
+ const height = Math.max(1, Math.round(settings.size));
221
+ return {
222
+ code: normalized,
223
+ src,
224
+ shape: settings.shape,
225
+ format: settings.source === "cdn" ? "svg" : settings.format,
226
+ width: Math.round(height * FLAG_RATIO[settings.shape]),
227
+ height,
228
+ name: flagName(normalized, settings.locale)
229
+ };
230
+ }
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
+
242
+ /**
243
+ * Attributes for a plain `<img>`, for vanilla, Astro and any template language.
244
+ *
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.
249
+ */
250
+ export function flagAttributes(code: string | null | undefined, options: FlagAttributeOptions = {}): Record<string, string | number> | null {
251
+ const view = flagView(code, options);
252
+ if (!view) return null;
253
+ const label = options.decorative ? "" : options.label?.trim() || view.name || "";
254
+ return {
255
+ src: view.src,
256
+ alt: label,
257
+ width: view.width,
258
+ height: view.height,
259
+ loading: "lazy",
260
+ decoding: "async",
261
+ "data-enigma-flag": "",
262
+ "data-flag-code": view.code,
263
+ "data-flag-shape": view.shape,
264
+ ...(label ? {} : { "aria-hidden": "true" })
265
+ };
266
+ }
@@ -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