@ericchen1990/pano-view 3.5.0 → 3.6.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.
@@ -0,0 +1,70 @@
1
+ /* Optional dialog appearance. Structural layout remains in the components. */
2
+ .pano-dialog-backdrop {
3
+ background: var(--pano-dialog-backdrop, rgb(0 0 0 / 65%));
4
+ }
5
+
6
+ .pano-dialog-content {
7
+ background: var(--pano-dialog-background, #161b22);
8
+ color: var(--pano-dialog-color, #f4f4f4);
9
+ border: var(--pano-dialog-border, 1px solid #39424e);
10
+ border-radius: var(--pano-dialog-radius, 12px);
11
+ box-shadow: var(--pano-dialog-shadow, 0 16px 64px rgb(0 0 0 / 45%));
12
+ font: inherit;
13
+ }
14
+
15
+ .pano-dialog-header {
16
+ padding: var(--pano-dialog-padding, 20px);
17
+ padding-top: max(var(--pano-dialog-padding, 20px), env(safe-area-inset-top, 0px));
18
+ padding-right: calc(var(--pano-dialog-padding, 20px) + 36px + 16px);
19
+ min-height: calc(max(var(--pano-dialog-padding, 20px), env(safe-area-inset-top, 0px)) + 36px + var(--pano-dialog-padding, 20px));
20
+ }
21
+
22
+ .pano-dialog-title {
23
+ margin: 0;
24
+ font-size: 1.125rem;
25
+ font-weight: 600;
26
+ }
27
+
28
+ .pano-dialog-description {
29
+ margin: 8px 0 0;
30
+ color: var(--pano-dialog-muted-color, #a7b4c4);
31
+ font-size: 0.875rem;
32
+ }
33
+
34
+ .pano-dialog-body {
35
+ padding: 0 var(--pano-dialog-padding, 20px) max(var(--pano-dialog-padding, 20px), env(safe-area-inset-bottom, 0px));
36
+ overscroll-behavior: contain;
37
+ }
38
+
39
+ .pano-dialog-close {
40
+ position: absolute;
41
+ top: max(var(--pano-dialog-padding, 20px), env(safe-area-inset-top, 0px));
42
+ right: var(--pano-dialog-padding, 20px);
43
+ box-sizing: border-box;
44
+ display: grid;
45
+ place-items: center;
46
+ width: 36px;
47
+ height: 36px;
48
+ padding: 0;
49
+ line-height: 1;
50
+ border: 1px solid currentColor;
51
+ border-radius: 6px;
52
+ background: transparent;
53
+ color: inherit;
54
+ font: inherit;
55
+ cursor: pointer;
56
+ }
57
+
58
+ .pano-dialog-close:focus-visible {
59
+ outline: 2px solid var(--pano-dialog-focus-color, #ffb300);
60
+ outline-offset: 2px;
61
+ }
62
+
63
+ .pano-project-popup {
64
+ display: grid;
65
+ gap: 16px;
66
+ }
67
+ .pano-project-popup p { margin: 0; }
68
+ .pano-project-popup img,
69
+ .pano-project-popup video { display: block; width: 100%; height: auto; }
70
+ .pano-project-popup audio { width: 100%; }
package/dist/ui.cjs ADDED
@@ -0,0 +1,195 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/ui.ts
32
+ var ui_exports = {};
33
+ __export(ui_exports, {
34
+ PanoDialog: () => PanoDialog,
35
+ PanoDialogBackdrop: () => PanoDialogBackdrop,
36
+ PanoDialogClose: () => PanoDialogClose,
37
+ PanoDialogContent: () => PanoDialogContent,
38
+ PanoDialogDescription: () => PanoDialogDescription,
39
+ PanoDialogRoot: () => PanoDialogRoot,
40
+ PanoDialogTitle: () => PanoDialogTitle,
41
+ ProjectPopupDialog: () => ProjectPopupDialog
42
+ });
43
+ module.exports = __toCommonJS(ui_exports);
44
+
45
+ // src/ui/dialog.tsx
46
+ var DialogPrimitive = __toESM(require("@radix-ui/react-dialog"));
47
+ var import_react = require("react");
48
+ var import_pano_overlay = require("./index.cjs");
49
+ var import_jsx_runtime = require("react/jsx-runtime");
50
+ function PanoDialogRoot({ open, onOpenChange, children }) {
51
+ (0, import_pano_overlay.usePanoModal)(open);
52
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Root, { open, onOpenChange, children });
53
+ }
54
+ var PanoDialogBackdrop = (0, import_react.forwardRef)(
55
+ function PanoDialogBackdrop2({ style, className, ...props }, ref) {
56
+ const { portalContainer } = (0, import_pano_overlay.usePanoOverlay)();
57
+ if (!portalContainer) return null;
58
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
59
+ DialogPrimitive.Overlay,
60
+ {
61
+ ...props,
62
+ ref,
63
+ "data-pano-dialog-backdrop": "",
64
+ className: ["pano-dialog-backdrop", className].filter(Boolean).join(" "),
65
+ style: { position: "absolute", inset: 0, pointerEvents: "auto", ...style }
66
+ }
67
+ ) });
68
+ }
69
+ );
70
+ var PanoDialogContent = (0, import_react.forwardRef)(
71
+ function PanoDialogContent2({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {
72
+ const { portalContainer, visibleRect } = (0, import_pano_overlay.usePanoOverlay)();
73
+ const [nestedContainer, setNestedContainer] = (0, import_react.useState)(null);
74
+ const returnFocus = (0, import_react.useRef)(null);
75
+ if (!portalContainer) return null;
76
+ const layout = {
77
+ "--pano-dialog-available-width": `${Math.max(0, visibleRect.width - 32)}px`,
78
+ "--pano-dialog-available-height": `${Math.max(0, visibleRect.height - 32)}px`,
79
+ position: "absolute",
80
+ top: visibleRect.top + visibleRect.height / 2,
81
+ left: visibleRect.left + visibleRect.width / 2,
82
+ transform: "translate(-50%, -50%)",
83
+ width: "min(var(--pano-dialog-width, 640px), var(--pano-dialog-available-width))",
84
+ maxHeight: "var(--pano-dialog-available-height)",
85
+ boxSizing: "border-box",
86
+ display: "flex",
87
+ flexDirection: "column",
88
+ overflow: "auto",
89
+ pointerEvents: "auto",
90
+ zIndex: 1,
91
+ ...style
92
+ };
93
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
94
+ DialogPrimitive.Content,
95
+ {
96
+ ...props,
97
+ ref: forwardedRef,
98
+ "data-pano-dialog": "",
99
+ className: ["pano-dialog-content", className].filter(Boolean).join(" "),
100
+ style: layout,
101
+ onOpenAutoFocus: (event) => {
102
+ const active = portalContainer.ownerDocument.activeElement;
103
+ returnFocus.current = active instanceof HTMLElement ? active : null;
104
+ onOpenAutoFocus?.(event);
105
+ },
106
+ onCloseAutoFocus: (event) => {
107
+ onCloseAutoFocus?.(event);
108
+ if (event.defaultPrevented) return;
109
+ event.preventDefault();
110
+ const previous = returnFocus.current;
111
+ const canvas = portalContainer.closest("[data-pano-viewer]")?.querySelector("canvas");
112
+ const target = previous?.isConnected && previous !== previous.ownerDocument.body && !previous.closest("[inert]") ? previous : canvas;
113
+ if (target && !target.closest("[inert]")) target.focus({ preventScroll: true });
114
+ },
115
+ children: [
116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_pano_overlay.PanoOverlayPortalProvider, { container: nestedContainer, children }),
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
118
+ "div",
119
+ {
120
+ "data-pano-dialog-portals": "",
121
+ ref: setNestedContainer,
122
+ style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: 2 }
123
+ }
124
+ )
125
+ ]
126
+ }
127
+ ) });
128
+ }
129
+ );
130
+ var PanoDialogTitle = DialogPrimitive.Title;
131
+ var PanoDialogDescription = DialogPrimitive.Description;
132
+ var PanoDialogClose = DialogPrimitive.Close;
133
+ function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Close", children, className, style }) {
134
+ const descriptionId = (0, import_react.useId)();
135
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(PanoDialogRoot, { open, onOpenChange, children: [
136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogBackdrop, {}),
137
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
138
+ PanoDialogContent,
139
+ {
140
+ className,
141
+ style: { overflow: "hidden", ...style },
142
+ "aria-describedby": description == null ? void 0 : descriptionId,
143
+ children: [
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("header", { className: "pano-dialog-header", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogTitle, { className: "pano-dialog-title", children: title }),
146
+ description == null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanoDialogDescription, { id: descriptionId, className: "pano-dialog-description", children: description })
147
+ ] }) }),
148
+ /* @__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 })
150
+ ]
151
+ }
152
+ )
153
+ ] });
154
+ }
155
+
156
+ // src/ui/project-popup-dialog.tsx
157
+ var import_react2 = require("react");
158
+ var import_jsx_runtime2 = require("react/jsx-runtime");
159
+ function PopupMedia({ block, label, onError }) {
160
+ const ref = (0, import_react2.useRef)(null);
161
+ (0, import_react2.useEffect)(() => {
162
+ const media = ref.current;
163
+ if (media && media.getAttribute("src") !== block.src) {
164
+ media.src = block.src;
165
+ media.load();
166
+ }
167
+ return () => {
168
+ if (!media) return;
169
+ media.pause();
170
+ media.removeAttribute("src");
171
+ media.load();
172
+ };
173
+ }, [block.src]);
174
+ const props = { controls: true, autoPlay: false, preload: "metadata", src: block.src, onError };
175
+ return block.type === "video" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("video", { ...props, ref: (node) => {
176
+ ref.current = node;
177
+ }, playsInline: true, poster: block.poster, "aria-label": label }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("audio", { ...props, ref: (node) => {
178
+ ref.current = node;
179
+ }, "aria-label": label });
180
+ }
181
+ function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }) {
182
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PanoDialog, { open: popup !== null, onOpenChange, title: popup?.title || popup?.id || "", closeLabel, children: popup ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pano-project-popup", children: popup.blocks.map((block) => block.type === "text" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: block.text }, block.id) : block.type === "image" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: block.src, alt: block.alt ?? "", onError: () => onMediaError?.(block) }, `${block.id}:${block.src}`) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PopupMedia, { block, label: popup.title, onError: () => onMediaError?.(block) }, `${block.id}:${block.src}`)) }, popup.id) : null });
183
+ }
184
+ // Annotate the CommonJS export names for ESM import in node:
185
+ 0 && (module.exports = {
186
+ PanoDialog,
187
+ PanoDialogBackdrop,
188
+ PanoDialogClose,
189
+ PanoDialogContent,
190
+ PanoDialogDescription,
191
+ PanoDialogRoot,
192
+ PanoDialogTitle,
193
+ ProjectPopupDialog
194
+ });
195
+ //# sourceMappingURL=ui.cjs.map
@@ -0,0 +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 return <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>{children}</DialogPrimitive.Root>;\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 in PanoViewer.renderOverlay; 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,sBAAiC;AACjC,mBAAuH;AACvH,0BAAwE;AAU/D;AAFF,SAAS,eAAe,EAAE,MAAM,cAAc,SAAS,GAAwB;AACpF,wCAAa,IAAI;AACjB,SAAO,4CAAiB,sBAAhB,EAAqB,MAAY,cAA6B,UAAS;AACjF;AAIO,IAAM,yBAAqB;AAAA,EAChC,SAASA,oBAAmB,EAAE,OAAO,WAAW,GAAG,MAAM,GAAG,KAAK;AAC/D,UAAM,EAAE,gBAAgB,QAAI,oCAAe;AAC3C,QAAI,CAAC,gBAAiB,QAAO;AAC7B,WACE,4CAAiB,wBAAhB,EAAuB,WAAW,iBACjC;AAAA,MAAiB;AAAA,MAAhB;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA,6BAA0B;AAAA,QAC1B,WAAW,CAAC,wBAAwB,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,QACvE,OAAO,EAAE,UAAU,YAAY,OAAO,GAAG,eAAe,QAAQ,GAAG,MAAM;AAAA;AAAA,IAC3E,GACF;AAAA,EAEJ;AACF;AAIO,IAAM,wBAAoB;AAAA,EAC/B,SAASC,mBAAkB,EAAE,UAAU,OAAO,WAAW,iBAAiB,kBAAkB,GAAG,MAAM,GAAG,cAAc;AACpH,UAAM,EAAE,iBAAiB,YAAY,QAAI,oCAAe;AACxD,UAAM,CAAC,iBAAiB,kBAAkB,QAAI,uBAAgC,IAAI;AAClF,UAAM,kBAAc,qBAA2B,IAAI;AACnD,QAAI,CAAC,gBAAiB,QAAO;AAC7B,UAAM,SAAoE;AAAA,MACxE,iCAAiC,GAAG,KAAK,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;AAAA,MACvE,kCAAkC,GAAG,KAAK,IAAI,GAAG,YAAY,SAAS,EAAE,CAAC;AAAA,MACzE,UAAU;AAAA,MACV,KAAK,YAAY,MAAM,YAAY,SAAS;AAAA,MAC5C,MAAM,YAAY,OAAO,YAAY,QAAQ;AAAA,MAC7C,WAAW;AAAA,MACX,OAAO;AAAA,MACP,WAAW;AAAA,MACX,WAAW;AAAA,MACX,SAAS;AAAA,MACT,eAAe;AAAA,MACf,UAAU;AAAA,MACV,eAAe;AAAA,MACf,QAAQ;AAAA,MACR,GAAG;AAAA,IACL;AACA,WACE,4CAAiB,wBAAhB,EAAuB,WAAW,iBACjC;AAAA,MAAiB;AAAA,MAAhB;AAAA,QACE,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,oBAAiB;AAAA,QACjB,WAAW,CAAC,uBAAuB,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,QACtE,OAAO;AAAA,QACP,iBAAiB,CAAC,UAAU;AAC1B,gBAAM,SAAS,gBAAgB,cAAc;AAC7C,sBAAY,UAAU,kBAAkB,cAAc,SAAS;AAC/D,4BAAkB,KAAK;AAAA,QACzB;AAAA,QACA,kBAAkB,CAAC,UAAU;AAC3B,6BAAmB,KAAK;AACxB,cAAI,MAAM,iBAAkB;AAC5B,gBAAM,eAAe;AACrB,gBAAM,WAAW,YAAY;AAC7B,gBAAM,SAAS,gBAAgB,QAAQ,oBAAoB,GAAG,cAAc,QAAQ;AACpF,gBAAM,SAAS,UAAU,eAAe,aAAa,SAAS,cAAc,QAAQ,CAAC,SAAS,QAAQ,SAAS,IAC3G,WAAW;AACf,cAAI,UAAU,CAAC,OAAO,QAAQ,SAAS,EAAG,QAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,QAChF;AAAA,QAEA;AAAA,sDAAC,iDAA0B,WAAW,iBACnC,UACH;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,4BAAyB;AAAA,cACzB,KAAK;AAAA,cACL,OAAO,EAAE,UAAU,YAAY,OAAO,GAAG,eAAe,QAAQ,QAAQ,EAAE;AAAA;AAAA,UAC5E;AAAA;AAAA;AAAA,IACF,GACF;AAAA,EAEJ;AACF;AAEO,IAAM,kBAAkC;AACxC,IAAM,wBAAwC;AAC9C,IAAM,kBAAkC;AAcxC,SAAS,WAAW,EAAE,MAAM,cAAc,OAAO,aAAa,aAAa,SAAS,UAAU,WAAW,MAAM,GAAoB;AACxI,QAAM,oBAAgB,oBAAM;AAC5B,SACE,6CAAC,kBAAe,MAAY,cAC1B;AAAA,gDAAC,sBAAmB;AAAA,IACpB;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,OAAO,EAAE,UAAU,UAAU,GAAG,MAAM;AAAA,QACtC,oBAAkB,eAAe,OAAO,SAAY;AAAA,QAEpD;AAAA,sDAAC,YAAO,WAAU,sBAAqB,OAAO,EAAE,YAAY,EAAE,GAC5D,uDAAC,SACC;AAAA,wDAAC,mBAAgB,WAAU,qBAAqB,iBAAM;AAAA,YACrD,eAAe,OAAO,OACrB,4CAAC,yBAAsB,IAAI,eAAe,WAAU,2BAA2B,uBAAY;AAAA,aAE/F,GACF;AAAA,UACA,4CAAC,mBAAgB,WAAU,qBAAoB,cAAY,YAAY,MAAK,UAC1E,sDAAC,SAAI,eAAY,QAAO,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SACjI,sDAAC,UAAK,GAAE,sBAAqB,GAC/B,GACF;AAAA,UACA,4CAAC,SAAI,WAAU,oBAAmB,OAAO,EAAE,UAAU,QAAQ,WAAW,EAAE,GAAI,UAAS;AAAA;AAAA;AAAA,IACzF;AAAA,KACF;AAEJ;;;AC7IA,IAAAC,gBAAkC;AAiC5B,IAAAC,sBAAA;AAtBN,SAAS,WAAW,EAAE,OAAO,OAAO,QAAQ,GAIzC;AACD,QAAM,UAAM,sBAAgC,IAAI;AAChD,+BAAU,MAAM;AACd,UAAM,QAAQ,IAAI;AAElB,QAAI,SAAS,MAAM,aAAa,KAAK,MAAM,MAAM,KAAK;AACpD,YAAM,MAAM,MAAM;AAClB,YAAM,KAAK;AAAA,IACb;AACA,WAAO,MAAM;AACX,UAAI,CAAC,MAAO;AACZ,YAAM,MAAM;AACZ,YAAM,gBAAgB,KAAK;AAC3B,YAAM,KAAK;AAAA,IACb;AAAA,EACF,GAAG,CAAC,MAAM,GAAG,CAAC;AACd,QAAM,QAAQ,EAAE,UAAU,MAAM,UAAU,OAAO,SAAS,YAAY,KAAK,MAAM,KAAK,QAAQ;AAC9F,SAAO,MAAM,SAAS,UAClB,6CAAC,WAAO,GAAG,OAAO,KAAK,CAAC,SAAS;AAAE,QAAI,UAAU;AAAA,EAAM,GAAG,aAAW,MAAC,QAAQ,MAAM,QAAQ,cAAY,OAAO,IAC/G,6CAAC,WAAO,GAAG,OAAO,KAAK,CAAC,SAAS;AAAE,QAAI,UAAU;AAAA,EAAM,GAAG,cAAY,OAAO;AACnF;AAGO,SAAS,mBAAmB,EAAE,OAAO,cAAc,YAAY,aAAa,GAA4B;AAC7G,SAAO,6CAAC,cAAW,MAAM,UAAU,MAAM,cAA4B,OAAO,OAAO,SAAS,OAAO,MAAM,IAAI,YAC1G,kBAAQ,6CAAC,SAAI,WAAU,sBACrB,gBAAM,OAAO,IAAI,CAAC,UAAU,MAAM,SAAS,SACxC,6CAAC,OAAiB,OAAO,EAAE,YAAY,YAAY,cAAc,WAAW,GAAI,gBAAM,QAA9E,MAAM,EAA6E,IAC3F,MAAM,SAAS,UACb,6CAAC,SAAqC,KAAK,MAAM,KAAK,KAAK,MAAM,OAAO,IAAI,SAAS,MAAM,eAAe,KAAK,KAArG,GAAG,MAAM,EAAE,IAAI,MAAM,GAAG,EAAgF,IAClH,6CAAC,cAA4C,OAAc,OAAO,MAAM,OAAO,SAAS,MAAM,eAAe,KAAK,KAAjG,GAAG,MAAM,EAAE,IAAI,MAAM,GAAG,EAA4E,CAAE,KAL7E,MAAM,EAMxD,IAAS,MACX;AACF;","names":["PanoDialogBackdrop","PanoDialogContent","import_react","import_jsx_runtime"]}
package/dist/ui.d.mts ADDED
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react';
3
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
4
+
5
+ type PanoDialogRootProps = {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ children: ReactNode;
9
+ };
10
+ declare function PanoDialogRoot({ open, onOpenChange, children }: PanoDialogRootProps): react.JSX.Element;
11
+ type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, "forceMount">;
12
+ declare const PanoDialogBackdrop: react.ForwardRefExoticComponent<PanoDialogBackdropProps & react.RefAttributes<HTMLDivElement>>;
13
+ type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, "forceMount" | "asChild">;
14
+ declare const PanoDialogContent: react.ForwardRefExoticComponent<PanoDialogContentProps & react.RefAttributes<HTMLDivElement>>;
15
+ declare const PanoDialogTitle: react.ForwardRefExoticComponent<DialogPrimitive.DialogTitleProps & react.RefAttributes<HTMLHeadingElement>>;
16
+ declare const PanoDialogDescription: react.ForwardRefExoticComponent<DialogPrimitive.DialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>>;
17
+ declare const PanoDialogClose: react.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
18
+ type PanoDialogTitleProps = ComponentPropsWithoutRef<typeof PanoDialogTitle>;
19
+ type PanoDialogDescriptionProps = ComponentPropsWithoutRef<typeof PanoDialogDescription>;
20
+ type PanoDialogCloseProps = ComponentPropsWithoutRef<typeof PanoDialogClose>;
21
+ type PanoDialogProps = PanoDialogRootProps & {
22
+ title: ReactNode;
23
+ description?: ReactNode;
24
+ closeLabel?: string;
25
+ className?: string;
26
+ style?: CSSProperties;
27
+ };
28
+ /** 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
+
31
+ type ProjectPopupBlock = {
32
+ id: string;
33
+ } & ({
34
+ type: "text";
35
+ text: string;
36
+ } | {
37
+ type: "image";
38
+ src: string;
39
+ alt?: string;
40
+ } | {
41
+ type: "video";
42
+ src: string;
43
+ poster?: string;
44
+ } | {
45
+ type: "audio";
46
+ src: string;
47
+ });
48
+ type ProjectPopupDefinition = {
49
+ id: string;
50
+ title: string;
51
+ blocks: ProjectPopupBlock[];
52
+ };
53
+
54
+ type ProjectPopupDialogProps = {
55
+ popup: ProjectPopupDefinition | null;
56
+ onOpenChange: (open: boolean) => void;
57
+ closeLabel?: string;
58
+ onMediaError?: (block: ProjectPopupBlock) => void;
59
+ };
60
+ /** Render in PanoViewer.renderOverlay; the host owns selection and may replace this UI. */
61
+ declare function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps): react.JSX.Element;
62
+
63
+ export { PanoDialog, PanoDialogBackdrop, type PanoDialogBackdropProps, PanoDialogClose, type PanoDialogCloseProps, PanoDialogContent, type PanoDialogContentProps, PanoDialogDescription, type PanoDialogDescriptionProps, type PanoDialogProps, PanoDialogRoot, type PanoDialogRootProps, PanoDialogTitle, type PanoDialogTitleProps, ProjectPopupDialog, type ProjectPopupDialogProps };
package/dist/ui.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react';
3
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
4
+
5
+ type PanoDialogRootProps = {
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ children: ReactNode;
9
+ };
10
+ declare function PanoDialogRoot({ open, onOpenChange, children }: PanoDialogRootProps): react.JSX.Element;
11
+ type PanoDialogBackdropProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>, "forceMount">;
12
+ declare const PanoDialogBackdrop: react.ForwardRefExoticComponent<PanoDialogBackdropProps & react.RefAttributes<HTMLDivElement>>;
13
+ type PanoDialogContentProps = Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, "forceMount" | "asChild">;
14
+ declare const PanoDialogContent: react.ForwardRefExoticComponent<PanoDialogContentProps & react.RefAttributes<HTMLDivElement>>;
15
+ declare const PanoDialogTitle: react.ForwardRefExoticComponent<DialogPrimitive.DialogTitleProps & react.RefAttributes<HTMLHeadingElement>>;
16
+ declare const PanoDialogDescription: react.ForwardRefExoticComponent<DialogPrimitive.DialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>>;
17
+ declare const PanoDialogClose: react.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
18
+ type PanoDialogTitleProps = ComponentPropsWithoutRef<typeof PanoDialogTitle>;
19
+ type PanoDialogDescriptionProps = ComponentPropsWithoutRef<typeof PanoDialogDescription>;
20
+ type PanoDialogCloseProps = ComponentPropsWithoutRef<typeof PanoDialogClose>;
21
+ type PanoDialogProps = PanoDialogRootProps & {
22
+ title: ReactNode;
23
+ description?: ReactNode;
24
+ closeLabel?: string;
25
+ className?: string;
26
+ style?: CSSProperties;
27
+ };
28
+ /** 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
+
31
+ type ProjectPopupBlock = {
32
+ id: string;
33
+ } & ({
34
+ type: "text";
35
+ text: string;
36
+ } | {
37
+ type: "image";
38
+ src: string;
39
+ alt?: string;
40
+ } | {
41
+ type: "video";
42
+ src: string;
43
+ poster?: string;
44
+ } | {
45
+ type: "audio";
46
+ src: string;
47
+ });
48
+ type ProjectPopupDefinition = {
49
+ id: string;
50
+ title: string;
51
+ blocks: ProjectPopupBlock[];
52
+ };
53
+
54
+ type ProjectPopupDialogProps = {
55
+ popup: ProjectPopupDefinition | null;
56
+ onOpenChange: (open: boolean) => void;
57
+ closeLabel?: string;
58
+ onMediaError?: (block: ProjectPopupBlock) => void;
59
+ };
60
+ /** Render in PanoViewer.renderOverlay; the host owns selection and may replace this UI. */
61
+ declare function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps): react.JSX.Element;
62
+
63
+ export { PanoDialog, PanoDialogBackdrop, type PanoDialogBackdropProps, PanoDialogClose, type PanoDialogCloseProps, PanoDialogContent, type PanoDialogContentProps, PanoDialogDescription, type PanoDialogDescriptionProps, type PanoDialogProps, PanoDialogRoot, type PanoDialogRootProps, PanoDialogTitle, type PanoDialogTitleProps, ProjectPopupDialog, type ProjectPopupDialogProps };
package/dist/ui.mjs ADDED
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ // src/ui/dialog.tsx
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { forwardRef, useId, useRef, useState } from "react";
6
+ import { PanoOverlayPortalProvider, usePanoModal, usePanoOverlay } from "./index.mjs";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function PanoDialogRoot({ open, onOpenChange, children }) {
9
+ usePanoModal(open);
10
+ return /* @__PURE__ */ jsx(DialogPrimitive.Root, { open, onOpenChange, children });
11
+ }
12
+ var PanoDialogBackdrop = forwardRef(
13
+ function PanoDialogBackdrop2({ style, className, ...props }, ref) {
14
+ const { portalContainer } = usePanoOverlay();
15
+ if (!portalContainer) return null;
16
+ return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
17
+ DialogPrimitive.Overlay,
18
+ {
19
+ ...props,
20
+ ref,
21
+ "data-pano-dialog-backdrop": "",
22
+ className: ["pano-dialog-backdrop", className].filter(Boolean).join(" "),
23
+ style: { position: "absolute", inset: 0, pointerEvents: "auto", ...style }
24
+ }
25
+ ) });
26
+ }
27
+ );
28
+ var PanoDialogContent = forwardRef(
29
+ function PanoDialogContent2({ children, style, className, onOpenAutoFocus, onCloseAutoFocus, ...props }, forwardedRef) {
30
+ const { portalContainer, visibleRect } = usePanoOverlay();
31
+ const [nestedContainer, setNestedContainer] = useState(null);
32
+ const returnFocus = useRef(null);
33
+ if (!portalContainer) return null;
34
+ const layout = {
35
+ "--pano-dialog-available-width": `${Math.max(0, visibleRect.width - 32)}px`,
36
+ "--pano-dialog-available-height": `${Math.max(0, visibleRect.height - 32)}px`,
37
+ position: "absolute",
38
+ top: visibleRect.top + visibleRect.height / 2,
39
+ left: visibleRect.left + visibleRect.width / 2,
40
+ transform: "translate(-50%, -50%)",
41
+ width: "min(var(--pano-dialog-width, 640px), var(--pano-dialog-available-width))",
42
+ maxHeight: "var(--pano-dialog-available-height)",
43
+ boxSizing: "border-box",
44
+ display: "flex",
45
+ flexDirection: "column",
46
+ overflow: "auto",
47
+ pointerEvents: "auto",
48
+ zIndex: 1,
49
+ ...style
50
+ };
51
+ return /* @__PURE__ */ jsx(DialogPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxs(
52
+ DialogPrimitive.Content,
53
+ {
54
+ ...props,
55
+ ref: forwardedRef,
56
+ "data-pano-dialog": "",
57
+ className: ["pano-dialog-content", className].filter(Boolean).join(" "),
58
+ style: layout,
59
+ onOpenAutoFocus: (event) => {
60
+ const active = portalContainer.ownerDocument.activeElement;
61
+ returnFocus.current = active instanceof HTMLElement ? active : null;
62
+ onOpenAutoFocus?.(event);
63
+ },
64
+ onCloseAutoFocus: (event) => {
65
+ onCloseAutoFocus?.(event);
66
+ if (event.defaultPrevented) return;
67
+ event.preventDefault();
68
+ const previous = returnFocus.current;
69
+ const canvas = portalContainer.closest("[data-pano-viewer]")?.querySelector("canvas");
70
+ const target = previous?.isConnected && previous !== previous.ownerDocument.body && !previous.closest("[inert]") ? previous : canvas;
71
+ if (target && !target.closest("[inert]")) target.focus({ preventScroll: true });
72
+ },
73
+ children: [
74
+ /* @__PURE__ */ jsx(PanoOverlayPortalProvider, { container: nestedContainer, children }),
75
+ /* @__PURE__ */ jsx(
76
+ "div",
77
+ {
78
+ "data-pano-dialog-portals": "",
79
+ ref: setNestedContainer,
80
+ style: { position: "absolute", inset: 0, pointerEvents: "none", zIndex: 2 }
81
+ }
82
+ )
83
+ ]
84
+ }
85
+ ) });
86
+ }
87
+ );
88
+ var PanoDialogTitle = DialogPrimitive.Title;
89
+ var PanoDialogDescription = DialogPrimitive.Description;
90
+ var PanoDialogClose = DialogPrimitive.Close;
91
+ function PanoDialog({ open, onOpenChange, title, description, closeLabel = "Close", children, className, style }) {
92
+ const descriptionId = useId();
93
+ return /* @__PURE__ */ jsxs(PanoDialogRoot, { open, onOpenChange, children: [
94
+ /* @__PURE__ */ jsx(PanoDialogBackdrop, {}),
95
+ /* @__PURE__ */ jsxs(
96
+ PanoDialogContent,
97
+ {
98
+ className,
99
+ style: { overflow: "hidden", ...style },
100
+ "aria-describedby": description == null ? void 0 : descriptionId,
101
+ children: [
102
+ /* @__PURE__ */ jsx("header", { className: "pano-dialog-header", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxs("div", { children: [
103
+ /* @__PURE__ */ jsx(PanoDialogTitle, { className: "pano-dialog-title", children: title }),
104
+ description == null ? null : /* @__PURE__ */ jsx(PanoDialogDescription, { id: descriptionId, className: "pano-dialog-description", children: description })
105
+ ] }) }),
106
+ /* @__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 })
108
+ ]
109
+ }
110
+ )
111
+ ] });
112
+ }
113
+
114
+ // src/ui/project-popup-dialog.tsx
115
+ import { useEffect, useRef as useRef2 } from "react";
116
+ import { jsx as jsx2 } from "react/jsx-runtime";
117
+ function PopupMedia({ block, label, onError }) {
118
+ const ref = useRef2(null);
119
+ useEffect(() => {
120
+ const media = ref.current;
121
+ if (media && media.getAttribute("src") !== block.src) {
122
+ media.src = block.src;
123
+ media.load();
124
+ }
125
+ return () => {
126
+ if (!media) return;
127
+ media.pause();
128
+ media.removeAttribute("src");
129
+ media.load();
130
+ };
131
+ }, [block.src]);
132
+ const props = { controls: true, autoPlay: false, preload: "metadata", src: block.src, onError };
133
+ return block.type === "video" ? /* @__PURE__ */ jsx2("video", { ...props, ref: (node) => {
134
+ ref.current = node;
135
+ }, playsInline: true, poster: block.poster, "aria-label": label }) : /* @__PURE__ */ jsx2("audio", { ...props, ref: (node) => {
136
+ ref.current = node;
137
+ }, "aria-label": label });
138
+ }
139
+ function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }) {
140
+ return /* @__PURE__ */ jsx2(PanoDialog, { open: popup !== null, onOpenChange, title: popup?.title || popup?.id || "", closeLabel, children: popup ? /* @__PURE__ */ jsx2("div", { className: "pano-project-popup", children: popup.blocks.map((block) => block.type === "text" ? /* @__PURE__ */ jsx2("p", { style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: block.text }, block.id) : block.type === "image" ? /* @__PURE__ */ jsx2("img", { src: block.src, alt: block.alt ?? "", onError: () => onMediaError?.(block) }, `${block.id}:${block.src}`) : /* @__PURE__ */ jsx2(PopupMedia, { block, label: popup.title, onError: () => onMediaError?.(block) }, `${block.id}:${block.src}`)) }, popup.id) : null });
141
+ }
142
+ export {
143
+ PanoDialog,
144
+ PanoDialogBackdrop,
145
+ PanoDialogClose,
146
+ PanoDialogContent,
147
+ PanoDialogDescription,
148
+ PanoDialogRoot,
149
+ PanoDialogTitle,
150
+ ProjectPopupDialog
151
+ };
152
+ //# sourceMappingURL=ui.mjs.map
@@ -0,0 +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 return <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>{children}</DialogPrimitive.Root>;\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 in PanoViewer.renderOverlay; 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"]}