@lovett/ui 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -131,6 +131,34 @@
131
131
  box-shadow: var(--ring-focus);
132
132
  }
133
133
 
134
+ /* ─────────────────────────────────────────────────────────────────────────
135
+ * BASELINE FOCUS INDICATOR
136
+ *
137
+ * Until this existed, only elements that opted in — primitives via .btn /
138
+ * .input-shell, and hand-written `focus-visible:` classes — showed OUR focus
139
+ * ring. Everything else fell through to the browser's `outline: auto`, which
140
+ * on macOS paints in the USER'S SYSTEM ACCENT COLOUR. So a keyboard user with
141
+ * an amber system accent saw an amber ring on most controls and our red ring
142
+ * on a few, in the same tab sequence. Measured in the Generate lens: 115
143
+ * buttons, 35 with a ring.
144
+ *
145
+ * `:where()` gives this ZERO specificity, so it is a floor, not an override —
146
+ * .btn, .input-shell, and any component or utility class still win.
147
+ * ───────────────────────────────────────────────────────────────────────── */
148
+ @layer base {
149
+ :where(a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex^='-'])):focus-visible {
150
+ /* OUTLINE, not box-shadow, deliberately. Tailwind's shadow/ring utilities
151
+ compose onto `box-shadow`, so a zero-specificity box-shadow baseline loses
152
+ to any element carrying one (e.g. a `ring-1` selected state) and silently
153
+ renders nothing. `outline` is untouched by that chain, follows
154
+ border-radius in every modern browser, and matches --ring-focus's geometry.
155
+ Elements with their own ring set `outline: none` alongside it, so they
156
+ override this cleanly rather than double-drawing. */
157
+ outline: 2px solid rgb(var(--accent));
158
+ outline-offset: 0;
159
+ }
160
+ }
161
+
134
162
  .btn-primary {
135
163
  background: rgb(var(--accent));
136
164
  color: white;
package/dist/tokens.css CHANGED
@@ -49,6 +49,9 @@
49
49
  * colored backgrounds — switches,
50
50
  * sliders, badges-on-accent)
51
51
  * --muted (muted fill for ghost UI)
52
+ * --surface-subtle (lightest structural tint — header bands
53
+ * and zebra on an already-bordered
54
+ * surface; 1.04:1, matched across themes)
52
55
  * --header-height (sticky AppHeader height — referenced
53
56
  * by sticky chip bars; same value lives
54
57
  * in src/lib/layout.ts for JS callsites)
@@ -92,6 +95,41 @@
92
95
  --accent-muted: 207 14 15 / 0.12;
93
96
  --accent-subtle: 207 14 15 / 0.06;
94
97
  --accent-glow: 207 14 15 / 0.25;
98
+ /* Ink ON an accent fill. PUBLIC, and theme-independent because --accent is:
99
+ the brand red does not change between light and dark, so its foreground
100
+ must not either. Measures 5.64:1 on --accent.
101
+ Added 2026-08-29 — needs a token ADR (CLAUDE.md §4). It closes a real gap:
102
+ app code had three different answers for "text on accent" (a `text-white`
103
+ literal, the INTERNAL --primary-foreground, and a reference to this very
104
+ name with an inline fallback), and one of them — `rgb(var(--foreground))`
105
+ — inverted to near-black on red at 3.34:1 in light mode. The public
106
+ --accent family had every value EXCEPT its foreground, so lens code had
107
+ nothing correct to reach for. */
108
+ --accent-foreground: 255 255 255;
109
+ /* Accent as TEXT on a normal surface — a different problem from
110
+ --accent-foreground (ink ON an accent fill). PUBLIC, and unlike --accent it
111
+ IS theme-tuned: the brand red measures 5.64:1 on a light card but only
112
+ 3.05:1 on a dark one, so an accent-coloured LABEL is legible in light and
113
+ fails AA in dark. The dark value is the lifted red that clears it (4.57:1).
114
+ Use for selected-state labels, accent links, accent metric values — never
115
+ as a fill. */
116
+ --accent-ink: 239 68 68;
117
+
118
+ /* SEMANTIC INK — PUBLIC. Same problem as --accent-ink, same shape of answer.
119
+ `--success` / `--warning` are sized to work as FILLS and as icon strokes;
120
+ used as TEXT ON THEIR OWN 10-12% TINT they are only legible in one theme.
121
+ Measured over the card: success reads 5.64:1 dark but 3.33:1 light, and
122
+ warning 6.46:1 dark but 2.86:1 light — both fail AA for body text in light
123
+ mode, which is exactly where the generate lens's tip panels and word-count
124
+ chips live. Dark keeps the base colour; light gets a darkened ink that
125
+ clears 4.5:1 while still reading green and amber rather than near-black.
126
+ Text only — never a fill, and never a border. */
127
+ --success-ink: var(--success);
128
+ --warning-ink: var(--warning);
129
+ /* Destructive is the one that fails in BOTH themes as text on its own tint
130
+ (3.53:1 light, 3.67:1 dark), so unlike the other two the dark value is not
131
+ the base — it is lifted 18% toward white to clear 5.11:1 / 4.51:1. */
132
+ --destructive-ink: 242 102 102;
95
133
 
96
134
  /* ============================================================
97
135
  HIERARCHY MAP PALETTE — PUBLIC, theme-INDEPENDENT.
@@ -167,6 +205,17 @@
167
205
  --folder-slate: 148 163 184;
168
206
  --folder-ink: 203 213 225;
169
207
 
208
+ /* OPAQUE categorical tints — the chip/pill FILL for a --folder-* hue.
209
+ Pre-composited (hue at 0.12 over --surface-card) so a chip needs no
210
+ alpha and therefore does not change appearance when the surface
211
+ under it changes. Only the two hues with consumers are defined;
212
+ extend per hue as consumers land rather than all nine up front.
213
+ NOTE: the hue itself is a FILL/GRAPHIC value, not an ink — e.g.
214
+ --folder-amber on its own tint measures 2.86:1 in light. Chip TEXT
215
+ is --foreground; the tint + border carry the category. */
216
+ --folder-amber-tint: 53 43 28;
217
+ --folder-blue-tint: 31 39 56;
218
+
170
219
  /* ============================================================
171
220
  SIDEBAR — INTERNAL (only <Sidebar/> reads these)
172
221
  ============================================================ */
@@ -298,8 +347,18 @@
298
347
  --shadow-xl: 0 16px 40px rgb(0 0 0 / 0.55);
299
348
  --shadow-2xl: 0 24px 60px rgb(0 0 0 / 0.60);
300
349
 
