@oxyhq/bloom 1.0.0 → 1.0.2

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 (218) hide show
  1. package/README.md +16 -4
  2. package/docs/accordion.mdx +69 -0
  3. package/docs/activity-heatmap.mdx +71 -0
  4. package/docs/admonition.mdx +77 -0
  5. package/docs/alert-dialog.mdx +75 -0
  6. package/docs/alert.mdx +93 -0
  7. package/docs/animated-check.mdx +53 -0
  8. package/docs/aspect-ratio.mdx +45 -0
  9. package/docs/avatar-group.mdx +91 -0
  10. package/docs/avatar.mdx +126 -0
  11. package/docs/badge.mdx +74 -0
  12. package/docs/benefit-list.mdx +56 -0
  13. package/docs/bottom-sheet.mdx +103 -0
  14. package/docs/button.mdx +218 -0
  15. package/docs/card.mdx +106 -0
  16. package/docs/checkbox.mdx +59 -0
  17. package/docs/chip.mdx +53 -0
  18. package/docs/code.mdx +50 -0
  19. package/docs/combobox.mdx +64 -0
  20. package/docs/command.mdx +85 -0
  21. package/docs/composition-bar.mdx +63 -0
  22. package/docs/connection-dots.mdx +50 -0
  23. package/docs/connection-status.mdx +56 -0
  24. package/docs/content-panel.mdx +62 -0
  25. package/docs/context-menu.mdx +65 -0
  26. package/docs/design-tokens.mdx +254 -0
  27. package/docs/dialog.mdx +277 -0
  28. package/docs/divider.mdx +56 -0
  29. package/docs/dot-grid-meter.mdx +66 -0
  30. package/docs/dropdown-menu.mdx +161 -0
  31. package/docs/error-boundary.mdx +72 -0
  32. package/docs/fab.mdx +67 -0
  33. package/docs/field.mdx +70 -0
  34. package/docs/fill.mdx +58 -0
  35. package/docs/fonts.mdx +144 -0
  36. package/docs/frosted-icon-button.mdx +60 -0
  37. package/docs/getting-started.mdx +75 -0
  38. package/docs/grid.mdx +64 -0
  39. package/docs/hooks.mdx +81 -0
  40. package/docs/icon-circle.mdx +66 -0
  41. package/docs/icons.mdx +69 -0
  42. package/docs/image-aspect-ratio-cache.mdx +63 -0
  43. package/docs/image-resolver.mdx +66 -0
  44. package/docs/index.mdx +44 -0
  45. package/docs/input-group.mdx +58 -0
  46. package/docs/item.mdx +69 -0
  47. package/docs/kbd.mdx +57 -0
  48. package/docs/label.mdx +50 -0
  49. package/docs/link-preview.mdx +66 -0
  50. package/docs/list.mdx +72 -0
  51. package/docs/loading.mdx +129 -0
  52. package/docs/media-inset-border.mdx +52 -0
  53. package/docs/menubar.mdx +86 -0
  54. package/docs/migrating-to-1.0.mdx +434 -0
  55. package/docs/motion.mdx +83 -0
  56. package/docs/overlay.mdx +75 -0
  57. package/docs/popover.mdx +73 -0
  58. package/docs/portal.mdx +63 -0
  59. package/docs/pressable-scale.mdx +51 -0
  60. package/docs/profile-card.mdx +66 -0
  61. package/docs/progressive-blur.mdx +54 -0
  62. package/docs/prompt-input.mdx +183 -0
  63. package/docs/provider.mdx +64 -0
  64. package/docs/radio-indicator.mdx +60 -0
  65. package/docs/radio.mdx +65 -0
  66. package/docs/scroll.mdx +66 -0
  67. package/docs/search.mdx +67 -0
  68. package/docs/segmented-control.mdx +76 -0
  69. package/docs/select.mdx +93 -0
  70. package/docs/settings-list.mdx +186 -0
  71. package/docs/skeleton.mdx +66 -0
  72. package/docs/slider.mdx +73 -0
  73. package/docs/stat-bar.mdx +59 -0
  74. package/docs/stat-widgets.mdx +33 -0
  75. package/docs/styles.mdx +61 -0
  76. package/docs/subtle-hover.mdx +53 -0
  77. package/docs/surfaces.mdx +72 -0
  78. package/docs/switch.mdx +90 -0
  79. package/docs/tab-bar.mdx +386 -0
  80. package/docs/tabs.mdx +75 -0
  81. package/docs/text-field.mdx +187 -0
  82. package/docs/theme.mdx +67 -0
  83. package/docs/toast.mdx +285 -0
  84. package/docs/tooltip.mdx +67 -0
  85. package/docs/typography.mdx +88 -0
  86. package/docs/user-hover-card.mdx +140 -0
  87. package/docs/zoomable-image-gallery.mdx +66 -0
  88. package/lib/commonjs/design-tokens/color-roles.js +21 -1
  89. package/lib/commonjs/design-tokens/color-roles.js.map +1 -1
  90. package/lib/commonjs/design-tokens/index.js +6 -0
  91. package/lib/commonjs/design-tokens/index.js.map +1 -1
  92. package/lib/commonjs/design-tokens/tailwind-preset.js +3 -1
  93. package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -1
  94. package/lib/commonjs/design-tokens/theme-css.js +1 -0
  95. package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
  96. package/lib/commonjs/design-tokens/theme.css +3 -0
  97. package/lib/commonjs/design-tokens/tokens-json.js +13 -4
  98. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -1
  99. package/lib/commonjs/design-tokens/tokens.json +6227 -265
  100. package/lib/commonjs/index.js +7 -0
  101. package/lib/commonjs/index.js.map +1 -1
  102. package/lib/commonjs/index.web.js +7 -0
  103. package/lib/commonjs/index.web.js.map +1 -1
  104. package/lib/commonjs/theme/ambient-store.js +16 -1
  105. package/lib/commonjs/theme/ambient-store.js.map +1 -1
  106. package/lib/commonjs/theme/color-policy.js +30 -33
  107. package/lib/commonjs/theme/color-policy.js.map +1 -1
  108. package/lib/commonjs/theme/color-presets.js +413 -166
  109. package/lib/commonjs/theme/color-presets.js.map +1 -1
  110. package/lib/commonjs/theme/color-scope/seed-scope.js +1 -0
  111. package/lib/commonjs/theme/color-scope/seed-scope.js.map +1 -1
  112. package/lib/commonjs/theme/glass-colors.js +18 -21
  113. package/lib/commonjs/theme/glass-colors.js.map +1 -1
  114. package/lib/commonjs/theme/index.js +30 -0
  115. package/lib/commonjs/theme/index.js.map +1 -1
  116. package/lib/commonjs/theme/index.web.js +30 -0
  117. package/lib/commonjs/theme/index.web.js.map +1 -1
  118. package/lib/module/design-tokens/color-roles.js +20 -0
  119. package/lib/module/design-tokens/color-roles.js.map +1 -1
  120. package/lib/module/design-tokens/index.js +2 -2
  121. package/lib/module/design-tokens/index.js.map +1 -1
  122. package/lib/module/design-tokens/tailwind-preset.js +4 -2
  123. package/lib/module/design-tokens/tailwind-preset.js.map +1 -1
  124. package/lib/module/design-tokens/theme-css.js +2 -1
  125. package/lib/module/design-tokens/theme-css.js.map +1 -1
  126. package/lib/module/design-tokens/theme.css +3 -0
  127. package/lib/module/design-tokens/tokens-json.js +14 -5
  128. package/lib/module/design-tokens/tokens-json.js.map +1 -1
  129. package/lib/module/design-tokens/tokens.json +6227 -265
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/index.web.js +1 -1
  133. package/lib/module/index.web.js.map +1 -1
  134. package/lib/module/theme/ambient-store.js +16 -1
  135. package/lib/module/theme/ambient-store.js.map +1 -1
  136. package/lib/module/theme/color-policy.js +30 -33
  137. package/lib/module/theme/color-policy.js.map +1 -1
  138. package/lib/module/theme/color-presets.js +412 -165
  139. package/lib/module/theme/color-presets.js.map +1 -1
  140. package/lib/module/theme/color-scope/seed-scope.js +2 -1
  141. package/lib/module/theme/color-scope/seed-scope.js.map +1 -1
  142. package/lib/module/theme/glass-colors.js +18 -21
  143. package/lib/module/theme/glass-colors.js.map +1 -1
  144. package/lib/module/theme/index.js +1 -1
  145. package/lib/module/theme/index.js.map +1 -1
  146. package/lib/module/theme/index.web.js +1 -1
  147. package/lib/module/theme/index.web.js.map +1 -1
  148. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +20 -0
  149. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/design-tokens/index.d.ts +3 -3
  151. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +12 -3
  155. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/index.d.ts +2 -2
  157. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  159. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/theme/color-presets.d.ts +386 -62
  163. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/theme/color-scope/seed-scope.d.ts.map +1 -1
  165. package/lib/typescript/commonjs/theme/glass-colors.d.ts +18 -21
  166. package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/theme/index.d.ts +2 -2
  168. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/theme/index.web.d.ts +2 -2
  170. package/lib/typescript/commonjs/theme/index.web.d.ts.map +1 -1
  171. package/lib/typescript/module/design-tokens/color-roles.d.ts +20 -0
  172. package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -1
  173. package/lib/typescript/module/design-tokens/index.d.ts +3 -3
  174. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
  175. package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -1
  176. package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
  177. package/lib/typescript/module/design-tokens/tokens-json.d.ts +12 -3
  178. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -1
  179. package/lib/typescript/module/index.d.ts +2 -2
  180. package/lib/typescript/module/index.d.ts.map +1 -1
  181. package/lib/typescript/module/index.web.d.ts +2 -2
  182. package/lib/typescript/module/index.web.d.ts.map +1 -1
  183. package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -1
  184. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
  185. package/lib/typescript/module/theme/color-presets.d.ts +386 -62
  186. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  187. package/lib/typescript/module/theme/color-scope/seed-scope.d.ts.map +1 -1
  188. package/lib/typescript/module/theme/glass-colors.d.ts +18 -21
  189. package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
  190. package/lib/typescript/module/theme/index.d.ts +2 -2
  191. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  192. package/lib/typescript/module/theme/index.web.d.ts +2 -2
  193. package/lib/typescript/module/theme/index.web.d.ts.map +1 -1
  194. package/package.json +5 -2
  195. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  196. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  197. package/src/__tests__/support/press-host.ts +30 -0
  198. package/src/__tests__/support/rendered-style.ts +99 -0
  199. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  200. package/src/design-tokens/color-roles.ts +21 -0
  201. package/src/design-tokens/index.ts +3 -2
  202. package/src/design-tokens/tailwind-preset.ts +3 -2
  203. package/src/design-tokens/theme-css.ts +2 -1
  204. package/src/design-tokens/theme.css +3 -0
  205. package/src/design-tokens/tokens-json.ts +29 -9
  206. package/src/design-tokens/tokens.json +6227 -265
  207. package/src/glass/GlassSurface.tsx +2 -2
  208. package/src/index.ts +2 -0
  209. package/src/index.web.ts +2 -0
  210. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +4082 -0
  211. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  212. package/src/theme/ambient-store.ts +21 -1
  213. package/src/theme/color-policy.ts +33 -38
  214. package/src/theme/color-presets.ts +473 -146
  215. package/src/theme/color-scope/seed-scope.ts +2 -1
  216. package/src/theme/glass-colors.ts +18 -21
  217. package/src/theme/index.ts +16 -1
  218. package/src/theme/index.web.ts +16 -1
