zkit-ui 2.0.5 → 2.0.7

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 (80) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryLayoutSupport.java → NativeImagePreviewLayoutSupport.java} +12 -12
  3. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryMediaItem.java → NativeImagePreviewMediaItem.java} +2 -2
  4. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
  5. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryPagerAdapter.java → NativeImagePreviewPagerAdapter.java} +18 -18
  6. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GallerySharedElementGeometry.java → NativeImagePreviewSharedElementGeometry.java} +2 -2
  7. package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
  8. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GallerySharedElementState.java → NativeImagePreviewSharedElementState.java} +4 -4
  9. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GallerySourceViewRegistry.java → NativeImagePreviewSourceViewRegistry.java} +2 -2
  10. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryTransitionSnapshotView.java → NativeImagePreviewTransitionSnapshotView.java} +3 -3
  11. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryVideoPlayerView.java → NativeImagePreviewVideoPlayerView.java} +7 -7
  12. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GaleriaView.kt → NativeImagePreviewView.kt} +18 -18
  13. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryViewerOverlayView.java → NativeImagePreviewViewerOverlayView.java} +49 -49
  14. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryViewerPageView.java → NativeImagePreviewViewerPageView.java} +27 -27
  15. package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryViewerTransitionController.java → NativeImagePreviewViewerTransitionController.java} +22 -22
  16. package/dist/ui/Button/index.d.ts +1 -1
  17. package/dist/ui/Button/index.d.ts.map +1 -1
  18. package/dist/ui/Button/index.js +18 -3
  19. package/dist/ui/NativeImagePreview/NativeImagePreview.js +6 -6
  20. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +1 -0
  21. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -1
  22. package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +105 -11
  23. package/dist/ui/NativeImagePreview/context.d.ts +1 -1
  24. package/dist/ui/NativeImagePreview/context.d.ts.map +1 -1
  25. package/dist/ui/NativeImagePreview/types.d.ts +3 -1
  26. package/dist/ui/NativeImagePreview/types.d.ts.map +1 -1
  27. package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -1
  28. package/dist/ui/NativeImagePreview/utils.js +1 -0
  29. package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
  30. package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
  31. package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
  32. package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
  33. package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
  34. package/dist/ui/NativeImagePreview/web/media.js +90 -0
  35. package/dist/ui/NativeImagePreview/web/styles.css +137 -0
  36. package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
  37. package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
  38. package/dist/ui/NativeImagePreview/web/transition.js +77 -0
  39. package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
  40. package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
  41. package/dist/ui/NativeImagePreview/web/types.js +3 -0
  42. package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
  43. package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
  44. package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
  45. package/expo-module.config.json +2 -2
  46. package/ios/{GalleryViewer/GaleriaImageLoader.swift → NativeImagePreview/NativeImagePreviewImageLoader.swift} +7 -7
  47. package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
  48. package/ios/{GalleryViewer/GaleriaLayoutSupport.swift → NativeImagePreview/NativeImagePreviewLayoutSupport.swift} +19 -19
  49. package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
  50. package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
  51. package/ios/{GalleryViewer/GaleriaVideoPlayerView.swift → NativeImagePreview/NativeImagePreviewVideoPlayerView.swift} +1 -1
  52. package/ios/{GalleryViewer/GaleriaViewerConfiguration.swift → NativeImagePreview/NativeImagePreviewViewerConfiguration.swift} +7 -7
  53. package/ios/{GalleryViewer/GaleriaViewerController.swift → NativeImagePreview/NativeImagePreviewViewerController.swift} +36 -36
  54. package/ios/{GalleryViewer/GaleriaViewerPageView.swift → NativeImagePreview/NativeImagePreviewViewerPageView.swift} +33 -33
  55. package/ios/{GalleryViewer/GaleriaViewerTransitionCoordinator.swift → NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift} +16 -16
  56. package/ios/{GaleriaModule.swift → NativeImagePreviewModule.swift} +4 -4
  57. package/ios/{GaleriaView.swift → NativeImagePreviewView.swift} +26 -27
  58. package/package.json +18 -12
  59. package/src/services/ImagePreviewService/README.md +1 -1
  60. package/src/types.d.ts +1 -0
  61. package/src/ui/Button/README.md +1 -1
  62. package/src/ui/Button/index.tsx +27 -7
  63. package/src/ui/NativeImagePreview/NativeImagePreview.tsx +7 -7
  64. package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +175 -11
  65. package/src/ui/NativeImagePreview/README.md +3 -3
  66. package/src/ui/NativeImagePreview/context.tsx +1 -1
  67. package/src/ui/NativeImagePreview/types.ts +3 -1
  68. package/src/ui/NativeImagePreview/utils.ts +1 -0
  69. package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
  70. package/src/ui/NativeImagePreview/web/media.ts +91 -0
  71. package/src/ui/NativeImagePreview/web/styles.css +137 -0
  72. package/src/ui/NativeImagePreview/web/transition.ts +111 -0
  73. package/src/ui/NativeImagePreview/web/types.ts +88 -0
  74. package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
  75. package/android/src/main/java/cn/fontree/zkit/ui/preview/GaleriaModule.kt +0 -43
  76. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementNames.java +0 -39
  77. package/ios/GalleryViewer/GaleriaImageViewInteraction.swift +0 -245
  78. package/ios/GalleryViewer/GaleriaMedia.swift +0 -28
  79. package/ios/GalleryViewer/GaleriaSourceViewRegistry.swift +0 -80
  80. /package/android/src/main/res/layout/{galeria_player_view.xml → native_image_preview_player_view.xml} +0 -0
