@neptune.fintech/icons 2.1.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.
@@ -8,147 +8,673 @@
8
8
  // TRADEMARKS owned by their respective owners. They are provided here ONLY as
9
9
  // SIMPLIFIED, SCHEMATIC IDENTIFICATION MARKS / PLACEHOLDERS so a UI can label a
10
10
  // payment method — they are deliberately NOT pixel-exact reproductions of any
11
- // official logo.
11
+ // official logo, and they are NOT traced from any official artwork. They are
12
+ // ORIGINAL, clean geometric placeholders authored for Neptune Odyssey.
12
13
  //
13
- // These marks are NOT original Neptune Odyssey artwork and are NOT licensed
14
- // under the Neptune Odyssey Community License. They are kept SEPARATE from the
15
- // monochrome ICONS set on purpose. In production, replace each with the brand's
16
- // OFFICIAL asset and follow that brand's brand/usage guidelines.
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.
17
18
  //
18
19
  // The Libyan / local marks (NUMO, Moamalat, LyPay, OnePay, Sadad, Tadawul) are
19
- // NEUTRAL PLACEHOLDERS (a simple badge + initials) — we do not ship their real
20
- // assets. Replace them with official artwork before shipping.
20
+ // NEUTRAL PLACEHOLDERS (a simple badge + initials/motif) — we do not ship their
21
+ // real assets. Replace them with official artwork before shipping.
21
22
  //
22
- // Unlike the stroke ICONS, brand marks are multicolour: each value is a
23
- // COMPLETE <svg> string (with its own viewBox), not just inner markup.
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";
24
46
  /**
25
- * name → complete multicolour <svg> string.
26
- *
27
- * Card-ratio canvas (viewBox 0 0 48 32) for wide marks; 32×32 for square
28
- * badges. A neutral card body (#F4F5F7 fill, #E1E3E8 hairline) frames most
29
- * marks so they sit well on any surface.
47
+ * name → mark definition. Each is an ORIGINAL simplified geometric placeholder
48
+ * recognisable by the brand's colours/basic forms — never traced artwork.
30
49
  */
31
- export const BRAND_MARKS = {
50
+ const MARK_DEFS = {
32
51
  // ── Card networks ───────────────────────────────────────────────────
33
- visa: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Visa" data-npt-brand-mark="visa">' +
34
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
35
- '<text x="24" y="21.5" font-family="Arial, Helvetica, sans-serif" font-size="13" font-style="italic" font-weight="700" fill="#1A1F71" text-anchor="middle" letter-spacing="0.5">VISA</text>' +
36
- '</svg>',
37
- mastercard: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Mastercard" data-npt-brand-mark="mastercard">' +
38
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
39
- '<circle cx="20" cy="16" r="8" fill="#EB001B"/><circle cx="28" cy="16" r="8" fill="#F79E1B"/>' +
40
- '<path d="M24 9.7a8 8 0 0 0 0 12.6 8 8 0 0 0 0-12.6Z" fill="#FF5F00"/>' +
41
- '</svg>',
42
- amex: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="American Express" data-npt-brand-mark="amex">' +
43
- '<rect width="48" height="32" rx="4" fill="#2E77BC"/>' +
44
- '<rect x="6" y="9" width="36" height="14" rx="2" fill="#FFFFFF" fill-opacity="0.12"/>' +
45
- '<text x="24" y="20.5" font-family="Arial, Helvetica, sans-serif" font-size="11" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="1">AMEX</text>' +
46
- '</svg>',
47
- discover: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Discover" data-npt-brand-mark="discover">' +
48
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
49
- '<text x="21" y="20.5" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="700" fill="#231F20" text-anchor="middle" letter-spacing="0.3">DISC</text>' +
50
- '<circle cx="36" cy="17" r="6" fill="#F58220"/>' +
51
- '</svg>',
52
- unionpay: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="UnionPay" data-npt-brand-mark="unionpay">' +
53
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
54
- '<rect x="10" y="7" width="9" height="18" rx="2" fill="#E21836"/>' +
55
- '<rect x="19" y="7" width="9" height="18" rx="2" fill="#00447C"/>' +
56
- '<rect x="28" y="7" width="9" height="18" rx="2" fill="#007B84"/>' +
57
- '<text x="24" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="6" font-weight="700" fill="#FFFFFF" text-anchor="middle">UPAY</text>' +
58
- '</svg>',
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
+ },
59
164
  // ── Money transfer ──────────────────────────────────────────────────
60
- "western-union": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Western Union" data-npt-brand-mark="western-union">' +
61
- '<rect width="48" height="32" rx="4" fill="#FFDD00"/>' +
62
- '<text x="24" y="14.5" font-family="Arial, Helvetica, sans-serif" font-size="7" font-weight="700" fill="#000000" text-anchor="middle" letter-spacing="0.5">WESTERN</text>' +
63
- '<text x="24" y="23.5" font-family="Arial, Helvetica, sans-serif" font-size="7" font-weight="700" fill="#000000" text-anchor="middle" letter-spacing="0.5">UNION</text>' +
64
- '</svg>',
65
- moneygram: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="MoneyGram" data-npt-brand-mark="moneygram">' +
66
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
67
- '<circle cx="13" cy="16" r="5" fill="#E51937"/>' +
68
- '<text x="29" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="7" font-weight="700" fill="#E51937" text-anchor="middle">MGRAM</text>' +
69
- '</svg>',
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
+ },
70
223
  // ── Libyan / local — PLACEHOLDERS (replace with official assets) ─────
