@oxyhq/bloom 0.32.0 → 0.33.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.
- package/lib/commonjs/button/Button.web.js +6 -29
- package/lib/commonjs/button/Button.web.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +11 -2
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/hooks/index.js +13 -0
- package/lib/commonjs/hooks/index.js.map +1 -1
- package/lib/commonjs/hooks/useImagePreload.js +52 -0
- package/lib/commonjs/hooks/useImagePreload.js.map +1 -0
- package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js +65 -0
- package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -0
- package/lib/commonjs/image-aspect-ratio-cache/index.js +37 -0
- package/lib/commonjs/image-aspect-ratio-cache/index.js.map +1 -0
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +15 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/styles/flatten-web-style.js +49 -0
- package/lib/commonjs/styles/flatten-web-style.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +664 -0
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/constants.js +41 -0
- package/lib/commonjs/zoomable-image-gallery/constants.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/index.js +19 -0
- package/lib/commonjs/zoomable-image-gallery/index.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/types.js +2 -0
- package/lib/commonjs/zoomable-image-gallery/types.js.map +1 -0
- package/lib/module/button/Button.web.js +6 -29
- package/lib/module/button/Button.web.js.map +1 -1
- package/lib/module/fab/Fab.web.js +11 -2
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useImagePreload.js +48 -0
- package/lib/module/hooks/useImagePreload.js.map +1 -0
- package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js +57 -0
- package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -0
- package/lib/module/image-aspect-ratio-cache/index.js +4 -0
- package/lib/module/image-aspect-ratio-cache/index.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/styles/flatten-web-style.js +45 -0
- package/lib/module/styles/flatten-web-style.js.map +1 -0
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +659 -0
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -0
- package/lib/module/zoomable-image-gallery/constants.js +38 -0
- package/lib/module/zoomable-image-gallery/constants.js.map +1 -0
- package/lib/module/zoomable-image-gallery/index.js +4 -0
- package/lib/module/zoomable-image-gallery/index.js.map +1 -0
- package/lib/module/zoomable-image-gallery/types.js +2 -0
- package/lib/module/zoomable-image-gallery/types.js.map +1 -0
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/index.d.ts +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/useImagePreload.d.ts +10 -0
- package/lib/typescript/commonjs/hooks/useImagePreload.d.ts.map +1 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +14 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts +2 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/flatten-web-style.d.ts +6 -0
- package/lib/typescript/commonjs/styles/flatten-web-style.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +5 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/constants.d.ts +33 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +3 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +46 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +1 -0
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/hooks/index.d.ts +1 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/useImagePreload.d.ts +10 -0
- package/lib/typescript/module/hooks/useImagePreload.d.ts.map +1 -0
- package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +14 -0
- package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -0
- package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts +2 -0
- package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/styles/flatten-web-style.d.ts +6 -0
- package/lib/typescript/module/styles/flatten-web-style.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +5 -0
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/constants.d.ts +33 -0
- package/lib/typescript/module/zoomable-image-gallery/constants.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts +3 -0
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts +46 -0
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +1 -0
- package/package.json +27 -1
- package/src/__tests__/Fab.web.test.tsx +62 -0
- package/src/button/Button.web.tsx +6 -36
- package/src/fab/Fab.web.tsx +11 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useImagePreload.ts +51 -0
- package/src/image-aspect-ratio-cache/imageAspectRatioCache.ts +62 -0
- package/src/image-aspect-ratio-cache/index.ts +7 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/styles/flatten-web-style.ts +58 -0
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +695 -0
- package/src/zoomable-image-gallery/constants.ts +25 -0
- package/src/zoomable-image-gallery/index.ts +8 -0
- package/src/zoomable-image-gallery/types.ts +49 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/constants.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,eAAO,MAAM,YAAY,MAAM,CAAC;AAChC,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,kFAAkF;AAClF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,uDAAuD;AACvD,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,0FAA0F;AAC1F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,eAAO,MAAM,WAAW;;;;CAAsD,CAAC;AAC/E,eAAO,MAAM,YAAY;;;;CAAuD,CAAC;AACjF,eAAO,MAAM,gBAAgB;;;;CAAsD,CAAC;AAEpF,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,IAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,IAAI,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAC/F,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface GalleryImage {
|
|
2
|
+
/** Source URI rendered both as the thumbnail and the zoomed image. */
|
|
3
|
+
uri: string;
|
|
4
|
+
/**
|
|
5
|
+
* Author-authored accessibility description (Bluesky-style "ALT"). When present,
|
|
6
|
+
* shown as a caption at the bottom of the fullscreen viewer for the active image.
|
|
7
|
+
*/
|
|
8
|
+
alt?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional intrinsic aspect ratio (width / height) supplied by the consumer.
|
|
11
|
+
* When known ahead of time it avoids a first-frame `Image.getSize` round-trip.
|
|
12
|
+
*/
|
|
13
|
+
aspectRatio?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* On-screen rectangle of a thumbnail, used to fly the zoom transition to/from
|
|
17
|
+
* the tapped image. Bloom-owned so consumers don't depend on an app-local shape.
|
|
18
|
+
*/
|
|
19
|
+
export interface MeasuredRect {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the on-screen rect of the thumbnail at `index` within the SAME
|
|
27
|
+
* images-only subset the gallery opens/pages in, so the close animation can fly
|
|
28
|
+
* back to the image currently being viewed. Resolves `null` when the thumbnail
|
|
29
|
+
* ref is missing (unmounted/virtualized), in which case the gallery falls back
|
|
30
|
+
* to a plain center fade-out.
|
|
31
|
+
*/
|
|
32
|
+
export type MeasureThumb = (index: number) => Promise<MeasuredRect | null>;
|
|
33
|
+
export interface ZoomableImageGalleryHandle {
|
|
34
|
+
/**
|
|
35
|
+
* Open the gallery at `index` within `images`, animating the zoom from the
|
|
36
|
+
* tapped thumbnail's measured screen rect (when provided).
|
|
37
|
+
*/
|
|
38
|
+
open: (images: GalleryImage[], index: number, rect?: MeasuredRect) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface ZoomableImageGalleryProps {
|
|
41
|
+
/** Measures any thumbnail by its images-only subset index, used on dismiss. */
|
|
42
|
+
measureThumb?: MeasureThumb;
|
|
43
|
+
/** Corner radius applied to the zoomed images. Defaults to `DEFAULT_CORNER_RADIUS`. */
|
|
44
|
+
cornerRadius?: number;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;AAE3E,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,IAAI,CAAC;CAC5E;AAED,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|
|
@@ -39,6 +39,17 @@
|
|
|
39
39
|
"default": "./lib/commonjs/image-resolver/index.js"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
|
+
"./image-aspect-ratio-cache": {
|
|
43
|
+
"react-native": "./src/image-aspect-ratio-cache/index.ts",
|
|
44
|
+
"import": {
|
|
45
|
+
"types": "./lib/typescript/module/image-aspect-ratio-cache/index.d.ts",
|
|
46
|
+
"default": "./lib/module/image-aspect-ratio-cache/index.js"
|
|
47
|
+
},
|
|
48
|
+
"require": {
|
|
49
|
+
"types": "./lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts",
|
|
50
|
+
"default": "./lib/commonjs/image-aspect-ratio-cache/index.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
42
53
|
"./theme": {
|
|
43
54
|
"react-native": "./src/theme/index.ts",
|
|
44
55
|
"import": {
|
|
@@ -382,6 +393,17 @@
|
|
|
382
393
|
"default": "./lib/commonjs/media-inset-border/index.js"
|
|
383
394
|
}
|
|
384
395
|
},
|
|
396
|
+
"./zoomable-image-gallery": {
|
|
397
|
+
"react-native": "./src/zoomable-image-gallery/index.ts",
|
|
398
|
+
"import": {
|
|
399
|
+
"types": "./lib/typescript/module/zoomable-image-gallery/index.d.ts",
|
|
400
|
+
"default": "./lib/module/zoomable-image-gallery/index.js"
|
|
401
|
+
},
|
|
402
|
+
"require": {
|
|
403
|
+
"types": "./lib/typescript/commonjs/zoomable-image-gallery/index.d.ts",
|
|
404
|
+
"default": "./lib/commonjs/zoomable-image-gallery/index.js"
|
|
405
|
+
}
|
|
406
|
+
},
|
|
385
407
|
"./pressable-scale": {
|
|
386
408
|
"react-native": "./src/pressable-scale/index.ts",
|
|
387
409
|
"import": {
|
|
@@ -1054,8 +1076,10 @@
|
|
|
1054
1076
|
"@types/react-dom": "^19.2.3",
|
|
1055
1077
|
"@types/react-native": "*",
|
|
1056
1078
|
"@vitejs/plugin-react": "^6.0.2",
|
|
1079
|
+
"expo-blur": "~56.0.3",
|
|
1057
1080
|
"expo-font": "^56.0.5",
|
|
1058
1081
|
"expo-haptics": "~56.0.3",
|
|
1082
|
+
"expo-image": "~56.0.11",
|
|
1059
1083
|
"expo-router": "^56.0.0",
|
|
1060
1084
|
"jest": "^30.3.0",
|
|
1061
1085
|
"jest-environment-jsdom": "^30.4.1",
|
|
@@ -1079,8 +1103,10 @@
|
|
|
1079
1103
|
},
|
|
1080
1104
|
"peerDependencies": {
|
|
1081
1105
|
"expo": "*",
|
|
1106
|
+
"expo-blur": "*",
|
|
1082
1107
|
"expo-font": "*",
|
|
1083
1108
|
"expo-haptics": "*",
|
|
1109
|
+
"expo-image": "*",
|
|
1084
1110
|
"expo-router": ">=3.0.0",
|
|
1085
1111
|
"react": ">=18.0.0",
|
|
1086
1112
|
"react-dom": ">=18.0.0",
|
|
@@ -138,4 +138,66 @@ describe('Fab.web', () => {
|
|
|
138
138
|
expect(fab.style.width).toBe('56px');
|
|
139
139
|
expect(fab.style.height).toBe('56px');
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
// Regression: `style` / `labelStyle` are React-Native `StyleProp`s (array-
|
|
143
|
+
// capable — the native fork passes `style={[placementStyle(...), {...}, style]}`).
|
|
144
|
+
// Spreading a raw array into the DOM element's inline style used to produce
|
|
145
|
+
// numeric keys and throw at runtime:
|
|
146
|
+
// "Failed to set an indexed property [0] on 'CSSStyleDeclaration'".
|
|
147
|
+
describe('style prop flattening', () => {
|
|
148
|
+
it('renders without crashing when style is an array with a falsy hole', () => {
|
|
149
|
+
const showBorder = false;
|
|
150
|
+
const c = mount(
|
|
151
|
+
<Fab
|
|
152
|
+
accessibilityLabel="Add"
|
|
153
|
+
icon={<span>+</span>}
|
|
154
|
+
style={[{ marginBottom: 10 }, showBorder && { borderColor: 'rgb(9, 9, 9)' }]}
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
157
|
+
const fab = getByRole(c, 'button');
|
|
158
|
+
expect(fab.tagName).toBe('BUTTON');
|
|
159
|
+
// The array is flattened onto the button; falsy entries are skipped.
|
|
160
|
+
expect(fab.style.marginBottom).toBe('10px');
|
|
161
|
+
// The crash signature: a leaked numeric ("0") style key.
|
|
162
|
+
expect(fab.getAttribute('style') ?? '').not.toMatch(/(^|;)\s*0\s*:/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('later array entries win, mirroring RN precedence, and override the base container style', () => {
|
|
166
|
+
const c = mount(
|
|
167
|
+
<Fab
|
|
168
|
+
accessibilityLabel="Add"
|
|
169
|
+
icon={<span>+</span>}
|
|
170
|
+
// `zIndex: 7` overrides the container's default zIndex (50).
|
|
171
|
+
style={[{ zIndex: 3 }, { zIndex: 7, backgroundColor: 'rgb(1, 2, 3)' }]}
|
|
172
|
+
/>,
|
|
173
|
+
);
|
|
174
|
+
const fab = getByRole(c, 'button');
|
|
175
|
+
expect(fab.style.zIndex).toBe('7');
|
|
176
|
+
// Caller style is spread after the variant container style, so it wins.
|
|
177
|
+
expect(fab.style.backgroundColor).toBe('rgb(1, 2, 3)');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('accepts a single style object', () => {
|
|
181
|
+
const c = mount(
|
|
182
|
+
<Fab accessibilityLabel="Add" icon={<span>+</span>} style={{ marginBottom: 5 }} />,
|
|
183
|
+
);
|
|
184
|
+
expect(getByRole(c, 'button').style.marginBottom).toBe('5px');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('flattens an array labelStyle onto the extended-FAB label span', () => {
|
|
188
|
+
const c = mount(
|
|
189
|
+
<Fab
|
|
190
|
+
label="Compose"
|
|
191
|
+
icon={<span>+</span>}
|
|
192
|
+
labelStyle={[{ fontWeight: '700' }, false, { letterSpacing: 2 }]}
|
|
193
|
+
/>,
|
|
194
|
+
);
|
|
195
|
+
const label = getByText(c, 'Compose');
|
|
196
|
+
expect(label.tagName).toBe('SPAN');
|
|
197
|
+
expect(label.style.fontWeight).toBe('700');
|
|
198
|
+
expect(label.style.letterSpacing).toBe('2px');
|
|
199
|
+
// No leaked numeric style key from the array spread.
|
|
200
|
+
expect(label.getAttribute('style') ?? '').not.toMatch(/(^|;)\s*0\s*:/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
141
203
|
});
|
|
@@ -9,11 +9,10 @@ import React, {
|
|
|
9
9
|
type ReactElement,
|
|
10
10
|
} from 'react';
|
|
11
11
|
|
|
12
|
-
import type { ViewStyle } from 'react-native';
|
|
13
|
-
|
|
14
12
|
import { useTheme } from '../theme/use-theme';
|
|
15
13
|
import type { Theme } from '../theme/types';
|
|
16
14
|
import { SpinnerIcon } from '../loading/SpinnerIcon.web';
|
|
15
|
+
import { flattenWebStyle } from '../styles/flatten-web-style';
|
|
17
16
|
import type { ButtonProps, ButtonSize, ButtonVariant } from './types';
|
|
18
17
|
|
|
19
18
|
export type { ButtonProps, ButtonVariant, ButtonSize } from './types';
|
|
@@ -218,37 +217,6 @@ function resolveVariantStyle(
|
|
|
218
217
|
}
|
|
219
218
|
}
|
|
220
219
|
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
// Style normalization
|
|
223
|
-
//
|
|
224
|
-
// Callers idiomatically pass `style` as a React-Native `StyleProp` — most
|
|
225
|
-
// often an ARRAY (`style={[base, cond && override]}`, possibly nested and
|
|
226
|
-
// containing falsy holes from `cond && {...}`). The DOM `<button>` below needs
|
|
227
|
-
// a SINGLE plain object: spreading a raw array into an object literal produces
|
|
228
|
-
// numeric keys (`"0"`, `"1"`, …), which React then assigns onto the real
|
|
229
|
-
// `CSSStyleDeclaration`, throwing
|
|
230
|
-
// `Failed to set an indexed property [0] on 'CSSStyleDeclaration'`. Flatten the
|
|
231
|
-
// StyleProp into one object here, once, before it reaches either merge site.
|
|
232
|
-
// Later entries win, mirroring React-Native array-style precedence.
|
|
233
|
-
//
|
|
234
|
-
// `StyleNode` is a readonly-closed recursion type (RN's own `StyleProp` array
|
|
235
|
-
// member yields `readonly` sub-arrays that aren't assignable back to
|
|
236
|
-
// `StyleProp`, so it can't be recursed over directly). `ButtonProps['style']`
|
|
237
|
-
// is assignable to it, so call sites pass `style` with no cast.
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
type StyleNode = ViewStyle | false | null | undefined | '' | ReadonlyArray<StyleNode>;
|
|
241
|
-
|
|
242
|
-
function flattenStyle(style: StyleNode): CSSProperties {
|
|
243
|
-
if (Array.isArray(style)) {
|
|
244
|
-
return style.reduce<CSSProperties>(
|
|
245
|
-
(merged, entry) => Object.assign(merged, flattenStyle(entry)),
|
|
246
|
-
{},
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
return style && typeof style === 'object' ? (style as CSSProperties) : {};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
220
|
// ---------------------------------------------------------------------------
|
|
253
221
|
// Component
|
|
254
222
|
// ---------------------------------------------------------------------------
|
|
@@ -350,9 +318,11 @@ const ButtonWebComponent: React.FC<ButtonProps> = ({
|
|
|
350
318
|
|
|
351
319
|
const spinnerColor = loadingColor ?? variantStyle.textColor;
|
|
352
320
|
|
|
353
|
-
// Normalize the caller's `style` (single object, array, or falsy)
|
|
354
|
-
// flat plain object
|
|
355
|
-
|
|
321
|
+
// Normalize the caller's `style` (single object, StyleProp array, or falsy)
|
|
322
|
+
// into ONE flat plain object here, once, so neither raw-DOM merge site below
|
|
323
|
+
// spreads a StyleProp array (which would leak numeric keys onto the button's
|
|
324
|
+
// CSSStyleDeclaration). See `flattenWebStyle` for the full rationale.
|
|
325
|
+
const resolvedStyle = flattenWebStyle(style);
|
|
356
326
|
|
|
357
327
|
const content = (
|
|
358
328
|
<>
|
package/src/fab/Fab.web.tsx
CHANGED
|
@@ -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 { flattenWebStyle } from '../styles/flatten-web-style';
|
|
13
14
|
import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
|
|
14
15
|
|
|
15
16
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './types';
|
|
@@ -261,12 +262,20 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
261
262
|
const ariaLabel = ariaLabelProp ?? accessibilityLabel ?? label;
|
|
262
263
|
const composedClassName = ['bloom-fab'].concat(className ? [className] : []).join(' ');
|
|
263
264
|
|
|
265
|
+
// Normalize `style`/`labelStyle` (single object, StyleProp array — the native
|
|
266
|
+
// fork passes `style={[placementStyle(...), {...}, style]}` — or falsy) into
|
|
267
|
+
// ONE flat plain object each, so neither raw-DOM merge site below spreads a
|
|
268
|
+
// StyleProp array (which would leak numeric keys onto the element's
|
|
269
|
+
// CSSStyleDeclaration). See `flattenWebStyle` for the full rationale.
|
|
270
|
+
const resolvedStyle = flattenWebStyle(style);
|
|
271
|
+
const resolvedLabelStyle = flattenWebStyle(labelStyle);
|
|
272
|
+
|
|
264
273
|
return (
|
|
265
274
|
<button
|
|
266
275
|
id={resolvedId}
|
|
267
276
|
type={type}
|
|
268
277
|
className={composedClassName}
|
|
269
|
-
style={{ ...containerStyle, ...
|
|
278
|
+
style={{ ...containerStyle, ...resolvedStyle }}
|
|
270
279
|
onClick={handleClick}
|
|
271
280
|
disabled={disabled}
|
|
272
281
|
aria-disabled={disabled || undefined}
|
|
@@ -288,7 +297,7 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
288
297
|
{content}
|
|
289
298
|
</span>
|
|
290
299
|
)}
|
|
291
|
-
{isExtended && <span style={
|
|
300
|
+
{isExtended && <span style={resolvedLabelStyle}>{label}</span>}
|
|
292
301
|
</button>
|
|
293
302
|
);
|
|
294
303
|
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { useHaptics, BloomHapticsProvider } from './useHaptics';
|
|
|
7
7
|
export type { HapticStrength, BloomHapticsProviderProps } from './useHaptics';
|
|
8
8
|
export { useGutters } from './useGutters';
|
|
9
9
|
export type { Gutter, Gutters } from './useGutters';
|
|
10
|
+
export { useImagePreload, preloadImage } from './useImagePreload';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { Image } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to preload images for better perceived performance
|
|
6
|
+
* Preloads images when they're likely to be viewed soon
|
|
7
|
+
*/
|
|
8
|
+
export function useImagePreload(urls: (string | undefined)[], enabled: boolean = true) {
|
|
9
|
+
const preloadedRef = useRef<Set<string>>(new Set());
|
|
10
|
+
|
|
11
|
+
// Memoize valid URLs to avoid unnecessary re-runs
|
|
12
|
+
const validUrls = useMemo(() => {
|
|
13
|
+
return urls.filter((url): url is string =>
|
|
14
|
+
Boolean(url && (url.startsWith('http://') || url.startsWith('https://')))
|
|
15
|
+
);
|
|
16
|
+
}, [urls]);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled || !validUrls.length) return;
|
|
20
|
+
|
|
21
|
+
validUrls.forEach((url) => {
|
|
22
|
+
if (preloadedRef.current.has(url)) return;
|
|
23
|
+
|
|
24
|
+
preloadedRef.current.add(url);
|
|
25
|
+
Image.prefetch(url).catch(() => {
|
|
26
|
+
// Silently handle prefetch errors - remove from set on error to allow retry
|
|
27
|
+
preloadedRef.current.delete(url);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}, [validUrls, enabled]);
|
|
31
|
+
|
|
32
|
+
// Limit cache size to prevent memory issues (keep last 1000)
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (preloadedRef.current.size > 1000) {
|
|
35
|
+
const entries = Array.from(preloadedRef.current);
|
|
36
|
+
const toRemove = entries.slice(0, entries.length - 1000);
|
|
37
|
+
toRemove.forEach(url => preloadedRef.current.delete(url));
|
|
38
|
+
}
|
|
39
|
+
}, [validUrls]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Preload a single image
|
|
44
|
+
*/
|
|
45
|
+
export function preloadImage(url: string | undefined): void {
|
|
46
|
+
if (!url || !url.startsWith('http')) return;
|
|
47
|
+
|
|
48
|
+
Image.prefetch(url).catch(() => {
|
|
49
|
+
// Silently handle prefetch errors
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared module-scoped cache of image aspect ratios (width / height), keyed by
|
|
3
|
+
* the image source URI.
|
|
4
|
+
*
|
|
5
|
+
* Intended for a thumbnail renderer and a fullscreen viewer (e.g.
|
|
6
|
+
* `@oxyhq/bloom/zoomable-image-gallery`) sharing the SAME image URI to read
|
|
7
|
+
* from and write to this one map, so an image already resolved as a thumbnail
|
|
8
|
+
* has its ratio on hand when it opens "big" — no second cache, no duplicate
|
|
9
|
+
* `Image.getSize` round-trip on the hot path. If the two surfaces render
|
|
10
|
+
* different URI variants of the same image (e.g. a smaller thumbnail variant
|
|
11
|
+
* vs. a larger lightbox variant), pass the known ratio through explicitly
|
|
12
|
+
* (`GalleryImage.aspectRatio`) instead of relying on cache-key reuse.
|
|
13
|
+
*/
|
|
14
|
+
import { Image } from 'react-native';
|
|
15
|
+
|
|
16
|
+
/** Fallback ratio used when a remote image's intrinsic size cannot be read. */
|
|
17
|
+
export const DEFAULT_ASPECT_RATIO = 4 / 3;
|
|
18
|
+
|
|
19
|
+
const aspectRatioCache = new Map<string, number>();
|
|
20
|
+
|
|
21
|
+
export const getAspectRatio = (uri: string): number | undefined => aspectRatioCache.get(uri);
|
|
22
|
+
|
|
23
|
+
export const hasAspectRatio = (uri: string): boolean => aspectRatioCache.has(uri);
|
|
24
|
+
|
|
25
|
+
export const setAspectRatio = (uri: string, ratio: number): void => {
|
|
26
|
+
if (uri && ratio > 0 && Number.isFinite(ratio)) {
|
|
27
|
+
aspectRatioCache.set(uri, ratio);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the aspect ratio for `uri`, returning the cached value immediately on
|
|
33
|
+
* a hit and otherwise fetching the intrinsic size via `Image.getSize`, writing
|
|
34
|
+
* the result back into the shared cache. On failure the shared
|
|
35
|
+
* `DEFAULT_ASPECT_RATIO` is cached and returned so callers never deadlock on a
|
|
36
|
+
* broken image.
|
|
37
|
+
*/
|
|
38
|
+
export const fetchAspectRatio = (uri: string): Promise<number> => {
|
|
39
|
+
const cached = aspectRatioCache.get(uri);
|
|
40
|
+
if (cached !== undefined) {
|
|
41
|
+
return Promise.resolve(cached);
|
|
42
|
+
}
|
|
43
|
+
return new Promise<number>((resolve) => {
|
|
44
|
+
Image.getSize(
|
|
45
|
+
uri,
|
|
46
|
+
(width, height) => {
|
|
47
|
+
if (width > 0 && height > 0) {
|
|
48
|
+
const ratio = width / height;
|
|
49
|
+
aspectRatioCache.set(uri, ratio);
|
|
50
|
+
resolve(ratio);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
aspectRatioCache.set(uri, DEFAULT_ASPECT_RATIO);
|
|
54
|
+
resolve(DEFAULT_ASPECT_RATIO);
|
|
55
|
+
},
|
|
56
|
+
() => {
|
|
57
|
+
aspectRatioCache.set(uri, DEFAULT_ASPECT_RATIO);
|
|
58
|
+
resolve(DEFAULT_ASPECT_RATIO);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export { useHaptics, BloomHapticsProvider } from './hooks/useHaptics';
|
|
|
44
44
|
export type { HapticStrength, BloomHapticsProviderProps } from './hooks/useHaptics';
|
|
45
45
|
export { useGutters } from './hooks/useGutters';
|
|
46
46
|
export type { Gutter, Gutters } from './hooks/useGutters';
|
|
47
|
+
export { useImagePreload, preloadImage } from './hooks/useImagePreload';
|
|
47
48
|
|
|
48
49
|
// Icons
|
|
49
50
|
export * as Icons from './icons';
|
package/src/index.web.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { useHaptics, BloomHapticsProvider } from './hooks/useHaptics';
|
|
|
49
49
|
export type { HapticStrength, BloomHapticsProviderProps } from './hooks/useHaptics';
|
|
50
50
|
export { useGutters } from './hooks/useGutters';
|
|
51
51
|
export type { Gutter, Gutters } from './hooks/useGutters';
|
|
52
|
+
export { useImagePreload, preloadImage } from './hooks/useImagePreload';
|
|
52
53
|
|
|
53
54
|
// Icons
|
|
54
55
|
export * as Icons from './icons';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// flattenWebStyle — StyleProp → plain CSSProperties, for raw-DOM web forks.
|
|
6
|
+
//
|
|
7
|
+
// A handful of Bloom's `.web.tsx` forks (`Button.web.tsx`, `Fab.web.tsx`)
|
|
8
|
+
// render REAL HTML elements through react-dom (`<button>`, `<span>`), NOT
|
|
9
|
+
// react-native-web components. Their `style` prop is therefore a single plain
|
|
10
|
+
// `CSSProperties` object that React commits straight to a live
|
|
11
|
+
// `CSSStyleDeclaration`.
|
|
12
|
+
//
|
|
13
|
+
// Callers, however, pass `style` as a React-Native `StyleProp` — most often an
|
|
14
|
+
// ARRAY (`style={[base, cond && override]}`, possibly nested, with falsy holes
|
|
15
|
+
// from `cond && {...}`). Spreading a raw array into an object literal produces
|
|
16
|
+
// numeric keys ("0", "1", …), which React then assigns onto the DOM node's
|
|
17
|
+
// `CSSStyleDeclaration`, throwing:
|
|
18
|
+
// `Failed to set an indexed property [0] on 'CSSStyleDeclaration'`.
|
|
19
|
+
//
|
|
20
|
+
// Why NOT `StyleSheet.flatten` (the convention the RNW-component forks such as
|
|
21
|
+
// `list/index.web.tsx` use): these raw-DOM forks have NO other react-native
|
|
22
|
+
// runtime dependency. Reaching for `StyleSheet.flatten` would couple them to
|
|
23
|
+
// the `react-native` → `react-native-web` bundler alias they otherwise never
|
|
24
|
+
// need — a coupling the jsdom test environment does not provide (its
|
|
25
|
+
// `react-native` mock stubs `flatten` as an identity no-op, so an array would
|
|
26
|
+
// pass through unflattened and the raw-DOM render would crash under test). A
|
|
27
|
+
// dependency-free flatten is provably correct in every bundling/test context,
|
|
28
|
+
// so it is the right convention for THIS sub-class of forks specifically.
|
|
29
|
+
//
|
|
30
|
+
// `StyleNode` is a readonly-closed recursion type: RN's own `StyleProp` array
|
|
31
|
+
// members are `readonly` and its `RegisteredStyle` members are branded numbers,
|
|
32
|
+
// neither of which is assignable back to `StyleProp` for direct recursion.
|
|
33
|
+
// `StyleProp<ViewStyle>` / `StyleProp<TextStyle>` ARE assignable to it, so call
|
|
34
|
+
// sites pass `style` / `labelStyle` with no cast. Later entries win, mirroring
|
|
35
|
+
// React-Native array-style precedence; falsy holes and unresolved registered-
|
|
36
|
+
// style ids (bare numbers — not resolvable without the RN runtime, and never
|
|
37
|
+
// passed to a raw-DOM fork in practice) are skipped.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
type StyleNode =
|
|
41
|
+
| ViewStyle
|
|
42
|
+
| TextStyle
|
|
43
|
+
| number
|
|
44
|
+
| false
|
|
45
|
+
| null
|
|
46
|
+
| undefined
|
|
47
|
+
| ''
|
|
48
|
+
| ReadonlyArray<StyleNode>;
|
|
49
|
+
|
|
50
|
+
export function flattenWebStyle(style: StyleNode): CSSProperties {
|
|
51
|
+
if (Array.isArray(style)) {
|
|
52
|
+
return style.reduce<CSSProperties>(
|
|
53
|
+
(merged, entry) => Object.assign(merged, flattenWebStyle(entry)),
|
|
54
|
+
{},
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return style && typeof style === 'object' ? (style as CSSProperties) : {};
|
|
58
|
+
}
|