@oxyhq/bloom 0.41.0 → 0.42.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.
Files changed (143) hide show
  1. package/lib/commonjs/theme/BloomThemeProvider.js +18 -7
  2. package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
  3. package/lib/commonjs/theme/apply-dark-class.js +2 -2
  4. package/lib/commonjs/theme/apply-dark-class.js.map +1 -1
  5. package/lib/commonjs/theme/build-theme-from-seed.js +17 -5
  6. package/lib/commonjs/theme/build-theme-from-seed.js.map +1 -1
  7. package/lib/commonjs/theme/build-theme.js +9 -6
  8. package/lib/commonjs/theme/build-theme.js.map +1 -1
  9. package/lib/commonjs/theme/color-engine/index.js +8 -1
  10. package/lib/commonjs/theme/color-engine/index.js.map +1 -1
  11. package/lib/commonjs/theme/color-engine/scheme-variants.js +42 -18
  12. package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -1
  13. package/lib/commonjs/theme/color-engine/seed-from-image.js +11 -1
  14. package/lib/commonjs/theme/color-engine/seed-from-image.js.map +1 -1
  15. package/lib/commonjs/theme/color-presets.js.map +1 -1
  16. package/lib/commonjs/theme/color-scope/seed-scope.js +3 -1
  17. package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
  18. package/lib/commonjs/theme/color-scope/style-builder.js +2 -2
  19. package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
  20. package/lib/commonjs/theme/index.js.map +1 -1
  21. package/lib/commonjs/theme/native-root-vars.js +1 -1
  22. package/lib/commonjs/theme/native-root-vars.js.map +1 -1
  23. package/lib/commonjs/theme/native-root-vars.native.js +2 -2
  24. package/lib/commonjs/theme/native-root-vars.native.js.map +1 -1
  25. package/lib/commonjs/theme/native-root-vars.web.js +1 -1
  26. package/lib/commonjs/theme/native-root-vars.web.js.map +1 -1
  27. package/lib/commonjs/theme/preset-vars.js +16 -2
  28. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  29. package/lib/commonjs/theme/token-registry.js +2 -2
  30. package/lib/commonjs/theme/token-registry.js.map +1 -1
  31. package/lib/module/theme/BloomThemeProvider.js +18 -7
  32. package/lib/module/theme/BloomThemeProvider.js.map +1 -1
  33. package/lib/module/theme/apply-dark-class.js +2 -2
  34. package/lib/module/theme/apply-dark-class.js.map +1 -1
  35. package/lib/module/theme/build-theme-from-seed.js +18 -5
  36. package/lib/module/theme/build-theme-from-seed.js.map +1 -1
  37. package/lib/module/theme/build-theme.js +9 -6
  38. package/lib/module/theme/build-theme.js.map +1 -1
  39. package/lib/module/theme/color-engine/index.js +8 -1
  40. package/lib/module/theme/color-engine/index.js.map +1 -1
  41. package/lib/module/theme/color-engine/scheme-variants.js +43 -18
  42. package/lib/module/theme/color-engine/scheme-variants.js.map +1 -1
  43. package/lib/module/theme/color-engine/seed-from-image.js +11 -1
  44. package/lib/module/theme/color-engine/seed-from-image.js.map +1 -1
  45. package/lib/module/theme/color-presets.js.map +1 -1
  46. package/lib/module/theme/color-scope/seed-scope.js +3 -1
  47. package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
  48. package/lib/module/theme/color-scope/style-builder.js +2 -2
  49. package/lib/module/theme/color-scope/style-builder.js.map +1 -1
  50. package/lib/module/theme/index.js.map +1 -1
  51. package/lib/module/theme/native-root-vars.js +1 -1
  52. package/lib/module/theme/native-root-vars.js.map +1 -1
  53. package/lib/module/theme/native-root-vars.native.js +2 -2
  54. package/lib/module/theme/native-root-vars.native.js.map +1 -1
  55. package/lib/module/theme/native-root-vars.web.js +1 -1
  56. package/lib/module/theme/native-root-vars.web.js.map +1 -1
  57. package/lib/module/theme/preset-vars.js +17 -2
  58. package/lib/module/theme/preset-vars.js.map +1 -1
  59. package/lib/module/theme/token-registry.js +2 -2
  60. package/lib/module/theme/token-registry.js.map +1 -1
  61. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts +15 -1
  62. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts +2 -1
  64. package/lib/typescript/commonjs/theme/apply-dark-class.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts +13 -2
  66. package/lib/typescript/commonjs/theme/build-theme-from-seed.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/theme/build-theme.d.ts +2 -1
  68. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/theme/color-engine/index.d.ts +17 -1
  70. package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +25 -7
  72. package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts +11 -1
  74. package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/theme/color-presets.d.ts +14 -0
  76. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts +12 -0
  78. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts +2 -1
  80. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/theme/index.d.ts +3 -1
  82. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/theme/native-root-vars.d.ts +2 -1
  84. package/lib/typescript/commonjs/theme/native-root-vars.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts +2 -1
  86. package/lib/typescript/commonjs/theme/native-root-vars.native.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/theme/native-root-vars.web.d.ts +2 -1
  88. package/lib/typescript/commonjs/theme/native-root-vars.web.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/theme/preset-vars.d.ts +12 -1
  90. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/theme/token-registry.d.ts +2 -1
  92. package/lib/typescript/commonjs/theme/token-registry.d.ts.map +1 -1
  93. package/lib/typescript/module/theme/BloomThemeProvider.d.ts +15 -1
  94. package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
  95. package/lib/typescript/module/theme/apply-dark-class.d.ts +2 -1
  96. package/lib/typescript/module/theme/apply-dark-class.d.ts.map +1 -1
  97. package/lib/typescript/module/theme/build-theme-from-seed.d.ts +13 -2
  98. package/lib/typescript/module/theme/build-theme-from-seed.d.ts.map +1 -1
  99. package/lib/typescript/module/theme/build-theme.d.ts +2 -1
  100. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  101. package/lib/typescript/module/theme/color-engine/index.d.ts +17 -1
  102. package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -1
  103. package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +25 -7
  104. package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -1
  105. package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts +11 -1
  106. package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts.map +1 -1
  107. package/lib/typescript/module/theme/color-presets.d.ts +14 -0
  108. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  109. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts +12 -0
  110. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
  111. package/lib/typescript/module/theme/color-scope/style-builder.d.ts +2 -1
  112. package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
  113. package/lib/typescript/module/theme/index.d.ts +3 -1
  114. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  115. package/lib/typescript/module/theme/native-root-vars.d.ts +2 -1
  116. package/lib/typescript/module/theme/native-root-vars.d.ts.map +1 -1
  117. package/lib/typescript/module/theme/native-root-vars.native.d.ts +2 -1
  118. package/lib/typescript/module/theme/native-root-vars.native.d.ts.map +1 -1
  119. package/lib/typescript/module/theme/native-root-vars.web.d.ts +2 -1
  120. package/lib/typescript/module/theme/native-root-vars.web.d.ts.map +1 -1
  121. package/lib/typescript/module/theme/preset-vars.d.ts +12 -1
  122. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  123. package/lib/typescript/module/theme/token-registry.d.ts +2 -1
  124. package/lib/typescript/module/theme/token-registry.d.ts.map +1 -1
  125. package/package.json +1 -1
  126. package/src/__tests__/theme.test.ts +3 -0
  127. package/src/theme/BloomThemeProvider.tsx +38 -8
  128. package/src/theme/__tests__/explicit-accents.test.ts +129 -0
  129. package/src/theme/apply-dark-class.ts +7 -2
  130. package/src/theme/build-theme-from-seed.ts +25 -2
  131. package/src/theme/build-theme.ts +9 -3
  132. package/src/theme/color-engine/index.ts +27 -1
  133. package/src/theme/color-engine/scheme-variants.ts +108 -30
  134. package/src/theme/color-engine/seed-from-image.ts +11 -1
  135. package/src/theme/color-presets.ts +14 -0
  136. package/src/theme/color-scope/seed-scope.ts +14 -0
  137. package/src/theme/color-scope/style-builder.ts +3 -1
  138. package/src/theme/index.ts +3 -1
  139. package/src/theme/native-root-vars.native.ts +7 -2
  140. package/src/theme/native-root-vars.ts +6 -1
  141. package/src/theme/native-root-vars.web.ts +6 -1
  142. package/src/theme/preset-vars.ts +19 -0
  143. package/src/theme/token-registry.ts +3 -2
