@oxyhq/bloom 0.5.0 → 0.6.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 (142) hide show
  1. package/lib/commonjs/error-boundary/ErrorBoundary.js +27 -7
  2. package/lib/commonjs/error-boundary/ErrorBoundary.js.map +1 -1
  3. package/lib/commonjs/fonts/FontLoader.js +6 -5
  4. package/lib/commonjs/fonts/FontLoader.js.map +1 -1
  5. package/lib/commonjs/fonts/apply-font-faces.js +4 -4
  6. package/lib/commonjs/fonts/apply-font-faces.web.js +13 -12
  7. package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
  8. package/lib/commonjs/fonts/font-assets.js +2 -2
  9. package/lib/commonjs/fonts/font-data.web.js +22 -0
  10. package/lib/commonjs/fonts/font-data.web.js.map +1 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js.map +1 -1
  13. package/lib/commonjs/skeleton/index.js +30 -0
  14. package/lib/commonjs/skeleton/index.js.map +1 -1
  15. package/lib/module/error-boundary/ErrorBoundary.js +27 -7
  16. package/lib/module/error-boundary/ErrorBoundary.js.map +1 -1
  17. package/lib/module/fonts/FontLoader.js +6 -5
  18. package/lib/module/fonts/FontLoader.js.map +1 -1
  19. package/lib/module/fonts/apply-font-faces.js +4 -4
  20. package/lib/module/fonts/apply-font-faces.web.js +13 -10
  21. package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
  22. package/lib/module/fonts/font-assets.js +2 -2
  23. package/lib/module/fonts/font-data.web.js +18 -0
  24. package/lib/module/fonts/font-data.web.js.map +1 -0
  25. package/lib/module/fonts/index.web.js +4 -4
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/index.web.js.map +1 -1
  28. package/lib/module/skeleton/index.js +29 -0
  29. package/lib/module/skeleton/index.js.map +1 -1
  30. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts +3 -1
  31. package/lib/typescript/commonjs/error-boundary/ErrorBoundary.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/error-boundary/index.d.ts +1 -1
  33. package/lib/typescript/commonjs/error-boundary/index.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/error-boundary/types.d.ts +41 -2
  35. package/lib/typescript/commonjs/error-boundary/types.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/fonts/FontLoader.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +8 -1
  38. package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/fonts/font-data.web.d.ts +5 -0
  40. package/lib/typescript/commonjs/fonts/font-data.web.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/index.d.ts +1 -1
  42. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/index.web.d.ts +1 -1
  44. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/skeleton/index.d.ts +24 -1
  46. package/lib/typescript/commonjs/skeleton/index.d.ts.map +1 -1
  47. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts +3 -1
  48. package/lib/typescript/module/error-boundary/ErrorBoundary.d.ts.map +1 -1
  49. package/lib/typescript/module/error-boundary/index.d.ts +1 -1
  50. package/lib/typescript/module/error-boundary/index.d.ts.map +1 -1
  51. package/lib/typescript/module/error-boundary/types.d.ts +41 -2
  52. package/lib/typescript/module/error-boundary/types.d.ts.map +1 -1
  53. package/lib/typescript/module/fonts/FontLoader.d.ts.map +1 -1
  54. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +8 -1
  55. package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
  56. package/lib/typescript/module/fonts/font-data.web.d.ts +5 -0
  57. package/lib/typescript/module/fonts/font-data.web.d.ts.map +1 -0
  58. package/lib/typescript/module/index.d.ts +1 -1
  59. package/lib/typescript/module/index.d.ts.map +1 -1
  60. package/lib/typescript/module/index.web.d.ts +1 -1
  61. package/lib/typescript/module/index.web.d.ts.map +1 -1
  62. package/lib/typescript/module/skeleton/index.d.ts +24 -1
  63. package/lib/typescript/module/skeleton/index.d.ts.map +1 -1
  64. package/package.json +36 -5
  65. package/src/__tests__/ErrorBoundary.test.tsx +217 -0
  66. package/src/__tests__/Skeleton.test.tsx +63 -0
  67. package/src/avatar/Avatar.stories.tsx +69 -0
  68. package/src/bottom-sheet/BottomSheet.stories.tsx +92 -0
  69. package/src/button/Button.stories.tsx +94 -0
  70. package/src/context-menu/ContextMenu.stories.tsx +71 -0
  71. package/src/dialog/Dialog.stories.tsx +112 -0
  72. package/src/error-boundary/ErrorBoundary.tsx +28 -5
  73. package/src/error-boundary/index.ts +5 -1
  74. package/src/error-boundary/types.ts +45 -2
  75. package/src/fonts/FontLoader.tsx +6 -5
  76. package/src/fonts/apply-font-faces.ts +4 -4
  77. package/src/fonts/apply-font-faces.web.ts +18 -10
  78. package/src/fonts/font-assets.ts +2 -2
  79. package/src/fonts/font-data.web.ts +15 -0
  80. package/src/fonts/index.web.ts +4 -4
  81. package/src/index.ts +5 -1
  82. package/src/index.web.ts +5 -1
  83. package/src/loading/Loading.stories.tsx +60 -0
  84. package/src/menu/Menu.stories.tsx +79 -0
  85. package/src/prompt-input/PromptInput.stories.tsx +82 -0
  86. package/src/select/Select.stories.tsx +84 -0
  87. package/src/settings-list/SettingsList.stories.tsx +106 -0
  88. package/src/skeleton/index.tsx +54 -1
  89. package/src/text-field/TextField.stories.tsx +90 -0
  90. package/src/toast/Toast.stories.tsx +109 -0
  91. package/lib/commonjs/fonts/assets/BlomusModernus-Bold.woff2 +0 -0
  92. package/lib/commonjs/fonts/assets/BlomusModernus-Regular.woff2 +0 -0
  93. package/lib/commonjs/fonts/assets/GeistMono-Variable.woff2 +0 -0
  94. package/lib/commonjs/fonts/assets/InterVariable.woff2 +0 -0
  95. package/lib/module/fonts/assets/BlomusModernus-Bold.woff2 +0 -0
  96. package/lib/module/fonts/assets/BlomusModernus-Regular.woff2 +0 -0
  97. package/lib/module/fonts/assets/GeistMono-Variable.woff2 +0 -0
  98. package/lib/module/fonts/assets/InterVariable.woff2 +0 -0
  99. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts +0 -5
  100. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +0 -1
  101. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts +0 -2
  102. package/lib/typescript/commonjs/__tests__/BloomThemeProvider.test.d.ts.map +0 -1
  103. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts +0 -2
  104. package/lib/typescript/commonjs/__tests__/BottomSheet.test.d.ts.map +0 -1
  105. package/lib/typescript/commonjs/__tests__/Button.test.d.ts +0 -2
  106. package/lib/typescript/commonjs/__tests__/Button.test.d.ts.map +0 -1
  107. package/lib/typescript/commonjs/__tests__/Code.test.d.ts +0 -2
  108. package/lib/typescript/commonjs/__tests__/Code.test.d.ts.map +0 -1
  109. package/lib/typescript/commonjs/__tests__/Dialog.test.d.ts +0 -2
  110. package/lib/typescript/commonjs/__tests__/Dialog.test.d.ts.map +0 -1
  111. package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts +0 -2
  112. package/lib/typescript/commonjs/__tests__/FontLoader.native.test.d.ts.map +0 -1
  113. package/lib/typescript/commonjs/__tests__/Pre.test.d.ts +0 -2
  114. package/lib/typescript/commonjs/__tests__/Pre.test.d.ts.map +0 -1
  115. package/lib/typescript/commonjs/__tests__/SettingsList.test.d.ts +0 -2
  116. package/lib/typescript/commonjs/__tests__/SettingsList.test.d.ts.map +0 -1
  117. package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts +0 -5
  118. package/lib/typescript/commonjs/__tests__/apply-font-faces.test.d.ts.map +0 -1
  119. package/lib/typescript/commonjs/__tests__/theme.test.d.ts +0 -2
  120. package/lib/typescript/commonjs/__tests__/theme.test.d.ts.map +0 -1
  121. package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts +0 -5
  122. package/lib/typescript/module/__tests__/BloomThemeProvider.fonts-web.test.d.ts.map +0 -1
  123. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts +0 -2
  124. package/lib/typescript/module/__tests__/BloomThemeProvider.test.d.ts.map +0 -1
  125. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts +0 -2
  126. package/lib/typescript/module/__tests__/BottomSheet.test.d.ts.map +0 -1
  127. package/lib/typescript/module/__tests__/Button.test.d.ts +0 -2
  128. package/lib/typescript/module/__tests__/Button.test.d.ts.map +0 -1
  129. package/lib/typescript/module/__tests__/Code.test.d.ts +0 -2
  130. package/lib/typescript/module/__tests__/Code.test.d.ts.map +0 -1
  131. package/lib/typescript/module/__tests__/Dialog.test.d.ts +0 -2
  132. package/lib/typescript/module/__tests__/Dialog.test.d.ts.map +0 -1
  133. package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts +0 -2
  134. package/lib/typescript/module/__tests__/FontLoader.native.test.d.ts.map +0 -1
  135. package/lib/typescript/module/__tests__/Pre.test.d.ts +0 -2
  136. package/lib/typescript/module/__tests__/Pre.test.d.ts.map +0 -1
  137. package/lib/typescript/module/__tests__/SettingsList.test.d.ts +0 -2
  138. package/lib/typescript/module/__tests__/SettingsList.test.d.ts.map +0 -1
  139. package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts +0 -5
  140. package/lib/typescript/module/__tests__/apply-font-faces.test.d.ts.map +0 -1
  141. package/lib/typescript/module/__tests__/theme.test.d.ts +0 -2
  142. package/lib/typescript/module/__tests__/theme.test.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -582,20 +582,42 @@