301
- --ring-focus: 0 0 0 3px rgb(207 14 15 / 0.25);
302
- --ring-error: 0 0 0 3px rgb(239 68 68 / 0.25);
350
+ /* Thin and SOLID, not a soft halo and this is a legibility fix as much as
351
+ a look. The old 3px @ 0.25/0.20 composited to rgb(245 207 207) on a light
352
+ card and rgb(63 24 27) on a dark one: 1.43:1 and 1.11:1 against the surface
353
+ it sits on, against the 3:1 that WCAG 2.2 SC 2.4.11 (Focus Appearance)
354
+ asks of a focus indicator. Alpha was measured across the range and NOTHING
355
+ below fully-opaque clears 3:1 in dark mode, so the ring is solid: 5.64:1
356
+ light, 3.05:1 dark. 2px keeps the >=2px perimeter the same SC requires
357
+ while reading crisper than the 3px glow it replaces.
358
+ Both resolve through the theme-aware tokens, so the light block needs no
359
+ override — var() resolves at use time. */
360
+ --ring-focus: 0 0 0 2px rgb(var(--accent));
361
+ --ring-error: 0 0 0 2px rgb(var(--destructive));
303
362
 
304
363
  --radius-xs: 6px;
305
364
 
@@ -339,6 +398,18 @@
339
398
  --surface-card: 27 27 30;
340
399
  --surface-inset: 45 45 48;
341
400
  --surface-hover: 55 55 59;
401
+ /* THE LIGHTEST STRUCTURAL TINT — PUBLIC. A header band or zebra stripe on a
402
+ surface that ALREADY HAS A BORDER, where the fill only has to hint that a
403
+ row is chrome rather than content.
404
+ Measured: the border alone is 1.25:1 in dark and 1.32:1 in light, stronger
405
+ than any fill step in the neutral ramp — so the fill is decoration on top
406
+ of structure that already reads, and it can go very light indeed.
407
+ Sized by OKLCH lightness so the two themes are the SAME design: ΔL 0.017
408
+ dark, 0.014 light (1.045:1 / 1.042:1). That symmetry is the point. Using
409
+ --muted for this job gave ΔL 0.075 in dark against 0.038 in light — twice
410
+ the step in one theme, which is why a header that looked right in dark read
411
+ as a grey slab in light. */
412
+ --surface-subtle: 31 31 34;
342
413
  /* A control you can act on — input, select, option tile, chip-button.
343
414
  Sits at CARD level on purpose: in light that is white against the
344
415
  frame behind it, which reads "type here"; in dark it is raised above
@@ -389,6 +460,20 @@
389
460
  [data-theme="light"], [data-theme="combo"] {
390
461
  /* Brand accent — base stays, hover darkens, glow softens */
391
462
  --accent-hover: 178 8 9;
463
+ /* Brand red passes on a light card (5.64:1) — see the :root note. */
464
+ --accent-ink: var(--accent);
465
+ /* Solved against the DARKEST tint these inks land on, not the lightest.
466
+ The lens mixes success at both 10% (tip panels) and 20% (score chips); an
467
+ ink tuned only to the 10% tint cleared 4.61:1 there and still failed at
468
+ 4.09:1 on the 20% chip. This value clears BOTH — 5.12:1 and 4.54:1 — and
469
+ at 77% of --success it is still unmistakably green. */
470
+ --success-ink: 4 116 81;
471
+ /* Same reasoning as --success-ink: solved against the 20% tint as well as
472
+ the 10% one (5.03:1 / 4.52:1, up from 2.86:1 / 4.05:1). 72% of --warning,
473
+ still unmistakably amber. */
474
+ --warning-ink: 156 86 4;
475
+ /* 5.28:1 / 4.51:1 on the 10% and 20% tints, up from 3.53:1. */
476
+ --destructive-ink: 189 33 33;
392
477
  --accent-muted: 207 14 15 / 0.10;
393
478
  --accent-subtle: 207 14 15 / 0.05;
394
479
  --accent-glow: 207 14 15 / 0.20;
@@ -406,9 +491,28 @@
406
491
  --card-foreground: 17 17 19;
407
492
  --popover-foreground: 17 17 19;
408
493
  --muted-foreground: 82 82 91;
409
- --text-secondary: 82 82 91;
410
- /* --text-tertiary stays from :root */
411
- /* Lifted 161->137 was 2.56:1 on a white card. */
494
+ /* Byte-identical to --muted-foreground in both themes since they were
495
+ introduced. Aliased rather than duplicated so the two names cannot
496
+ drift apart; prefer --muted-foreground in new code. */
497
+ --text-secondary: var(--muted-foreground);
498
+ /* Was "stays from :root" — but :root IS the dark theme, so light
499
+ inherited the dark-tuned 132 132 141, which measures 3.71:1 on a
500
+ 255 card and fails AA for the 13px body text that consumes it.
501
+ Re-picked by measurement, not by eye: 110 110 119 is the first rung
502
+ with headroom on every light surface it can land on —
503
+ 5.05:1 on --surface-card/page (255), 4.72:1 on --surface-frame (247),
504
+ 4.52:1 on --surface-inset (242). Keeps the ramp's 286° cool cast.
505
+ Side effect: it also repairs the ink ramp's shape. The tiers were
506
+ L 0.442 / 0.616 / 0.633 — a wide gap then a collision, tertiary and
507
+ muted less than half a surface-step apart. They are now
508
+ 0.442 / 0.541 / 0.633: three evenly-spread rungs. */
509
+ --text-tertiary: 110 110 119;
510
+ /* Lifted 161->137 — was 2.56:1 on a white card. Still only 3.47:1 on
511
+ 255, i.e. it clears the 3:1 non-text floor and NOT the 4.5:1 text
512
+ floor. That is deliberate: --text-muted is the quietest rung and is
513
+ for NON-TEXT and DISABLED chrome only — quiet icons, chevrons,
514
+ disabled labels. Real text one tier up (--text-tertiary) or higher.
515
+ Lifting this to pass 4.5:1 would collapse it into --text-tertiary. */
412
516
  --text-muted: 137 137 146;
413
517
  --secondary-foreground: 17 17 19;
414
518
 
@@ -446,9 +550,6 @@
446
550
  --shadow-2xl: 0 24px 60px rgb(0 0 0 / 0.16);
447
551
  --shadow-glow: 0 0 20px rgb(207 14 15 / 0.20);
448
552
 
449
- --ring-focus: 0 0 0 3px rgb(207 14 15 / 0.20);
450
- --ring-error: 0 0 0 3px rgb(220 38 38 / 0.18);
451
-
452
553
  /* Match --page-bg (254/254/254). Was 241/245/249 (slate) — that's
453
554
  * what was actually painting "the steely background" the owner
454
555
  * flagged; --page-bg is the body, but MainLayout's content area
@@ -508,6 +609,8 @@
508
609
  --surface-frame: 247 247 249;
509
610
  --surface-inset: 242 242 244;
510
611
  --surface-hover: 238 238 240;
612
+ /* 1.042:1 against the card — a band, not a slab. See the :root note. */
613
+ --surface-subtle: 250 250 252;
511
614
  /* A control you can act on — input, select, option tile, chip-button.
512
615
  Sits at CARD level on purpose: in light that is white against the
513
616
  frame behind it, which reads "type here"; in dark it is raised above
@@ -606,6 +709,9 @@
606
709
  --folder-pink: 219 39 119;
607
710
  --folder-slate: 71 85 105;
608
711
  --folder-ink: 15 23 42;
712
+ /* Opaque categorical tints — hue at 0.10 over the 255 light card. */
713
+ --folder-amber-tint: 251 241 230;
714
+ --folder-blue-tint: 232 237 251;
609
715
  }