@@ -15,6 +15,29 @@ import { Variant } from './variant';
15
15
 
16
16
  export type SchemeVariant = 'vivid' | 'vibrant' | 'expressive' | 'tonalSpot' | 'neutral';
17
17
 
18
+ /**
19
+ * Optional explicit accent seeds for a scheme. When a source HCT is supplied, its
20
+ * palette is built from that colour itself (keeping the brand hue + chroma) instead
21
+ * of the derived hue-rotation — so a brand with a distinct secondary/tertiary accent
22
+ * (e.g. blue primary + yellow secondary) can PIN its real accent family. The M3 role
23
+ * tones still apply on top (a pinned yellow yields a yellow-hued secondary family:
24
+ * light container, dark on-colour) — that is correct M3 behaviour.
25
+ */
26
+ export interface AccentSources {
27
+ secondarySource?: Hct;
28
+ tertiarySource?: Hct;
29
+ }
30
+
31
+ /**
32
+ * Build the accent tonal palette for a role: from the pinned brand colour when a
33
+ * `source` is provided (preserving its hue + chroma), otherwise from the caller's
34
+ * derived rotation. Uses `TonalPalette.fromInt` so the palette keeps the exact
35
+ * brand key colour; the role tones apply the same way for both.
36
+ */
37
+ function accentPalette(source: Hct | undefined, derived: () => TonalPalette): TonalPalette {
38
+ return source !== undefined ? TonalPalette.fromInt(source.toInt()) : derived();
39
+ }
40
+
18
41
  const VIBRANT_HUES = [0.0, 41.0, 61.0, 101.0, 131.0, 181.0, 251.0, 301.0, 360.0];
