@oxyhq/bloom 0.87.1 → 0.88.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 (95) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -12
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/button/Button.web.js +2 -6
  4. package/lib/commonjs/button/Button.web.js.map +1 -1
  5. package/lib/commonjs/connection-dots/ConnectionDots.web.js +2 -6
  6. package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
  7. package/lib/commonjs/design-tokens/index.js +19 -0
  8. package/lib/commonjs/design-tokens/index.js.map +1 -1
  9. package/lib/commonjs/design-tokens/tokens-json.js +149 -0
  10. package/lib/commonjs/design-tokens/tokens-json.js.map +1 -0
  11. package/lib/commonjs/design-tokens/tokens.json +6681 -0
  12. package/lib/commonjs/dialog/Dialog.web.js +2 -6
  13. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  14. package/lib/commonjs/fab/Fab.web.js +2 -6
  15. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  16. package/lib/commonjs/fonts/apply-font-faces.web.js +8 -10
  17. package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
  18. package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js +2 -6
  19. package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
  20. package/lib/commonjs/loading/SpinnerIcon.web.js +2 -6
  21. package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
  22. package/lib/commonjs/styles/adopt-style-sheet.js +126 -0
  23. package/lib/commonjs/styles/adopt-style-sheet.js.map +1 -0
  24. package/lib/module/bottom-sheet/BottomSheetBase.js +7 -12
  25. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  26. package/lib/module/button/Button.web.js +2 -6
  27. package/lib/module/button/Button.web.js.map +1 -1
  28. package/lib/module/connection-dots/ConnectionDots.web.js +2 -6
  29. package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
  30. package/lib/module/design-tokens/index.js +5 -0
  31. package/lib/module/design-tokens/index.js.map +1 -1
  32. package/lib/module/design-tokens/tokens-json.js +144 -0
  33. package/lib/module/design-tokens/tokens-json.js.map +1 -0
  34. package/lib/module/design-tokens/tokens.json +6681 -0
  35. package/lib/module/dialog/Dialog.web.js +2 -6
  36. package/lib/module/dialog/Dialog.web.js.map +1 -1
  37. package/lib/module/fab/Fab.web.js +2 -6
  38. package/lib/module/fab/Fab.web.js.map +1 -1
  39. package/lib/module/fonts/apply-font-faces.web.js +8 -10
  40. package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
  41. package/lib/module/frosted-icon-button/FrostedIconButton.web.js +2 -6
  42. package/lib/module/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
  43. package/lib/module/loading/SpinnerIcon.web.js +2 -6
  44. package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
  45. package/lib/module/styles/adopt-style-sheet.js +121 -0
  46. package/lib/module/styles/adopt-style-sheet.js.map +1 -0
  47. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/design-tokens/index.d.ts +2 -0
  51. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +85 -0
  53. package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +5 -3
  57. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts +22 -0
  61. package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts.map +1 -0
  62. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  63. package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
  64. package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
  65. package/lib/typescript/module/design-tokens/index.d.ts +2 -0
  66. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
  67. package/lib/typescript/module/design-tokens/tokens-json.d.ts +85 -0
  68. package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -0
  69. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  70. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  71. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +5 -3
  72. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
  73. package/lib/typescript/module/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
  74. package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
  75. package/lib/typescript/module/styles/adopt-style-sheet.d.ts +22 -0
  76. package/lib/typescript/module/styles/adopt-style-sheet.d.ts.map +1 -0
  77. package/package.json +4 -2
  78. package/src/__tests__/adopt-style-sheet.node.test.ts +21 -0
  79. package/src/__tests__/adopt-style-sheet.test.ts +165 -0
  80. package/src/__tests__/apply-font-faces.test.ts +32 -0
  81. package/src/__tests__/design-tokens-json.test.ts +112 -0
  82. package/src/__tests__/exports-map-contract.test.ts +5 -1
  83. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  84. package/src/bottom-sheet/BottomSheetBase.tsx +10 -14
  85. package/src/button/Button.web.tsx +2 -6
  86. package/src/connection-dots/ConnectionDots.web.tsx +2 -6
  87. package/src/design-tokens/index.ts +16 -0
  88. package/src/design-tokens/tokens-json.ts +214 -0
  89. package/src/design-tokens/tokens.json +6681 -0
  90. package/src/dialog/Dialog.web.tsx +2 -6
  91. package/src/fab/Fab.web.tsx +2 -6
  92. package/src/fonts/apply-font-faces.web.ts +11 -11
  93. package/src/frosted-icon-button/FrostedIconButton.web.tsx +2 -6
  94. package/src/loading/SpinnerIcon.web.tsx +2 -6
  95. package/src/styles/adopt-style-sheet.ts +131 -0