582
582
  "license": "MIT",
583
583
  "scripts": {
584
584
  "generate:exports": "node scripts/generate-platform-exports.mjs",
585
- "prebuild": "node scripts/generate-platform-exports.mjs",
585
+ "generate:font-data": "node scripts/generate-font-data.mjs",
586
+ "prebuild": "node scripts/generate-platform-exports.mjs && node scripts/generate-font-data.mjs",
586
587
  "build": "bob build",
587
588
  "test": "jest",
589
+ "pretest": "node scripts/generate-font-data.mjs",
588
590
  "typescript": "tsc --noEmit",
591
+ "pretypescript": "node scripts/generate-font-data.mjs",
589
592
  "clean": "rm -rf lib",
590
593
  "prepare": "bob build || true",
591
- "release": "rm -rf lib && npm run build && release-it"
594
+ "release": "rm -rf lib && bun run build && release-it",
595
+ "storybook": "storybook dev -p 6006 --no-open",
596
+ "storybook:build": "storybook build -o storybook-static"
597
+ },
598
+ "release-it": {
599
+ "git": {
600
+ "tagName": "@oxyhq/bloom@${version}",
601
+ "tagAnnotation": "Release @oxyhq/bloom@${version}",
602
+ "commitMessage": "chore: release @oxyhq/bloom@${version}"
603
+ },
604
+ "github": {
605
+ "release": true,
606
+ "releaseName": "@oxyhq/bloom@${version}"
607
+ },
608
+ "npm": {
609
+ "publish": true
610
+ }
592
611
  },