19
42
  const VIBRANT_SECONDARY_ROTATIONS = [18.0, 15.0, 10.0, 12.0, 15.0, 18.0, 15.0, 12.0, 12.0];
20
43
  const VIBRANT_TERTIARY_ROTATIONS = [35.0, 30.0, 20.0, 25.0, 30.0, 35.0, 30.0, 25.0, 25.0];
@@ -24,20 +47,29 @@ const EXPRESSIVE_SECONDARY_ROTATIONS = [45.0, 95.0, 45.0, 20.0, 45.0, 90.0, 45.0
24
47
  const EXPRESSIVE_TERTIARY_ROTATIONS = [120.0, 120.0, 20.0, 45.0, 20.0, 15.0, 20.0, 120.0, 120.0];
25
48
 
26
49
  /** Maxed-out colourfulness at each position of the primary palette. */
27
- export function schemeVibrant(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
50
+ export function schemeVibrant(
51
+ source: Hct,
52
+ isDark: boolean,
53
+ contrastLevel: number,
54
+ accents: AccentSources = {},
55
+ ): DynamicScheme {
28
56
  return new DynamicScheme({
29
57
  sourceColorArgb: source.toInt(),
30
58
  variant: Variant.VIBRANT,
31
59
  contrastLevel,
32
60
  isDark,
33
61
  primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 200.0),
34
- secondaryPalette: TonalPalette.fromHueAndChroma(
35
- DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
36
- 24.0,
62
+ secondaryPalette: accentPalette(accents.secondarySource, () =>
63
+ TonalPalette.fromHueAndChroma(
64
+ DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
65
+ 24.0,
66
+ ),
37
67
  ),
38
- tertiaryPalette: TonalPalette.fromHueAndChroma(
39
- DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
40
- 32.0,
68
+ tertiaryPalette: accentPalette(accents.tertiarySource, () =>
69
+ TonalPalette.fromHueAndChroma(
70
+ DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
71
+ 32.0,
72
+ ),
41
73
  ),
42
74
  neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 10.0),
43
75
  neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
@@ -53,40 +85,58 @@ export function schemeVibrant(source: Hct, isDark: boolean, contrastLevel: numbe
53
85
  * old hand-authored palette felt more vivid than pure M3, and this restores that
54
86
  * across the whole primary/secondary/tertiary trio.
55
87
  */
56
- export function schemeVivid(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
88
+ export function schemeVivid(
89
+ source: Hct,
90
+ isDark: boolean,
91
+ contrastLevel: number,
92
+ accents: AccentSources = {},
93
+ ): DynamicScheme {
57
94
  return new DynamicScheme({
58
95
  sourceColorArgb: source.toInt(),
59
96
  variant: Variant.VIBRANT,
60
97
  contrastLevel,
61
98
  isDark,
62
99
  primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 200.0),
63
- secondaryPalette: TonalPalette.fromHueAndChroma(
64
- DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
65
- 64.0,
100
+ secondaryPalette: accentPalette(accents.secondarySource, () =>
101
+ TonalPalette.fromHueAndChroma(
102
+ DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
103
+ 64.0,
104
+ ),
66
105
  ),
67
- tertiaryPalette: TonalPalette.fromHueAndChroma(
68
- DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
69
- 72.0,
106
+ tertiaryPalette: accentPalette(accents.tertiarySource, () =>
107
+ TonalPalette.fromHueAndChroma(
108
+ DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
109
+ 72.0,
110
+ ),
70
111
  ),
71
112
  neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 10.0),
72
113
  neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
73
114
  });
74
115
  }
75
116
 
76
- export function schemeExpressive(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
117
+ export function schemeExpressive(
118
+ source: Hct,
119
+ isDark: boolean,
120
+ contrastLevel: number,
121
+ accents: AccentSources = {},
122
+ ): DynamicScheme {
77
123
  return new DynamicScheme({
78
124
  sourceColorArgb: source.toInt(),
79
125
  variant: Variant.EXPRESSIVE,
80
126
  contrastLevel,
81
127
  isDark,
82
128
  primaryPalette: TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 240.0), 40.0),
83
- secondaryPalette: TonalPalette.fromHueAndChroma(
84
- DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_SECONDARY_ROTATIONS),
85
- 24.0,
129
+ secondaryPalette: accentPalette(accents.secondarySource, () =>
130
+ TonalPalette.fromHueAndChroma(
131
+ DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_SECONDARY_ROTATIONS),
132
+ 24.0,
133
+ ),
86
134
  ),
87
- tertiaryPalette: TonalPalette.fromHueAndChroma(
88
- DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_TERTIARY_ROTATIONS),
89
- 32.0,
135
+ tertiaryPalette: accentPalette(accents.tertiarySource, () =>
136
+ TonalPalette.fromHueAndChroma(
137
+ DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_TERTIARY_ROTATIONS),
138
+ 32.0,
139
+ ),
90
140
  ),
91
141
  neutralPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 8.0),
92
142
  neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 12.0),
@@ -94,36 +144,57 @@ export function schemeExpressive(source: Hct, isDark: boolean, contrastLevel: nu
94
144
  }
95
145
 
96
146
  /** The Material You default — a calm, balanced treatment of the seed. */
97
- export function schemeTonalSpot(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
147
+ export function schemeTonalSpot(
148
+ source: Hct,
149
+ isDark: boolean,
150
+ contrastLevel: number,
151
+ accents: AccentSources = {},
152
+ ): DynamicScheme {
98
153
  return new DynamicScheme({
99
154
  sourceColorArgb: source.toInt(),
100
155
  variant: Variant.TONAL_SPOT,
101
156
  contrastLevel,
102
157
  isDark,
103
158
  primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 36.0),
104
- secondaryPalette: TonalPalette.fromHueAndChroma(source.hue, 16.0),
105
- tertiaryPalette: TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 60.0), 24.0),
159
+ secondaryPalette: accentPalette(accents.secondarySource, () =>
160
+ TonalPalette.fromHueAndChroma(source.hue, 16.0),
161
+ ),
162
+ tertiaryPalette: accentPalette(accents.tertiarySource, () =>
163
+ TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 60.0), 24.0),
164
+ ),
106
165
  neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 6.0),
107
166
  neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 8.0),
108
167
  });
