@oxyhq/bloom 0.87.1 → 0.89.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 (214) hide show
  1. package/lib/commonjs/accordion/Accordion.js +7 -5
  2. package/lib/commonjs/accordion/Accordion.js.map +1 -1
  3. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -12
  4. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  5. package/lib/commonjs/button/Button.js +8 -7
  6. package/lib/commonjs/button/Button.js.map +1 -1
  7. package/lib/commonjs/button/Button.web.js +2 -6
  8. package/lib/commonjs/button/Button.web.js.map +1 -1
  9. package/lib/commonjs/checkbox/Checkbox.js +10 -5
  10. package/lib/commonjs/checkbox/Checkbox.js.map +1 -1
  11. package/lib/commonjs/chip/Chip.js +10 -1
  12. package/lib/commonjs/chip/Chip.js.map +1 -1
  13. package/lib/commonjs/collapsible/Collapsible.js +5 -4
  14. package/lib/commonjs/collapsible/Collapsible.js.map +1 -1
  15. package/lib/commonjs/composition-bar/CompositionBar.js +6 -1
  16. package/lib/commonjs/composition-bar/CompositionBar.js.map +1 -1
  17. package/lib/commonjs/connection-dots/ConnectionDots.web.js +2 -6
  18. package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
  19. package/lib/commonjs/design-tokens/index.js +19 -0
  20. package/lib/commonjs/design-tokens/index.js.map +1 -1
  21. package/lib/commonjs/design-tokens/tokens-json.js +149 -0
  22. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -0
  23. package/lib/commonjs/design-tokens/tokens.json +6681 -0
  24. package/lib/commonjs/dialog/Dialog.web.js +2 -6
  25. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  26. package/lib/commonjs/fab/Fab.web.js +2 -6
  27. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  28. package/lib/commonjs/fonts/apply-font-faces.web.js +8 -10
  29. package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
  30. package/lib/commonjs/frosted-icon-button/FrostedIconButton.js +7 -1
  31. package/lib/commonjs/frosted-icon-button/FrostedIconButton.js.map +1 -1
  32. package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js +2 -6
  33. package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
  34. package/lib/commonjs/input-group/InputGroup.js +6 -4
  35. package/lib/commonjs/input-group/InputGroup.js.map +1 -1
  36. package/lib/commonjs/item/Item.js +23 -5
  37. package/lib/commonjs/item/Item.js.map +1 -1
  38. package/lib/commonjs/loading/SpinnerIcon.web.js +2 -6
  39. package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
  40. package/lib/commonjs/segmented-control/index.js +12 -5
  41. package/lib/commonjs/segmented-control/index.js.map +1 -1
  42. package/lib/commonjs/select/index.js +6 -4
  43. package/lib/commonjs/select/index.js.map +1 -1
  44. package/lib/commonjs/select/index.web.js +6 -4
  45. package/lib/commonjs/select/index.web.js.map +1 -1
  46. package/lib/commonjs/settings-list/SettingsList.js +6 -4
  47. package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
  48. package/lib/commonjs/slider/Slider.js +12 -9
  49. package/lib/commonjs/slider/Slider.js.map +1 -1
  50. package/lib/commonjs/styles/adopt-style-sheet.js +126 -0
  51. package/lib/commonjs/styles/adopt-style-sheet.js.map +1 -0
  52. package/lib/commonjs/switch/Switch.js +9 -5
  53. package/lib/commonjs/switch/Switch.js.map +1 -1
  54. package/lib/commonjs/tab-bar/TabBarBase.js +6 -4
  55. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  56. package/lib/module/accordion/Accordion.js +7 -5
  57. package/lib/module/accordion/Accordion.js.map +1 -1
  58. package/lib/module/bottom-sheet/BottomSheetBase.js +7 -12
  59. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  60. package/lib/module/button/Button.js +8 -7
  61. package/lib/module/button/Button.js.map +1 -1
  62. package/lib/module/button/Button.web.js +2 -6
  63. package/lib/module/button/Button.web.js.map +1 -1
  64. package/lib/module/checkbox/Checkbox.js +10 -5
  65. package/lib/module/checkbox/Checkbox.js.map +1 -1
  66. package/lib/module/chip/Chip.js +10 -1
  67. package/lib/module/chip/Chip.js.map +1 -1
  68. package/lib/module/collapsible/Collapsible.js +5 -4
  69. package/lib/module/collapsible/Collapsible.js.map +1 -1
  70. package/lib/module/composition-bar/CompositionBar.js +6 -1
  71. package/lib/module/composition-bar/CompositionBar.js.map +1 -1
  72. package/lib/module/connection-dots/ConnectionDots.web.js +2 -6
  73. package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
  74. package/lib/module/design-tokens/index.js +5 -0
  75. package/lib/module/design-tokens/index.js.map +1 -1
  76. package/lib/module/design-tokens/tokens-json.js +144 -0
  77. package/lib/module/design-tokens/tokens-json.js.map +1 -0
  78. package/lib/module/design-tokens/tokens.json +6681 -0
  79. package/lib/module/dialog/Dialog.web.js +2 -6
  80. package/lib/module/dialog/Dialog.web.js.map +1 -1
  81. package/lib/module/fab/Fab.web.js +2 -6
  82. package/lib/module/fab/Fab.web.js.map +1 -1
  83. package/lib/module/fonts/apply-font-faces.web.js +8 -10
  84. package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
  85. package/lib/module/frosted-icon-button/FrostedIconButton.js +7 -1
  86. package/lib/module/frosted-icon-button/FrostedIconButton.js.map +1 -1
  87. package/lib/module/frosted-icon-button/FrostedIconButton.web.js +2 -6
  88. package/lib/module/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
  89. package/lib/module/input-group/InputGroup.js +6 -4
  90. package/lib/module/input-group/InputGroup.js.map +1 -1
  91. package/lib/module/item/Item.js +23 -5
  92. package/lib/module/item/Item.js.map +1 -1
  93. package/lib/module/loading/SpinnerIcon.web.js +2 -6
  94. package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
  95. package/lib/module/segmented-control/index.js +12 -5
  96. package/lib/module/segmented-control/index.js.map +1 -1
  97. package/lib/module/select/index.js +6 -4
  98. package/lib/module/select/index.js.map +1 -1
  99. package/lib/module/select/index.web.js +6 -4
  100. package/lib/module/select/index.web.js.map +1 -1
  101. package/lib/module/settings-list/SettingsList.js +6 -4
  102. package/lib/module/settings-list/SettingsList.js.map +1 -1
  103. package/lib/module/slider/Slider.js +12 -9
  104. package/lib/module/slider/Slider.js.map +1 -1
  105. package/lib/module/styles/adopt-style-sheet.js +121 -0
  106. package/lib/module/styles/adopt-style-sheet.js.map +1 -0
  107. package/lib/module/switch/Switch.js +9 -5
  108. package/lib/module/switch/Switch.js.map +1 -1
  109. package/lib/module/tab-bar/TabBarBase.js +6 -4
  110. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  111. package/lib/typescript/commonjs/accordion/Accordion.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/collapsible/Collapsible.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/composition-bar/CompositionBar.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/design-tokens/index.d.ts +2 -0
  121. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +85 -0
  123. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +5 -3
  127. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/input-group/InputGroup.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/item/Item.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/segmented-control/index.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/select/index.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/settings-list/SettingsList.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts +22 -0
  139. package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  142. package/lib/typescript/module/accordion/Accordion.d.ts.map +1 -1
  143. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  144. package/lib/typescript/module/button/Button.d.ts.map +1 -1
  145. package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
  146. package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -1
  147. package/lib/typescript/module/chip/Chip.d.ts.map +1 -1
  148. package/lib/typescript/module/collapsible/Collapsible.d.ts.map +1 -1
  149. package/lib/typescript/module/composition-bar/CompositionBar.d.ts.map +1 -1
  150. package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
  151. package/lib/typescript/module/design-tokens/index.d.ts +2 -0
  152. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
  153. package/lib/typescript/module/design-tokens/tokens-json.d.ts +85 -0
  154. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -0
  155. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  156. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  157. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +5 -3
  158. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
  159. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.d.ts.map +1 -1
  160. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
  161. package/lib/typescript/module/input-group/InputGroup.d.ts.map +1 -1
  162. package/lib/typescript/module/item/Item.d.ts.map +1 -1
  163. package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
  164. package/lib/typescript/module/segmented-control/index.d.ts.map +1 -1
  165. package/lib/typescript/module/select/index.d.ts.map +1 -1
  166. package/lib/typescript/module/select/index.web.d.ts.map +1 -1
  167. package/lib/typescript/module/settings-list/SettingsList.d.ts.map +1 -1
  168. package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
  169. package/lib/typescript/module/styles/adopt-style-sheet.d.ts +22 -0
  170. package/lib/typescript/module/styles/adopt-style-sheet.d.ts.map +1 -0
  171. package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
  172. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  173. package/package.json +4 -2
  174. package/src/__tests__/Button.test.tsx +11 -3
  175. package/src/__tests__/InputGroup.test.tsx +4 -1
  176. package/src/__tests__/Item.test.tsx +5 -1
  177. package/src/__tests__/Slider.test.tsx +11 -2
  178. package/src/__tests__/TabBar.test.tsx +9 -1
  179. package/src/__tests__/TabBarNoSelection.test.tsx +3 -1
  180. package/src/__tests__/adopt-style-sheet.node.test.ts +21 -0
  181. package/src/__tests__/adopt-style-sheet.test.ts +165 -0
  182. package/src/__tests__/apply-font-faces.test.ts +32 -0
  183. package/src/__tests__/aria-state-web.test.tsx +466 -0
  184. package/src/__tests__/design-tokens-json.test.ts +112 -0
  185. package/src/__tests__/exports-map-contract.test.ts +5 -1
  186. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  187. package/src/accordion/Accordion.tsx +5 -1
  188. package/src/bottom-sheet/BottomSheetBase.tsx +10 -14
  189. package/src/button/Button.tsx +6 -1
  190. package/src/button/Button.web.tsx +2 -6
  191. package/src/checkbox/Checkbox.tsx +8 -1
  192. package/src/chip/Chip.tsx +8 -0
  193. package/src/collapsible/Collapsible.tsx +3 -1
  194. package/src/composition-bar/CompositionBar.tsx +4 -0
  195. package/src/connection-dots/ConnectionDots.web.tsx +2 -6
  196. package/src/design-tokens/index.ts +16 -0
  197. package/src/design-tokens/tokens-json.ts +214 -0
  198. package/src/design-tokens/tokens.json +6681 -0
  199. package/src/dialog/Dialog.web.tsx +2 -6
  200. package/src/fab/Fab.web.tsx +2 -6
  201. package/src/fonts/apply-font-faces.web.ts +11 -11
  202. package/src/frosted-icon-button/FrostedIconButton.tsx +5 -0
  203. package/src/frosted-icon-button/FrostedIconButton.web.tsx +2 -6
  204. package/src/input-group/InputGroup.tsx +4 -1
  205. package/src/item/Item.tsx +24 -2
  206. package/src/loading/SpinnerIcon.web.tsx +2 -6
  207. package/src/segmented-control/index.tsx +8 -1
  208. package/src/select/index.tsx +4 -1
  209. package/src/select/index.web.tsx +4 -1
  210. package/src/settings-list/SettingsList.tsx +4 -1
  211. package/src/slider/Slider.tsx +10 -2
  212. package/src/styles/adopt-style-sheet.ts +131 -0
  213. package/src/switch/Switch.tsx +7 -1
  214. package/src/tab-bar/TabBarBase.tsx +4 -1
