@ericchen1990/pano-view 3.8.0 → 3.10.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.
@@ -22,9 +22,9 @@
22
22
 
23
23
  | 需求 | 推荐方式 | 应用负责 |
24
24
  | --- | --- | --- |
25
- | 直接使用弹窗,仅调整主题 | `PanoDialog` + CSS 变量 | 打开状态和内容 |
26
- | 自定义标题栏、关闭按钮、底部操作或布局 | 组合 `PanoDialog*` 组件 | 结构、样式和内容 |
27
- | 已有 Dialog 或设计系统 | `renderOverlay` + `usePanoOverlay` + `usePanoModal` | 弹窗实现、无障碍行为和样式 |
25
+ | 直接使用弹窗,仅调整主题 | `PanoOverlay` + `PanoDialog` + CSS 变量 | 打开状态和内容 |
26
+ | 自定义标题栏、关闭按钮、底部操作或布局 | `PanoOverlay` + 组合 `PanoDialog*` 组件 | 结构、样式和内容 |
27
+ | 已有 Dialog 或设计系统 | `PanoOverlay` + `usePanoOverlay` + `usePanoModal` | 弹窗实现、无障碍行为和样式 |
28
28
 
29
29
  默认弹窗与组成组件负责焦点、遮罩关闭、Escape 和 viewer 输入锁。第三方弹窗保留自己的无障碍行为;`usePanoModal` 只协调 viewer 输入。以上方式均不要求修改热点定义或项目 JSON。
30
30
 
@@ -36,8 +36,10 @@
36
36
  ```tsx
37
37
  import { useState } from "react";
38
38
  import {
39
+ PanoOverlay,
39
40
  PanoViewer,
40
41
  ProjectScenes,
42
+ usePanoOverlay,
41
43
  usePanoViewer,
42
44
  type LoadedPanoProject,
43
45
  type ProjectHotspotDefinition,
@@ -47,6 +49,19 @@ import "@ericchen1990/pano-view/styles.css";
47
49
  import "@ericchen1990/pano-view/ui/styles.css";
48
50
  import "./tour.css";
49
51
 
52
+ function FullscreenButton({ onToggle }: { onToggle: () => void }) {
53
+ const { isFullscreen } = usePanoOverlay();
54
+ return (
55
+ <button
56
+ type="button"
57
+ style={{ position: "absolute", right: 16, bottom: 16, pointerEvents: "auto" }}
58
+ onClick={onToggle}
59
+ >
60
+ {isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
61
+ </button>
62
+ );
63
+ }
64
+
50
65
  export function Tour({ project }: { project: LoadedPanoProject }) {
51
66
  const viewer = usePanoViewer();
52
67
  const [selected, setSelected] = useState<ProjectHotspotDefinition | null>(null);
@@ -56,26 +71,6 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
56
71
  ref={viewer.ref}
57
72
  className="my-tour"
58
73
  style={{ height: 560 }}
59
- renderOverlay={({ isFullscreen }) => (
60
- <>
61
- <button
62
- type="button"
63
- style={{ position: "absolute", right: 16, bottom: 16, pointerEvents: "auto" }}
64
- onClick={() => void viewer.toggleFullscreen()}
65
- >
66
- {isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
67
- </button>
68
- <PanoDialog
69
- open={selected !== null}
70
- onOpenChange={(open) => { if (!open) setSelected(null); }}
71
- title={selected?.label ?? "Details"}
72
- description="Discover more about this place."
73
- closeLabel="Close details"
74
- >
75
- {selected ? <p key={selected.id}>Content for {selected.label ?? selected.id}</p> : null}
76
- </PanoDialog>
77
- </>
78
- )}
79
74
  >
80
75
  <ProjectScenes
81
76
  project={project}
@@ -86,6 +81,18 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
86
81
  }}
87
82
  onActiveSceneIdChange={() => setSelected(null)}
88
83
  />
84
+ <PanoOverlay>
85
+ <FullscreenButton onToggle={() => void viewer.toggleFullscreen()} />
86
+ <PanoDialog
87
+ open={selected !== null}
88
+ onOpenChange={(open) => { if (!open) setSelected(null); }}
89
+ title={selected?.label ?? "Details"}
90
+ description="Discover more about this place."
91
+ closeLabel="Close details"
92
+ >
93
+ {selected ? <p key={selected.id}>Content for {selected.label ?? selected.id}</p> : null}
94
+ </PanoDialog>
95
+ </PanoOverlay>
89
96
  </PanoViewer>
90
97
  );
91
98
  }
@@ -93,7 +100,7 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
93
100
 
94
101
  传入已加载的项目,并在项目中准备一个 ID 为 `details` 的热点;`loadPanoProject` 用法见[静态项目与 JSON 数据](../README.zh-CN.md#静态项目与-json-数据)。按自己的内容映射调整选中条件。只有应用接管点击时才调用 `preventDefault()`;其他热点继续使用默认的场景跳转或媒体播放。示例显式选择在切换场景时关闭弹窗,库本身不强制这种业务策略。
95
102
 
96
- `PanoViewer.children` 是 Three.js 场景树。普通 DOM 弹窗放在 `renderOverlay` 中,不要与 `Sphere`、`ProjectScenes` 并列放进场景树。全屏变化时保持返回的组件树稳定;以全屏状态作为 key 会重置内容。
103
+ `PanoViewer` 的场景子节点留在 Three.js 树中。普通 DOM 弹窗放在 `PanoOverlay` 里,不要作为会进入场景树的裸兄弟节点与 `Sphere`、`ProjectScenes` 并列。全屏变化时保持 overlay 树稳定;以全屏状态作为 key 会重置内容。
97
104
 
98
105
  多个热点的接入请继续看[下方完整映射示例](#choose-content-by-hotspot):一个弹窗复用五份内容,并提供对应的项目 JSON。
99
106
 
@@ -120,6 +127,7 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
120
127
  ```tsx