109
168
  }
110
169
 
111
170
  /** Near-greyscale — the seed hue is present only as a whisper. */
112
- export function schemeNeutral(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
171
+ export function schemeNeutral(
172
+ source: Hct,
173
+ isDark: boolean,
174
+ contrastLevel: number,
175
+ accents: AccentSources = {},
176
+ ): DynamicScheme {
113
177
  return new DynamicScheme({
114
178
  sourceColorArgb: source.toInt(),
115
179
  variant: Variant.NEUTRAL,
116
180
  contrastLevel,
117
181
  isDark,
118
182
  primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
119
- secondaryPalette: TonalPalette.fromHueAndChroma(source.hue, 8.0),
120
- tertiaryPalette: TonalPalette.fromHueAndChroma(source.hue, 16.0),
183
+ secondaryPalette: accentPalette(accents.secondarySource, () =>
184
+ TonalPalette.fromHueAndChroma(source.hue, 8.0),
185
+ ),
186
+ tertiaryPalette: accentPalette(accents.tertiarySource, () =>
187
+ TonalPalette.fromHueAndChroma(source.hue, 16.0),
188
+ ),
121
189
  neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
122
190
  neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
123
191
  });
124
192
  }
125
193
 
126
- const BUILDERS: Record<SchemeVariant, (s: Hct, d: boolean, c: number) => DynamicScheme> = {
194
+ const BUILDERS: Record<
195
+ SchemeVariant,
196
+ (s: Hct, d: boolean, c: number, accents: AccentSources) => DynamicScheme
197
+ > = {
127
198
  vivid: schemeVivid,
128
199
  vibrant: schemeVibrant,
129
200
  expressive: schemeExpressive,
@@ -131,12 +202,19 @@ const BUILDERS: Record<SchemeVariant, (s: Hct, d: boolean, c: number) => Dynamic
131
202
  neutral: schemeNeutral,
132
203
  };
133
204
 
134
- /** Build a scheme by variant name. */
205
+ /**
206
+ * Build a scheme by variant name.
207
+ *
208
+ * `accents` optionally pins the secondary and/or tertiary palette to an explicit
209
+ * brand colour (see {@link AccentSources}); omitted accents keep the variant's
210
+ * derived hue-rotation, so existing callers get byte-identical output.
211
+ */
135
212
  export function buildScheme(
136
213
  variant: SchemeVariant,
137
214
  source: Hct,
138
215
  isDark: boolean,
139
216
  contrastLevel = 0,
217
+ accents: AccentSources = {},
140
218
  ): DynamicScheme {
141
- return BUILDERS[variant](source, isDark, contrastLevel);
219
+ return BUILDERS[variant](source, isDark, contrastLevel, accents);
142
220
  }
@@ -51,7 +51,17 @@ export function quantizeImage(pixels: number[], maxColors = 128): Map<number, nu
51
51
  return result;
52
52
  }
53
53
 
54
- /** Ranked seed candidates (best first) extracted from an image's pixels (ARGB). */
54
+ /**
55
+ * Ranked seed candidates (best first) extracted from an image's pixels (ARGB).
56
+ *
57
+ * The top result (`seeds[0]`) is the primary seed for `generateRoleColors({ seed })`.
58
+ * To derive a richer, brand-accurate theme from artwork, a caller can additionally
59
+ * pin distinct accents from the ranked list: pass `hexFromArgb(seeds[1])` as
60
+ * `secondarySeed` and `hexFromArgb(seeds[2])` as `tertiarySeed` to
61
+ * `generateRoleColors` / `buildSeedScopeVars` / `buildThemeFromSeed` so the
62
+ * secondary and tertiary families reflect the artwork's real supporting colours
63
+ * instead of hue-rotations of the primary.
64
+ */
55
65
  export function seedsFromImagePixels(pixels: number[], options?: ScoreOptions): number[] {
56
66
  return score(quantizeImage(pixels), options);
57
67
  }
@@ -32,6 +32,20 @@ export interface AppColorPreset {
32
32
  hex: string;
33
33
  /** The tonal scheme variant the engine derives roles with. */
34
34
  variant: SchemeVariant;
35
+ /**
36
+ * Optional explicit secondary-accent seed, `#rrggbb`. When set, the secondary
37
+ * palette is PINNED to this brand colour (hue + chroma preserved; M3 role tones
38
+ * still apply) instead of the derived hue-rotation of {@link AppColorPreset.hex}.
39
+ * Bloom's built-in presets do NOT set this, so their generated output is
40
+ * unchanged; a consumer brand with a distinct accent (blue primary + yellow
41
+ * secondary) sets it to pin its real accent family.
42
+ */
43
+ secondaryHex?: string;
44
+ /**
45
+ * Optional explicit tertiary-accent seed, `#rrggbb`. Same semantics as
46
+ * {@link AppColorPreset.secondaryHex} for the tertiary palette.
47
+ */
48
+ tertiaryHex?: string;
35
49
  }
36
50
 
37
51
  /**
@@ -72,6 +72,18 @@ export interface SeedScopeOptions {
72
72
  variant?: SchemeVariant;
73
73
  /** −1 (low) … 0 (normal) … 1 (high). Defaults to 0. */
74
74
  contrastLevel?: number;
75
+ /**
76
+ * Optional explicit secondary-accent seed, `#rrggbb`. Pins the secondary palette
77
+ * to this brand colour instead of the derived rotation (M3 role tones still
78
+ * apply). Handy for an artwork-extracted theme: pass the second-ranked seed from
79
+ * `seedsFromImagePixels` (`seeds[1]`) as a distinct secondary accent.
80
+ */
81
+ secondarySeed?: string;
82
+ /**
83
+ * Optional explicit tertiary-accent seed, `#rrggbb`. Same semantics; pass
84
+ * `seedsFromImagePixels`' `seeds[2]` for a third artwork-derived accent.
85
+ */
86
+ tertiarySeed?: string;
75
87
  }