@@ -11,6 +11,7 @@ import {
11
11
  type StyleProp,
12
12
  } from 'react-native';
13
13
  import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler';
14
+ import { adoptStyleSheet, dropStyleSheet } from '../styles/adopt-style-sheet';
14
15
  import type { WebCssStyle } from '../styles/web-view-style';
15
16
  import { Z_INDEX } from '../styles/z-index';
16
17
  import Animated, {
@@ -921,15 +922,7 @@ const styles = StyleSheet.create({
921
922
 
922
923
  // Create web scrollbar styles dynamically based on theme
923
924
  const createWebScrollbarStyle = (borderColor: string) => {
924
- if (Platform.OS !== 'web' || typeof document === 'undefined') return;
925
-
926
- let styleElement = document.getElementById(SCROLLBAR_STYLE_ID) as HTMLStyleElement | null;
927
-
928
- if (!styleElement) {
929
- styleElement = document.createElement('style');
930
- styleElement.id = SCROLLBAR_STYLE_ID;
931
- document.head.appendChild(styleElement);
932
- }
925
+ if (Platform.OS !== 'web') return;
933
926
 
934
927
  // Derive a slightly darker scrollbar hover color from the border color
935
928
  const scrollbarColor = borderColor;
@@ -937,7 +930,9 @@ const createWebScrollbarStyle = (borderColor: string) => {
937
930
  ? borderColor.replace(/\)$/, ' / 0.7)') // add alpha for hover
938
931
  : '#888';
939
932
 
940
- styleElement.textContent = `
933
+ adoptStyleSheet(
934
+ SCROLLBAR_STYLE_ID,
935
+ `
941
936
  .bottom-sheet-scrollview::-webkit-scrollbar {
942
937
  width: 6px;
943
938
  }
@@ -952,13 +947,14 @@ const createWebScrollbarStyle = (borderColor: string) => {
952
947
  .bottom-sheet-scrollview::-webkit-scrollbar-thumb:hover {
953
948
  background: ${scrollbarHoverColor};
954
949
  }
955
- `;
950
+ `,
951
+ );
956
952
  };
957
953
 
958
954
  const SCROLLBAR_STYLE_ID = 'bottom-sheet-scrollbar-style';
959
955
 
960
- /** Remove the injected scrollbar <style> tag on unmount. */
956
+ /** Detach the scrollbar rules on unmount. */
961
957
  const removeWebScrollbarStyle = () => {
962
- if (Platform.OS !== 'web' || typeof document === 'undefined') return;
963
- document.getElementById(SCROLLBAR_STYLE_ID)?.remove();
958
+ if (Platform.OS !== 'web') return;
959
+ dropStyleSheet(SCROLLBAR_STYLE_ID);
964
960
  };
@@ -334,7 +334,12 @@ const ButtonComponent: React.FC<ButtonProps> = ({
334
334
  accessibilityLabel={accessibilityLabel}
335
335
  accessibilityHint={accessibilityHint}
336
336
  accessibilityRole="button"
337
- accessibilityState={loading ? { disabled: isInteractionBlocked, busy: true } : { disabled: isInteractionBlocked }}
337
+ // `aria-busy` matches what `Button.web.tsx` emits, so a loading button
338
+ // announces the same thing whichever fork a bundler resolves.
339
+ // react-native-web never reads `accessibilityState`; React Native folds
340
+ // `aria-busy` back into it. The disabled half travels on the `disabled`
341
+ // prop above, which both platforms map.
342
+ aria-busy={loading || undefined}
338
343
  testID={testID}
339
344
  >
340
345
  {loading ? (
@@ -12,6 +12,7 @@ import React, {
12
12
  import { useTheme } from '../theme/use-theme';
13
13
  import type { Theme } from '../theme/types';
14
14
  import { SpinnerIcon } from '../loading/SpinnerIcon.web';
15
+ import { adoptStyleSheet } from '../styles/adopt-style-sheet';
15
16
  import { flattenWebStyle } from '../styles/flatten-web-style';
16
17
  import type { ButtonProps, ButtonSize, ButtonVariant } from './types';
17
18
 
@@ -128,12 +129,7 @@ const BLOOM_BUTTON_CSS = `
128
129
 
129
130
  function useButtonCss(): void {
130
131
  useEffect(() => {
131
- if (typeof document === 'undefined') return;
132
- if (document.getElementById(STYLE_ID)) return;
133
- const style = document.createElement('style');
134
- style.id = STYLE_ID;
135
- style.textContent = BLOOM_BUTTON_CSS;
136
- document.head.appendChild(style);
132
+ adoptStyleSheet(STYLE_ID, BLOOM_BUTTON_CSS);
137
133
  }, []);
138
134
  }
139
135
 
@@ -106,7 +106,14 @@ const CheckboxComponent: React.FC<CheckboxProps> = ({
106
106
  onPressOut={onPressOut}
107
107
  disabled={disabled}
108
108
  accessibilityRole="checkbox"
109
- accessibilityState={{ checked: indeterminate ? 'mixed' : checked, disabled }}
109
+ // `aria-checked`, not `accessibilityState`: react-native-web's
110
+ // `createDOMProps` never reads `accessibilityState`, so a checkbox that
111
+ // set only that announced no state at all on web — the box was drawn
112
+ // checked while assistive tech saw an unchecked control. React Native's
113
+ // `Pressable` folds `aria-checked` back into `accessibilityState`, so
114
+ // this one prop is the spelling both platforms honour. `disabled`
115
+ // travels on the `disabled` prop above, which both platforms map.
116
+ aria-checked={indeterminate ? 'mixed' : checked}
110
117
  accessibilityLabel={accessibilityLabel ?? label}
111
118
  hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
112
119
  testID={testID}
package/src/chip/Chip.tsx CHANGED
@@ -121,7 +121,15 @@ const ChipComponent: React.FC<ChipProps> = ({
121
121
  disabled={disabled}
122
122
  accessibilityLabel={accessibilityLabel}
123
123
  accessibilityRole="button"
124
+ // Both spellings, because neither platform reads the other's and
125
+ // there is no single prop that serves both here: react-native-web
126
+ // ignores `accessibilityState`, while React Native has no
127
+ // `aria-pressed` at all (its `AccessibilityState` is
128
+ // disabled/selected/checked/busy/expanded). `aria-pressed` is also
129
+ // the state ARIA defines for a toggle with `role="button"` —
130
+ // `aria-selected` would be invalid on that role.
124
131
  accessibilityState={{ disabled, selected }}
132
+ aria-pressed={selected}
125
133
  testID={testID}
126
134
  >
127
135
  {content}
@@ -58,7 +58,9 @@ const CollapsibleComponent: React.FC<CollapsibleProps> = ({
58
58
  onPress={handleToggle}
59
59
  activeOpacity={0.7}
60
60
  accessibilityRole="button"
61
- accessibilityState={{ expanded: isOpen }}
61
+ // See `Accordion`: react-native-web reads `aria-expanded`, never
62
+ // `accessibilityState`; React Native folds it back into that state.
63
+ aria-expanded={isOpen}
62
64
  style={styles.trigger}
63
65
  >
64
66
  {chevronIcon ?? (
@@ -75,7 +75,11 @@ const CompositionBarComponent: React.FC<CompositionBarProps> = ({
75
75
  onPress={() => onSelect(category.key)}
76
76
  accessibilityRole="button"
77
77
  accessibilityLabel={category.name}
78
+ // See `Chip`: `accessibilityState` carries this on native (which
79
+ // has no `aria-pressed`), `aria-pressed` on web (where
80
+ // react-native-web never reads `accessibilityState`).
78
81
  accessibilityState={{ selected: category.key === selectedKey }}
82
+ aria-pressed={category.key === selectedKey}
79
83
  style={[
80
84
  styles.segment,
81
85
  {
@@ -1,6 +1,7 @@
1
1
  import React, { memo, useEffect, useState } from 'react';
2
2
  import { View, type ViewStyle } from 'react-native';
3
3
 
4
+ import { adoptStyleSheet } from '../styles/adopt-style-sheet';
4
5
  import type { WebCssStyle } from '../styles/web-view-style';
5
6
  import { useTheme } from '../theme/use-theme';
6
7
  import { ConnectionDotsBase } from './ConnectionDotsBase';
@@ -31,12 +32,7 @@ export const BLOOM_CONNECTION_DOTS_CSS = `
31
32
 
32
33
  function useKeyframes(): void {
33
34
  useEffect(() => {
34
- if (typeof document === 'undefined') return;
35
- if (document.getElementById(KEYFRAMES_ID)) return;
36
- const style = document.createElement('style');
37
- style.id = KEYFRAMES_ID;
38
- style.textContent = BLOOM_CONNECTION_DOTS_CSS;
39
- document.head.appendChild(style);
35
+ adoptStyleSheet(KEYFRAMES_ID, BLOOM_CONNECTION_DOTS_CSS);
40
36
  }, []);
41
37
  }
42
38
 
@@ -27,6 +27,22 @@ export type {
27
27
 
28
28
  export { bloomThemeCss, bloomThemeBlock } from './theme-css';
29
29
 
30
+ /* The same tokens as a data structure rather than a stylesheet, for a consumer
31
+ * that is not a browser (`@oxyhq/bloom/design-tokens/tokens.json` is the
32
+ * generated artifact; these rebuild it in memory). Both come from the colour
33
+ * engine, so the JSON and the CSS cannot disagree. */
34
+ export {
35
+ bloomDesignTokens,
36
+ renderBloomTokensJson,
37
+ resolvedColorToHex,
38
+ } from './tokens-json';
39
+ export type {
40
+ BloomDesignTokens,
41
+ DesignToken,
42
+ PresetExtensions,
43
+ PresetGroup,
44
+ } from './tokens-json';
45
+
30
46
  /* The resolved counterpart of `bloomThemeCss()`. That emits the alias layer
31
47
  * (`--color-x: var(--x)`); these resolve `--x` itself, so a build script can
32
48
  * write a static `:root` / `.dark` block and a prerendered page paints the real
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The machine-readable form of every Bloom design token, in W3C Design Tokens
3
+ * (DTCG) format.
4
+ *
5
+ * The CSS artifacts next to this file (`theme.css`, `bloomThemeCss()`) are the
6
+ * ALIAS layer and only make sense to a browser. A consumer that is not a
7
+ * browser — Astro codegens `SkColor` tables for Chromium's native UI so the
8
+ * toolbar and the WebUI pages cannot disagree about what "primary" is — needs
9
+ * the RESOLVED values, for every preset and both schemes, in a format it can
10
+ * read without running Bloom's colour engine. That is this file.
11
+ *
12
+ * Same single source as everything else: `getResolvedTokens`, i.e. the colour
13
+ * engine. `tokens.json` is generated from it by
14
+ * `scripts/generate-design-tokens-json.ts`, and
15
+ * `src/__tests__/design-tokens-json.test.ts` fails if the checked-in file is
16
+ * not byte-identical to a fresh render — so the JSON, the CSS and the runtime
17
+ * palette cannot drift from each other.
18
+ */
19
+
20
+ import {
21
+ APP_COLOR_NAMES,
22
+ APP_COLOR_PRESETS,
23
+ type AppColorName,
24
+ } from '../theme/color-presets';
25
+ import { getResolvedTokens } from '../theme/token-registry';
26
+ import {
27
+ BORDER_WIDTH,
28
+ RADIUS,
29
+ SPACING,
30
+ TYPOGRAPHY,
31
+ type TypeRoleName,
32
+ } from './scales';
33
+
34
+ /** One DTCG token. `$type` is inherited from the nearest group that declares it. */
35
+ export interface DesignToken {
36
+ $value: string | number;
37
+ $type?: string;
38
+ $description?: string;
39
+ }
40
+
41
+ /** Bloom-specific metadata on a preset group, namespaced per the DTCG `$extensions` rule. */
42
+ export interface PresetExtensions {
43
+ 'so.oxy.bloom': {
44
+ /** The brand seed the engine derived the whole palette from. */
45
+ seed: string;
46
+ /** The tonal scheme variant used with that seed. */
47
+ variant: string;
48
+ /** Who may pick the preset; absent means everyone. */
49
+ gate?: string;
50
+ /** Present when the preset forces a white label in both schemes. */
51
+ label?: string;
52
+ };
53
+ }
54
+
55
+ export interface PresetGroup {
56
+ $description: string;
57
+ $extensions: PresetExtensions;
58
+ light: Record<string, DesignToken>;
59
+ dark: Record<string, DesignToken>;
60
+ }
61
+
62
+ export interface BloomDesignTokens {
63
+ $description: string;
64
+ color: { $type: 'color'; $description: string } & Record<string, unknown>;
65
+ spacing: { $type: 'dimension'; $description: string } & Record<string, unknown>;
66
+ radius: { $type: 'dimension'; $description: string } & Record<string, unknown>;
67
+ borderWidth: { $type: 'dimension'; $description: string } & Record<string, unknown>;
68
+ typography: { $description: string } & Record<string, unknown>;
69
+ }
70
+
71
+ const FILE_DESCRIPTION = [
72
+ 'Bloom design tokens, resolved. W3C Design Tokens Community Group format.',
73
+ 'SHAPE CONTRACT (consumers codegen against this — additions are additive, existing paths do not move):',
74
+ 'color.<preset>.<scheme>.<token> — <preset> is a Bloom AppColorName, <scheme> is "light" or "dark",',
75
+ 'and <token> is the CSS custom property Bloom writes on the document root with its leading "--" removed,',
76
+ 'so color.oxy.dark.card carries the value of --card under the oxy preset in dark.',
77
+ 'Every colour $value is an sRGB hex string: "#rrggbb", or "#rrggbbaa" when the token carries alpha',
78
+ '(the -subtle family does). spacing/radius/borderWidth are DTCG dimensions with a px unit in the string.',
79
+ 'typography.<role> is a group of fontSize (dimension), lineHeight (dimension) and fontWeight (number);',
80
+ 'font FAMILIES are deliberately absent — they resolve to CSS variables a non-browser consumer cannot use.',
81
+ 'Shadows are absent for the same reason: Bloom ships them as a platform-forked style object (bloomShadowStyle),',
82
+ 'not as a value.',
83
+ 'AUTO-GENERATED by scripts/generate-design-tokens-json.ts from Bloom’s colour engine — do not edit.',
84
+ ].join(' ');
85
+
86
+ /**
87
+ * Turn one resolved token value into an sRGB hex string.
88
+ *
89
+ * The engine emits exactly two forms — `rgb(r g b)` for opaque tokens and
90
+ * `rgba(r, g, b, a)` for the translucent `-subtle` family. Anything else is a
91
+ * change in the engine's output that this file has not been taught about, and
92
+ * throwing at generation time is the only way that surfaces: a silently
93
+ * passed-through CSS string would end up in a C++ colour table as garbage.
94
+ */
95
+ export function resolvedColorToHex(value: string): string {
96
+ const channels = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[,/]\s*([\d.]+%?)\s*)?\)$/.exec(
97
+ value,
98
+ );
99
+ if (!channels) {
100
+ throw new Error(
101
+ `Bloom token value is not an rgb()/rgba() colour: ${JSON.stringify(value)}`,
102
+ );
103
+ }
104
+
105
+ const [, red = '0', green = '0', blue = '0', alpha] = channels;
106
+ const byte = (channel: string): string =>
107
+ Math.round(Number(channel)).toString(16).padStart(2, '0');
108
+ const hex = `#${byte(red)}${byte(green)}${byte(blue)}`;
109
+
110
+ if (alpha === undefined) return hex;
111
+ const fraction = alpha.endsWith('%')
112
+ ? Number(alpha.slice(0, -1)) / 100
113
+ : Number(alpha);
114
+ if (fraction >= 1) return hex;
115
+ return `${hex}${byte(String(fraction * 255))}`;
116
+ }
117
+
118
+ function schemeTokens(
119
+ preset: AppColorName,
120
+ scheme: 'light' | 'dark',
121
+ ): Record<string, DesignToken> {
122
+ const tokens: Record<string, DesignToken> = {};
123
+ for (const [property, value] of Object.entries(getResolvedTokens(preset, scheme))) {
124
+ tokens[property.replace(/^--/, '')] = { $value: resolvedColorToHex(value) };
125
+ }
126
+ return tokens;
127
+ }
128
+
129
+ function colorGroup(): BloomDesignTokens['color'] {
130
+ const group: Record<string, unknown> = {
131
+ $type: 'color',
132
+ $description:
133
+ 'One group per Bloom colour preset, each with a light and a dark scheme. Token names match the CSS custom properties without the leading "--".',
134
+ };
135
+
136
+ for (const name of APP_COLOR_NAMES) {
137
+ const preset = APP_COLOR_PRESETS[name];
138
+ const extensions: PresetExtensions = {
139
+ 'so.oxy.bloom': { seed: preset.hex, variant: preset.variant },
140
+ };
141
+ if (preset.gate) extensions['so.oxy.bloom'].gate = preset.gate;
142
+ if (preset.label) extensions['so.oxy.bloom'].label = preset.label;
143
+
144
+ group[name] = {
145
+ $description: `Derived by Bloom’s colour engine from seed ${preset.hex} (${preset.variant}).`,
146
+ $extensions: extensions,
147
+ light: schemeTokens(name, 'light'),
148
+ dark: schemeTokens(name, 'dark'),
149
+ } satisfies PresetGroup;
150
+ }
151
+
152
+ return group as BloomDesignTokens['color'];
153
+ }
154
+
155
+ function dimensionGroup(
156
+ description: string,
157
+ scale: Record<string, number>,
158
+ ): BloomDesignTokens['spacing'] {
159
+ const group: Record<string, unknown> = {
160
+ $type: 'dimension',
161
+ $description: description,
162
+ };
163
+ for (const [key, value] of Object.entries(scale)) {
164
+ group[key] = { $value: `${value}px` } satisfies DesignToken;
165
+ }
166
+ return group as BloomDesignTokens['spacing'];
167
+ }
168
+
169
+ function typographyGroup(): BloomDesignTokens['typography'] {
170
+ const group: Record<string, unknown> = {
171
+ $description:
172
+ 'One group per type role. Sizes and line-heights are px dimensions; weights are DTCG numeric font weights.',
173
+ };
174
+ for (const name of Object.keys(TYPOGRAPHY) as TypeRoleName[]) {
175
+ const role = TYPOGRAPHY[name];
176
+ group[name] = {
177
+ fontSize: { $type: 'dimension', $value: `${role.size}px` },
178
+ lineHeight: { $type: 'dimension', $value: `${role.lineHeight}px` },
179
+ fontWeight: { $type: 'fontWeight', $value: Number(role.weight) },
180
+ };
181
+ }
182
+ return group as BloomDesignTokens['typography'];
183
+ }
184
+
185
+ /** The whole token file, as a plain object. */
186
+ export function bloomDesignTokens(): BloomDesignTokens {
187
+ return {
188
+ $description: FILE_DESCRIPTION,
189
+ color: colorGroup(),
190
+ spacing: dimensionGroup(
191
+ 'The Oxy spacing scale. Same keys as the Tailwind/NativeWind utilities (p-space-8, gap-space-20, px-screen-margin).',
192
+ SPACING,
193
+ ),
194
+ radius: dimensionGroup(
195
+ 'The corner-radius scale. Same keys as the rounded-radius-* utilities; radius-max is the pill/circle sentinel.',
196
+ RADIUS,
197
+ ),
198
+ borderWidth: dimensionGroup(
199
+ 'Border widths. hairline is the 0.5px rule that pairs with the border-image colour role.',
200
+ BORDER_WIDTH,
201
+ ),
202
+ typography: typographyGroup(),
203
+ };
204
+ }
205
+
206
+ /**
207
+ * The exact bytes of `design-tokens/tokens.json`.
208
+ *
209
+ * Both the generator script and the drift test call this, so neither can be
210
+ * right about the file while the other is wrong.
211
+ */
212
+ export function renderBloomTokensJson(): string {
213
+ return `${JSON.stringify(bloomDesignTokens(), null, 2)}\n`;
214
+ }