121
128
  import { useEffect, useRef, useState, type ReactNode } from "react";
122
129
  import {
130
+ PanoOverlay,
123
131
  PanoViewer,
124
132
  ProjectScenes,
125
133
  type LoadedPanoProject,
@@ -199,15 +207,7 @@ export function MultiHotspotTour({ project }: { project: LoadedPanoProject }) {
199
207
  const contentKey = selected ? JSON.stringify([selected.sceneId, selected.hotspotId]) : "closed";
200
208
 
201
209
  return (
202
- <PanoViewer style={{ height: 560 }} renderOverlay={() => (
203
- <PanoDialog
204
- open={popup !== undefined}
205
- onOpenChange={(open) => { if (!open) setSelected(null); }}
206
- title={popup?.title ?? "Hotspot content"}
207
- >
208
- {popup ? <div key={contentKey}>{popup.body}</div> : null}
209
- </PanoDialog>
210
- )}>
210
+ <PanoViewer style={{ height: 560 }}>
211
211
  <ProjectScenes
212
212
  project={project}
213
213
  onHotspotClick={(event) => {
@@ -218,6 +218,15 @@ export function MultiHotspotTour({ project }: { project: LoadedPanoProject }) {
218
218
  }}
219
219
  onActiveSceneIdChange={() => setSelected(null)}
220
220
  />
221
+ <PanoOverlay>
222
+ <PanoDialog
223
+ open={popup !== undefined}
224
+ onOpenChange={(open) => { if (!open) setSelected(null); }}
225
+ title={popup?.title ?? "Hotspot content"}
226
+ >
227
+ {popup ? <div key={contentKey}>{popup.body}</div> : null}
228
+ </PanoDialog>
229
+ </PanoOverlay>
221
230
  </PanoViewer>
222
231
  );
223
232
  }
@@ -340,7 +349,7 @@ const style: CSSProperties & { "--pano-dialog-width": string } = {
340
349
  borderRadius: 8,
341
350
  };
342
351
 
343
- // Render this component inside PanoViewer.renderOverlay.
352
+ // Render this component inside PanoOverlay.
344
353
  export function StyledDialog(props: PanoDialogProps) {
345
354
  return <PanoDialog {...props} className="my-dialog" style={style} />;
346
355
  }
@@ -361,7 +370,7 @@ export function StyledDialog(props: PanoDialogProps) {
361
370
  <a id="compose-a-different-layout"></a>
362
371
  ## 组合自己的布局
363
372
 
364
- 需要底部操作、不同的关闭控件或完全控制内容结构时,使用组成组件。下面示例提供自己的视觉样式,不需要 `ui/styles.css`。从 `renderOverlay` 返回 `CustomLayoutDialog`,传入打开状态和内容即可。
373
+ 需要底部操作、不同的关闭控件或完全控制内容结构时,使用组成组件。下面示例提供自己的视觉样式,不需要 `ui/styles.css`。在 `PanoOverlay` 中渲染 `CustomLayoutDialog`,传入打开状态和内容即可。
365
374
 
366
375
  ```tsx
367
376
  import type { ReactNode } from "react";
@@ -426,7 +435,7 @@ Root 已经获取 viewer 模态锁,不要再为它调用 `usePanoModal`。Back
426
435
  <a id="use-an-existing-dialog"></a>
427
436
  ## 接入已有 Dialog
428
437
 
429
- viewer 不强制应用使用可选 UI 组件。第三方接入需要连接三件事:本地 Portal 容器、测量布局与 viewer 输入协调。下面以 Radix 为例,组件应从 `renderOverlay` 渲染。应用直接导入 `@radix-ui/react-dialog` 时,需要将它安装为自己的直接依赖。
438
+ viewer 不强制应用使用可选 UI 组件。第三方接入需要连接三件事:本地 Portal 容器、测量布局与 viewer 输入协调。下面以 Radix 为例,组件应在 `PanoOverlay` 中渲染。应用直接导入 `@radix-ui/react-dialog` 时,需要将它安装为自己的直接依赖。
430
439
 
431
440
  ```tsx
432
441
  import { useRef, useState, type ReactNode } from "react";
@@ -490,9 +499,9 @@ export function ExistingDialog({ open, onOpenChange, children }: {
490
499
  }
491
500
  ```
492
501
 
493
- 等待容器非 null 后再创建 Portal;viewer 挂载期间也不能让第三方 Portal 回退到 `document.body`。已有 MUI、Ant Design、shadcn 或其他封装,需要能配置等价的挂载目标;按实际暴露的 Portal API 适配。仅将组件放进 `renderOverlay`,不会自动修复它内部写死的 body Portal。
502
+ 等待容器非 null 后再创建 Portal;viewer 挂载期间也不能让第三方 Portal 回退到 `document.body`。已有 MUI、Ant Design、shadcn 或其他封装,需要能配置等价的挂载目标;按实际暴露的 Portal API 适配。仅将组件放进 `PanoOverlay`,不会自动修复它内部写死的 body Portal。
494
503
 
495
- `usePanoModal(open)` 暂停 viewer 输入与自动旋转,让背景 viewer 界面 inert 并关闭其右键菜单。它不会为第三方弹窗提供焦点约束、无障碍名称、关闭控件或页面滚动管理。锁按 viewer 隔离、引用计数;关闭弹窗不会释放其他弹窗或转场持有的锁。hook 应在子组件中无条件调用,不要直接写在 `renderOverlay` 回调中。
504
+ `usePanoModal(open)` 暂停 viewer 输入与自动旋转,让背景 viewer 界面 inert 并关闭其右键菜单。它不会为第三方弹窗提供焦点约束、无障碍名称、关闭控件或页面滚动管理。锁按 viewer 隔离、引用计数;关闭弹窗不会释放其他弹窗或转场持有的锁。hook 应在 `PanoOverlay` 下的子组件中无条件调用,不要写在 `PanoViewer` 的父组件里。
496
505
 
497
506
  <a id="nested-portals"></a>
498
507
  ## 二级 Portal
@@ -574,7 +583,8 @@ export function DialogHelp() {
574
583
 
575
584
  | API | 约定 |
576
585
  | --- | --- |
577
- | `PanoViewer.renderOverlay(context)` | 返回 Canvas 之外的 DOM React 内容;不是调用 hook 的位置 |
586
+ | `PanoOverlay` | 推荐的屏幕平面包裹;作为 `PanoViewer` 的直接子节点(或其下 Fragment 内) |
587
+ | `PanoViewer.renderOverlay(context)` | 可选的回调式 DOM 内容,位于 Canvas 之外;不是调用 hook 的位置 |
578
588
  | `usePanoOverlay()` | 为 DOM 后代提供相同 context;在 Overlay provider 外调用会抛错 |
579
589
  | `usePanoModal(open)` | 获取/释放当前 viewer 的模态输入锁,无返回值 |
580
590
  | `PanoOverlayPortalProvider` | 必填 `container: HTMLDivElement \| null`、`children`;只覆盖后代的 Portal 目标 |
@@ -599,8 +609,8 @@ export function DialogHelp() {
599
609
  | 不导入默认样式仍然有定位 | 结构定位、尺寸和指针行为按设计使用内联样式。省略 `ui/styles.css` 后提供自己的外观;完全控制 DOM 时接入自己的 Dialog。 |
600
610
  | 长内容把关闭按钮挤走 | 组成布局中,header/footer 设置 `flex-shrink: 0`,body 设置 `min-height: 0; overflow: auto`,并保持 Content 的测量尺寸约束。 |
601
611
  | 切换全屏后视频/表单重置 | 移除依赖全屏状态的 key、容器切换、不同弹窗实现间的条件切换,以及在 render 回调内定义的组件函数。 |
602
- | 自定义弹窗后面的全景仍会移动 | 在 `renderOverlay` 下的自定义组件中调用 `usePanoModal(open)`;默认/组成组件的 Root 已经处理。 |
612
+ | 自定义弹窗后面的全景仍会移动 | 在 `PanoOverlay` 下的自定义组件中调用 `usePanoModal(open)`;默认/组成组件的 Root 已经处理。 |
603
613
  | 二级 Popover 被隐藏或不可点击 | 使用 `usePanoOverlay()` 返回的 Content 内部容器,并在实际面板上开启 pointer events。 |
604
- | hook 提示缺少 provider | 从 `renderOverlay` 返回自定义 DOM 组件;不要在 `PanoViewer` 的父组件或回调本身调用 Overlay hook。 |
614
+ | hook 提示缺少 provider | 把自定义 DOM 组件放在 `PanoOverlay` 内;不要在 `PanoViewer` 的父组件调用 Overlay hook。 |
605
615
 
606
616
  接入时应确认普通模式与真实元素全屏、viewer 部分滚出屏幕、键盘开关与焦点恢复、长内容、二级 Portal、媒体清理及目标 Safari/移动设备的表现。最大化浏览器窗口不能替代 Fullscreen API 验证。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericchen1990/pano-view",
3
- "version": "3.8.0",
3
+ "version": "3.10.0",
4
4
  "description": "React components for panoramic viewing experiences.",
5
5
  "author": "Eric Chen",
6
6
  "license": "MIT",