76
88
 
77
89
  /**
@@ -92,6 +104,8 @@ export function buildSeedScopeVars(options: SeedScopeOptions): Record<string, st
92
104
  variant: options.variant ?? 'vivid',
93
105
  isDark: options.mode === 'dark',
94
106
  contrastLevel: options.contrastLevel ?? 0,
107
+ secondarySeed: options.secondarySeed,
108
+ tertiarySeed: options.tertiarySeed,
95
109
  });
96
110
  const tokens = roleColorsToPresetTokens(roles);
97
111
  const vars: Record<string, string> = { ...tokens };
@@ -3,6 +3,7 @@ import { Platform, type StyleProp, type ViewStyle } from 'react-native';
3
3
 
4
4
  import { CANONICAL_TOKENS, getResolvedTokens } from '../token-registry';
5
5
  import type { AppColorName } from '../color-presets';
6
+ import type { ExplicitAccents } from '../preset-vars';
6
7
  import { lazyRequire } from '../../utils/lazy-require';
7
8
 
8
9
  /**
@@ -36,8 +37,9 @@ const getNativeWindVars = lazyRequire<NativeWindVarsModule>('nativewind');
36
37
  export function buildScopeVars(
37
38
  colorPreset: AppColorName,
38
39
  mode: 'light' | 'dark',
40
+ accents?: ExplicitAccents,
39
41
  ): Record<string, string> {
40
- const tokens = getResolvedTokens(colorPreset, mode);
42
+ const tokens = getResolvedTokens(colorPreset, mode, accents);
41
43
  const vars: Record<string, string> = { ...tokens };
42
44
 
43
45
  for (const token of CANONICAL_TOKENS) {
@@ -24,9 +24,10 @@ export {
24
24
  greenFromArgb,
25
25
  blueFromArgb,
26
26
  } from './color-engine';
27
- export type { RoleColors, GenerateOptions, RoleName, SchemeVariant } from './color-engine';
27
+ export type { RoleColors, GenerateOptions, RoleName, SchemeVariant, AccentSources } from './color-engine';
28
28
  export { buildTheme, STATUS_COLORS } from './build-theme';
29
29
  export { buildThemeFromSeed, buildColorsFromSeed } from './build-theme-from-seed';
30
+ export type { SeedAccents } from './build-theme-from-seed';
30
31
  export { THEME_GRADIENTS } from './gradients';
31
32
  export { useTheme, useThemeColor, useBloomTheme } from './use-theme';
32
33
  export { useNavigationTheme } from './use-navigation-theme';
@@ -43,6 +44,7 @@ export {
43
44
  export { parseRgb, withAlpha } from './color-utils';
44
45
  export type { RgbChannels } from './color-utils';
45
46
  export { getPresetVars, applyPresetVarsToDocument } from './preset-vars';
47
+ export type { ExplicitAccents } from './preset-vars';
46
48
  export { applyDarkClass } from './apply-dark-class';
47
49
  export { setColorSchemeSafe } from './set-color-scheme-safe';
48
50
  export { initCssInteropDarkMode } from './init-css-interop';
@@ -17,6 +17,7 @@
17
17
  import { rootVariables } from 'react-native-css/native-internal';
18
18
 
19
19
  import { type AppColorName } from './color-presets';
20
+ import type { ExplicitAccents } from './preset-vars';
20
21
  import { getResolvedTokens } from './token-registry';
21
22
 
22
23
  /**
@@ -121,10 +122,14 @@ import { getResolvedTokens } from './token-registry';
121
122
  * Defensive guard: if a host ships a react-native-css build that doesn't expose
122
123
  * `rootVariables` as a callable (it always does on @3), we bail rather than throw.
123
124
  */