610
716
 
611
717
  /* ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovett/ui",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system primitives, tokens, layouts, and patterns for the Lovett portfolio.",
@@ -1,13 +1,17 @@
1
1
  /**
2
2
  * Modal — 51 consuming files plus 113 sub-component call sites.
3
3
  *
4
- * Note what these tests deliberately do NOT claim: Modal has no
5
- * role="dialog", no aria-modal, no focus trap and no focus restore, so
6
- * there is no dialog role to query by. That is a real accessibility gap
7
- * (a keyboard user can Tab through the page behind the backdrop) and it
8
- * is tracked separately these tests pin the behaviour that exists so
9
- * the a11y work can be verified as a change, not guessed at.
4
+ * The panel is a role="dialog" aria-modal="true" region: named by its own
5
+ * <h2> (or the `ariaLabel` prop when titleless), focused on open, focus
6
+ * RESTORED to the opener on close, and Tab/Shift+Tab wrapped inside it.
7
+ * The "a11y gap" block that used to stand here no dialog role, no trap,
8
+ * no restoreis closed, and the describe() below is what closes it.
9
+ *
10
+ * Background inertness is by focus trap, NOT the `inert` attribute: Modal
11
+ * renders inline inside #root, so inerting the app root would inert the
12
+ * modal too. See the note in modal.tsx.
10
13
  */
14
+ import { useState } from 'react'
11
15
  import { describe, expect, it, vi } from 'vitest'
12
16
  import { render, screen } from '@testing-library/react'
13
17
  import userEvent from '@testing-library/user-event'
@@ -142,6 +146,176 @@ describe('Modal', () => {
142
146
  })
143
147
  })
144
148
 
