@ericchen1990/pano-view 3.9.0 → 4.0.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/README.md +207 -95
- package/README.zh-CN.md +170 -75
- package/dist/data.cjs +64 -17
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.mts +24 -12
- package/dist/data.d.ts +24 -12
- package/dist/data.mjs +64 -17
- package/dist/data.mjs.map +1 -1
- package/dist/index.cjs +1998 -1031
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +162 -48
- package/dist/index.d.ts +162 -48
- package/dist/index.mjs +1789 -822
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +87 -74
- package/dist/ui.cjs.map +1 -1
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.mjs.map +1 -1
- package/docs/dialogs.md +52 -42
- package/docs/dialogs.zh-CN.md +52 -42
- package/package.json +1 -1
package/docs/dialogs.zh-CN.md
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
|
|
23
23
|
| 需求 | 推荐方式 | 应用负责 |
|
|
24
24
|
| --- | --- | --- |
|
|
25
|
-
| 直接使用弹窗,仅调整主题 | `PanoDialog` + CSS 变量 | 打开状态和内容 |
|
|
26
|
-
| 自定义标题栏、关闭按钮、底部操作或布局 | 组合 `PanoDialog*` 组件 | 结构、样式和内容 |
|
|
27
|
-
| 已有 Dialog 或设计系统 | `
|
|
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
|
|
103
|
+
`PanoViewer` 的场景子节点留在 Three.js 树中。普通 DOM 弹窗放在 `PanoOverlay` 里,不要作为会进入场景树的裸兄弟节点与 `Sphere`、`ProjectScenes` 并列。全屏变化时保持 overlay 树稳定;以全屏状态作为 key 会重置内容。
|
|
97
104
|
|
|
98
105
|
多个热点的接入请继续看[下方完整映射示例](#choose-content-by-hotspot):一个弹窗复用五份内容,并提供对应的项目 JSON。
|
|
99
106
|
|
|
@@ -115,11 +122,12 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
|
115
122
|
| `entrance` | `guide` | iframe | Courtyard guide |
|
|
116
123
|
| `hall` | `story` | article | Hall story |
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
下面是完整源码,包含所有内容组件、映射、选中状态和点击处理。`/overlays` 页的 Default dialog 展示这份源码;Compose parts 与 Third-party dialog 会换成各自的弹窗示例。保存为 `multi-hotspot-example.tsx`,以 `project` 传入已加载的项目即可;在仓库外使用时,将 `/fixtures/...` 替换为自己的资源地址。
|
|
119
126
|
|
|
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 }}
|
|
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
|
|
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
|
|
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
|
|
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 适配。仅将组件放进 `
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
| 自定义弹窗后面的全景仍会移动 | 在 `
|
|
612
|
+
| 自定义弹窗后面的全景仍会移动 | 在 `PanoOverlay` 下的自定义组件中调用 `usePanoModal(open)`;默认/组成组件的 Root 已经处理。 |
|
|
603
613
|
| 二级 Popover 被隐藏或不可点击 | 使用 `usePanoOverlay()` 返回的 Content 内部容器,并在实际面板上开启 pointer events。 |
|
|
604
|
-
| hook 提示缺少 provider |
|
|
614
|
+
| hook 提示缺少 provider | 把自定义 DOM 组件放在 `PanoOverlay` 内;不要在 `PanoViewer` 的父组件调用 Overlay hook。 |
|
|
605
615
|
|
|
606
616
|
接入时应确认普通模式与真实元素全屏、viewer 部分滚出屏幕、键盘开关与焦点恢复、长内容、二级 Portal、媒体清理及目标 Safari/移动设备的表现。最大化浏览器窗口不能替代 Fullscreen API 验证。
|