593
612
  "devDependencies": {
613
+ "@storybook/addon-docs": "^10",
614
+ "@storybook/react-vite": "^10",
594
615
  "@testing-library/react-native": "^13.3.3",
595
616
  "@types/jest": "^30.0.0",
596
617
  "@types/react": "~19.1.0",
597
618
  "@types/react-dom": "^19.2.3",
598
619
  "@types/react-native": "*",
620
+ "@vitejs/plugin-react": "^6.0.2",
599
621
  "expo-font": "^56.0.5",
600
622
  "jest": "^30.3.0",
601
623
  "jest-environment-jsdom": "^30.4.1",
@@ -607,12 +629,15 @@
607
629
  "react-native-reanimated": "^4.2.2",
608
630
  "react-native-safe-area-context": "~5.6.0",
609
631
  "react-native-svg": "^15.15.3",
632
+ "react-native-web": "^0.21.2",
610
633
  "react-test-renderer": "^19.2.0",
611
634
  "release-it": "^19.0.6",
612
635
  "sonner": "^2.0.3",
613
636
  "sonner-native": "^0.23.1",
637
+ "storybook": "^10",
614
638
  "ts-jest": "^29.4.6",
615
- "typescript": "~5.9.2"
639
+ "typescript": "~5.9.2",
640
+ "vite": "^7"
616
641
  },
617
642
  "peerDependencies": {
618
643
  "expo": "*",
@@ -656,6 +681,7 @@
656
681
  "react-native-builder-bob": {
657
682
  "source": "src",
658
683
  "output": "lib",
684
+ "exclude": "{**/{__tests__,__fixtures__,__mocks__}/**,**/*.stories.{ts,tsx},**/*.{test,spec}.{ts,tsx},**/*.woff2}",
659
685
  "targets": [
660
686
  [
661
687
  "commonjs",
@@ -669,7 +695,12 @@
669
695
  "esm": true
670
696
  }
671
697
  ],
672
- "typescript"
698
+ [
699
+ "typescript",
700
+ {
701
+ "project": "tsconfig.build.json"
702
+ }
703
+ ]
673
704
  ]
674
705
  },