124
- export function applyNativeRootVars(colorPreset: AppColorName, mode: 'light' | 'dark'): void {
125
+ export function applyNativeRootVars(
126
+ colorPreset: AppColorName,
127
+ mode: 'light' | 'dark',
128
+ accents?: ExplicitAccents,
129
+ ): void {
125
130
  if (typeof rootVariables !== 'function') return;
126
131
 
127
- const vars = getResolvedTokens(colorPreset, mode);
132
+ const vars = getResolvedTokens(colorPreset, mode, accents);
128
133
  for (const [key, value] of Object.entries(vars)) {
129
134
  // The family is keyed by the bare name; `getResolvedTokens` keys include `--`.
130
135
  const name = key.startsWith('--') ? key.slice(2) : key;
@@ -1,4 +1,5 @@
1
1
  import { type AppColorName } from './color-presets';
2
+ import type { ExplicitAccents } from './preset-vars';
2
3
 
3
4
  /**
4
5
  * Platform-neutral DEFAULT variant of `applyNativeRootVars` — a no-op.
@@ -30,7 +31,11 @@ import { type AppColorName } from './color-presets';
30
31
  * The signature is intentionally identical to those variants so the import site in
31
32
  * `BloomThemeProvider` is platform-agnostic.
32
33
  */
33
- export function applyNativeRootVars(_colorPreset: AppColorName, _mode: 'light' | 'dark'): void {
34
+ export function applyNativeRootVars(
35
+ _colorPreset: AppColorName,
36
+ _mode: 'light' | 'dark',
37
+ _accents?: ExplicitAccents,
38
+ ): void {
34
39
  // Intentionally empty — the native variant (`native-root-vars.native.ts`) does
35
40
  // the real work; this neutral default and the web fork are inert.
36
41
  }
@@ -1,4 +1,5 @@
1
1
  import { type AppColorName } from './color-presets';
2
+ import type { ExplicitAccents } from './preset-vars';
2
3
 
3
4
  /**
4
5
  * Web fork of `applyNativeRootVars` — a no-op.
@@ -17,6 +18,10 @@ import { type AppColorName } from './color-presets';
17
18
  * The signature is intentionally identical to the plain variant so the import
18
19
  * site in `BloomThemeProvider` is platform-agnostic.
19
20
  */
20
- export function applyNativeRootVars(_colorPreset: AppColorName, _mode: 'light' | 'dark'): void {
21
+ export function applyNativeRootVars(
22
+ _colorPreset: AppColorName,
23
+ _mode: 'light' | 'dark',
24
+ _accents?: ExplicitAccents,
25
+ ): void {
21
26
  // Intentionally empty — web applies preset vars to `document.documentElement`.
22
27
  }
@@ -1,5 +1,17 @@
1
1
  import { APP_COLOR_PRESETS, type AppColorName, type PresetTokens } from './color-presets';
2
2
  import { generateRoleColors, type RoleColors } from './color-engine';
3
+
4
+ /**
5
+ * Optional explicit accent-colour overrides (`#rrggbb`) that PIN the secondary
6
+ * and/or tertiary palette app-wide, regardless of the active preset's own seed.
7
+ * Threaded from `BloomThemeProvider`'s `secondaryColor` / `tertiaryColor` props
8
+ * down through the whole token pipeline. When both are omitted, the preset's own
9
+ * (usually derived) accents are used — output is unchanged.
10
+ */
11
+ export interface ExplicitAccents {
12
+ secondaryHex?: string;
13
+ tertiaryHex?: string;
14
+ }
3
15
  // Circular at the module graph level (`token-registry` imports `getPresetVars`
4
16
  // from here), but safe: `getResolvedTokens` is only referenced inside the
5
17
  // `applyPresetVarsToDocument` function body — i.e. at call time, after both
@@ -26,12 +38,19 @@ import { getResolvedTokens } from './token-registry';
26
38
  export function getPresetVars(
27
39
  colorName: AppColorName,
28
40
  mode: 'light' | 'dark',
41
+ accents?: ExplicitAccents,
29
42
  ): PresetTokens {
30
43
  const preset = APP_COLOR_PRESETS[colorName];
31
44
  const r: RoleColors = generateRoleColors({
32
45
  seed: preset.hex,
33
46
  variant: preset.variant,
34
47
  isDark: mode === 'dark',
48
+ // Pinned accents: an explicit app-wide override (from the provider) wins,
49
+ // else the preset's own declared accent, else undefined → derived rotation.
50
+ // The built-in presets declare none, so with no override their generated
51
+ // tokens stay byte-identical.
52
+ secondarySeed: accents?.secondaryHex ?? preset.secondaryHex,
53
+ tertiarySeed: accents?.tertiaryHex ?? preset.tertiaryHex,
35
54
  });
36
55
 
37
56
  return {
@@ -1,5 +1,5 @@
1
1
  import { APP_COLOR_NAMES, type AppColorName } from './color-presets';
2
- import { getPresetVars } from './preset-vars';
2
+ import { getPresetVars, type ExplicitAccents } from './preset-vars';
3
3
 
4
4
  /**
5
5
  * The canonical set of Bloom theme token names — WITHOUT the leading `--`.
@@ -149,8 +149,9 @@ export function hslToSrgb(triple: string): string {
149
149
  export function getResolvedTokens(
150
150
  preset: AppColorName,
151
151
  mode: 'light' | 'dark',
152
+ accents?: ExplicitAccents,
152
153
  ): Record<string, string> {
153
- const raw = getPresetVars(preset, mode);
154
+ const raw = getPresetVars(preset, mode, accents);
154
155
  const resolved: Record<string, string> = {};
155
156
  for (const [key, value] of Object.entries(raw)) {
156
157
  resolved[key] = RAW_HSL_TRIPLE.test(value) ? hslToSrgb(value) : value;