@astryxdesign/theme-neutral 0.0.0-bootstrap.0 → 0.1.0-canary.08d4cf4

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,603 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Neutral Theme
5
+ *
6
+ * A pure grayscale spine with a from-scratch OKLCH-derived categorical
7
+ * palette. Hues are placed at evenly-spaced positions on the OKLCH wheel,
8
+ * chosen to keep each color recognizable at every tone (no red drift for
9
+ * orange, no blue drift for purple) and well-separated from its neighbors.
10
+ *
11
+ * Core neutral palette: #fafafa, #f5f5f5, #e5e5e5, #737373, #262626, #0a0a0a
12
+ *
13
+ * Categorical hues (OKLCH; chroma = max-in-gamut at the saturated stop):
14
+ * Red H=25 Orange H=65 Yellow H=90 Green H=145
15
+ * Teal H=180 Cyan H=215 Blue H=250 Purple H=320 Pink H=355
16
+ *
17
+ * Saturated badge stops:
18
+ * • Cool/medium hues sit at OKLCH L=0.48–0.50 with white text (AA+)
19
+ * • Bright warm hues (orange L=0.68, yellow L=0.80) use dark text
20
+ *
21
+ * Token tonal stops:
22
+ * bg = T90 (light) / T20 (dark)
23
+ * border = T80 / T30
24
+ * icon = T30 / T80
25
+ * text = T30 / T80
26
+ *
27
+ * All 9 saturated badge values pass WCAG AA (5.6–9.6 contrast range).
28
+ *
29
+ * Only overrides tokens that differ from the defaults.
30
+ */
31
+
32
+ import {defineTheme, defineSyntaxTheme} from '@astryxdesign/core/theme';
33
+ import {neutralIconRegistry} from './icons';
34
+
35
+ /**
36
+ * Neutral syntax palette — pulled from the OKLCH T30 (light) / T80 (dark)
37
+ * stops of the categorical ramps. Same colors used by --color-icon-* tokens.
38
+ */
39
+ const neutralSyntax = defineSyntaxTheme({
40
+ name: 'xds-neutral',
41
+ tokens: {
42
+ keyword: ['#700084', '#efa8ff'], // purple T30/T80
43
+ string: ['#005600', '#a6d2a2'], // green (sat T30 / pastel T80)
44
+ comment: ['#737373', '#a3a3a3'], // neutral
45
+ number: ['#6e3500', '#ffb37f'], // orange
46
+ function: ['#00458c', '#a0caff'], // blue T30/T80 H=255
47
+ type: ['#700084', '#efa8ff'], // purple
48
+ variable: ['#171717', '#e5e5e5'], // near-black / near-white
49
+ operator: ['#737373', '#a3a3a3'], // neutral
50
+ constant: ['#6e3500', '#ffb37f'], // orange
51
+ tag: ['#89001a', '#ffaeaa'], // red
52
+ attribute: ['#584400', '#eec12f'], // yellow
53
+ property: ['#005348', '#83dac9'], // teal
54
+ punctuation: ['#a3a3a3', '#525252'],// neutral
55
+ background: ['#fafafa', '#0a0a0a'],
56
+ },
57
+ });
58
+
59
+ export const neutralTheme = defineTheme({
60
+ name: 'neutral',
61
+
62
+ // Typography: Figtree across body, heading, and display sizes (display
63
+ // size tokens inherit from heading.family). Monospace stays as the
64
+ // platform default for code.
65
+ // Scale: base=14, ratio=1.2. Bold weights on h3/h4 for subsection hierarchy.
66
+ typography: {
67
+ scale: {base: 14, ratio: 1.2},
68
+ body: {
69
+ family: 'Figtree',
70
+ fallbacks:
71
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
72
+ },
73
+ heading: {
74
+ family: 'Figtree',
75
+ fallbacks:
76
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
77
+ weights: {3: 'bold', 4: 'bold'},
78
+ },
79
+ code: {
80
+ family: 'ui-monospace',
81
+ fallbacks:
82
+ '"SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
83
+ },
84
+ },
85
+
86
+ // Motion: snappier than default to match shadcn/Tailwind conventions.
87
+ // Produces: fast-min=95ms, fast=125ms, fast-max=165ms,
88
+ // medium-min=225ms, medium=300ms, medium-max=400ms.
89
+ motion: {fast: 125, medium: 300, slow: 700, ratio: 0.75},
90
+
91
+ syntax: neutralSyntax,
92
+
93
+ tokens: {
94
+ // =========================================================================
95
+ // Core — pure grayscale spine (Tailwind neutral)
96
+ // 50:#fafafa 100:#f5f5f5 200:#e5e5e5 300:#d4d4d4 400:#a3a3a3
97
+ // 500:#737373 600:#525252 700:#404040 800:#262626 900:#171717 950:#0a0a0a
98
+ // =========================================================================
99
+
100
+ // =========================================================================
101
+ // Backgrounds — Figma-style flat with a single lifted surface.
102
+ //
103
+ // Dark mode collapses card / popover / muted to body T10. Cards and
104
+ // popovers lift purely via shadow + inset highlight (see --shadow-*
105
+ // below) — they don't need a distinct tone.
106
+ //
107
+ // Surface is the exception: it's tonally LIGHTER than body (T15) so
108
+ // interactive components that sit on top of body have a clear,
109
+ // differentiated foreground. Real consumers of --color-background-surface
110
+ // are: switches, radios, checkboxes, multi-selectors, dialogs, app
111
+ // shells, sections — all things that need to lift above the canvas.
112
+ //
113
+ // surface T15 #262626 — interactive surfaces lifted above body
114
+ // body T10 #1b1b1b — main canvas
115
+ // card T10 #1b1b1b — same as body, lifts via --shadow-low
116
+ // popover T10 #1b1b1b — same as body, lifts via --shadow-med
117
+ // muted T10 #1b1b1b — same as body
118
+ //
119
+ // Light mode keeps the standard ladder (white surfaces float on tinted
120
+ // body; shadows do most of the lifting):
121
+ // surface T100 #ffffff
122
+ // body T95 #f1f1f1
123
+ // card T100 #ffffff
124
+ // popover T100 #ffffff
125
+ // muted T95 #f1f1f1
126
+ //
127
+ // All values use the OKLCH Neutral tonal palette (chroma=0).
128
+ // =========================================================================
129
+ '--color-background-surface': ['#ffffff', '#262626'],
130
+ '--color-background-body': ['#f1f1f1', '#1b1b1b'],
131
+ '--color-background-card': ['#ffffff', '#1b1b1b'],
132
+ '--color-background-popover': ['#ffffff', '#1b1b1b'],
133
+ '--color-background-muted': ['#f1f1f1', '#1b1b1b'],
134
+
135
+ // Accent + neutral surface tints (sit alongside backgrounds)
136
+ '--color-accent': ['#262626', '#ebebeb'],
137
+ '--color-accent-muted': ['#f1f1f1', '#262626'],
138
+ '--color-neutral': ['#0000000F', '#FFFFFF1A'],
139
+
140
+ // Overlays (modal scrims, hover/pressed tints)
141
+ '--color-overlay': ['#00000080', '#000000CC'],
142
+ '--color-overlay-hover': ['#0000000D', '#FFFFFF0D'],
143
+ '--color-overlay-pressed': ['#0000001A', '#FFFFFF1A'],
144
+
145
+ // Text
146
+ '--color-text-primary': ['#171717', '#fafafa'],
147
+ '--color-text-secondary': ['#737373', '#a3a3a3'],
148
+ '--color-text-disabled': ['#a3a3a3', '#525252'],
149
+ '--color-text-accent': ['#262626', '#ebebeb'],
150
+ '--color-on-dark': '#ffffff',
151
+ '--color-on-light': '#171717',
152
+ // Contrast: neutral accent is near-black (L) / near-white (D)
153
+ '--color-on-accent': ['#ffffff', '#171717'],
154
+ '--color-on-success': ['#ffffff', '#171717'],
155
+ '--color-on-error': ['#ffffff', '#171717'],
156
+ '--color-on-warning': '#171717',
157
+
158
+ // Icon
159
+ '--color-icon-accent': ['#262626', '#ebebeb'],
160
+ '--color-icon-primary': ['#171717', '#fafafa'],
161
+ '--color-icon-secondary': ['#737373', '#a3a3a3'],
162
+ '--color-icon-disabled': ['#a3a3a3', '#525252'],
163
+
164
+ // Status / Sentiment — dark mode follows the issue #2150 rubric:
165
+ //
166
+ // Light mode: pastel T90 banner bg + dark T30/T40 text/icon. Locked
167
+ // light values for cards/banners/inputs/destructive btn.
168
+ // Dark mode : tinted-dark T20 bg + light pastel T80 text. INVERTED
169
+ // from light. Avoids the §5 "pastel-in-both-modes"
170
+ // anti-pattern (locked pastels glow against a dark body).
171
+ //
172
+ // --color-X = "saturated text/icon stop":
173
+ // light = T40 dark colored (sits on light pastel)
174
+ // dark = T80 light pastel (sits on dark tinted bg)
175
+ // Used by destructive button text, input border/icon
176
+ // (in light), banner-status-* text overrides.
177
+ // --color-X-muted = "muted bg stop":
178
+ // light = T90 light pastel
179
+ // dark = hue-tinted alpha overlay (T70 stop @ 24%)
180
+ // Used by banner bg, status-input message bg,
181
+ // destructive button bg. Dark mode uses an alpha
182
+ // overlay rather than a solid T20 tinted bg so
183
+ // the surface composes onto whatever sits behind
184
+ // it (body, card, popover) rather than reading
185
+ // as a hard colored panel.
186
+ //
187
+ // 24% alpha = '3D' suffix. Hue values match --color-icon-{X} dark
188
+ // slots (palette T70). Composited onto body #1b1b1b, the effective
189
+ // bg luminance hits ~1.65-1.70:1 vs body — visible colored surface
190
+ // without the heaviness of a solid T20 panel.
191
+ '--color-success': ['#007004', '#9fe59b'],
192
+ '--color-error': ['#a50c25', '#ffc6c1'],
193
+ '--color-warning': ['#745b00', '#fdcf4f'],
194
+ '--color-success-muted': ['#c5e5c0', '#84c9803D'],
195
+ '--color-error-muted': ['#facecb', '#ff9e973D'],
196
+ '--color-warning-muted': ['#f8da9d', '#deb4333D'],
197
+
198
+ // Border
199
+ '--color-border': ['#ebebeb', '#FFFFFF1A'],
200
+ '--color-border-emphasized': ['#d4d4d4', '#525252'],
201
+
202
+ // Effects
203
+ '--color-skeleton': ['#ebebeb', '#525252'],
204
+ '--color-shadow': ['#0000001A', '#0000004D'],
205
+ '--color-tint-hover': ['black', 'white'],
206
+
207
+ // =========================================================================
208
+ // Categorical — light mode uses pastel surfaces + dark colored text;
209
+ // dark mode INVERTS to a hue-tinted alpha overlay surface +
210
+ // light pastel text (per #2150 rubric §3 — pick the tone
211
+ // that satisfies required contrast against every surface
212
+ // the token touches).
213
+ //
214
+ // Per-token tone choice (CIELab L*):
215
+ // bg light=T87-T90 pastel dark=T70 hue @ 24% alpha overlay
216
+ // (composites onto body to ~1.65:1
217
+ // vs body — colored surface that
218
+ // feels lighter than a solid T20
219
+ // panel; same hue as --color-icon-X
220
+ // dark slot, just at lower opacity)
221
+ // border light=T80 pastel dark=T60 mid-bright (>=5.8:1 vs body)
222
+ // icon light=T30 dark colored dark=T70 light pastel
223
+ // text light=T30 dark colored dark=T80 light pastel (>=7:1 on bg)
224
+ //
225
+ // Light pastels still use the per-hue chroma table (red/blue C=0.05,
226
+ // orange/green/purple/pink C=0.06, teal/cyan C=0.07, yellow H=85 C=0.10)
227
+ // for equal PERCEIVED saturation. Dark stops (T60/T70/T80) come from
228
+ // the dark-mode tonal palette (chroma×0.85, +5 tone lift tapering 80-95).
229
+ // =========================================================================
230
+
231
+ // Each row's dark slots are HCT-derived from the source hex listed in
232
+ // apps/sandbox/src/app/(fullscreen)/pages/neutral-palette/page.tsx via
233
+ // the canonical dark-ramp transform (chroma×0.85, +5 tone-lift taper)
234
+ // — same algorithm the Tonal Palettes preview renders. Border=T60,
235
+ // icon=T70, text=T80. Background uses the T70 hue at 24% alpha so the
236
+ // overlay surface composites onto body to ~1.65:1 luminance.
237
+
238
+ // Red H=22 — source #eb183a
239
+ '--color-background-red': ['#facecb', '#ff9e973D'],
240
+ '--color-border-red': ['#e6bab8', '#ff6f6c'],
241
+ '--color-icon-red': ['#89001a', '#ff9e97'],
242
+ '--color-text-red': ['#89001a', '#ffc6c1'],
243
+
244
+ // Orange H=55 — source #d57113
245
+ '--color-background-orange': ['#fad0b5', '#ffa2583D'],
246
+ '--color-border-orange': ['#e6bda2', '#e2883e'],
247
+ '--color-icon-orange': ['#6e3500', '#ffa258'],
248
+ '--color-text-orange': ['#6e3500', '#ffc9a2'],
249
+
250
+ // Yellow H=90 — source #f8c723
251
+ // Light-mode butter-yellow pastel at H=85 C=0.085 L=0.90 — yellow
252
+ // sits at the green-cyan luminance peak so it feels louder than the
253
+ // other status hues at the same canonical L. Picker decision: pull
254
+ // L down one step (0.91→0.90) and C down to its identity floor
255
+ // (0.10→0.085, just above the bronze threshold) so it sits closer
256
+ // to red/blue's perceived brightness without losing yellow identity.
257
+ // Dark-mode comes from the canonical H=90 ramp for tonal-palette
258
+ // consistency.
259
+ '--color-background-yellow': ['#f8da9d', '#deb4333D'],
260
+ '--color-border-yellow': ['#e4c279', '#c0990e'],
261
+ '--color-icon-yellow': ['#584400', '#deb433'],
262
+ '--color-text-yellow': ['#584400', '#fdcf4f'],
263
+
264
+ // Green H=144 — source #358a3a
265
+ '--color-background-green': ['#c5e5c0', '#84c9803D'],
266
+ '--color-border-green': ['#b2d1ac', '#69ad67'],
267
+ '--color-icon-green': ['#0c5700', '#84c980'],
268
+ '--color-text-green': ['#0c5700', '#9fe59b'],
269
+
270
+ // Teal H=180 — source #0c7365
271
+ // Light pastel uses L=0.87 C=0.065 (a step darker + less chroma than
272
+ // the L=0.888 C=0.07 used by other hues) to compensate for the
273
+ // green-cyan luminance overshoot — at the same OKLCH L, teal/cyan read
274
+ // ~5% brighter than red/blue because the eye's luminance response
275
+ // peaks in this band. Dropping L+C brings perceived brightness in
276
+ // line with the rest of the palette without losing hue identity.
277
+ '--color-background-teal': ['#a5e3d6', '#7ec6b83D'],
278
+ '--color-border-teal': ['#94d6c8', '#63ab9d'],
279
+ '--color-icon-teal': ['#005348', '#7ec6b8'],
280
+ '--color-text-teal': ['#005348', '#99e2d3'],
281
+
282
+ // Cyan H=215 — source #0c6f82
283
+ // Same L=0.87 C=0.065 pastel as teal (luminance overshoot compensation).
284
+ '--color-background-cyan': ['#a3e0ef', '#83c2d43D'],
285
+ '--color-border-cyan': ['#91d3e3', '#67a7b8'],
286
+ '--color-icon-cyan': ['#00505f', '#83c2d4'],
287
+ '--color-text-cyan': ['#00505f', '#9edef0'],
288
+
289
+ // Blue H=255 — source #0074e2
290
+ // T50 #0074e2 reserved for filled Info badge / progressbar / inset hover.
291
+ '--color-background-blue': ['#c4ddfb', '#9eb7ff3D'],
292
+ '--color-border-blue': ['#b1c9e7', '#6d9cfe'],
293
+ '--color-icon-blue': ['#00458c', '#9eb7ff'],
294
+ '--color-text-blue': ['#00458c', '#c7d3ff'],
295
+
296
+ // Purple H=320 — source #980fb2
297
+ '--color-background-purple': ['#eccef3', '#f297ff3D'],
298
+ '--color-border-purple': ['#d8bbdf', '#dd74f0'],
299
+ '--color-icon-purple': ['#700084', '#f297ff'],
300
+ '--color-text-purple': ['#700084', '#fac1ff'],
301
+
302
+ // Pink H=355 — source #b10e69
303
+ '--color-background-pink': ['#fccadc', '#ff99c33D'],
304
+ '--color-border-pink': ['#e7b7c8', '#f273aa'],
305
+ '--color-icon-pink': ['#83004b', '#ff99c3'],
306
+ '--color-text-pink': ['#83004b', '#ffc3da'],
307
+
308
+ // Gray (categorical neutral, chroma 0)
309
+ // Light: #e5e5e5 (Neutral 200) so it's visibly distinct from the
310
+ // lighter body / muted surface (both #f5f5f5).
311
+ // Dark : var(--color-neutral) — semi-transparent white wash
312
+ // (#FFFFFF1A, 10%). Matches the same treatment the gray
313
+ // badge uses; clearly distinct from the body T10 #1b1b1b
314
+ // while staying chroma-0 neutral. Solid T15 #1c1c1c was
315
+ // indistinguishable from --color-background-muted.
316
+ '--color-background-gray': ['#e5e5e5', 'var(--color-neutral)'],
317
+ '--color-border-gray': ['#d4d4d4', '#262626'],
318
+ '--color-icon-gray': ['#525252', '#a3a3a3'],
319
+ '--color-text-gray': ['#262626', '#e5e5e5'],
320
+
321
+ // =========================================================================
322
+ // Radius — slightly larger than default (kept as-is)
323
+ // =========================================================================
324
+ '--radius-none': '0.25rem',
325
+ '--radius-inner': '0.375rem',
326
+ '--radius-element': '0.625rem',
327
+ '--radius-container': '0.75rem',
328
+ '--radius-page': '1.75rem',
329
+ '--radius-full': '9999px',
330
+
331
+ // =========================================================================
332
+ // Shadows
333
+ //
334
+ // Light mode: matches origin/main exactly (5%/10% low+med, 10%/15% high).
335
+ // Subtle drops; light surfaces don't need rim highlights.
336
+ //
337
+ // Dark mode: deepened drops + an all-around 1px white inset that wraps
338
+ // every edge ("Figma-style bezel"). The inset mimics ambient light
339
+ // catching the surface's rim on every side, giving cards/popovers/modals
340
+ // a substantial "lit from above" feel that drop shadows alone can't
341
+ // achieve against a dark canvas.
342
+ // low : drops 25%/40% + 8% white all-around inset
343
+ // med : drops 35%/50% + 12% white all-around inset
344
+ // high : drops 50%/70% + 15% white all-around inset
345
+ //
346
+ // The inset layer uses light-dark(transparent, ...) so light mode is
347
+ // unaffected — main's exact light values are preserved.
348
+ // =========================================================================
349
+ '--shadow-low':
350
+ '0 2px 4px light-dark(oklch(0 0 0 / 5%), oklch(0 0 0 / 25%)), ' +
351
+ '0 4px 8px light-dark(oklch(0 0 0 / 10%), oklch(0 0 0 / 40%)), ' +
352
+ 'inset 0 0 0 1px light-dark(transparent, oklch(1 0 0 / 8%))',
353
+ '--shadow-med':
354
+ '0 2px 4px light-dark(oklch(0 0 0 / 5%), oklch(0 0 0 / 35%)), ' +
355
+ '0 4px 12px light-dark(oklch(0 0 0 / 10%), oklch(0 0 0 / 50%)), ' +
356
+ 'inset 0 0 0 1px light-dark(transparent, oklch(1 0 0 / 12%))',
357
+ '--shadow-high':
358
+ '0 4px 6px light-dark(oklch(0 0 0 / 10%), oklch(0 0 0 / 50%)), ' +
359
+ '0 12px 24px light-dark(oklch(0 0 0 / 15%), oklch(0 0 0 / 70%)), ' +
360
+ 'inset 0 0 0 1px light-dark(transparent, oklch(1 0 0 / 15%))',
361
+ '--shadow-inset-hover': 'inset 0px 0px 0px 2px #0074e24D',
362
+ '--shadow-inset-selected': 'inset 0px 0px 0px 2px #0074e280',
363
+ '--shadow-inset-success': 'inset 0px 0px 0px 2px #1981004D',
364
+ '--shadow-inset-warning': 'inset 0px 0px 0px 2px #ffce2f4D',
365
+ '--shadow-inset-error': 'inset 0px 0px 0px 2px #e33f4a4D',
366
+ },
367
+
368
+ components: {
369
+ // =========================================================================
370
+ // Button — primary gets white text, secondary gets a border, destructive
371
+ // uses the OKLCH red filled treatment.
372
+ // =========================================================================
373
+ button: {
374
+ 'variant:destructive': {
375
+ backgroundColor: 'var(--color-error-muted)', // locked pastel red bg
376
+ color: 'var(--color-error)', // locked T30 red — matches banner/input error text
377
+ },
378
+ },
379
+
380
+ // =========================================================================
381
+ // Badge —
382
+ // Semantic (info/success/warning/error): filled saturated T50 + contrasting
383
+ // text (white, or dark on yellow). The filled-button rule from #2150
384
+ // §3 — text contrast locks the bg tone, so this stays at T50 in
385
+ // BOTH modes, unlike pastel surfaces which invert by mode.
386
+ // Categorical (blue/green/red/orange/etc.): pastel-tinted hue surface +
387
+ // colored text — light mode = soft T87-T90 + dark T30 text; dark mode
388
+ // = T20 tinted + T80 light pastel text (sources: --color-background-X
389
+ // and --color-text-X tokens).
390
+ // Neutral: light gray bg + dark text (or inverted in dark mode).
391
+ // =========================================================================
392
+ badge: {
393
+ // Semantic — filled saturated bg + contrasting text.
394
+ // Light: vivid T45-T55 from the OKLCH palette + white text
395
+ // (~4.5-5:1 — Material/Linear/Vercel pop).
396
+ // Dark : T60 stop from the dark-mode tonal palette (chroma×0.85,
397
+ // +5 tone-lift taper from issue #2150 §4) + DARK text.
398
+ // T60+white fails AA-large (~2.7:1); T60+dark hits 6.6-7:1
399
+ // and tames the §4 vibration. Same dark-text-on-bright-bg
400
+ // treatment that warning yellow uses in both modes.
401
+ 'variant:info': {
402
+ // Light: T50 #0074e2 (palette saturated stop)
403
+ // Dark : T60 stop from dark-mode tonal palette of source #0074e2
404
+ backgroundColor: 'light-dark(#0074e2, #6d9cfe)',
405
+ color: 'light-dark(#ffffff, #171717)',
406
+ },
407
+ 'variant:neutral': {
408
+ // Mirrors the gray categorical badge — same neutral chip treatment
409
+ // (Neutral 200 light / semi-transparent white wash dark) sourced
410
+ // from the gray hue tokens, so a single change at the token layer
411
+ // updates both variants.
412
+ backgroundColor: 'var(--color-background-gray)',
413
+ color: 'var(--color-text-gray)',
414
+ },
415
+ 'variant:success': {
416
+ // Light: T45 #198100 (palette saturated stop)
417
+ // Dark : T60 stop from dark-mode tonal palette of source #198100
418
+ backgroundColor: 'light-dark(#198100, #64af4c)',
419
+ color: 'light-dark(#ffffff, #171717)',
420
+ },
421
+ 'variant:warning': {
422
+ // Yellow stays at the same hex in both modes — chroma reduction
423
+ // is barely visible at T85, and dark text on yellow doesn't
424
+ // suffer from the §4 vibration concern.
425
+ backgroundColor: '#ffce2f',
426
+ color: '#171717',
427
+ },
428
+ 'variant:error': {
429
+ // Light: T55 #e33f4a (palette saturated stop)
430
+ // Dark : T60 stop from dark-mode tonal palette of Tailwind red-600
431
+ // source #dc2626 (kept on H=27 alarm-red rather than coral)
432
+ backgroundColor: 'light-dark(#e33f4a, #ff705d)',
433
+ color: 'light-dark(#ffffff, #171717)',
434
+ },
435
+
436
+ // Categorical — bg + text reference the per-hue tokens, so behavior
437
+ // tracks the categorical palette automatically:
438
+ // Light: pastel T87-T90 bg + dark T30 colored text (low-key chip)
439
+ // Dark : tinted T20 bg + light T80 colored text (per #2150 §5,
440
+ // inverted from light to avoid the "pastel-in-both-modes"
441
+ // anti-pattern that makes locked light pastels glow on a
442
+ // dark body)
443
+ 'variant:red': {
444
+ backgroundColor: 'var(--color-background-red)',
445
+ color: 'var(--color-text-red)',
446
+ },
447
+ 'variant:orange': {
448
+ backgroundColor: 'var(--color-background-orange)',
449
+ color: 'var(--color-text-orange)',
450
+ },
451
+ 'variant:yellow': {
452
+ backgroundColor: 'var(--color-background-yellow)',
453
+ color: 'var(--color-text-yellow)',
454
+ },
455
+ 'variant:green': {
456
+ backgroundColor: 'var(--color-background-green)',
457
+ color: 'var(--color-text-green)',
458
+ },
459
+ 'variant:teal': {
460
+ backgroundColor: 'var(--color-background-teal)',
461
+ color: 'var(--color-text-teal)',
462
+ },
463
+ 'variant:cyan': {
464
+ backgroundColor: 'var(--color-background-cyan)',
465
+ color: 'var(--color-text-cyan)',
466
+ },
467
+ 'variant:blue': {
468
+ backgroundColor: 'var(--color-background-blue)',
469
+ color: 'var(--color-text-blue)',
470
+ },
471
+ 'variant:purple': {
472
+ backgroundColor: 'var(--color-background-purple)',
473
+ color: 'var(--color-text-purple)',
474
+ },
475
+ 'variant:pink': {
476
+ backgroundColor: 'var(--color-background-pink)',
477
+ color: 'var(--color-text-pink)',
478
+ },
479
+ 'variant:gray': {
480
+ backgroundColor: 'var(--color-background-gray)',
481
+ color: 'var(--color-text-gray)',
482
+ },
483
+ },
484
+
485
+ // =========================================================================
486
+ // Banner — sits on a hue-tinted surface with colored text/icon:
487
+ // Light: pastel T90 bg (pulled from --color-{X}-muted / --color-background-blue)
488
+ // + dark T30 colored text (--color-text-{hue}).
489
+ // Dark : tinted T20 bg (same tokens, dark slot) + light T80 colored text.
490
+ // Per #2150 §5 — large hue-tinted surfaces in dark mode invert
491
+ // to a deep tinted bg + light text rather than locking the
492
+ // light-mode pastel.
493
+ //
494
+ // The inner-header *-muted token is forced transparent so the outer
495
+ // tinted background shows through cleanly.
496
+ //
497
+ // Status overrides reference --color-text-{hue} so text/icon colors
498
+ // stay in sync with the palette anchors automatically.
499
+ banner: {
500
+ 'status:info': {
501
+ backgroundColor: 'var(--color-background-blue)',
502
+ '--color-accent-muted': 'transparent',
503
+ '--color-text-primary': 'var(--color-text-blue)',
504
+ '--color-text-secondary': 'var(--color-text-blue)',
505
+ '--color-accent': 'var(--color-text-blue)',
506
+ },
507
+ // success/warning/error banner bgs come from --color-{X}-muted, which
508
+ // already carries the correct light/dark tinted values. We only need
509
+ // to redirect the text/icon to the palette colored stop.
510
+ 'status:success': {
511
+ '--color-text-primary': 'var(--color-text-green)',
512
+ '--color-text-secondary': 'var(--color-text-green)',
513
+ '--color-success': 'var(--color-text-green)',
514
+ },
515
+ 'status:warning': {
516
+ '--color-text-primary': 'var(--color-text-yellow)',
517
+ '--color-text-secondary': 'var(--color-text-yellow)',
518
+ '--color-warning': 'var(--color-text-yellow)',
519
+ },
520
+ 'status:error': {
521
+ '--color-text-primary': 'var(--color-text-red)',
522
+ '--color-text-secondary': 'var(--color-text-red)',
523
+ '--color-error': 'var(--color-text-red)',
524
+ },
525
+ },
526
+
527
+ // =========================================================================
528
+ // TextInput — no per-status overrides needed. The global tokens
529
+ // --color-{success,error,warning} carry the correct values in both
530
+ // modes (light=T40 dark colored, dark=T80 light pastel) for both
531
+ // surfaces the input border/icon touches: the input surface
532
+ // (white/T15-dark) and the status message bubble (light pastel T90 /
533
+ // dark T20). Verified all six combinations clear AA non-text 3:1.
534
+ // =========================================================================
535
+
536
+ // =========================================================================
537
+ // Switch — off-state track uses the same lifted-neutral surface as the
538
+ // ProgressBar track (--color-border-emphasized). Aligns the two
539
+ // "channel-on-body" components so their off-states share one visual
540
+ // language: light T85 #d4d4d4 sits one step darker than the body T95
541
+ // bg, dark T35 #525252 sits one step lighter than the body T10. Each
542
+ // is a defined channel, not a wash that blends in.
543
+ // =========================================================================
544
+ switch: {
545
+ base: {
546
+ '--color-background-gray': 'var(--color-border-emphasized)',
547
+ },
548
+ },
549
+
550
+ progressbar: {
551
+ base: {
552
+ // Track uses --color-background-muted; override it to
553
+ // --color-border-emphasized (Neutral T85 #d4d4d4 in light mode) so
554
+ // the track is clearly darker than the body bg (Neutral T95 #f1f1f1)
555
+ // and reads as a defined channel rather than blending in. Dark
556
+ // mode inherits T35 #525252 — same one-step-lighter behavior.
557
+ '--color-background-muted': 'var(--color-border-emphasized)',
558
+ },
559
+ // Vivid stops match the filled semantic badge colors (info/success/
560
+ // warning/error variants in the badge override above). Same hex
561
+ // values; documented per role with palette provenance.
562
+ 'variant:accent': {
563
+ // Blue T50 saturated stop (= variant:info badge bg)
564
+ '--color-accent': '#0074e2',
565
+ },
566
+ 'variant:success': {
567
+ // Green T45 saturated stop (= variant:success badge bg)
568
+ '--color-success': '#198100',
569
+ },
570
+ 'variant:warning': {
571
+ // Yellow T85 saturated stop (= variant:warning badge bg)
572
+ '--color-warning': '#ffce2f',
573
+ },
574
+ 'variant:error': {
575
+ // Red T55 saturated stop (= variant:error badge bg)
576
+ '--color-error': '#e33f4a',
577
+ },
578
+ },
579
+
580
+ // =========================================================================
581
+ // Card — tighter padding via public card padding token
582
+ // =========================================================================
583
+ card: {
584
+ base: {
585
+ padding: 'var(--spacing-3)',
586
+ },
587
+ },
588
+
589
+ // =========================================================================
590
+ // Section — tighter padding via public section padding token
591
+ // =========================================================================
592
+ section: {
593
+ base: {
594
+ padding: 'var(--spacing-3)',
595
+ },
596
+ },
597
+
598
+ // Heading and text component overrides are auto-generated by typography.scale.
599
+ // h3/h4 bold weights come from typography.heading.weights above.
600
+ },
601
+
602
+ icons: neutralIconRegistry,
603
+ });
package/src/source.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export {neutralTheme} from './neutralTheme';
4
+ export {neutralIconRegistry} from './icons';