@ericchen1990/pano-view 3.8.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +745 -97
- package/README.zh-CN.md +606 -56
- package/dist/data.cjs +19 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.mts +17 -1
- package/dist/data.d.ts +17 -1
- package/dist/data.mjs +19 -0
- package/dist/data.mjs.map +1 -1
- package/dist/index.cjs +2630 -819
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +218 -6
- package/dist/index.d.ts +218 -6
- package/dist/index.mjs +2533 -711
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +282 -1
- package/dist/ui.cjs.map +1 -1
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.mjs.map +1 -1
- package/docs/dialogs.md +51 -41
- package/docs/dialogs.zh-CN.md +51 -41
- package/package.json +1 -1
package/dist/styles.css
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
.pano-video-controls,
|
|
9
9
|
.pano-video-chrome-button,
|
|
10
10
|
.pano-video-chrome-menu,
|
|
11
|
-
.pano-video-volume-popover
|
|
11
|
+
.pano-video-volume-popover,
|
|
12
|
+
.pano-scene-menu,
|
|
13
|
+
.pano-scene-menu-body,
|
|
14
|
+
.pano-scene-menu-list,
|
|
15
|
+
.pano-scene-menu-item,
|
|
16
|
+
.pano-scene-menu-thumb,
|
|
17
|
+
.pano-scene-menu-label,
|
|
18
|
+
.pano-scene-menu-control {
|
|
12
19
|
box-sizing: border-box;
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -215,6 +222,14 @@
|
|
|
215
222
|
.pano-webvr-toolbar-actions {
|
|
216
223
|
display: flex;
|
|
217
224
|
gap: 0.5rem;
|
|
225
|
+
opacity: 1;
|
|
226
|
+
pointer-events: auto;
|
|
227
|
+
transition: opacity 300ms ease;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.pano-webvr-toolbar-actions-hidden {
|
|
231
|
+
opacity: 0;
|
|
232
|
+
pointer-events: none;
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
.pano-webvr-portrait-prompt {
|
|
@@ -577,3 +592,269 @@
|
|
|
577
592
|
.pano-map-dialog-body {
|
|
578
593
|
padding: 0 16px 16px;
|
|
579
594
|
}
|
|
595
|
+
|
|
596
|
+
.pano-scene-menu {
|
|
597
|
+
background: transparent;
|
|
598
|
+
display: block;
|
|
599
|
+
pointer-events: none;
|
|
600
|
+
position: absolute;
|
|
601
|
+
transition: transform 240ms ease;
|
|
602
|
+
z-index: 2;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.pano-scene-menu[data-placement="bottom"][data-expanded="false"] {
|
|
606
|
+
transform: translateY(
|
|
607
|
+
calc(100% + var(--pano-scene-menu-dock-inset, 12px))
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.pano-scene-menu[data-placement="top"][data-expanded="false"] {
|
|
612
|
+
transform: translateY(
|
|
613
|
+
calc(-100% - var(--pano-scene-menu-dock-inset, 12px))
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.pano-scene-menu[data-placement="left"][data-expanded="false"] {
|
|
618
|
+
transform: translateX(
|
|
619
|
+
calc(-100% - var(--pano-scene-menu-dock-inset, 12px))
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.pano-scene-menu[data-placement="right"][data-expanded="false"] {
|
|
624
|
+
transform: translateX(
|
|
625
|
+
calc(100% + var(--pano-scene-menu-dock-inset, 12px))
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
@media (prefers-reduced-motion: reduce) {
|
|
630
|
+
.pano-scene-menu {
|
|
631
|
+
transition: none;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.pano-scene-menu-body {
|
|
636
|
+
align-items: center;
|
|
637
|
+
background: var(--pano-scene-menu-background, rgba(16, 16, 16, 0.72));
|
|
638
|
+
border-radius: var(--pano-scene-menu-radius, 8px);
|
|
639
|
+
display: flex;
|
|
640
|
+
gap: var(--pano-scene-menu-gap, 8px);
|
|
641
|
+
min-height: 0;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
padding: var(--pano-scene-menu-padding, 8px 10px);
|
|
644
|
+
pointer-events: auto;
|
|
645
|
+
width: 100%;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.pano-scene-menu[data-orientation="horizontal"] .pano-scene-menu-body {
|
|
649
|
+
flex-direction: row;
|
|
650
|
+
height: 100%;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.pano-scene-menu[data-orientation="vertical"] .pano-scene-menu-body {
|
|
654
|
+
flex-direction: column;
|
|
655
|
+
height: 100%;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.pano-scene-menu-list {
|
|
659
|
+
display: flex;
|
|
660
|
+
flex: 1 1 auto;
|
|
661
|
+
gap: var(--pano-scene-menu-gap, 8px);
|
|
662
|
+
min-height: 0;
|
|
663
|
+
min-width: 0;
|
|
664
|
+
scrollbar-width: thin;
|
|
665
|
+
-webkit-overflow-scrolling: touch;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.pano-scene-menu[data-align="center"] .pano-scene-menu-list {
|
|
669
|
+
justify-content: safe center;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.pano-scene-menu[data-align="end"] .pano-scene-menu-list {
|
|
673
|
+
justify-content: safe flex-end;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.pano-scene-menu[data-align="start"] .pano-scene-menu-list {
|
|
677
|
+
justify-content: flex-start;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.pano-scene-menu-track {
|
|
681
|
+
display: flex;
|
|
682
|
+
flex: 0 0 auto;
|
|
683
|
+
gap: var(--pano-scene-menu-gap, 8px);
|
|
684
|
+
min-height: 0;
|
|
685
|
+
min-width: 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.pano-scene-menu[data-orientation="horizontal"] .pano-scene-menu-track {
|
|
689
|
+
flex-direction: row;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.pano-scene-menu[data-orientation="vertical"] .pano-scene-menu-track {
|
|
693
|
+
flex-direction: column;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.pano-scene-menu[data-orientation="horizontal"] .pano-scene-menu-list {
|
|
697
|
+
flex-direction: row;
|
|
698
|
+
overflow-x: auto;
|
|
699
|
+
overflow-y: hidden;
|
|
700
|
+
overscroll-behavior-x: contain;
|
|
701
|
+
touch-action: pan-x;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.pano-scene-menu[data-orientation="vertical"] .pano-scene-menu-list {
|
|
705
|
+
flex-direction: column;
|
|
706
|
+
overflow-x: hidden;
|
|
707
|
+
overflow-y: auto;
|
|
708
|
+
overscroll-behavior-y: contain;
|
|
709
|
+
touch-action: pan-y;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.pano-scene-menu[data-drag-motion="custom"] .pano-scene-menu-list {
|
|
713
|
+
overflow: hidden;
|
|
714
|
+
overscroll-behavior: none;
|
|
715
|
+
touch-action: none;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.pano-scene-menu-item {
|
|
719
|
+
appearance: none;
|
|
720
|
+
background: transparent;
|
|
721
|
+
border: 0;
|
|
722
|
+
color: inherit;
|
|
723
|
+
cursor: pointer;
|
|
724
|
+
display: block;
|
|
725
|
+
flex: 0 0 auto;
|
|
726
|
+
font: inherit;
|
|
727
|
+
height: var(--pano-scene-menu-item-height, 80px);
|
|
728
|
+
padding: 0;
|
|
729
|
+
width: var(--pano-scene-menu-item-width, 128px);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.pano-scene-menu-item:focus-visible,
|
|
733
|
+
.pano-scene-menu-control:focus-visible {
|
|
734
|
+
outline: 2px solid var(--pano-scene-menu-selection-color, #ffffff);
|
|
735
|
+
outline-offset: 2px;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.pano-scene-menu-thumb {
|
|
739
|
+
border-radius: var(--pano-scene-menu-radius, 8px);
|
|
740
|
+
display: block;
|
|
741
|
+
height: 100%;
|
|
742
|
+
overflow: hidden;
|
|
743
|
+
position: relative;
|
|
744
|
+
width: 100%;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.pano-scene-menu-thumb img {
|
|
748
|
+
display: block;
|
|
749
|
+
height: 100%;
|
|
750
|
+
object-fit: cover;
|
|
751
|
+
pointer-events: none;
|
|
752
|
+
user-select: none;
|
|
753
|
+
width: 100%;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.pano-scene-menu-item[data-active="true"] .pano-scene-menu-thumb::after {
|
|
757
|
+
border: var(--pano-scene-menu-selection-width, 2px) solid
|
|
758
|
+
var(--pano-scene-menu-selection-color, #ffffff);
|
|
759
|
+
border-radius: inherit;
|
|
760
|
+
box-sizing: border-box;
|
|
761
|
+
content: "";
|
|
762
|
+
inset: 0;
|
|
763
|
+
pointer-events: none;
|
|
764
|
+
position: absolute;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.pano-scene-menu-label {
|
|
768
|
+
background: var(--pano-scene-menu-label-background, rgba(0, 0, 0, 0.55));
|
|
769
|
+
bottom: 0;
|
|
770
|
+
color: var(--pano-scene-menu-label-color, #ffffff);
|
|
771
|
+
font-size: var(--pano-scene-menu-font-size, 12px);
|
|
772
|
+
left: 0;
|
|
773
|
+
line-height: 1.3;
|
|
774
|
+
overflow: hidden;
|
|
775
|
+
padding: 0.25rem 0.4rem;
|
|
776
|
+
position: absolute;
|
|
777
|
+
right: 0;
|
|
778
|
+
text-align: var(--pano-scene-menu-label-align, start);
|
|
779
|
+
text-overflow: ellipsis;
|
|
780
|
+
white-space: nowrap;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.pano-scene-menu-control {
|
|
784
|
+
align-items: center;
|
|
785
|
+
appearance: none;
|
|
786
|
+
background: var(--pano-scene-menu-control-background, rgba(255, 255, 255, 0.12));
|
|
787
|
+
border: 0;
|
|
788
|
+
border-radius: var(--pano-scene-menu-radius, 8px);
|
|
789
|
+
color: var(--pano-scene-menu-control-color, #ffffff);
|
|
790
|
+
cursor: pointer;
|
|
791
|
+
display: inline-flex;
|
|
792
|
+
flex: 0 0 auto;
|
|
793
|
+
font: inherit;
|
|
794
|
+
height: 2rem;
|
|
795
|
+
justify-content: center;
|
|
796
|
+
min-height: 2rem;
|
|
797
|
+
min-width: 2rem;
|
|
798
|
+
padding: 0;
|
|
799
|
+
width: 2rem;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.pano-scene-menu-control:disabled {
|
|
803
|
+
cursor: default;
|
|
804
|
+
opacity: 0.35;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.pano-scene-menu-toggle {
|
|
808
|
+
background: var(--pano-scene-menu-background, rgba(16, 16, 16, 0.72));
|
|
809
|
+
pointer-events: auto;
|
|
810
|
+
position: absolute;
|
|
811
|
+
z-index: 1;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.pano-scene-menu[data-placement="bottom"] .pano-scene-menu-toggle,
|
|
815
|
+
.pano-scene-menu[data-placement="top"] .pano-scene-menu-toggle {
|
|
816
|
+
height: 1.5rem;
|
|
817
|
+
min-height: 1.5rem;
|
|
818
|
+
min-width: 4.5rem;
|
|
819
|
+
width: 4.5rem;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.pano-scene-menu[data-placement="left"] .pano-scene-menu-toggle,
|
|
823
|
+
.pano-scene-menu[data-placement="right"] .pano-scene-menu-toggle {
|
|
824
|
+
height: 4.5rem;
|
|
825
|
+
min-height: 4.5rem;
|
|
826
|
+
min-width: 1.5rem;
|
|
827
|
+
width: 1.5rem;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.pano-scene-menu[data-placement="bottom"] .pano-scene-menu-toggle {
|
|
831
|
+
border-radius: var(--pano-scene-menu-radius, 8px)
|
|
832
|
+
var(--pano-scene-menu-radius, 8px) 0 0;
|
|
833
|
+
bottom: 100%;
|
|
834
|
+
left: 50%;
|
|
835
|
+
transform: translateX(-50%);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.pano-scene-menu[data-placement="top"] .pano-scene-menu-toggle {
|
|
839
|
+
border-radius: 0 0 var(--pano-scene-menu-radius, 8px)
|
|
840
|
+
var(--pano-scene-menu-radius, 8px);
|
|
841
|
+
left: 50%;
|
|
842
|
+
top: 100%;
|
|
843
|
+
transform: translateX(-50%);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.pano-scene-menu[data-placement="left"] .pano-scene-menu-toggle {
|
|
847
|
+
border-radius: 0 var(--pano-scene-menu-radius, 8px)
|
|
848
|
+
var(--pano-scene-menu-radius, 8px) 0;
|
|
849
|
+
left: 100%;
|
|
850
|
+
top: 50%;
|
|
851
|
+
transform: translateY(-50%);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.pano-scene-menu[data-placement="right"] .pano-scene-menu-toggle {
|
|
855
|
+
border-radius: var(--pano-scene-menu-radius, 8px) 0 0
|
|
856
|
+
var(--pano-scene-menu-radius, 8px);
|
|
857
|
+
right: 100%;
|
|
858
|
+
top: 50%;
|
|
859
|
+
transform: translateY(-50%);
|
|
860
|
+
}
|
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 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","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 { 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"]}
|
package/dist/ui.d.mts
CHANGED
|
@@ -57,7 +57,7 @@ type ProjectPopupDialogProps = {
|
|
|
57
57
|
closeLabel?: string;
|
|
58
58
|
onMediaError?: (block: ProjectPopupBlock) => void;
|
|
59
59
|
};
|
|
60
|
-
/** Render
|
|
60
|
+
/** Render inside PanoOverlay; the host owns selection and may replace this UI. */
|
|
61
61
|
declare function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps): react.JSX.Element;
|
|
62
62
|
|
|
63
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
CHANGED
|
@@ -57,7 +57,7 @@ type ProjectPopupDialogProps = {
|
|
|
57
57
|
closeLabel?: string;
|
|
58
58
|
onMediaError?: (block: ProjectPopupBlock) => void;
|
|
59
59
|
};
|
|
60
|
-
/** Render
|
|
60
|
+
/** Render inside PanoOverlay; the host owns selection and may replace this UI. */
|
|
61
61
|
declare function ProjectPopupDialog({ popup, onOpenChange, closeLabel, onMediaError }: ProjectPopupDialogProps): react.JSX.Element;
|
|
62
62
|
|
|
63
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.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 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"]}
|
|
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"]}
|
package/docs/dialogs.md
CHANGED
|
@@ -22,9 +22,9 @@ This guide covers hotspot dialogs with text/images, video, galleries and iframes
|
|
|
22
22
|
|
|
23
23
|
| Need | Approach | What you own |
|
|
24
24
|
| --- | --- | --- |
|
|
25
|
-
| A working modal with minor theme changes | `PanoDialog` + CSS variables | Open state and content |
|
|
26
|
-
| Different header, close button, footer or layout |
|
|
27
|
-
| An existing Dialog / design system | `
|
|
25
|
+
| A working modal with minor theme changes | `PanoOverlay` + `PanoDialog` + CSS variables | Open state and content |
|
|
26
|
+
| Different header, close button, footer or layout | `PanoOverlay` + compose the `PanoDialog*` parts | Markup, styling and content |
|
|
27
|
+
| An existing Dialog / design system | `PanoOverlay` + `usePanoOverlay` + `usePanoModal` | Dialog implementation, accessibility and styling |
|
|
28
28
|
|
|
29
29
|
The default and composed dialogs handle focus, backdrop dismissal, Escape and viewer input locking. A third-party dialog keeps its own accessibility behavior; `usePanoModal` only coordinates viewer input. None of these approaches changes hotspot definitions or project JSON.
|
|
30
30
|
|
|
@@ -36,8 +36,10 @@ Install the viewer and its existing peer dependencies as described in [Install](
|
|
|
36
36
|
```tsx
|
|
37
37
|
import { useState } from "react";
|
|
38
38
|
import {
|
|
39
|
+
PanoOverlay,
|
|
39
40
|
PanoViewer,
|
|
40
41
|
ProjectScenes,
|
|
42
|
+
usePanoOverlay,
|
|
41
43
|
usePanoViewer,
|
|
42
44
|
type LoadedPanoProject,
|
|
43
45
|
type ProjectHotspotDefinition,
|
|
@@ -47,6 +49,19 @@ import "@ericchen1990/pano-view/styles.css";
|
|
|
47
49
|
import "@ericchen1990/pano-view/ui/styles.css";
|
|
48
50
|
import "./tour.css";
|
|
49
51
|
|
|
52
|
+
function FullscreenButton({ onToggle }: { onToggle: () => void }) {
|
|
53
|
+
const { isFullscreen } = usePanoOverlay();
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
style={{ position: "absolute", right: 16, bottom: 16, pointerEvents: "auto" }}
|
|
58
|
+
onClick={onToggle}
|
|
59
|
+
>
|
|
60
|
+
{isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
51
66
|
const viewer = usePanoViewer();
|
|
52
67
|
const [selected, setSelected] = useState<ProjectHotspotDefinition | null>(null);
|
|
@@ -56,26 +71,6 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
|
56
71
|
ref={viewer.ref}
|
|
57
72
|
className="my-tour"
|
|
58
73
|
style={{ height: 560 }}
|
|
59
|
-
renderOverlay={({ isFullscreen }) => (
|
|
60
|
-
<>
|
|
61
|
-
<button
|
|
62
|
-
type="button"
|
|
63
|
-
style={{ position: "absolute", right: 16, bottom: 16, pointerEvents: "auto" }}
|
|
64
|
-
onClick={() => void viewer.toggleFullscreen()}
|
|
65
|
-
>
|
|
66
|
-
{isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
67
|
-
</button>
|
|
68
|
-
<PanoDialog
|
|
69
|
-
open={selected !== null}
|
|
70
|
-
onOpenChange={(open) => { if (!open) setSelected(null); }}
|
|
71
|
-
title={selected?.label ?? "Details"}
|
|
72
|
-
description="Discover more about this place."
|
|
73
|
-
closeLabel="Close details"
|
|
74
|
-
>
|
|
75
|
-
{selected ? <p key={selected.id}>Content for {selected.label ?? selected.id}</p> : null}
|
|
76
|
-
</PanoDialog>
|
|
77
|
-
</>
|
|
78
|
-
)}
|
|
79
74
|
>
|
|
80
75
|
<ProjectScenes
|
|
81
76
|
project={project}
|
|
@@ -86,6 +81,18 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
|
86
81
|
}}
|
|
87
82
|
onActiveSceneIdChange={() => setSelected(null)}
|
|
88
83
|
/>
|
|
84
|
+
<PanoOverlay>
|
|
85
|
+
<FullscreenButton onToggle={() => void viewer.toggleFullscreen()} />
|
|
86
|
+
<PanoDialog
|
|
87
|
+
open={selected !== null}
|
|
88
|
+
onOpenChange={(open) => { if (!open) setSelected(null); }}
|
|
89
|
+
title={selected?.label ?? "Details"}
|
|
90
|
+
description="Discover more about this place."
|
|
91
|
+
closeLabel="Close details"
|
|
92
|
+
>
|
|
93
|
+
{selected ? <p key={selected.id}>Content for {selected.label ?? selected.id}</p> : null}
|
|
94
|
+
</PanoDialog>
|
|
95
|
+
</PanoOverlay>
|
|
89
96
|
</PanoViewer>
|
|
90
97
|
);
|
|
91
98
|
}
|
|
@@ -93,7 +100,7 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
|
93
100
|
|
|
94
101
|
Supply a loaded project containing a hotspot with ID `details`; see [Static projects and JSON data](../README.md#static-projects-and-json-data) for `loadPanoProject`. Adapt the selection condition to your content mapping. Only call `preventDefault()` when handling a hotspot yourself: other hotspots retain their normal scene navigation or media playback. The example explicitly closes the dialog on scene changes; the library does not impose that policy.
|
|
95
102
|
|
|
96
|
-
`PanoViewer
|
|
103
|
+
`PanoViewer` scene children stay in the Three.js tree. Put ordinary DOM dialogs inside `PanoOverlay`, not as bare siblings of `Sphere` or `ProjectScenes` that would enter that tree. Keep the overlay tree stable when `isFullscreen` changes; a key based on fullscreen state would reset its content.
|
|
97
104
|
|
|
98
105
|
For multiple hotspots, continue with the [complete mapping example below](#choose-content-by-hotspot): it reuses one dialog for five pieces of content and includes the matching project JSON.
|
|
99
106
|
|
|
@@ -120,6 +127,7 @@ The complete source defines every content component, the registry, selection sta
|
|
|
120
127
|
```tsx
|
|
121
128
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
122
129
|
import {
|
|
130
|
+
PanoOverlay,
|
|
123
131
|
PanoViewer,
|
|
124
132
|
ProjectScenes,
|
|
125
133
|
type LoadedPanoProject,
|
|
@@ -199,15 +207,7 @@ export function MultiHotspotTour({ project }: { project: LoadedPanoProject }) {
|
|
|
199
207
|
const contentKey = selected ? JSON.stringify([selected.sceneId, selected.hotspotId]) : "closed";
|
|
200
208
|
|
|
201
209
|
return (
|
|
202
|
-
<PanoViewer style={{ height: 560 }}
|
|
203
|
-
<PanoDialog
|
|
204
|
-
open={popup !== undefined}
|
|
205
|
-
onOpenChange={(open) => { if (!open) setSelected(null); }}
|
|
206
|
-
title={popup?.title ?? "Hotspot content"}
|
|
207
|
-
>
|
|
208
|
-
{popup ? <div key={contentKey}>{popup.body}</div> : null}
|
|
209
|
-
</PanoDialog>
|
|
210
|
-
)}>
|
|
210
|
+
<PanoViewer style={{ height: 560 }}>
|
|
211
211
|
<ProjectScenes
|
|
212
212
|
project={project}
|
|
213
213
|
onHotspotClick={(event) => {
|
|
@@ -218,6 +218,15 @@ export function MultiHotspotTour({ project }: { project: LoadedPanoProject }) {
|
|
|
218
218
|
}}
|
|
219
219
|
onActiveSceneIdChange={() => setSelected(null)}
|
|
220
220
|
/>
|
|
221
|
+
<PanoOverlay>
|
|
222
|
+
<PanoDialog
|
|
223
|
+
open={popup !== undefined}
|
|
224
|
+
onOpenChange={(open) => { if (!open) setSelected(null); }}
|
|
225
|
+
title={popup?.title ?? "Hotspot content"}
|
|
226
|
+
>
|
|
227
|
+
{popup ? <div key={contentKey}>{popup.body}</div> : null}
|
|
228
|
+
</PanoDialog>
|
|
229
|
+
</PanoOverlay>
|
|
221
230
|
</PanoViewer>
|
|
222
231
|
);
|
|
223
232
|
}
|
|
@@ -340,7 +349,7 @@ const style: CSSProperties & { "--pano-dialog-width": string } = {
|
|
|
340
349
|
borderRadius: 8,
|
|
341
350
|
};
|
|
342
351
|
|
|
343
|
-
// Render this component inside
|
|
352
|
+
// Render this component inside PanoOverlay.
|
|
344
353
|
export function StyledDialog(props: PanoDialogProps) {
|
|
345
354
|
return <PanoDialog {...props} className="my-dialog" style={style} />;
|
|
346
355
|
}
|
|
@@ -361,7 +370,7 @@ The default panel has inline structural styles for positioning, width, maximum h
|
|
|
361
370
|
<a id="compose-a-different-layout"></a>
|
|
362
371
|
## Compose a different layout
|
|
363
372
|
|
|
364
|
-
Use parts when you need a footer, a different close control, or full ownership of the markup. This example supplies its own visual CSS and does not need `ui/styles.css`. Render `CustomLayoutDialog`
|
|
373
|
+
Use parts when you need a footer, a different close control, or full ownership of the markup. This example supplies its own visual CSS and does not need `ui/styles.css`. Render `CustomLayoutDialog` inside `PanoOverlay` and pass the desired state and content.
|
|
365
374
|
|
|
366
375
|
```tsx
|
|
367
376
|
import type { ReactNode } from "react";
|
|
@@ -426,7 +435,7 @@ To prevent Escape dismissal or outside dismissal, use Content's `onEscapeKeyDown
|
|
|
426
435
|
<a id="use-an-existing-dialog"></a>
|
|
427
436
|
## Use an existing Dialog
|
|
428
437
|
|
|
429
|
-
The viewer does not require your application to use the optional UI components. A third-party integration needs three connections: the local Portal container, measured layout and viewer input coordination. The following Radix example is a component to render
|
|
438
|
+
The viewer does not require your application to use the optional UI components. A third-party integration needs three connections: the local Portal container, measured layout and viewer input coordination. The following Radix example is a component to render inside `PanoOverlay`. Install `@radix-ui/react-dialog` as a direct dependency in the consuming app if you import it there.
|
|
430
439
|
|
|
431
440
|
```tsx
|
|
432
441
|
import { useRef, useState, type ReactNode } from "react";
|
|
@@ -490,9 +499,9 @@ export function ExistingDialog({ open, onOpenChange, children }: {
|
|
|
490
499
|
}
|
|
491
500
|
```
|
|
492
501
|
|
|
493
|
-
Wait for a non-null container. Do not let a third-party Portal default to `document.body` while the viewer is mounting. Existing MUI, Ant Design, shadcn or other wrappers need an equivalent configurable mount target; adapt or wrap the actual Portal API your component exposes. An unsupported wrapper is not made fullscreen-safe merely by rendering it in `
|
|
502
|
+
Wait for a non-null container. Do not let a third-party Portal default to `document.body` while the viewer is mounting. Existing MUI, Ant Design, shadcn or other wrappers need an equivalent configurable mount target; adapt or wrap the actual Portal API your component exposes. An unsupported wrapper is not made fullscreen-safe merely by rendering it in `PanoOverlay`.
|
|
494
503
|
|
|
495
|
-
`usePanoModal(open)` pauses viewer input and automatic rotation, makes the background viewer UI inert and closes its context menu. It does not supply the third-party modal's focus trap, accessible names, close controls or page scroll handling. Locks are scoped per viewer and reference counted; releasing a modal does not release another modal's or transition's lock. Call hooks unconditionally in a child component
|
|
504
|
+
`usePanoModal(open)` pauses viewer input and automatic rotation, makes the background viewer UI inert and closes its context menu. It does not supply the third-party modal's focus trap, accessible names, close controls or page scroll handling. Locks are scoped per viewer and reference counted; releasing a modal does not release another modal's or transition's lock. Call hooks unconditionally in a child component under `PanoOverlay`, not in the parent of `PanoViewer`.
|
|
496
505
|
|
|
497
506
|
<a id="nested-portals"></a>
|
|
498
507
|
## Nested portals
|
|
@@ -574,7 +583,8 @@ Each export has a corresponding `*Props` type. Root is controlled and does not e
|
|
|
574
583
|
|
|
575
584
|
| API | Contract |
|
|
576
585
|
| --- | --- |
|
|
577
|
-
| `
|
|
586
|
+
| `PanoOverlay` | Preferred screen-plane wrapper; direct child of `PanoViewer` (or a Fragment under it) |
|
|
587
|
+
| `PanoViewer.renderOverlay(context)` | Optional callback-style DOM content outside the Canvas; not a hook invocation site |
|
|
578
588
|
| `usePanoOverlay()` | Same context for DOM descendants; throws outside the Overlay provider |
|
|
579
589
|
| `usePanoModal(open)` | Acquires/releases this viewer's modal input lock; no return value |
|
|
580
590
|
| `PanoOverlayPortalProvider` | Required `container: HTMLDivElement \| null` and `children`; overrides only the descendant Portal target |
|
|
@@ -599,8 +609,8 @@ Each export has a corresponding `*Props` type. Root is controlled and does not e
|
|
|
599
609
|
| Unstyled mode still has layout styles | Structural sizing/positioning and pointer behavior are inline by design. Omit `ui/styles.css` and supply visual styling, or use your own Dialog for full DOM ownership. |
|
|
600
610
|
| Long content hides Close | In a composed dialog, keep header/footer `flex-shrink: 0`, body `min-height: 0; overflow: auto`, and Content within measured space. |
|
|
601
611
|
| Video/form resets on fullscreen changes | Remove fullscreen-dependent keys, container swaps, conditional dialog implementations and component definitions nested in render callbacks. |
|
|
602
|
-
| Viewer still moves behind a custom modal | Call `usePanoModal(open)` from the custom component under `
|
|
612
|
+
| Viewer still moves behind a custom modal | Call `usePanoModal(open)` from the custom component under `PanoOverlay`. Default/composed Root already does this. |
|
|
603
613
|
| Nested popover is hidden or cannot receive input | Pass the Content-local target from `usePanoOverlay()` and enable pointer events on its actual panel. |
|
|
604
|
-
| A hook reports a missing provider | Render the custom DOM component
|
|
614
|
+
| A hook reports a missing provider | Render the custom DOM component inside `PanoOverlay`; do not call Overlay hooks in the parent of `PanoViewer`. |
|
|
605
615
|
|
|
606
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.
|