@ericchen1990/pano-view 4.0.1 → 4.1.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 +18 -5
- package/README.zh-CN.md +12 -3
- 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 +4696 -2631
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +148 -9
- package/dist/index.d.ts +148 -9
- package/dist/index.mjs +4660 -2568
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +18 -0
- package/dist/ui.cjs +24 -10
- 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 +26 -12
- package/dist/ui.mjs.map +1 -1
- package/docs/dialogs.md +58 -0
- package/docs/dialogs.zh-CN.md +64 -0
- package/package.json +3 -2
package/dist/styles.css
CHANGED
|
@@ -863,3 +863,21 @@
|
|
|
863
863
|
--pano-overlay-anchor-radius: var(--pano-scene-menu-radius, 8px);
|
|
864
864
|
background: var(--pano-scene-menu-background, rgba(16, 16, 16, 0.72));
|
|
865
865
|
}
|
|
866
|
+
|
|
867
|
+
.pano-control-group { display:flex; flex-wrap:wrap; justify-content:center; gap:var(--pano-control-gap,12px); padding:var(--pano-control-padding,12px); }
|
|
868
|
+
.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; }
|
|
869
|
+
.pano-control-button svg { width:32px; height:32px; }
|
|
870
|
+
.pano-control-button:disabled { opacity:.4; cursor:default; }
|
|
871
|
+
.pano-control-button:focus-visible { outline:3px solid var(--pano-control-focus,#ffb300); outline-offset:2px; }
|
|
872
|
+
|
|
873
|
+
.pano-scroll-controls { display:block; }
|
|
874
|
+
.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; }
|
|
875
|
+
.pano-scroll-controls .pano-control-button:hover,
|
|
876
|
+
.pano-scroll-controls .pano-control-button:focus-visible,
|
|
877
|
+
.pano-scroll-controls [data-pano-gaze-active] { opacity:var(--pano-scroll-active-opacity,.9); }
|
|
878
|
+
.pano-scroll-controls [data-scroll-direction="up"] { top:6px; left:50%; transform:translateX(-50%); }
|
|
879
|
+
.pano-scroll-controls [data-scroll-direction="down"] { bottom:6px; left:50%; transform:translateX(-50%); }
|
|
880
|
+
.pano-scroll-controls [data-scroll-direction="left"],
|
|
881
|
+
.pano-scroll-controls [data-scroll-direction="right"] { width:44px; top:50%; transform:translateY(-50%); }
|
|
882
|
+
.pano-scroll-controls [data-scroll-direction="left"] { left:6px; }
|
|
883
|
+
.pano-scroll-controls [data-scroll-direction="right"] { right:6px; }
|
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,9 +144,9 @@ 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,
|
|
@@ -146,7 +160,7 @@ function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Clos
|
|
|
146
160
|
description == null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogDescription, { id: descriptionId, className: "pano-dialog-description", children: description })
|
|
147
161
|
] }) }),
|
|
148
162
|
/* @__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 })
|
|
163
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-pano-scroll-viewport": "", className: "pano-dialog-body", style: { overflow: "auto", minHeight: 0 }, children })
|
|
150
164
|
]
|
|
151
165
|
}
|
|
152
166
|
)
|
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={{ 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 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,9 +102,9 @@ 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,
|
|
@@ -104,7 +118,7 @@ function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Clos
|
|
|
104
118
|
description == null ? null : /* @__PURE__ */ jsx(PanoDialogDescription, { id: descriptionId, className: "pano-dialog-description", children: description })
|
|
105
119
|
] }) }),
|
|
106
120
|
/* @__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 })
|
|
121
|
+
/* @__PURE__ */ jsx("div", { "data-pano-scroll-viewport": "", className: "pano-dialog-body", style: { overflow: "auto", minHeight: 0 }, children })
|
|
108
122
|
]
|
|
109
123
|
}
|
|
110
124
|
)
|
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={{ 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 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
|
@@ -614,3 +614,61 @@ 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
|
+
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.
|
|
625
|
+
|
|
626
|
+
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.
|
|
627
|
+
|
|
628
|
+
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`).
|
|
629
|
+
|
|
630
|
+
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.
|
|
631
|
+
|
|
632
|
+
## VR icon controls and scrolling
|
|
633
|
+
|
|
634
|
+
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`.
|
|
635
|
+
|
|
636
|
+
Exports: `PanoMediaControls`, `PanoModelControls`, `PanoScrollControls`, `PanoControlGroup`, `PanoControlButton`, `PanoSpatialControls`, `usePanoMediaController`, and the `PanoMediaController` / `PanoControlAction` types.
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
const mediaRef = useRef<HTMLVideoElement>(null);
|
|
640
|
+
const controller = usePanoMediaController(mediaRef);
|
|
641
|
+
// Inside PanoDialog:
|
|
642
|
+
<video ref={mediaRef} src="/movie.mp4" controls playsInline />
|
|
643
|
+
<PanoMediaControls controller={controller} />
|
|
644
|
+
// For a scene video, inside PanoOverlay:
|
|
645
|
+
<PanoSpatialControls position={{ yaw: 55, pitch: -14 }} width={28} distance={10}>
|
|
646
|
+
<PanoMediaControls controller={controller} />
|
|
647
|
+
</PanoSpatialControls>
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`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.
|
|
651
|
+
|
|
652
|
+
`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`.
|
|
653
|
+
|
|
654
|
+
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.
|
|
655
|
+
|
|
656
|
+
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.
|
|
657
|
+
|
|
658
|
+
```tsx
|
|
659
|
+
<PanoMediaControls controller={controller} actions={["rewind", "play", "pause", "forward"]}>
|
|
660
|
+
{actions => <div className="my-layout">{actions.map(action =>
|
|
661
|
+
<PanoControlButton key={action.id === "pause" ? "play" : action.id} action={action} />
|
|
662
|
+
)}</div>}
|
|
663
|
+
</PanoMediaControls>
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
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.
|
|
667
|
+
|
|
668
|
+
Customize explicit groups with `className`, `style`, and `--pano-control-background/color/border/radius/size/gap/padding/focus`. Automatic controls support the background variable; supply explicit controls for custom icons or layouts. Explicit groups suppress automatic controls for the same target.
|
|
669
|
+
|
|
670
|
+
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 disable when the platform does not support volume adjustment; mute remains available.
|
|
671
|
+
|
|
672
|
+
Customize scroll arrows with `--pano-scroll-opacity`, `--pano-scroll-active-opacity`, `--pano-scroll-background` and `--pano-scroll-color`.
|
|
673
|
+
|
|
674
|
+
See Playground `/overlays` for custom layouts, scene video, long content, nested scrolling and mouse/keyboard model controls; `/video` includes a 360° spatial control bar.
|
package/docs/dialogs.zh-CN.md
CHANGED
|
@@ -614,3 +614,67 @@ export function DialogHelp() {
|
|
|
614
614
|
| hook 提示缺少 provider | 把自定义 DOM 组件放在 `PanoOverlay` 内;不要在 `PanoViewer` 的父组件调用 Overlay hook。 |
|
|
615
615
|
|
|
616
616
|
接入时应确认普通模式与真实元素全屏、viewer 部分滚出屏幕、键盘开关与焦点恢复、长内容、二级 Portal、媒体清理及目标 Safari/移动设备的表现。最大化浏览器窗口不能替代 Fullscreen API 验证。
|
|
617
|
+
|
|
618
|
+
## VR 空间弹窗与凝视操作
|
|
619
|
+
|
|
620
|
+
`PanoDialog`、组合式 `PanoDialogRoot/Content` 和 `ProjectPopupDialog` 会自动使用同一份内容切换为空间面板。原弹窗右上角的关闭按钮继续可用;面板底部只提供内容操作,不增加关闭或退出 VR 按钮。VR 中热点 Tooltip 使用固定于热点的三维精灵,与场景共用双眼投影和镜片校正,保留显示并位于弹窗下方。手机和模拟 VR 的弹窗通过每眼独立平面图层合成,不经过全景镜片畸变;最后统一应用镜片可见区域遮罩,弹窗不会越过遮罩。
|
|
621
|
+
|
|
622
|
+
第三方弹窗可使用主入口的 `usePanoDialogSurface({ open, onOpenChange })`,将 `contentRef` 连接到内容节点,使用返回的 `portalContainer`、`visibleRect` 和 `presentation`。将返回的 `onInteractOutside` 传给弹窗内容:空间模式和操作 viewer 外的宿主控件(包括进入 VR)时会阻止自动关闭,普通模式仍支持 viewer 内的遮罩点击关闭。空间模式将内容放到 viewer 内的离屏测量区域(保留 DOM 实例),不要对内容调用 `display:none` 或将 portal 移到 `body`。保留可凝视点击的关闭按钮。完整接入参见 Playground 的第三方弹窗示例。
|
|
623
|
+
|
|
624
|
+
宿主模型使用 `usePanoSpatialControls(elementRef, adapter)` 接入一次。`PanoSpatialControlAdapter` 支持 `rotateBy(yawDegrees, pitchDegrees)`、`zoomBy(factor)`、`resetView()`、`canExecute(action)`、`subscribeFrame(invalidate)` 和同一 WebGL 上下文的 `texture`。动作名为 `left/right/up/down/in/out/reset`。仅显示已提供能力;宿主负责限制观察角度、缩放范围及复位状态。独立 Canvas 应在渲染结束后提供可读取的画面;纹理必须属于当前 viewer 的渲染上下文。不会重新加载模型或改变项目 JSON。
|
|
625
|
+
|
|
626
|
+
连续操作在凝视计时完成后开始,移开即停;滚动默认每隔 700 毫秒移动一小步;旋转每秒 30 度、缩放每秒 1.5 倍。关闭、复位、图集切换为单次操作。明确禁用 `cursor` 或将 `cursorDwellMs` 设为零仍会禁用自动凝视。文字输入和虚拟键盘不在支持范围。跨域 iframe、不可读取的 Canvas 或被浏览器阻止的媒体需要退出 VR 后查看或授权;凝视不能替代浏览器要求的可信用户手势。
|
|
627
|
+
|
|
628
|
+
空间窗口默认距视点约 2 米,外框按宽不超过 32°、高不超过 40°的视角缩放,避免长内容撑满双眼视野。关闭按钮视觉尺寸在普通模式和 VR 中均为 36px;VR 使用至少 56px 的不可见凝视命中范围。原生捕获会完整检测 `content="drawable"` 和绘制 API;旧实验 API 使用兼容捕获。可通过内容节点的 `data-pano-spatial-state`(`loading`、`ready`、`error`)观察捕获状态。
|
|
629
|
+
|
|
630
|
+
`PanoOverlay` / `renderOverlay` 内的屏幕平面 UI 在 VR 中隐藏且不可聚焦,退出后恢复。Portal 容器保持挂载和可测量,以便已注册的弹窗继续渲染为空间面板。内置 VR 会话控制仍可使用。
|
|
631
|
+
|
|
632
|
+
## VR 图标控件与滚动
|
|
633
|
+
|
|
634
|
+
这些新增控件只在 VR 模式显示;退出后隐藏且不可聚焦。普通模式继续使用视频原生控件、鼠标模型交互和原生滚动。无需改变项目 JSON。
|
|
635
|
+
|
|
636
|
+
主入口导出 `PanoMediaControls`、`PanoModelControls`、`PanoScrollControls`、`PanoControlGroup`、`PanoControlButton`、`PanoSpatialControls`、`usePanoMediaController`,以及 `PanoMediaController`、`PanoControlAction` 类型。导入 `@ericchen1990/pano-view/styles.css`。
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
const mediaRef = useRef<HTMLVideoElement>(null);
|
|
640
|
+
const controller = usePanoMediaController(mediaRef);
|
|
641
|
+
// 在 PanoDialog 内;同一个 video 保留原生控件供普通模式使用。
|
|
642
|
+
<video ref={mediaRef} src="/movie.mp4" controls playsInline />
|
|
643
|
+
<PanoMediaControls controller={controller} />
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
场景内按钮组放在 `PanoOverlay` 内,使用 `PanoSpatialControls` 将 DOM 内容呈现为空间面板:
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
<PanoOverlay>
|
|
650
|
+
<PanoSpatialControls position={{ yaw: 55, pitch: -14 }} width={28} distance={10}>
|
|
651
|
+
<PanoMediaControls controller={controller} />
|
|
652
|
+
</PanoSpatialControls>
|
|
653
|
+
</PanoOverlay>
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
`position` 使用与热点相同的 yaw/pitch 度数;`width` 是角宽(默认 28°),`distance` 默认 2。省略 position 时,面板出现在进入 VR 时的视线下方并保持位置。面板应位于内容旁边,避免遮挡视频。360° 视频可通过 `usePanoViewer()` 的 `getVideo` / `subscribeVideo` 获取 controller;订阅后用同一组件。`VideoHotspot` 的 `ref` 返回 controller,配合 `onPlaybackStateChange` 同步宿主的 `playing` 状态。关闭、切源和卸载仍遵循媒体本身的生命周期。
|
|
657
|
+
|
|
658
|
+
`PanoModelControls` 接收已有 `adapter`,支持 `rotateSpeed`(默认每秒 30°)、`zoomSpeed`(默认每秒 1.5 倍);不会加载模型或接管宿主视角状态。宿主通过 `canExecute` 限制角度和缩放范围。
|
|
659
|
+
|
|
660
|
+
默认 VR 弹窗自动提供半透明滚动图标,使用双箭头,贴在可滚动区域的上下边缘,并可与模型操作同时存在。仅显示仍可滚动的方向;没有溢出时隐藏。默认不透明度为 0.3,凝视或悬停时提升至 0.9。默认弹窗绑定正文;嵌套内容可自动选择最近滚动区域。第三方弹窗可向 `usePanoDialogSurface({ open, onOpenChange, scrollTargetRef })` 传入滚动容器 ref;组合弹窗可传给 `PanoDialogRoot`。显式绑定优先于自动选择。也可自行添加 `<PanoScrollControls targetRef={scrollRef} speed={0.2} />`,组件会将按钮叠加到目标滚动区域边缘。跨域 iframe 的内部页面无法访问,仍需退出 VR 操作。
|
|
661
|
+
|
|
662
|
+
所有操作组支持 `actions` 数组:内置动作 ID 与自定义动作可混合排序。媒体 ID 为 `play/pause/rewind/forward/vol-/vol+/mute/unmute`,模型 ID 为 `left/right/up/down/in/out/reset`,滚动 ID 为 `up/down/left/right`。播放与静音 ID 随状态变化;筛选时同时包含两种状态。
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
<PanoMediaControls controller={controller} actions={["rewind", "play", "pause", "forward"]}>
|
|
666
|
+
{actions => <div className="my-layout">{actions.map(action =>
|
|
667
|
+
<PanoControlButton key={action.id === "pause" ? "play" : action.id} action={action} />
|
|
668
|
+
)}</div>}
|
|
669
|
+
</PanoMediaControls>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
自定义动作包含 `id`、无障碍 `label`、可选 React `icon`、`disabled` / `enabled()`、单次 `activate()`、连续 `update(seconds)` 和停止 `end()`;可选 `repeatIntervalMs` 将连续动作改为间隔执行。使用 `PanoControlButton` 保留凝视绑定;切换播放/静音图标时保持按钮 key 稳定,防止重新计时。`PanoControlGroup` 可直接渲染自定义动作数组。
|
|
673
|
+
|
|
674
|
+
可通过 `className`、`style` 和 `--pano-control-background/color/border/radius/size/gap/padding/focus` CSS 变量定制显式按钮组。自动操作区支持 background 变量;完整布局或图标替换需显式按钮组。同一目标的显式媒体、模型或滚动组会替代自动操作组。
|
|
675
|
+
|
|
676
|
+
凝视完成 `cursorDwellMs` 后,播放、静音、复位执行一次;跳转、音量和模型持续执行;滚动每隔 700 毫秒执行一小步,移开即停。默认跳转每秒 10 秒(`seekSpeed`),音量每秒 0.25(`volumeSpeed`),滚动每秒 0.2 个可见区域(`speed`),每步默认移动 0.14 个可见区域。`PanoScrollControls.repeatIntervalMs` 可调整重复间隔;间隔计时不受组件重新渲染影响,移开会清零,重新凝视需重新完成 dwell。关闭、退出 VR、禁用和失焦会停止操作。凝视不能替代浏览器要求的可信用户手势;音量不可调整的平台禁用音量按钮,仍可静音。
|
|
677
|
+
|
|
678
|
+
通过 `--pano-scroll-opacity`、`--pano-scroll-active-opacity`、`--pano-scroll-background` 和 `--pano-scroll-color` 定制滚动按钮。
|
|
679
|
+
|
|
680
|
+
在 Playground `/overlays` 查看长正文、嵌套滚动、模型鼠标/键盘操作、场景视频和自定义主题;`/video` 展示 360° 视频空间控制条。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericchen1990/pano-view",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "React components for panoramic viewing experiences.",
|
|
5
5
|
"author": "Eric Chen",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@radix-ui/react-dialog": "^1.3.1",
|
|
66
66
|
"@types/three": "^0.185.0",
|
|
67
|
-
"howler": "^2.2.4"
|
|
67
|
+
"howler": "^2.2.4",
|
|
68
|
+
"modern-screenshot": "4.7.0"
|
|
68
69
|
},
|
|
69
70
|
"peerDependencies": {
|
|
70
71
|
"@react-three/drei": "^10.0.0",
|