@neptune.fintech/icons 2.0.0 → 2.2.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.
@@ -0,0 +1,967 @@
1
+ // Neptune Odyssey — payment-network & fintech BRAND MARKS · © 2026 Neptune.Fintech (neptune.ly)
2
+ //
3
+ // ⚠️ TRADEMARK NOTICE — READ BEFORE USE
4
+ // ----------------------------------------------------------------------------
5
+ // The marks in this module (Visa, Mastercard, American Express, Discover,
6
+ // UnionPay, Western Union, MoneyGram, Apple Pay, Google Pay, PayPal, SWIFT,
7
+ // NUMO, Moamalat, LyPay, OnePay, Sadad, Tadawul, etc.) are THIRD-PARTY
8
+ // TRADEMARKS owned by their respective owners. They are provided here ONLY as
9
+ // SIMPLIFIED, SCHEMATIC IDENTIFICATION MARKS / PLACEHOLDERS so a UI can label a
10
+ // payment method — they are deliberately NOT pixel-exact reproductions of any
11
+ // official logo, and they are NOT traced from any official artwork. They are
12
+ // ORIGINAL, clean geometric placeholders authored for Neptune Odyssey.
13
+ //
14
+ // These marks are NOT licensed under the Neptune Odyssey Community License and
15
+ // are kept SEPARATE from the monochrome ICONS set on purpose. In production,
16
+ // replace each with the brand's OFFICIAL asset via registerBrandMark() (see
17
+ // below) and follow that brand's brand/usage guidelines.
18
+ //
19
+ // The Libyan / local marks (NUMO, Moamalat, LyPay, OnePay, Sadad, Tadawul) are
20
+ // NEUTRAL PLACEHOLDERS (a simple badge + initials/motif) — we do not ship their
21
+ // real assets. Replace them with official artwork before shipping.
22
+ //
23
+ // ── THREE-VARIANT SYSTEM ────────────────────────────────────────────────────
24
+ // Every mark is authored as a small array of shape PRIMITIVES tagged by ROLE,
25
+ // plus a per-mark brand-colour map. It then renders in three variants:
26
+ // • "color" — multicolour, brand colours by role. The default.
27
+ // • "mono" — a single flat silhouette in `currentColor` (fills only).
28
+ // • "outline" — line style: stroke="currentColor", fill="none", round joins.
29
+ // Licensed users can drop in a brand's official SVG with registerBrandMark();
30
+ // after that, brandMarkSvg() returns the override.
31
+ // Neutral framing colours shared by the "card body" marks (not brand colours).
32
+ const CARD_BG = "#F4F5F7";
33
+ const CARD_HAIRLINE = "#E1E3E8";
34
+ // A neutral card body + hairline frame, as two shapes. Reused by many marks.
35
+ function cardFrame(bg = CARD_BG) {
36
+ return [
37
+ { kind: "rect", role: "bg", attrs: { width: 48, height: 32, rx: 4, fill: bg } },
38
+ {
39
+ kind: "rect",
40
+ role: "hairline",
41
+ attrs: { x: 0.5, y: 0.5, width: 47, height: 31, rx: 3.5 },
42
+ },
43
+ ];
44
+ }
45
+ const FONT = "Arial, Helvetica, sans-serif";
46
+ /**
47
+ * name → mark definition. Each is an ORIGINAL simplified geometric placeholder
48
+ * recognisable by the brand's colours/basic forms — never traced artwork.
49
+ */
50
+ const MARK_DEFS = {
51
+ // ── Card networks ───────────────────────────────────────────────────
52
+ visa: {
53
+ viewBox: "0 0 48 32",
54
+ label: "Visa",
55
+ colors: { a: "#1A1F71", ink: "#1A1F71" },
56
+ shapes: [
57
+ ...cardFrame(),
58
+ {
59
+ kind: "text",
60
+ role: "a",
61
+ text: "VISA",
62
+ attrs: {
63
+ x: 24,
64
+ y: 21.5,
65
+ "font-family": FONT,
66
+ "font-size": 13,
67
+ "font-style": "italic",
68
+ "font-weight": 700,
69
+ "text-anchor": "middle",
70
+ "letter-spacing": 0.5,
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ mastercard: {
76
+ viewBox: "0 0 48 32",
77
+ label: "Mastercard",
78
+ colors: { a: "#EB001B", b: "#F79E1B", c: "#FF5F00" },
79
+ shapes: [
80
+ ...cardFrame(),
81
+ { kind: "circle", role: "a", attrs: { cx: 20, cy: 16, r: 8 } },
82
+ { kind: "circle", role: "b", attrs: { cx: 28, cy: 16, r: 8 } },
83
+ {
84
+ kind: "path",
85
+ role: "c",
86
+ attrs: { d: "M24 9.7a8 8 0 0 0 0 12.6 8 8 0 0 0 0-12.6Z" },
87
+ },
88
+ ],
89
+ },
90
+ amex: {
91
+ viewBox: "0 0 48 32",
92
+ label: "American Express",
93
+ colors: { a: "#2E77BC", ink: "#FFFFFF" },
94
+ shapes: [
95
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
96
+ {
97
+ kind: "rect",
98
+ role: "ink",
99
+ attrs: { x: 6, y: 9, width: 36, height: 14, rx: 2, "fill-opacity": 0.12 },
100
+ },
101
+ {
102
+ kind: "text",
103
+ role: "ink",
104
+ text: "AMEX",
105
+ attrs: {
106
+ x: 24,
107
+ y: 20.5,
108
+ "font-family": FONT,
109
+ "font-size": 11,
110
+ "font-weight": 700,
111
+ "text-anchor": "middle",
112
+ "letter-spacing": 1,
113
+ },
114
+ },
115
+ ],
116
+ },
117
+ discover: {
118
+ viewBox: "0 0 48 32",
119
+ label: "Discover",
120
+ colors: { ink: "#231F20", a: "#F58220" },
121
+ shapes: [
122
+ ...cardFrame(),
123
+ {
124
+ kind: "text",
125
+ role: "ink",
126
+ text: "DISC",
127
+ attrs: {
128
+ x: 21,
129
+ y: 20.5,
130
+ "font-family": FONT,
131
+ "font-size": 9,
132
+ "font-weight": 700,
133
+ "text-anchor": "middle",
134
+ "letter-spacing": 0.3,
135
+ },
136
+ },
137
+ { kind: "circle", role: "a", attrs: { cx: 36, cy: 17, r: 6 } },
138
+ ],
139
+ },
140
+ unionpay: {
141
+ viewBox: "0 0 48 32",
142
+ label: "UnionPay",
143
+ colors: { a: "#E21836", b: "#00447C", c: "#007B84", ink: "#FFFFFF" },
144
+ shapes: [
145
+ ...cardFrame(),
146
+ { kind: "rect", role: "a", attrs: { x: 10, y: 7, width: 9, height: 18, rx: 2 } },
147
+ { kind: "rect", role: "b", attrs: { x: 19, y: 7, width: 9, height: 18, rx: 2 } },
148
+ { kind: "rect", role: "c", attrs: { x: 28, y: 7, width: 9, height: 18, rx: 2 } },
149
+ {
150
+ kind: "text",
151
+ role: "ink",
152
+ text: "UPAY",
153
+ attrs: {
154
+ x: 24,
155
+ y: 19.5,
156
+ "font-family": FONT,
157
+ "font-size": 6,
158
+ "font-weight": 700,
159
+ "text-anchor": "middle",
160
+ },
161
+ },
162
+ ],
163
+ },
164
+ // ── Money transfer ──────────────────────────────────────────────────
165
+ "western-union": {
166
+ viewBox: "0 0 48 32",
167
+ label: "Western Union",
168
+ colors: { a: "#FFDD00", ink: "#000000" },
169
+ shapes: [
170
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
171
+ {
172
+ kind: "text",
173
+ role: "ink",
174
+ text: "WESTERN",
175
+ attrs: {
176
+ x: 24,
177
+ y: 14.5,
178
+ "font-family": FONT,
179
+ "font-size": 7,
180
+ "font-weight": 700,
181
+ "text-anchor": "middle",
182
+ "letter-spacing": 0.5,
183
+ },
184
+ },
185
+ {
186
+ kind: "text",
187
+ role: "ink",
188
+ text: "UNION",
189
+ attrs: {
190
+ x: 24,
191
+ y: 23.5,
192
+ "font-family": FONT,
193
+ "font-size": 7,
194
+ "font-weight": 700,
195
+ "text-anchor": "middle",
196
+ "letter-spacing": 0.5,
197
+ },
198
+ },
199
+ ],
200
+ },
201
+ moneygram: {
202
+ viewBox: "0 0 48 32",
203
+ label: "MoneyGram",
204
+ colors: { a: "#E51937", ink: "#E51937" },
205
+ shapes: [
206
+ ...cardFrame(),
207
+ { kind: "circle", role: "a", attrs: { cx: 13, cy: 16, r: 5 } },
208
+ {
209
+ kind: "text",
210
+ role: "ink",
211
+ text: "MGRAM",
212
+ attrs: {
213
+ x: 29,
214
+ y: 19.5,
215
+ "font-family": FONT,
216
+ "font-size": 7,
217
+ "font-weight": 700,
218
+ "text-anchor": "middle",
219
+ },
220
+ },
221
+ ],
222
+ },
223
+ // ── Libyan / local — PLACEHOLDERS (replace with official assets) ─────
224
+ numo: {
225
+ viewBox: "0 0 48 32",
226
+ label: "NUMO (placeholder mark)",
227
+ placeholder: true,
228
+ colors: { a: "#0E2A47", b: "#5AA9E6", ink: "#FFFFFF" },
229
+ shapes: [
230
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
231
+ {
232
+ kind: "rect",
233
+ role: "b",
234
+ attrs: { x: 6, y: 8, width: 36, height: 16, rx: 3, fill: "none", "stroke-width": 1.4 },
235
+ },
236
+ {
237
+ kind: "text",
238
+ role: "ink",
239
+ text: "NUMO",
240
+ attrs: {
241
+ x: 24,
242
+ y: 21,
243
+ "font-family": FONT,
244
+ "font-size": 9,
245
+ "font-weight": 700,
246
+ "text-anchor": "middle",
247
+ "letter-spacing": 1.5,
248
+ },
249
+ },
250
+ ],
251
+ },
252
+ moamalat: {
253
+ viewBox: "0 0 48 32",
254
+ label: "Moamalat (placeholder mark)",
255
+ placeholder: true,
256
+ colors: { a: "#1C7A4D", b: "#FFFFFF", ink: "#FFFFFF" },
257
+ shapes: [
258
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
259
+ {
260
+ kind: "circle",
261
+ role: "b",
262
+ attrs: { cx: 13, cy: 16, r: 6, fill: "none", "stroke-width": 1.4 },
263
+ },
264
+ {
265
+ kind: "text",
266
+ role: "ink",
267
+ text: "M",
268
+ attrs: {
269
+ x: 13,
270
+ y: 19,
271
+ "font-family": FONT,
272
+ "font-size": 8,
273
+ "font-weight": 700,
274
+ "text-anchor": "middle",
275
+ },
276
+ },
277
+ {
278
+ kind: "text",
279
+ role: "ink",
280
+ text: "MOAM",
281
+ attrs: {
282
+ x: 31,
283
+ y: 19.5,
284
+ "font-family": FONT,
285
+ "font-size": 7,
286
+ "font-weight": 700,
287
+ "text-anchor": "middle",
288
+ "letter-spacing": 0.5,
289
+ },
290
+ },
291
+ ],
292
+ },
293
+ // LyPay — a green→blue swoosh/flag beside a "LyPay" wordmark block.
294
+ // Original geometry: two short strokes (green, blue) form a flag, wordmark right.
295
+ lypay: {
296
+ viewBox: "0 0 48 32",
297
+ label: "LyPay (placeholder mark)",
298
+ placeholder: true,
299
+ colors: { a: "#3FBF7F", b: "#2AA0D8", ink: "#0E3A2E" },
300
+ shapes: [
301
+ ...cardFrame("#EAF7F0"),
302
+ // green swoosh stroke
303
+ {
304
+ kind: "path",
305
+ role: "a",
306
+ attrs: {
307
+ d: "M7 21c3-6 6-9 11-9",
308
+ fill: "none",
309
+ "stroke-width": 2.6,
310
+ "stroke-linecap": "round",
311
+ },
312
+ },
313
+ // blue swoosh stroke (above the green), a flag
314
+ {
315
+ kind: "path",
316
+ role: "b",
317
+ attrs: {
318
+ d: "M7 16c3-5 6-7.5 11-7.5",
319
+ fill: "none",
320
+ "stroke-width": 2.6,
321
+ "stroke-linecap": "round",
322
+ },
323
+ },
324
+ {
325
+ kind: "text",
326
+ role: "ink",
327
+ text: "LyPay",
328
+ attrs: {
329
+ x: 33,
330
+ y: 20,
331
+ "font-family": FONT,
332
+ "font-size": 9,
333
+ "font-weight": 700,
334
+ "text-anchor": "middle",
335
+ "letter-spacing": 0.3,
336
+ },
337
+ },
338
+ ],
339
+ },
340
+ // OnePay — a deep-blue rounded tile with a stylised folded "1" ribbon motif.
341
+ // Original geometry: rounded tile (deep blue), a folded "1" from two strokes
342
+ // in a lighter blue, plus a small "PAY" wordmark.
343
+ onepay: {
344
+ viewBox: "0 0 48 32",
345
+ label: "OnePay (placeholder mark)",
346
+ placeholder: true,
347
+ colors: { a: "#1F6FB2", b: "#2AA0D8", ink: "#FFFFFF" },
348
+ shapes: [
349
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 6 } },
350
+ // folded "1": a flag-foot stroke + the upright, in the lighter blue tone.
351
+ {
352
+ kind: "path",
353
+ role: "b",
354
+ attrs: {
355
+ d: "M10 11.5l4-2.5v14",
356
+ fill: "none",
357
+ "stroke-width": 2.6,
358
+ "stroke-linecap": "round",
359
+ "stroke-linejoin": "round",
360
+ },
361
+ },
362
+ {
363
+ kind: "path",
364
+ role: "b",
365
+ attrs: {
366
+ d: "M11 23h6",
367
+ fill: "none",
368
+ "stroke-width": 2.6,
369
+ "stroke-linecap": "round",
370
+ },
371
+ },
372
+ {
373
+ kind: "text",
374
+ role: "ink",
375
+ text: "PAY",
376
+ attrs: {
377
+ x: 32,
378
+ y: 19.5,
379
+ "font-family": FONT,
380
+ "font-size": 8,
381
+ "font-weight": 700,
382
+ "text-anchor": "middle",
383
+ "letter-spacing": 0.5,
384
+ },
385
+ },
386
+ ],
387
+ },
388
+ sadad: {
389
+ viewBox: "0 0 48 32",
390
+ label: "Sadad (placeholder mark)",
391
+ placeholder: true,
392
+ colors: { a: "#5B2E91", b: "#FFFFFF", ink: "#FFFFFF" },
393
+ shapes: [
394
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
395
+ {
396
+ kind: "rect",
397
+ role: "b",
398
+ attrs: { x: 7, y: 9, width: 34, height: 14, rx: 3, fill: "none", "stroke-width": 1.4 },
399
+ },
400
+ {
401
+ kind: "text",
402
+ role: "ink",
403
+ text: "SADAD",
404
+ attrs: {
405
+ x: 24,
406
+ y: 20.5,
407
+ "font-family": FONT,
408
+ "font-size": 9,
409
+ "font-weight": 700,
410
+ "text-anchor": "middle",
411
+ "letter-spacing": 1,
412
+ },
413
+ },
414
+ ],
415
+ },
416
+ tadawul: {
417
+ viewBox: "0 0 48 32",
418
+ label: "Tadawul (placeholder mark)",
419
+ placeholder: true,
420
+ colors: { a: "#1A4D4D", b: "#3FC1C9", ink: "#FFFFFF" },
421
+ shapes: [
422
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
423
+ {
424
+ kind: "path",
425
+ role: "b",
426
+ attrs: {
427
+ d: "M9 20l5-5 4 3 6-7",
428
+ fill: "none",
429
+ "stroke-width": 1.6,
430
+ "stroke-linecap": "round",
431
+ "stroke-linejoin": "round",
432
+ },
433
+ },
434
+ {
435
+ kind: "text",
436
+ role: "ink",
437
+ text: "TDWL",
438
+ attrs: {
439
+ x: 33,
440
+ y: 19.5,
441
+ "font-family": FONT,
442
+ "font-size": 6,
443
+ "font-weight": 700,
444
+ "text-anchor": "middle",
445
+ "letter-spacing": 0.5,
446
+ },
447
+ },
448
+ ],
449
+ },
450
+ // ── Wallets / generic ───────────────────────────────────────────────
451
+ "apple-pay": {
452
+ viewBox: "0 0 48 32",
453
+ label: "Apple Pay",
454
+ colors: { ink: "#000000" },
455
+ shapes: [
456
+ ...cardFrame("#FFFFFF"),
457
+ {
458
+ kind: "path",
459
+ role: "ink",
460
+ attrs: {
461
+ d: "M14.4 12.1c.5-.6.8-1.4.7-2.2-.7 0-1.5.5-2 1.1-.4.5-.8 1.3-.7 2.1.8.1 1.5-.4 2-1Zm.7 1.1c-1.1-.1-2 .6-2.5.6-.5 0-1.3-.6-2.1-.6-1.1 0-2.1.6-2.7 1.6-1.1 2-.3 4.9.8 6.5.5.8 1.2 1.7 2 1.6.8 0 1.1-.5 2.1-.5s1.3.5 2.1.5c.9 0 1.4-.8 2-1.5.6-.9.8-1.7.9-1.8 0 0-1.7-.7-1.7-2.6 0-1.6 1.3-2.4 1.4-2.4-.8-1.1-2-1.4-2.3-1.5Z",
462
+ },
463
+ },
464
+ {
465
+ kind: "text",
466
+ role: "ink",
467
+ text: "Pay",
468
+ attrs: {
469
+ x: 33,
470
+ y: 19.5,
471
+ "font-family": FONT,
472
+ "font-size": 9,
473
+ "font-weight": 600,
474
+ "text-anchor": "middle",
475
+ },
476
+ },
477
+ ],
478
+ },
479
+ "google-pay": {
480
+ viewBox: "0 0 48 32",
481
+ label: "Google Pay",
482
+ colors: { a: "#4285F4", b: "#34A853", ink: "#5F6368" },
483
+ shapes: [
484
+ ...cardFrame("#FFFFFF"),
485
+ {
486
+ kind: "path",
487
+ role: "a",
488
+ attrs: {
489
+ d: "M16 16.2v2.1h3a2.6 2.6 0 0 1-1.1 1.7 3.2 3.2 0 1 1-1-4.6l1.5-1.5a5.3 5.3 0 1 0 1.6 4.5h-5Z",
490
+ },
491
+ },
492
+ {
493
+ kind: "path",
494
+ role: "b",
495
+ attrs: { d: "M19 16.2h-3v2.1h3a3 3 0 0 0 .1-.8 4 4 0 0 0-.1-1.3Z" },
496
+ },
497
+ {
498
+ kind: "text",
499
+ role: "ink",
500
+ text: "Pay",
501
+ attrs: {
502
+ x: 33,
503
+ y: 19.5,
504
+ "font-family": FONT,
505
+ "font-size": 9,
506
+ "font-weight": 600,
507
+ "text-anchor": "middle",
508
+ },
509
+ },
510
+ ],
511
+ },
512
+ paypal: {
513
+ viewBox: "0 0 48 32",
514
+ label: "PayPal",
515
+ colors: { a: "#003087", b: "#009CDE", ink: "#003087" },
516
+ shapes: [
517
+ ...cardFrame(),
518
+ {
519
+ kind: "path",
520
+ role: "a",
521
+ attrs: { d: "M15 9h5.2c2.4 0 4 1.4 3.6 3.9-.4 2.6-2.4 3.9-4.9 3.9h-1.7l-.7 4.3h-2.9L15 9Z" },
522
+ },
523
+ {
524
+ kind: "path",
525
+ role: "b",
526
+ attrs: { d: "M18 11h4.3c2.4 0 4 1.4 3.6 3.9-.4 2.6-2.4 3.9-4.9 3.9h-1.7l-.7 4.3h-2.9L18 11Z" },
527
+ },
528
+ {
529
+ kind: "text",
530
+ role: "ink",
531
+ text: "Pal",
532
+ attrs: {
533
+ x: 34,
534
+ y: 20,
535
+ "font-family": FONT,
536
+ "font-size": 7,
537
+ "font-weight": 700,
538
+ "text-anchor": "middle",
539
+ },
540
+ },
541
+ ],
542
+ },
543
+ swift: {
544
+ viewBox: "0 0 48 32",
545
+ label: "SWIFT",
546
+ colors: { a: "#0033A0", ink: "#FFFFFF" },
547
+ shapes: [
548
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
549
+ {
550
+ kind: "text",
551
+ role: "ink",
552
+ text: "SWIFT",
553
+ attrs: {
554
+ x: 24,
555
+ y: 20.5,
556
+ "font-family": FONT,
557
+ "font-size": 10,
558
+ "font-weight": 700,
559
+ "text-anchor": "middle",
560
+ "letter-spacing": 1.5,
561
+ },
562
+ },
563
+ ],
564
+ },
565
+ mada: {
566
+ viewBox: "0 0 48 32",
567
+ label: "mada-style domestic scheme (generic)",
568
+ placeholder: true,
569
+ colors: { a: "#84BD00", b: "#1F3661" },
570
+ shapes: [
571
+ ...cardFrame(),
572
+ { kind: "rect", role: "a", attrs: { x: 9, y: 13, width: 14, height: 6, rx: 3 } },
573
+ { kind: "rect", role: "b", attrs: { x: 25, y: 13, width: 14, height: 6, rx: 3 } },
574
+ ],
575
+ },
576
+ "generic-card": {
577
+ viewBox: "0 0 48 32",
578
+ label: "Card",
579
+ colors: { a: "#3C4858", b: "#E8C56B", ink: "#FFFFFF" },
580
+ shapes: [
581
+ { kind: "rect", role: "a", attrs: { width: 48, height: 32, rx: 4 } },
582
+ { kind: "rect", role: "b", attrs: { x: 6, y: 11, width: 8, height: 6, rx: 1.2 } },
583
+ {
584
+ kind: "path",
585
+ role: "ink",
586
+ attrs: {
587
+ d: "M6 22h20",
588
+ fill: "none",
589
+ "stroke-width": 1.4,
590
+ "stroke-linecap": "round",
591
+ "stroke-opacity": 0.7,
592
+ },
593
+ },
594
+ {
595
+ kind: "path",
596
+ role: "ink",
597
+ attrs: {
598
+ d: "M30 22h12",
599
+ fill: "none",
600
+ "stroke-width": 1.4,
601
+ "stroke-linecap": "round",
602
+ "stroke-opacity": 0.4,
603
+ },
604
+ },
605
+ ],
606
+ },
607
+ "contactless-pay": {
608
+ viewBox: "0 0 48 32",
609
+ label: "Contactless payment",
610
+ colors: { a: "#1F6FEB" },
611
+ shapes: [
612
+ ...cardFrame(),
613
+ {
614
+ kind: "g",
615
+ role: "a",
616
+ attrs: { fill: "none", "stroke-width": 1.8, "stroke-linecap": "round" },
617
+ children: [
618
+ { kind: "path", role: "a", attrs: { d: "M18 12a8 8 0 0 1 0 8" } },
619
+ { kind: "path", role: "a", attrs: { d: "M22 9.5a12 12 0 0 1 0 13" } },
620
+ { kind: "path", role: "a", attrs: { d: "M26 7.5a16 16 0 0 1 0 17" } },
621
+ ],
622
+ },
623
+ ],
624
+ },
625
+ cash: {
626
+ viewBox: "0 0 48 32",
627
+ label: "Cash",
628
+ colors: { a: "#2E7D32", b: "#A5D6A7", ink: "#A5D6A7" },
629
+ shapes: [
630
+ ...cardFrame("#E8F5E9"),
631
+ { kind: "rect", role: "a", attrs: { x: 9, y: 9, width: 30, height: 14, rx: 2 } },
632
+ {
633
+ kind: "circle",
634
+ role: "b",
635
+ attrs: { cx: 24, cy: 16, r: 4, fill: "none", "stroke-width": 1.4 },
636
+ },
637
+ {
638
+ kind: "text",
639
+ role: "ink",
640
+ text: "$",
641
+ attrs: {
642
+ x: 24,
643
+ y: 18.5,
644
+ "font-family": FONT,
645
+ "font-size": 6,
646
+ "font-weight": 700,
647
+ "text-anchor": "middle",
648
+ },
649
+ },
650
+ ],
651
+ },
652
+ "bank-building": {
653
+ viewBox: "0 0 48 32",
654
+ label: "Bank",
655
+ colors: { a: "#2A3A5A" },
656
+ shapes: [
657
+ ...cardFrame("#EEF1F6"),
658
+ {
659
+ kind: "g",
660
+ role: "a",
661
+ attrs: {
662
+ fill: "none",
663
+ "stroke-width": 1.6,
664
+ "stroke-linecap": "round",
665
+ "stroke-linejoin": "round",
666
+ },
667
+ children: [
668
+ { kind: "path", role: "a", attrs: { d: "M14 14l10-5 10 5" } },
669
+ { kind: "path", role: "a", attrs: { d: "M16 14v7" } },
670
+ { kind: "path", role: "a", attrs: { d: "M21 14v7" } },
671
+ { kind: "path", role: "a", attrs: { d: "M27 14v7" } },
672
+ { kind: "path", role: "a", attrs: { d: "M32 14v7" } },
673
+ { kind: "path", role: "a", attrs: { d: "M13 23h22" } },
674
+ ],
675
+ },
676
+ ],
677
+ },
678
+ };
679
+ /** All brand-mark names, in catalogue order. */
680
+ export const BRAND_MARK_NAMES = [
681
+ "visa",
682
+ "mastercard",
683
+ "amex",
684
+ "discover",
685
+ "unionpay",
686
+ "western-union",
687
+ "moneygram",
688
+ "numo",
689
+ "moamalat",
690
+ "lypay",
691
+ "onepay",
692
+ "sadad",
693
+ "tadawul",
694
+ "apple-pay",
695
+ "google-pay",
696
+ "paypal",
697
+ "swift",
698
+ "mada",
699
+ "generic-card",
700
+ "contactless-pay",
701
+ "cash",
702
+ "bank-building",
703
+ ];
704
+ /** True when `name` is a known brand mark. Acts as a type guard. */
705
+ export function isBrandMarkName(name) {
706
+ return Object.prototype.hasOwnProperty.call(MARK_DEFS, name);
707
+ }
708
+ // ── Rendering ───────────────────────────────────────────────────────────────
709
+ const escapeAttr = (v) => v.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
710
+ const escapeText = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
711
+ /** The single SVG tag for a shape kind ('g' has children, 'text' has a body). */
712
+ const SELF_CLOSING = {
713
+ rect: true,
714
+ circle: true,
715
+ ellipse: true,
716
+ path: true,
717
+ line: true,
718
+ text: false,
719
+ g: false,
720
+ };
721
+ /** A rect that fills the whole canvas — a coloured card body / backdrop. */
722
+ function isFullBleedBackdrop(shape, def) {
723
+ if (shape.kind !== "rect")
724
+ return false;
725
+ const [, , vbW, vbH] = def.viewBox.split(" ").map(Number);
726
+ const w = Number(shape.attrs.width);
727
+ const h = Number(shape.attrs.height);
728
+ // Anchored at the origin (no x/y, or 0) and spanning the full viewBox.
729
+ const x = Number(shape.attrs.x ?? 0);
730
+ const y = Number(shape.attrs.y ?? 0);
731
+ return x === 0 && y === 0 && w === vbW && h === vbH;
732
+ }
733
+ /**
734
+ * Resolve the paint attributes a shape gets in a given variant.
735
+ * - color → brand colour by role; strokes (fill:none in attrs) keep stroke=colour.
736
+ * - mono → everything is a flat `currentColor` silhouette (fill); neutral
737
+ * frames AND full-bleed coloured backdrops are dropped so the inked
738
+ * glyph reads as the silhouette (e.g. AMEX/SWIFT lettering).
739
+ * - outline → stroke="currentColor", fill="none", round joins; frames/backdrops
740
+ * dropped.
741
+ * Returns null when the shape should be omitted in this variant.
742
+ */
743
+ function paintFor(shape, variant, def) {
744
+ const isStrokeShape = shape.attrs.fill === "none" || shape.attrs["stroke-width"] !== undefined;
745
+ const isFrame = shape.role === "bg" || shape.role === "hairline";
746
+ if (variant === "color") {
747
+ const out = {};
748
+ if (isFrame) {
749
+ // Neutral frame keeps its literal colours (bg fill / hairline stroke).
750
+ if (shape.role === "bg") {
751
+ out.fill = shape.attrs.fill ?? CARD_BG;
752
+ }
753
+ else {
754
+ out.fill = "none";
755
+ out.stroke = CARD_HAIRLINE;
756
+ }
757
+ return out;
758
+ }
759
+ const brand = def.colors[shape.role] ?? shape.attrs.fill ?? "#000000";
760
+ if (isStrokeShape) {
761
+ out.fill = "none";
762
+ out.stroke = brand;
763
+ }
764
+ else {
765
+ out.fill = brand;
766
+ }
767
+ return out;
768
+ }
769
+ // mono + outline: drop the neutral frame and any full-bleed coloured backdrop,
770
+ // so the foreground glyph (text/paths) becomes the silhouette.
771
+ if (isFrame || isFullBleedBackdrop(shape, def))
772
+ return null;
773
+ if (variant === "mono") {
774
+ // Flatten to a single currentColor silhouette. Stroke shapes stay strokes
775
+ // (so line-art marks like contactless/bank still read), filled shapes fill.
776
+ if (isStrokeShape) {
777
+ return { fill: "none", stroke: "currentColor" };
778
+ }
779
+ return { fill: "currentColor" };
780
+ }
781
+ // outline
782
+ return { fill: "none", stroke: "currentColor" };
783
+ }
784
+ /** Stroke-width to use for a shape in mono/outline (keeps line marks tidy). */
785
+ function strokeWidthFor(shape, variant) {
786
+ if (variant === "color") {
787
+ return shape.attrs["stroke-width"];
788
+ }
789
+ // For native stroke shapes, keep their authored weight; otherwise use 1.8.
790
+ const authored = shape.attrs["stroke-width"];
791
+ if (typeof authored === "number")
792
+ return authored;
793
+ return variant === "outline" ? 1.8 : authored === undefined ? undefined : Number(authored);
794
+ }
795
+ // Attrs that are PAINT (resolved per variant) rather than geometry/typography.
796
+ const PAINT_KEYS = new Set([
797
+ "fill",
798
+ "stroke",
799
+ "fill-opacity",
800
+ "stroke-opacity",
801
+ "stroke-width",
802
+ "stroke-linecap",
803
+ "stroke-linejoin",
804
+ ]);
805
+ /**
806
+ * Render one shape to an SVG element string for the given variant.
807
+ *
808
+ * `inGroup` marks a child of a <g>: the group already carries the resolved
809
+ * paint, so children emit ONLY geometry and INHERIT fill/stroke. This keeps
810
+ * grouped line-art (contactless, bank) as strokes in every variant instead of
811
+ * accidentally giving each child a brand fill.
812
+ */
813
+ function renderShape(shape, variant, def, inGroup = false) {
814
+ const paint = inGroup ? {} : paintFor(shape, variant, def);
815
+ if (paint === null)
816
+ return "";
817
+ const merged = {};
818
+ for (const [k, v] of Object.entries(shape.attrs)) {
819
+ if (PAINT_KEYS.has(k))
820
+ continue;
821
+ merged[k] = v;
822
+ }
823
+ // Apply resolved paint (skipped for group children — they inherit).
824
+ for (const [k, v] of Object.entries(paint))
825
+ merged[k] = v;
826
+ if (!inGroup) {
827
+ // Stroke niceties for line shapes / mono / outline.
828
+ const sw = strokeWidthFor(shape, variant);
829
+ if (sw !== undefined && (merged.stroke !== undefined || merged.fill === "none")) {
830
+ merged["stroke-width"] = sw;
831
+ }
832
+ if ((variant === "outline" || (variant === "mono" && merged.stroke === "currentColor")) &&
833
+ merged.stroke !== undefined) {
834
+ merged["stroke-linecap"] = shape.attrs["stroke-linecap"] ?? "round";
835
+ merged["stroke-linejoin"] = shape.attrs["stroke-linejoin"] ?? "round";
836
+ }
837
+ // Carry through opacities that aren't a brand colour.
838
+ if (shape.attrs["fill-opacity"] !== undefined && merged.fill !== "none") {
839
+ merged["fill-opacity"] = shape.attrs["fill-opacity"];
840
+ }
841
+ if (shape.attrs["stroke-opacity"] !== undefined && merged.stroke !== undefined) {
842
+ merged["stroke-opacity"] = shape.attrs["stroke-opacity"];
843
+ }
844
+ }
845
+ const attrStr = Object.entries(merged)
846
+ .map(([k, v]) => `${k}="${typeof v === "string" ? escapeAttr(v) : v}"`)
847
+ .join(" ");
848
+ if (shape.kind === "g") {
849
+ const inner = (shape.children ?? []).map((c) => renderShape(c, variant, def, true)).join("");
850
+ return `<g ${attrStr}>${inner}</g>`;
851
+ }
852
+ if (shape.kind === "text") {
853
+ return `<text ${attrStr}>${escapeText(shape.text ?? "")}</text>`;
854
+ }
855
+ if (SELF_CLOSING[shape.kind]) {
856
+ return `<${shape.kind} ${attrStr}/>`;
857
+ }
858
+ return `<${shape.kind} ${attrStr}></${shape.kind}>`;
859
+ }
860
+ const OVERRIDES = new Map();
861
+ /**
862
+ * Register a brand's OFFICIAL, licensed SVG, overriding the bundled placeholder.
863
+ *
864
+ * Licensed users SHOULD call this to render approved official artwork:
865
+ * registerBrandMark("visa", officialVisaSvg); // all variants
866
+ * registerBrandMark("visa", { color, mono, outline }); // per-variant
867
+ *
868
+ * After registration, brandMarkSvg(name, { variant }) returns the override,
869
+ * falling back across variants when only some are provided
870
+ * (color → mono → outline, whichever exists). <npt-brand-mark> uses it too.
871
+ *
872
+ * `name` is typed as BrandMarkName | string so you may also register your own
873
+ * additional brands (then render them via brandMarkSvg(yourName)).
874
+ */
875
+ export function registerBrandMark(name, svg) {
876
+ const entry = typeof svg === "string" ? { color: svg, mono: svg, outline: svg } : { ...svg };
877
+ OVERRIDES.set(name, entry);
878
+ }
879
+ /** Remove a previously registered override (mainly for tests/tooling). */
880
+ export function unregisterBrandMark(name) {
881
+ OVERRIDES.delete(name);
882
+ }
883
+ /** True when an official-asset override has been registered for `name`. */
884
+ export function hasBrandMarkOverride(name) {
885
+ return OVERRIDES.has(name);
886
+ }
887
+ /** Pick the best override string for a variant, falling back across variants. */
888
+ function resolveOverride(entry, variant) {
889
+ const order = variant === "color"
890
+ ? ["color", "mono", "outline"]
891
+ : variant === "mono"
892
+ ? ["mono", "color", "outline"]
893
+ : ["outline", "color", "mono"];
894
+ for (const v of order) {
895
+ const s = entry[v];
896
+ if (s)
897
+ return s;
898
+ }
899
+ return undefined;
900
+ }
901
+ /** Inject/replace width & height on an <svg> head, sized to `height` by aspect. */
902
+ function sizeSvg(svg, height) {
903
+ const vb = svg.match(/viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/);
904
+ const vbW = vb ? Number(vb[1]) : 48;
905
+ const vbH = vb ? Number(vb[2]) : 32;
906
+ const width = Math.round(((height * vbW) / vbH) * 100) / 100;
907
+ const gt = svg.indexOf(">");
908
+ let head = svg.slice(0, gt);
909
+ head = head.replace(/\s(?:width|height)="[^"]*"/g, "");
910
+ head += ` width="${width}" height="${height}"`;
911
+ return head + svg.slice(gt);
912
+ }
913
+ /**
914
+ * Build the complete <svg> for a placeholder mark in a given variant.
915
+ */
916
+ function buildMarkSvg(name, variant, cls) {
917
+ const def = MARK_DEFS[name];
918
+ const [, , w, h] = def.viewBox.split(" ");
919
+ void w;
920
+ void h;
921
+ const body = def.shapes.map((s) => renderShape(s, variant, def)).join("");
922
+ const classAttr = cls ? ` class="${escapeAttr(cls)}"` : "";
923
+ const placeholder = def.placeholder ? ' data-placeholder="true"' : "";
924
+ // mono/outline silhouettes paint with currentColor — round joins are global.
925
+ const lineAttrs = variant === "outline"
926
+ ? ' fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"'
927
+ : "";
928
+ return (`<svg xmlns="http://www.w3.org/2000/svg"${classAttr} viewBox="${def.viewBox}" ` +
929
+ `role="img" aria-label="${escapeAttr(def.label)}" ` +
930
+ `data-npt-brand-mark="${escapeAttr(name)}" data-variant="${variant}"${placeholder}${lineAttrs}>` +
931
+ `${body}</svg>`);
932
+ }
933
+ /**
934
+ * Return the complete <svg> for `name`, in the requested variant, sized to a
935
+ * height. Aspect ratio is preserved from the mark's intrinsic viewBox.
936
+ *
937
+ * If an official asset was registered via registerBrandMark(), that override is
938
+ * returned (with width/height sized to `height`), falling back across variants.
939
+ *
940
+ * @throws RangeError when `name` is not a known BrandMarkName and has no override.
941
+ */
942
+ export function brandMarkSvg(name, opts = {}) {
943
+ const variant = opts.variant ?? "color";
944
+ // 1) Official override wins.
945
+ const override = OVERRIDES.get(name);
946
+ if (override) {
947
+ const raw = resolveOverride(override, variant);
948
+ if (raw)
949
+ return opts.height === undefined ? raw : sizeSvg(raw, opts.height);
950
+ }
951
+ // 2) Bundled placeholder.
952
+ if (!isBrandMarkName(name)) {
953
+ throw new RangeError(`Unknown Neptune brand mark: "${String(name)}"`);
954
+ }
955
+ const svg = buildMarkSvg(name, variant, opts.class);
956
+ return opts.height === undefined ? svg : sizeSvg(svg, opts.height);
957
+ }
958
+ /**
959
+ * BRAND_MARKS — name → complete multicolour ("color" variant) <svg> string.
960
+ * Kept as a convenience map (back-compat). For mono/outline or overrides, use
961
+ * brandMarkSvg(name, { variant }).
962
+ */
963
+ export const BRAND_MARKS = BRAND_MARK_NAMES.reduce((acc, name) => {
964
+ acc[name] = buildMarkSvg(name, "color");
965
+ return acc;
966
+ }, {});
967
+ //# sourceMappingURL=brand-marks.js.map