@@ -25,6 +25,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
25
25
 
26
26
  import { Backdrop, OverlayRoot } from '../overlay';
27
27
  import { Portal } from '../portal/index.web';
28
+ import { adoptStyleSheet } from '../styles/adopt-style-sheet';
28
29
  import { Z_INDEX } from '../styles/z-index';
29
30
  import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
30
31
  import { bloomShadowStyle } from '../design-tokens/shadows';
@@ -106,12 +107,7 @@ const DIALOG_STYLE_ID = 'bloom-dialog-web-css';
106
107
 
107
108
  function useDialogCss(): void {
108
109
  useEffect(() => {
109
- if (typeof document === 'undefined') return;
110
- if (document.getElementById(DIALOG_STYLE_ID)) return;
111
- const style = document.createElement('style');
112
- style.id = DIALOG_STYLE_ID;
113
- style.textContent = BLOOM_DIALOG_CSS;
114
- document.head.appendChild(style);
110
+ adoptStyleSheet(DIALOG_STYLE_ID, BLOOM_DIALOG_CSS);
115
111
  }, []);
116
112
  }
117
113
 
@@ -10,6 +10,7 @@ import React, {
10
10
 
11
11
  import { useTheme } from '../theme/use-theme';
12
12
  import type { Theme } from '../theme/types';
13
+ import { adoptStyleSheet } from '../styles/adopt-style-sheet';
13
14
  import { flattenWebStyle } from '../styles/flatten-web-style';
14
15
  import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
15
16
 
@@ -103,12 +104,7 @@ const BLOOM_FAB_CSS = `
103
104
 
104
105
  function useFabCss(): void {
105
106
  useEffect(() => {
106
- if (typeof document === 'undefined') return;
107
- if (document.getElementById(STYLE_ID)) return;
108
- const style = document.createElement('style');
109
- style.id = STYLE_ID;
110
- style.textContent = BLOOM_FAB_CSS;
111
- document.head.appendChild(style);
107
+ adoptStyleSheet(STYLE_ID, BLOOM_FAB_CSS);
112
108
  }, []);
113
109
  }
114
110
 
@@ -1,3 +1,4 @@
1
+ import { adoptStyleSheet } from '../styles/adopt-style-sheet';
1
2
  import {
2
3
  blomusModernusRegular,
3
4
  blomusModernusBold,
@@ -14,9 +15,11 @@ const STYLE_ID = 'bloom-fonts';
14
15
  * Web-only. The native counterpart in `apply-font-faces.ts` is a no-op
15
16
  * stub — native loads its fonts through `useFonts(FONT_ASSETS)`, so the file
16
17
  * split keeps the web font payload out of the native bundle entirely.
17
- * Idempotent: safe to call multiple times; subsequent calls early-return
18
- * after the `<style id="bloom-fonts">` tag has been mounted. SSR-safe via
19
- * the `typeof document === 'undefined'` guard.
18
+ * Idempotent: safe to call multiple times; `adoptStyleSheet` keys the sheet by
19
+ * id, so repeat calls re-parse nothing. SSR-safe via its `typeof document`
20
+ * guard. `BloomThemeProvider` calls this on every mount by default, which is
21
+ * why it also has to survive a page whose CSP forbids inline styles — see
22
+ * `styles/adopt-style-sheet.ts`.
20
23
  *
21
24
  * The URLs come from `./font-urls.web`, which imports the `.woff2` files so
22
25
  * the consuming bundler emits them as separate, cacheable assets. See that
@@ -24,12 +27,9 @@ const STYLE_ID = 'bloom-fonts';
24
27
  * provide (Metro needs `woff2` in `assetExts`).
25
28
  */
26
29
  export function applyFontFaces(): void {
27
- if (typeof document === 'undefined') return;
28
- if (document.getElementById(STYLE_ID)) return;
29
-
30
- const style = document.createElement('style');
31
- style.id = STYLE_ID;
32
- style.textContent = `
30
+ adoptStyleSheet(
31
+ STYLE_ID,
32
+ `
33
33
  @font-face { font-family: 'BlomusModernus'; src: url("${blomusModernusRegular}") format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
34
34
  @font-face { font-family: 'BlomusModernus'; src: url("${blomusModernusBold}") format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
35
35
  @font-face { font-family: 'Inter'; src: url("${interVariable}") format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
@@ -39,6 +39,6 @@ export function applyFontFaces(): void {
39
39
  --bloom-font-sans: ${fontFamilies.sans};
40
40
  --bloom-font-mono: ${fontFamilies.mono};
41
41
  }
42
- `;
43
- document.head.appendChild(style);
42
+ `,
43
+ );
44
44
  }
@@ -9,6 +9,7 @@ import React, {
9
9
  } from 'react';
10
10
 
11
11
  import { useTheme } from '../theme/use-theme';
12
+ import { adoptStyleSheet } from '../styles/adopt-style-sheet';
12
13
  import { flattenWebStyle } from '../styles/flatten-web-style';
13
14
  import {
14
15
  applyIconColor,
@@ -74,12 +75,7 @@ const BLOOM_FROSTED_ICON_BUTTON_CSS = `
74
75
 
75
76
  function useFrostedIconButtonCss(): void {
76
77
  useEffect(() => {
77
- if (typeof document === 'undefined') return;
78
- if (document.getElementById(STYLE_ID)) return;
79
- const style = document.createElement('style');
80
- style.id = STYLE_ID;
81
- style.textContent = BLOOM_FROSTED_ICON_BUTTON_CSS;
82
- document.head.appendChild(style);
78
+ adoptStyleSheet(STYLE_ID, BLOOM_FROSTED_ICON_BUTTON_CSS);
83
79
  }, []);
84
80
  }
85
81
 
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { StyleSheet, 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
 
6
7
  interface SpinnerIconProps {
@@ -72,12 +73,7 @@ const SPIN_STYLE: WebCssStyle = {
72
73
 
73
74
  function useKeyframes(): void {
74
75
  useEffect(() => {
75
- if (typeof document === 'undefined') return;
76
- if (document.getElementById(KEYFRAMES_ID)) return;
77
- const style = document.createElement('style');
78
- style.id = KEYFRAMES_ID;
79
- style.textContent = BLOOM_SPINNER_CSS;
80
- document.head.appendChild(style);
76
+ adoptStyleSheet(KEYFRAMES_ID, BLOOM_SPINNER_CSS);
81
77
  }, []);
82
78
  }
83
79
 
@@ -0,0 +1,131 @@
1
+ // ---------------------------------------------------------------------------
2
+ // adoptStyleSheet — the ONE way a Bloom web fork puts CSS on the page.
3
+ //
4
+ // Bloom's `.web.tsx` forks self-inject the CSS they need (`@keyframes`,
5
+ // pseudo-classes, `::-webkit-scrollbar`) rather than asking consumers to paste
6
+ // it into a global stylesheet — see AGENTS.md, "Web Fork CSS & Style
7
+ // Conventions". The mechanism used to be a `<style>` element carrying the CSS
8
+ // as text content. That is exactly the shape a Content-Security-Policy of
9
+ // `style-src 'self'` blocks: the check runs when a style element's text
10
+ // content is parsed into a sheet, so the rules are dropped, the component
11
+ // renders with none of its CSS, and NOTHING is thrown — the fork's own
12
+ // "already injected?" guard even finds its element in the DOM and reports
13
+ // success. Astro's `astro://` WebUI pages ship that CSP, and every other Oxy
14
+ // surface behind one would fail the same silent way.
15
+ //
16
+ // A constructed stylesheet is not subject to `style-src` at all — CSSOM is
17
+ // outside CSP's inline hooks — so `new CSSStyleSheet()` + `replaceSync()` +
18
+ // `document.adoptedStyleSheets` applies the same CSS under a strict policy
19
+ // with no nonce, no hash, and no consumer-side wiring. (This is also why
20
+ // react-native-web is unaffected: it appends an EMPTY style element and fills
21
+ // it through `sheet.insertRule`, never through text content.)
22
+ //
23
+ // The `<style>` path survives only as the fallback for a document without
24
+ // constructed-stylesheet support — jsdom 26 has the `CSSStyleSheet`
25
+ // constructor but neither `replaceSync` nor `adoptedStyleSheets`, so the whole
26
+ // jest suite takes it, and so do browsers older than Chrome 73 / Safari 16.4 /
27
+ // Firefox 101. There it behaves exactly as Bloom did before, CSP included:
28
+ // the fallback cannot make anything worse, and cannot make a strict policy
29
+ // work either.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ interface AdoptedSheet {
33
+ sheet: CSSStyleSheet;
34
+ /** The CSS currently in `sheet`, so a re-render re-parses nothing. */
35
+ css: string;
36
+ }
37
+
38
+ /**
39
+ * The sheets Bloom has constructed, per document. Keyed by document rather than
40
+ * held in one flat map because a constructed sheet belongs to the realm that
41
+ * created it — adopting one into a different document throws — and Bloom runs
42
+ * in more than one when a consumer opens a popout window.
43
+ */
44
+ const adoptedByDocument = new WeakMap<Document, Map<string, AdoptedSheet>>();
45
+
46
+ function sheetsFor(doc: Document): Map<string, AdoptedSheet> {
47
+ const existing = adoptedByDocument.get(doc);
48
+ if (existing) return existing;
49
+ const created = new Map<string, AdoptedSheet>();
50
+ adoptedByDocument.set(doc, created);
51
+ return created;
52
+ }
53
+
54
+ /**
55
+ * Whether `doc` can take a constructed stylesheet. Evaluated per call, not once
56
+ * at module scope: a test installs the API on its window after this module has
57
+ * already been imported, and a module-scope constant would freeze the answer
58
+ * from before that.
59
+ */
60
+ function supportsAdoption(doc: Document): boolean {
61
+ return (
62
+ 'adoptedStyleSheets' in doc &&
63
+ typeof CSSStyleSheet === 'function' &&
64
+ typeof CSSStyleSheet.prototype.replaceSync === 'function'
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Make `css` apply to the document, under the identity `id`.
70
+ *
71
+ * Idempotent in both directions: calling it again with the same CSS re-parses
72
+ * nothing and adopts nothing twice (the usual case — every mount of every
73
+ * instance of a component calls it), and calling it with DIFFERENT CSS under
74
+ * the same id replaces the previous content in place (the theme-dependent case,
75
+ * `BottomSheetBase`'s scrollbar colours).
76
+ *
77
+ * No-op off the web, so a platform-neutral component can call it unguarded.
78
+ */
79
+ export function adoptStyleSheet(id: string, css: string): void {
80
+ if (typeof document === 'undefined') return;
81
+
82
+ if (supportsAdoption(document)) {
83
+ const sheets = sheetsFor(document);
84
+ let entry = sheets.get(id);
85
+ if (!entry) {
86
+ entry = { sheet: new CSSStyleSheet(), css: '' };
87
+ sheets.set(id, entry);
88
+ }
89
+ if (entry.css !== css) {
90
+ entry.sheet.replaceSync(css);
91
+ entry.css = css;
92
+ }
93
+ // Re-adoption after a consumer (or a test) has reset the list. `includes`
94
+ // is what makes the common repeat call free.
95
+ if (!document.adoptedStyleSheets.includes(entry.sheet)) {
96
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, entry.sheet];
97
+ }
98
+ return;
99
+ }
100
+
101
+ const existing = document.getElementById(id);
102
+ if (existing instanceof HTMLStyleElement) {
103
+ if (existing.textContent !== css) existing.textContent = css;
104
+ return;
105
+ }
106
+ const element = document.createElement('style');
107
+ element.id = id;
108
+ element.textContent = css;
109
+ document.head.appendChild(element);
110
+ }
111
+
112
+ /**
113
+ * Detach the stylesheet `id`, whichever path installed it.
114
+ *
115
+ * Only for CSS whose lifetime is a component's — `BottomSheetBase` removes its
116
+ * scrollbar rules when the last sheet unmounts. The static per-component CSS is
117
+ * never dropped: it is the same bytes for the life of the page, and removing it
118
+ * on unmount would make a re-mount re-parse it.
119
+ */
120
+ export function dropStyleSheet(id: string): void {
121
+ if (typeof document === 'undefined') return;
122
+
123
+ const entry = adoptedByDocument.get(document)?.get(id);
124
+ if (entry) {
125
+ document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
126
+ (sheet) => sheet !== entry.sheet,
127
+ );
128
+ adoptedByDocument.get(document)?.delete(id);
129
+ }
130
+ document.getElementById(id)?.remove();
131
+ }