@@ -1,10 +1,10 @@
1
1
  import ExpoModulesCore
2
2
  import UIKit
3
3
 
4
- class GaleriaView: ExpoView {
4
+ class NativeImagePreviewView: ExpoView {
5
5
  private var childImageView: UIImageView?
6
6
  private weak var registeredAnchorImageView: UIImageView?
7
- private var registeredGalleryId: String?
7
+ private var registeredPreviewGroupId: String?
8
8
  private var registeredIndex: Int?
9
9
 
10
10
  deinit {
@@ -57,11 +57,10 @@ class GaleriaView: ExpoView {
57
57
  #endif
58
58
 
59
59
  #if RCT_NEW_ARCH_ENABLED
60
- // https://github.com/nandorojo/galeria/issues/19
61
- // Cleanup gesture recognizers from the image view to work with fabric view recycling
60
+ // Cleanup gesture recognizers from the image view to work with Fabric view recycling.
62
61
  override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
63
62
  unregisterCurrentAnchor()
64
- childImageView?.removeGaleriaViewerInteraction()
63
+ childImageView?.removeNativeImagePreviewViewerInteraction()
65
64
  childImageView = nil
66
65
  super.unmountChildComponentView(childComponentView, index: index)
67
66
  }
@@ -71,7 +70,7 @@ class GaleriaView: ExpoView {
71
70
  var urls: [String]?
72
71
  var items: [[String: Any]]?
73
72
  var initialIndex: Int?
74
- var galleryId: String?
73
+ var previewGroupId: String?
75
74
  var closeIconName: String?
76
75
  var rightNavItemIconName: String?
77
76
  let onPressRightNavItemIcon = EventDispatcher()
@@ -99,7 +98,7 @@ class GaleriaView: ExpoView {
99
98
  ) {
100
99
  let configuration = buildViewerConfiguration()
101
100
 
102
- let imageItems: [GaleriaMediaItem] = items.compactMap { item in
101
+ let imageItems: [NativeImagePreviewMediaItem] = items.compactMap { item in
103
102
  guard let type = item["type"] as? String else { return nil }
104
103
 
105
104
  if type == "video", let urlStr = item["url"] as? String, let url = URL(string: urlStr) {
@@ -107,25 +106,25 @@ class GaleriaView: ExpoView {
107
106
  if let posterStr = item["poster"] as? String {
108
107
  posterUrl = URL(string: posterStr)
109
108
  }
110
- return GaleriaMediaItem.video(url: url, poster: posterUrl)
109
+ return NativeImagePreviewMediaItem.video(url: url, poster: posterUrl)
111
110
  } else if type == "image", let urlStr = item["url"] as? String {
112
111
  if urlStr.hasPrefix("http://") || urlStr.hasPrefix("https://") || urlStr.hasPrefix("file://") {
113
112
  if let url = URL(string: urlStr) {
114
- return GaleriaMediaItem.imageURL(url, placeholder: nil)
113
+ return NativeImagePreviewMediaItem.imageURL(url, placeholder: nil)
115
114
  }
116
115
  } else {
117
- return GaleriaMediaItem.imageURL(URL(fileURLWithPath: urlStr), placeholder: nil)
116
+ return NativeImagePreviewMediaItem.imageURL(URL(fileURLWithPath: urlStr), placeholder: nil)
118
117
  }
119
118
  }
120
119
  return nil
121
120
  }
122
121
 
123
- let datasource = GaleriaArrayDataSource(items: imageItems)
124
- childImage.setupGaleriaViewer(
122
+ let datasource = NativeImagePreviewArrayDataSource(items: imageItems)
123
+ childImage.setupNativeImagePreviewViewer(
125
124
  dataSource: datasource,
126
125
  initialIndex: initialIndex,
127
126
  configuration: configuration,
128
- galleryId: galleryId)
127
+ previewGroupId: previewGroupId)
129
128
  }
130
129
 
131
130
 
@@ -143,11 +142,11 @@ class GaleriaView: ExpoView {
143
142
  return URL(fileURLWithPath: string)
144
143
  }
145
144
 
146
- childImage.setupGaleriaViewer(
145
+ childImage.setupNativeImagePreviewViewer(
147
146
  urls: urlObjects,
148
147
  initialIndex: initialIndex,
149
148
  configuration: configuration,
150
- galleryId: galleryId)
149
+ previewGroupId: previewGroupId)
151
150
  }
152
151
 
153
152
  private func setupViewerWithSingleImage(_ childImage: UIImageView) {
@@ -157,11 +156,11 @@ class GaleriaView: ExpoView {
157
156
  }
158
157
  let configuration = buildViewerConfiguration()
159
158
 
160
- childImage.setupGaleriaViewer(images: [img], configuration: configuration, galleryId: galleryId)
159
+ childImage.setupNativeImagePreviewViewer(images: [img], configuration: configuration, previewGroupId: previewGroupId)
161
160
  }
162
161
 
163
- private func buildViewerConfiguration() -> GaleriaViewerConfiguration {
164
- var configuration = GaleriaViewerConfiguration(theme: theme.toViewerTheme())
162
+ private func buildViewerConfiguration() -> NativeImagePreviewViewerConfiguration {
163
+ var configuration = NativeImagePreviewViewerConfiguration(theme: theme.toViewerTheme())
165
164
  let iconColor = theme.iconColor()
166
165
 
167
166
  if let closeIconName = closeIconName,
@@ -191,7 +190,7 @@ class GaleriaView: ExpoView {
191
190
  private func registerCurrentAnchorIfNeeded() {
192
191
  guard
193
192
  window != nil,
194
- let galleryId,
193
+ let previewGroupId,
195
194
  let initialIndex,
196
195
  let childImage = getChildImageView()
197
196
  else {
@@ -202,30 +201,30 @@ class GaleriaView: ExpoView {
202
201
  if
203
202
  let registeredAnchorImageView,
204
203
  (registeredAnchorImageView !== childImage
205
- || registeredGalleryId != galleryId
204
+ || registeredPreviewGroupId != previewGroupId
206
205
  || registeredIndex != initialIndex)
207
206
  {
208
207
  unregisterCurrentAnchor()
209
208
  }
210
209
 
211
- GaleriaSourceViewRegistry.shared.register(
210
+ NativeImagePreviewSourceViewRegistry.shared.register(
212
211
  childImage,
213
- galleryId: galleryId,
212
+ previewGroupId: previewGroupId,
214
213
  index: initialIndex)
215
214
  registeredAnchorImageView = childImage
216
- registeredGalleryId = galleryId
215
+ registeredPreviewGroupId = previewGroupId
217
216
  registeredIndex = initialIndex
218
217
  }
219
218
 
220
219
  private func unregisterCurrentAnchor() {
221
220
  if let registeredAnchorImageView = registeredAnchorImageView {
222
- GaleriaSourceViewRegistry.shared.unregister(
221
+ NativeImagePreviewSourceViewRegistry.shared.unregister(
223
222
  registeredAnchorImageView,
224
- galleryId: registeredGalleryId,
223
+ previewGroupId: registeredPreviewGroupId,
225
224
  index: registeredIndex)
226
225
  }
227
226
  registeredAnchorImageView = nil
228
- registeredGalleryId = nil
227
+ registeredPreviewGroupId = nil
229
228
  registeredIndex = nil
230
229
  }
231
230
  }
@@ -234,7 +233,7 @@ enum Theme: String, Enumerable {
234
233
  case dark
235
234
  case light
236
235
 
237
- func toViewerTheme() -> GaleriaViewerTheme {
236
+ func toViewerTheme() -> NativeImagePreviewViewerTheme {
238
237
  switch self {
239
238
  case .dark:
240
239
  return .dark
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zkit-ui",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -170,7 +170,10 @@
170
170
  "./assets/*": "./dist/assets/*",
171
171
  "./package.json": "./package.json"
172
172
  },
173
- "sideEffects": false,
173
+ "sideEffects": [
174
+ "*.css",
175
+ "**/*.css"
176
+ ],
174
177
  "repository": {
175
178
  "type": "git",
176
179
  "url": "git+https://github.com/Concur-max/zkit.git",
@@ -181,7 +184,8 @@
181
184
  "url": "https://github.com/Concur-max/zkit/issues"
182
185
  },
183
186
  "files": [
184
- "android",
187
+ "android/build.gradle",
188
+ "android/src",
185
189
  "dist",
186
190
  "expo-module.config.json",
187
191
  "src",
@@ -193,6 +197,14 @@
193
197
  "publishConfig": {
194
198
  "registry": "https://registry.npmjs.org/"
195
199
  },
200
+ "scripts": {
201
+ "build": "node scripts/build.cjs",
202
+ "verify:entrypoints": "node scripts/verify-entrypoints.cjs",
203
+ "verify": "pnpm run build && pnpm run verify:entrypoints",
204
+ "typecheck": "tsc -p tsconfig.json --noEmit",
205
+ "prepack": "pnpm run build",
206
+ "prepublishOnly": "pnpm run build"
207
+ },
196
208
  "dependencies": {
197
209
  "@vant/area-data": "^2.1.0",
198
210
  "dayjs": "^1.11.13",
@@ -219,6 +231,7 @@
219
231
  },
220
232
  "devDependencies": {
221
233
  "@expo/vector-icons": "^15.1.1",
234
+ "@lodev09/react-native-true-sheet": "workspace:*",
222
235
  "@shopify/flash-list": "^2.3.1",
223
236
  "@types/react": "~19.1.17",
224
237
  "dayjs": "^1.11.13",
@@ -235,13 +248,6 @@
235
248
  "react-native-svg": "^15.12.1",
236
249
  "react-native-worklets": "0.5.1",
237
250
  "sonner-native": "^0.22.2",
238
- "typescript": "~5.9.3",
239
- "@lodev09/react-native-true-sheet": "3.10.0"
240
- },
241
- "scripts": {
242
- "build": "node scripts/build.cjs",
243
- "verify:entrypoints": "node scripts/verify-entrypoints.cjs",
244
- "verify": "pnpm run build && pnpm run verify:entrypoints",
245
- "typecheck": "tsc -p tsconfig.json --noEmit"
251
+ "typescript": "~5.9.3"
246
252
  }
247
- }
253
+ }
@@ -77,7 +77,7 @@ import { NativeImagePreview } from 'zkit-ui/native-image-preview';
77
77
  两者不要混用成同一个本地封装:
78
78
 
79
79
  - `ImagePreview`:普通全屏预览,支持声明式 `open/value` 和命令式 `imagePreview.open()`。
80
- - `NativeImagePreview`:原生共享转场预览,需要包住真实缩略图节点,iOS / Android 走原生 viewer。
80
+ - `NativeImagePreview`:共享转场预览,需要包住真实缩略图节点,iOS / Android 走原生 viewer,Web 走 H5 viewer
81
81
 
82
82
  ## 手动确认点
83
83
 
package/src/types.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '*.css';
@@ -186,5 +186,5 @@ export function Demo() {
186
186
 
187
187
  - `disabled` 会禁用交互并进入禁用视觉;`loading` 只禁用交互,不强制置灰
188
188
  - 显式传入 `hitSlop` 时以调用侧为准;否则小尺寸和 `link` 会自动补足触控热区
189
- - `accessibilityRole` 固定为 `button`
189
+ - Native 端 `accessibilityRole` 固定为 `button`;Web 端根节点不设置 `button` role,避免 RN Web 将 Button 渲染为真实 `<button>` 后产生嵌套按钮问题
190
190
  - `iconOnly` 必须提供可读的 `accessibilityLabel`,除非 children 是可作为标签的文本
@@ -108,7 +108,7 @@ export type ButtonColors = {
108
108
 
109
109
  type NativePressableProps = Omit<
110
110
  React.ComponentPropsWithoutRef<typeof Pressable>,
111
- 'accessibilityRole' | 'accessibilityState' | 'children' | 'disabled' | 'style'
111
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'disabled' | 'role' | 'style'
112
112
  >;
113
113
 
114
114
  export type ButtonRef = React.ComponentRef<typeof Pressable>;
@@ -839,6 +839,20 @@ function resolveWebCursorStyle(disabled: boolean): ViewStyle | undefined {
839
839
  return { cursor: disabled ? 'not-allowed' : 'pointer' } as ViewStyle;
840
840
  }
841
841
 
842
+ function resolveRootAccessibilityRole() {
843
+ return Platform.OS === 'web' ? undefined : 'button';
844
+ }
845
+
846
+ function resolveRootPressableProps<T extends object>(props: T): T {
847
+ if (Platform.OS !== 'web') return props;
848
+
849
+ const { accessibilityRole: _accessibilityRole, role: _role, ...rest } = props as T & {
850
+ accessibilityRole?: unknown;
851
+ role?: unknown;
852
+ };
853
+ return rest as T;
854
+ }
855
+
842
856
  function requiresAnimatedPressEffect(effect: ButtonPressEffect | undefined) {
843
857
  return (
844
858
  effect === 'scale' ||
@@ -1161,6 +1175,8 @@ function ButtonStaticImpl(
1161
1175
  },
1162
1176
  [interactionDisabled, onPress]
1163
1177
  );
1178
+ const rootAccessibilityRole = resolveRootAccessibilityRole();
1179
+ const rootPressableProps = resolveRootPressableProps(pressableProps);
1164
1180
  const pressOpacityEnabled = !interactionDisabled && (pressEffect === 'auto' || pressEffect === 'opacity');
1165
1181
  const rootStyle = React.useCallback(
1166
1182
  ({ pressed }: PressableStateCallbackType) => [
@@ -1186,10 +1202,10 @@ function ButtonStaticImpl(
1186
1202
 
1187
1203
  return (
1188
1204
  <Pressable
1189
- {...pressableProps}
1205
+ {...rootPressableProps}
1190
1206
  ref={ref}
1191
1207
  accessibilityLabel={resolvedAccessibilityLabel}
1192
- accessibilityRole="button"
1208
+ accessibilityRole={rootAccessibilityRole}
1193
1209
  accessibilityState={{
1194
1210
  ...accessibilityState,
1195
1211
  busy: Boolean(loading || accessibilityState?.busy),
@@ -1398,13 +1414,15 @@ function ButtonPressAnimatedImpl(
1398
1414
  },
1399
1415
  [interactionDisabled, onPressOut, pressSv, resolvedPressEffect]
1400
1416
  );
1417
+ const rootAccessibilityRole = resolveRootAccessibilityRole();
1418
+ const rootPressableProps = resolveRootPressableProps(pressableProps);
1401
1419
 
1402
1420
  return (
1403
1421
  <AnimatedPressable
1404
- {...pressableProps}
1422
+ {...rootPressableProps}
1405
1423
  ref={ref}
1406
1424
  accessibilityLabel={resolvedAccessibilityLabel}
1407
- accessibilityRole="button"
1425
+ accessibilityRole={rootAccessibilityRole}
1408
1426
  accessibilityState={{
1409
1427
  ...accessibilityState,
1410
1428
  busy: Boolean(loading || accessibilityState?.busy),
@@ -1736,13 +1754,15 @@ function ButtonAnimatedImpl(
1736
1754
  },
1737
1755
  [interactionDisabled, onPressOut, pressSv, resolvedPressEffect]
1738
1756
  );
1757
+ const rootAccessibilityRole = resolveRootAccessibilityRole();
1758
+ const rootPressableProps = resolveRootPressableProps(pressableProps);
1739
1759
 
1740
1760
  return (
1741
1761
  <AnimatedPressable
1742
- {...pressableProps}
1762
+ {...rootPressableProps}
1743
1763
  ref={ref}
1744
1764
  accessibilityLabel={resolvedAccessibilityLabel}
1745
- accessibilityRole="button"
1765
+ accessibilityRole={rootAccessibilityRole}
1746
1766
  accessibilityState={{
1747
1767
  ...accessibilityState,
1748
1768
  busy: Boolean(loading || accessibilityState?.busy),
@@ -25,7 +25,7 @@ type NativePreviewViewProps = {
25
25
  children: React.ReactElement;
26
26
  index: number;
27
27
  style?: StyleProp<ViewStyle>;
28
- galleryId: string;
28
+ previewGroupId: string;
29
29
  items: readonly NativeImagePreviewNativeItem[];
30
30
  theme: 'dark' | 'light';
31
31
  edgeToEdge: boolean;
@@ -39,7 +39,7 @@ const NativePreviewView = requireNativeView<NativePreviewViewProps>(
39
39
 
40
40
  const ANDROID_EDGE_TO_EDGE = Platform.OS === 'android' ? isEdgeToEdge() : false;
41
41
 
42
- function createGalleryId() {
42
+ function createPreviewGroupId() {
43
43
  return `zkit-native-image-preview-${Date.now().toString(36)}-${Math.random()
44
44
  .toString(36)
45
45
  .slice(2, 10)}`;
@@ -55,9 +55,9 @@ const NativeImagePreviewRoot = React.memo(function NativeImagePreview({
55
55
  onChange,
56
56
  }: NativeImagePreviewProps) {
57
57
  const systemColorScheme = useColorScheme();
58
- const galleryIdRef = React.useRef<string | null>(null);
59
- if (galleryIdRef.current == null) {
60
- galleryIdRef.current = createGalleryId();
58
+ const previewGroupIdRef = React.useRef<string | null>(null);
59
+ if (previewGroupIdRef.current == null) {
60
+ previewGroupIdRef.current = createPreviewGroupId();
61
61
  }
62
62
 
63
63
  const resolvedItems = React.useMemo(
@@ -71,7 +71,7 @@ const NativeImagePreviewRoot = React.memo(function NativeImagePreview({
71
71
  const resolvedColorScheme = resolveNativeImagePreviewColorScheme(colorScheme, systemColorScheme);
72
72
  const contextValue = React.useMemo(
73
73
  () => ({
74
- galleryId: galleryIdRef.current!,
74
+ previewGroupId: previewGroupIdRef.current!,
75
75
  items: resolvedItems,
76
76
  nativeItems,
77
77
  colorScheme: resolvedColorScheme,
@@ -129,7 +129,7 @@ const NativeImagePreviewItem = React.memo(function NativeImagePreviewItem({
129
129
  <NativePreviewView
130
130
  closeIconName={context.nativeProps?.iosCloseIconName}
131
131
  edgeToEdge={resolvedEdgeToEdge}
132
- galleryId={context.galleryId}
132
+ previewGroupId={context.previewGroupId}
133
133
  index={clampPreviewIndex(index, context.nativeItems.length)}
134
134
  items={context.nativeItems}
135
135
  onIndexChange={handleIndexChange}
@@ -1,35 +1,199 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import './web/styles.css';
4
+
1
5
  import * as React from 'react';
2
- import { View } from 'react-native';
6
+ import { View, useColorScheme } from 'react-native';
7
+ import { clampPreviewIndex, normalizeNativeImagePreviewItems, resolveNativeImagePreviewColorScheme } from './utils';
3
8
  import type {
9
+ NativeImagePreviewChangeMeta,
4
10
  NativeImagePreviewComponent,
5
11
  NativeImagePreviewItemProps,
6
12
  NativeImagePreviewProps,
13
+ NativeImagePreviewResolvedItem,
7
14
  } from './types';
15
+ import { createNativeImagePreviewWeb } from './web/viewer';
16
+ import type {
17
+ NativeImagePreviewWebChangeMeta,
18
+ NativeImagePreviewWebController,
19
+ NativeImagePreviewWebMediaItem,
20
+ } from './web/types';
8
21
 
9
- let didWarnUnsupportedWeb = false;
22
+ type SourceElement = HTMLElement | null;
10
23
 
11
- function warnUnsupportedWeb() {
12
- if (!__DEV__ || didWarnUnsupportedWeb) return;
24
+ type NativeImagePreviewWebContextValue = {
25
+ disabled: boolean;
26
+ items: readonly NativeImagePreviewResolvedItem[];
27
+ open: (
28
+ index: number,
29
+ onItemChange?: (value: number, meta: NativeImagePreviewChangeMeta) => void
30
+ ) => void;
31
+ sourceElementsRef: { current: SourceElement[] };
32
+ };
13
33
 
14
- didWarnUnsupportedWeb = true;
15
- console.warn(
16
- '[zkit-ui][NativeImagePreview] Native shared-transition preview is only available in iOS and Android native builds.'
17
- );
34
+ const NativeImagePreviewWebContext =
35
+ React.createContext<NativeImagePreviewWebContextValue | null>(null);
36
+
37
+ const WebView = View as unknown as React.ElementType;
38
+
39
+ function toWebPreviewItems(
40
+ items: readonly NativeImagePreviewResolvedItem[]
41
+ ): NativeImagePreviewWebMediaItem[] {
42
+ return items.map(({ alt, height, id, poster, type, url, width }) => ({
43
+ alt: alt ?? id,
44
+ height,
45
+ id,
46
+ poster,
47
+ type,
48
+ url,
49
+ width,
50
+ }));
51
+ }
52
+
53
+ function findPreviewSourceElement(element: SourceElement): SourceElement {
54
+ if (!element) return null;
55
+ if (element instanceof HTMLImageElement || element instanceof HTMLVideoElement) {
56
+ return element;
57
+ }
58
+ return element.querySelector('img, video') ?? element;
59
+ }
60
+
61
+ function toChangeMeta(
62
+ meta: NativeImagePreviewWebChangeMeta,
63
+ items: readonly NativeImagePreviewResolvedItem[]
64
+ ): NativeImagePreviewChangeMeta {
65
+ return {
66
+ reason: meta.reason,
67
+ item: items[meta.index] ?? null,
68
+ };
18
69
  }
19
70
 
20
71
  const NativeImagePreviewRoot = React.memo(function NativeImagePreview({
21
72
  children,
73
+ colorScheme = 'dark',
74
+ disabled = false,
75
+ items,
76
+ onChange,
22
77
  }: NativeImagePreviewProps) {
23
- React.useEffect(warnUnsupportedWeb, []);
78
+ const systemColorScheme = useColorScheme();
79
+ const controllerRef = React.useRef<NativeImagePreviewWebController | null>(null);
80
+ const sourceElementsRef = React.useRef<SourceElement[]>([]);
81
+ const resolvedItems = React.useMemo(
82
+ () => normalizeNativeImagePreviewItems(items),
83
+ [items]
84
+ );
85
+ const webItems = React.useMemo(
86
+ () => toWebPreviewItems(resolvedItems),
87
+ [resolvedItems]
88
+ );
89
+ const resolvedColorScheme = resolveNativeImagePreviewColorScheme(colorScheme, systemColorScheme);
90
+
91
+ React.useEffect(() => {
92
+ const controller = createNativeImagePreviewWeb();
93
+ controllerRef.current = controller;
94
+
95
+ return () => {
96
+ controller.destroy();
97
+ controllerRef.current = null;
98
+ };
99
+ }, []);
24
100
 
25
- return <>{children}</>;
101
+ const open = React.useCallback<NativeImagePreviewWebContextValue['open']>(
102
+ (index, onItemChange) => {
103
+ const controller = controllerRef.current;
104
+ if (disabled || !controller || webItems.length === 0) return;
105
+
106
+ const value = clampPreviewIndex(index, webItems.length);
107
+ const getSourceElement = (nextIndex: number) =>
108
+ findPreviewSourceElement(sourceElementsRef.current[nextIndex] ?? null);
109
+
110
+ void controller.open({
111
+ getSourceElement,
112
+ index: value,
113
+ items: webItems,
114
+ objectFit: 'cover',
115
+ sourceElement: getSourceElement(value),
116
+ sourceImageSize: resolvedItems[value]
117
+ ? {
118
+ width: resolvedItems[value].width ?? 0,
119
+ height: resolvedItems[value].height ?? 0,
120
+ }
121
+ : null,
122
+ sourceObjectFit: 'cover',
123
+ sourceVisibility: 'hidden',
124
+ theme: resolvedColorScheme,
125
+ onChange: (nextValue, meta) => {
126
+ const changeMeta = toChangeMeta(meta, resolvedItems);
127
+ onChange?.(nextValue, changeMeta);
128
+ onItemChange?.(nextValue, changeMeta);
129
+ },
130
+ });
131
+ },
132
+ [disabled, onChange, resolvedColorScheme, resolvedItems, webItems]
133
+ );
134
+
135
+ const contextValue = React.useMemo<NativeImagePreviewWebContextValue>(
136
+ () => ({
137
+ disabled,
138
+ items: resolvedItems,
139
+ open,
140
+ sourceElementsRef,
141
+ }),
142
+ [disabled, open, resolvedItems]
143
+ );
144
+
145
+ return (
146
+ <NativeImagePreviewWebContext.Provider value={contextValue}>
147
+ {children}
148
+ </NativeImagePreviewWebContext.Provider>
149
+ );
26
150
  });
27
151
 
28
152
  const NativeImagePreviewItem = React.memo(function NativeImagePreviewItem({
29
153
  children,
154
+ disabled = false,
155
+ index,
156
+ onChange,
30
157
  style,
31
158
  }: NativeImagePreviewItemProps) {
32
- return <View style={style}>{children}</View>;
159
+ const context = React.useContext(NativeImagePreviewWebContext);
160
+ const sourceRef = React.useCallback(
161
+ (node: SourceElement) => {
162
+ if (!context) return;
163
+ context.sourceElementsRef.current[index] = node;
164
+ },
165
+ [context, index]
166
+ );
167
+ const itemDisabled = !context || context.disabled || disabled || context.items.length === 0;
168
+
169
+ const open = React.useCallback(() => {
170
+ if (itemDisabled) return;
171
+ context.open(index, onChange);
172
+ }, [context, index, itemDisabled, onChange]);
173
+
174
+ const handleKeyDown = React.useCallback(
175
+ (event: React.KeyboardEvent) => {
176
+ if (itemDisabled) return;
177
+ if (event.key !== 'Enter' && event.key !== ' ') return;
178
+ event.preventDefault();
179
+ open();
180
+ },
181
+ [itemDisabled, open]
182
+ );
183
+
184
+ return (
185
+ <WebView
186
+ ref={sourceRef}
187
+ aria-label={itemDisabled ? undefined : 'Open image preview'}
188
+ onClick={itemDisabled ? undefined : open}
189
+ onKeyDown={itemDisabled ? undefined : handleKeyDown}
190
+ role={itemDisabled ? undefined : 'button'}
191
+ style={style}
192
+ tabIndex={itemDisabled ? undefined : 0}
193
+ >
194
+ {children}
195
+ </WebView>
196
+ );
33
197
  });
34
198
 
35
199
  export const NativeImagePreview = Object.assign(NativeImagePreviewRoot, {
@@ -1,6 +1,6 @@
1
1
  # NativeImagePreview
2
2
 
3
- `NativeImagePreview` 是强调原生共享转场体验的图片预览组件。它和普通 `ImagePreview` 服务分工不同:`ImagePreview` 适合命令式打开图片;`NativeImagePreview` 适合从真实缩略图自然过渡到全屏 viewer,并在 iOS / Android 上走原生手势、缩放、滑动和拖拽关闭路径。
3
+ `NativeImagePreview` 是强调真实缩略图锚点与共享转场体验的图片预览组件。它和普通 `ImagePreview` 服务分工不同:`ImagePreview` 适合命令式打开图片;`NativeImagePreview` 适合从真实缩略图自然过渡到全屏 viewer,并在 iOS / Android 上走原生手势、缩放、滑动和拖拽关闭路径,在 Web 上走 H5 共享转场 viewer。
4
4
 
5
5
  ## 适用场景
6
6
 
@@ -95,14 +95,14 @@ import { imagePreview } from 'zkit-ui/image-preview';
95
95
  imagePreview.open({ images, index: 0 });
96
96
  ```
97
97
 
98
- `ImagePreview` 是普通全屏预览服务,适合没有稳定缩略图锚点的命令式场景。`NativeImagePreview` 是声明式共享转场组件,必须就地包住真实图片节点。两者不要混成同一个封装,否则很容易把原生共享转场退化成普通弹层。
98
+ `ImagePreview` 是普通全屏预览服务,适合没有稳定缩略图锚点的命令式场景。`NativeImagePreview` 是声明式共享转场组件,必须就地包住真实图片节点。两者不要混成同一个封装,否则很容易把共享转场退化成普通弹层。
99
99
 
100
100
  ## 平台行为
101
101
 
102
102
  - iOS / Android:使用原生 viewer 和真实图片锚点,主打共享转场、缩放、滑动和拖拽关闭。
103
+ - Web:使用 H5 viewer 和真实 DOM 图片锚点,支持共享转场、缩放、滑动、视频播放和拖拽关闭。
103
104
  - 首次接入或变更原生依赖后需要重新构建 native app;仅刷新 JS bundle 不会注册新的原生 ViewManager。
104
105
  - 原生模块未注册时不会降级到 `ImagePreview`。这是 native 接入或构建错误,需要重新安装依赖并重建 native app。
105
- - Web:仅保留子节点渲染,不提供预览能力,也不会模拟普通 `ImagePreview`。
106
106
 
107
107
  ## 手动确认点
108
108
 
@@ -8,7 +8,7 @@ import type {
8
8
  } from './types';
9
9
 
10
10
  export type NativeImagePreviewContextValue = {
11
- galleryId: string;
11
+ previewGroupId: string;
12
12
  items: readonly NativeImagePreviewResolvedItem[];
13
13
  nativeItems: readonly NativeImagePreviewNativeItem[];
14
14
  colorScheme: NativeImagePreviewResolvedColorScheme;
@@ -15,6 +15,7 @@ export type NativeImagePreviewSource = ImageSourcePropType | string;
15
15
 
16
16
  export type NativeImagePreviewItemDescriptor = {
17
17
  id?: string;
18
+ alt?: string;
18
19
  type?: NativeImagePreviewMediaType;
19
20
  url?: string;
20
21
  source?: NativeImagePreviewSource;
@@ -34,6 +35,7 @@ export type NativeImagePreviewResolvedItem = {
34
35
  poster?: string;
35
36
  width?: number;
36
37
  height?: number;
38
+ alt?: string;
37
39
  raw: NativeImagePreviewItem;
38
40
  };
39
41
 
@@ -46,7 +48,7 @@ export type NativeImagePreviewNativeItem = {
46
48
  height?: number;
47
49
  };
48
50
 
49
- export type NativeImagePreviewChangeReason = 'swipe';
51
+ export type NativeImagePreviewChangeReason = 'api' | 'swipe';
50
52
 
51
53
  export type NativeImagePreviewChangeEventPayload = {
52
54
  currentIndex: number;
@@ -56,6 +56,7 @@ function normalizePreviewItem(
56
56
  poster,
57
57
  width: item.width,
58
58
  height: item.height,
59
+ alt: item.alt,
59
60
  raw: item,
60
61
  };
61
62
  }