149
+ describe('accessibility', () => {
150
+ it('exposes the panel as a modal dialog named by its title', () => {
151
+ render(
152
+ <Modal isOpen onClose={noop} title="Confirm">
153
+ body
154
+ </Modal>,
155
+ )
156
+ const dialog = screen.getByRole('dialog', { name: 'Confirm' })
157
+ expect(dialog).toHaveAttribute('aria-modal', 'true')
158
+ expect(dialog).toHaveClass('ds-card-surface')
159
+ })
160
+
161
+ it('gives each open modal its own title id', () => {
162
+ render(
163
+ <>
164
+ <Modal isOpen onClose={noop} title="First">
165
+ a
166
+ </Modal>
167
+ <Modal isOpen onClose={noop} title="Second">
168
+ b
169
+ </Modal>
170
+ </>,
171
+ )
172
+ const [first, second] = screen.getAllByRole('dialog')
173
+ expect(first!.getAttribute('aria-labelledby')).not.toBe(
174
+ second!.getAttribute('aria-labelledby'),
175
+ )
176
+ expect(screen.getByRole('dialog', { name: 'First' })).toBeInTheDocument()
177
+ expect(screen.getByRole('dialog', { name: 'Second' })).toBeInTheDocument()
178
+ })
179
+
180
+ it('falls back to ariaLabel when there is no title', () => {
181
+ render(
182
+ <Modal isOpen onClose={noop} title="" ariaLabel="Asset details">
183
+ body
184
+ </Modal>,
185
+ )
186
+ const dialog = screen.getByRole('dialog', { name: 'Asset details' })
187
+ expect(dialog).not.toHaveAttribute('aria-labelledby')
188
+ })
189
+
190
+ it('moves focus to the panel on open', () => {
191
+ render(
192
+ <Modal isOpen onClose={noop} title="Confirm">
193
+ <button type="button">Inside</button>
194
+ </Modal>,
195
+ )
196
+ expect(document.activeElement).toBe(screen.getByRole('dialog'))
197
+ })
198
+
199
+ it('honours a [data-autofocus] control', () => {
200
+ render(
201
+ <Modal isOpen onClose={noop} title="Confirm">
202
+ <input data-autofocus aria-label="Name" />
203
+ </Modal>,
204
+ )
205
+ expect(document.activeElement).toBe(screen.getByRole('textbox'))
206
+ })
207
+
208
+ it('restores focus to the opener on close', async () => {
209
+ function Harness() {
210
+ const [open, setOpen] = useState(false)
211
+ return (
212
+ <>
213
+ <button type="button" onClick={() => setOpen(true)}>
214
+ Open
215
+ </button>
216
+ <Modal isOpen={open} onClose={() => setOpen(false)} title="Confirm">
217
+ body
218
+ </Modal>
219
+ </>
220
+ )
221
+ }
222
+ render(<Harness />)
223
+ const opener = screen.getByRole('button', { name: 'Open' })
224
+ await userEvent.click(opener)
225
+ expect(document.activeElement).toBe(screen.getByRole('dialog'))
226
+
227
+ await userEvent.click(screen.getByRole('button', { name: 'Close' }))
228
+ expect(screen.queryByRole('dialog')).toBeNull()
229
+ expect(document.activeElement).toBe(opener)
230
+ })
231
+
232
+ it('restores focus when the modal unmounts outright', async () => {
233
+ const opener = document.createElement('button')
234
+ document.body.appendChild(opener)
235
+ opener.focus()
236
+ const { unmount } = render(
237
+ <Modal isOpen onClose={noop} title="Confirm">
238
+ body
239
+ </Modal>,
240
+ )
241
+ expect(document.activeElement).not.toBe(opener)
242
+ unmount()
243
+ expect(document.activeElement).toBe(opener)
244
+ opener.remove()
245
+ })
246
+
247
+ it('wraps Tab from the last control back to the first', async () => {
248
+ render(
249
+ <Modal isOpen onClose={noop} title="Confirm">
250
+ <button type="button">Last</button>
251
+ </Modal>,
252
+ )
253
+ const close = screen.getByRole('button', { name: 'Close' })
254
+ const last = screen.getByRole('button', { name: 'Last' })
255
+ last.focus()
256
+ await userEvent.tab()
257
+ expect(document.activeElement).toBe(close)
258
+ })
259
+
260
+ it('wraps Shift+Tab from the first control round to the last', async () => {
261
+ render(
262
+ <Modal isOpen onClose={noop} title="Confirm">
263
+ <button type="button">Last</button>
264
+ </Modal>,
265
+ )
266
+ const close = screen.getByRole('button', { name: 'Close' })
267
+ close.focus()
268
+ await userEvent.tab({ shift: true })
269
+ expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Last' }))
270
+ })
271
+
272
+ it('wraps Shift+Tab from the panel itself round to the last control', async () => {
273
+ render(
274
+ <Modal isOpen onClose={noop} title="Confirm">
275
+ <button type="button">Last</button>
276
+ </Modal>,
277
+ )
278
+ expect(document.activeElement).toBe(screen.getByRole('dialog'))
279
+ await userEvent.tab({ shift: true })
280
+ expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Last' }))
281
+ })
282
+
283
+ it('does not let Tab leave the panel for the page behind it', async () => {
284
+ const outside = document.createElement('button')
285
+ outside.textContent = 'Behind'
286
+ document.body.appendChild(outside)
287
+ render(
288
+ <Modal isOpen onClose={noop} title="Confirm">
289
+ <button type="button">Last</button>
290
+ </Modal>,
291
+ )
292
+ const dialog = screen.getByRole('dialog')
293
+ for (let i = 0; i < 6; i++) {
294
+ await userEvent.tab()
295
+ expect(dialog.contains(document.activeElement)).toBe(true)
296
+ }
297
+ outside.remove()
298
+ })
299
+
300
+ it('closes only the topmost modal on Escape', async () => {
301
+ const closeOuter = vi.fn()
302
+ const closeInner = vi.fn()
303
+ render(
304
+ <>
305
+ <Modal isOpen onClose={closeOuter} title="Outer">
306
+ outer
307
+ </Modal>
308
+ <Modal isOpen onClose={closeInner} title="Inner">
309
+ inner
310
+ </Modal>
311
+ </>,
312
+ )
313
+ await userEvent.keyboard('{Escape}')
314
+ expect(closeInner).toHaveBeenCalledTimes(1)
315
+ expect(closeOuter).not.toHaveBeenCalled()
316
+ })
317
+ })
318
+
145
319
  it('merges a caller className onto the panel', () => {
146
320
  render(
147
321
  <Modal isOpen onClose={noop} title="Confirm" className="max-w-sm">
@@ -12,18 +12,7 @@
12
12
  import { useEffect, useState, type HTMLAttributes } from 'react'
13
13
  import { Check, Copy } from 'lucide-react'
14
14
  import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'
15
- // The `/index.js` is REQUIRED and is not a style preference.
16
- //
17
- // `react-syntax-highlighter/dist/esm/styles/prism` is a DIRECTORY. Vite's
18
- // resolver follows a directory import; **Node's ESM resolver does not**, and
19
- // `tsup` externalises this specifier verbatim into `dist/index.js`. So the
20
- // package built green and published, and any consumer loading it under Node ESM
21
- // — vitest, most obviously — failed at IMPORT, before a single assertion ran.
22
- //
23
- // Reported by the studios plane against `@lovett/ui@0.0.7` and reproduced from
24
- // the registry: two of three suites failed to load, not to assert. A green build
25
- // and dead tests, with nothing connecting the two facts.
26
- import { vscDarkPlus, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism/index.js'
15
+ import { vscDarkPlus, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
27
16
 
28
17
  import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash'
29
18
  import css from 'react-syntax-highlighter/dist/esm/languages/prism/css'
@@ -18,11 +18,13 @@
18
18
  * platforms themselves, exactly like the platform-chrome in the ad previews.
19
19
  */
20
20
 
21
+ import type { ReactElement } from 'react'
22
+
21
23
  export interface BrandIconProps {
22
24
  /** Square box size in px; the glyph letterboxes inside (non-square viewBoxes
23
25
  * centre via preserveAspectRatio). Default 16 — a tab/affix glyph. */
24
- size?: number
25
- className?: string
26
+ size?: number | undefined
27
+ className?: string | undefined
26
28
  }
27
29
 
28
30
  /** Meta brand glyph (mono, currentColor). */
@@ -185,3 +187,80 @@ export const BRAND_ICONS = {
185
187
  } as const
186
188
 
187
189
  export type BrandIconKey = keyof typeof BRAND_ICONS
190
+
191
+ // ─── Full-colour marks ───────────────────────────────────────────────────
192
+ //
193
+ // The mono glyphs above are for CONTEXT — inside a red pill, on a tab, next to
194
+ // a label — where a full-colour mark would clash and where `currentColor` is
195
+ // the whole point. These are for the opposite case: a mark standing alone in a
196
+ // list, where colour is what makes a platform recognisable at 16px and there is
197
+ // no surrounding colour to fight.
198
+ //
199
+ // Same source (the owner's logos-apps fork), same nominative use. Three ship
200
+ // monochrome upstream — Instagram, Snapchat, X — so their fills are already
201
+ // `currentColor` here and they follow the theme; baked black would vanish in
202
+ // dark mode, and both are legitimate presentations of those marks.
203
+ //
204
+ // Added 2026-08-30. These began life as a separate `platform-logo.tsx` written
205
+ // without noticing ADR-135 had already solved half the problem. Folded in here
206
+ // rather than left as a rival module: two files of brand marks from one source
207
+ // is how a design system ends up with an Instagram that looks different on two
208
+ // screens.
209
+
210
+ interface ColorMark {
211
+ viewBox: string
212
+ path: ReactElement
213
+ }
214
+
215
+ const COLOR_MARKS: Record<string, ColorMark> = {
216
+ instagram: { viewBox: '0 0 256 256', path: (<><path fill="currentColor" d="M128 23.064c34.177 0 38.225.13 51.722.745c12.48.57 19.258 2.655 23.769 4.408c5.974 2.322 10.238 5.096 14.717 9.575s7.253 8.743 9.575 14.717c1.753 4.511 3.838 11.289 4.408 23.768c.615 13.498.745 17.546.745 51.723s-.13 38.226-.745 51.723c-.57 12.48-2.655 19.257-4.408 23.768c-2.322 5.974-5.096 10.239-9.575 14.718s-8.743 7.253-14.717 9.574c-4.511 1.753-11.289 3.839-23.769 4.408c-13.495.616-17.543.746-51.722.746s-38.228-.13-51.723-.746c-12.48-.57-19.257-2.655-23.768-4.408c-5.974-2.321-10.239-5.095-14.718-9.574c-4.479-4.48-7.253-8.744-9.574-14.718c-1.753-4.51-3.839-11.288-4.408-23.768c-.616-13.497-.746-17.545-.746-51.723s.13-38.225.746-51.722c.57-12.48 2.655-19.258 4.408-23.769c2.321-5.974 5.095-10.238 9.574-14.717c4.48-4.48 8.744-7.253 14.718-9.575c4.51-1.753 11.288-3.838 23.768-4.408c13.497-.615 17.545-.745 51.723-.745M128 0C93.237 0 88.878.147 75.226.77c-13.625.622-22.93 2.786-31.071 5.95c-8.418 3.271-15.556 7.648-22.672 14.764S9.991 35.738 6.72 44.155C3.555 52.297 1.392 61.602.77 75.226C.147 88.878 0 93.237 0 128s.147 39.122.77 52.774c.622 13.625 2.785 22.93 5.95 31.071c3.27 8.417 7.647 15.556 14.763 22.672s14.254 11.492 22.672 14.763c8.142 3.165 17.446 5.328 31.07 5.95c13.653.623 18.012.77 52.775.77s39.122-.147 52.774-.77c13.624-.622 22.929-2.785 31.07-5.95c8.418-3.27 15.556-7.647 22.672-14.763s11.493-14.254 14.764-22.672c3.164-8.142 5.328-17.446 5.95-31.07c.623-13.653.77-18.012.77-52.775s-.147-39.122-.77-52.774c-.622-13.624-2.786-22.929-5.95-31.07c-3.271-8.418-7.648-15.556-14.764-22.672S220.262 9.99 211.845 6.72c-8.142-3.164-17.447-5.328-31.071-5.95C167.122.147 162.763 0 128 0m0 62.27c-36.302 0-65.73 29.43-65.73 65.73s29.428 65.73 65.73 65.73c36.301 0 65.73-29.428 65.73-65.73c0-36.301-29.429-65.73-65.73-65.73m0 108.397c-23.564 0-42.667-19.103-42.667-42.667S104.436 85.333 128 85.333s42.667 19.103 42.667 42.667s-19.103 42.667-42.667 42.667m83.686-110.994c0 8.484-6.876 15.36-15.36 15.36s-15.36-6.876-15.36-15.36s6.877-15.36 15.36-15.36s15.36 6.877 15.36 15.36"/></>) },
217
+ facebook: { viewBox: '0 0 256 256', path: (<><path fill="#1877f2" d="M256 128C256 57.308 198.692 0 128 0S0 57.308 0 128c0 63.888 46.808 116.843 108 126.445V165H75.5v-37H108V99.8c0-32.08 19.11-49.8 48.348-49.8C170.352 50 185 52.5 185 52.5V84h-16.14C152.959 84 148 93.867 148 103.99V128h35.5l-5.675 37H148v89.445c61.192-9.602 108-62.556 108-126.445"/><path fill="#fff" d="m177.825 165l5.675-37H148v-24.01C148 93.866 152.959 84 168.86 84H185V52.5S170.352 50 156.347 50C127.11 50 108 67.72 108 99.8V128H75.5v37H108v89.445A129 129 0 0 0 128 256a129 129 0 0 0 20-1.555V165z"/></>) },
218
+ meta: { viewBox: '0 0 256 171', path: (<><defs><linearGradient id="SVGYeLhubCc" x1="13.878%" x2="89.144%" y1="55.934%" y2="58.694%"><stop offset="0%" stopColor="#0064e1"/><stop offset="40%" stopColor="#0064e1"/><stop offset="83%" stopColor="#0073ee"/><stop offset="100%" stopColor="#0082fb"/></linearGradient><linearGradient id="SVGll66Sdsg" x1="54.315%" x2="54.315%" y1="82.782%" y2="39.307%"><stop offset="0%" stopColor="#0082fb"/><stop offset="100%" stopColor="#0064e0"/></linearGradient></defs><path fill="#0081fb" d="M27.651 112.136c0 9.775 2.146 17.28 4.95 21.82c3.677 5.947 9.16 8.466 14.751 8.466c7.211 0 13.808-1.79 26.52-19.372c10.185-14.092 22.186-33.874 30.26-46.275l13.675-21.01c9.499-14.591 20.493-30.811 33.1-41.806C161.196 4.985 172.298 0 183.47 0c18.758 0 36.625 10.87 50.3 31.257C248.735 53.584 256 81.707 256 110.729c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363v-27.616c15.695 0 19.612-14.422 19.612-30.927c0-23.52-5.484-49.623-17.564-68.273c-8.574-13.23-19.684-21.313-31.907-21.313c-13.22 0-23.859 9.97-35.815 27.75c-6.356 9.445-12.882 20.956-20.208 33.944l-8.066 14.289c-16.203 28.728-20.307 35.271-28.408 46.07c-14.2 18.91-26.324 26.076-42.287 26.076c-18.935 0-30.91-8.2-38.325-20.556C2.973 139.413 0 126.202 0 111.148z"/><path fill="url(#SVGYeLhubCc)" d="M21.802 33.206C34.48 13.666 52.774 0 73.757 0C85.91 0 97.99 3.597 110.605 13.897c13.798 11.261 28.505 29.805 46.853 60.368l6.58 10.967c15.881 26.459 24.917 40.07 30.205 46.49c6.802 8.243 11.565 10.7 17.752 10.7c15.695 0 19.612-14.422 19.612-30.927l24.393-.766c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363c-11.395 0-21.49-2.475-32.654-13.007c-8.582-8.083-18.615-22.443-26.334-35.352l-22.96-38.352C118.528 64.08 107.96 49.73 101.845 43.23c-6.578-6.988-15.036-15.428-28.532-15.428c-10.923 0-20.2 7.666-27.963 19.39z"/><path fill="url(#SVGll66Sdsg)" d="M73.312 27.802c-10.923 0-20.2 7.666-27.963 19.39c-10.976 16.568-17.698 41.245-17.698 64.944c0 9.775 2.146 17.28 4.95 21.82L9.027 149.482C2.973 139.413 0 126.202 0 111.148C0 83.772 7.514 55.24 21.802 33.206C34.48 13.666 52.774 0 73.757 0z"/></>) },
219
+ x: { viewBox: '0 0 251 256', path: (<><path d="M149.079 108.399L242.33 0h-22.098l-80.97 94.12L74.59 0H0l97.796 142.328L0 256h22.1l85.507-99.395L175.905 256h74.59L149.073 108.399zM118.81 143.58l-9.909-14.172l-78.84-112.773h33.943l63.625 91.011l9.909 14.173l82.705 118.3H186.3l-67.49-96.533z"/></>) },
220
+ linkedin: { viewBox: '0 0 256 256', path: (<><path fill="#0a66c2" d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4c-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.91 39.91 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186zM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009s9.851-22.014 22.008-22.016c12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97zM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453"/></>) },
221
+ youtube: { viewBox: '0 0 256 180', path: (<><path fill="red" d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134"/><path fill="#fff" d="m102.421 128.06l66.328-38.418l-66.328-38.418z"/></>) },
222
+ pinterest: { viewBox: '0 0 256 256', path: (<><path fill="#cb1f27" d="M0 128.002c0 52.414 31.518 97.442 76.619 117.239c-.36-8.938-.064-19.668 2.228-29.393c2.461-10.391 16.47-69.748 16.47-69.748s-4.089-8.173-4.089-20.252c0-18.969 10.994-33.136 24.686-33.136c11.643 0 17.268 8.745 17.268 19.217c0 11.704-7.465 29.211-11.304 45.426c-3.207 13.578 6.808 24.653 20.203 24.653c24.252 0 40.586-31.149 40.586-68.055c0-28.054-18.895-49.052-53.262-49.052c-38.828 0-63.017 28.956-63.017 61.3c0 11.152 3.288 19.016 8.438 25.106c2.368 2.797 2.697 3.922 1.84 7.134c-.614 2.355-2.024 8.025-2.608 10.272c-.852 3.242-3.479 4.401-6.409 3.204c-17.884-7.301-26.213-26.886-26.213-48.902c0-36.361 30.666-79.961 91.482-79.961c48.87 0 81.035 35.364 81.035 73.325c0 50.213-27.916 87.726-69.066 87.726c-13.819 0-26.818-7.47-31.271-15.955c0 0-7.431 29.492-9.005 35.187c-2.714 9.869-8.026 19.733-12.883 27.421a127.9 127.9 0 0 0 36.277 5.249c70.684 0 127.996-57.309 127.996-128.005C256.001 57.309 198.689 0 128.005 0C57.314 0 0 57.309 0 128.002"/></>) },
223
+ tiktok: { viewBox: '0 0 256 290', path: (<><path fill="#ff004f" d="M189.72 104.421c18.678 13.345 41.56 21.197 66.273 21.197v-47.53a67 67 0 0 1-13.918-1.456v37.413c-24.711 0-47.59-7.851-66.272-21.195v96.996c0 48.523-39.356 87.855-87.9 87.855c-18.113 0-34.949-5.473-48.934-14.86c15.962 16.313 38.222 26.432 62.848 26.432c48.548 0 87.905-39.332 87.905-87.857v-96.995zm17.17-47.952c-9.546-10.423-15.814-23.893-17.17-38.785v-6.113h-13.189c3.32 18.927 14.644 35.097 30.358 44.898M69.673 225.607a40 40 0 0 1-8.203-24.33c0-22.192 18.001-40.186 40.21-40.186a40.3 40.3 0 0 1 12.197 1.883v-48.593c-4.61-.631-9.262-.9-13.912-.801v37.822a40.3 40.3 0 0 0-12.203-1.882c-22.208 0-40.208 17.992-40.208 40.187c0 15.694 8.997 29.281 22.119 35.9"/><path d="M175.803 92.849c18.683 13.344 41.56 21.195 66.272 21.195V76.631c-13.794-2.937-26.005-10.141-35.186-20.162c-15.715-9.802-27.038-25.972-30.358-44.898h-34.643v189.843c-.079 22.132-18.049 40.052-40.21 40.052c-13.058 0-24.66-6.221-32.007-15.86c-13.12-6.618-22.118-20.206-22.118-35.898c0-22.193 18-40.187 40.208-40.187c4.255 0 8.356.662 12.203 1.882v-37.822c-47.692.985-86.047 39.933-86.047 87.834c0 23.912 9.551 45.589 25.053 61.428c13.985 9.385 30.82 14.86 48.934 14.86c48.545 0 87.9-39.335 87.9-87.857z"/><path fill="#00f2ea" d="M242.075 76.63V66.516a66.3 66.3 0 0 1-35.186-10.047a66.47 66.47 0 0 0 35.186 20.163M176.53 11.57a68 68 0 0 1-.728-5.457V0h-47.834v189.845c-.076 22.13-18.046 40.05-40.208 40.05a40.06 40.06 0 0 1-18.09-4.287c7.347 9.637 18.949 15.857 32.007 15.857c22.16 0 40.132-17.918 40.21-40.05V11.571zM99.966 113.58v-10.769a89 89 0 0 0-12.061-.818C39.355 101.993 0 141.327 0 189.845c0 30.419 15.467 57.227 38.971 72.996c-15.502-15.838-25.053-37.516-25.053-61.427c0-47.9 38.354-86.848 86.048-87.833"/></>) },
224
+ snapchat: { viewBox: '147.353 39.286 514.631 514.631', path: (<><path style={{ fill: '#FFFC00' }} d="M147.553,423.021v0.023c0.308,11.424,0.403,22.914,2.33,34.268 c2.042,12.012,4.961,23.725,10.53,34.627c7.529,14.756,17.869,27.217,30.921,37.396c9.371,7.309,19.608,13.111,30.94,16.771 c16.524,5.33,33.571,7.373,50.867,7.473c10.791,0.068,21.575,0.338,32.37,0.293c78.395-0.33,156.792,0.566,235.189-0.484 c10.403-0.141,20.636-1.41,30.846-3.277c19.569-3.582,36.864-11.932,51.661-25.133c17.245-15.381,28.88-34.205,34.132-56.924 c3.437-14.85,4.297-29.916,4.444-45.035v-3.016c0-1.17-0.445-256.892-0.486-260.272c-0.115-9.285-0.799-18.5-2.54-27.636 c-2.117-11.133-5.108-21.981-10.439-32.053c-5.629-10.641-12.68-20.209-21.401-28.57c-13.359-12.81-28.775-21.869-46.722-26.661 c-16.21-4.327-32.747-5.285-49.405-5.27c-0.027-0.004-0.09-0.173-0.094-0.255H278.56c-0.005,0.086-0.008,0.172-0.014,0.255 c-9.454,0.173-18.922,0.102-28.328,1.268c-10.304,1.281-20.509,3.21-30.262,6.812c-15.362,5.682-28.709,14.532-40.11,26.347 c-12.917,13.386-22.022,28.867-26.853,46.894c-4.31,16.084-5.248,32.488-5.271,49.008"> </path> <path style={{ fill: '#FFFFFF' }} d="M407.001,473.488c-1.068,0-2.087-0.039-2.862-0.076c-0.615,0.053-1.25,0.076-1.886,0.076 c-22.437,0-37.439-10.607-50.678-19.973c-9.489-6.703-18.438-13.031-28.922-14.775c-5.149-0.854-10.271-1.287-15.22-1.287 c-8.917,0-15.964,1.383-21.109,2.389c-3.166,0.617-5.896,1.148-8.006,1.148c-2.21,0-4.895-0.49-6.014-4.311 c-0.887-3.014-1.523-5.934-2.137-8.746c-1.536-7.027-2.65-11.316-5.281-11.723c-28.141-4.342-44.768-10.738-48.08-18.484 c-0.347-0.814-0.541-1.633-0.584-2.443c-0.129-2.309,1.501-4.334,3.777-4.711c22.348-3.68,42.219-15.492,59.064-35.119 c13.049-15.195,19.457-29.713,20.145-31.316c0.03-0.072,0.065-0.148,0.101-0.217c3.247-6.588,3.893-12.281,1.926-16.916 c-3.626-8.551-15.635-12.361-23.58-14.882c-1.976-0.625-3.845-1.217-5.334-1.808c-7.043-2.782-18.626-8.66-17.083-16.773 c1.124-5.916,8.949-10.036,15.273-10.036c1.756,0,3.312,0.308,4.622,0.923c7.146,3.348,13.575,5.045,19.104,5.045 c6.876,0,10.197-2.618,11-3.362c-0.198-3.668-0.44-7.546-0.674-11.214c0-0.004-0.005-0.048-0.005-0.048 c-1.614-25.675-3.627-57.627,4.546-75.95c24.462-54.847,76.339-59.112,91.651-59.112c0.408,0,6.674-0.062,6.674-0.062 c0.283-0.005,0.59-0.009,0.908-0.009c15.354,0,67.339,4.27,91.816,59.15c8.173,18.335,6.158,50.314,4.539,76.016l-0.076,1.23 c-0.222,3.49-0.427,6.793-0.6,9.995c0.756,0.696,3.795,3.096,9.978,3.339c5.271-0.202,11.328-1.891,17.998-5.014 c2.062-0.968,4.345-1.169,5.895-1.169c2.343,0,4.727,0.456,6.714,1.285l0.106,0.041c5.66,2.009,9.367,6.024,9.447,10.242 c0.071,3.932-2.851,9.809-17.223,15.485c-1.472,0.583-3.35,1.179-5.334,1.808c-7.952,2.524-19.951,6.332-23.577,14.878 c-1.97,4.635-1.322,10.326,1.926,16.912c0.036,0.072,0.067,0.145,0.102,0.221c1,2.344,25.205,57.535,79.209,66.432 c2.275,0.379,3.908,2.406,3.778,4.711c-0.048,0.828-0.248,1.656-0.598,2.465c-3.289,7.703-19.915,14.09-48.064,18.438 c-2.642,0.408-3.755,4.678-5.277,11.668c-0.63,2.887-1.271,5.717-2.146,8.691c-0.819,2.797-2.641,4.164-5.567,4.164h-0.441 c-1.905,0-4.604-0.346-8.008-1.012c-5.95-1.158-12.623-2.236-21.109-2.236c-4.948,0-10.069,0.434-15.224,1.287 c-10.473,1.744-19.421,8.062-28.893,14.758C444.443,462.88,429.436,473.488,407.001,473.488"> </path> <path style={{ fill: '#020202' }} d="M408.336,124.235c14.455,0,64.231,3.883,87.688,56.472c7.724,17.317,5.744,48.686,4.156,73.885 c-0.248,3.999-0.494,7.875-0.694,11.576l-0.084,1.591l1.062,1.185c0.429,0.476,4.444,4.672,13.374,5.017l0.144,0.008l0.15-0.003 c5.904-0.225,12.554-2.059,19.776-5.442c1.064-0.498,2.48-0.741,3.978-0.741c1.707,0,3.521,0.321,5.017,0.951l0.226,0.09 c3.787,1.327,6.464,3.829,6.505,6.093c0.022,1.28-0.935,5.891-14.359,11.194c-1.312,0.518-3.039,1.069-5.041,1.7 c-8.736,2.774-21.934,6.96-26.376,17.427c-2.501,5.896-1.816,12.854,2.034,20.678c1.584,3.697,26.52,59.865,82.631,69.111 c-0.011,0.266-0.079,0.557-0.229,0.9c-0.951,2.24-6.996,9.979-44.612,15.783c-5.886,0.902-7.328,7.5-9,15.17 c-0.604,2.746-1.218,5.518-2.062,8.381c-0.258,0.865-0.306,0.914-1.233,0.914c-0.128,0-0.278,0-0.442,0 c-1.668,0-4.2-0.346-7.135-0.922c-5.345-1.041-12.647-2.318-21.982-2.318c-5.21,0-10.577,0.453-15.962,1.352 c-11.511,1.914-20.872,8.535-30.786,15.543c-13.314,9.408-27.075,19.143-48.071,19.143c-0.917,0-1.812-0.031-2.709-0.076 l-0.236-0.01l-0.237,0.018c-0.515,0.045-1.034,0.068-1.564,0.068c-20.993,0-34.76-9.732-48.068-19.143 c-9.916-7.008-19.282-13.629-30.791-15.543c-5.38-0.896-10.752-1.352-15.959-1.352c-9.333,0-16.644,1.428-21.978,2.471 c-2.935,0.574-5.476,1.066-7.139,1.066c-1.362,0-1.388-0.08-1.676-1.064c-0.844-2.865-1.461-5.703-2.062-8.445 c-1.676-7.678-3.119-14.312-9.002-15.215c-37.613-5.809-43.659-13.561-44.613-15.795c-0.149-0.352-0.216-0.652-0.231-0.918 c56.11-9.238,81.041-65.408,82.63-69.119c3.857-7.818,4.541-14.775,2.032-20.678c-4.442-10.461-17.638-14.653-26.368-17.422 c-2.007-0.635-3.735-1.187-5.048-1.705c-11.336-4.479-14.823-8.991-14.305-11.725c0.601-3.153,6.067-6.359,10.837-6.359 c1.072,0,2.012,0.173,2.707,0.498c7.747,3.631,14.819,5.472,21.022,5.472c9.751,0,14.091-4.537,14.557-5.055l1.057-1.182 l-0.085-1.583c-0.197-3.699-0.44-7.574-0.696-11.565c-1.583-25.205-3.563-56.553,4.158-73.871 c23.37-52.396,72.903-56.435,87.525-56.435c0.36,0,6.717-0.065,6.717-0.065C407.744,124.239,408.033,124.235,408.336,124.235 M408.336,115.197h-0.017c-0.333,0-0.646,0-0.944,0.004c-2.376,0.024-6.282,0.062-6.633,0.066c-8.566,0-25.705,1.21-44.115,9.336 c-10.526,4.643-19.994,10.921-28.14,18.66c-9.712,9.221-17.624,20.59-23.512,33.796c-8.623,19.336-6.576,51.905-4.932,78.078 l0.006,0.041c0.176,2.803,0.361,5.73,0.53,8.582c-1.265,0.581-3.316,1.194-6.339,1.194c-4.864,0-10.648-1.555-17.187-4.619 c-1.924-0.896-4.12-1.349-6.543-1.349c-3.893,0-7.997,1.146-11.557,3.239c-4.479,2.63-7.373,6.347-8.159,10.468 c-0.518,2.726-0.493,8.114,5.492,13.578c3.292,3.008,8.128,5.782,14.37,8.249c1.638,0.645,3.582,1.261,5.641,1.914 c7.145,2.271,17.959,5.702,20.779,12.339c1.429,3.365,0.814,7.793-1.823,13.145c-0.069,0.146-0.138,0.289-0.201,0.439 c-0.659,1.539-6.807,15.465-19.418,30.152c-7.166,8.352-15.059,15.332-23.447,20.752c-10.238,6.617-21.316,10.943-32.923,12.855 c-4.558,0.748-7.813,4.809-7.559,9.424c0.078,1.33,0.39,2.656,0.931,3.939c0.004,0.008,0.009,0.016,0.013,0.023 c1.843,4.311,6.116,7.973,13.063,11.203c8.489,3.943,21.185,7.26,37.732,9.855c0.836,1.59,1.704,5.586,2.305,8.322 c0.629,2.908,1.285,5.898,2.22,9.074c1.009,3.441,3.626,7.553,10.349,7.553c2.548,0,5.478-0.574,8.871-1.232 c4.969-0.975,11.764-2.305,20.245-2.305c4.702,0,9.575,0.414,14.48,1.229c9.455,1.574,17.606,7.332,27.037,14 c13.804,9.758,29.429,20.803,53.302,20.803c0.651,0,1.304-0.021,1.949-0.066c0.789,0.037,1.767,0.066,2.799,0.066 c23.88,0,39.501-11.049,53.29-20.799l0.022-0.02c9.433-6.66,17.575-12.41,27.027-13.984c4.903-0.814,9.775-1.229,14.479-1.229 c8.102,0,14.517,1.033,20.245,2.15c3.738,0.736,6.643,1.09,8.872,1.09l0.218,0.004h0.226c4.917,0,8.53-2.699,9.909-7.422 c0.916-3.109,1.57-6.029,2.215-8.986c0.562-2.564,1.46-6.674,2.296-8.281c16.558-2.6,29.249-5.91,37.739-9.852 c6.931-3.215,11.199-6.873,13.053-11.166c0.556-1.287,0.881-2.621,0.954-3.979c0.261-4.607-2.999-8.676-7.56-9.424 c-51.585-8.502-74.824-61.506-75.785-63.758c-0.062-0.148-0.132-0.295-0.205-0.438c-2.637-5.354-3.246-9.777-1.816-13.148 c2.814-6.631,13.621-10.062,20.771-12.332c2.07-0.652,4.021-1.272,5.646-1.914c7.039-2.78,12.07-5.796,15.389-9.221 c3.964-4.083,4.736-7.995,4.688-10.555c-0.121-6.194-4.856-11.698-12.388-14.393c-2.544-1.052-5.445-1.607-8.399-1.607 c-2.011,0-4.989,0.276-7.808,1.592c-6.035,2.824-11.441,4.368-16.082,4.588c-2.468-0.125-4.199-0.66-5.32-1.171 c0.141-2.416,0.297-4.898,0.458-7.486l0.067-1.108c1.653-26.19,3.707-58.784-4.92-78.134c-5.913-13.253-13.853-24.651-23.604-33.892 c-8.178-7.744-17.678-14.021-28.242-18.661C434.052,116.402,416.914,115.197,408.336,115.197"> </path> <rect x="147.553" y="39.443" style={{ fill: 'none' }} width="514.231" height="514.23"> </rect></>) },
225
+ google: { viewBox: '0 0 256 262', path: (<><path fill="#4285f4" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"/><path fill="#34a853" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"/><path fill="#fbbc05" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"/><path fill="#eb4335" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"/></>) },
226
+ amazon: { viewBox: '0 0 512 512', path: (<><path d="m0 0H512V512H0" fill="#f90"/><path fill="#fff" d="M283 187c-62 2-121 19-121 81 0 43 26 64 61 64 31 0 51-12 68-30 8 11 10 16 24 27q5 3 10-1l31-27q4-4 0-10c-7-11-15-19-15-39v-64c0-27 2-52-18-70-17-16-38-20-62-21-53-1-88 28-93 62q-1 8 7 9l37 5c6 1 9-4 10-8 6-22 29-28 43-23 20 6 18 29 18 45m-36 105c-15 0-25-13-25-30 1-36 29-42 61-42v18c0 32-17 54-36 54m168 106c13-11 26-38 25-57 0-7-1-8-8-10-13-4-46-5-62 10-3 3-2 5 1 5 11-2 45-6 50 2 4 7-8 35-12 47-2 5 2 6 6 3M58 342c96 91 247 94 345 25 7-4 0-12-6-9A376 376 0 0162 337c-4-3-8 2-4 5"/></>) },
227
+ walmart: { viewBox: '-231.5 619.2 64 64', path: (<><path d="M-199.47 640.732c1.488 0 2.778-.794 2.878-1.786l1.488-16.77c0-1.687-1.985-2.977-4.366-2.977-2.48 0-4.366 1.4-4.366 2.977l1.488 16.77c.1.992 1.4 1.786 2.878 1.786M-208.6 646c.794-1.3.695-2.778-.1-3.374L-222.492 633c-1.4-.794-3.572.198-4.763 2.282s-.992 4.465.397 5.26l15.28 7.144c.992.298 2.282-.397 2.977-1.687m18.16 0c.794 1.3 2.084 1.984 2.977 1.588l15.28-7.144c1.4-.794 1.687-3.175.397-5.26-1.2-2.084-3.374-3.076-4.763-2.282l-13.792 9.625c-.794.695-.893 2.183-.1 3.473m-9.027 15.678c1.488 0 2.778.794 2.878 1.786l1.488 16.77c0 1.687-1.985 2.977-4.366 2.977-2.48 0-4.366-1.4-4.366-2.977l1.488-16.77c.1-.992 1.4-1.786 2.878-1.786m9.027-5.277c.794-1.3 2.084-1.985 2.977-1.588l15.28 7.045c1.4.794 1.687 3.175.397 5.26-1.2 2.084-3.374 3.076-4.763 2.282l-13.792-9.625c-.794-.595-.893-2.084-.1-3.374m-18.157 0c.794 1.3.695 2.778-.1 3.374l-13.792 9.625c-1.4.794-3.572-.198-4.763-2.282s-.992-4.465.397-5.26l15.28-7.045c.992-.397 2.282.298 2.977 1.588"/></>) },
228
+ }
229
+
230
+ /** Every platform `<BrandIcon>` can draw in colour. */
231
+ export function hasBrandIcon(name: string): boolean {
232
+ return name in COLOR_MARKS
233
+ }
234
+
235
+ export interface BrandIconRegistryProps extends BrandIconProps {
236
+ /** Platform key, e.g. "facebook", "youtube". */
237
+ name: string
238
+ /** Give a label only when the mark stands alone; decorative beside text. */
239
+ title?: string | undefined
240
+ }
241
+
242
+ /**
243
+ * A full-colour platform mark, looked up by key. Use when the platform is
244
+ * chosen at runtime; use the named mono `Brand*` components when it is known
245
+ * at author time and the mark sits in coloured chrome.
246
+ */
247
+ export function BrandIcon({ name, size = 16, className, title }: BrandIconRegistryProps) {
248
+ const mark = COLOR_MARKS[name]
249
+ if (!mark) return null
250
+ return (
251
+ <svg
252
+ viewBox={mark.viewBox}
253
+ width={size}
254
+ height={size}
255
+ className={className}
256
+ // Meta is 256x171 and YouTube 256x180 — assuming square would stretch them.
257
+ preserveAspectRatio="xMidYMid meet"
258
+ role={title ? 'img' : undefined}
259
+ aria-hidden={title ? undefined : true}
260
+ focusable="false"
261
+ >
262
+ {title && <title>{title}</title>}
263
+ {mark.path}
264
+ </svg>
265
+ )
266
+ }
package/src/index.ts CHANGED
@@ -28,6 +28,8 @@ export { default as MicrosoftLogo, type MicrosoftLogoProps } from './microsoft-l
28
28
  export {
29
29
  BrandMeta,
30
30
  BrandFacebook,
31
+ BrandIcon,
32
+ hasBrandIcon,
31
33
  BrandInstagram,
32
34
  BrandMessenger,
33
35
  BrandLinkedIn,
@@ -45,7 +47,10 @@ export {
45
47
  AppScroll,
46
48
  PageHeaderHost,
47
49
  PageHeaderSlotProvider,
50
+ HomeCrumbLink,
51
+ useBreadcrumbHome,
48
52
  type Crumb,
53
+ type HomeCrumb,
49
54
  } from './page-shell'
50
55
  export { Toaster, toast } from './toast'
51
56