@oxyhq/bloom 0.35.8 → 0.37.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 (310) hide show
  1. package/lib/commonjs/frosted-icon-button/FrostedIconButton.js +148 -0
  2. package/lib/commonjs/frosted-icon-button/FrostedIconButton.js.map +1 -0
  3. package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js +166 -0
  4. package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js.map +1 -0
  5. package/lib/commonjs/frosted-icon-button/index.js +13 -0
  6. package/lib/commonjs/frosted-icon-button/index.js.map +1 -0
  7. package/lib/commonjs/frosted-icon-button/index.web.js +13 -0
  8. package/lib/commonjs/frosted-icon-button/index.web.js.map +1 -0
  9. package/lib/commonjs/frosted-icon-button/shared.js +112 -0
  10. package/lib/commonjs/frosted-icon-button/shared.js.map +1 -0
  11. package/lib/commonjs/frosted-icon-button/types.js +6 -0
  12. package/lib/commonjs/frosted-icon-button/types.js.map +1 -0
  13. package/lib/commonjs/index.js +10 -1
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +46 -37
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/theme/build-theme.js +31 -43
  18. package/lib/commonjs/theme/build-theme.js.map +1 -1
  19. package/lib/commonjs/theme/color-engine/cam16.js +161 -0
  20. package/lib/commonjs/theme/color-engine/cam16.js.map +1 -0
  21. package/lib/commonjs/theme/color-engine/color-role.js +183 -0
  22. package/lib/commonjs/theme/color-engine/color-role.js.map +1 -0
  23. package/lib/commonjs/theme/color-engine/color-roles.js +416 -0
  24. package/lib/commonjs/theme/color-engine/color-roles.js.map +1 -0
  25. package/lib/commonjs/theme/color-engine/color-utils.js +157 -0
  26. package/lib/commonjs/theme/color-engine/color-utils.js.map +1 -0
  27. package/lib/commonjs/theme/color-engine/contrast-curve.js +32 -0
  28. package/lib/commonjs/theme/color-engine/contrast-curve.js.map +1 -0
  29. package/lib/commonjs/theme/color-engine/contrast.js +70 -0
  30. package/lib/commonjs/theme/color-engine/contrast.js.map +1 -0
  31. package/lib/commonjs/theme/color-engine/dislike-analyzer.js +26 -0
  32. package/lib/commonjs/theme/color-engine/dislike-analyzer.js.map +1 -0
  33. package/lib/commonjs/theme/color-engine/dynamic-scheme.js +70 -0
  34. package/lib/commonjs/theme/color-engine/dynamic-scheme.js.map +1 -0
  35. package/lib/commonjs/theme/color-engine/hct-solver.js +236 -0
  36. package/lib/commonjs/theme/color-engine/hct-solver.js.map +1 -0
  37. package/lib/commonjs/theme/color-engine/hct.js +67 -0
  38. package/lib/commonjs/theme/color-engine/hct.js.map +1 -0
  39. package/lib/commonjs/theme/color-engine/index.js +173 -0
  40. package/lib/commonjs/theme/color-engine/index.js.map +1 -0
  41. package/lib/commonjs/theme/color-engine/lab-point-provider.js +32 -0
  42. package/lib/commonjs/theme/color-engine/lab-point-provider.js.map +1 -0
  43. package/lib/commonjs/theme/color-engine/math-utils.js +77 -0
  44. package/lib/commonjs/theme/color-engine/math-utils.js.map +1 -0
  45. package/lib/commonjs/theme/color-engine/quantizer-map.js +24 -0
  46. package/lib/commonjs/theme/color-engine/quantizer-map.js.map +1 -0
  47. package/lib/commonjs/theme/color-engine/quantizer-wu.js +266 -0
  48. package/lib/commonjs/theme/color-engine/quantizer-wu.js.map +1 -0
  49. package/lib/commonjs/theme/color-engine/scheme-variants.js +102 -0
  50. package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -0
  51. package/lib/commonjs/theme/color-engine/score.js +80 -0
  52. package/lib/commonjs/theme/color-engine/score.js.map +1 -0
  53. package/lib/commonjs/theme/color-engine/seed-from-image.js +73 -0
  54. package/lib/commonjs/theme/color-engine/seed-from-image.js.map +1 -0
  55. package/lib/commonjs/theme/color-engine/tonal-palette.js +95 -0
  56. package/lib/commonjs/theme/color-engine/tonal-palette.js.map +1 -0
  57. package/lib/commonjs/theme/color-engine/tone-delta-pair.js +26 -0
  58. package/lib/commonjs/theme/color-engine/tone-delta-pair.js.map +1 -0
  59. package/lib/commonjs/theme/color-engine/variant.js +26 -0
  60. package/lib/commonjs/theme/color-engine/variant.js.map +1 -0
  61. package/lib/commonjs/theme/color-engine/viewing-conditions.js +61 -0
  62. package/lib/commonjs/theme/color-engine/viewing-conditions.js.map +1 -0
  63. package/lib/commonjs/theme/color-presets.js +44 -572
  64. package/lib/commonjs/theme/color-presets.js.map +1 -1
  65. package/lib/commonjs/theme/preset-vars.js +64 -47
  66. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  67. package/lib/module/frosted-icon-button/FrostedIconButton.js +143 -0
  68. package/lib/module/frosted-icon-button/FrostedIconButton.js.map +1 -0
  69. package/lib/module/frosted-icon-button/FrostedIconButton.web.js +161 -0
  70. package/lib/module/frosted-icon-button/FrostedIconButton.web.js.map +1 -0
  71. package/lib/module/frosted-icon-button/index.js +4 -0
  72. package/lib/module/frosted-icon-button/index.js.map +1 -0
  73. package/lib/module/frosted-icon-button/index.web.js +11 -0
  74. package/lib/module/frosted-icon-button/index.web.js.map +1 -0
  75. package/lib/module/frosted-icon-button/shared.js +107 -0
  76. package/lib/module/frosted-icon-button/shared.js.map +1 -0
  77. package/lib/module/frosted-icon-button/types.js +4 -0
  78. package/lib/module/frosted-icon-button/types.js.map +1 -0
  79. package/lib/module/index.js +1 -0
  80. package/lib/module/index.js.map +1 -1
  81. package/lib/module/index.web.js +1 -0
  82. package/lib/module/index.web.js.map +1 -1
  83. package/lib/module/theme/build-theme.js +32 -44
  84. package/lib/module/theme/build-theme.js.map +1 -1
  85. package/lib/module/theme/color-engine/cam16.js +155 -0
  86. package/lib/module/theme/color-engine/cam16.js.map +1 -0
  87. package/lib/module/theme/color-engine/color-role.js +177 -0
  88. package/lib/module/theme/color-engine/color-role.js.map +1 -0
  89. package/lib/module/theme/color-engine/color-roles.js +411 -0
  90. package/lib/module/theme/color-engine/color-roles.js.map +1 -0
  91. package/lib/module/theme/color-engine/color-utils.js +135 -0
  92. package/lib/module/theme/color-engine/color-utils.js.map +1 -0
  93. package/lib/module/theme/color-engine/contrast-curve.js +26 -0
  94. package/lib/module/theme/color-engine/contrast-curve.js.map +1 -0
  95. package/lib/module/theme/color-engine/contrast.js +61 -0
  96. package/lib/module/theme/color-engine/contrast.js.map +1 -0
  97. package/lib/module/theme/color-engine/dislike-analyzer.js +20 -0
  98. package/lib/module/theme/color-engine/dislike-analyzer.js.map +1 -0
  99. package/lib/module/theme/color-engine/dynamic-scheme.js +65 -0
  100. package/lib/module/theme/color-engine/dynamic-scheme.js.map +1 -0
  101. package/lib/module/theme/color-engine/hct-solver.js +231 -0
  102. package/lib/module/theme/color-engine/hct-solver.js.map +1 -0
  103. package/lib/module/theme/color-engine/hct.js +61 -0
  104. package/lib/module/theme/color-engine/hct.js.map +1 -0
  105. package/lib/module/theme/color-engine/index.js +57 -0
  106. package/lib/module/theme/color-engine/index.js.map +1 -0
  107. package/lib/module/theme/color-engine/lab-point-provider.js +25 -0
  108. package/lib/module/theme/color-engine/lab-point-provider.js.map +1 -0
  109. package/lib/module/theme/color-engine/math-utils.js +65 -0
  110. package/lib/module/theme/color-engine/math-utils.js.map +1 -0
  111. package/lib/module/theme/color-engine/quantizer-map.js +20 -0
  112. package/lib/module/theme/color-engine/quantizer-map.js.map +1 -0
  113. package/lib/module/theme/color-engine/quantizer-wu.js +261 -0
  114. package/lib/module/theme/color-engine/quantizer-wu.js.map +1 -0
  115. package/lib/module/theme/color-engine/scheme-variants.js +93 -0
  116. package/lib/module/theme/color-engine/scheme-variants.js.map +1 -0
  117. package/lib/module/theme/color-engine/score.js +75 -0
  118. package/lib/module/theme/color-engine/score.js.map +1 -0
  119. package/lib/module/theme/color-engine/seed-from-image.js +67 -0
  120. package/lib/module/theme/color-engine/seed-from-image.js.map +1 -0
  121. package/lib/module/theme/color-engine/tonal-palette.js +89 -0
  122. package/lib/module/theme/color-engine/tonal-palette.js.map +1 -0
  123. package/lib/module/theme/color-engine/tone-delta-pair.js +21 -0
  124. package/lib/module/theme/color-engine/tone-delta-pair.js.map +1 -0
  125. package/lib/module/theme/color-engine/variant.js +22 -0
  126. package/lib/module/theme/color-engine/variant.js.map +1 -0
  127. package/lib/module/theme/color-engine/viewing-conditions.js +55 -0
  128. package/lib/module/theme/color-engine/viewing-conditions.js.map +1 -0
  129. package/lib/module/theme/color-presets.js +44 -572
  130. package/lib/module/theme/color-presets.js.map +1 -1
  131. package/lib/module/theme/preset-vars.js +64 -46
  132. package/lib/module/theme/preset-vars.js.map +1 -1
  133. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.d.ts +5 -0
  134. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.web.d.ts +5 -0
  136. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/frosted-icon-button/index.d.ts +3 -0
  138. package/lib/typescript/commonjs/frosted-icon-button/index.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/frosted-icon-button/index.web.d.ts +3 -0
  140. package/lib/typescript/commonjs/frosted-icon-button/index.web.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/frosted-icon-button/shared.d.ts +60 -0
  142. package/lib/typescript/commonjs/frosted-icon-button/shared.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/frosted-icon-button/types.d.ts +69 -0
  144. package/lib/typescript/commonjs/frosted-icon-button/types.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/index.d.ts +2 -0
  146. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  148. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/theme/color-engine/cam16.d.ts +26 -0
  151. package/lib/typescript/commonjs/theme/color-engine/cam16.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/theme/color-engine/color-role.d.ts +36 -0
  153. package/lib/typescript/commonjs/theme/color-engine/color-role.d.ts.map +1 -0
  154. package/lib/typescript/commonjs/theme/color-engine/color-roles.d.ts +58 -0
  155. package/lib/typescript/commonjs/theme/color-engine/color-roles.d.ts.map +1 -0
  156. package/lib/typescript/commonjs/theme/color-engine/color-utils.d.ts +32 -0
  157. package/lib/typescript/commonjs/theme/color-engine/color-utils.d.ts.map +1 -0
  158. package/lib/typescript/commonjs/theme/color-engine/contrast-curve.d.ts +9 -0
  159. package/lib/typescript/commonjs/theme/color-engine/contrast-curve.d.ts.map +1 -0
  160. package/lib/typescript/commonjs/theme/color-engine/contrast.d.ts +12 -0
  161. package/lib/typescript/commonjs/theme/color-engine/contrast.d.ts.map +1 -0
  162. package/lib/typescript/commonjs/theme/color-engine/dislike-analyzer.d.ts +11 -0
  163. package/lib/typescript/commonjs/theme/color-engine/dislike-analyzer.d.ts.map +1 -0
  164. package/lib/typescript/commonjs/theme/color-engine/dynamic-scheme.d.ts +47 -0
  165. package/lib/typescript/commonjs/theme/color-engine/dynamic-scheme.d.ts.map +1 -0
  166. package/lib/typescript/commonjs/theme/color-engine/hct-solver.d.ts +6 -0
  167. package/lib/typescript/commonjs/theme/color-engine/hct-solver.d.ts.map +1 -0
  168. package/lib/typescript/commonjs/theme/color-engine/hct.d.ts +20 -0
  169. package/lib/typescript/commonjs/theme/color-engine/hct.d.ts.map +1 -0
  170. package/lib/typescript/commonjs/theme/color-engine/index.d.ts +44 -0
  171. package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -0
  172. package/lib/typescript/commonjs/theme/color-engine/lab-point-provider.d.ts +6 -0
  173. package/lib/typescript/commonjs/theme/color-engine/lab-point-provider.d.ts.map +1 -0
  174. package/lib/typescript/commonjs/theme/color-engine/math-utils.d.ts +46 -0
  175. package/lib/typescript/commonjs/theme/color-engine/math-utils.d.ts.map +1 -0
  176. package/lib/typescript/commonjs/theme/color-engine/quantizer-map.d.ts +3 -0
  177. package/lib/typescript/commonjs/theme/color-engine/quantizer-map.d.ts.map +1 -0
  178. package/lib/typescript/commonjs/theme/color-engine/quantizer-wu.d.ts +3 -0
  179. package/lib/typescript/commonjs/theme/color-engine/quantizer-wu.d.ts.map +1 -0
  180. package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +23 -0
  181. package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -0
  182. package/lib/typescript/commonjs/theme/color-engine/score.d.ts +8 -0
  183. package/lib/typescript/commonjs/theme/color-engine/score.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts +13 -0
  185. package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/theme/color-engine/tonal-palette.d.ts +24 -0
  187. package/lib/typescript/commonjs/theme/color-engine/tonal-palette.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/theme/color-engine/tone-delta-pair.d.ts +19 -0
  189. package/lib/typescript/commonjs/theme/color-engine/tone-delta-pair.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/theme/color-engine/variant.d.ts +19 -0
  191. package/lib/typescript/commonjs/theme/color-engine/variant.d.ts.map +1 -0
  192. package/lib/typescript/commonjs/theme/color-engine/viewing-conditions.d.ts +18 -0
  193. package/lib/typescript/commonjs/theme/color-engine/viewing-conditions.d.ts.map +1 -0
  194. package/lib/typescript/commonjs/theme/color-presets.d.ts +25 -20
  195. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  196. package/lib/typescript/commonjs/theme/preset-vars.d.ts +16 -12
  197. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  198. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.d.ts +5 -0
  199. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.d.ts.map +1 -0
  200. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.web.d.ts +5 -0
  201. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -0
  202. package/lib/typescript/module/frosted-icon-button/index.d.ts +3 -0
  203. package/lib/typescript/module/frosted-icon-button/index.d.ts.map +1 -0
  204. package/lib/typescript/module/frosted-icon-button/index.web.d.ts +3 -0
  205. package/lib/typescript/module/frosted-icon-button/index.web.d.ts.map +1 -0
  206. package/lib/typescript/module/frosted-icon-button/shared.d.ts +60 -0
  207. package/lib/typescript/module/frosted-icon-button/shared.d.ts.map +1 -0
  208. package/lib/typescript/module/frosted-icon-button/types.d.ts +69 -0
  209. package/lib/typescript/module/frosted-icon-button/types.d.ts.map +1 -0
  210. package/lib/typescript/module/index.d.ts +2 -0
  211. package/lib/typescript/module/index.d.ts.map +1 -1
  212. package/lib/typescript/module/index.web.d.ts +2 -0
  213. package/lib/typescript/module/index.web.d.ts.map +1 -1
  214. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  215. package/lib/typescript/module/theme/color-engine/cam16.d.ts +26 -0
  216. package/lib/typescript/module/theme/color-engine/cam16.d.ts.map +1 -0
  217. package/lib/typescript/module/theme/color-engine/color-role.d.ts +36 -0
  218. package/lib/typescript/module/theme/color-engine/color-role.d.ts.map +1 -0
  219. package/lib/typescript/module/theme/color-engine/color-roles.d.ts +58 -0
  220. package/lib/typescript/module/theme/color-engine/color-roles.d.ts.map +1 -0
  221. package/lib/typescript/module/theme/color-engine/color-utils.d.ts +32 -0
  222. package/lib/typescript/module/theme/color-engine/color-utils.d.ts.map +1 -0
  223. package/lib/typescript/module/theme/color-engine/contrast-curve.d.ts +9 -0
  224. package/lib/typescript/module/theme/color-engine/contrast-curve.d.ts.map +1 -0
  225. package/lib/typescript/module/theme/color-engine/contrast.d.ts +12 -0
  226. package/lib/typescript/module/theme/color-engine/contrast.d.ts.map +1 -0
  227. package/lib/typescript/module/theme/color-engine/dislike-analyzer.d.ts +11 -0
  228. package/lib/typescript/module/theme/color-engine/dislike-analyzer.d.ts.map +1 -0
  229. package/lib/typescript/module/theme/color-engine/dynamic-scheme.d.ts +47 -0
  230. package/lib/typescript/module/theme/color-engine/dynamic-scheme.d.ts.map +1 -0
  231. package/lib/typescript/module/theme/color-engine/hct-solver.d.ts +6 -0
  232. package/lib/typescript/module/theme/color-engine/hct-solver.d.ts.map +1 -0
  233. package/lib/typescript/module/theme/color-engine/hct.d.ts +20 -0
  234. package/lib/typescript/module/theme/color-engine/hct.d.ts.map +1 -0
  235. package/lib/typescript/module/theme/color-engine/index.d.ts +44 -0
  236. package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -0
  237. package/lib/typescript/module/theme/color-engine/lab-point-provider.d.ts +6 -0
  238. package/lib/typescript/module/theme/color-engine/lab-point-provider.d.ts.map +1 -0
  239. package/lib/typescript/module/theme/color-engine/math-utils.d.ts +46 -0
  240. package/lib/typescript/module/theme/color-engine/math-utils.d.ts.map +1 -0
  241. package/lib/typescript/module/theme/color-engine/quantizer-map.d.ts +3 -0
  242. package/lib/typescript/module/theme/color-engine/quantizer-map.d.ts.map +1 -0
  243. package/lib/typescript/module/theme/color-engine/quantizer-wu.d.ts +3 -0
  244. package/lib/typescript/module/theme/color-engine/quantizer-wu.d.ts.map +1 -0
  245. package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +23 -0
  246. package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -0
  247. package/lib/typescript/module/theme/color-engine/score.d.ts +8 -0
  248. package/lib/typescript/module/theme/color-engine/score.d.ts.map +1 -0
  249. package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts +13 -0
  250. package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts.map +1 -0
  251. package/lib/typescript/module/theme/color-engine/tonal-palette.d.ts +24 -0
  252. package/lib/typescript/module/theme/color-engine/tonal-palette.d.ts.map +1 -0
  253. package/lib/typescript/module/theme/color-engine/tone-delta-pair.d.ts +19 -0
  254. package/lib/typescript/module/theme/color-engine/tone-delta-pair.d.ts.map +1 -0
  255. package/lib/typescript/module/theme/color-engine/variant.d.ts +19 -0
  256. package/lib/typescript/module/theme/color-engine/variant.d.ts.map +1 -0
  257. package/lib/typescript/module/theme/color-engine/viewing-conditions.d.ts +18 -0
  258. package/lib/typescript/module/theme/color-engine/viewing-conditions.d.ts.map +1 -0
  259. package/lib/typescript/module/theme/color-presets.d.ts +25 -20
  260. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  261. package/lib/typescript/module/theme/preset-vars.d.ts +16 -12
  262. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  263. package/package.json +17 -1
  264. package/src/__tests__/BloomThemeProvider.test.tsx +9 -4
  265. package/src/__tests__/FrostedIconButton.test.tsx +137 -0
  266. package/src/__tests__/FrostedIconButton.web.test.tsx +246 -0
  267. package/src/__tests__/native-root-vars.test.ts +8 -4
  268. package/src/__tests__/preset-vars.test.ts +66 -37
  269. package/src/__tests__/theme.test.ts +20 -12
  270. package/src/frosted-icon-button/FrostedIconButton.stories.tsx +65 -0
  271. package/src/frosted-icon-button/FrostedIconButton.tsx +156 -0
  272. package/src/frosted-icon-button/FrostedIconButton.web.tsx +207 -0
  273. package/src/frosted-icon-button/index.ts +2 -0
  274. package/src/frosted-icon-button/index.web.ts +9 -0
  275. package/src/frosted-icon-button/shared.ts +118 -0
  276. package/src/frosted-icon-button/types.ts +81 -0
  277. package/src/index.ts +2 -0
  278. package/src/index.web.ts +2 -0
  279. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +509 -509
  280. package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +613 -613
  281. package/src/theme/__tests__/color-engine-golden.test.ts +81 -0
  282. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  283. package/src/theme/__tests__/golden-tokens.test.ts +6 -7
  284. package/src/theme/__tests__/runtime-contract.test.ts +8 -8
  285. package/src/theme/__tests__/theme-colors-parity.test.ts +44 -88
  286. package/src/theme/build-theme.ts +32 -44
  287. package/src/theme/color-engine/cam16.ts +179 -0
  288. package/src/theme/color-engine/color-role.ts +217 -0
  289. package/src/theme/color-engine/color-roles.ts +435 -0
  290. package/src/theme/color-engine/color-utils.ts +157 -0
  291. package/src/theme/color-engine/contrast-curve.ts +25 -0
  292. package/src/theme/color-engine/contrast.ts +59 -0
  293. package/src/theme/color-engine/dislike-analyzer.ts +19 -0
  294. package/src/theme/color-engine/dynamic-scheme.ts +90 -0
  295. package/src/theme/color-engine/hct-solver.ts +263 -0
  296. package/src/theme/color-engine/hct.ts +69 -0
  297. package/src/theme/color-engine/index.ts +73 -0
  298. package/src/theme/color-engine/lab-point-provider.ts +26 -0
  299. package/src/theme/color-engine/math-utils.ts +69 -0
  300. package/src/theme/color-engine/quantizer-map.ts +17 -0
  301. package/src/theme/color-engine/quantizer-wu.ts +320 -0
  302. package/src/theme/color-engine/scheme-variants.ts +113 -0
  303. package/src/theme/color-engine/score.ts +82 -0
  304. package/src/theme/color-engine/seed-from-image.ts +64 -0
  305. package/src/theme/color-engine/tonal-palette.ts +94 -0
  306. package/src/theme/color-engine/tone-delta-pair.ts +21 -0
  307. package/src/theme/color-engine/variant.ts +18 -0
  308. package/src/theme/color-engine/viewing-conditions.ts +77 -0
  309. package/src/theme/color-presets.ts +54 -631
  310. package/src/theme/preset-vars.ts +65 -49
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import React from 'react';
6
+ import { act } from 'react';
7
+ import { createRoot, type Root } from 'react-dom/client';
8
+ import { getByRole, fireEvent } from '@testing-library/dom';
9
+ import { renderToStaticMarkup } from 'react-dom/server';
10
+ import '@testing-library/jest-dom';
11
+
12
+ import { BloomThemeProvider } from '../theme/BloomThemeProvider';
13
+ import { FrostedIconButton } from '../frosted-icon-button/FrostedIconButton.web';
14
+ import { parseRgb } from '../theme/color-utils';
15
+
16
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
17
+
18
+ let container: HTMLDivElement;
19
+ let root: Root;
20
+
21
+ function mount(ui: React.ReactElement, mode: 'light' | 'dark' = 'dark'): HTMLElement {
22
+ act(() => {
23
+ root.render(
24
+ <BloomThemeProvider mode={mode} colorPreset="blue">
25
+ {ui}
26
+ </BloomThemeProvider>,
27
+ );
28
+ });
29
+ return container;
30
+ }
31
+
32
+ beforeEach(() => {
33
+ container = document.createElement('div');
34
+ document.body.appendChild(container);
35
+ root = createRoot(container);
36
+ });
37
+
38
+ afterEach(() => {
39
+ act(() => root.unmount());
40
+ container.remove();
41
+ });
42
+
43
+ // Mention's measured dark page background — the "invisible circle" bug composited
44
+ // a near-black `card` fill onto this and vanished. The frosted chip must read.
45
+ const PAGE_DARK = { r: 11, g: 11, b: 15 };
46
+
47
+ /** Composite an `rgba(...)` surface over an opaque background (source-over). */
48
+ function compositeOver(surface: string, bg: { r: number; g: number; b: number }) {
49
+ const base = parseRgb(surface);
50
+ const m = /rgba?\(([^)]+)\)/i.exec(surface);
51
+ const rawAlpha = m?.[1]?.split(',')[3];
52
+ const a = rawAlpha === undefined ? 1 : Number(rawAlpha);
53
+ if (!base) throw new Error(`not an rgba string: ${surface}`);
54
+ return {
55
+ r: Math.round(bg.r * (1 - a) + base.r * a),
56
+ g: Math.round(bg.g * (1 - a) + base.g * a),
57
+ b: Math.round(bg.b * (1 - a) + base.b * a),
58
+ };
59
+ }
60
+
61
+ describe('FrostedIconButton.web', () => {
62
+ it('renders a real <button> with the accessible label', () => {
63
+ const c = mount(<FrostedIconButton accessibilityLabel="Back" icon={<span>x</span>} />);
64
+ const btn = getByRole(c, 'button', { name: 'Back' });
65
+ expect(btn.tagName).toBe('BUTTON');
66
+ expect(btn).toHaveClass('bloom-frosted-icon-btn');
67
+ });
68
+
69
+ it('self-injects its interaction stylesheet once', () => {
70
+ mount(<FrostedIconButton accessibilityLabel="Back" icon={<span>x</span>} />);
71
+ mount(<FrostedIconButton accessibilityLabel="Fwd" icon={<span>y</span>} />);
72
+ expect(document.querySelectorAll('#bloom-frosted-icon-button-web-css')).toHaveLength(1);
73
+ });
74
+
75
+ // THE bug this component exists to prevent: on a dark SOLID page, a `card`-fill
76
+ // circle composited to near-black and vanished. Prove the frosted surface is a
77
+ // LIGHT tint that reads as a distinct, clearly-lighter circle on rgb(11,11,15).
78
+ describe('reads on a solid dark background (nothing behind it)', () => {
79
+ it('uses a low-opacity LIGHT tint distinguishable from the page', () => {
80
+ const c = mount(<FrostedIconButton accessibilityLabel="Back" icon={<span>x</span>} />);
81
+ const btn = getByRole(c, 'button');
82
+ const surface = btn.style.backgroundColor;
83
+
84
+ // Not the page color, and a genuine translucent (rgba) surface.
85
+ expect(surface).not.toBe('rgb(11, 11, 15)');
86
+ expect(surface).toMatch(/^rgba\(/);
87
+
88
+ // The tint's BASE color is light (each channel high) — a light frost, not a
89
+ // dark card fill.
90
+ const base = parseRgb(surface);
91
+ if (!base) throw new Error('unreachable');
92
+ expect(base.r).toBeGreaterThanOrEqual(200);
93
+ expect(base.g).toBeGreaterThanOrEqual(200);
94
+ expect(base.b).toBeGreaterThanOrEqual(200);
95
+
96
+ // Composited over the page it is clearly LIGHTER than rgb(11,11,15).
97
+ const composite = compositeOver(surface, PAGE_DARK);
98
+ expect(composite.r - PAGE_DARK.r).toBeGreaterThanOrEqual(15);
99
+ expect(composite.g - PAGE_DARK.g).toBeGreaterThanOrEqual(15);
100
+ expect(composite.b - PAGE_DARK.b).toBeGreaterThanOrEqual(15);
101
+ });
102
+
103
+ it('renders a hairline ring and a shadow that survive dark mode', () => {
104
+ const c = mount(<FrostedIconButton accessibilityLabel="Back" icon={<span>x</span>} />);
105
+ const btn = getByRole(c, 'button');
106
+ // Ring: a translucent border COLOR is present inline (per-instance token).
107
+ expect(btn.style.borderColor).toMatch(/^rgba\(/);
108
+ // The hairline border-style/width live in the self-injected stylesheet.
109
+ const css = document.getElementById('bloom-frosted-icon-button-web-css')?.textContent ?? '';
110
+ expect(css).toMatch(/border-style:\s*solid/);
111
+ expect(css).toMatch(/border-width:\s*1px/);
112
+ // Shadow is NOT killed in dark mode.
113
+ expect(btn.style.boxShadow).not.toBe('');
114
+ expect(btn.style.boxShadow).toMatch(/rgba?\(/);
115
+ });
116
+
117
+ // jsdom's CSS engine drops `backdrop-filter`, so assert it via SSR style
118
+ // serialization — which emits EVERY style key exactly as the browser receives
119
+ // it (react-dom writes the same inline style on a real DOM <button>).
120
+ it('emits a real CSS backdrop-filter blur when frosted (and drops it when active)', () => {
121
+ const frosted = renderToStaticMarkup(
122
+ <BloomThemeProvider mode="dark" colorPreset="blue">
123
+ <FrostedIconButton accessibilityLabel="Back" icon={<span>x</span>} />
124
+ </BloomThemeProvider>,
125
+ );
126
+ expect(frosted).toMatch(/backdrop-filter:\s*blur\(\d+px\)/);
127
+
128
+ const active = renderToStaticMarkup(
129
+ <BloomThemeProvider mode="dark" colorPreset="blue">
130
+ <FrostedIconButton accessibilityLabel="Back" active icon={<span>x</span>} />
131
+ </BloomThemeProvider>,
132
+ );
133
+ // Active is solid: no blur (either absent or explicitly "none").
134
+ expect(active).not.toMatch(/backdrop-filter:\s*blur\(/);
135
+ });
136
+ });
137
+
138
+ describe('active (solid "on") state', () => {
139
+ it('fills opaque (no rgba alpha), drops the blur, and sets aria-pressed', () => {
140
+ const c = mount(<FrostedIconButton accessibilityLabel="Mute" active icon={<span>x</span>} />);
141
+ const btn = getByRole(c, 'button');
142
+ // Opaque solid fill — an `rgb(...)` (not translucent `rgba`).
143
+ expect(btn.style.backgroundColor).toMatch(/^rgb\(/);
144
+ expect(btn.style.backgroundColor).not.toMatch(/^rgba\(/);
145
+ expect(btn).toHaveAttribute('aria-pressed', 'true');
146
+ expect(btn).toHaveAttribute('data-active', 'true');
147
+ const bf = btn.style.getPropertyValue('backdrop-filter');
148
+ expect(bf === '' || bf === 'none').toBe(true);
149
+ });
150
+
151
+ it('reports aria-pressed=false when not active', () => {
152
+ const c = mount(<FrostedIconButton accessibilityLabel="Mute" icon={<span>x</span>} />);
153
+ expect(getByRole(c, 'button')).toHaveAttribute('aria-pressed', 'false');
154
+ });
155
+ });
156
+
157
+ describe('icon color', () => {
158
+ it('injects the theme icon color as a fallback fill on a bare icon', () => {
159
+ const c = mount(
160
+ <FrostedIconButton accessibilityLabel="Back" icon={<svg data-testid="ic" />} />,
161
+ );
162
+ const icon = c.querySelector('[data-testid="ic"]');
163
+ expect(icon?.getAttribute('fill')).toMatch(/^rgb/);
164
+ });
165
+
166
+ it('never overrides an explicit fill on the icon', () => {
167
+ const c = mount(
168
+ <FrostedIconButton
169
+ accessibilityLabel="Back"
170
+ icon={<svg data-testid="ic" fill="rgb(1, 2, 3)" />}
171
+ />,
172
+ );
173
+ expect(c.querySelector('[data-testid="ic"]')?.getAttribute('fill')).toBe('rgb(1, 2, 3)');
174
+ });
175
+ });
176
+
177
+ describe('light theme', () => {
178
+ it('uses a subtle DARK tint (base near-black) distinguishable from a white page', () => {
179
+ const c = mount(
180
+ <FrostedIconButton accessibilityLabel="Back" icon={<span>x</span>} />,
181
+ 'light',
182
+ );
183
+ const surface = getByRole(c, 'button').style.backgroundColor;
184
+ expect(surface).toMatch(/^rgba\(/);
185
+ const base = parseRgb(surface);
186
+ if (!base) throw new Error('unreachable');
187
+ expect(base.r).toBeLessThanOrEqual(60);
188
+ expect(base.g).toBeLessThanOrEqual(60);
189
+ expect(base.b).toBeLessThanOrEqual(60);
190
+ });
191
+ });
192
+
193
+ describe('behavior', () => {
194
+ it('fires both onClick and onPress on click', () => {
195
+ const onClick = jest.fn();
196
+ const onPress = jest.fn();
197
+ const c = mount(
198
+ <FrostedIconButton
199
+ accessibilityLabel="Back"
200
+ onClick={onClick}
201
+ onPress={onPress}
202
+ icon={<span>x</span>}
203
+ />,
204
+ );
205
+ act(() => {
206
+ fireEvent.click(getByRole(c, 'button'));
207
+ });
208
+ expect(onClick).toHaveBeenCalledTimes(1);
209
+ expect(onPress).toHaveBeenCalledTimes(1);
210
+ });
211
+
212
+ it('does not fire handlers when disabled', () => {
213
+ const onPress = jest.fn();
214
+ const c = mount(
215
+ <FrostedIconButton accessibilityLabel="Back" disabled onPress={onPress} icon={<span>x</span>} />,
216
+ );
217
+ const btn = getByRole(c, 'button');
218
+ expect(btn).toBeDisabled();
219
+ act(() => {
220
+ fireEvent.click(btn);
221
+ });
222
+ expect(onPress).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it('applies preset sizes (md = 36px, sm = 32px)', () => {
226
+ const c = mount(<FrostedIconButton accessibilityLabel="Back" size="md" icon={<span>x</span>} />);
227
+ expect(getByRole(c, 'button').style.width).toBe('36px');
228
+ const c2 = mount(<FrostedIconButton accessibilityLabel="Back" size="sm" icon={<span>x</span>} />);
229
+ expect(getByRole(c2, 'button').style.width).toBe('32px');
230
+ });
231
+
232
+ it('does not crash on an array-form style prop (StyleProp flattening)', () => {
233
+ const showBorder = false;
234
+ const c = mount(
235
+ <FrostedIconButton
236
+ accessibilityLabel="Back"
237
+ icon={<span>x</span>}
238
+ style={[{ marginTop: 8 }, showBorder && { borderColor: 'rgb(9, 9, 9)' }]}
239
+ />,
240
+ );
241
+ const btn = getByRole(c, 'button');
242
+ expect(btn.style.marginTop).toBe('8px');
243
+ expect(btn.getAttribute('style') ?? '').not.toMatch(/(^|;)\s*0\s*:/);
244
+ });
245
+ });
246
+ });
@@ -53,12 +53,16 @@ describe('applyNativeRootVars (native variant)', () => {
53
53
  });
54
54
 
55
55
  // Keys are bare (no `--`), and the value is the single canonical sRGB
56
- // `rgb(...)` color from `getResolvedTokens` (`205 87% 53%` `rgb(31 153 239)`).
57
- // sRGB is the space react-native-css's `color-mix` resolver registers, so
58
- // alpha utilities (`bg-primary/10`) resolve on native.
56
+ // `rgb(...)` color from `getResolvedTokens` (now engine-derived from the blue
57
+ // seed). sRGB is the space react-native-css's `color-mix` resolver registers,
58
+ // so alpha utilities (`bg-primary/10`) resolve on native.
59
+ const { getResolvedTokens } = require('../theme/token-registry') as {
60
+ getResolvedTokens: (preset: string, mode: 'light' | 'dark') => Record<string, string>;
61
+ };
62
+ const expectedPrimary = getResolvedTokens('blue', 'light')['--primary'];
59
63
  expect(sets).toHaveProperty('primary');
60
64
  expect(sets).not.toHaveProperty('--primary');
61
- expect(sets.primary).toEqual([['rgb(31 153 239)']]);
65
+ expect(sets.primary).toEqual([[expectedPrimary]]);
62
66
 
63
67
  // The legacy resolved-color companion universe is gone — the base token IS
64
68
  // rgb, so no `color-primary` companion is written.
@@ -1,4 +1,4 @@
1
- import { APP_COLOR_NAMES, APP_COLOR_PRESETS } from '../theme/color-presets';
1
+ import { APP_COLOR_NAMES } from '../theme/color-presets';
2
2
  import { getPresetVars } from '../theme/preset-vars';
3
3
  import { getResolvedTokens } from '../theme/token-registry';
4
4
 
@@ -20,70 +20,99 @@ const EXTENDED_KEYS = [
20
20
  '--sidebar-ring',
21
21
  ] as const;
22
22
 
23
- describe('getPresetVars', () => {
24
- it('includes every base preset token plus extended tokens for all presets and modes', () => {
23
+ const BASE_KEYS = [
24
+ '--background',
25
+ '--foreground',
26
+ '--surface',
27
+ '--surface-foreground',
28
+ '--popover',
29
+ '--popover-foreground',
30
+ '--primary',
31
+ '--primary-foreground',
32
+ '--secondary',
33
+ '--secondary-foreground',
34
+ '--muted',
35
+ '--muted-foreground',
36
+ '--accent',
37
+ '--accent-foreground',
38
+ '--destructive',
39
+ '--border',
40
+ '--input',
41
+ '--ring',
42
+ '--sidebar',
43
+ ] as const;
44
+
45
+ describe('getPresetVars (engine-backed)', () => {
46
+ it('emits every base + extended token as a full rgb() color for all presets and modes', () => {
25
47
  for (const name of APP_COLOR_NAMES) {
26
48
  for (const mode of ['light', 'dark'] as const) {
27
49
  const vars = getPresetVars(name, mode);
28
- const base = mode === 'light' ? APP_COLOR_PRESETS[name].light : APP_COLOR_PRESETS[name].dark;
29
-
30
- for (const key of Object.keys(base)) {
31
- expect(vars[key]).toBe(base[key]);
32
- }
33
- for (const key of EXTENDED_KEYS) {
50
+ for (const key of [...BASE_KEYS, ...EXTENDED_KEYS]) {
34
51
  expect(vars[key]).toBeDefined();
52
+ // The engine emits `rgb(r g b)` directly — never a bare HSL triple.
53
+ expect(vars[key]).toMatch(/^rgb\(\d+ \d+ \d+\)$/);
35
54
  }
36
55
  }
37
56
  }
38
57
  });
39
58
 
40
- it('derives chart/sidebar hue from the preset primary', () => {
59
+ it('drives sidebar-primary / ring from the engine primary role', () => {
41
60
  const vars = getPresetVars('blue', 'light');
42
- const primary = APP_COLOR_PRESETS.blue.light['--primary'] ?? '';
43
- const primaryHue = primary.split(' ')[0] ?? '';
44
- const chart1 = vars['--chart-1'] ?? '';
45
- expect(chart1.startsWith(`${primaryHue} `)).toBe(true);
46
- expect(vars['--sidebar-primary']).toBe(primary);
61
+ expect(vars['--sidebar-primary']).toBe(vars['--primary']);
62
+ expect(vars['--ring']).toBe(vars['--primary']);
63
+ expect(vars['--chart-1']).toBe(vars['--primary']);
64
+ });
65
+
66
+ it('makes secondary a real contrast color, not a mirror of primary (the fix)', () => {
67
+ for (const name of APP_COLOR_NAMES) {
68
+ for (const mode of ['light', 'dark'] as const) {
69
+ const vars = getPresetVars(name, mode);
70
+ expect(vars['--secondary']).not.toBe(vars['--primary']);
71
+ }
72
+ }
47
73
  });
48
74
 
49
- it('returns raw HSL triples (no resolved colors) getResolvedTokens owns rgb', () => {
50
- // `getPresetVars` is the raw triple source feeding the single canonical
51
- // resolver. It must NOT pre-resolve to rgb/hsl, and the legacy resolved-color
52
- // companion universe must be gone (no key carries the resolved-color prefix).
75
+ it('makes card the lightest surface in light mode, darkest in dark (the fix)', () => {
76
+ const channels = (rgb: string): number[] =>
77
+ (rgb.match(/\d+/g) ?? []).map(Number);
78
+ const sum = (rgb: string): number => channels(rgb).reduce((a, b) => a + b, 0);
79
+
80
+ for (const name of APP_COLOR_NAMES) {
81
+ const light = getPresetVars(name, 'light');
82
+ // card (surfaceContainerLowest) must be lighter than background in light.
83
+ expect(sum(light['--card'] ?? '')).toBeGreaterThan(sum(light['--background'] ?? ''));
84
+
85
+ const dark = getPresetVars(name, 'dark');
86
+ // card must be darker than (or equal to) background in dark — correct inversion.
87
+ expect(sum(dark['--card'] ?? '')).toBeLessThanOrEqual(sum(dark['--background'] ?? ''));
88
+ }
89
+ });
90
+
91
+ it('carries no legacy resolved-color companion universe', () => {
53
92
  const vars = getPresetVars('green', 'dark');
54
- expect(vars['--primary']).toBe(APP_COLOR_PRESETS.green.dark['--primary']);
55
- expect(/^-?\d[\d.]*\s+[\d.]+%/.test(vars['--primary'] ?? '')).toBe(true);
56
93
  const legacyPrefix = `--${'color'}-`;
57
94
  expect(Object.keys(vars).some((k) => k.startsWith(legacyPrefix))).toBe(false);
58
95
  });
59
96
  });
60
97
 
61
98
  describe('getResolvedTokens (single canonical rgb pipeline)', () => {
62
- it('resolves every base + extended token to a space-separated rgb() string', () => {
99
+ it('passes the engine rgb() values through unchanged', () => {
63
100
  const resolved = getResolvedTokens('green', 'dark');
64
- const primaryTriple = APP_COLOR_PRESETS.green.dark['--primary'] ?? '';
65
- // No resolved-color companion universe anymore the base token is rgb.
66
- const legacyPrefix = `--${'color'}-`;
67
- expect(Object.keys(resolved).some((k) => k.startsWith(legacyPrefix))).toBe(false);
101
+ const raw = getPresetVars('green', 'dark');
102
+ // The engine already emits rgb(); getResolvedTokens must not re-touch it.
103
+ expect(resolved['--primary']).toBe(raw['--primary']);
104
+ expect(resolved['--card']).toBe(raw['--card']);
68
105
  expect(resolved['--primary']).toMatch(/^rgb\(\d+ \d+ \d+\)$/);
69
106
  expect(resolved['--card']).toMatch(/^rgb\(\d+ \d+ \d+\)$/);
70
- // primary triple resolves to the same rgb the registry produces.
71
- expect(resolved['--primary']).toBe(getResolvedTokens('green', 'dark')['--primary']);
72
- expect(primaryTriple).toMatch(/%/);
73
- });
74
-
75
- it('resolves --primary to rgb for the blue preset (matches web reference)', () => {
76
- // `blue` preset light `--primary` is `205 87% 53%` → rgb(31 153 239).
77
- const resolved = getResolvedTokens('blue', 'light');
78
- expect(resolved['--primary']).toBe('rgb(31 153 239)');
107
+ const legacyPrefix = `--${'color'}-`;
108
+ expect(Object.keys(resolved).some((k) => k.startsWith(legacyPrefix))).toBe(false);
79
109
  });
80
110
 
81
111
  it('produces a full CSS color for every base preset token of every preset/mode', () => {
82
112
  for (const name of APP_COLOR_NAMES) {
83
113
  for (const mode of ['light', 'dark'] as const) {
84
- const base = mode === 'light' ? APP_COLOR_PRESETS[name].light : APP_COLOR_PRESETS[name].dark;
85
114
  const resolved = getResolvedTokens(name, mode);
86
- for (const key of Object.keys(base)) {
115
+ for (const key of BASE_KEYS) {
87
116
  const value = resolved[key] ?? '';
88
117
  // A bare triple (`185 50% 5%`) is INVALID CSS — the incident. Every
89
118
  // base token must come out as a complete color the browser parses.
@@ -5,6 +5,7 @@ import {
5
5
  hexToAppColorName,
6
6
  } from '../theme/color-presets';
7
7
  import { buildTheme } from '../theme/build-theme';
8
+ import { getPresetVars } from '../theme/preset-vars';
8
9
  import { THEME_GRADIENTS } from '../theme/gradients';
9
10
  import type { Theme, ThemeColors, ThemeMode } from '../theme/types';
10
11
 
@@ -18,7 +19,9 @@ describe('Theme system', () => {
18
19
  }
19
20
  });
20
21
 
21
- it('each preset has both light and dark color variables', () => {
22
+ it('each preset generates both light and dark canonical tokens from its seed', () => {
23
+ // The palette is now engine-derived from the seed; every preset must
24
+ // produce a full canonical token set in both modes via `getPresetVars`.
22
25
  const requiredVars = [
23
26
  '--background',
24
27
  '--foreground',
@@ -32,31 +35,36 @@ describe('Theme system', () => {
32
35
  ];
33
36
 
34
37
  for (const name of APP_COLOR_NAMES) {
35
- const preset = APP_COLOR_PRESETS[name]!;
38
+ const light = getPresetVars(name, 'light');
39
+ const dark = getPresetVars(name, 'dark');
36
40
  for (const v of requiredVars) {
37
- expect(preset.light[v]).toBeDefined();
38
- expect(preset.dark[v]).toBeDefined();
41
+ expect(light[v]).toBeDefined();
42
+ expect(dark[v]).toBeDefined();
39
43
  }
40
44
  }
41
45
  });
42
46
 
43
47
  it('light and dark variants have different values', () => {
44
48
  for (const name of APP_COLOR_NAMES) {
45
- const preset = APP_COLOR_PRESETS[name]!;
46
49
  // Background should differ between light and dark
47
- expect(preset.light['--background']).not.toBe(preset.dark['--background']);
50
+ expect(getPresetVars(name, 'light')['--background']).not.toBe(
51
+ getPresetVars(name, 'dark')['--background'],
52
+ );
48
53
  }
49
54
  });
50
55
  });
51
56
 
52
57
  describe('buildTheme primaryForeground', () => {
53
58
  it('resolves a readable foreground for the primary fill per preset', () => {
54
- // blue's --primary-foreground is `0 0% 100%` → white (canonical sRGB rgb)
55
- expect(buildTheme('blue', 'light').colors.primaryForeground).toBe('rgb(255 255 255)');
56
- expect(buildTheme('blue', 'dark').colors.primaryForeground).toBe('rgb(255 255 255)');
57
- // yellow's --primary-foreground is `0 0% 0%` black (canonical sRGB rgb)
58
- expect(buildTheme('yellow', 'light').colors.primaryForeground).toBe('rgb(0 0 0)');
59
- expect(buildTheme('yellow', 'dark').colors.primaryForeground).toBe('rgb(0 0 0)');
59
+ // Every preset's primary foreground is the engine's `on-primary` role,
60
+ // surfaced as `--primary-foreground` — a legible on-primary contrast color.
61
+ for (const name of APP_COLOR_NAMES) {
62
+ for (const mode of ['light', 'dark'] as const) {
63
+ expect(buildTheme(name, mode).colors.primaryForeground).toBe(
64
+ getPresetVars(name, mode)['--primary-foreground'],
65
+ );
66
+ }
67
+ }
60
68
  });
61
69
 
62
70
  it('sets a primaryForeground for every preset in both modes', () => {
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { View, ImageBackground } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { FrostedIconButton } from './FrostedIconButton';
6
+ import * as Icons from '../icons';
7
+
8
+ const meta: Meta<typeof FrostedIconButton> = {
9
+ title: 'Components/FrostedIconButton',
10
+ component: FrostedIconButton,
11
+ args: {
12
+ accessibilityLabel: 'Back',
13
+ icon: <Icons.ChevronLeft_Stroke2_Corner0_Rounded size="md" />,
14
+ onPress: () => {},
15
+ },
16
+ argTypes: {
17
+ size: { control: 'select', options: ['sm', 'md'] },
18
+ active: { control: 'boolean' },
19
+ disabled: { control: 'boolean' },
20
+ },
21
+ };
22
+
23
+ export default meta;
24
+
25
+ type Story = StoryObj<typeof FrostedIconButton>;
26
+
27
+ /** The frosted chip on a SOLID dark background — must read as a distinct button. */
28
+ export const OnSolidDark: Story = {
29
+ render: (args) => (
30
+ <View style={{ padding: 40, backgroundColor: 'rgb(11, 11, 15)' }}>
31
+ <FrostedIconButton {...args} />
32
+ </View>
33
+ ),
34
+ };
35
+
36
+ /** The solid "on" state for toggles (no blur, brand fill). */
37
+ export const Active: Story = {
38
+ render: (args) => (
39
+ <View style={{ padding: 40, backgroundColor: 'rgb(11, 11, 15)' }}>
40
+ <FrostedIconButton {...args} active />
41
+ </View>
42
+ ),
43
+ };
44
+
45
+ /** Frosting over an image (the reason it exists) — blur shows the banner behind. */
46
+ export const OverImage: Story = {
47
+ render: (args) => (
48
+ <ImageBackground
49
+ source={{ uri: 'https://picsum.photos/600/300' }}
50
+ style={{ width: 320, height: 180, padding: 16, justifyContent: 'flex-start' }}
51
+ >
52
+ <FrostedIconButton {...args} />
53
+ </ImageBackground>
54
+ ),
55
+ };
56
+
57
+ /** Both sizes side by side. */
58
+ export const Sizes: Story = {
59
+ render: (args) => (
60
+ <View style={{ flexDirection: 'row', gap: 12, padding: 40, backgroundColor: 'rgb(11, 11, 15)' }}>
61
+ <FrostedIconButton {...args} size="sm" />
62
+ <FrostedIconButton {...args} size="md" />
63
+ </View>
64
+ ),
65
+ };