@oxyhq/bloom 0.80.0 → 0.81.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.
- package/lib/commonjs/tabs/Tabs.js +153 -15
- package/lib/commonjs/tabs/Tabs.js.map +1 -1
- package/lib/commonjs/theme/color-engine/index.js +6 -0
- package/lib/commonjs/theme/color-engine/index.js.map +1 -1
- package/lib/commonjs/theme/color-engine/scheme-variants.js +29 -1
- package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/commonjs/theme/color-policy.js +54 -10
- package/lib/commonjs/theme/color-policy.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +14 -2
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js +5 -1
- package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/module/tabs/Tabs.js +153 -15
- package/lib/module/tabs/Tabs.js.map +1 -1
- package/lib/module/theme/color-engine/index.js +1 -1
- package/lib/module/theme/color-engine/index.js.map +1 -1
- package/lib/module/theme/color-engine/scheme-variants.js +28 -1
- package/lib/module/theme/color-engine/scheme-variants.js.map +1 -1
- package/lib/module/theme/color-policy.js +53 -10
- package/lib/module/theme/color-policy.js.map +1 -1
- package/lib/module/theme/color-presets.js +14 -2
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/module/theme/color-scope/seed-scope.js +6 -2
- package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
- package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts +1 -1
- package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +13 -1
- package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-policy.d.ts +6 -0
- package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/index.d.ts +1 -1
- package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +13 -1
- package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-policy.d.ts +6 -0
- package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/Tabs.test.tsx +286 -10
- package/src/__tests__/theme.test.ts +2 -1
- package/src/tabs/Tabs.tsx +172 -12
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +120 -0
- package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +70 -0
- package/src/theme/__tests__/policy-legibility.test.ts +12 -4
- package/src/theme/color-engine/index.ts +1 -1
- package/src/theme/color-engine/scheme-variants.ts +38 -1
- package/src/theme/color-policy.ts +68 -14
- package/src/theme/color-presets.ts +11 -2
- package/src/theme/color-scope/seed-scope.ts +6 -2
|
@@ -1558,5 +1558,125 @@
|
|
|
1558
1558
|
"--info-foreground": "rgb(255 255 255)",
|
|
1559
1559
|
"--info-text": "rgb(173 198 255)",
|
|
1560
1560
|
"--info-subtle": "rgba(59, 130, 246, 0.24)"
|
|
1561
|
+
},
|
|
1562
|
+
"mono/light": {
|
|
1563
|
+
"--background": "rgb(243 243 243)",
|
|
1564
|
+
"--foreground": "rgb(27 27 27)",
|
|
1565
|
+
"--surface": "rgb(243 243 243)",
|
|
1566
|
+
"--surface-foreground": "rgb(27 27 27)",
|
|
1567
|
+
"--popover": "rgb(238 238 238)",
|
|
1568
|
+
"--popover-foreground": "rgb(27 27 27)",
|
|
1569
|
+
"--primary": "rgb(31 31 31)",
|
|
1570
|
+
"--primary-foreground": "rgb(255 255 255)",
|
|
1571
|
+
"--secondary": "rgb(85 85 85)",
|
|
1572
|
+
"--secondary-foreground": "rgb(255 255 255)",
|
|
1573
|
+
"--tertiary": "rgb(31 31 31)",
|
|
1574
|
+
"--tertiary-foreground": "rgb(255 255 255)",
|
|
1575
|
+
"--muted": "rgb(232 232 232)",
|
|
1576
|
+
"--muted-foreground": "rgb(71 71 71)",
|
|
1577
|
+
"--accent": "rgb(232 232 232)",
|
|
1578
|
+
"--accent-foreground": "rgb(71 71 71)",
|
|
1579
|
+
"--destructive": "rgb(186 26 26)",
|
|
1580
|
+
"--border": "rgb(198 198 198)",
|
|
1581
|
+
"--input": "rgb(198 198 198)",
|
|
1582
|
+
"--ring": "rgb(0 0 0)",
|
|
1583
|
+
"--sidebar": "rgb(243 243 243)",
|
|
1584
|
+
"--card": "rgb(255 255 255)",
|
|
1585
|
+
"--card-foreground": "rgb(27 27 27)",
|
|
1586
|
+
"--chart-1": "rgb(193 193 193)",
|
|
1587
|
+
"--chart-2": "rgb(158 158 158)",
|
|
1588
|
+
"--chart-3": "rgb(124 124 124)",
|
|
1589
|
+
"--chart-4": "rgb(92 92 92)",
|
|
1590
|
+
"--chart-5": "rgb(62 62 62)",
|
|
1591
|
+
"--content-area": "rgb(249 249 249)",
|
|
1592
|
+
"--sidebar-foreground": "rgb(27 27 27)",
|
|
1593
|
+
"--sidebar-primary": "rgb(0 0 0)",
|
|
1594
|
+
"--sidebar-primary-foreground": "rgb(226 226 226)",
|
|
1595
|
+
"--sidebar-accent": "rgb(212 212 212)",
|
|
1596
|
+
"--sidebar-accent-foreground": "rgb(27 27 27)",
|
|
1597
|
+
"--sidebar-border": "rgb(198 198 198)",
|
|
1598
|
+
"--sidebar-ring": "rgb(0 0 0)",
|
|
1599
|
+
"--primary-text": "rgb(0 0 0)",
|
|
1600
|
+
"--primary-subtle": "rgba(145, 145, 145, 0.13)",
|
|
1601
|
+
"--secondary-text": "rgb(94 94 94)",
|
|
1602
|
+
"--secondary-subtle": "rgba(145, 145, 145, 0.13)",
|
|
1603
|
+
"--tertiary-text": "rgb(94 94 94)",
|
|
1604
|
+
"--tertiary-subtle": "rgba(145, 145, 145, 0.13)",
|
|
1605
|
+
"--success": "rgb(0 122 83)",
|
|
1606
|
+
"--success-foreground": "rgb(255 255 255)",
|
|
1607
|
+
"--success-text": "rgb(0 108 73)",
|
|
1608
|
+
"--success-subtle": "rgba(16, 185, 129, 0.13)",
|
|
1609
|
+
"--error": "rgb(203 41 46)",
|
|
1610
|
+
"--error-foreground": "rgb(255 255 255)",
|
|
1611
|
+
"--error-text": "rgb(167 55 53)",
|
|
1612
|
+
"--error-subtle": "rgba(239, 68, 68, 0.13)",
|
|
1613
|
+
"--warning": "rgb(150 94 0)",
|
|
1614
|
+
"--warning-foreground": "rgb(255 255 255)",
|
|
1615
|
+
"--warning-text": "rgb(133 83 0)",
|
|
1616
|
+
"--warning-subtle": "rgba(245, 158, 11, 0.13)",
|
|
1617
|
+
"--info": "rgb(5 102 217)",
|
|
1618
|
+
"--info-foreground": "rgb(255 255 255)",
|
|
1619
|
+
"--info-text": "rgb(1 90 194)",
|
|
1620
|
+
"--info-subtle": "rgba(59, 130, 246, 0.13)"
|
|
1621
|
+
},
|
|
1622
|
+
"mono/dark": {
|
|
1623
|
+
"--background": "rgb(14 14 14)",
|
|
1624
|
+
"--foreground": "rgb(226 226 226)",
|
|
1625
|
+
"--surface": "rgb(27 27 27)",
|
|
1626
|
+
"--surface-foreground": "rgb(226 226 226)",
|
|
1627
|
+
"--popover": "rgb(31 31 31)",
|
|
1628
|
+
"--popover-foreground": "rgb(226 226 226)",
|
|
1629
|
+
"--primary": "rgb(238 238 238)",
|
|
1630
|
+
"--primary-foreground": "rgb(0 0 0)",
|
|
1631
|
+
"--secondary": "rgb(176 176 176)",
|
|
1632
|
+
"--secondary-foreground": "rgb(0 0 0)",
|
|
1633
|
+
"--tertiary": "rgb(238 238 238)",
|
|
1634
|
+
"--tertiary-foreground": "rgb(0 0 0)",
|
|
1635
|
+
"--muted": "rgb(42 42 42)",
|
|
1636
|
+
"--muted-foreground": "rgb(198 198 198)",
|
|
1637
|
+
"--accent": "rgb(42 42 42)",
|
|
1638
|
+
"--accent-foreground": "rgb(198 198 198)",
|
|
1639
|
+
"--destructive": "rgb(255 180 171)",
|
|
1640
|
+
"--border": "rgb(71 71 71)",
|
|
1641
|
+
"--input": "rgb(71 71 71)",
|
|
1642
|
+
"--ring": "rgb(255 255 255)",
|
|
1643
|
+
"--sidebar": "rgb(27 27 27)",
|
|
1644
|
+
"--card": "rgb(31 31 31)",
|
|
1645
|
+
"--card-foreground": "rgb(226 226 226)",
|
|
1646
|
+
"--chart-1": "rgb(94 94 94)",
|
|
1647
|
+
"--chart-2": "rgb(127 127 127)",
|
|
1648
|
+
"--chart-3": "rgb(160 160 160)",
|
|
1649
|
+
"--chart-4": "rgb(196 196 196)",
|
|
1650
|
+
"--chart-5": "rgb(232 232 232)",
|
|
1651
|
+
"--content-area": "rgb(19 19 19)",
|
|
1652
|
+
"--sidebar-foreground": "rgb(226 226 226)",
|
|
1653
|
+
"--sidebar-primary": "rgb(255 255 255)",
|
|
1654
|
+
"--sidebar-primary-foreground": "rgb(27 27 27)",
|
|
1655
|
+
"--sidebar-accent": "rgb(71 71 71)",
|
|
1656
|
+
"--sidebar-accent-foreground": "rgb(226 226 226)",
|
|
1657
|
+
"--sidebar-border": "rgb(71 71 71)",
|
|
1658
|
+
"--sidebar-ring": "rgb(255 255 255)",
|
|
1659
|
+
"--primary-text": "rgb(255 255 255)",
|
|
1660
|
+
"--primary-subtle": "rgba(145, 145, 145, 0.24)",
|
|
1661
|
+
"--secondary-text": "rgb(198 198 198)",
|
|
1662
|
+
"--secondary-subtle": "rgba(145, 145, 145, 0.24)",
|
|
1663
|
+
"--tertiary-text": "rgb(198 198 198)",
|
|
1664
|
+
"--tertiary-subtle": "rgba(145, 145, 145, 0.24)",
|
|
1665
|
+
"--success": "rgb(0 133 91)",
|
|
1666
|
+
"--success-foreground": "rgb(255 255 255)",
|
|
1667
|
+
"--success-text": "rgb(59 224 161)",
|
|
1668
|
+
"--success-subtle": "rgba(16, 185, 129, 0.24)",
|
|
1669
|
+
"--error": "rgb(218 52 55)",
|
|
1670
|
+
"--error-foreground": "rgb(255 255 255)",
|
|
1671
|
+
"--error-text": "rgb(255 179 173)",
|
|
1672
|
+
"--error-subtle": "rgba(239, 68, 68, 0.24)",
|
|
1673
|
+
"--warning": "rgb(163 103 0)",
|
|
1674
|
+
"--warning-foreground": "rgb(255 255 255)",
|
|
1675
|
+
"--warning-text": "rgb(255 185 95)",
|
|
1676
|
+
"--warning-subtle": "rgba(245, 158, 11, 0.24)",
|
|
1677
|
+
"--info": "rgb(33 112 228)",
|
|
1678
|
+
"--info-foreground": "rgb(255 255 255)",
|
|
1679
|
+
"--info-text": "rgb(173 198 255)",
|
|
1680
|
+
"--info-subtle": "rgba(59, 130, 246, 0.24)"
|
|
1561
1681
|
}
|
|
1562
1682
|
}
|
|
@@ -352,6 +352,76 @@ exports[`theme.colors snapshot for all presets (catches unintended palette shift
|
|
|
352
352
|
"tint": "rgb(0 120 108)",
|
|
353
353
|
"warning": "rgb(150 94 0)",
|
|
354
354
|
},
|
|
355
|
+
"mono/dark": {
|
|
356
|
+
"background": "rgb(14 14 14)",
|
|
357
|
+
"backgroundSecondary": "rgb(27 27 27)",
|
|
358
|
+
"backgroundTertiary": "rgb(31 31 31)",
|
|
359
|
+
"border": "rgb(71 71 71)",
|
|
360
|
+
"borderLight": "rgb(71 71 71)",
|
|
361
|
+
"card": "rgb(31 31 31)",
|
|
362
|
+
"contrast50": "rgb(42 42 42)",
|
|
363
|
+
"error": "rgb(218 52 55)",
|
|
364
|
+
"icon": "rgb(198 198 198)",
|
|
365
|
+
"iconActive": "rgb(238 238 238)",
|
|
366
|
+
"info": "rgb(33 112 228)",
|
|
367
|
+
"negative": "rgb(255 180 171)",
|
|
368
|
+
"negativeForeground": "rgb(105 0 5)",
|
|
369
|
+
"negativeSubtle": "rgba(239, 68, 68, 0.24)",
|
|
370
|
+
"negativeSubtleForeground": "rgb(255 179 173)",
|
|
371
|
+
"overlay": "rgba(0, 0, 0, 0.5)",
|
|
372
|
+
"primary": "rgb(238 238 238)",
|
|
373
|
+
"primaryDark": "rgb(14 14 14)",
|
|
374
|
+
"primaryForeground": "rgb(0 0 0)",
|
|
375
|
+
"primaryLight": "rgb(27 27 27)",
|
|
376
|
+
"primarySubtle": "rgba(145, 145, 145, 0.24)",
|
|
377
|
+
"primarySubtleForeground": "rgb(0 0 0)",
|
|
378
|
+
"secondary": "rgb(176 176 176)",
|
|
379
|
+
"secondaryForeground": "rgb(0 0 0)",
|
|
380
|
+
"shadow": "rgba(0, 0, 0, 0.3)",
|
|
381
|
+
"success": "rgb(0 133 91)",
|
|
382
|
+
"tertiary": "rgb(238 238 238)",
|
|
383
|
+
"tertiaryForeground": "rgb(0 0 0)",
|
|
384
|
+
"text": "rgb(226 226 226)",
|
|
385
|
+
"textSecondary": "rgb(198 198 198)",
|
|
386
|
+
"textTertiary": "rgb(145 145 145)",
|
|
387
|
+
"tint": "rgb(238 238 238)",
|
|
388
|
+
"warning": "rgb(163 103 0)",
|
|
389
|
+
},
|
|
390
|
+
"mono/light": {
|
|
391
|
+
"background": "rgb(243 243 243)",
|
|
392
|
+
"backgroundSecondary": "rgb(243 243 243)",
|
|
393
|
+
"backgroundTertiary": "rgb(238 238 238)",
|
|
394
|
+
"border": "rgb(198 198 198)",
|
|
395
|
+
"borderLight": "rgb(198 198 198)",
|
|
396
|
+
"card": "rgb(255 255 255)",
|
|
397
|
+
"contrast50": "rgb(232 232 232)",
|
|
398
|
+
"error": "rgb(203 41 46)",
|
|
399
|
+
"icon": "rgb(71 71 71)",
|
|
400
|
+
"iconActive": "rgb(31 31 31)",
|
|
401
|
+
"info": "rgb(5 102 217)",
|
|
402
|
+
"negative": "rgb(186 26 26)",
|
|
403
|
+
"negativeForeground": "rgb(255 255 255)",
|
|
404
|
+
"negativeSubtle": "rgba(239, 68, 68, 0.13)",
|
|
405
|
+
"negativeSubtleForeground": "rgb(167 55 53)",
|
|
406
|
+
"overlay": "rgba(0, 0, 0, 0.5)",
|
|
407
|
+
"primary": "rgb(31 31 31)",
|
|
408
|
+
"primaryDark": "rgb(243 243 243)",
|
|
409
|
+
"primaryForeground": "rgb(255 255 255)",
|
|
410
|
+
"primaryLight": "rgb(243 243 243)",
|
|
411
|
+
"primarySubtle": "rgba(145, 145, 145, 0.13)",
|
|
412
|
+
"primarySubtleForeground": "rgb(255 255 255)",
|
|
413
|
+
"secondary": "rgb(85 85 85)",
|
|
414
|
+
"secondaryForeground": "rgb(255 255 255)",
|
|
415
|
+
"shadow": "rgba(0, 0, 0, 0.1)",
|
|
416
|
+
"success": "rgb(0 122 83)",
|
|
417
|
+
"tertiary": "rgb(31 31 31)",
|
|
418
|
+
"tertiaryForeground": "rgb(255 255 255)",
|
|
419
|
+
"text": "rgb(27 27 27)",
|
|
420
|
+
"textSecondary": "rgb(71 71 71)",
|
|
421
|
+
"textTertiary": "rgb(119 119 119)",
|
|
422
|
+
"tint": "rgb(31 31 31)",
|
|
423
|
+
"warning": "rgb(150 94 0)",
|
|
424
|
+
},
|
|
355
425
|
"orange/dark": {
|
|
356
426
|
"background": "rgb(23 11 5)",
|
|
357
427
|
"backgroundSecondary": "rgb(38 24 17)",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every accent and status family is generated, not authored, so nothing else in
|
|
5
5
|
* the suite can notice when a tuning change makes a label unreadable. This walks
|
|
6
|
-
* the full matrix —
|
|
6
|
+
* the full matrix — every preset x 2 modes x 7 families — and asserts the property
|
|
7
7
|
* a user actually perceives.
|
|
8
8
|
*
|
|
9
9
|
* Two failure shapes it exists to catch, both of which have already happened:
|
|
@@ -124,7 +124,11 @@ describe('colour policy legibility', () => {
|
|
|
124
124
|
// Each half was individually legible, so nothing else could catch either.
|
|
125
125
|
it('the brand fill keeps light exemption-free and dark budgeted', () => {
|
|
126
126
|
const white = { light: 0, dark: 0 };
|
|
127
|
-
|
|
127
|
+
// `mono` is not governed by the budget — it has no chroma to preserve, and its
|
|
128
|
+
// dark fill is near-WHITE by design, so it takes a black label on purpose.
|
|
129
|
+
// Counting it here would let a real regression hide inside its slack.
|
|
130
|
+
const chromatic = APP_COLOR_NAMES.filter((name) => name !== 'mono');
|
|
131
|
+
for (const preset of chromatic) {
|
|
128
132
|
for (const mode of ['light', 'dark'] as const) {
|
|
129
133
|
if (getResolvedTokens(preset, mode)['--primary-foreground'] === 'rgb(255 255 255)') {
|
|
130
134
|
white[mode] += 1;
|
|
@@ -133,7 +137,11 @@ describe('colour policy legibility', () => {
|
|
|
133
137
|
}
|
|
134
138
|
// Light admits no exemption, so every preset carries white there. Dark keeps
|
|
135
139
|
// the budget, so the seeds that are already light keep their colour instead.
|
|
136
|
-
expect(white.light).toBe(
|
|
137
|
-
expect(white.dark).toBeGreaterThan(
|
|
140
|
+
expect(white.light).toBe(chromatic.length);
|
|
141
|
+
expect(white.dark).toBeGreaterThan(chromatic.length - 4);
|
|
142
|
+
// And the monochrome exception itself, stated rather than tolerated: a fill at
|
|
143
|
+
// each end of the scale, carrying the opposite label.
|
|
144
|
+
expect(getResolvedTokens('mono', 'light')['--primary-foreground']).toBe('rgb(255 255 255)');
|
|
145
|
+
expect(getResolvedTokens('mono', 'dark')['--primary-foreground']).toBe('rgb(0 0 0)');
|
|
138
146
|
});
|
|
139
147
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export { Hct } from './hct';
|
|
11
11
|
export { TonalPalette } from './tonal-palette';
|
|
12
12
|
export { DynamicScheme } from './dynamic-scheme';
|
|
13
|
-
export { buildScheme, schemeVibrant, schemeExpressive, schemeTonalSpot, schemeNeutral } from './scheme-variants';
|
|
13
|
+
export { buildScheme, schemeVibrant, schemeExpressive, schemeTonalSpot, schemeNeutral, schemeMonochrome } from './scheme-variants';
|
|
14
14
|
export type { SchemeVariant, AccentSources } from './scheme-variants';
|
|
15
15
|
export { Roles } from './color-roles';
|
|
16
16
|
export type { RoleName } from './color-roles';
|
|
@@ -13,7 +13,13 @@ import { sanitizeDegreesDouble } from './math-utils';
|
|
|
13
13
|
import { TonalPalette } from './tonal-palette';
|
|
14
14
|
import { Variant } from './variant';
|
|
15
15
|
|
|
16
|
-
export type SchemeVariant =
|
|
16
|
+
export type SchemeVariant =
|
|
17
|
+
| 'vivid'
|
|
18
|
+
| 'vibrant'
|
|
19
|
+
| 'expressive'
|
|
20
|
+
| 'tonalSpot'
|
|
21
|
+
| 'neutral'
|
|
22
|
+
| 'monochrome';
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Optional explicit accent seeds for a scheme. When a source HCT is supplied, its
|
|
@@ -191,6 +197,36 @@ export function schemeNeutral(
|
|
|
191
197
|
});
|
|
192
198
|
}
|
|
193
199
|
|
|
200
|
+
/**
|
|
201
|
+
* No colour at all: every palette is greyscale, so the seed's hue is irrelevant
|
|
202
|
+
* and only its tone survives. This is the variant the `isMonochrome` role
|
|
203
|
+
* branches in `color-roles` were written for — they push `primary` to the far end
|
|
204
|
+
* of the scale (tone 0 on a light page, 100 on a dark one) rather than the
|
|
205
|
+
* mid-tone the chromatic curves would pick, which is what keeps a black-and-white
|
|
206
|
+
* theme from reading as a grey one.
|
|
207
|
+
*
|
|
208
|
+
* A pinned accent is ignored on purpose: honouring it would put one coloured
|
|
209
|
+
* family back into a scheme whose entire point is that nothing is coloured.
|
|
210
|
+
*/
|
|
211
|
+
export function schemeMonochrome(
|
|
212
|
+
source: Hct,
|
|
213
|
+
isDark: boolean,
|
|
214
|
+
contrastLevel: number,
|
|
215
|
+
): DynamicScheme {
|
|
216
|
+
const grey = (): TonalPalette => TonalPalette.fromHueAndChroma(source.hue, 0);
|
|
217
|
+
return new DynamicScheme({
|
|
218
|
+
sourceColorArgb: source.toInt(),
|
|
219
|
+
variant: Variant.MONOCHROME,
|
|
220
|
+
contrastLevel,
|
|
221
|
+
isDark,
|
|
222
|
+
primaryPalette: grey(),
|
|
223
|
+
secondaryPalette: grey(),
|
|
224
|
+
tertiaryPalette: grey(),
|
|
225
|
+
neutralPalette: grey(),
|
|
226
|
+
neutralVariantPalette: grey(),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
194
230
|
const BUILDERS: Record<
|
|
195
231
|
SchemeVariant,
|
|
196
232
|
(s: Hct, d: boolean, c: number, accents: AccentSources) => DynamicScheme
|
|
@@ -200,6 +236,7 @@ const BUILDERS: Record<
|
|
|
200
236
|
expressive: schemeExpressive,
|
|
201
237
|
tonalSpot: schemeTonalSpot,
|
|
202
238
|
neutral: schemeNeutral,
|
|
239
|
+
monochrome: schemeMonochrome,
|
|
203
240
|
};
|
|
204
241
|
|
|
205
242
|
/**
|
|
@@ -131,6 +131,29 @@ const TEXT_CHROMA = 60;
|
|
|
131
131
|
const SUBTLE_SOURCE_TONE = 60;
|
|
132
132
|
const SUBTLE_ALPHA = { light: 0.13, dark: 0.24 } as const;
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A seed at or below this chroma has no colour to derive from, so the whole
|
|
136
|
+
* palette goes greyscale — the black-and-white theme. Keyed on the seed rather
|
|
137
|
+
* than on a flag, so picking any grey in a colour wheel lands there too instead
|
|
138
|
+
* of on a nearly-grey approximation of it.
|
|
139
|
+
*/
|
|
140
|
+
const MONOCHROME_MAX_CHROMA = 6;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether a seed carries no colour to derive a theme from — the one authority for
|
|
144
|
+
* that question, so the policy and the scheme choice can never disagree about a
|
|
145
|
+
* given seed and produce a grey palette wearing chromatic surfaces.
|
|
146
|
+
*/
|
|
147
|
+
export function isColourlessSeed(seedHex: string): boolean {
|
|
148
|
+
return Hct.fromInt(argbFromHex(seedHex)).chroma <= MONOCHROME_MAX_CHROMA;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Where a monochrome brand fill sits: near-black on a white page, near-white on a black one. */
|
|
152
|
+
const MONOCHROME_FILL_TONE = { light: 12, dark: 94 } as const;
|
|
153
|
+
|
|
154
|
+
/** The quiet member of a monochrome trio — the only one that is actually grey. */
|
|
155
|
+
const MONOCHROME_SECONDARY_TONE = { light: 36, dark: 72 } as const;
|
|
156
|
+
|
|
134
157
|
/** Split-complementary: the two accents sit either side of the seed's complement. */
|
|
135
158
|
const SPLIT_ROTATIONS = [150, 210] as const;
|
|
136
159
|
|
|
@@ -211,7 +234,8 @@ function vividHueNear(hue: number): number {
|
|
|
211
234
|
* ceasing to be itself: yellow turns to brown and lime to moss. Those keep their
|
|
212
235
|
* own tone and take a black label instead, which is what a bright yellow button
|
|
213
236
|
* wants anyway. The 25-tone budget is what separates the two cases, and it is
|
|
214
|
-
* why 11 of the 13 presets carry white and exactly the two light ones
|
|
237
|
+
* why 11 of the 13 chromatic presets carry white and exactly the two light ones
|
|
238
|
+
* do not. (`mono` sits outside this rule — see MONOCHROME_FILL_TONE.)
|
|
215
239
|
*/
|
|
216
240
|
function whiteLabelTone(palette: TonalPalette, seedTone: number, isDark: boolean): number {
|
|
217
241
|
// LIGHT: every fill comes down, no exemption. On a near-white page a bright
|
|
@@ -306,12 +330,24 @@ export function buildPolicyTokens(
|
|
|
306
330
|
// tone <= 49, so that is the ceiling — in dark as much as in light. Letting the
|
|
307
331
|
// dark fill sit at the seed's own tone instead makes it brighter, but every one
|
|
308
332
|
// of those labels turns black, which costs more than the brightness buys.
|
|
333
|
+
const monochrome = seed.chroma <= MONOCHROME_MAX_CHROMA;
|
|
309
334
|
// DARK keys off the seed itself so the fill IS the brand hex; LIGHT uses the
|
|
310
335
|
// max-chroma palette, which is what stops a deepened fill from going pastel.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
// A monochrome seed has no chroma to preserve either way, and its fill goes to
|
|
337
|
+
// the far end of the scale rather than the white-label ceiling: a mid-grey
|
|
338
|
+
// button reads as disabled, where near-black on white reads as the primary
|
|
339
|
+
// action.
|
|
340
|
+
const brandPalette = monochrome
|
|
341
|
+
? TonalPalette.fromHueAndChroma(0, 0)
|
|
342
|
+
: isDark
|
|
343
|
+
? TonalPalette.fromInt(argbFromHex(seedHex))
|
|
344
|
+
: TonalPalette.fromHueAndChroma(seed.hue, 200);
|
|
345
|
+
const primary = fillPair(
|
|
346
|
+
brandPalette,
|
|
347
|
+
monochrome
|
|
348
|
+
? (isDark ? MONOCHROME_FILL_TONE.dark : MONOCHROME_FILL_TONE.light)
|
|
349
|
+
: whiteLabelTone(brandPalette, seed.tone, isDark),
|
|
350
|
+
);
|
|
315
351
|
|
|
316
352
|
const tokens: PolicyTokens = {
|
|
317
353
|
'--primary': primary.fill,
|
|
@@ -320,7 +356,7 @@ export function buildPolicyTokens(
|
|
|
320
356
|
// does correctly, and which the fill must stop trying to do at the same time.
|
|
321
357
|
'--primary-text': roles.primary,
|
|
322
358
|
'--primary-subtle': rgba(
|
|
323
|
-
TonalPalette.fromHueAndChroma(seed.hue, 200).tone(SUBTLE_SOURCE_TONE),
|
|
359
|
+
TonalPalette.fromHueAndChroma(seed.hue, monochrome ? 0 : 200).tone(SUBTLE_SOURCE_TONE),
|
|
324
360
|
isDark ? SUBTLE_ALPHA.dark : SUBTLE_ALPHA.light,
|
|
325
361
|
),
|
|
326
362
|
'--ring': roles.primary,
|
|
@@ -332,7 +368,9 @@ export function buildPolicyTokens(
|
|
|
332
368
|
'--card': isDark ? roles.surfaceContainer : roles.surfaceContainerLowest,
|
|
333
369
|
// A touch darker than M3's page background: near-white reads as unfinished
|
|
334
370
|
// next to the card, and the extra step gives the surface ramp somewhere to sit.
|
|
335
|
-
'--background': rgb(
|
|
371
|
+
'--background': rgb(
|
|
372
|
+
TonalPalette.fromHueAndChroma(seed.hue, monochrome ? 0 : 10).tone(isDark ? 4 : 96),
|
|
373
|
+
),
|
|
336
374
|
// `--accent` stays what every consumer actually uses it for: a hover surface.
|
|
337
375
|
'--accent': roles.surfaceContainerHigh,
|
|
338
376
|
'--accent-foreground': roles.onSurfaceVariant,
|
|
@@ -344,9 +382,11 @@ export function buildPolicyTokens(
|
|
|
344
382
|
const hue = pin !== undefined ? Hct.fromInt(argbFromHex(pin)).hue : hues[role];
|
|
345
383
|
// One hue, three palettes. Every member shares it, so the family still reads
|
|
346
384
|
// as one colour; only how loudly each speaks differs.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
385
|
+
// A monochrome theme has no accents to rotate to; the trio becomes a ladder
|
|
386
|
+
// of greys, which still separates a standout button from a quiet one.
|
|
387
|
+
const fillPalette = TonalPalette.fromHueAndChroma(hue, monochrome ? 0 : ACCENT_CHROMA);
|
|
388
|
+
const textPalette = TonalPalette.fromHueAndChroma(hue, monochrome ? 0 : TEXT_CHROMA);
|
|
389
|
+
const vividPalette = TonalPalette.fromHueAndChroma(hue, monochrome ? 0 : 200);
|
|
350
390
|
// DARK sits at the hue's peak tone, floored by the same rule the brand fill
|
|
351
391
|
// uses: some hues peak dark (violet at 35), and rendering them there gives a
|
|
352
392
|
// slab rather than a highlight. LIGHT goes deep like every other fill, but
|
|
@@ -362,7 +402,20 @@ export function buildPolicyTokens(
|
|
|
362
402
|
// black. The accent chroma cap already keeps these hues out of neon territory,
|
|
363
403
|
// and the hue itself is the caller's brand rather than ours to move. The
|
|
364
404
|
// analyzer still runs where it was designed to, inside `color-roles`.
|
|
365
|
-
|
|
405
|
+
// Monochrome keeps `tertiary` AT the brand fill rather than a step off it:
|
|
406
|
+
// tertiary is what a compose button and a FAB paint with, and those are the
|
|
407
|
+
// primary action — a step of grey between them and `primary` reads as two
|
|
408
|
+
// buttons disagreeing, not as hierarchy. `secondary` carries the whole
|
|
409
|
+
// demotion instead.
|
|
410
|
+
const tone = monochrome
|
|
411
|
+
? role === 'tertiary'
|
|
412
|
+
? isDark
|
|
413
|
+
? MONOCHROME_FILL_TONE.dark
|
|
414
|
+
: MONOCHROME_FILL_TONE.light
|
|
415
|
+
: isDark
|
|
416
|
+
? MONOCHROME_SECONDARY_TONE.dark
|
|
417
|
+
: MONOCHROME_SECONDARY_TONE.light
|
|
418
|
+
: accentTone(hue, isDark);
|
|
366
419
|
const pair = fillPair(fillPalette, tone);
|
|
367
420
|
tokens[`--${role}`] = pair.fill;
|
|
368
421
|
tokens[`--${role}-foreground`] = pair.foreground;
|
|
@@ -377,9 +430,10 @@ export function buildPolicyTokens(
|
|
|
377
430
|
// The old ramp drew all five from the primary/secondary/tertiary trio, which
|
|
378
431
|
// spans ~35 degrees, so adjacent series were indistinguishable.
|
|
379
432
|
for (let i = 0; i < 5; i += 1) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
433
|
+
// Greyscale charts spread by TONE instead of hue — the only axis left.
|
|
434
|
+
tokens[`--chart-${i + 1}`] = monochrome
|
|
435
|
+
? rgb(TonalPalette.fromHueAndChroma(0, 0).tone(isDark ? 40 + i * 13 : 78 - i * 13))
|
|
436
|
+
: rgb(TonalPalette.fromHueAndChroma((seed.hue + i * 72) % 360, 60).tone(isDark ? 72 : 48));
|
|
383
437
|
}
|
|
384
438
|
|
|
385
439
|
// The status family, themed per mode. The four frozen hexes it replaces all
|
|
@@ -13,7 +13,8 @@ export type AppColorName =
|
|
|
13
13
|
| 'orange'
|
|
14
14
|
| 'mint'
|
|
15
15
|
| 'oxy'
|
|
16
|
-
| 'faircoin'
|
|
16
|
+
| 'faircoin'
|
|
17
|
+
| 'mono';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* A single colour preset: a brand SEED colour plus the tonal scheme variant to
|
|
@@ -56,7 +57,7 @@ export interface AppColorPreset {
|
|
|
56
57
|
*/
|
|
57
58
|
export type PresetTokens = Record<string, string>;
|
|
58
59
|
|
|
59
|
-
export const APP_COLOR_NAMES: readonly AppColorName[] = ['teal', 'blue', 'green', 'amber', 'yellow', 'red', 'purple', 'pink', 'sky', 'orange', 'mint', 'oxy', 'faircoin'];
|
|
60
|
+
export const APP_COLOR_NAMES: readonly AppColorName[] = ['teal', 'blue', 'green', 'amber', 'yellow', 'red', 'purple', 'pink', 'sky', 'orange', 'mint', 'oxy', 'faircoin', 'mono'];
|
|
60
61
|
|
|
61
62
|
/** Premium-exclusive presets, hidden from the standard color picker. */
|
|
62
63
|
export const PREMIUM_COLOR_NAMES: readonly AppColorName[] = ['oxy', 'faircoin'];
|
|
@@ -75,6 +76,7 @@ export const HEX_TO_APP_COLOR: Record<string, AppColorName> = {
|
|
|
75
76
|
'#14b8a6': 'mint',
|
|
76
77
|
'#c46ede': 'oxy',
|
|
77
78
|
'#9ffb50': 'faircoin',
|
|
79
|
+
'#000000': 'mono',
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
export function hexToAppColorName(hex: string): AppColorName {
|
|
@@ -101,4 +103,11 @@ export const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset> = {
|
|
|
101
103
|
mint: { name: 'mint', hex: '#14b8a6', variant: 'vivid' },
|
|
102
104
|
oxy: { name: 'oxy', hex: '#c46ede', variant: 'vivid' },
|
|
103
105
|
faircoin: { name: 'faircoin', hex: '#9ffb50', variant: 'vivid' },
|
|
106
|
+
/**
|
|
107
|
+
* No colour at all — the black-and-white theme. Its seed carries zero chroma,
|
|
108
|
+
* which is the whole mechanism: the policy derives a greyscale palette from any
|
|
109
|
+
* seed with none, so a user who picks a grey in the colour wheel lands here too
|
|
110
|
+
* rather than on a nearly-grey approximation.
|
|
111
|
+
*/
|
|
112
|
+
mono: { name: 'mono', hex: '#000000', variant: 'monochrome' },
|
|
104
113
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateRoleColors, type RoleColors, type SchemeVariant } from '../color-engine';
|
|
2
|
-
import { buildPolicyTokens } from '../color-policy';
|
|
2
|
+
import { buildPolicyTokens, isColourlessSeed } from '../color-policy';
|
|
3
3
|
import { CANONICAL_TOKENS } from '../token-registry';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -104,7 +104,11 @@ export interface SeedScopeOptions {
|
|
|
104
104
|
export function buildSeedScopeVars(options: SeedScopeOptions): Record<string, string> {
|
|
105
105
|
const roles = generateRoleColors({
|
|
106
106
|
seed: options.seed,
|
|
107
|
-
|
|
107
|
+
// A seed with no chroma has no colour for a chromatic variant to work with,
|
|
108
|
+
// so it takes the greyscale scheme unless the caller asked for something else.
|
|
109
|
+
// This is what makes a grey picked in a colour wheel produce the SAME
|
|
110
|
+
// black-and-white theme as the `mono` preset instead of a tinted near-grey.
|
|
111
|
+
variant: options.variant ?? (isColourlessSeed(options.seed) ? 'monochrome' : 'vivid'),
|
|
108
112
|
isDark: options.mode === 'dark',
|
|
109
113
|
contrastLevel: options.contrastLevel ?? 0,
|
|
110
114
|
secondarySeed: options.secondarySeed,
|