675
706
  "dependencies": {
@@ -0,0 +1,217 @@
1
+ import React, { type ErrorInfo, type ReactNode } from 'react';
2
+ import { Text } from 'react-native';
3
+ import { act, render, fireEvent } from '@testing-library/react-native';
4
+
5
+ import { ErrorBoundary } from '../error-boundary';
6
+ import type {
7
+ ErrorBoundaryFallback,
8
+ ErrorBoundaryFallbackContext,
9
+ } from '../error-boundary';
10
+
11
+ /**
12
+ * Renders normally on the first mount, then throws once we flip the
13
+ * controlled flag. Used to drive the boundary into / out of its error state.
14
+ */
15
+ function MaybeThrow({ shouldThrow, message = 'boom' }: { shouldThrow: boolean; message?: string }) {
16
+ if (shouldThrow) throw new Error(message);
17
+ return <Text>ok</Text>;
18
+ }
19
+
20
+ describe('ErrorBoundary', () => {
21
+ // React intentionally logs errors caught by boundaries — silence the noise
22
+ // so test output stays useful.
23
+ let errorSpy: jest.SpyInstance;
24
+ beforeEach(() => {
25
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
26
+ });
27
+ afterEach(() => {
28
+ errorSpy.mockRestore();
29
+ });
30
+
31
+ it('renders children when no error is thrown', () => {
32
+ const { getByText } = render(
33
+ <ErrorBoundary>
34
+ <MaybeThrow shouldThrow={false} />
35
+ </ErrorBoundary>,
36
+ );
37
+ expect(getByText('ok')).toBeTruthy();
38
+ });
39
+
40
+ it('renders the default fallback when a child throws', () => {
41
+ const { getByText } = render(
42
+ <ErrorBoundary>
43
+ <MaybeThrow shouldThrow />
44
+ </ErrorBoundary>,
45
+ );
46
+ expect(getByText('Something went wrong')).toBeTruthy();
47
+ expect(getByText('An unexpected error occurred')).toBeTruthy();
48
+ expect(getByText('Try Again')).toBeTruthy();
49
+ });
50
+
51
+ it('renders a static ReactNode fallback (backward-compatible)', () => {
52
+ const { getByText, queryByText } = render(
53
+ <ErrorBoundary fallback={<Text>Static fallback</Text>}>
54
+ <MaybeThrow shouldThrow />
55
+ </ErrorBoundary>,
56
+ );
57
+ expect(getByText('Static fallback')).toBeTruthy();
58
+ // Default fallback must NOT also appear.
59
+ expect(queryByText('Something went wrong')).toBeNull();
60
+ });
61
+
62
+ it('renders a render-prop fallback with error context', () => {
63
+ const { getByText } = render(
64
+ <ErrorBoundary
65
+ fallback={({ error, retryCount }) => (
66
+ <Text>{`err:${error.message} count:${retryCount}`}</Text>
67
+ )}>
68
+ <MaybeThrow shouldThrow message="custom-boom" />
69
+ </ErrorBoundary>,
70
+ );
71
+ expect(getByText('err:custom-boom count:0')).toBeTruthy();
72
+ });
73
+
74
+ it('passes errorInfo to the render-prop after componentDidCatch', () => {
75
+ let captured: ErrorBoundaryFallbackContext | null = null;
76
+ render(
77
+ <ErrorBoundary
78
+ fallback={(ctx) => {
79
+ captured = ctx;
80
+ return <Text>captured</Text>;
81
+ }}>
82
+ <MaybeThrow shouldThrow />
83
+ </ErrorBoundary>,
84
+ );
85
+ expect(captured).not.toBeNull();
86
+ const ctx = captured as unknown as ErrorBoundaryFallbackContext;
87
+ expect(ctx.error.message).toBe('boom');
88
+ // componentDidCatch populates errorInfo with a componentStack string.
89
+ expect(ctx.errorInfo).not.toBeNull();
90
+ expect(typeof ctx.errorInfo?.componentStack).toBe('string');
91
+ });
92
+
93
+ it('invokes onError when a child throws', () => {
94
+ const onError = jest.fn<void, [Error, ErrorInfo]>();
95
+ render(
96
+ <ErrorBoundary onError={onError}>
97
+ <MaybeThrow shouldThrow message="reported" />
98
+ </ErrorBoundary>,
99
+ );
100
+ expect(onError).toHaveBeenCalledTimes(1);
101
+ const [err, info] = onError.mock.calls[0]!;
102
+ expect(err.message).toBe('reported');
103
+ expect(typeof info.componentStack).toBe('string');
104
+ });
105
+
106
+ it('increments retryCount each time retry() is invoked', () => {
107
+ /**
108
+ * The boundary's `retry()` resets `hasError` and re-renders children. To
109
+ * observe successive `retryCount` values without immediately re-throwing
110
+ * (which would just snap straight back to the fallback), we capture each
111
+ * call's count via the render-prop and trigger retry from the fallback.
112
+ */
113
+ const seenCounts: number[] = [];
114
+ let triggerRetry: (() => void) | null = null;
115
+
116
+ const fallback: ErrorBoundaryFallback = ({ retry, retryCount }) => {
117
+ seenCounts.push(retryCount);
118
+ triggerRetry = retry;
119
+ return <Text testID="retry-btn">{`count:${retryCount}`}</Text>;
120
+ };
121
+
122
+ function AlwaysThrow(): React.ReactElement {
123
+ throw new Error('persistent');
124
+ }
125
+
126
+ render(
127
+ <ErrorBoundary fallback={fallback}>
128
+ <AlwaysThrow />
129
+ </ErrorBoundary>,
130
+ );
131
+
132
+ expect(seenCounts[0]).toBe(0);
133
+ expect(triggerRetry).not.toBeNull();
134
+
135
+ // First retry — boundary resets, child throws again, fallback re-renders
136
+ // with retryCount === 1. Wrap in act() because retry() triggers React
137
+ // state updates and a synchronous error during the re-render.
138
+ act(() => {
139
+ triggerRetry?.();
140
+ });
141
+ expect(seenCounts).toContain(1);
142
+
143
+ // Second retry → count 2.
144
+ act(() => {
145
+ triggerRetry?.();
146
+ });
147
+ expect(seenCounts).toContain(2);
148
+ });
149
+
150
+ /**
151
+ * Type-only compile-time test. These assignments must type-check; they are
152
+ * never executed. If a future change breaks either variant, ts-jest will
153
+ * fail the suite at compile time.
154
+ */
155
+ it('accepts both ReactNode and render-prop fallback (compile-time)', () => {
156
+ const _staticFallback: ErrorBoundaryFallback = <Text>static</Text>;
157
+ const _undefinedFallback: ErrorBoundaryFallback = undefined as ReactNode;
158
+ const _functionFallback: ErrorBoundaryFallback = ({
159
+ error,
160
+ errorInfo,
161
+ retry,
162
+ retryCount,
163
+ }) => (
164
+ <Text>
165
+ {error.message}
166
+ {errorInfo?.componentStack ?? ''}
167
+ {retryCount}
168
+ {retry.name}
169
+ </Text>
170
+ );
171
+
172
+ // Use the values so TS does not flag them as unused.
173
+ expect(_staticFallback).toBeDefined();
174
+ expect(_undefinedFallback).toBeUndefined();
175
+ expect(typeof _functionFallback).toBe('function');
176
+ });
177
+
178
+ it('also accepts assigning fallback directly onto ErrorBoundary props', () => {
179
+ // Smoke check that the props.fallback union compiles for both variants.
180
+ const _staticProps = (
181
+ <ErrorBoundary fallback={<Text>x</Text>}>
182
+ <Text>y</Text>
183
+ </ErrorBoundary>
184
+ );
185
+ const _renderProps = (
186
+ <ErrorBoundary fallback={({ retry }) => <Text onPress={retry}>z</Text>}>
187
+ <Text>y</Text>
188
+ </ErrorBoundary>
189
+ );
190
+ expect(_staticProps).toBeDefined();
191
+ expect(_renderProps).toBeDefined();
192
+ });
193
+
194
+ it('uses custom default-fallback labels when provided', () => {
195
+ const { getByText } = render(
196
+ <ErrorBoundary title="Oops" message="boom message" retryLabel="Retry!">
197
+ <MaybeThrow shouldThrow />
198
+ </ErrorBoundary>,
199
+ );
200
+ expect(getByText('Oops')).toBeTruthy();
201
+ expect(getByText('boom message')).toBeTruthy();
202
+ expect(getByText('Retry!')).toBeTruthy();
203
+ });
204
+
205
+ it('fires the default-fallback retry button without crashing', () => {
206
+ const { getByText } = render(
207
+ <ErrorBoundary>
208
+ <MaybeThrow shouldThrow />
209
+ </ErrorBoundary>,
210
+ );
211
+ // The button is rendered inside a TouchableOpacity; pressing fires the
212
+ // internal handleRetry which resets state. The child will throw again on
213
+ // the next render, but the test only asserts the press itself does not
214
+ // throw synchronously.
215
+ expect(() => fireEvent.press(getByText('Try Again'))).not.toThrow();
216
+ });
217
+ });
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react-native';
3
+
4
+ import { BloomThemeProvider } from '../theme/BloomThemeProvider';
5
+ import * as Skeleton from '../skeleton';
6
+
7
+ function renderWithTheme(ui: React.ReactElement) {
8
+ return render(
9
+ <BloomThemeProvider mode="light" colorPreset="teal">
10
+ {ui}
11
+ </BloomThemeProvider>,
12
+ );
13
+ }
14
+
15
+ describe('Skeleton.Box', () => {
16
+ it('is exported from the Skeleton namespace', () => {
17
+ expect(typeof Skeleton.Box).toBe('function');
18
+ expect(Skeleton.Box.displayName).toBe('Skeleton.Box');
19
+ });
20
+
21
+ it('renders without crashing with numeric width/height', () => {
22
+ const { UNSAFE_root } = renderWithTheme(
23
+ <Skeleton.Box width={100} height={48} />,
24
+ );
25
+ expect(UNSAFE_root).toBeTruthy();
26
+ });
27
+
28
+ it('renders without crashing with percentage width', () => {
29
+ const { UNSAFE_root } = renderWithTheme(
30
+ <Skeleton.Box width="100%" height={200} />,
31
+ );
32
+ expect(UNSAFE_root).toBeTruthy();
33
+ });
34
+
35
+ it('renders with sharp corners when borderRadius is 0', () => {
36
+ const { UNSAFE_root } = renderWithTheme(
37
+ <Skeleton.Box width={50} height={50} borderRadius={0} />,
38
+ );
39
+ expect(UNSAFE_root).toBeTruthy();
40
+ });
41
+
42
+ it('renders with a custom large borderRadius', () => {
43
+ const { UNSAFE_root } = renderWithTheme(
44
+ <Skeleton.Box width={50} height={50} borderRadius={24} />,
45
+ );
46
+ expect(UNSAFE_root).toBeTruthy();
47
+ });
48
+
49
+ it('renders when blend prop is set', () => {
50
+ const { UNSAFE_root } = renderWithTheme(
51
+ <Skeleton.Box width={50} height={50} blend />,
52
+ );
53
+ expect(UNSAFE_root).toBeTruthy();
54
+ });
55
+
56
+ it('still exposes existing primitives (Pill, Circle, Text, Row, Col)', () => {
57
+ expect(typeof Skeleton.Pill).toBe('function');
58
+ expect(typeof Skeleton.Circle).toBe('function');
59
+ expect(typeof Skeleton.Text).toBe('function');
60
+ expect(typeof Skeleton.Row).toBe('function');
61
+ expect(typeof Skeleton.Col).toBe('function');
62
+ });
63
+ });
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Avatar } from './Avatar';
6
+
7
+ const meta: Meta<typeof Avatar> = {
8
+ title: 'Components/Avatar',
9
+ component: Avatar,
10
+ argTypes: {
11
+ size: {
12
+ control: { type: 'number', min: 16, max: 256, step: 4 },
13
+ },
14
+ shape: {
15
+ control: 'select',
16
+ options: ['circle', 'squircle'],
17
+ },
18
+ verified: { control: 'boolean' },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+
24
+ type Story = StoryObj<typeof Avatar>;
25
+
26
+ const SAMPLE_URI =
27
+ 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop';
28
+
29
+ export const Basic: Story = {
30
+ args: { size: 64, name: 'Nate Isern' },
31
+ };
32
+
33
+ export const FromUri: Story = {
34
+ args: { size: 64, uri: SAMPLE_URI },
35
+ name: 'From URI',
36
+ };
37
+
38
+ export const Initials: Story = {
39
+ args: { size: 64, name: 'Ada Lovelace' },
40
+ };
41
+
42
+ export const Squircle: Story = {
43
+ args: { size: 64, name: 'Ada Lovelace', shape: 'squircle' },
44
+ };
45
+
46
+ export const Sizes: Story = {
47
+ render: () => (
48
+ <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
49
+ <Avatar size={24} name="Ada" />
50
+ <Avatar size={32} name="Ada" />
51
+ <Avatar size={40} name="Ada" />
52
+ <Avatar size={56} name="Ada" />
53
+ <Avatar size={80} name="Ada" />
54
+ <Avatar size={120} name="Ada" />
55
+ </View>
56
+ ),
57
+ };
58
+
59
+ export const Composition: Story = {
60
+ render: () => (
61
+ <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
62
+ <Avatar size={48} name="Ada Lovelace" />
63
+ <Avatar size={48} name="Grace Hopper" />
64
+ <Avatar size={48} name="Alan Turing" />
65
+ <Avatar size={48} name="Linus Torvalds" />
66
+ <Avatar size={48} name="Margaret Hamilton" />
67
+ </View>
68
+ ),
69
+ };
@@ -0,0 +1,92 @@
1
+ import React, { useRef } from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Button } from '../button';
6
+ import { BottomSheet, type BottomSheetRef } from './index';
7
+
8
+ const meta: Meta<typeof BottomSheet> = {
9
+ title: 'Components/BottomSheet',
10
+ component: BottomSheet,
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof BottomSheet>;
16
+
17
+ function BasicSheet() {
18
+ const ref = useRef<BottomSheetRef>(null);
19
+ return (
20
+ <>
21
+ <Button onPress={() => ref.current?.present()}>Open sheet</Button>
22
+ <BottomSheet ref={ref}>
23
+ <View style={{ padding: 24, gap: 12 }}>
24
+ <Text style={{ fontSize: 20, fontWeight: '700' }}>Bottom sheet</Text>
25
+ <Text>
26
+ Pan down to close, or tap the backdrop. This is the default sheet
27
+ (flush, rounded top corners only).
28
+ </Text>
29
+ </View>
30
+ </BottomSheet>
31
+ </>
32
+ );
33
+ }
34
+
35
+ function DetachedSheet() {
36
+ const ref = useRef<BottomSheetRef>(null);
37
+ return (
38
+ <>
39
+ <Button onPress={() => ref.current?.present()}>Open detached</Button>
40
+ <BottomSheet ref={ref} detached>
41
+ <View style={{ padding: 24, gap: 12 }}>
42
+ <Text style={{ fontSize: 20, fontWeight: '700' }}>Detached</Text>
43
+ <Text>
44
+ Floating card with margins and rounded corners on all sides.
45
+ </Text>
46
+ </View>
47
+ </BottomSheet>
48
+ </>
49
+ );
50
+ }
51
+
52
+ function NonScrollableSheet() {
53
+ const ref = useRef<BottomSheetRef>(null);
54
+ return (
55
+ <>
56
+ <Button onPress={() => ref.current?.present()}>Open non-scrollable</Button>
57
+ <BottomSheet ref={ref} scrollable={false}>
58
+ <View style={{ padding: 24, gap: 12 }}>
59
+ <Text style={{ fontSize: 20, fontWeight: '700' }}>
60
+ Non-scrollable
61
+ </Text>
62
+ <Text>
63
+ Use when the body owns its own VirtualizedList (FlatList,
64
+ SectionList, etc.).
65
+ </Text>
66
+ </View>
67
+ </BottomSheet>
68
+ </>
69
+ );
70
+ }
71
+
72
+ export const Basic: Story = {
73
+ render: () => <BasicSheet />,
74
+ };
75
+
76
+ export const Detached: Story = {
77
+ render: () => <DetachedSheet />,
78
+ };
79
+
80
+ export const NonScrollable: Story = {
81
+ render: () => <NonScrollableSheet />,
82
+ };
83
+
84
+ export const Composition: Story = {
85
+ render: () => (
86
+ <View style={{ gap: 12, alignItems: 'flex-start' }}>
87
+ <BasicSheet />
88
+ <DetachedSheet />
89
+ <NonScrollableSheet />
90
+ </View>
91
+ ),
92
+ };
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Button } from './Button';
6
+
7
+ const meta: Meta<typeof Button> = {
8
+ title: 'Components/Button',
9
+ component: Button,
10
+ args: {
11
+ children: 'Button',
12
+ onPress: () => {},
13
+ },
14
+ argTypes: {
15
+ variant: {
16
+ control: 'select',
17
+ options: ['primary', 'secondary', 'inverse', 'icon', 'ghost', 'text'],
18
+ },
19
+ size: {
20
+ control: 'select',
21
+ options: ['small', 'medium', 'large'],
22
+ },
23
+ disabled: { control: 'boolean' },
24
+ loading: { control: 'boolean' },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+
30
+ type Story = StoryObj<typeof Button>;
31
+
32
+ export const Basic: Story = {
33
+ args: { children: 'Save' },
34
+ };
35
+
36
+ export const Primary: Story = {
37
+ args: { variant: 'primary', children: 'Primary' },
38
+ };
39
+
40
+ export const Secondary: Story = {
41
+ args: { variant: 'secondary', children: 'Secondary' },
42
+ };
43
+
44
+ export const Ghost: Story = {
45
+ args: { variant: 'ghost', children: 'Ghost' },
46
+ };
47
+
48
+ export const TextOnly: Story = {
49
+ args: { variant: 'text', children: 'Text button' },
50
+ name: 'Text',
51
+ };
52
+
53
+ export const Inverse: Story = {
54
+ args: { variant: 'inverse', children: 'Inverse' },
55
+ };
56
+
57
+ export const Variants: Story = {
58
+ render: () => (
59
+ <View style={{ gap: 12, alignItems: 'flex-start' }}>
60
+ <Button variant="primary">Primary</Button>
61
+ <Button variant="secondary">Secondary</Button>
62
+ <Button variant="inverse">Inverse</Button>
63
+ <Button variant="ghost">Ghost</Button>
64
+ <Button variant="text">Text</Button>
65
+ </View>
66
+ ),
67
+ };
68
+
69
+ export const Sizes: Story = {
70
+ render: () => (
71
+ <View style={{ gap: 12, alignItems: 'flex-start' }}>
72
+ <Button size="small">Small</Button>
73
+ <Button size="medium">Medium</Button>
74
+ <Button size="large">Large</Button>
75
+ </View>
76
+ ),
77
+ };
78
+
79
+ export const Loading: Story = {
80
+ args: { loading: true, children: 'Submitting' },
81
+ };
82
+
83
+ export const Disabled: Story = {
84
+ args: { disabled: true, children: 'Disabled' },
85
+ };
86
+
87
+ export const Composition: Story = {
88
+ render: () => (
89
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
90
+ <Button variant="primary">Save</Button>
91
+ <Button variant="secondary">Cancel</Button>
92
+ </View>
93
+ ),
94
+ };
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { Pressable, Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { useTheme } from '../theme/use-theme';
6
+ import * as ContextMenu from './index';
7
+
8
+ const meta: Meta = {
9
+ title: 'Components/ContextMenu',
10
+ };
11
+
12
+ export default meta;
13
+
14
+ type Story = StoryObj;
15
+
16
+ function TriggerSurface() {
17
+ const theme = useTheme();
18
+ return (
19
+ <ContextMenu.Root>
20
+ <ContextMenu.Trigger label="Long-press for actions">
21
+ {({ props }) => (
22
+ <Pressable
23
+ onPress={() => props.onPress?.()}
24
+ onLongPress={() => props.onLongPress?.()}
25
+ accessibilityLabel={props.accessibilityLabel}
26
+ accessibilityHint={props.accessibilityHint}
27
+ style={{
28
+ padding: 24,
29
+ borderRadius: 12,
30
+ backgroundColor: theme.colors.backgroundSecondary,
31
+ borderWidth: 1,
32
+ borderColor: theme.colors.borderLight,
33
+ minWidth: 240,
34
+ alignItems: 'center',
35
+ }}
36
+ >
37
+ <Text style={{ color: theme.colors.text }}>
38
+ Long-press / right-click me
39
+ </Text>
40
+ </Pressable>
41
+ )}
42
+ </ContextMenu.Trigger>
43
+ <ContextMenu.Outer>
44
+ <ContextMenu.Group>
45
+ <ContextMenu.Item label="Open" onPress={() => {}}>
46
+ <ContextMenu.ItemText>Open</ContextMenu.ItemText>
47
+ </ContextMenu.Item>
48
+ <ContextMenu.Item label="Rename" onPress={() => {}}>
49
+ <ContextMenu.ItemText>Rename</ContextMenu.ItemText>
50
+ </ContextMenu.Item>
51
+ <ContextMenu.Item label="Delete" onPress={() => {}}>
52
+ <ContextMenu.ItemText>Delete</ContextMenu.ItemText>
53
+ </ContextMenu.Item>
54
+ </ContextMenu.Group>
55
+ </ContextMenu.Outer>
56
+ </ContextMenu.Root>
57
+ );
58
+ }
59
+
60
+ export const Basic: Story = {
61
+ render: () => <TriggerSurface />,
62
+ };
63
+
64
+ export const Composition: Story = {
65
+ render: () => (
66
+ <View style={{ gap: 16 }}>
67
+ <TriggerSurface />
68
+ <TriggerSurface />
69
+ </View>
70
+ ),
71
+ };