71
- numo: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="NUMO (placeholder mark)" data-npt-brand-mark="numo" data-placeholder="true">' +
72
- '<rect width="48" height="32" rx="4" fill="#0E2A47"/>' +
73
- '<rect x="6" y="8" width="36" height="16" rx="3" fill="none" stroke="#5AA9E6" stroke-width="1.4"/>' +
74
- '<text x="24" y="21" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="1.5">NUMO</text>' +
75
- '</svg>',
76
- moamalat: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Moamalat (placeholder mark)" data-npt-brand-mark="moamalat" data-placeholder="true">' +
77
- '<rect width="48" height="32" rx="4" fill="#1C7A4D"/>' +
78
- '<circle cx="13" cy="16" r="6" fill="none" stroke="#FFFFFF" stroke-width="1.4"/>' +
79
- '<text x="13" y="19" font-family="Arial, Helvetica, sans-serif" font-size="8" font-weight="700" fill="#FFFFFF" text-anchor="middle">M</text>' +
80
- '<text x="30" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="7" font-weight="700" fill="#FFFFFF" text-anchor="middle">MOAM</text>' +
81
- '</svg>',
82
- lypay: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="LyPay (placeholder mark)" data-npt-brand-mark="lypay" data-placeholder="true">' +
83
- '<rect width="48" height="32" rx="4" fill="#0B6E4F"/>' +
84
- '<rect x="7" y="9" width="34" height="14" rx="7" fill="#FFFFFF" fill-opacity="0.1"/>' +
85
- '<text x="24" y="21" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="0.5">LyPay</text>' +
86
- '</svg>',
87
- onepay: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="OnePay (placeholder mark)" data-npt-brand-mark="onepay" data-placeholder="true">' +
88
- '<rect width="48" height="32" rx="4" fill="#243B6B"/>' +
89
- '<circle cx="13" cy="16" r="6.5" fill="none" stroke="#F5A623" stroke-width="1.6"/>' +
90
- '<text x="13" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="700" fill="#F5A623" text-anchor="middle">1</text>' +
91
- '<text x="30" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="7" font-weight="700" fill="#FFFFFF" text-anchor="middle">PAY</text>' +
92
- '</svg>',
93
- sadad: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Sadad (placeholder mark)" data-npt-brand-mark="sadad" data-placeholder="true">' +
94
- '<rect width="48" height="32" rx="4" fill="#5B2E91"/>' +
95
- '<rect x="7" y="9" width="34" height="14" rx="3" fill="#FFFFFF" fill-opacity="0.1"/>' +
96
- '<text x="24" y="21" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="1">SADAD</text>' +
97
- '</svg>',
98
- tadawul: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Tadawul (placeholder mark)" data-npt-brand-mark="tadawul" data-placeholder="true">' +
99
- '<rect width="48" height="32" rx="4" fill="#1A4D4D"/>' +
100
- '<path d="M9 20l5-5 4 3 6-7" fill="none" stroke="#3FC1C9" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>' +
101
- '<text x="32" y="20" font-family="Arial, Helvetica, sans-serif" font-size="6" font-weight="700" fill="#FFFFFF" text-anchor="middle">TDWL</text>' +
102
- '</svg>',
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
+ },
103
450
  // ── Wallets / generic ───────────────────────────────────────────────