@@ -1,177 +1,504 @@
1
1
  import type { SchemeVariant } from './color-engine';
2
2
 
3
- export type AppColorName =
4
- | 'teal'
5
- | 'blue'
6
- | 'green'
7
- | 'yellow'
8
- | 'red'
9
- | 'purple'
10
- | 'pink'
11
- | 'sky'
12
- | 'orange'
13
- | 'mint'
14
- | 'oxy'
15
- | 'faircoin'
16
- | 'pumpkin'
17
- | 'gray'
18
- | 'brown'
19
- | 'peach'
20
- | 'rose'
21
- | 'mono';
22
-
23
- /**
24
- * A single colour preset: a brand SEED colour plus the tonal scheme variant to
25
- * derive its full role set with.
26
- *
27
- * Every preset's palette is now GENERATED by Bloom's dependency-free colour
28
- * engine (`color-engine/`) from this seed — no hand-authored HSL triples. The
29
- * engine's `generateRoleColors({ seed, variant, isDark })` yields the full
30
- * Material-3 role set for light and dark; `getPresetVars` (in `preset-vars.ts`)
31
- * maps those roles onto Bloom's canonical token names. A user-picked colour
32
- * flows through the identical path — a preset is just a fixed seed.
33
- */
34
- /**
35
- * Why a preset is not offered to every user. Absent means it is.
36
- *
37
- * The two are NOT interchangeable and must not collapse back into one list: a
38
- * `handle` preset belongs to a specific account and cannot be bought, while a
39
- * `premium` one is exactly what is bought. Conflating them is how the picker
40
- * came to offer both of Oxy's reserved brand colours to everybody.
41
- */
3
+ /** Why a preset is not offered to every user. Absent means it is free. */
42
4
  export type ColorPresetGate =
