@oxyhq/bloom 0.2.3 → 0.3.1

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 (164) hide show
  1. package/assets/fonts/BlomusModernus-Bold.ttf +0 -0
  2. package/assets/fonts/BlomusModernus-Bold.woff2 +0 -0
  3. package/assets/fonts/BlomusModernus-Regular.ttf +0 -0
  4. package/assets/fonts/BlomusModernus-Regular.woff2 +0 -0
  5. package/assets/fonts/GeistMono-Variable.ttf +0 -0
  6. package/assets/fonts/GeistMono-Variable.woff2 +0 -0
  7. package/assets/fonts/InterVariable.ttf +0 -0
  8. package/assets/fonts/InterVariable.woff2 +0 -0
  9. package/lib/commonjs/code/Code.js +47 -0
  10. package/lib/commonjs/code/Code.js.map +1 -0
  11. package/lib/commonjs/code/Pre.js +61 -0
  12. package/lib/commonjs/code/Pre.js.map +1 -0
  13. package/lib/commonjs/code/index.js +20 -0
  14. package/lib/commonjs/code/index.js.map +1 -0
  15. package/lib/commonjs/error-boundary/ErrorBoundary.js +27 -18
  16. package/lib/commonjs/error-boundary/ErrorBoundary.js.map +1 -1
  17. package/lib/commonjs/fonts/FontLoader.js +35 -0
  18. package/lib/commonjs/fonts/FontLoader.js.map +1 -0
  19. package/lib/commonjs/fonts/FontLoader.native.js +36 -0
  20. package/lib/commonjs/fonts/FontLoader.native.js.map +1 -0
  21. package/lib/commonjs/fonts/apply-font-faces.js +49 -0
  22. package/lib/commonjs/fonts/apply-font-faces.js.map +1 -0
  23. package/lib/commonjs/fonts/font-assets.js +23 -0
  24. package/lib/commonjs/fonts/font-assets.js.map +1 -0
  25. package/lib/commonjs/fonts/index.js +40 -0
  26. package/lib/commonjs/fonts/index.js.map +1 -0
  27. package/lib/commonjs/fonts/tokens.js +27 -0
  28. package/lib/commonjs/fonts/tokens.js.map +1 -0
  29. package/lib/commonjs/index.js +9 -3
  30. package/lib/commonjs/index.js.map +1 -1
  31. package/lib/commonjs/index.web.js +9 -3
  32. package/lib/commonjs/index.web.js.map +1 -1
  33. package/lib/commonjs/theme/BloomThemeProvider.js +8 -1
  34. package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
  35. package/lib/commonjs/typography/index.js +41 -3
  36. package/lib/commonjs/typography/index.js.map +1 -1
  37. package/lib/module/code/Code.js +42 -0
  38. package/lib/module/code/Code.js.map +1 -0
  39. package/lib/module/code/Pre.js +56 -0
  40. package/lib/module/code/Pre.js.map +1 -0
  41. package/lib/module/code/index.js +5 -0
  42. package/lib/module/code/index.js.map +1 -0
  43. package/lib/module/error-boundary/ErrorBoundary.js +27 -18
  44. package/lib/module/error-boundary/ErrorBoundary.js.map +1 -1
  45. package/lib/module/fonts/FontLoader.js +30 -0
  46. package/lib/module/fonts/FontLoader.js.map +1 -0
  47. package/lib/module/fonts/FontLoader.native.js +31 -0
  48. package/lib/module/fonts/FontLoader.native.js.map +1 -0
  49. package/lib/module/fonts/apply-font-faces.js +43 -0
  50. package/lib/module/fonts/apply-font-faces.js.map +1 -0
  51. package/lib/module/fonts/font-assets.js +19 -0
  52. package/lib/module/fonts/font-assets.js.map +1 -0
  53. package/lib/module/fonts/index.js +7 -0
  54. package/lib/module/fonts/index.js.map +1 -0
  55. package/lib/module/fonts/tokens.js +23 -0
  56. package/lib/module/fonts/tokens.js.map +1 -0
  57. package/lib/module/index.js +6 -0
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/index.web.js +6 -0
  60. package/lib/module/index.web.js.map +1 -1
  61. package/lib/module/theme/BloomThemeProvider.js +8 -1
  62. package/lib/module/theme/BloomThemeProvider.js.map +1 -1
  63. package/lib/module/typography/index.js +36 -3
  64. package/lib/module/typography/index.js.map +1 -1
  65. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts +5 -0
  66. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/__tests__/Code.test.d.ts +2 -0
  68. package/lib/typescript/commonjs/__tests__/Code.test.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts +2 -0
  70. package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/__tests__/Pre.test.d.ts +2 -0
  72. package/lib/typescript/commonjs/__tests__/Pre.test.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts +5 -0
  74. package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/code/Code.d.ts +7 -0
  76. package/lib/typescript/commonjs/code/Code.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/code/Pre.d.ts +8 -0
  78. package/lib/typescript/commonjs/code/Pre.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/code/index.d.ts +5 -0
  80. package/lib/typescript/commonjs/code/index.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/fonts/FontLoader.d.ts +28 -0
  83. package/lib/typescript/commonjs/fonts/FontLoader.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/fonts/FontLoader.native.d.ts +22 -0
  85. package/lib/typescript/commonjs/fonts/FontLoader.native.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/fonts/apply-font-faces.d.ts +15 -0
  87. package/lib/typescript/commonjs/fonts/apply-font-faces.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/fonts/font-assets.d.ts +7 -0
  89. package/lib/typescript/commonjs/fonts/font-assets.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/fonts/index.d.ts +7 -0
  91. package/lib/typescript/commonjs/fonts/index.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/fonts/tokens.d.ts +21 -0
  93. package/lib/typescript/commonjs/fonts/tokens.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/icons/common.d.ts +1 -1
  95. package/lib/typescript/commonjs/index.d.ts +2 -0
  96. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  98. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts +13 -1
  100. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/toast/index.d.ts +7 -7
  102. package/lib/typescript/commonjs/typography/index.d.ts +2 -0
  103. package/lib/typescript/commonjs/typography/index.d.ts.map +1 -1
  104. package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts +5 -0
  105. package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +1 -0
  106. package/lib/typescript/module/__tests__/Code.test.d.ts +2 -0
  107. package/lib/typescript/module/__tests__/Code.test.d.ts.map +1 -0
  108. package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts +2 -0
  109. package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts.map +1 -0
  110. package/lib/typescript/module/__tests__/Pre.test.d.ts +2 -0
  111. package/lib/typescript/module/__tests__/Pre.test.d.ts.map +1 -0
  112. package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts +5 -0
  113. package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts.map +1 -0
  114. package/lib/typescript/module/code/Code.d.ts +7 -0
  115. package/lib/typescript/module/code/Code.d.ts.map +1 -0
  116. package/lib/typescript/module/code/Pre.d.ts +8 -0
  117. package/lib/typescript/module/code/Pre.d.ts.map +1 -0
  118. package/lib/typescript/module/code/index.d.ts +5 -0
  119. package/lib/typescript/module/code/index.d.ts.map +1 -0
  120. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts.map +1 -1
  121. package/lib/typescript/module/fonts/FontLoader.d.ts +28 -0
  122. package/lib/typescript/module/fonts/FontLoader.d.ts.map +1 -0
  123. package/lib/typescript/module/fonts/FontLoader.native.d.ts +22 -0
  124. package/lib/typescript/module/fonts/FontLoader.native.d.ts.map +1 -0
  125. package/lib/typescript/module/fonts/apply-font-faces.d.ts +15 -0
  126. package/lib/typescript/module/fonts/apply-font-faces.d.ts.map +1 -0
  127. package/lib/typescript/module/fonts/font-assets.d.ts +7 -0
  128. package/lib/typescript/module/fonts/font-assets.d.ts.map +1 -0
  129. package/lib/typescript/module/fonts/index.d.ts +7 -0
  130. package/lib/typescript/module/fonts/index.d.ts.map +1 -0
  131. package/lib/typescript/module/fonts/tokens.d.ts +21 -0
  132. package/lib/typescript/module/fonts/tokens.d.ts.map +1 -0
  133. package/lib/typescript/module/icons/common.d.ts +1 -1
  134. package/lib/typescript/module/index.d.ts +2 -0
  135. package/lib/typescript/module/index.d.ts.map +1 -1
  136. package/lib/typescript/module/index.web.d.ts +2 -0
  137. package/lib/typescript/module/index.web.d.ts.map +1 -1
  138. package/lib/typescript/module/theme/BloomThemeProvider.d.ts +13 -1
  139. package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
  140. package/lib/typescript/module/toast/index.d.ts +7 -7
  141. package/lib/typescript/module/typography/index.d.ts +2 -0
  142. package/lib/typescript/module/typography/index.d.ts.map +1 -1
  143. package/package.json +37 -2
  144. package/src/__tests__/BloomThemeProvider.fonts-web.test.tsx +42 -0
  145. package/src/__tests__/BloomThemeProvider.test.tsx +22 -0
  146. package/src/__tests__/Code.test.tsx +25 -0
  147. package/src/__tests__/FontLoader.native.test.tsx +75 -0
  148. package/src/__tests__/Pre.test.tsx +25 -0
  149. package/src/__tests__/apply-font-faces.test.ts +59 -0
  150. package/src/assets.d.ts +20 -0
  151. package/src/code/Code.tsx +52 -0
  152. package/src/code/Pre.tsx +76 -0
  153. package/src/code/index.ts +4 -0
  154. package/src/error-boundary/ErrorBoundary.tsx +22 -7
  155. package/src/fonts/FontLoader.native.tsx +30 -0
  156. package/src/fonts/FontLoader.tsx +37 -0
  157. package/src/fonts/apply-font-faces.ts +42 -0
  158. package/src/fonts/font-assets.ts +16 -0
  159. package/src/fonts/index.ts +6 -0
  160. package/src/fonts/tokens.ts +23 -0
  161. package/src/index.ts +6 -0
  162. package/src/index.web.ts +6 -0
  163. package/src/theme/BloomThemeProvider.tsx +18 -1
  164. package/src/typography/index.tsx +32 -3
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import { render } from '@testing-library/react-native';
4
+
5
+ type UseFontsResult = readonly [boolean, Error | null];
6
+
7
+ let mockUseFontsResult: UseFontsResult = [true, null];
8
+
9
+ jest.mock('expo-font', () => ({
10
+ useFonts: () => mockUseFontsResult,
11
+ }));
12
+
13
+ import { FontLoader } from '../fonts/FontLoader.native';
14
+
15
+ function setUseFontsResult(result: UseFontsResult): void {
16
+ mockUseFontsResult = result;
17
+ }
18
+
19
+ describe('FontLoader (native)', () => {
20
+ afterEach(() => {
21
+ setUseFontsResult([true, null]);
22
+ });
23
+
24
+ it('renders children when fonts are loaded and enabled', () => {
25
+ setUseFontsResult([true, null]);
26
+ const { getByText, queryByText } = render(
27
+ <FontLoader enabled fallback={<Text>fallback</Text>}>
28
+ <Text>content</Text>
29
+ </FontLoader>,
30
+ );
31
+ expect(getByText('content')).toBeTruthy();
32
+ expect(queryByText('fallback')).toBeNull();
33
+ });
34
+
35
+ it('renders fallback while fonts are loading and enabled', () => {
36
+ setUseFontsResult([false, null]);
37
+ const { getByText, queryByText } = render(
38
+ <FontLoader enabled fallback={<Text>fallback</Text>}>
39
+ <Text>content</Text>
40
+ </FontLoader>,
41
+ );
42
+ expect(getByText('fallback')).toBeTruthy();
43
+ expect(queryByText('content')).toBeNull();
44
+ });
45
+
46
+ it('renders null fallback by default while loading', () => {
47
+ setUseFontsResult([false, null]);
48
+ const { queryByText } = render(
49
+ <FontLoader enabled>
50
+ <Text>content</Text>
51
+ </FontLoader>,
52
+ );
53
+ expect(queryByText('content')).toBeNull();
54
+ });
55
+
56
+ it('renders children when disabled, regardless of load state', () => {
57
+ setUseFontsResult([false, null]);
58
+ const { getByText } = render(
59
+ <FontLoader enabled={false} fallback={<Text>fallback</Text>}>
60
+ <Text>content</Text>
61
+ </FontLoader>,
62
+ );
63
+ expect(getByText('content')).toBeTruthy();
64
+ });
65
+
66
+ it('renders children when disabled even with a loaded result', () => {
67
+ setUseFontsResult([true, null]);
68
+ const { getByText } = render(
69
+ <FontLoader enabled={false}>
70
+ <Text>content</Text>
71
+ </FontLoader>,
72
+ );
73
+ expect(getByText('content')).toBeTruthy();
74
+ });
75
+ });
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react-native';
3
+
4
+ import { BloomThemeProvider } from '../theme/BloomThemeProvider';
5
+ import { Pre } from '../code';
6
+
7
+ function renderWithTheme(ui: React.ReactElement) {
8
+ return render(<BloomThemeProvider mode="light">{ui}</BloomThemeProvider>);
9
+ }
10
+
11
+ describe('Pre', () => {
12
+ it('renders children', () => {
13
+ const { getByText } = renderWithTheme(<Pre>{`function foo() {}`}</Pre>);
14
+ expect(getByText('function foo() {}')).toBeTruthy();
15
+ });
16
+
17
+ it('applies the Geist Mono font family on native', () => {
18
+ const { getByText } = renderWithTheme(<Pre>bar</Pre>);
19
+ const node = getByText('bar');
20
+ const flat = Array.isArray(node.props.style)
21
+ ? Object.assign({}, ...node.props.style.filter(Boolean))
22
+ : node.props.style;
23
+ expect(flat.fontFamily).toBe('Geist Mono');
24
+ });
25
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import { Platform } from 'react-native';
6
+ import { applyFontFaces } from '../fonts/apply-font-faces';
7
+
8
+ describe('applyFontFaces', () => {
9
+ const originalOS = Platform.OS;
10
+
11
+ beforeEach(() => {
12
+ document.head.innerHTML = '';
13
+ document.body.innerHTML = '';
14
+ Platform.OS = 'web';
15
+ });
16
+
17
+ afterAll(() => {
18
+ Platform.OS = originalOS;
19
+ });
20
+
21
+ it('injects a <style id="bloom-fonts"> element on first call', () => {
22
+ expect(document.getElementById('bloom-fonts')).toBeNull();
23
+ applyFontFaces();
24
+ const style = document.getElementById('bloom-fonts');
25
+ expect(style).not.toBeNull();
26
+ expect(style?.tagName).toBe('STYLE');
27
+ });
28
+
29
+ it('is idempotent — calling twice does not duplicate the <style>', () => {
30
+ applyFontFaces();
31
+ applyFontFaces();
32
+ applyFontFaces();
33
+ const styles = document.querySelectorAll('style#bloom-fonts');
34
+ expect(styles.length).toBe(1);
35
+ });
36
+
37
+ it('emits four @font-face rules', () => {
38
+ applyFontFaces();
39
+ const cssText = document.getElementById('bloom-fonts')?.textContent ?? '';
40
+ const matches = cssText.match(/@font-face\s*{/g) ?? [];
41
+ expect(matches.length).toBe(4);
42
+ });
43
+
44
+ it('declares the three Bloom font CSS variables on :root', () => {
45
+ applyFontFaces();
46
+ const cssText = document.getElementById('bloom-fonts')?.textContent ?? '';
47
+ expect(cssText).toMatch(/--bloom-font-display:/);
48
+ expect(cssText).toMatch(/--bloom-font-sans:/);
49
+ expect(cssText).toMatch(/--bloom-font-mono:/);
50
+ });
51
+
52
+ it('references all four font families by name', () => {
53
+ applyFontFaces();
54
+ const cssText = document.getElementById('bloom-fonts')?.textContent ?? '';
55
+ expect(cssText).toMatch(/font-family: 'BlomusModernus'/);
56
+ expect(cssText).toMatch(/font-family: 'Inter'/);
57
+ expect(cssText).toMatch(/font-family: 'Geist Mono'/);
58
+ });
59
+ });
package/src/assets.d.ts CHANGED
@@ -24,3 +24,23 @@ declare module '*.webp' {
24
24
  const value: number;
25
25
  export default value;
26
26
  }
27
+
28
+ declare module '*.woff2' {
29
+ const value: string;
30
+ export default value;
31
+ }
32
+
33
+ declare module '*.woff' {
34
+ const value: string;
35
+ export default value;
36
+ }
37
+
38
+ declare module '*.ttf' {
39
+ const value: number;
40
+ export default value;
41
+ }
42
+
43
+ declare module '*.otf' {
44
+ const value: number;
45
+ export default value;
46
+ }
@@ -0,0 +1,52 @@
1
+ import React, { memo } from 'react';
2
+ import {
3
+ Text as RNText,
4
+ type TextProps as RNTextProps,
5
+ Platform,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ } from 'react-native';
9
+
10
+ import { useTheme } from '../theme/use-theme';
11
+
12
+ export interface CodeProps extends RNTextProps {
13
+ style?: StyleProp<TextStyle>;
14
+ }
15
+
16
+ /**
17
+ * Inline monospace text — render as `<code>` on web (CSS var family) and
18
+ * `<Text fontFamily="Geist Mono">` on native.
19
+ */
20
+ const CodeComponent = function Code({ children, style, ...rest }: CodeProps) {
21
+ const { colors } = useTheme();
22
+
23
+ if (Platform.OS === 'web') {
24
+ return React.createElement(
25
+ 'code',
26
+ {
27
+ ...rest,
28
+ style: {
29
+ fontFamily: 'var(--bloom-font-mono)',
30
+ fontSize: '0.92em',
31
+ color: colors.text,
32
+ ...(style as object | undefined),
33
+ },
34
+ },
35
+ children,
36
+ );
37
+ }
38
+
39
+ return (
40
+ <RNText
41
+ {...rest}
42
+ style={[
43
+ { fontFamily: 'Geist Mono', fontSize: 13, color: colors.text },
44
+ style,
45
+ ]}>
46
+ {children}
47
+ </RNText>
48
+ );
49
+ };
50
+
51
+ export const Code = memo(CodeComponent);
52
+ Code.displayName = 'Code';
@@ -0,0 +1,76 @@
1
+ import React, { memo } from 'react';
2
+ import {
3
+ Text as RNText,
4
+ View,
5
+ type TextProps as RNTextProps,
6
+ Platform,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ type TextStyle,
10
+ } from 'react-native';
11
+
12
+ import { useTheme } from '../theme/use-theme';
13
+
14
+ export interface PreProps extends Omit<RNTextProps, 'style'> {
15
+ containerStyle?: StyleProp<ViewStyle>;
16
+ style?: StyleProp<TextStyle>;
17
+ }
18
+
19
+ /**
20
+ * Block-level monospace text — render as `<pre>` on web (CSS var family)
21
+ * and `<View><Text fontFamily="Geist Mono"></View>` on native.
22
+ */
23
+ const PreComponent = function Pre({
24
+ children,
25
+ containerStyle,
26
+ style,
27
+ ...rest
28
+ }: PreProps) {
29
+ const { colors } = useTheme();
30
+
31
+ if (Platform.OS === 'web') {
32
+ return React.createElement(
33
+ 'pre',
34
+ {
35
+ ...rest,
36
+ style: {
37
+ fontFamily: 'var(--bloom-font-mono)',
38
+ fontSize: '0.92em',
39
+ color: colors.text,
40
+ backgroundColor: colors.backgroundSecondary,
41
+ padding: '12px 16px',
42
+ borderRadius: 8,
43
+ overflow: 'auto',
44
+ margin: 0,
45
+ ...(containerStyle as object | undefined),
46
+ ...(style as object | undefined),
47
+ },
48
+ },
49
+ children,
50
+ );
51
+ }
52
+
53
+ return (
54
+ <View
55
+ style={[
56
+ {
57
+ backgroundColor: colors.backgroundSecondary,
58
+ padding: 12,
59
+ borderRadius: 8,
60
+ },
61
+ containerStyle,
62
+ ]}>
63
+ <RNText
64
+ {...rest}
65
+ style={[
66
+ { fontFamily: 'Geist Mono', fontSize: 13, color: colors.text },
67
+ style,
68
+ ]}>
69
+ {children}
70
+ </RNText>
71
+ </View>
72
+ );
73
+ };
74
+
75
+ export const Pre = memo(PreComponent);
76
+ Pre.displayName = 'Pre';
@@ -0,0 +1,4 @@
1
+ export { Code } from './Code';
2
+ export type { CodeProps } from './Code';
3
+ export { Pre } from './Pre';
4
+ export type { PreProps } from './Pre';
@@ -1,7 +1,6 @@
1
1
  import React, { Component, type ErrorInfo, type ReactNode } from 'react';
2
2
  import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
3
3
 
4
- import { useTheme } from '../theme/use-theme';
5
4
  import type { ErrorBoundaryProps } from './types';
6
5
 
7
6
  interface ErrorBoundaryState {
@@ -9,6 +8,18 @@ interface ErrorBoundaryState {
9
8
  error: Error | null;
10
9
  }
11
10
 
11
+ /**
12
+ * Default fallback UI used when an ErrorBoundary catches a render error.
13
+ *
14
+ * This component is the LAST line of defense — by the time it renders, the
15
+ * rest of the React tree has already crashed. It therefore MUST NOT depend
16
+ * on any other context provider (theme, navigation, router, etc.) because
17
+ * those providers may be unmounted, broken, or never have been part of
18
+ * the tree above the boundary.
19
+ *
20
+ * Use only literal styles and built-in React Native primitives. No hooks
21
+ * that read context, no upstream theming, no animation libraries.
22
+ */
12
23
  function DefaultFallback({
13
24
  title,
14
25
  message,
@@ -20,14 +31,12 @@ function DefaultFallback({
20
31
  retryLabel: string;
21
32
  onRetry: () => void;
22
33
  }) {
23
- const theme = useTheme();
24
-
25
34
  return (
26
- <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
27
- <Text style={[styles.title, { color: theme.colors.text }]}>{title}</Text>
28
- <Text style={[styles.message, { color: theme.colors.textSecondary }]}>{message}</Text>
35
+ <View style={styles.container}>
36
+ <Text style={styles.title}>{title}</Text>
37
+ <Text style={styles.message}>{message}</Text>
29
38
  <TouchableOpacity
30
- style={[styles.retryButton, { backgroundColor: theme.colors.primary }]}
39
+ style={styles.retryButton}
31
40
  onPress={onRetry}
32
41
  activeOpacity={0.7}
33
42
  >
@@ -77,18 +86,22 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
77
86
  }
78
87
  }
79
88
 
89
+ // Literal colors only — must never depend on a theme provider. The neutral
90
+ // palette is chosen to be readable on both light and dark device defaults.
80
91
  const styles = StyleSheet.create({
81
92
  container: {
82
93
  flex: 1,
83
94
  alignItems: 'center',
84
95
  justifyContent: 'center',
85
96
  padding: 20,
97
+ backgroundColor: '#FFFFFF',
86
98
  },
87
99
  title: {
88
100
  fontSize: 24,
89
101
  fontWeight: 'bold',
90
102
  marginBottom: 12,
91
103
  textAlign: 'center',
104
+ color: '#111111',
92
105
  },
93
106
  message: {
94
107
  fontSize: 16,
@@ -96,6 +109,7 @@ const styles = StyleSheet.create({
96
109
  marginBottom: 24,
97
110
  lineHeight: 22,
98
111
  paddingHorizontal: 16,
112
+ color: '#555555',
99
113
  },
100
114
  retryButton: {
101
115
  paddingHorizontal: 24,
@@ -103,6 +117,7 @@ const styles = StyleSheet.create({
103
117
  borderRadius: 12,
104
118
  minWidth: 120,
105
119
  alignItems: 'center',
120
+ backgroundColor: '#0066FF',
106
121
  },
107
122
  retryText: {
108
123
  color: '#FFFFFF',
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useFonts } from 'expo-font';
3
+ import { FONT_ASSETS } from './font-assets';
4
+
5
+ export interface FontLoaderProps {
6
+ /**
7
+ * Whether to gate rendering on fonts being loaded. When false the
8
+ * component is a pass-through that still calls `useFonts` (the hook must
9
+ * run unconditionally per the Rules of Hooks).
10
+ */
11
+ enabled: boolean;
12
+ /** Rendered while native fonts load. Defaults to `null`. */
13
+ fallback?: React.ReactNode;
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ /**
18
+ * Native font loader. Calls `expo-font`'s `useFonts` with the Bloom font
19
+ * asset map and gates `children` on the load result. The hook is invoked
20
+ * unconditionally — `enabled` only affects what's rendered, not whether
21
+ * fonts get loaded. This keeps Hook order stable across renders and means
22
+ * `<BloomThemeProvider fonts={false}>` still gets fonts pre-loaded if the
23
+ * provider tree ever flips `fonts` back on.
24
+ */
25
+ export function FontLoader({ enabled, fallback, children }: FontLoaderProps) {
26
+ const [loaded] = useFonts(FONT_ASSETS);
27
+ if (!enabled) return <>{children}</>;
28
+ if (!loaded) return <>{fallback ?? null}</>;
29
+ return <>{children}</>;
30
+ }
@@ -0,0 +1,37 @@
1
+ import React, { useRef } from 'react';
2
+ import { applyFontFaces } from './apply-font-faces';
3
+
4
+ export interface FontLoaderProps {
5
+ /**
6
+ * Whether to load and inject the Bloom font system. When false, this
7
+ * component is a pass-through. Default true (set by `BloomThemeProvider`).
8
+ */
9
+ enabled: boolean;
10
+ /**
11
+ * Rendered while native fonts load. Ignored on web — the web variant
12
+ * applies CSS `@font-face` rules synchronously and uses `font-display: swap`
13
+ * to handle the FOUT period.
14
+ */
15
+ fallback?: React.ReactNode;
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ /**
20
+ * Web font loader. Applies the Bloom `@font-face` rules and CSS variables
21
+ * to `:root` on first render, before the first paint, using the same
22
+ * `useRef`-during-render pattern as `BloomThemeProvider`'s color application.
23
+ *
24
+ * No `useEffect`: the side effect runs synchronously inside the render phase,
25
+ * gated by a ref so it only executes once. Subsequent renders short-circuit.
26
+ *
27
+ * Native consumers resolve `FontLoader.native.tsx` via the React Native /
28
+ * Metro bundler's platform extension resolution — they never load this file.
29
+ */
30
+ export function FontLoader({ enabled, children }: FontLoaderProps) {
31
+ const applied = useRef(false);
32
+ if (enabled && !applied.current) {
33
+ applied.current = true;
34
+ applyFontFaces();
35
+ }
36
+ return <>{children}</>;
37
+ }
@@ -0,0 +1,42 @@
1
+ /// <reference path="../assets.d.ts" />
2
+ import { Platform } from 'react-native';
3
+ import blomusReg from '../../assets/fonts/BlomusModernus-Regular.woff2';
4
+ import blomusBold from '../../assets/fonts/BlomusModernus-Bold.woff2';
5
+ import interVar from '../../assets/fonts/InterVariable.woff2';
6
+ import geistMono from '../../assets/fonts/GeistMono-Variable.woff2';
7
+ import { fontFamilies } from './tokens';
8
+
9
+ const STYLE_ID = 'bloom-fonts';
10
+
11
+ /**
12
+ * Inject @font-face rules and font CSS variables onto :root.
13
+ *
14
+ * No-op on native and when `document` is unavailable (SSR). Idempotent —
15
+ * safe to call multiple times; subsequent calls early-return after the
16
+ * `<style id="bloom-fonts">` tag has been mounted.
17
+ *
18
+ * Follows the same shape as `applyDarkClass` / `applyColorPresetVars`: a
19
+ * single file with an internal `Platform.OS` check rather than a `.web.ts` /
20
+ * `.native.ts` split. Bundlers strip the unreachable web import code path
21
+ * on native because the function body short-circuits before referencing the
22
+ * woff2 URLs.
23
+ */
24
+ export function applyFontFaces(): void {
25
+ if (Platform.OS !== 'web' || typeof document === 'undefined') return;
26
+ if (document.getElementById(STYLE_ID)) return;
27
+
28
+ const style = document.createElement('style');
29
+ style.id = STYLE_ID;
30
+ style.textContent = `
31
+ @font-face { font-family: 'BlomusModernus'; src: url(${blomusReg}) format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
32
+ @font-face { font-family: 'BlomusModernus'; src: url(${blomusBold}) format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
33
+ @font-face { font-family: 'Inter'; src: url(${interVar}) format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
34
+ @font-face { font-family: 'Geist Mono'; src: url(${geistMono}) format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
35
+ :root {
36
+ --bloom-font-display: ${fontFamilies.display};
37
+ --bloom-font-sans: ${fontFamilies.sans};
38
+ --bloom-font-mono: ${fontFamilies.mono};
39
+ }
40
+ `;
41
+ document.head.appendChild(style);
42
+ }
@@ -0,0 +1,16 @@
1
+ // Native font asset map for `useFonts(FONT_ASSETS)`.
2
+ //
3
+ // We ship variable .ttf files for Inter and Geist Mono (extracted from the
4
+ // official rsms/inter and vercel/geist-font releases) so the same family name
5
+ // covers all weights at runtime. `@fontsource(-variable)?/*` packages only
6
+ // publish .woff2 for their variable axes — modern react-native font loading
7
+ // requires .ttf, so we use the upstream variable TTFs instead. On web,
8
+ // `apply-font-faces.ts` references the .woff2 variants directly and this file
9
+ // is never imported.
10
+
11
+ export const FONT_ASSETS = {
12
+ BlomusModernus: require('../../assets/fonts/BlomusModernus-Regular.ttf'),
13
+ 'BlomusModernus-Bold': require('../../assets/fonts/BlomusModernus-Bold.ttf'),
14
+ Inter: require('../../assets/fonts/InterVariable.ttf'),
15
+ 'Geist Mono': require('../../assets/fonts/GeistMono-Variable.ttf'),
16
+ } as const;
@@ -0,0 +1,6 @@
1
+ export { fontFamilies, fontCssVars } from './tokens';
2
+ export type { FontFamilyName } from './tokens';
3
+ export { applyFontFaces } from './apply-font-faces';
4
+ export { FONT_ASSETS } from './font-assets';
5
+ export { FontLoader } from './FontLoader';
6
+ export type { FontLoaderProps } from './FontLoader';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Font family tokens for the Bloom design system.
3
+ *
4
+ * `fontFamilies` resolves to CSS-style stacks (with system fallbacks) and is
5
+ * used to populate the `:root` custom properties on web and as the literal
6
+ * `fontFamily` string source on native.
7
+ *
8
+ * `fontCssVars` is the inverse map: name -> CSS custom property name.
9
+ */
10
+
11
+ export const fontFamilies = {
12
+ display: 'BlomusModernus, Georgia, "Times New Roman", serif',
13
+ sans: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
14
+ mono: '"Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace',
15
+ } as const;
16
+
17
+ export const fontCssVars = {
18
+ display: '--bloom-font-display',
19
+ sans: '--bloom-font-sans',
20
+ mono: '--bloom-font-mono',
21
+ } as const;
22
+
23
+ export type FontFamilyName = keyof typeof fontFamilies;
package/src/index.ts CHANGED
@@ -68,3 +68,9 @@ export * as Menu from './menu';
68
68
  export * as Tooltip from './tooltip';
69
69
  export * as Select from './select';
70
70
  export * as ContextMenu from './context-menu';
71
+
72
+ // Code (mono)
73
+ export * as Code from './code';
74
+
75
+ // Fonts
76
+ export * as Fonts from './fonts';
package/src/index.web.ts CHANGED
@@ -73,3 +73,9 @@ export * as Menu from './menu/index.web';
73
73
  export * as Tooltip from './tooltip/index.web';
74
74
  export * as Select from './select/index.web';
75
75
  export * as ContextMenu from './context-menu/index.web';
76
+
77
+ // Code (mono)
78
+ export * as Code from './code';
79
+
80
+ // Fonts
81
+ export * as Fonts from './fonts';
@@ -4,6 +4,7 @@ import { APP_COLOR_PRESETS, type AppColorName } from './color-presets';
4
4
  import { getAdaptiveColors } from './adaptive-colors';
5
5
  import { applyDarkClass, applyColorPresetVars } from './apply-dark-class';
6
6
  import { setColorSchemeSafe } from './set-color-scheme-safe';
7
+ import { FontLoader } from '../fonts/FontLoader';
7
8
  import type { Theme, ThemeColors, ThemeMode } from './types';
8
9
 
9
10
  function hslVarToCSS(value: string): string {
@@ -112,6 +113,18 @@ export interface BloomThemeProviderProps {
112
113
  colorPreset?: AppColorName;
113
114
  onModeChange?: (mode: ThemeMode) => void;
114
115
  onColorPresetChange?: (preset: AppColorName) => void;
116
+ /**
117
+ * Load and inject the Bloom font system (BlomusModernus + Inter Variable
118
+ * + Geist Mono Variable). Default true. Set to false to opt out — e.g.
119
+ * apps that already ship their own font loader.
120
+ */
121
+ fonts?: boolean;
122
+ /**
123
+ * Rendered while native fonts load. Ignored on web. Useful for matching
124
+ * an app-level splash screen so consumers don't see a system-font flash
125
+ * before the bundled fonts resolve.
126
+ */
127
+ onFontsLoading?: React.ReactNode;
115
128
  children: React.ReactNode;
116
129
  }
117
130
 
@@ -120,6 +133,8 @@ export function BloomThemeProvider({
120
133
  colorPreset: controlledPreset,
121
134
  onModeChange,
122
135
  onColorPresetChange,
136
+ fonts = true,
137
+ onFontsLoading,
123
138
  children,
124
139
  }: BloomThemeProviderProps) {
125
140
  const rnScheme = useRNColorScheme();
@@ -175,7 +190,9 @@ export function BloomThemeProvider({
175
190
 
176
191
  return (
177
192
  <BloomThemeContext.Provider value={contextValue}>
178
- {children}
193
+ <FontLoader enabled={fonts} fallback={onFontsLoading}>
194
+ {children}
195
+ </FontLoader>
179
196
  </BloomThemeContext.Provider>
180
197
  );
181
198
  }