@ericchen1990/pano-view 4.0.1 → 5.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 +94 -15
- package/README.zh-CN.md +71 -8
- package/dist/data.cjs +4 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.mts +5 -0
- package/dist/data.d.ts +5 -0
- package/dist/data.mjs +4 -0
- package/dist/data.mjs.map +1 -1
- package/dist/index.cjs +6494 -2650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +319 -120
- package/dist/index.d.ts +319 -120
- package/dist/index.mjs +6576 -2688
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +47 -3
- package/dist/ui/styles.css +3 -0
- package/dist/ui.cjs +31 -11
- package/dist/ui.cjs.map +1 -1
- package/dist/ui.d.mts +4 -3
- package/dist/ui.d.ts +4 -3
- package/dist/ui.mjs +33 -13
- package/dist/ui.mjs.map +1 -1
- package/docs/dialogs.md +74 -2
- package/docs/dialogs.zh-CN.md +80 -2
- package/package.json +3 -2
package/dist/styles.css
CHANGED
|
@@ -808,9 +808,8 @@
|
|
|
808
808
|
width: 100%;
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
.pano-scene-menu-
|
|
812
|
-
border: var(--pano-scene-menu-selection-width, 2px) solid
|
|
813
|
-
var(--pano-scene-menu-selection-color, #ffffff);
|
|
811
|
+
.pano-scene-menu-thumb::after {
|
|
812
|
+
border: var(--pano-scene-menu-selection-width, 2px) solid transparent;
|
|
814
813
|
border-radius: inherit;
|
|
815
814
|
box-sizing: border-box;
|
|
816
815
|
content: "";
|
|
@@ -819,6 +818,29 @@
|
|
|
819
818
|
position: absolute;
|
|
820
819
|
}
|
|
821
820
|
|
|
821
|
+
.pano-scene-menu-item[data-active="true"] .pano-scene-menu-thumb::after {
|
|
822
|
+
border-color: var(--pano-scene-menu-selection-color, #ffffff);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.pano-scene-menu-item:focus-visible .pano-scene-menu-thumb::after {
|
|
826
|
+
border-color: var(--pano-scene-menu-focus-color, #7dd3fc);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@media (hover: hover) {
|
|
830
|
+
.pano-scene-menu-item:hover .pano-scene-menu-thumb::after {
|
|
831
|
+
border-color: var(--pano-scene-menu-focus-color, #7dd3fc);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.pano-scene-menu-visited {
|
|
836
|
+
height: 18px;
|
|
837
|
+
pointer-events: none;
|
|
838
|
+
position: absolute;
|
|
839
|
+
right: 6px;
|
|
840
|
+
top: 6px;
|
|
841
|
+
width: 18px;
|
|
842
|
+
}
|
|
843
|
+
|
|
822
844
|
.pano-scene-menu-label {
|
|
823
845
|
background: var(--pano-scene-menu-label-background, rgba(0, 0, 0, 0.55));
|
|
824
846
|
bottom: 0;
|
|
@@ -863,3 +885,25 @@
|
|
|
863
885
|
--pano-overlay-anchor-radius: var(--pano-scene-menu-radius, 8px);
|
|
864
886
|
background: var(--pano-scene-menu-background, rgba(16, 16, 16, 0.72));
|
|
865
887
|
}
|
|
888
|
+
|
|
889
|
+
.pano-control-group { display:flex; flex-wrap:wrap; justify-content:center; gap:var(--pano-control-gap,12px); padding:var(--pano-control-padding,12px); }
|
|
890
|
+
.pano-control-button { display:grid; place-items:center; width:var(--pano-control-size,64px); height:var(--pano-control-size,64px); border:var(--pano-control-border,1px solid #68778a); border-radius:var(--pano-control-radius,12px); background:var(--pano-control-background,#39424e); color:var(--pano-control-color,#fff); cursor:pointer; }
|
|
891
|
+
.pano-control-button svg { width:32px; height:32px; }
|
|
892
|
+
.pano-control-button:disabled { opacity:.4; cursor:default; }
|
|
893
|
+
.pano-control-button:focus-visible { outline:3px solid var(--pano-control-focus,#ffb300); outline-offset:2px; }
|
|
894
|
+
.pano-media-controls { background:#161b22; gap:var(--pano-control-gap,8px); padding:var(--pano-control-padding,8px); }
|
|
895
|
+
.pano-media-controls .pano-control-button { flex:none; width:var(--pano-control-size,48px); height:var(--pano-control-size,48px); border-radius:var(--pano-control-radius,50%); color:var(--pano-control-color,#baf5ff); }
|
|
896
|
+
.pano-media-controls .pano-control-button svg { width:24px; height:24px; }
|
|
897
|
+
.pano-media-controls [data-pano-gaze-active] { outline:3px solid var(--pano-control-focus,#ffb300); outline-offset:2px; }
|
|
898
|
+
|
|
899
|
+
.pano-scroll-controls { display:block; }
|
|
900
|
+
.pano-scroll-controls .pano-control-button { position:absolute; width:112px; height:44px; border:0; border-radius:12px; background:var(--pano-scroll-background,#101923); color:var(--pano-scroll-color,#fff); opacity:var(--pano-scroll-opacity,.3); pointer-events:auto; transition:opacity .15s; }
|
|
901
|
+
.pano-scroll-controls .pano-control-button:hover,
|
|
902
|
+
.pano-scroll-controls .pano-control-button:focus-visible,
|
|
903
|
+
.pano-scroll-controls [data-pano-gaze-active] { opacity:var(--pano-scroll-active-opacity,.9); }
|
|
904
|
+
.pano-scroll-controls [data-scroll-direction="up"] { top:6px; left:50%; transform:translateX(-50%); }
|
|
905
|
+
.pano-scroll-controls [data-scroll-direction="down"] { bottom:6px; left:50%; transform:translateX(-50%); }
|
|
906
|
+
.pano-scroll-controls [data-scroll-direction="left"],
|
|
907
|
+
.pano-scroll-controls [data-scroll-direction="right"] { width:44px; top:50%; transform:translateY(-50%); }
|
|
908
|
+
.pano-scroll-controls [data-scroll-direction="left"] { left:6px; }
|
|
909
|
+
.pano-scroll-controls [data-scroll-direction="right"] { right:6px; }
|
package/dist/ui/styles.css
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
border-radius: var(--pano-dialog-radius, 12px);
|
|
11
11
|
box-shadow: var(--pano-dialog-shadow, 0 16px 64px rgb(0 0 0 / 45%));
|
|
12
12
|
font: inherit;
|
|
13
|
+
scrollbar-color: var(--pano-dialog-muted-color, #a7b4c4) transparent;
|
|
14
|
+
scrollbar-width: thin;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
.pano-dialog-header {
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
.pano-dialog-body {
|
|
35
37
|
padding: 0 var(--pano-dialog-padding, 20px) max(var(--pano-dialog-padding, 20px), env(safe-area-inset-bottom, 0px));
|
|
36
38
|
overscroll-behavior: contain;
|
|
39
|
+
scrollbar-width: thin;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
.pano-dialog-close {
|
package/dist/ui.cjs
CHANGED
|
@@ -47,14 +47,15 @@ var DialogPrimitive = __toESM(require("@radix-ui/react-dialog"));
|
|
|
47
47
|
var import_react = require("react");
|
|
48
48
|
var import_pano_overlay = require("./index.cjs");
|
|
49
49
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
50
|
-
function PanoDialogRoot({ open, onOpenChange, children }) {
|
|
51
|
-
(0, import_pano_overlay.
|
|
52
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Root, { modal: false, open, onOpenChange, children });
|
|
50
|
+
function PanoDialogRoot({ open, onOpenChange, children, scrollTargetRef }) {
|
|
51
|
+
const surface = (0, import_pano_overlay.usePanoDialogSurface)({ open, onOpenChange, scrollTargetRef });
|
|
52
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Root, { modal: false, open, onOpenChange, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_pano_overlay.PanoDialogSurfaceContext.Provider, { value: surface, children }) });
|
|
53
53
|
}
|
|
54
54
|
var PanoDialogBackdrop = (0, import_react.forwardRef)(
|
|
55
55
|
function PanoDialogBackdrop2({ style, className, ...props }, ref) {
|
|
56
56
|
const { portalContainer } = (0, import_pano_overlay.usePanoOverlay)();
|
|
57
|
-
|
|
57
|
+
const surface = (0, import_react.useContext)(import_pano_overlay.PanoDialogSurfaceContext);
|
|
58
|
+
if (!portalContainer || surface?.presentation === "spatial") return null;
|
|
58
59
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
59
60
|
DialogPrimitive.Overlay,
|
|
60
61
|
{
|
|
@@ -69,9 +70,16 @@ var PanoDialogBackdrop = (0, import_react.forwardRef)(
|
|
|
69
70
|
);
|
|
70
71
|
var PanoDialogContent = (0, import_react.forwardRef)(
|
|
71
72
|
function PanoDialogContent2({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {
|
|
72
|
-
const
|
|
73
|
+
const overlay = (0, import_pano_overlay.usePanoOverlay)();
|
|
74
|
+
const surface = (0, import_react.useContext)(import_pano_overlay.PanoDialogSurfaceContext);
|
|
75
|
+
const { portalContainer, visibleRect } = surface ?? overlay;
|
|
73
76
|
const [nestedContainer, setNestedContainer] = (0, import_react.useState)(null);
|
|
74
77
|
const returnFocus = (0, import_react.useRef)(null);
|
|
78
|
+
const contentRef = (0, import_react.useCallback)((node) => {
|
|
79
|
+
surface?.contentRef(node);
|
|
80
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
81
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
82
|
+
}, [surface?.contentRef, forwardedRef]);
|
|
75
83
|
if (!portalContainer) return null;
|
|
76
84
|
const layout = {
|
|
77
85
|
"--pano-dialog-available-width": `${Math.max(0, visibleRect.width - 32)}px`,
|
|
@@ -88,19 +96,25 @@ var PanoDialogContent = (0, import_react.forwardRef)(
|
|
|
88
96
|
overflow: "auto",
|
|
89
97
|
pointerEvents: "auto",
|
|
90
98
|
zIndex: 1,
|
|
91
|
-
...style
|
|
99
|
+
...style,
|
|
100
|
+
...surface?.presentation === "spatial" ? { left: -1e4, top: 0, transform: "none", width: 640, maxHeight: 688 } : {}
|
|
92
101
|
};
|
|
93
102
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
94
103
|
DialogPrimitive.Content,
|
|
95
104
|
{
|
|
96
105
|
...props,
|
|
97
|
-
ref:
|
|
106
|
+
ref: contentRef,
|
|
98
107
|
"data-pano-dialog": "",
|
|
99
108
|
className: ["pano-dialog-content", className].filter(Boolean).join(" "),
|
|
100
109
|
style: layout,
|
|
110
|
+
onInteractOutside: (event) => {
|
|
111
|
+
surface?.onInteractOutside(event);
|
|
112
|
+
props.onInteractOutside?.(event);
|
|
113
|
+
},
|
|
101
114
|
onOpenAutoFocus: (event) => {
|
|
102
115
|
const active = portalContainer.ownerDocument.activeElement;
|
|
103
116
|
returnFocus.current = active instanceof HTMLElement ? active : null;
|
|
117
|
+
if (surface?.presentation === "spatial") event.preventDefault();
|
|
104
118
|
onOpenAutoFocus?.(event);
|
|
105
119
|
},
|
|
106
120
|
onCloseAutoFocus: (event) => {
|
|
@@ -130,15 +144,21 @@ var PanoDialogContent = (0, import_react.forwardRef)(
|
|
|
130
144
|
var PanoDialogTitle = DialogPrimitive.Title;
|
|
131
145
|
var PanoDialogDescription = DialogPrimitive.Description;
|
|
132
146
|
var PanoDialogClose = DialogPrimitive.Close;
|
|
133
|
-
function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Close", children, className, style }) {
|
|
147
|
+
function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Close", children, className, style, scrollTargetRef }) {
|
|
134
148
|
const descriptionId = (0, import_react.useId)();
|
|
135
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(PanoDialogRoot, { open, onOpenChange, children: [
|
|
149
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(PanoDialogRoot, { open, onOpenChange, scrollTargetRef, children: [
|
|
136
150
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogBackdrop, {}),
|
|
137
151
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
138
152
|
PanoDialogContent,
|
|
139
153
|
{
|
|
140
154
|
className,
|
|
141
|
-
style: {
|
|
155
|
+
style: {
|
|
156
|
+
// Recompute media height when scrollbars change its available width.
|
|
157
|
+
display: "grid",
|
|
158
|
+
gridTemplateRows: "auto minmax(0, 1fr)",
|
|
159
|
+
overflow: "hidden",
|
|
160
|
+
...style
|
|
161
|
+
},
|
|
142
162
|
"aria-describedby": description == null ? void 0 : descriptionId,
|
|
143
163
|
children: [
|
|
144
164
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("header", { className: "pano-dialog-header", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
@@ -146,7 +166,7 @@ function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Clos
|
|
|
146
166
|
description == null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogDescription, { id: descriptionId, className: "pano-dialog-description", children: description })
|
|
147
167
|
] }) }),
|
|
148
168
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogClose, { className: "pano-dialog-close", "aria-label": closeLabel, type: "button", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m4 4 8 8M12 4l-8 8" }) }) }),
|
|
149
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pano-dialog-body", style: { overflow: "auto", minHeight: 0 }, children })
|
|
169
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-pano-scroll-viewport": "", className: "pano-dialog-body", style: { overflow: "auto", minHeight: 0 }, children })
|
|
150
170
|
]
|
|
151
171
|
}
|
|
152
172
|
)
|
package/dist/ui.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui.ts","../src/ui/dialog.tsx","../src/ui/project-popup-dialog.tsx"],"sourcesContent":["export {\n PanoDialog,\n PanoDialogRoot,\n PanoDialogBackdrop,\n PanoDialogContent,\n PanoDialogTitle,\n PanoDialogDescription,\n PanoDialogClose,\n} from \"./ui/dialog\";\nexport type {\n PanoDialogProps,\n PanoDialogRootProps,\n PanoDialogBackdropProps,\n PanoDialogContentProps,\n PanoDialogTitleProps,\n PanoDialogDescriptionProps,\n PanoDialogCloseProps,\n} from \"./ui/dialog\";\nexport { ProjectPopupDialog } from \"./ui/project-popup-dialog\";\nexport type { ProjectPopupDialogProps } from \"./ui/project-popup-dialog\";\n","import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { forwardRef, useId, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from \"react\";\nimport { PanoOverlayPortalProvider, usePanoModal, usePanoOverlay } from \"../overlay/pano-overlay\";\n\nexport type PanoDialogRootProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n children: ReactNode;\n};\n\nexport function PanoDialogRoot({ open, onOpenChange, children }: PanoDialogRootProps) {\n usePanoModal(open);\n // Keep modality inside the viewer overlay. Radix modal mode would lock page\n // scroll and disable the host document; viewer input is already gated by usePanoModal.\n return (\n <DialogPrimitive.Root modal={false} open={open} onOpenChange={onOpenChange}>\n {children}\n </DialogPrimitive.Root>\n );\n}\n\nexport type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, \"forceMount\">;\n\nexport const PanoDialogBackdrop = forwardRef<HTMLDivElement, PanoDialogBackdropProps>(\n function PanoDialogBackdrop({ style, className, ...props }, ref) {\n const { portalContainer } = usePanoOverlay();\n if (!portalContainer) return null;\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Overlay\n {...props}\n ref={ref}\n data-pano-dialog-backdrop=\"\"\n className={[\"pano-dialog-backdrop\", className].filter(Boolean).join(\" \")}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"auto\", ...style }}\n />\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, \"forceMount\" | \"asChild\">;\n\nexport const PanoDialogContent = forwardRef<HTMLDivElement, PanoDialogContentProps>(\n function PanoDialogContent({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {\n const { portalContainer, visibleRect } = usePanoOverlay();\n const [nestedContainer, setNestedContainer] = useState<HTMLDivElement | null>(null);\n const returnFocus = useRef<HTMLElement | null>(null);\n if (!portalContainer) return null;\n const layout: CSSProperties & Record<`--pano-dialog-${string}`, string> = {\n \"--pano-dialog-available-width\": `${Math.max(0, visibleRect.width - 32)}px`,\n \"--pano-dialog-available-height\": `${Math.max(0, visibleRect.height - 32)}px`,\n position: \"absolute\",\n top: visibleRect.top + visibleRect.height / 2,\n left: visibleRect.left + visibleRect.width / 2,\n transform: \"translate(-50%, -50%)\",\n width: \"min(var(--pano-dialog-width, 640px), var(--pano-dialog-available-width))\",\n maxHeight: \"var(--pano-dialog-available-height)\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"auto\",\n pointerEvents: \"auto\",\n zIndex: 1,\n ...style,\n };\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Content\n {...props}\n ref={forwardedRef}\n data-pano-dialog=\"\"\n className={[\"pano-dialog-content\", className].filter(Boolean).join(\" \")}\n style={layout}\n onOpenAutoFocus={(event) => {\n const active = portalContainer.ownerDocument.activeElement;\n returnFocus.current = active instanceof HTMLElement ? active : null;\n onOpenAutoFocus?.(event);\n }}\n onCloseAutoFocus={(event) => {\n onCloseAutoFocus?.(event);\n if (event.defaultPrevented) return;\n event.preventDefault();\n const previous = returnFocus.current;\n const canvas = portalContainer.closest(\"[data-pano-viewer]\")?.querySelector(\"canvas\");\n const target = previous?.isConnected && previous !== previous.ownerDocument.body && !previous.closest(\"[inert]\")\n ? previous : canvas;\n if (target && !target.closest(\"[inert]\")) target.focus({ preventScroll: true });\n }}\n >\n <PanoOverlayPortalProvider container={nestedContainer}>\n {children}\n </PanoOverlayPortalProvider>\n <div\n data-pano-dialog-portals=\"\"\n ref={setNestedContainer}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\", zIndex: 2 }}\n />\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport const PanoDialogTitle = DialogPrimitive.Title;\nexport const PanoDialogDescription = DialogPrimitive.Description;\nexport const PanoDialogClose = DialogPrimitive.Close;\nexport type PanoDialogTitleProps = ComponentPropsWithoutRef<typeof PanoDialogTitle>;\nexport type PanoDialogDescriptionProps = ComponentPropsWithoutRef<typeof PanoDialogDescription>;\nexport type PanoDialogCloseProps = ComponentPropsWithoutRef<typeof PanoDialogClose>;\n\nexport type PanoDialogProps = PanoDialogRootProps & {\n title: ReactNode;\n description?: ReactNode;\n closeLabel?: string;\n className?: string;\n style?: CSSProperties;\n};\n\n/** A ready-to-use modal; compose the exported parts for a different layout. */\nexport function PanoDialog({ open, onOpenChange, title, description, closeLabel = \"Close\", children, className, style }: PanoDialogProps) {\n const descriptionId = useId();\n return (\n <PanoDialogRoot open={open} onOpenChange={onOpenChange}>\n <PanoDialogBackdrop />\n <PanoDialogContent\n className={className}\n style={{ overflow: \"hidden\", ...style }}\n aria-describedby={description == null ? undefined : descriptionId}\n >\n <header className=\"pano-dialog-header\" style={{ flexShrink: 0 }}>\n <div>\n <PanoDialogTitle className=\"pano-dialog-title\">{title}</PanoDialogTitle>\n {description == null ? null : (\n <PanoDialogDescription id={descriptionId} className=\"pano-dialog-description\">{description}</PanoDialogDescription>\n )}\n </div>\n </header>\n <PanoDialogClose className=\"pano-dialog-close\" aria-label={closeLabel} type=\"button\">\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </PanoDialogClose>\n <div className=\"pano-dialog-body\" style={{ overflow: \"auto\", minHeight: 0 }}>{children}</div>\n </PanoDialogContent>\n </PanoDialogRoot>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport type { ProjectPopupBlock, ProjectPopupDefinition } from \"../data\";\nimport { PanoDialog } from \"./dialog\";\n\nexport type ProjectPopupDialogProps = {\n popup: ProjectPopupDefinition | null;\n onOpenChange: (open: boolean) => void;\n closeLabel?: string;\n onMediaError?: (block: ProjectPopupBlock) => void;\n};\n\nfunction PopupMedia({ block, label, onError }: {\n block: Extract<ProjectPopupBlock, { type: \"audio\" | \"video\" }>;\n label: string;\n onError?: () => void;\n}) {\n const ref = useRef<HTMLMediaElement | null>(null);\n useEffect(() => {\n const media = ref.current;\n // Strict Mode replays cleanup/setup on the same DOM node.\n if (media && media.getAttribute(\"src\") !== block.src) {\n media.src = block.src;\n media.load();\n }\n return () => {\n if (!media) return;\n media.pause();\n media.removeAttribute(\"src\");\n media.load();\n };\n }, [block.src]);\n const props = { controls: true, autoPlay: false, preload: \"metadata\", src: block.src, onError } as const;\n return block.type === \"video\"\n ? <video {...props} ref={(node) => { ref.current = node; }} playsInline poster={block.poster} aria-label={label} />\n : <audio {...props} ref={(node) => { ref.current = node; }} aria-label={label} />;\n}\n\n/** Render inside PanoOverlay; the host owns selection and may replace this UI. */\nexport function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps) {\n return <PanoDialog open={popup !== null} onOpenChange={onOpenChange} title={popup?.title || popup?.id || \"\"} closeLabel={closeLabel}>\n {popup ? <div className=\"pano-project-popup\" key={popup.id}>\n {popup.blocks.map((block) => block.type === \"text\"\n ? <p key={block.id} style={{ whiteSpace: \"pre-wrap\", overflowWrap: \"anywhere\" }}>{block.text}</p>\n : block.type === \"image\"\n ? <img key={`${block.id}:${block.src}`} src={block.src} alt={block.alt ?? \"\"} onError={() => onMediaError?.(block)} />\n : <PopupMedia key={`${block.id}:${block.src}`} block={block} label={popup.title} onError={() => onMediaError?.(block)} />)}\n </div> : null}\n </PanoDialog>;\n}\n"],"mappings":"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","names":["PanoDialogBackdrop","PanoDialogContent","import_react","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/ui.ts","../src/ui/dialog.tsx","../src/ui/project-popup-dialog.tsx"],"sourcesContent":["export {\n PanoDialog,\n PanoDialogRoot,\n PanoDialogBackdrop,\n PanoDialogContent,\n PanoDialogTitle,\n PanoDialogDescription,\n PanoDialogClose,\n} from \"./ui/dialog\";\nexport type {\n PanoDialogProps,\n PanoDialogRootProps,\n PanoDialogBackdropProps,\n PanoDialogContentProps,\n PanoDialogTitleProps,\n PanoDialogDescriptionProps,\n PanoDialogCloseProps,\n} from \"./ui/dialog\";\nexport { ProjectPopupDialog } from \"./ui/project-popup-dialog\";\nexport type { ProjectPopupDialogProps } from \"./ui/project-popup-dialog\";\n","import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { useCallback, useContext, forwardRef, useId, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type ReactNode, type RefObject } from \"react\";\nimport { PanoOverlayPortalProvider, PanoDialogSurfaceContext as SurfaceContext, usePanoDialogSurface, usePanoOverlay } from \"../overlay/pano-overlay\";\n\nexport type PanoDialogRootProps = {\n scrollTargetRef?: RefObject<HTMLElement | null>;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n children: ReactNode;\n};\n\nexport function PanoDialogRoot({ open, onOpenChange, children, scrollTargetRef }: PanoDialogRootProps) {\n const surface = usePanoDialogSurface({ open, onOpenChange, scrollTargetRef });\n // Keep modality inside the viewer overlay. Radix modal mode would lock page\n // scroll and disable the host document; viewer input is already gated by usePanoModal.\n return (\n <DialogPrimitive.Root modal={false} open={open} onOpenChange={onOpenChange}>\n <SurfaceContext.Provider value={surface}>{children}</SurfaceContext.Provider>\n </DialogPrimitive.Root>\n );\n}\n\nexport type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, \"forceMount\">;\n\nexport const PanoDialogBackdrop = forwardRef<HTMLDivElement, PanoDialogBackdropProps>(\n function PanoDialogBackdrop({ style, className, ...props }, ref) {\n const { portalContainer } = usePanoOverlay();\n const surface = useContext(SurfaceContext);\n if (!portalContainer || surface?.presentation === \"spatial\") return null;\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Overlay\n {...props}\n ref={ref}\n data-pano-dialog-backdrop=\"\"\n className={[\"pano-dialog-backdrop\", className].filter(Boolean).join(\" \")}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"auto\", ...style }}\n />\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, \"forceMount\" | \"asChild\">;\n\nexport const PanoDialogContent = forwardRef<HTMLDivElement, PanoDialogContentProps>(\n function PanoDialogContent({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {\n const overlay = usePanoOverlay();\n const surface = useContext(SurfaceContext);\n const { portalContainer, visibleRect } = surface ?? overlay;\n const [nestedContainer, setNestedContainer] = useState<HTMLDivElement | null>(null);\n const returnFocus = useRef<HTMLElement | null>(null);\n const contentRef = useCallback((node: HTMLDivElement | null) => { surface?.contentRef(node); if (typeof forwardedRef === \"function\") forwardedRef(node); else if (forwardedRef) forwardedRef.current = node; }, [surface?.contentRef, forwardedRef]);\n if (!portalContainer) return null;\n const layout: CSSProperties & Record<`--pano-dialog-${string}`, string> = {\n \"--pano-dialog-available-width\": `${Math.max(0, visibleRect.width - 32)}px`,\n \"--pano-dialog-available-height\": `${Math.max(0, visibleRect.height - 32)}px`,\n position: \"absolute\",\n top: visibleRect.top + visibleRect.height / 2,\n left: visibleRect.left + visibleRect.width / 2,\n transform: \"translate(-50%, -50%)\",\n width: \"min(var(--pano-dialog-width, 640px), var(--pano-dialog-available-width))\",\n maxHeight: \"var(--pano-dialog-available-height)\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"auto\",\n pointerEvents: \"auto\",\n zIndex: 1,\n ...style,\n ...(surface?.presentation === \"spatial\" ? { left: -10000, top: 0, transform: \"none\", width: 640, maxHeight: 688 } : {}),\n };\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Content\n {...props}\n ref={contentRef}\n data-pano-dialog=\"\"\n className={[\"pano-dialog-content\", className].filter(Boolean).join(\" \")}\n style={layout}\n onInteractOutside={(event) => { surface?.onInteractOutside(event); props.onInteractOutside?.(event); }}\n onOpenAutoFocus={(event) => {\n const active = portalContainer.ownerDocument.activeElement;\n returnFocus.current = active instanceof HTMLElement ? active : null;\n if (surface?.presentation === \"spatial\") event.preventDefault();\n onOpenAutoFocus?.(event);\n }}\n onCloseAutoFocus={(event) => {\n onCloseAutoFocus?.(event);\n if (event.defaultPrevented) return;\n event.preventDefault();\n const previous = returnFocus.current;\n const canvas = portalContainer.closest(\"[data-pano-viewer]\")?.querySelector(\"canvas\");\n const target = previous?.isConnected && previous !== previous.ownerDocument.body && !previous.closest(\"[inert]\")\n ? previous : canvas;\n if (target && !target.closest(\"[inert]\")) target.focus({ preventScroll: true });\n }}\n >\n <PanoOverlayPortalProvider container={nestedContainer}>\n {children}\n </PanoOverlayPortalProvider>\n <div\n data-pano-dialog-portals=\"\"\n ref={setNestedContainer}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\", zIndex: 2 }}\n />\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport const PanoDialogTitle = DialogPrimitive.Title;\nexport const PanoDialogDescription = DialogPrimitive.Description;\nexport const PanoDialogClose = DialogPrimitive.Close;\nexport type PanoDialogTitleProps = ComponentPropsWithoutRef<typeof PanoDialogTitle>;\nexport type PanoDialogDescriptionProps = ComponentPropsWithoutRef<typeof PanoDialogDescription>;\nexport type PanoDialogCloseProps = ComponentPropsWithoutRef<typeof PanoDialogClose>;\n\nexport type PanoDialogProps = PanoDialogRootProps & {\n title: ReactNode;\n description?: ReactNode;\n closeLabel?: string;\n className?: string;\n style?: CSSProperties;\n};\n\n/** A ready-to-use modal; compose the exported parts for a different layout. */\nexport function PanoDialog({ open, onOpenChange, title, description, closeLabel = \"Close\", children, className, style, scrollTargetRef }: PanoDialogProps) {\n const descriptionId = useId();\n return (\n <PanoDialogRoot open={open} onOpenChange={onOpenChange} scrollTargetRef={scrollTargetRef}>\n <PanoDialogBackdrop />\n <PanoDialogContent\n className={className}\n style={{\n // Recompute media height when scrollbars change its available width.\n display: \"grid\",\n gridTemplateRows: \"auto minmax(0, 1fr)\",\n overflow: \"hidden\",\n ...style,\n }}\n aria-describedby={description == null ? undefined : descriptionId}\n >\n <header className=\"pano-dialog-header\" style={{ flexShrink: 0 }}>\n <div>\n <PanoDialogTitle className=\"pano-dialog-title\">{title}</PanoDialogTitle>\n {description == null ? null : (\n <PanoDialogDescription id={descriptionId} className=\"pano-dialog-description\">{description}</PanoDialogDescription>\n )}\n </div>\n </header>\n <PanoDialogClose className=\"pano-dialog-close\" aria-label={closeLabel} type=\"button\">\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </PanoDialogClose>\n <div data-pano-scroll-viewport=\"\" className=\"pano-dialog-body\" style={{ overflow: \"auto\", minHeight: 0 }}>{children}</div>\n </PanoDialogContent>\n </PanoDialogRoot>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport type { ProjectPopupBlock, ProjectPopupDefinition } from \"../data\";\nimport { PanoDialog } from \"./dialog\";\n\nexport type ProjectPopupDialogProps = {\n popup: ProjectPopupDefinition | null;\n onOpenChange: (open: boolean) => void;\n closeLabel?: string;\n onMediaError?: (block: ProjectPopupBlock) => void;\n};\n\nfunction PopupMedia({ block, label, onError }: {\n block: Extract<ProjectPopupBlock, { type: \"audio\" | \"video\" }>;\n label: string;\n onError?: () => void;\n}) {\n const ref = useRef<HTMLMediaElement | null>(null);\n useEffect(() => {\n const media = ref.current;\n // Strict Mode replays cleanup/setup on the same DOM node.\n if (media && media.getAttribute(\"src\") !== block.src) {\n media.src = block.src;\n media.load();\n }\n return () => {\n if (!media) return;\n media.pause();\n media.removeAttribute(\"src\");\n media.load();\n };\n }, [block.src]);\n const props = { controls: true, autoPlay: false, preload: \"metadata\", src: block.src, onError } as const;\n return block.type === \"video\"\n ? <video {...props} ref={(node) => { ref.current = node; }} playsInline poster={block.poster} aria-label={label} />\n : <audio {...props} ref={(node) => { ref.current = node; }} aria-label={label} />;\n}\n\n/** Render inside PanoOverlay; the host owns selection and may replace this UI. */\nexport function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps) {\n return <PanoDialog open={popup !== null} onOpenChange={onOpenChange} title={popup?.title || popup?.id || \"\"} closeLabel={closeLabel}>\n {popup ? <div className=\"pano-project-popup\" key={popup.id}>\n {popup.blocks.map((block) => block.type === \"text\"\n ? <p key={block.id} style={{ whiteSpace: \"pre-wrap\", overflowWrap: \"anywhere\" }}>{block.text}</p>\n : block.type === \"image\"\n ? <img key={`${block.id}:${block.src}`} src={block.src} alt={block.alt ?? \"\"} onError={() => onMediaError?.(block)} />\n : <PopupMedia key={`${block.id}:${block.src}`} block={block} label={popup.title} onError={() => onMediaError?.(block)} />)}\n </div> : null}\n </PanoDialog>;\n}\n"],"mappings":"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","names":["SurfaceContext","PanoDialogBackdrop","PanoDialogContent","import_react","import_jsx_runtime"]}
|
package/dist/ui.d.mts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { RefObject, ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
4
4
|
|
|
5
5
|
type PanoDialogRootProps = {
|
|
6
|
+
scrollTargetRef?: RefObject<HTMLElement | null>;
|
|
6
7
|
open: boolean;
|
|
7
8
|
onOpenChange: (open: boolean) => void;
|
|
8
9
|
children: ReactNode;
|
|
9
10
|
};
|
|
10
|
-
declare function PanoDialogRoot({ open, onOpenChange, children }: PanoDialogRootProps): react.JSX.Element;
|
|
11
|
+
declare function PanoDialogRoot({ open, onOpenChange, children, scrollTargetRef }: PanoDialogRootProps): react.JSX.Element;
|
|
11
12
|
type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, "forceMount">;
|
|
12
13
|
declare const PanoDialogBackdrop: react.ForwardRefExoticComponent<PanoDialogBackdropProps & react.RefAttributes<HTMLDivElement>>;
|
|
13
14
|
type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, "forceMount" | "asChild">;
|
|
@@ -26,7 +27,7 @@ type PanoDialogProps = PanoDialogRootProps & {
|
|
|
26
27
|
style?: CSSProperties;
|
|
27
28
|
};
|
|
28
29
|
/** A ready-to-use modal; compose the exported parts for a different layout. */
|
|
29
|
-
declare function PanoDialog({ open, onOpenChange, title, description, closeLabel, children, className, style }: PanoDialogProps): react.JSX.Element;
|
|
30
|
+
declare function PanoDialog({ open, onOpenChange, title, description, closeLabel, children, className, style, scrollTargetRef }: PanoDialogProps): react.JSX.Element;
|
|
30
31
|
|
|
31
32
|
type ProjectPopupBlock = {
|
|
32
33
|
id: string;
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { RefObject, ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
4
4
|
|
|
5
5
|
type PanoDialogRootProps = {
|
|
6
|
+
scrollTargetRef?: RefObject<HTMLElement | null>;
|
|
6
7
|
open: boolean;
|
|
7
8
|
onOpenChange: (open: boolean) => void;
|
|
8
9
|
children: ReactNode;
|
|
9
10
|
};
|
|
10
|
-
declare function PanoDialogRoot({ open, onOpenChange, children }: PanoDialogRootProps): react.JSX.Element;
|
|
11
|
+
declare function PanoDialogRoot({ open, onOpenChange, children, scrollTargetRef }: PanoDialogRootProps): react.JSX.Element;
|
|
11
12
|
type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, "forceMount">;
|
|
12
13
|
declare const PanoDialogBackdrop: react.ForwardRefExoticComponent<PanoDialogBackdropProps & react.RefAttributes<HTMLDivElement>>;
|
|
13
14
|
type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, "forceMount" | "asChild">;
|
|
@@ -26,7 +27,7 @@ type PanoDialogProps = PanoDialogRootProps & {
|
|
|
26
27
|
style?: CSSProperties;
|
|
27
28
|
};
|
|
28
29
|
/** A ready-to-use modal; compose the exported parts for a different layout. */
|
|
29
|
-
declare function PanoDialog({ open, onOpenChange, title, description, closeLabel, children, className, style }: PanoDialogProps): react.JSX.Element;
|
|
30
|
+
declare function PanoDialog({ open, onOpenChange, title, description, closeLabel, children, className, style, scrollTargetRef }: PanoDialogProps): react.JSX.Element;
|
|
30
31
|
|
|
31
32
|
type ProjectPopupBlock = {
|
|
32
33
|
id: string;
|
package/dist/ui.mjs
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
// src/ui/dialog.tsx
|
|
4
4
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
-
import { forwardRef, useId, useRef, useState } from "react";
|
|
6
|
-
import { PanoOverlayPortalProvider,
|
|
5
|
+
import { useCallback, useContext, forwardRef, useId, useRef, useState } from "react";
|
|
6
|
+
import { PanoOverlayPortalProvider, PanoDialogSurfaceContext as SurfaceContext, usePanoDialogSurface, usePanoOverlay } from "./index.mjs";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
function PanoDialogRoot({ open, onOpenChange, children }) {
|
|
9
|
-
|
|
10
|
-
return /* @__PURE__ */ jsx(DialogPrimitive.Root, { modal: false, open, onOpenChange, children });
|
|
8
|
+
function PanoDialogRoot({ open, onOpenChange, children, scrollTargetRef }) {
|
|
9
|
+
const surface = usePanoDialogSurface({ open, onOpenChange, scrollTargetRef });
|
|
10
|
+
return /* @__PURE__ */ jsx(DialogPrimitive.Root, { modal: false, open, onOpenChange, children: /* @__PURE__ */ jsx(SurfaceContext.Provider, { value: surface, children }) });
|
|
11
11
|
}
|
|
12
12
|
var PanoDialogBackdrop = forwardRef(
|
|
13
13
|
function PanoDialogBackdrop2({ style, className, ...props }, ref) {
|
|
14
14
|
const { portalContainer } = usePanoOverlay();
|
|
15
|
-
|
|
15
|
+
const surface = useContext(SurfaceContext);
|
|
16
|
+
if (!portalContainer || surface?.presentation === "spatial") return null;
|
|
16
17
|
return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
|
|
17
18
|
DialogPrimitive.Overlay,
|
|
18
19
|
{
|
|
@@ -27,9 +28,16 @@ var PanoDialogBackdrop = forwardRef(
|
|
|
27
28
|
);
|
|
28
29
|
var PanoDialogContent = forwardRef(
|
|
29
30
|
function PanoDialogContent2({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {
|
|
30
|
-
const
|
|
31
|
+
const overlay = usePanoOverlay();
|
|
32
|
+
const surface = useContext(SurfaceContext);
|
|
33
|
+
const { portalContainer, visibleRect } = surface ?? overlay;
|
|
31
34
|
const [nestedContainer, setNestedContainer] = useState(null);
|
|
32
35
|
const returnFocus = useRef(null);
|
|
36
|
+
const contentRef = useCallback((node) => {
|
|
37
|
+
surface?.contentRef(node);
|
|
38
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
39
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
40
|
+
}, [surface?.contentRef, forwardedRef]);
|
|
33
41
|
if (!portalContainer) return null;
|
|
34
42
|
const layout = {
|
|
35
43
|
"--pano-dialog-available-width": `${Math.max(0, visibleRect.width - 32)}px`,
|
|
@@ -46,19 +54,25 @@ var PanoDialogContent = forwardRef(
|
|
|
46
54
|
overflow: "auto",
|
|
47
55
|
pointerEvents: "auto",
|
|
48
56
|
zIndex: 1,
|
|
49
|
-
...style
|
|
57
|
+
...style,
|
|
58
|
+
...surface?.presentation === "spatial" ? { left: -1e4, top: 0, transform: "none", width: 640, maxHeight: 688 } : {}
|
|
50
59
|
};
|
|
51
60
|
return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxs(
|
|
52
61
|
DialogPrimitive.Content,
|
|
53
62
|
{
|
|
54
63
|
...props,
|
|
55
|
-
ref:
|
|
64
|
+
ref: contentRef,
|
|
56
65
|
"data-pano-dialog": "",
|
|
57
66
|
className: ["pano-dialog-content", className].filter(Boolean).join(" "),
|
|
58
67
|
style: layout,
|
|
68
|
+
onInteractOutside: (event) => {
|
|
69
|
+
surface?.onInteractOutside(event);
|
|
70
|
+
props.onInteractOutside?.(event);
|
|
71
|
+
},
|
|
59
72
|
onOpenAutoFocus: (event) => {
|
|
60
73
|
const active = portalContainer.ownerDocument.activeElement;
|
|
61
74
|
returnFocus.current = active instanceof HTMLElement ? active : null;
|
|
75
|
+
if (surface?.presentation === "spatial") event.preventDefault();
|
|
62
76
|
onOpenAutoFocus?.(event);
|
|
63
77
|
},
|
|
64
78
|
onCloseAutoFocus: (event) => {
|
|
@@ -88,15 +102,21 @@ var PanoDialogContent = forwardRef(
|
|
|
88
102
|
var PanoDialogTitle = DialogPrimitive.Title;
|
|
89
103
|
var PanoDialogDescription = DialogPrimitive.Description;
|
|
90
104
|
var PanoDialogClose = DialogPrimitive.Close;
|
|
91
|
-
function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Close", children, className, style }) {
|
|
105
|
+
function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Close", children, className, style, scrollTargetRef }) {
|
|
92
106
|
const descriptionId = useId();
|
|
93
|
-
return /* @__PURE__ */ jsxs(PanoDialogRoot, { open, onOpenChange, children: [
|
|
107
|
+
return /* @__PURE__ */ jsxs(PanoDialogRoot, { open, onOpenChange, scrollTargetRef, children: [
|
|
94
108
|
/* @__PURE__ */ jsx(PanoDialogBackdrop, {}),
|
|
95
109
|
/* @__PURE__ */ jsxs(
|
|
96
110
|
PanoDialogContent,
|
|
97
111
|
{
|
|
98
112
|
className,
|
|
99
|
-
style: {
|
|
113
|
+
style: {
|
|
114
|
+
// Recompute media height when scrollbars change its available width.
|
|
115
|
+
display: "grid",
|
|
116
|
+
gridTemplateRows: "auto minmax(0, 1fr)",
|
|
117
|
+
overflow: "hidden",
|
|
118
|
+
...style
|
|
119
|
+
},
|
|
100
120
|
"aria-describedby": description == null ? void 0 : descriptionId,
|
|
101
121
|
children: [
|
|
102
122
|
/* @__PURE__ */ jsx("header", { className: "pano-dialog-header", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -104,7 +124,7 @@ function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Clos
|
|
|
104
124
|
description == null ? null : /* @__PURE__ */ jsx(PanoDialogDescription, { id: descriptionId, className: "pano-dialog-description", children: description })
|
|
105
125
|
] }) }),
|
|
106
126
|
/* @__PURE__ */ jsx(PanoDialogClose, { className: "pano-dialog-close", "aria-label": closeLabel, type: "button", children: /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" }) }) }),
|
|
107
|
-
/* @__PURE__ */ jsx("div", { className: "pano-dialog-body", style: { overflow: "auto", minHeight: 0 }, children })
|
|
127
|
+
/* @__PURE__ */ jsx("div", { "data-pano-scroll-viewport": "", className: "pano-dialog-body", style: { overflow: "auto", minHeight: 0 }, children })
|
|
108
128
|
]
|
|
109
129
|
}
|
|
110
130
|
)
|
package/dist/ui.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/dialog.tsx","../src/ui/project-popup-dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { forwardRef, useId, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from \"react\";\nimport { PanoOverlayPortalProvider, usePanoModal, usePanoOverlay } from \"../overlay/pano-overlay\";\n\nexport type PanoDialogRootProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n children: ReactNode;\n};\n\nexport function PanoDialogRoot({ open, onOpenChange, children }: PanoDialogRootProps) {\n usePanoModal(open);\n // Keep modality inside the viewer overlay. Radix modal mode would lock page\n // scroll and disable the host document; viewer input is already gated by usePanoModal.\n return (\n <DialogPrimitive.Root modal={false} open={open} onOpenChange={onOpenChange}>\n {children}\n </DialogPrimitive.Root>\n );\n}\n\nexport type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, \"forceMount\">;\n\nexport const PanoDialogBackdrop = forwardRef<HTMLDivElement, PanoDialogBackdropProps>(\n function PanoDialogBackdrop({ style, className, ...props }, ref) {\n const { portalContainer } = usePanoOverlay();\n if (!portalContainer) return null;\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Overlay\n {...props}\n ref={ref}\n data-pano-dialog-backdrop=\"\"\n className={[\"pano-dialog-backdrop\", className].filter(Boolean).join(\" \")}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"auto\", ...style }}\n />\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, \"forceMount\" | \"asChild\">;\n\nexport const PanoDialogContent = forwardRef<HTMLDivElement, PanoDialogContentProps>(\n function PanoDialogContent({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {\n const { portalContainer, visibleRect } = usePanoOverlay();\n const [nestedContainer, setNestedContainer] = useState<HTMLDivElement | null>(null);\n const returnFocus = useRef<HTMLElement | null>(null);\n if (!portalContainer) return null;\n const layout: CSSProperties & Record<`--pano-dialog-${string}`, string> = {\n \"--pano-dialog-available-width\": `${Math.max(0, visibleRect.width - 32)}px`,\n \"--pano-dialog-available-height\": `${Math.max(0, visibleRect.height - 32)}px`,\n position: \"absolute\",\n top: visibleRect.top + visibleRect.height / 2,\n left: visibleRect.left + visibleRect.width / 2,\n transform: \"translate(-50%, -50%)\",\n width: \"min(var(--pano-dialog-width, 640px), var(--pano-dialog-available-width))\",\n maxHeight: \"var(--pano-dialog-available-height)\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"auto\",\n pointerEvents: \"auto\",\n zIndex: 1,\n ...style,\n };\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Content\n {...props}\n ref={forwardedRef}\n data-pano-dialog=\"\"\n className={[\"pano-dialog-content\", className].filter(Boolean).join(\" \")}\n style={layout}\n onOpenAutoFocus={(event) => {\n const active = portalContainer.ownerDocument.activeElement;\n returnFocus.current = active instanceof HTMLElement ? active : null;\n onOpenAutoFocus?.(event);\n }}\n onCloseAutoFocus={(event) => {\n onCloseAutoFocus?.(event);\n if (event.defaultPrevented) return;\n event.preventDefault();\n const previous = returnFocus.current;\n const canvas = portalContainer.closest(\"[data-pano-viewer]\")?.querySelector(\"canvas\");\n const target = previous?.isConnected && previous !== previous.ownerDocument.body && !previous.closest(\"[inert]\")\n ? previous : canvas;\n if (target && !target.closest(\"[inert]\")) target.focus({ preventScroll: true });\n }}\n >\n <PanoOverlayPortalProvider container={nestedContainer}>\n {children}\n </PanoOverlayPortalProvider>\n <div\n data-pano-dialog-portals=\"\"\n ref={setNestedContainer}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\", zIndex: 2 }}\n />\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport const PanoDialogTitle = DialogPrimitive.Title;\nexport const PanoDialogDescription = DialogPrimitive.Description;\nexport const PanoDialogClose = DialogPrimitive.Close;\nexport type PanoDialogTitleProps = ComponentPropsWithoutRef<typeof PanoDialogTitle>;\nexport type PanoDialogDescriptionProps = ComponentPropsWithoutRef<typeof PanoDialogDescription>;\nexport type PanoDialogCloseProps = ComponentPropsWithoutRef<typeof PanoDialogClose>;\n\nexport type PanoDialogProps = PanoDialogRootProps & {\n title: ReactNode;\n description?: ReactNode;\n closeLabel?: string;\n className?: string;\n style?: CSSProperties;\n};\n\n/** A ready-to-use modal; compose the exported parts for a different layout. */\nexport function PanoDialog({ open, onOpenChange, title, description, closeLabel = \"Close\", children, className, style }: PanoDialogProps) {\n const descriptionId = useId();\n return (\n <PanoDialogRoot open={open} onOpenChange={onOpenChange}>\n <PanoDialogBackdrop />\n <PanoDialogContent\n className={className}\n style={{ overflow: \"hidden\", ...style }}\n aria-describedby={description == null ? undefined : descriptionId}\n >\n <header className=\"pano-dialog-header\" style={{ flexShrink: 0 }}>\n <div>\n <PanoDialogTitle className=\"pano-dialog-title\">{title}</PanoDialogTitle>\n {description == null ? null : (\n <PanoDialogDescription id={descriptionId} className=\"pano-dialog-description\">{description}</PanoDialogDescription>\n )}\n </div>\n </header>\n <PanoDialogClose className=\"pano-dialog-close\" aria-label={closeLabel} type=\"button\">\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </PanoDialogClose>\n <div className=\"pano-dialog-body\" style={{ overflow: \"auto\", minHeight: 0 }}>{children}</div>\n </PanoDialogContent>\n </PanoDialogRoot>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport type { ProjectPopupBlock, ProjectPopupDefinition } from \"../data\";\nimport { PanoDialog } from \"./dialog\";\n\nexport type ProjectPopupDialogProps = {\n popup: ProjectPopupDefinition | null;\n onOpenChange: (open: boolean) => void;\n closeLabel?: string;\n onMediaError?: (block: ProjectPopupBlock) => void;\n};\n\nfunction PopupMedia({ block, label, onError }: {\n block: Extract<ProjectPopupBlock, { type: \"audio\" | \"video\" }>;\n label: string;\n onError?: () => void;\n}) {\n const ref = useRef<HTMLMediaElement | null>(null);\n useEffect(() => {\n const media = ref.current;\n // Strict Mode replays cleanup/setup on the same DOM node.\n if (media && media.getAttribute(\"src\") !== block.src) {\n media.src = block.src;\n media.load();\n }\n return () => {\n if (!media) return;\n media.pause();\n media.removeAttribute(\"src\");\n media.load();\n };\n }, [block.src]);\n const props = { controls: true, autoPlay: false, preload: \"metadata\", src: block.src, onError } as const;\n return block.type === \"video\"\n ? <video {...props} ref={(node) => { ref.current = node; }} playsInline poster={block.poster} aria-label={label} />\n : <audio {...props} ref={(node) => { ref.current = node; }} aria-label={label} />;\n}\n\n/** Render inside PanoOverlay; the host owns selection and may replace this UI. */\nexport function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps) {\n return <PanoDialog open={popup !== null} onOpenChange={onOpenChange} title={popup?.title || popup?.id || \"\"} closeLabel={closeLabel}>\n {popup ? <div className=\"pano-project-popup\" key={popup.id}>\n {popup.blocks.map((block) => block.type === \"text\"\n ? <p key={block.id} style={{ whiteSpace: \"pre-wrap\", overflowWrap: \"anywhere\" }}>{block.text}</p>\n : block.type === \"image\"\n ? <img key={`${block.id}:${block.src}`} src={block.src} alt={block.alt ?? \"\"} onError={() => onMediaError?.(block)} />\n : <PopupMedia key={`${block.id}:${block.src}`} block={block} label={popup.title} onError={() => onMediaError?.(block)} />)}\n </div> : null}\n </PanoDialog>;\n}\n"],"mappings":"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","names":["PanoDialogBackdrop","PanoDialogContent","useRef","jsx","useRef"]}
|
|
1
|
+
{"version":3,"sources":["../src/ui/dialog.tsx","../src/ui/project-popup-dialog.tsx"],"sourcesContent":["import * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { useCallback, useContext, forwardRef, useId, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type ReactNode, type RefObject } from \"react\";\nimport { PanoOverlayPortalProvider, PanoDialogSurfaceContext as SurfaceContext, usePanoDialogSurface, usePanoOverlay } from \"../overlay/pano-overlay\";\n\nexport type PanoDialogRootProps = {\n scrollTargetRef?: RefObject<HTMLElement | null>;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n children: ReactNode;\n};\n\nexport function PanoDialogRoot({ open, onOpenChange, children, scrollTargetRef }: PanoDialogRootProps) {\n const surface = usePanoDialogSurface({ open, onOpenChange, scrollTargetRef });\n // Keep modality inside the viewer overlay. Radix modal mode would lock page\n // scroll and disable the host document; viewer input is already gated by usePanoModal.\n return (\n <DialogPrimitive.Root modal={false} open={open} onOpenChange={onOpenChange}>\n <SurfaceContext.Provider value={surface}>{children}</SurfaceContext.Provider>\n </DialogPrimitive.Root>\n );\n}\n\nexport type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, \"forceMount\">;\n\nexport const PanoDialogBackdrop = forwardRef<HTMLDivElement, PanoDialogBackdropProps>(\n function PanoDialogBackdrop({ style, className, ...props }, ref) {\n const { portalContainer } = usePanoOverlay();\n const surface = useContext(SurfaceContext);\n if (!portalContainer || surface?.presentation === \"spatial\") return null;\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Overlay\n {...props}\n ref={ref}\n data-pano-dialog-backdrop=\"\"\n className={[\"pano-dialog-backdrop\", className].filter(Boolean).join(\" \")}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"auto\", ...style }}\n />\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, \"forceMount\" | \"asChild\">;\n\nexport const PanoDialogContent = forwardRef<HTMLDivElement, PanoDialogContentProps>(\n function PanoDialogContent({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {\n const overlay = usePanoOverlay();\n const surface = useContext(SurfaceContext);\n const { portalContainer, visibleRect } = surface ?? overlay;\n const [nestedContainer, setNestedContainer] = useState<HTMLDivElement | null>(null);\n const returnFocus = useRef<HTMLElement | null>(null);\n const contentRef = useCallback((node: HTMLDivElement | null) => { surface?.contentRef(node); if (typeof forwardedRef === \"function\") forwardedRef(node); else if (forwardedRef) forwardedRef.current = node; }, [surface?.contentRef, forwardedRef]);\n if (!portalContainer) return null;\n const layout: CSSProperties & Record<`--pano-dialog-${string}`, string> = {\n \"--pano-dialog-available-width\": `${Math.max(0, visibleRect.width - 32)}px`,\n \"--pano-dialog-available-height\": `${Math.max(0, visibleRect.height - 32)}px`,\n position: \"absolute\",\n top: visibleRect.top + visibleRect.height / 2,\n left: visibleRect.left + visibleRect.width / 2,\n transform: \"translate(-50%, -50%)\",\n width: \"min(var(--pano-dialog-width, 640px), var(--pano-dialog-available-width))\",\n maxHeight: \"var(--pano-dialog-available-height)\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"auto\",\n pointerEvents: \"auto\",\n zIndex: 1,\n ...style,\n ...(surface?.presentation === \"spatial\" ? { left: -10000, top: 0, transform: \"none\", width: 640, maxHeight: 688 } : {}),\n };\n return (\n <DialogPrimitive.Portal container={portalContainer}>\n <DialogPrimitive.Content\n {...props}\n ref={contentRef}\n data-pano-dialog=\"\"\n className={[\"pano-dialog-content\", className].filter(Boolean).join(\" \")}\n style={layout}\n onInteractOutside={(event) => { surface?.onInteractOutside(event); props.onInteractOutside?.(event); }}\n onOpenAutoFocus={(event) => {\n const active = portalContainer.ownerDocument.activeElement;\n returnFocus.current = active instanceof HTMLElement ? active : null;\n if (surface?.presentation === \"spatial\") event.preventDefault();\n onOpenAutoFocus?.(event);\n }}\n onCloseAutoFocus={(event) => {\n onCloseAutoFocus?.(event);\n if (event.defaultPrevented) return;\n event.preventDefault();\n const previous = returnFocus.current;\n const canvas = portalContainer.closest(\"[data-pano-viewer]\")?.querySelector(\"canvas\");\n const target = previous?.isConnected && previous !== previous.ownerDocument.body && !previous.closest(\"[inert]\")\n ? previous : canvas;\n if (target && !target.closest(\"[inert]\")) target.focus({ preventScroll: true });\n }}\n >\n <PanoOverlayPortalProvider container={nestedContainer}>\n {children}\n </PanoOverlayPortalProvider>\n <div\n data-pano-dialog-portals=\"\"\n ref={setNestedContainer}\n style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\", zIndex: 2 }}\n />\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n },\n);\n\nexport const PanoDialogTitle = DialogPrimitive.Title;\nexport const PanoDialogDescription = DialogPrimitive.Description;\nexport const PanoDialogClose = DialogPrimitive.Close;\nexport type PanoDialogTitleProps = ComponentPropsWithoutRef<typeof PanoDialogTitle>;\nexport type PanoDialogDescriptionProps = ComponentPropsWithoutRef<typeof PanoDialogDescription>;\nexport type PanoDialogCloseProps = ComponentPropsWithoutRef<typeof PanoDialogClose>;\n\nexport type PanoDialogProps = PanoDialogRootProps & {\n title: ReactNode;\n description?: ReactNode;\n closeLabel?: string;\n className?: string;\n style?: CSSProperties;\n};\n\n/** A ready-to-use modal; compose the exported parts for a different layout. */\nexport function PanoDialog({ open, onOpenChange, title, description, closeLabel = \"Close\", children, className, style, scrollTargetRef }: PanoDialogProps) {\n const descriptionId = useId();\n return (\n <PanoDialogRoot open={open} onOpenChange={onOpenChange} scrollTargetRef={scrollTargetRef}>\n <PanoDialogBackdrop />\n <PanoDialogContent\n className={className}\n style={{\n // Recompute media height when scrollbars change its available width.\n display: \"grid\",\n gridTemplateRows: \"auto minmax(0, 1fr)\",\n overflow: \"hidden\",\n ...style,\n }}\n aria-describedby={description == null ? undefined : descriptionId}\n >\n <header className=\"pano-dialog-header\" style={{ flexShrink: 0 }}>\n <div>\n <PanoDialogTitle className=\"pano-dialog-title\">{title}</PanoDialogTitle>\n {description == null ? null : (\n <PanoDialogDescription id={descriptionId} className=\"pano-dialog-description\">{description}</PanoDialogDescription>\n )}\n </div>\n </header>\n <PanoDialogClose className=\"pano-dialog-close\" aria-label={closeLabel} type=\"button\">\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </PanoDialogClose>\n <div data-pano-scroll-viewport=\"\" className=\"pano-dialog-body\" style={{ overflow: \"auto\", minHeight: 0 }}>{children}</div>\n </PanoDialogContent>\n </PanoDialogRoot>\n );\n}\n","import { useEffect, useRef } from \"react\";\nimport type { ProjectPopupBlock, ProjectPopupDefinition } from \"../data\";\nimport { PanoDialog } from \"./dialog\";\n\nexport type ProjectPopupDialogProps = {\n popup: ProjectPopupDefinition | null;\n onOpenChange: (open: boolean) => void;\n closeLabel?: string;\n onMediaError?: (block: ProjectPopupBlock) => void;\n};\n\nfunction PopupMedia({ block, label, onError }: {\n block: Extract<ProjectPopupBlock, { type: \"audio\" | \"video\" }>;\n label: string;\n onError?: () => void;\n}) {\n const ref = useRef<HTMLMediaElement | null>(null);\n useEffect(() => {\n const media = ref.current;\n // Strict Mode replays cleanup/setup on the same DOM node.\n if (media && media.getAttribute(\"src\") !== block.src) {\n media.src = block.src;\n media.load();\n }\n return () => {\n if (!media) return;\n media.pause();\n media.removeAttribute(\"src\");\n media.load();\n };\n }, [block.src]);\n const props = { controls: true, autoPlay: false, preload: \"metadata\", src: block.src, onError } as const;\n return block.type === \"video\"\n ? <video {...props} ref={(node) => { ref.current = node; }} playsInline poster={block.poster} aria-label={label} />\n : <audio {...props} ref={(node) => { ref.current = node; }} aria-label={label} />;\n}\n\n/** Render inside PanoOverlay; the host owns selection and may replace this UI. */\nexport function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps) {\n return <PanoDialog open={popup !== null} onOpenChange={onOpenChange} title={popup?.title || popup?.id || \"\"} closeLabel={closeLabel}>\n {popup ? <div className=\"pano-project-popup\" key={popup.id}>\n {popup.blocks.map((block) => block.type === \"text\"\n ? <p key={block.id} style={{ whiteSpace: \"pre-wrap\", overflowWrap: \"anywhere\" }}>{block.text}</p>\n : block.type === \"image\"\n ? <img key={`${block.id}:${block.src}`} src={block.src} alt={block.alt ?? \"\"} onError={() => onMediaError?.(block)} />\n : <PopupMedia key={`${block.id}:${block.src}`} block={block} label={popup.title} onError={() => onMediaError?.(block)} />)}\n </div> : null}\n </PanoDialog>;\n}\n"],"mappings":"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","names":["PanoDialogBackdrop","PanoDialogContent","useRef","jsx","useRef"]}
|
package/docs/dialogs.md
CHANGED
|
@@ -122,7 +122,7 @@ The registry below contains two different articles, a video, a gallery and an if
|
|
|
122
122
|
| `entrance` | `guide` | iframe | Courtyard guide |
|
|
123
123
|
| `hall` | `story` | article | Hall story |
|
|
124
124
|
|
|
125
|
-
The complete source defines every content component, the registry, selection state and click handler.
|
|
125
|
+
The complete source defines every content component, the registry, selection state and click handler. The `/immersive-tour` integration extends this mapping with models and VR. `/overlays` uses a smaller single-scene example to compare the three dialog shells. Save it as `multi-hotspot-example.tsx`, pass a loaded project as `project`, and replace the `/fixtures/...` URLs with your own assets outside this repository.
|
|
126
126
|
|
|
127
127
|
```tsx
|
|
128
128
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
@@ -280,7 +280,7 @@ All these markers use `type: "graphic"`. The `kind` field in `POPUPS_BY_SCENE` i
|
|
|
280
280
|
5. The content wrapper's key includes both IDs. Direct programmatic selection changes reset the previous content's form/gallery state and run its cleanup, including pausing the old video. The outer dialog and its focus boundary remain shared.
|
|
281
281
|
6. Closing or changing scenes clears `selected`, unmounts the content and releases the modal lock. Fullscreen changes do not alter the selection key, so they do not reset the content.
|
|
282
282
|
|
|
283
|
-
In `/
|
|
283
|
+
In `/immersive-tour`, open **Courtyard story** and **Garden details** to compare two articles, then try video, gallery and guide. Use **Go to next room** to enter the hall; the `story` ID now opens **Hall story** with different content. The mapping table below the viewer and the copyable code show the exact relationship.
|
|
284
284
|
|
|
285
285
|
`body` accepts any React node: native media, a business component, rich text, or a component that fetches API data. Handle loading/errors in that component and dispose custom players/subscriptions in cleanup. If the content comes from JSON, map a validated content kind to a React renderer rather than putting React nodes in JSON. Configure iframe permissions for the embed; third-party response headers can reject embedding. Video autoplay remains opt-in and subject to browser policy.
|
|
286
286
|
|
|
@@ -614,3 +614,75 @@ Each export has a corresponding `*Props` type. Root is controlled and does not e
|
|
|
614
614
|
| A hook reports a missing provider | Render the custom DOM component inside `PanoOverlay`; do not call Overlay hooks in the parent of `PanoViewer`. |
|
|
615
615
|
|
|
616
616
|
Before integrating, verify normal and real element fullscreen modes, a partially scrolled viewer, keyboard open/close and focus restoration, long content, nested portals, media cleanup and your supported Safari/mobile devices. A maximized browser window does not exercise the Fullscreen API.
|
|
617
|
+
|
|
618
|
+
## Spatial dialogs and gaze controls
|
|
619
|
+
|
|
620
|
+
`PanoDialog`, its composed parts, and `ProjectPopupDialog` automatically reuse their content in VR. The original header close button remains available. The content toolbar does not add Close or Exit VR buttons. VR hotspot tooltips use scene sprites anchored to their hotspots, share the scene’s stereo projection and lens correction, and remain behind spatial dialogs. Fallback stereo renders dialogs in a separate flat pass after panorama lens distortion, then applies the final lens aperture mask to all content.
|
|
621
|
+
|
|
622
|
+
Third-party dialogs can connect `usePanoDialogSurface({ open, onOpenChange })` from the main entry. Attach `contentRef` to the content node and use the returned `portalContainer`, `visibleRect`, and `presentation`. Pass the returned `onInteractOutside` to the dialog content: it prevents dismissal in spatial mode and when operating host controls outside the viewer, including VR entry. Backdrop dismissal inside the viewer remains available in screen mode. Keep the same DOM node in a viewer-local offscreen measuring area in spatial mode; do not hide it with `display:none` or move portals to `body`. Retain a clickable close button. See the Playground third-party dialog example.
|
|
623
|
+
|
|
624
|
+
In VR, looking outside a dialog allows gaze targeting of scene hotspots. Passing over a hotspot or looking at empty space keeps the dialog open. After the existing `cursorDwellMs` completes on a hotspot, the current dialog stack closes from top to bottom before that hotspot activates once; a dialog opened by the new action stays open and anchors in the current gaze direction. The old stack is committed closed before activation, including when the host reuses a shared dialog component. Dialog content retains gaze priority over hotspots behind it. Entering or leaving VR alone does not dismiss an open dialog.
|
|
625
|
+
|
|
626
|
+
Register host model controls with `usePanoSpatialControls(elementRef, adapter)`. `PanoSpatialControlAdapter` supports `rotateBy(yawDegrees, pitchDegrees)`, `zoomBy(factor)`, `resetView()`, `canExecute(action)`, `subscribeFrame(invalidate)`, and a same-renderer-context `texture`. Action names are `left/right/up/down/in/out/reset`. The host owns limits and reset state. Separate canvases must expose readable frames after rendering. Models and project configuration are reused.
|
|
627
|
+
|
|
628
|
+
Continuous actions start after `cursorDwellMs`, stop on gaze exit, and require fresh dwell on reentry. Defaults: half a viewport per second for scrolling, 30 degrees per second for rotation, and a 1.5 scale factor per second. Reset, close, and gallery buttons activate once. Explicitly disabling the cursor or dwell remains supported. Text input and virtual keyboards are out of scope. Cross-origin frames, unreadable canvases, and media blocked by browser policy require viewing or authorization outside VR; gaze cannot create trusted browser user activation.
|
|
629
|
+
|
|
630
|
+
The spatial window sits about two meters away and scales to at most 32° horizontally and 40° vertically. The close button keeps its 36px visual size in both modes, with a minimum invisible 56px gaze target in VR. Native capture requires both `content="drawable"` and the painting APIs; older experimental implementations use the compatible renderer. The content node exposes capture status through `data-pano-spatial-state` (`loading`, `ready`, `error`).
|
|
631
|
+
|
|
632
|
+
Screen-plane UI inside `PanoOverlay` / `renderOverlay` is hidden and cannot receive focus during VR; it returns on exit. The portal host stays mounted and measurable so registered dialogs can still render as spatial panels. Built-in VR session controls remain available.
|
|
633
|
+
|
|
634
|
+
## VR icon controls and scrolling
|
|
635
|
+
|
|
636
|
+
New controls are visible and interactive only in VR. Outside VR, retain native video controls, scrolling and host model pointer controls. No project JSON changes are required. Import the main package and its `styles.css`.
|
|
637
|
+
|
|
638
|
+
Exports: `PanoMediaControls`, `PanoModelControls`, `PanoScrollControls`, `PanoControlGroup`, `PanoControlButton`, `PanoSpatialControls`, `usePanoMediaController`, and the `PanoMediaController` / `PanoControlAction` types.
|
|
639
|
+
|
|
640
|
+
```tsx
|
|
641
|
+
const mediaRef = useRef<HTMLVideoElement>(null);
|
|
642
|
+
const controller = usePanoMediaController(mediaRef);
|
|
643
|
+
// Inside PanoDialog:
|
|
644
|
+
<video ref={mediaRef} src="/movie.mp4" controls playsInline />
|
|
645
|
+
<PanoMediaControls controller={controller} />
|
|
646
|
+
// For a scene video, inside PanoOverlay:
|
|
647
|
+
<PanoSpatialControls position={{ yaw: 55, pitch: -14 }} width={28} distance={10}>
|
|
648
|
+
<PanoMediaControls controller={controller} />
|
|
649
|
+
</PanoSpatialControls>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
`position` uses hotspot yaw/pitch degrees, `width` is angular width (28° by default), and `distance` defaults to 2. Omit position to place the bar below the initial VR view and keep it stationary. `PanoSpatialControls` must be inside `PanoOverlay`. Use `usePanoViewer().getVideo/subscribeVideo` for the active 360° video controller. `VideoHotspot` now accepts a controller `ref`; synchronize controlled `playing` through `onPlaybackStateChange`. Source and playback lifetime remain owned by the media component.
|
|
653
|
+
|
|
654
|
+
`PanoModelControls` takes an existing `adapter`, `rotateSpeed` (30 degrees/second) and `zoomSpeed` (1.5 times/second). The host retains loading, view state and limits through `canExecute`.
|
|
655
|
+
|
|
656
|
+
VR dialogs overlay translucent double-chevron scroll arrows at the top and bottom edges of the scroll viewport, independently of model controls. Only directions with remaining scroll space are shown. Default opacity is 0.3, increasing to 0.9 on gaze or hover. Nested regions select the nearest scrollable ancestor of the gaze target and retain it while operating arrows. Default dialogs bind their body. For explicit targeting, pass `scrollTargetRef` to `usePanoDialogSurface` or `PanoDialogRoot`. Alternatively place `<PanoScrollControls targetRef={scrollRef} speed={0.2} />` to overlay controls on the target viewport edges. Cross-origin iframe scrolling is unavailable.
|
|
657
|
+
|
|
658
|
+
Each control group accepts an `actions` array mixing built-in IDs and custom actions, plus a children render function for custom layouts. Media IDs: `play/pause/rewind/forward/vol-/vol+/mute/unmute`; model IDs: `left/right/up/down/in/out/reset`; scroll IDs: `up/down/left/right`. Include both play/pause and mute/unmute when filtering dynamic states.
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
<PanoMediaControls controller={controller} actions={["rewind", "play", "pause", "forward"]}>
|
|
662
|
+
{actions => <div className="my-layout">{actions.map(action =>
|
|
663
|
+
<PanoControlButton key={action.id === "pause" ? "play" : action.id} action={action} />
|
|
664
|
+
)}</div>}
|
|
665
|
+
</PanoMediaControls>
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Custom actions have `id`, accessible `label`, optional React `icon`, `disabled` / `enabled()`, one-shot `activate()`, continuous `update(seconds)` and cleanup `end()`. Optional `repeatIntervalMs` spaces held updates instead of running every frame. Keep toggle button keys stable across icon changes to avoid restarting dwell. Use `PanoControlButton` to preserve gaze binding; `PanoControlGroup` accepts custom action arrays directly.
|
|
669
|
+
|
|
670
|
+
Customize explicit groups with `className`, `style`, and `--pano-control-background/color/border/radius/size/gap/padding/focus`. Automatic video controls support the same variables; other automatic controls support the background variable. Supply explicit controls for custom icons or layouts. Explicit groups suppress automatic controls for the same target.
|
|
671
|
+
|
|
672
|
+
Each automatically controlled dialog video has a separate row of circular controls below its picture: backward, play/pause, forward, mute/unmute, then volume down/up where supported. The bar occupies its own layout space without covering the video or following content, scrolls with the video, and follows its container's clipping. Media controls default to 48px buttons with 24px light-blue outline icons, 8px gaps and 8px padding, and can wrap in narrow containers. These defaults apply to automatic video rows and explicit `PanoMediaControls` groups, including audio; existing control CSS variables still override button size, gaps and padding. Leaving VR removes the automatic control slot while preserving the video node and its normal-mode native controls. Before playback, the spatial panel displays the video's poster, or requests a first frame by temporarily hinting `preload="auto"` without playing, seeking, reloading, or creating another video element. If no readable image is available, a visible video placeholder remains. Poster images require same-origin access or CORS permission; unreadable media never taints the panel texture. Preview resources are released and temporary preload settings are restored when the panel closes or leaves VR.
|
|
673
|
+
|
|
674
|
+
After `cursorDwellMs`, play, mute and reset fire once; seek, volume, rotation and zoom continue until gaze leaves. Scrolling advances in paced steps, with a 700 ms pause between updates. Defaults: `seekSpeed=10` seconds/second, `volumeSpeed=0.25` per second, scroll `speed=0.2` viewports/second (0.14 viewports per step at the default interval). Set `PanoScrollControls.repeatIntervalMs` to adjust the repeat interval. Leaving the target clears pending time and requires a fresh dwell. Closing, leaving VR, disabling or losing visibility stops actions. Gaze cannot bypass trusted media-gesture requirements. Volume buttons are omitted when the platform does not support volume adjustment (including iOS); mute remains available. On supported platforms, volume buttons disable at the corresponding volume limit.
|
|
675
|
+
|
|
676
|
+
Customize scroll arrows with `--pano-scroll-opacity`, `--pano-scroll-active-opacity`, `--pano-scroll-background` and `--pano-scroll-color`.
|
|
677
|
+
|
|
678
|
+
Use `/overlays` to compare dialog integration styles, `/spatial-controls` for separate scene-video, 360-video, model and scrolling examples, and `/immersive-tour` for the complete combination. `/video` focuses on ordinary 360° playback, quality and captions.
|
|
679
|
+
|
|
680
|
+
### VR input and adaptive controls
|
|
681
|
+
|
|
682
|
+
`WebVR.interactionMode` defaults to `"auto"`: phone and simulated VR use dwell, while WebXR uses explicit controller, hand, or system selection. Set `"gaze"` or `"pointer"` to enforce a host policy. The optional `input` snapshot from `getVRState()` / `subscribeVR()` reports `mode` (`gaze/pointer/paused/null`) and `source` (`head/controller/hand/system/null`) without changing presentation modes.
|
|
683
|
+
|
|
684
|
+
Point at a registered model and hold the trigger to rotate; use the thumbstick vertically to zoom. Over scrollable content, the stick scrolls the nearest container, with explicit scroll targets taking precedence. Model zoom takes precedence over outer scrolling. The stick has a 0.2 dead zone, scrolling is capped at one viewport per second, and zoom defaults to 1.5 times per second. Default directional buttons disappear only when their replacement capability is available. Reset, media, close, and scene navigation remain available. Explicit `actions` and custom render callbacks retain their requested controls.
|
|
685
|
+
|
|
686
|
+
Loss of an active controller pauses operations without enabling dwell. The WebXR session toolbar provides exit and an explicit gaze entry point; only that entry point accepts head dwell while waiting or paused. Host cursor/dwell restrictions, forced input policies, and `chrome={false}` remain authoritative. The first selection after recovery only switches input. Transient system inputs do not cause gaze fallback when absent. Basic hand selection keeps directional controls; two-hand scaling, near touch, and moving panels are outside this version.
|
|
687
|
+
|
|
688
|
+
When Immersive Web Emulator takes ownership of the canvas, the preview fills the browser content area and keeps its display dimensions and aspect ratio aligned with the emulator as the window resizes. It does not request browser fullscreen. Exiting restores the embedded canvas styles. Native WebXR headsets, MobileVR, and non-XR desktop presentation are unchanged.
|