43
5
  /** Reserved for the account whose brand it is — not purchasable. */
44
6
  | 'handle'
45
7
  /** Sold with a subscription. */
46
8
  | 'premium';
47
9
 
48
- export interface AppColorPreset {
49
- name: AppColorName;
50
- /** The brand seed colour, `#rrggbb`. Also the value the colour picker matches. */
10
+ /** Stable picker/playground categories, ordered from specific to general. */
11
+ const COLOR_PRESET_FAMILY_SOURCES = [
12
+ { name: 'brand', displayName: 'Brand', description: 'Reserved ecosystem identities.' },
13
+ { name: 'botanical', displayName: 'Botanical', description: 'Greens, jade and living hues.' },
14
+ { name: 'ocean', displayName: 'Ocean', description: 'Blue, cyan and aquatic identities.' },
15
+ { name: 'sunset', displayName: 'Sunset', description: 'Red, orange and golden identities.' },
16
+ { name: 'earth', displayName: 'Earth', description: 'Warm mineral and grounded identities.' },
17
+ { name: 'jewel', displayName: 'Jewel', description: 'Purple, rose and saturated gem hues.' },
18
+ { name: 'neutral', displayName: 'Neutral', description: 'Slate, graphite and monochrome.' },
19
+ ] as const;
20
+
21
+ export type ColorPresetFamily = (typeof COLOR_PRESET_FAMILY_SOURCES)[number]['name'];
22
+
23
+ export interface ColorPresetFamilyDefinition {
24
+ name: ColorPresetFamily;
25
+ displayName: string;
26
+ description: string;
27
+ }
28
+
29
+ export const COLOR_PRESET_FAMILY_REGISTRY: readonly ColorPresetFamilyDefinition[] =
30
+ COLOR_PRESET_FAMILY_SOURCES;
31
+
32
+ export const COLOR_PRESET_FAMILIES: readonly ColorPresetFamily[] =
33
+ COLOR_PRESET_FAMILY_REGISTRY.map(({ name }) => name);
34
+ export type ColorPresetPairing = 'derived' | 'curated';
35
+
36
+ interface ColorPresetSource {
37
+ name: string;
38
+ /** Human-readable label; consumers must not rebuild one from the identifier. */
39
+ displayName: string;
40
+ family: ColorPresetFamily;
41
+ description: string;
42
+ /** The identity seed, `#rrggbb`. */
51
43
  hex: string;
52
- /** The tonal scheme variant the engine derives roles with. */
53
44
  variant: SchemeVariant;
54
- /**
55
- * Optional explicit secondary-accent seed, `#rrggbb`. When set, the secondary
56
- * palette is PINNED to this brand colour (hue + chroma preserved; M3 role tones
57
- * still apply) instead of the derived hue-rotation of {@link AppColorPreset.hex}.
58
- * Bloom's built-in presets do NOT set this, so their generated output is
59
- * unchanged; a consumer brand with a distinct accent (blue primary + yellow
60
- * secondary) sets it to pin its real accent family.
61
- */
62
45
  secondaryHex?: string;
63
- /**
64
- * Optional explicit tertiary-accent seed, `#rrggbb`. Same semantics as
65
- * {@link AppColorPreset.secondaryHex} for the tertiary palette.
66
- */
46
+ /** The standout action seed. `tertiary` owns FABs and compose actions. */
67
47
  tertiaryHex?: string;
68
- /**
69
- * Force the fill to a tone a WHITE label fits, in BOTH modes — overriding the
70
- * budget that otherwise lets an already-light seed keep its own tone in dark
71
- * and take a black label.
72
- *
73
- * It has to be declared, because it cannot be derived: `yellow` (seed tone 86)
74
- * wants to come down to a deep gold with white on it, and `faircoin` (tone 90)
75
- * wants to stay a bright lime with black on it. Two seeds four tones apart,
76
- * opposite answers — nothing in the hex distinguishes them, only what the brand
77
- * is for.
78
- */
48
+ pairing: ColorPresetPairing;
49
+ /** Included in Bloom's permanent Mention-shaped colour playground. */
50
+ featured?: boolean;
51
+ /** Force a white primary label even when preserving the seed would choose black. */
79
52
  label?: 'white';
80
- /**
81
- * Who may pick this preset. Absent = everyone. Bloom only DECLARES the gate;
82
- * whether a given viewer satisfies it is the consuming app's question, since
83
- * only the app knows who is signed in and what they pay for.
84
- */
85
53
  gate?: ColorPresetGate;
86
54
  }
87
55
 
88
56
  /**
89
- * Historically each preset carried a full light/dark map of raw HSL triples.
90
- * That map is gone — the engine derives every role from {@link AppColorPreset.hex}.
91
- * The type alias is retained for the public `PresetTokens` export shape (the
92
- * resolved `--token -> value` record that `getPresetVars` produces).
57
+ * The ONE authored preset source.
58
+ *
59
+ * Everything public below the name union, ordered registry, lookup record,
60
+ * hex lookup, gate lists, family groups and playground list — is derived from
61
+ * this tuple. A preset is a seed recipe, never a frozen light/dark token table;
62
+ * Bloom's colour policy resolves both modes at runtime.
93
63
  */
94
- export type PresetTokens = Record<string, string>;
64
+ const COLOR_PRESET_SOURCES = [
65
+ {
66
+ name: 'teal',
67
+ displayName: 'Teal',
68
+ family: 'ocean',
69
+ description: 'Deep teal identity with dynamically derived split-complementary accents.',
70
+ hex: '#005c67',
71
+ variant: 'vivid',
72
+ pairing: 'derived',
73
+ },
74
+ {
75
+ name: 'blue',
76
+ displayName: 'Blue',
77
+ family: 'ocean',
78
+ description: 'Clear social blue with vivid generated supporting colours.',
79
+ hex: '#1d9bf0',
80
+ variant: 'vivid',
81
+ pairing: 'derived',
82
+ },
83
+ {
84
+ name: 'green',
85
+ displayName: 'Green',
86
+ family: 'botanical',
87
+ description: 'Fresh green identity with dynamically generated contrast accents.',
88
+ hex: '#10b981',
89
+ variant: 'vivid',
90
+ pairing: 'derived',
91
+ },
92
+ {
93
+ name: 'yellow',
94
+ displayName: 'Yellow',
95
+ family: 'sunset',
96
+ description: 'A signal-yellow identity deliberately brought deep enough for a white label.',
97
+ hex: '#ffd400',
98
+ variant: 'vivid',
99
+ pairing: 'derived',
100
+ label: 'white',
101
+ },
102
+ {
103
+ name: 'red',
104
+ displayName: 'Red',
105
+ family: 'sunset',
106
+ description: 'Direct red identity with contrasting generated accent arms.',
107
+ hex: '#ef4444',
108
+ variant: 'vivid',
109
+ pairing: 'derived',
110
+ },
111
+ {
112
+ name: 'purple',
113
+ displayName: 'Purple',
114
+ family: 'jewel',
115
+ description: 'Bright purple identity with dynamically derived action colours.',
116
+ hex: '#b866ff',
117
+ variant: 'vivid',
118
+ pairing: 'derived',
119
+ },
120
+ {
121
+ name: 'pink',
122
+ displayName: 'Pink',
123
+ family: 'jewel',
124
+ description: 'Hot pink identity with a vivid generated complement.',
125
+ hex: '#f91880',
126
+ variant: 'vivid',
127
+ pairing: 'derived',
128
+ },
129
+ {
130
+ name: 'sky',
131
+ displayName: 'Sky',
132
+ family: 'ocean',
133
+ description: 'Open sky blue with dynamically generated supporting accents.',
134
+ hex: '#03a9f4',
135
+ variant: 'vivid',
136
+ pairing: 'derived',
137
+ },
138
+ {
139
+ name: 'orange',
140
+ displayName: 'Orange',
141
+ family: 'sunset',
142
+ description: 'A gamut-aware orange that stays orange at white-label fill tones.',
143
+ hex: '#ff7a00',
144
+ variant: 'vivid',
145
+ pairing: 'derived',
146
+ },
147
+ {
148
+ name: 'mint',
149
+ displayName: 'Mint',
150
+ family: 'botanical',
151
+ description: 'Cool mint identity with vivid generated supporting hues.',
152
+ hex: '#14b8a6',
153
+ variant: 'vivid',
154
+ pairing: 'derived',
155
+ },
156
+ {
157
+ name: 'oxy',
158
+ displayName: 'Oxy',
159
+ family: 'brand',
160
+ description: 'Oxy purple, reserved for the Oxy account identity.',
161
+ hex: '#c46ede',
162
+ variant: 'vivid',
163
+ pairing: 'derived',
164
+ gate: 'handle',
165
+ },
166
+ {
167
+ name: 'faircoin',
168
+ displayName: 'FairCoin',
169
+ family: 'brand',
170
+ description: 'FairCoin lime, reserved for the FairCoin account identity.',
171
+ hex: '#9ffb50',
172
+ variant: 'vivid',
173
+ pairing: 'derived',
174
+ gate: 'handle',
175
+ },
176
+ {
177
+ name: 'pumpkin',
178
+ displayName: 'Pumpkin',
179
+ family: 'sunset',
180
+ description: 'Golden pumpkin orange with a generated split complement.',
181
+ hex: '#ff9800',
182
+ variant: 'vivid',
183
+ pairing: 'derived',
184
+ },
185
+ {
186
+ name: 'gray',
187
+ displayName: 'Slate',
188
+ family: 'neutral',
189
+ description: 'Cool slate identity with restrained chroma and generated accents.',
190
+ hex: '#607d8b',
191
+ variant: 'vivid',
192
+ pairing: 'derived',
193
+ },
194
+ {
195
+ name: 'brown',
196
+ displayName: 'Brown',
197
+ family: 'earth',
198
+ description: 'Deep earthen brown with a contrasting generated action colour.',
199
+ hex: '#813519',
200
+ variant: 'vivid',
201
+ pairing: 'derived',
202
+ },
203
+ {
204
+ name: 'peach',
205
+ displayName: 'Peach',
206
+ family: 'earth',
207
+ description: 'Warm peach identity with dynamically derived contrast accents.',
208
+ hex: '#ffb28d',
209
+ variant: 'vivid',
210
+ pairing: 'derived',
211
+ },
212
+ {
213
+ name: 'rose',
214
+ displayName: 'Rose',
215
+ family: 'jewel',
216
+ description: 'Light rose identity with generated vivid supporting hues.',
217
+ hex: '#fcaffe',
218
+ variant: 'vivid',
219
+ pairing: 'derived',
220
+ },
221
+ {
222
+ name: 'mono',
223
+ displayName: 'Monochrome',
224
+ family: 'neutral',
225
+ description: 'Colourless identity and surfaces, derived as a true greyscale theme.',
226
+ hex: '#000000',
227
+ variant: 'monochrome',
228
+ pairing: 'derived',
229
+ gate: 'premium',
230
+ },
231
+ {
232
+ name: 'grove',
233
+ displayName: 'Grove + Marigold',
234
+ family: 'botanical',
235
+ description: 'Green identity with warm marigold reserved for the action that must win.',
236
+ hex: '#087a3e',
237
+ tertiaryHex: '#ffb000',
238
+ variant: 'vivid',
239
+ pairing: 'curated',
240
+ featured: true,
241
+ },
242
+ {
243
+ name: 'ember',
244
+ displayName: 'Terracotta + Saffron',
245
+ family: 'earth',
246
+ description: 'Terracotta identity with a high-energy saffron action.',
247
+ hex: '#b52f0c',
248
+ tertiaryHex: '#ffb000',
249
+ variant: 'vivid',
250
+ pairing: 'curated',
251
+ featured: true,
252
+ },
253
+ {
254
+ name: 'merlot',
255
+ displayName: 'Oxblood + Coral',
256
+ family: 'jewel',
257
+ description: 'Deep wine identity with coral action, expressive without reading destructive.',
258
+ hex: '#a5003c',
259
+ tertiaryHex: '#ff542e',
260
+ variant: 'vivid',
261
+ pairing: 'curated',
262
+ featured: true,
263
+ },
264
+ {
265
+ name: 'cobalt',
266
+ displayName: 'Cobalt + Signal Yellow',
267
+ family: 'ocean',
268
+ description: 'Cobalt structure cut by a saturated signal-yellow action.',
269
+ hex: '#154fd7',
270
+ tertiaryHex: '#ffd000',
271
+ variant: 'vivid',
272
+ pairing: 'curated',
273
+ featured: true,
274
+ },
275
+ {
276
+ name: 'jade',
277
+ displayName: 'Jade + Tangerine',
278
+ family: 'botanical',
279
+ description: 'Clean jade identity with a direct tangerine action.',
280
+ hex: '#007a4b',
281
+ tertiaryHex: '#ff5e00',
282
+ variant: 'vivid',
283
+ pairing: 'curated',
284
+ featured: true,
285
+ },
286
+ {
287
+ name: 'pine',
288
+ displayName: 'Pine + Rose',
289
+ family: 'botanical',
290
+ description: 'Sober pine identity with a vivid rose action.',
291
+ hex: '#135c2d',
292
+ tertiaryHex: '#f0005a',
293
+ variant: 'vivid',
294
+ pairing: 'curated',
295
+ featured: true,
296
+ },
297
+ {
298
+ name: 'olive',
299
+ displayName: 'Olive + Lilac',
300
+ family: 'earth',
301
+ description: 'Editorial olive identity with an unexpected clear lilac action.',
302
+ hex: '#5f6400',
303
+ tertiaryHex: '#7c25e8',
304
+ variant: 'vivid',
305
+ pairing: 'curated',
306
+ featured: true,
307
+ },
308
+ {
309
+ name: 'lagoon',
310
+ displayName: 'Teal + Coral',
311
+ family: 'ocean',
312
+ description: 'Aquatic teal identity with a warm coral action.',
313
+ hex: '#00706d',
314
+ tertiaryHex: '#ff4935',
315
+ variant: 'vivid',
316
+ pairing: 'curated',
317
+ featured: true,
318
+ },
319
+ {
320
+ name: 'navy',
321
+ displayName: 'Navy + Apricot',
322
+ family: 'ocean',
323
+ description: 'Serious navy identity softened by a human apricot action.',
324
+ hex: '#0b2c72',
325
+ tertiaryHex: '#ff6b20',
326
+ variant: 'vivid',
327
+ pairing: 'curated',
328
+ featured: true,
329
+ },
330
+ {
331
+ name: 'azure',
332
+ displayName: 'Sky + Vermilion',
333
+ family: 'ocean',
334
+ description: 'Airy sky identity with a decisive vermilion action.',
335
+ hex: '#006a94',
336
+ tertiaryHex: '#e82e12',
337
+ variant: 'vivid',
338
+ pairing: 'curated',
339
+ featured: true,
340
+ },
341
+ {
342
+ name: 'violet',
343
+ displayName: 'Violet + Lime',
344
+ family: 'jewel',
345
+ description: 'Deep violet identity with an electric lime action.',
346
+ hex: '#7227c4',
347
+ tertiaryHex: '#78c900',
348
+ variant: 'vivid',
349
+ pairing: 'curated',
350
+ featured: true,
351
+ },
352
+ {
353
+ name: 'lavender',
354
+ displayName: 'Lavender + Brass',
355
+ family: 'jewel',
356
+ description: 'Calm lavender identity with a warmer, more editorial brass action.',
357
+ hex: '#5c3db5',
358
+ tertiaryHex: '#d98900',
359
+ variant: 'vivid',
360
+ pairing: 'curated',
361
+ featured: true,
362
+ },
363
+ {
364
+ name: 'plum',
365
+ displayName: 'Plum + Aqua',
366
+ family: 'jewel',
367
+ description: 'Nocturnal plum identity with a bright aqua action.',
368
+ hex: '#8d075f',
369
+ tertiaryHex: '#00aeb8',
370
+ variant: 'vivid',
371
+ pairing: 'curated',
372
+ featured: true,
373
+ },
374
+ {
375
+ name: 'cherry',
376
+ displayName: 'Cherry + Powder Blue',
377
+ family: 'jewel',
378
+ description: 'Cherry identity with a cool powder-blue counterpoint.',
379
+ hex: '#b00038',
380
+ tertiaryHex: '#2979e8',
381
+ variant: 'vivid',
382
+ pairing: 'curated',
383
+ featured: true,
384
+ },
385
+ {
386
+ name: 'chocolate',
387
+ displayName: 'Chocolate + Cyan',
388
+ family: 'earth',
389
+ description: 'Material chocolate identity with a crisp technological cyan action.',
390
+ hex: '#7a2a00',
391
+ tertiaryHex: '#00aabe',
392
+ variant: 'vivid',
393
+ pairing: 'curated',
394
+ featured: true,
395
+ },
396
+ {
397
+ name: 'graphite',
398
+ displayName: 'Graphite + Acid',
399
+ family: 'neutral',
400
+ description: 'Near-neutral identity that gives the stage to an acid-green action.',
401
+ hex: '#272b28',
402
+ tertiaryHex: '#78c900',
403
+ variant: 'vivid',
404
+ pairing: 'curated',
405
+ featured: true,
406
+ },
407
+ ] as const satisfies readonly ColorPresetSource[];
95
408
 
96
- export const APP_COLOR_NAMES: readonly AppColorName[] = ['teal', 'blue', 'green', 'yellow', 'red', 'purple', 'pink', 'sky', 'orange', 'mint', 'oxy', 'faircoin', 'pumpkin', 'gray', 'brown', 'peach', 'rose', 'mono'];
97
-
98
- export const HEX_TO_APP_COLOR: Record<string, AppColorName> = {
99
- '#005c67': 'teal',
100
- '#1d9bf0': 'blue',
101
- '#10b981': 'green',
102
- '#ffd400': 'yellow',
103
- '#ef4444': 'red',
104
- '#b866ff': 'purple',
105
- '#f91880': 'pink',
106
- '#03a9f4': 'sky',
107
- '#ff7a00': 'orange',
108
- '#14b8a6': 'mint',
109
- '#c46ede': 'oxy',
110
- '#9ffb50': 'faircoin',
111
- '#ff9800': 'pumpkin',
112
- '#607d8b': 'gray',
113
- '#813519': 'brown',
114
- '#ffb28d': 'peach',
115
- '#fcaffe': 'rose',
116
- '#000000': 'mono',
117
- };
409
+ /** Preset identifiers are inferred from the authored registry, never repeated. */
410
+ export type AppColorName = (typeof COLOR_PRESET_SOURCES)[number]['name'];
411
+
412
+ /**
413
+ * Backwards-compatible named-preset contract. Rich picker metadata lives on
414
+ * {@link ColorPresetRecipe} so existing consumers constructing this type do not
415
+ * acquire new required fields in a patch release.
416
+ */
417
+ export interface AppColorPreset {
418
+ name: AppColorName;
419
+ hex: string;
420
+ variant: SchemeVariant;
421
+ secondaryHex?: string;
422
+ tertiaryHex?: string;
423
+ label?: 'white';
424
+ gate?: ColorPresetGate;
425
+ }
426
+
427
+ /** Enriched registry entry for a picker or colour playground. */
428
+ export interface ColorPresetRecipe extends AppColorPreset {
429
+ displayName: string;
430
+ family: ColorPresetFamily;
431
+ description: string;
432
+ pairing: ColorPresetPairing;
433
+ featured: boolean;
434
+ }
435
+
436
+ /** Stable ordered entries for pickers, filters and the Storybook playground. */
437
+ export const COLOR_PRESET_REGISTRY: readonly ColorPresetRecipe[] = COLOR_PRESET_SOURCES.map(
438
+ (source) => ({ featured: false, ...source }),
439
+ );
440
+
441
+ export const APP_COLOR_NAMES: readonly AppColorName[] = COLOR_PRESET_REGISTRY.map(
442
+ ({ name }) => name,
443
+ );
444
+
445
+ export const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset> =
446
+ COLOR_PRESET_REGISTRY.reduce<Record<AppColorName, AppColorPreset>>((presets, preset) => {
447
+ // Preserve the historical runtime shape as well as its TypeScript type.
448
+ // Picker metadata belongs to COLOR_PRESET_REGISTRY; adding it invisibly to
449
+ // this older record would change Object.keys()/serialization in consumers.
450
+ presets[preset.name] = {
451
+ name: preset.name,
452
+ hex: preset.hex,
453
+ variant: preset.variant,
454
+ ...(preset.secondaryHex !== undefined ? { secondaryHex: preset.secondaryHex } : {}),
455
+ ...(preset.tertiaryHex !== undefined ? { tertiaryHex: preset.tertiaryHex } : {}),
456
+ ...(preset.label !== undefined ? { label: preset.label } : {}),
457
+ ...(preset.gate !== undefined ? { gate: preset.gate } : {}),
458
+ };
459
+ return presets;
460
+ }, {} as Record<AppColorName, AppColorPreset>);
461
+
462
+ export const HEX_TO_APP_COLOR: Record<string, AppColorName> =
463
+ COLOR_PRESET_REGISTRY.reduce<Record<string, AppColorName>>((names, preset) => {
464
+ names[preset.hex.toLowerCase()] = preset.name;
465
+ return names;
466
+ }, {});
118
467
 
119
468
  export function hexToAppColorName(hex: string): AppColorName {
120
469
  return HEX_TO_APP_COLOR[hex.toLowerCase()] ?? 'teal';
121
470
  }
122
471
 
123
- /**
124
- * The 13 named presets, each a brand seed + scheme variant. The engine derives
125
- * the full role set (light + dark) from the seed. Seeds are the brand hex of
126
- * each preset; `oxy` reads most faithfully as `vibrant`, everything else keeps a
127
- * bold `vibrant` derivation to match the prior saturated palette.
128
- */
129
- export const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset> = {
130
- teal: { name: 'teal', hex: '#005c67', variant: 'vivid' },
131
- blue: { name: 'blue', hex: '#1d9bf0', variant: 'vivid' },
132
- green: { name: 'green', hex: '#10b981', variant: 'vivid' },
133
- yellow: { name: 'yellow', hex: '#ffd400', variant: 'vivid', label: 'white' },
134
- red: { name: 'red', hex: '#ef4444', variant: 'vivid' },
135
- purple: { name: 'purple', hex: '#b866ff', variant: 'vivid' },
136
- pink: { name: 'pink', hex: '#f91880', variant: 'vivid' },
137
- sky: { name: 'sky', hex: '#03a9f4', variant: 'vivid' },
138
- // Deliberately NOT Tailwind's orange-500 (`#f97316`). That seed sits at HCT hue
139
- // 46, which reads as orange only because it is also light: at hue 46 the sRGB
140
- // gamut allows a chroma of just 62 at the tones a white label needs (<= 49), so
141
- // the brand fill resolved to `rgb(177 76 0)` — a brown. Hue 34 carries a chroma
142
- // of 84 at that same tone, with the same white label and the same contrast
143
- // headroom, so the fill reads as orange instead of paying for the hue twice.
144
- orange: { name: 'orange', hex: '#ff7a00', variant: 'vivid' },
145
- mint: { name: 'mint', hex: '#14b8a6', variant: 'vivid' },
146
- oxy: { name: 'oxy', hex: '#c46ede', variant: 'vivid', gate: 'handle' },
147
- faircoin: { name: 'faircoin', hex: '#9ffb50', variant: 'vivid', gate: 'handle' },
148
- pumpkin: { name: 'pumpkin', hex: '#ff9800', variant: 'vivid' },
149
- gray: { name: 'gray', hex: '#607d8b', variant: 'vivid' },
150
- brown: { name: 'brown', hex: '#813519', variant: 'vivid' },
151
- peach: { name: 'peach', hex: '#ffb28d', variant: 'vivid' },
152
- rose: { name: 'rose', hex: '#fcaffe', variant: 'vivid' },
153
- /**
154
- * No colour at all — the black-and-white theme. Its seed carries zero chroma,
155
- * which is the whole mechanism: the policy derives a greyscale palette from any
156
- * seed with none, so a user who picks a grey in the colour wheel lands here too
157
- * rather than on a nearly-grey approximation.
158
- */
159
- mono: { name: 'mono', hex: '#000000', variant: 'monochrome', gate: 'premium' },
160
- };
472
+ /** The sixteen approved paired directions in the permanent Mention playground. */
473
+ export const COLOR_PLAYGROUND_NAMES: readonly AppColorName[] = COLOR_PRESET_REGISTRY
474
+ .filter(({ featured }) => featured)
475
+ .map(({ name }) => name);
476
+
477
+ export interface ColorPresetGroup extends ColorPresetFamilyDefinition {
478
+ presets: readonly ColorPresetRecipe[];
479
+ }
480
+
481
+ /** Ordered family metadata and entries for consumer pickers. */
482
+ export const COLOR_PRESET_GROUPS: Readonly<Record<ColorPresetFamily, ColorPresetGroup>> =
483
+ COLOR_PRESET_FAMILY_REGISTRY.reduce<Record<ColorPresetFamily, ColorPresetGroup>>(
484
+ (groups, family) => {
485
+ groups[family.name] = {
486
+ ...family,
487
+ presets: COLOR_PRESET_REGISTRY.filter((preset) => preset.family === family.name),
488
+ };
489
+ return groups;
490
+ },
491
+ {} as Record<ColorPresetFamily, ColorPresetGroup>,
492
+ );
161
493
 
162
494
  /**
163
- * The three gate groups, DERIVED from the presets rather than listed again, so a
164
- * preset cannot be gated in one place and offered in another. Together they
165
- * partition {@link APP_COLOR_NAMES} exactly — asserted in
166
- * `__tests__/color-preset-gates.test.ts`, since a preset silently missing from
167
- * every group would simply vanish from a picker with no error anywhere.
168
- *
169
- * A consumer builds its visible list from these; it must NOT start from
170
- * `APP_COLOR_NAMES` and add the unlocked ones, which yields the gated presets to
171
- * everybody and duplicates whatever the viewer did unlock.
495
+ * Historically each preset carried a full light/dark map of raw HSL triples.
496
+ * The engine now derives every role from the registry's seeds.
172
497
  */
498
+ export type PresetTokens = Record<string, string>;
499
+
173
500
  const namesGatedBy = (gate: ColorPresetGate | undefined): readonly AppColorName[] =>
174
- APP_COLOR_NAMES.filter((name) => APP_COLOR_PRESETS[name].gate === gate);
501
+ COLOR_PRESET_REGISTRY.filter((preset) => preset.gate === gate).map(({ name }) => name);
175
502
 
176
503
  /** Available to every user, signed in or not. */
177
504
  export const FREE_COLOR_NAMES: readonly AppColorName[] = namesGatedBy(undefined);
@@ -1,6 +1,6 @@
1
1
  import { generateRoleColors, type RoleColors, type SchemeVariant } from '../color-engine';
2
2
  import { buildPolicyTokens, isColourlessSeed } from '../color-policy';
3
- import { BORDER_ROLES, FILL_ROLES, TEXT_ROLES } from '../../design-tokens/color-roles';
3
+ import { ACCENT_TEXT_ROLES, BORDER_ROLES, FILL_ROLES, TEXT_ROLES } from '../../design-tokens/color-roles';
4
4
 
5
5
  /**
6
6
  * Every alias `theme.css` declares at `:root` as a reference to a canonical
@@ -19,6 +19,7 @@ import { BORDER_ROLES, FILL_ROLES, TEXT_ROLES } from '../../design-tokens/color-
19
19
  const SCOPED_ALIASES: Readonly<Record<string, string>> = {
20
20
  ...FILL_ROLES,
21
21
  ...TEXT_ROLES,
22
+ ...ACCENT_TEXT_ROLES,
22
23
  ...BORDER_ROLES,
23
24
  divider: 'var(--border)',
24
25
  };