104
- "apple-pay": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Apple Pay" data-npt-brand-mark="apple-pay">' +
105
- '<rect width="48" height="32" rx="4" fill="#FFFFFF"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
106
- '<path 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" fill="#000000"/>' +
107
- '<text x="33" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="600" fill="#000000" text-anchor="middle">Pay</text>' +
108
- '</svg>',
109
- "google-pay": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Google Pay" data-npt-brand-mark="google-pay">' +
110
- '<rect width="48" height="32" rx="4" fill="#FFFFFF"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
111
- '<path 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" fill="#4285F4"/>' +
112
- '<path d="M19 16.2h-3v2.1h3a3 3 0 0 0 .1-.8 4 4 0 0 0-.1-1.3Z" fill="#34A853"/>' +
113
- '<text x="33" y="19.5" font-family="Arial, Helvetica, sans-serif" font-size="9" font-weight="600" fill="#5F6368" text-anchor="middle">Pay</text>' +
114
- '</svg>',
115
- paypal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="PayPal" data-npt-brand-mark="paypal">' +
116
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
117
- '<path 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" fill="#003087"/>' +
118
- '<path 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" fill="#009CDE"/>' +
119
- '<text x="34" y="20" font-family="Arial, Helvetica, sans-serif" font-size="7" font-weight="700" fill="#003087" text-anchor="middle">Pal</text>' +
120
- '</svg>',
121
- swift: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="SWIFT" data-npt-brand-mark="swift">' +
122
- '<rect width="48" height="32" rx="4" fill="#0033A0"/>' +
123
- '<text x="24" y="20.5" font-family="Arial, Helvetica, sans-serif" font-size="10" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="1.5">SWIFT</text>' +
124
- '</svg>',
125
- mada: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="mada-style domestic scheme (generic)" data-npt-brand-mark="mada" data-placeholder="true">' +
126
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
127
- '<rect x="9" y="13" width="14" height="6" rx="3" fill="#84BD00"/>' +
128
- '<rect x="25" y="13" width="14" height="6" rx="3" fill="#1F3661"/>' +
129
- '</svg>',
130
- "generic-card": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Card" data-npt-brand-mark="generic-card">' +
131
- '<rect width="48" height="32" rx="4" fill="#3C4858"/>' +
132
- '<rect x="6" y="11" width="8" height="6" rx="1.2" fill="#E8C56B"/>' +
133
- '<path d="M6 22h20" stroke="#FFFFFF" stroke-width="1.4" stroke-linecap="round" stroke-opacity="0.7"/>' +
134
- '<path d="M30 22h12" stroke="#FFFFFF" stroke-width="1.4" stroke-linecap="round" stroke-opacity="0.4"/>' +
135
- '</svg>',
136
- "contactless-pay": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Contactless payment" data-npt-brand-mark="contactless-pay">' +
137
- '<rect width="48" height="32" rx="4" fill="#F4F5F7"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
138
- '<g fill="none" stroke="#1F6FEB" stroke-width="1.8" stroke-linecap="round">' +
139
- '<path d="M18 12a8 8 0 0 1 0 8"/><path d="M22 9.5a12 12 0 0 1 0 13"/><path d="M26 7.5a16 16 0 0 1 0 17"/></g>' +
140
- '</svg>',
141
- cash: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Cash" data-npt-brand-mark="cash">' +
142
- '<rect width="48" height="32" rx="4" fill="#E8F5E9"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#C8E6C9"/>' +
143
- '<rect x="9" y="9" width="30" height="14" rx="2" fill="#2E7D32"/>' +
144
- '<circle cx="24" cy="16" r="4" fill="none" stroke="#A5D6A7" stroke-width="1.4"/>' +
145
- '<text x="24" y="18.5" font-family="Arial, Helvetica, sans-serif" font-size="6" font-weight="700" fill="#A5D6A7" text-anchor="middle">$</text>' +
146
- '</svg>',
147
- "bank-building": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32" role="img" aria-label="Bank" data-npt-brand-mark="bank-building">' +
148
- '<rect width="48" height="32" rx="4" fill="#EEF1F6"/><rect x="0.5" y="0.5" width="47" height="31" rx="3.5" fill="none" stroke="#E1E3E8"/>' +
149
- '<g fill="none" stroke="#2A3A5A" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
150
- '<path d="M14 14l10-5 10 5"/><path d="M16 14v7"/><path d="M21 14v7"/><path d="M27 14v7"/><path d="M32 14v7"/><path d="M13 23h22"/></g>' +
151
- '</svg>',
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
+ },
152
678
  };
153
679
  /** All brand-mark names, in catalogue order. */
154
680
  export const BRAND_MARK_NAMES = [
@@ -177,29 +703,265 @@ export const BRAND_MARK_NAMES = [
177
703
  ];
178
704
  /** True when `name` is a known brand mark. Acts as a type guard. */
179
705
  export function isBrandMarkName(name) {
180
- return Object.prototype.hasOwnProperty.call(BRAND_MARKS, 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;
181
732
  }
182
733
  /**
183
- * Return the complete multicolour <svg> for `name`, sized to a given height.
184
- * Aspect ratio is preserved from the mark's intrinsic viewBox.
185
- * @throws RangeError when `name` is not a known BrandMarkName.
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.
186
742
  */
187
- export function brandMarkSvg(name, opts = {}) {
188
- if (!isBrandMarkName(name)) {
189
- throw new RangeError(`Unknown Neptune brand mark: "${String(name)}"`);
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"];
190
788
  }
191
- const svg = BRAND_MARKS[name];
192
- if (opts.height === undefined)
193
- return svg;
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) {
194
903
  const vb = svg.match(/viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/);
195
904
  const vbW = vb ? Number(vb[1]) : 48;
196
905
  const vbH = vb ? Number(vb[2]) : 32;
197
- const height = opts.height;
198
- const width = Math.round((height * vbW) / vbH * 100) / 100;
199
- // Inject width/height after the opening "<svg" — replace any existing ones.
200
- let head = svg.slice(0, svg.indexOf(">"));
906
+ const width = Math.round(((height * vbW) / vbH) * 100) / 100;
907
+ const gt = svg.indexOf(">");
908
+ let head = svg.slice(0, gt);
201
909
  head = head.replace(/\s(?:width|height)="[^"]*"/g, "");
202
910
  head += ` width="${width}" height="${height}"`;
203
- return head + svg.slice(svg.indexOf(">"));
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);
204
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
+ }, {});
205
967
  //# sourceMappingURL=brand-marks.js.map