@mmmmzxe/react-360-viewer 0.1.3 → 0.1.4
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/dist/index.d.ts +3 -2
- package/dist/index.js +123 -164
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/viewer360ClassNames.ts +2 -1
- package/src/feature/Viewer360HotspotOverlay.tsx +16 -29
- package/src/feature/Viewer360MarkerPin.tsx +41 -40
- package/src/types/Viewer360Marker.ts +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React$1 from 'react';
|
|
2
|
-
import { ReactNode,
|
|
2
|
+
import { ReactNode, MouseEvent, CSSProperties, JSX, RefObject, PointerEvent, WheelEvent } from 'react';
|
|
3
3
|
import { ClassValue } from 'clsx';
|
|
4
4
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
5
5
|
import { VariantProps } from 'class-variance-authority';
|
|
@@ -104,6 +104,7 @@ type Viewer360MarkerPinProps<TData = unknown> = {
|
|
|
104
104
|
topPercent: number;
|
|
105
105
|
onDelete?: (id: string) => void;
|
|
106
106
|
isDeletePending?: boolean;
|
|
107
|
+
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
107
108
|
renderTag?: (props: Viewer360MarkerPinRenderProps<TData>) => ReactNode;
|
|
108
109
|
classNames?: Viewer360MarkerPinClassNames;
|
|
109
110
|
labels?: Viewer360MarkerPinLabels;
|
|
@@ -230,7 +231,7 @@ type Viewer360LoadingOverlayProps = {
|
|
|
230
231
|
};
|
|
231
232
|
declare function Viewer360LoadingOverlay({ className, textClassName, label }: Viewer360LoadingOverlayProps): JSX.Element;
|
|
232
233
|
|
|
233
|
-
declare function Viewer360MarkerPin<TData = unknown>({ marker, hotspot, leftPercent, topPercent, onDelete, isDeletePending, renderTag, classNames, labels, }: Viewer360MarkerPinProps<TData>): JSX.Element;
|
|
234
|
+
declare function Viewer360MarkerPin<TData = unknown>({ marker, hotspot, leftPercent, topPercent, onDelete, isDeletePending, onClick, renderTag, classNames, labels, }: Viewer360MarkerPinProps<TData>): JSX.Element;
|
|
234
235
|
|
|
235
236
|
type Viewer360ToolbarProps = Viewer360ToolbarRenderProps;
|
|
236
237
|
declare function Viewer360Toolbar({ showDragHint, showHotspotModeControl, showZoomControls, showResetControl, labels, isHotspotMode, zoom, minZoom, maxZoom, isResetDisabled, onHotspotModeChange, onZoomIn, onZoomOut, onResetView, }: Viewer360ToolbarProps): JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/feature/Viewer360.tsx
|
|
4
|
-
import { useEffect as useEffect2, useMemo as useMemo2, useState as
|
|
4
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
5
5
|
|
|
6
6
|
// src/constants/viewer360Labels.ts
|
|
7
7
|
var defaultViewer360Labels = {
|
|
@@ -577,73 +577,20 @@ function toViewer360Hotspots(markers, mapData) {
|
|
|
577
577
|
}));
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
// src/components/ui/
|
|
580
|
+
// src/components/ui/Item/index.tsx
|
|
581
581
|
import { cva as cva2 } from "class-variance-authority";
|
|
582
582
|
import { Slot as Slot2 } from "radix-ui";
|
|
583
|
-
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
584
|
-
var buttonVariants = cva2(
|
|
585
|
-
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
|
586
|
-
{
|
|
587
|
-
variants: {
|
|
588
|
-
variant: {
|
|
589
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
590
|
-
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
|
591
|
-
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
592
|
-
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
593
|
-
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
594
|
-
link: "text-primary underline-offset-4 hover:underline"
|
|
595
|
-
},
|
|
596
|
-
size: {
|
|
597
|
-
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
|
|
598
|
-
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
|
|
599
|
-
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
|
|
600
|
-
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3",
|
|
601
|
-
icon: "size-9",
|
|
602
|
-
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md",
|
|
603
|
-
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
|
604
|
-
"icon-lg": "size-10"
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
defaultVariants: {
|
|
608
|
-
variant: "default",
|
|
609
|
-
size: "default"
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
);
|
|
613
|
-
function Button({
|
|
614
|
-
className,
|
|
615
|
-
variant = "default",
|
|
616
|
-
size = "default",
|
|
617
|
-
asChild = false,
|
|
618
|
-
...props
|
|
619
|
-
}) {
|
|
620
|
-
const Comp = asChild ? Slot2.Root : "button";
|
|
621
|
-
return /* @__PURE__ */ jsx5(
|
|
622
|
-
Comp,
|
|
623
|
-
{
|
|
624
|
-
"data-slot": "button",
|
|
625
|
-
"data-variant": variant,
|
|
626
|
-
"data-size": size,
|
|
627
|
-
className: cn(buttonVariants({ variant, size, className })),
|
|
628
|
-
...props
|
|
629
|
-
}
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// src/components/ui/Item/index.tsx
|
|
634
|
-
import { cva as cva3 } from "class-variance-authority";
|
|
635
|
-
import { Slot as Slot3 } from "radix-ui";
|
|
636
583
|
|
|
637
584
|
// src/components/ui/Separator/index.tsx
|
|
638
585
|
import { Separator as SeparatorPrimitive } from "radix-ui";
|
|
639
|
-
import { jsx as
|
|
586
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
640
587
|
function Separator({
|
|
641
588
|
className,
|
|
642
589
|
orientation = "horizontal",
|
|
643
590
|
decorative = true,
|
|
644
591
|
...props
|
|
645
592
|
}) {
|
|
646
|
-
return /* @__PURE__ */
|
|
593
|
+
return /* @__PURE__ */ jsx5(
|
|
647
594
|
SeparatorPrimitive.Root,
|
|
648
595
|
{
|
|
649
596
|
"data-slot": "separator",
|
|
@@ -659,8 +606,8 @@ function Separator({
|
|
|
659
606
|
}
|
|
660
607
|
|
|
661
608
|
// src/components/ui/Item/index.tsx
|
|
662
|
-
import { jsx as
|
|
663
|
-
var itemVariants =
|
|
609
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
610
|
+
var itemVariants = cva2(
|
|
664
611
|
"[a]:hover:bg-muted rounded-md border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors",
|
|
665
612
|
{
|
|
666
613
|
variants: {
|
|
@@ -688,8 +635,8 @@ function Item({
|
|
|
688
635
|
asChild = false,
|
|
689
636
|
...props
|
|
690
637
|
}) {
|
|
691
|
-
const Comp = asChild ?
|
|
692
|
-
return /* @__PURE__ */
|
|
638
|
+
const Comp = asChild ? Slot2.Root : "div";
|
|
639
|
+
return /* @__PURE__ */ jsx6(
|
|
693
640
|
Comp,
|
|
694
641
|
{
|
|
695
642
|
"data-slot": "item",
|
|
@@ -700,7 +647,7 @@ function Item({
|
|
|
700
647
|
}
|
|
701
648
|
);
|
|
702
649
|
}
|
|
703
|
-
var itemMediaVariants =
|
|
650
|
+
var itemMediaVariants = cva2(
|
|
704
651
|
"gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
|
|
705
652
|
{
|
|
706
653
|
variants: {
|
|
@@ -716,7 +663,7 @@ var itemMediaVariants = cva3(
|
|
|
716
663
|
}
|
|
717
664
|
);
|
|
718
665
|
function ItemContent({ className, ...props }) {
|
|
719
|
-
return /* @__PURE__ */
|
|
666
|
+
return /* @__PURE__ */ jsx6(
|
|
720
667
|
"div",
|
|
721
668
|
{
|
|
722
669
|
"data-slot": "item-content",
|
|
@@ -726,7 +673,7 @@ function ItemContent({ className, ...props }) {
|
|
|
726
673
|
);
|
|
727
674
|
}
|
|
728
675
|
function ItemTitle({ className, ...props }) {
|
|
729
|
-
return /* @__PURE__ */
|
|
676
|
+
return /* @__PURE__ */ jsx6(
|
|
730
677
|
"div",
|
|
731
678
|
{
|
|
732
679
|
"data-slot": "item-title",
|
|
@@ -736,7 +683,7 @@ function ItemTitle({ className, ...props }) {
|
|
|
736
683
|
);
|
|
737
684
|
}
|
|
738
685
|
function ItemDescription({ className, ...props }) {
|
|
739
|
-
return /* @__PURE__ */
|
|
686
|
+
return /* @__PURE__ */ jsx6(
|
|
740
687
|
"p",
|
|
741
688
|
{
|
|
742
689
|
"data-slot": "item-description",
|
|
@@ -749,11 +696,10 @@ function ItemDescription({ className, ...props }) {
|
|
|
749
696
|
);
|
|
750
697
|
}
|
|
751
698
|
function ItemActions({ className, ...props }) {
|
|
752
|
-
return /* @__PURE__ */
|
|
699
|
+
return /* @__PURE__ */ jsx6("div", { "data-slot": "item-actions", className: cn("gap-2 flex items-center", className), ...props });
|
|
753
700
|
}
|
|
754
701
|
|
|
755
702
|
// src/feature/Viewer360MarkerPin.tsx
|
|
756
|
-
import { useState as useState2 } from "react";
|
|
757
703
|
import { Trash2 } from "lucide-react";
|
|
758
704
|
|
|
759
705
|
// src/constants/viewer360MarkerLabels.ts
|
|
@@ -761,38 +707,61 @@ var defaultViewer360MarkerPinLabels = {
|
|
|
761
707
|
delete: "Remove marker"
|
|
762
708
|
};
|
|
763
709
|
|
|
764
|
-
// src/components/ui/
|
|
765
|
-
import {
|
|
766
|
-
import {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
710
|
+
// src/components/ui/Button/index.tsx
|
|
711
|
+
import { cva as cva3 } from "class-variance-authority";
|
|
712
|
+
import { Slot as Slot3 } from "radix-ui";
|
|
713
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
714
|
+
var buttonVariants = cva3(
|
|
715
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
|
716
|
+
{
|
|
717
|
+
variants: {
|
|
718
|
+
variant: {
|
|
719
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
720
|
+
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
|
|
721
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
722
|
+
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
723
|
+
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
724
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
725
|
+
},
|
|
726
|
+
size: {
|
|
727
|
+
default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
|
|
728
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
|
|
729
|
+
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
|
|
730
|
+
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3",
|
|
731
|
+
icon: "size-9",
|
|
732
|
+
"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md",
|
|
733
|
+
"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
|
734
|
+
"icon-lg": "size-10"
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
defaultVariants: {
|
|
738
|
+
variant: "default",
|
|
739
|
+
size: "default"
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
function Button({
|
|
774
744
|
className,
|
|
775
|
-
|
|
776
|
-
|
|
745
|
+
variant = "default",
|
|
746
|
+
size = "default",
|
|
747
|
+
asChild = false,
|
|
777
748
|
...props
|
|
778
749
|
}) {
|
|
779
|
-
|
|
780
|
-
|
|
750
|
+
const Comp = asChild ? Slot3.Root : "button";
|
|
751
|
+
return /* @__PURE__ */ jsx7(
|
|
752
|
+
Comp,
|
|
781
753
|
{
|
|
782
|
-
"data-slot": "
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
className: cn(
|
|
786
|
-
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-md p-4 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--radix-popover-content-transform-origin) outline-hidden",
|
|
787
|
-
className
|
|
788
|
-
),
|
|
754
|
+
"data-slot": "button",
|
|
755
|
+
"data-variant": variant,
|
|
756
|
+
"data-size": size,
|
|
757
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
789
758
|
...props
|
|
790
759
|
}
|
|
791
|
-
)
|
|
760
|
+
);
|
|
792
761
|
}
|
|
793
762
|
|
|
794
763
|
// src/feature/Viewer360MarkerPin.tsx
|
|
795
|
-
import { jsx as
|
|
764
|
+
import { jsx as jsx8, jsxs } from "react/jsx-runtime";
|
|
796
765
|
function Viewer360MarkerPin({
|
|
797
766
|
marker,
|
|
798
767
|
hotspot,
|
|
@@ -800,23 +769,26 @@ function Viewer360MarkerPin({
|
|
|
800
769
|
topPercent,
|
|
801
770
|
onDelete,
|
|
802
771
|
isDeletePending = false,
|
|
772
|
+
onClick,
|
|
803
773
|
renderTag,
|
|
804
774
|
classNames,
|
|
805
775
|
labels
|
|
806
776
|
}) {
|
|
807
|
-
const [isOpen, setIsOpen] = useState2(false);
|
|
808
777
|
const deleteLabel = labels?.delete ?? defaultViewer360MarkerPinLabels.delete;
|
|
809
|
-
|
|
778
|
+
const showTooltip = Boolean(marker.title || marker.description || onDelete || renderTag);
|
|
779
|
+
return /* @__PURE__ */ jsxs(
|
|
810
780
|
Item,
|
|
811
781
|
{
|
|
812
782
|
size: "xs",
|
|
813
783
|
variant: "default",
|
|
814
|
-
className: cn(
|
|
784
|
+
className: cn(
|
|
785
|
+
viewer360MarkerPinClassNames.root,
|
|
786
|
+
classNames?.root,
|
|
787
|
+
"group/marker w-auto border-transparent p-0"
|
|
788
|
+
),
|
|
815
789
|
style: { left: `${leftPercent}%`, top: `${topPercent}%` },
|
|
816
|
-
onMouseEnter: () => setIsOpen(true),
|
|
817
|
-
onMouseLeave: () => setIsOpen(false),
|
|
818
790
|
children: [
|
|
819
|
-
/* @__PURE__ */
|
|
791
|
+
/* @__PURE__ */ jsx8(
|
|
820
792
|
Badge,
|
|
821
793
|
{
|
|
822
794
|
variant: "destructive",
|
|
@@ -824,7 +796,7 @@ function Viewer360MarkerPin({
|
|
|
824
796
|
"aria-hidden": "true"
|
|
825
797
|
}
|
|
826
798
|
),
|
|
827
|
-
/* @__PURE__ */
|
|
799
|
+
/* @__PURE__ */ jsx8(
|
|
828
800
|
Button,
|
|
829
801
|
{
|
|
830
802
|
type: "button",
|
|
@@ -835,16 +807,18 @@ function Viewer360MarkerPin({
|
|
|
835
807
|
classNames?.dot,
|
|
836
808
|
"size-4 min-h-4 min-w-4 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:scale-125 hover:bg-destructive"
|
|
837
809
|
),
|
|
838
|
-
"aria-label": marker.title
|
|
810
|
+
"aria-label": marker.title,
|
|
811
|
+
onClick
|
|
839
812
|
}
|
|
840
|
-
)
|
|
841
|
-
/* @__PURE__ */
|
|
842
|
-
|
|
813
|
+
),
|
|
814
|
+
showTooltip && /* @__PURE__ */ jsx8(
|
|
815
|
+
"div",
|
|
843
816
|
{
|
|
844
|
-
className: cn(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
817
|
+
className: cn(
|
|
818
|
+
viewer360MarkerPinClassNames.tooltip,
|
|
819
|
+
classNames?.tooltip,
|
|
820
|
+
"pointer-events-none opacity-0 transition-opacity duration-150 group-hover/marker:pointer-events-auto group-hover/marker:opacity-100 group-focus-within/marker:pointer-events-auto group-focus-within/marker:opacity-100"
|
|
821
|
+
),
|
|
848
822
|
children: /* @__PURE__ */ jsxs(
|
|
849
823
|
Item,
|
|
850
824
|
{
|
|
@@ -853,11 +827,11 @@ function Viewer360MarkerPin({
|
|
|
853
827
|
className: cn(viewer360MarkerPinClassNames.tooltipHeader, classNames?.tooltipHeader, "w-full border-transparent"),
|
|
854
828
|
children: [
|
|
855
829
|
/* @__PURE__ */ jsxs(ItemContent, { className: cn(viewer360MarkerPinClassNames.tooltipBody, classNames?.tooltipBody), children: [
|
|
856
|
-
/* @__PURE__ */
|
|
830
|
+
/* @__PURE__ */ jsx8(ItemTitle, { className: cn(viewer360MarkerPinClassNames.tooltipTitle, classNames?.tooltipTitle), children: marker.title }),
|
|
857
831
|
renderTag?.({ marker, hotspot }),
|
|
858
|
-
marker.description && /* @__PURE__ */
|
|
832
|
+
marker.description && /* @__PURE__ */ jsx8(ItemDescription, { className: cn(viewer360MarkerPinClassNames.tooltipDescription, classNames?.tooltipDescription), children: marker.description })
|
|
859
833
|
] }),
|
|
860
|
-
onDelete && /* @__PURE__ */
|
|
834
|
+
onDelete && /* @__PURE__ */ jsx8(ItemActions, { children: /* @__PURE__ */ jsx8(
|
|
861
835
|
Button,
|
|
862
836
|
{
|
|
863
837
|
variant: "ghost",
|
|
@@ -869,7 +843,7 @@ function Viewer360MarkerPin({
|
|
|
869
843
|
event.stopPropagation();
|
|
870
844
|
onDelete(marker.id);
|
|
871
845
|
},
|
|
872
|
-
children: /* @__PURE__ */
|
|
846
|
+
children: /* @__PURE__ */ jsx8(Trash2, { className: "size-4" })
|
|
873
847
|
}
|
|
874
848
|
) })
|
|
875
849
|
]
|
|
@@ -879,11 +853,11 @@ function Viewer360MarkerPin({
|
|
|
879
853
|
)
|
|
880
854
|
]
|
|
881
855
|
}
|
|
882
|
-
)
|
|
856
|
+
);
|
|
883
857
|
}
|
|
884
858
|
|
|
885
859
|
// src/feature/Viewer360HotspotOverlay.tsx
|
|
886
|
-
import { jsx as
|
|
860
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
887
861
|
function Viewer360HotspotOverlay({
|
|
888
862
|
hotspot,
|
|
889
863
|
leftPercent,
|
|
@@ -893,45 +867,30 @@ function Viewer360HotspotOverlay({
|
|
|
893
867
|
onHotspotClick
|
|
894
868
|
}) {
|
|
895
869
|
if (renderHotspot) {
|
|
896
|
-
return /* @__PURE__ */
|
|
897
|
-
}
|
|
898
|
-
if (hotspotPin) {
|
|
899
|
-
const marker = hotspotPin.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);
|
|
900
|
-
return /* @__PURE__ */ jsx10(
|
|
901
|
-
Viewer360MarkerPin,
|
|
902
|
-
{
|
|
903
|
-
marker,
|
|
904
|
-
hotspot,
|
|
905
|
-
leftPercent,
|
|
906
|
-
topPercent,
|
|
907
|
-
onDelete: hotspotPin.onDelete,
|
|
908
|
-
isDeletePending: hotspotPin.deletingMarkerId === hotspot.id,
|
|
909
|
-
renderTag: hotspotPin.renderTag,
|
|
910
|
-
classNames: hotspotPin.classNames,
|
|
911
|
-
labels: hotspotPin.labels
|
|
912
|
-
}
|
|
913
|
-
);
|
|
870
|
+
return /* @__PURE__ */ jsx9(Item, { size: "xs", variant: "default", className: "pointer-events-auto w-auto border-transparent p-0", children: renderHotspot({ hotspot, leftPercent, topPercent }) });
|
|
914
871
|
}
|
|
915
|
-
|
|
916
|
-
|
|
872
|
+
const marker = hotspotPin?.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);
|
|
873
|
+
return /* @__PURE__ */ jsx9(
|
|
874
|
+
Viewer360MarkerPin,
|
|
917
875
|
{
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
876
|
+
marker,
|
|
877
|
+
hotspot,
|
|
878
|
+
leftPercent,
|
|
879
|
+
topPercent,
|
|
880
|
+
onDelete: hotspotPin?.onDelete,
|
|
881
|
+
isDeletePending: hotspotPin?.deletingMarkerId === hotspot.id,
|
|
882
|
+
renderTag: hotspotPin?.renderTag,
|
|
883
|
+
classNames: hotspotPin?.classNames,
|
|
884
|
+
labels: hotspotPin?.labels,
|
|
885
|
+
onClick: onHotspotClick ? (event) => onHotspotClick(hotspot, event) : void 0
|
|
927
886
|
}
|
|
928
887
|
);
|
|
929
888
|
}
|
|
930
889
|
|
|
931
890
|
// src/components/ui/Label/index.tsx
|
|
932
|
-
import { jsx as
|
|
891
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
933
892
|
function Label({ className, ...props }) {
|
|
934
|
-
return /* @__PURE__ */
|
|
893
|
+
return /* @__PURE__ */ jsx10(
|
|
935
894
|
"label",
|
|
936
895
|
{
|
|
937
896
|
"data-slot": "label",
|
|
@@ -946,13 +905,13 @@ function Label({ className, ...props }) {
|
|
|
946
905
|
|
|
947
906
|
// src/components/ui/Spinner/index.tsx
|
|
948
907
|
import { Loader2Icon } from "lucide-react";
|
|
949
|
-
import { jsx as
|
|
908
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
950
909
|
function Spinner({ className, ...props }) {
|
|
951
|
-
return /* @__PURE__ */
|
|
910
|
+
return /* @__PURE__ */ jsx11(Loader2Icon, { role: "status", "aria-label": "Loading", className: cn("size-4 animate-spin", className), ...props });
|
|
952
911
|
}
|
|
953
912
|
|
|
954
913
|
// src/feature/Viewer360LoadingOverlay.tsx
|
|
955
|
-
import { jsx as
|
|
914
|
+
import { jsx as jsx12, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
956
915
|
function Viewer360LoadingOverlay({ className, textClassName, label }) {
|
|
957
916
|
return /* @__PURE__ */ jsxs2(
|
|
958
917
|
Item,
|
|
@@ -961,8 +920,8 @@ function Viewer360LoadingOverlay({ className, textClassName, label }) {
|
|
|
961
920
|
variant: "muted",
|
|
962
921
|
className: cn("pointer-events-none w-auto justify-center border-transparent bg-muted/80", className),
|
|
963
922
|
children: [
|
|
964
|
-
/* @__PURE__ */
|
|
965
|
-
/* @__PURE__ */
|
|
923
|
+
/* @__PURE__ */ jsx12(Spinner, { className: "size-5 text-muted-foreground" }),
|
|
924
|
+
/* @__PURE__ */ jsx12(Label, { className: cn("font-normal text-muted-foreground", textClassName), children: label })
|
|
966
925
|
]
|
|
967
926
|
}
|
|
968
927
|
);
|
|
@@ -970,7 +929,7 @@ function Viewer360LoadingOverlay({ className, textClassName, label }) {
|
|
|
970
929
|
|
|
971
930
|
// src/feature/Viewer360Toolbar.tsx
|
|
972
931
|
import { Crosshair, Minus, Plus, RotateCcw, ZoomIn } from "lucide-react";
|
|
973
|
-
import { Fragment, jsx as
|
|
932
|
+
import { Fragment, jsx as jsx13, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
974
933
|
function Viewer360Toolbar({
|
|
975
934
|
showDragHint,
|
|
976
935
|
showHotspotModeControl,
|
|
@@ -988,30 +947,30 @@ function Viewer360Toolbar({
|
|
|
988
947
|
onResetView
|
|
989
948
|
}) {
|
|
990
949
|
return /* @__PURE__ */ jsxs3(CardFooter, { className: cn(viewer360ClassNames.toolbar, "gap-2 border-t px-4 py-3 pt-3"), children: [
|
|
991
|
-
showDragHint && /* @__PURE__ */
|
|
950
|
+
showDragHint && /* @__PURE__ */ jsx13(Label, { className: cn(viewer360ClassNames.dragHint, "font-normal text-muted-foreground"), children: labels.dragHint }),
|
|
992
951
|
/* @__PURE__ */ jsxs3("div", { className: viewer360ClassNames.controls, children: [
|
|
993
952
|
showHotspotModeControl && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
994
953
|
/* @__PURE__ */ jsxs3(Button, { variant: isHotspotMode ? "default" : "outline", size: "sm", onClick: () => onHotspotModeChange(!isHotspotMode), children: [
|
|
995
|
-
/* @__PURE__ */
|
|
954
|
+
/* @__PURE__ */ jsx13(Crosshair, { className: "me-1.5 size-4" }),
|
|
996
955
|
labels.addHotspot
|
|
997
956
|
] }),
|
|
998
|
-
/* @__PURE__ */
|
|
957
|
+
/* @__PURE__ */ jsx13(Separator, { orientation: "vertical", className: cn(viewer360ClassNames.divider, "h-6") })
|
|
999
958
|
] }),
|
|
1000
959
|
showZoomControls && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1001
|
-
/* @__PURE__ */
|
|
960
|
+
/* @__PURE__ */ jsx13(Button, { variant: "outline", size: "icon-sm", disabled: zoom <= minZoom, "aria-label": labels.zoomOut, onClick: onZoomOut, children: /* @__PURE__ */ jsx13(Minus, { className: "size-4" }) }),
|
|
1002
961
|
/* @__PURE__ */ jsxs3(Badge, { variant: "outline", className: cn(viewer360ClassNames.zoomDisplay, "h-8 gap-1 px-2 py-1"), children: [
|
|
1003
|
-
/* @__PURE__ */
|
|
962
|
+
/* @__PURE__ */ jsx13(ZoomIn, { className: "size-3 text-muted-foreground" }),
|
|
1004
963
|
labels.zoom(Math.round(zoom * 100))
|
|
1005
964
|
] }),
|
|
1006
|
-
/* @__PURE__ */
|
|
965
|
+
/* @__PURE__ */ jsx13(Button, { variant: "outline", size: "icon-sm", disabled: zoom >= maxZoom, "aria-label": labels.zoomIn, onClick: onZoomIn, children: /* @__PURE__ */ jsx13(Plus, { className: "size-4" }) })
|
|
1007
966
|
] }),
|
|
1008
|
-
showResetControl && /* @__PURE__ */
|
|
967
|
+
showResetControl && /* @__PURE__ */ jsx13(Button, { variant: "outline", size: "icon-sm", disabled: isResetDisabled2, "aria-label": labels.resetView, onClick: onResetView, children: /* @__PURE__ */ jsx13(RotateCcw, { className: "size-4" }) })
|
|
1009
968
|
] })
|
|
1010
969
|
] });
|
|
1011
970
|
}
|
|
1012
971
|
|
|
1013
972
|
// src/feature/Viewer360.tsx
|
|
1014
|
-
import { jsx as
|
|
973
|
+
import { jsx as jsx14, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1015
974
|
function Viewer360({
|
|
1016
975
|
frames,
|
|
1017
976
|
currentFrameIndex: controlledFrameIndex,
|
|
@@ -1046,8 +1005,8 @@ function Viewer360({
|
|
|
1046
1005
|
const mergedLabels = useMemo2(() => mergeViewer360Labels(labels), [labels]);
|
|
1047
1006
|
const mergedClassNames = useMemo2(() => mergeViewer360ClassNames(classNames), [classNames]);
|
|
1048
1007
|
const themeStyle = useMemo2(() => buildViewer360ThemeStyle(theme), [theme]);
|
|
1049
|
-
const [internalFrameIndex, setInternalFrameIndex] =
|
|
1050
|
-
const [internalHotspotMode, setInternalHotspotMode] =
|
|
1008
|
+
const [internalFrameIndex, setInternalFrameIndex] = useState2(defaultFrameIndex);
|
|
1009
|
+
const [internalHotspotMode, setInternalHotspotMode] = useState2(defaultHotspotMode);
|
|
1051
1010
|
const currentFrameIndex = controlledFrameIndex ?? internalFrameIndex;
|
|
1052
1011
|
const hotspotMode = controlledHotspotMode ?? internalHotspotMode;
|
|
1053
1012
|
function handleFrameChange(index) {
|
|
@@ -1137,11 +1096,11 @@ function Viewer360({
|
|
|
1137
1096
|
onWheel: handleWheel,
|
|
1138
1097
|
onClick: handleCanvasClick,
|
|
1139
1098
|
children: [
|
|
1140
|
-
/* @__PURE__ */
|
|
1099
|
+
/* @__PURE__ */ jsx14("canvas", { ref: canvasRef, className: mergedClassNames.canvas }),
|
|
1141
1100
|
/* @__PURE__ */ jsxs4("div", { className: mergedClassNames.overlay, children: [
|
|
1142
1101
|
currentFrameHotspots.map((hotspot) => {
|
|
1143
1102
|
const position = getHotspotScreenPosition(hotspot);
|
|
1144
|
-
return /* @__PURE__ */
|
|
1103
|
+
return /* @__PURE__ */ jsx14(
|
|
1145
1104
|
Viewer360HotspotOverlay,
|
|
1146
1105
|
{
|
|
1147
1106
|
hotspot,
|
|
@@ -1156,7 +1115,7 @@ function Viewer360({
|
|
|
1156
1115
|
}),
|
|
1157
1116
|
children
|
|
1158
1117
|
] }),
|
|
1159
|
-
!imagesLoaded && (renderLoading ? renderLoading() : /* @__PURE__ */
|
|
1118
|
+
!imagesLoaded && (renderLoading ? renderLoading() : /* @__PURE__ */ jsx14(
|
|
1160
1119
|
Viewer360LoadingOverlay,
|
|
1161
1120
|
{
|
|
1162
1121
|
className: mergedClassNames.loading,
|
|
@@ -1164,7 +1123,7 @@ function Viewer360({
|
|
|
1164
1123
|
label: mergedLabels.loading
|
|
1165
1124
|
}
|
|
1166
1125
|
)),
|
|
1167
|
-
showFrameIndicator && frames.length > 0 && (renderFrameIndicator ? renderFrameIndicator(overlayProps) : /* @__PURE__ */
|
|
1126
|
+
showFrameIndicator && frames.length > 0 && (renderFrameIndicator ? renderFrameIndicator(overlayProps) : /* @__PURE__ */ jsx14(
|
|
1168
1127
|
Viewer360FrameIndicator,
|
|
1169
1128
|
{
|
|
1170
1129
|
className: mergedClassNames.frameIndicator,
|
|
@@ -1175,11 +1134,11 @@ function Viewer360({
|
|
|
1175
1134
|
})
|
|
1176
1135
|
}
|
|
1177
1136
|
)),
|
|
1178
|
-
isHotspotMode && onHotspotAdd && (renderHotspotModeBanner ? renderHotspotModeBanner({ labels: mergedLabels }) : /* @__PURE__ */
|
|
1137
|
+
isHotspotMode && onHotspotAdd && (renderHotspotModeBanner ? renderHotspotModeBanner({ labels: mergedLabels }) : /* @__PURE__ */ jsx14(Viewer360AddModeBanner, { className: mergedClassNames.hotspotModeBanner, label: mergedLabels.hotspotModeActive }))
|
|
1179
1138
|
]
|
|
1180
1139
|
}
|
|
1181
1140
|
),
|
|
1182
|
-
renderToolbar ? renderToolbar(toolbarProps) : showDefaultToolbar ? /* @__PURE__ */
|
|
1141
|
+
renderToolbar ? renderToolbar(toolbarProps) : showDefaultToolbar ? /* @__PURE__ */ jsx14(Viewer360Toolbar, { ...toolbarProps }) : null
|
|
1183
1142
|
] });
|
|
1184
1143
|
}
|
|
1185
1144
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/feature/Viewer360.tsx","../src/constants/viewer360Labels.ts","../src/constants/viewer360ClassNames.ts","../src/components/utils/index.ts","../src/helpers/viewer360PropsHelpers.ts","../src/hooks/useViewer360.ts","../src/helpers/adjustViewerZoom.ts","../src/helpers/computeDragFrameIndex.ts","../src/helpers/computeViewerImageLayout.ts","../src/helpers/computeViewerPanOffset.ts","../src/helpers/viewerHelpers.ts","../src/components/ui/Card/index.tsx","../src/components/ui/Badge/index.tsx","../src/feature/Viewer360AddModeBanner.tsx","../src/feature/Viewer360FrameIndicator.tsx","../src/helpers/markerHelpers.ts","../src/components/ui/Button/index.tsx","../src/components/ui/Item/index.tsx","../src/components/ui/Separator/index.tsx","../src/feature/Viewer360MarkerPin.tsx","../src/constants/viewer360MarkerLabels.ts","../src/components/ui/Popover/index.tsx","../src/feature/Viewer360HotspotOverlay.tsx","../src/components/ui/Label/index.tsx","../src/components/ui/Spinner/index.tsx","../src/feature/Viewer360LoadingOverlay.tsx","../src/feature/Viewer360Toolbar.tsx","../src/constants/viewer360Config.ts"],"sourcesContent":["import type { JSX } from 'react';\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { buildViewer360ThemeStyle, mergeViewer360ClassNames, mergeViewer360Labels } from '../helpers/viewer360PropsHelpers';\nimport { useViewer360 } from '../hooks/useViewer360';\nimport type { Viewer360OverlayRenderProps, Viewer360Props, Viewer360ToolbarRenderProps } from '../types';\nimport { Card } from '@/components/ui/Card';\nimport { cn } from '@/components/utils';\n\nimport { Viewer360AddModeBanner } from './Viewer360AddModeBanner';\nimport { Viewer360FrameIndicator } from './Viewer360FrameIndicator';\nimport { Viewer360HotspotOverlay } from './Viewer360HotspotOverlay';\nimport { Viewer360LoadingOverlay } from './Viewer360LoadingOverlay';\nimport { Viewer360Toolbar } from './Viewer360Toolbar';\n\nexport function Viewer360<TData = unknown>({\n frames,\n currentFrameIndex: controlledFrameIndex,\n defaultFrameIndex = 0,\n onFrameChange,\n config,\n className,\n classNames,\n style,\n theme,\n labels,\n aspectRatio = '16 / 10',\n showZoomControls = true,\n showResetControl = true,\n showFrameIndicator = true,\n showDragHint = true,\n showHotspotModeControl = false,\n hotspotPin,\n hotspots = [],\n renderHotspot,\n renderLoading,\n renderFrameIndicator,\n renderHotspotModeBanner,\n renderToolbar,\n onHotspotClick,\n hotspotMode: controlledHotspotMode,\n defaultHotspotMode = false,\n onHotspotModeChange,\n onHotspotAdd,\n children,\n}: Viewer360Props<TData>): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: States & Constants\n // ----------------------------------------------------------------------------------------------------\n const mergedLabels = useMemo(() => mergeViewer360Labels(labels), [labels]);\n const mergedClassNames = useMemo(() => mergeViewer360ClassNames(classNames), [classNames]);\n const themeStyle = useMemo(() => buildViewer360ThemeStyle(theme), [theme]);\n\n const [internalFrameIndex, setInternalFrameIndex] = useState(defaultFrameIndex);\n const [internalHotspotMode, setInternalHotspotMode] = useState(defaultHotspotMode);\n\n const currentFrameIndex = controlledFrameIndex ?? internalFrameIndex;\n const hotspotMode = controlledHotspotMode ?? internalHotspotMode;\n\n // ----------------------------------------------------------------------------------------------------\n // MARK: Functions\n // ----------------------------------------------------------------------------------------------------\n function handleFrameChange(index: number): void {\n if (controlledFrameIndex === undefined) {\n setInternalFrameIndex(index);\n }\n\n onFrameChange?.(index);\n }\n\n function handleHotspotModeChange(active: boolean): void {\n if (controlledHotspotMode === undefined) {\n setInternalHotspotMode(active);\n }\n\n onHotspotModeChange?.(active);\n }\n\n const {\n canvasRef,\n containerRef,\n currentFrame,\n currentFrameHotspots,\n imagesLoaded,\n isHotspotMode,\n isResetDisabled,\n maxZoom,\n minZoom,\n viewerCursorClass,\n zoom,\n getHotspotScreenPosition,\n handleCanvasClick,\n handlePointerDown,\n handlePointerMove,\n handlePointerUp,\n handleWheel,\n handleResetView,\n handleZoomIn,\n handleZoomOut,\n } = useViewer360<TData>({\n frames,\n hotspots,\n currentFrameIndex,\n onFrameChange: handleFrameChange,\n config,\n hotspotMode,\n onHotspotAdd,\n });\n\n useEffect(() => {\n if (controlledHotspotMode === undefined) return;\n if (controlledHotspotMode !== internalHotspotMode) {\n setInternalHotspotMode(controlledHotspotMode);\n }\n }, [controlledHotspotMode, internalHotspotMode]);\n\n const frameLabel = currentFrame?.label ?? frames[currentFrameIndex]?.label;\n const overlayProps: Viewer360OverlayRenderProps = {\n currentFrameIndex,\n frameCount: frames.length,\n frameLabel,\n isHotspotMode,\n labels: mergedLabels,\n };\n const toolbarProps: Viewer360ToolbarRenderProps = {\n zoom,\n minZoom,\n maxZoom,\n isResetDisabled,\n isHotspotMode,\n showHotspotModeControl,\n showZoomControls,\n showResetControl,\n showDragHint,\n labels: mergedLabels,\n onZoomIn: handleZoomIn,\n onZoomOut: handleZoomOut,\n onResetView: handleResetView,\n onHotspotModeChange: handleHotspotModeChange,\n };\n const showDefaultToolbar = showZoomControls || showResetControl || showHotspotModeControl || showDragHint;\n\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Card className={cn(mergedClassNames.root, 'gap-0 py-0 shadow-none ring-0', className)} style={{ ...themeStyle, ...style }}>\n <div\n ref={containerRef}\n className={cn(mergedClassNames.viewport, viewerCursorClass)}\n style={{ aspectRatio }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerLeave={handlePointerUp}\n onWheel={handleWheel}\n onClick={handleCanvasClick}\n >\n <canvas ref={canvasRef} className={mergedClassNames.canvas} />\n\n <div className={mergedClassNames.overlay}>\n {currentFrameHotspots.map((hotspot) => {\n const position = getHotspotScreenPosition(hotspot);\n\n return (\n <Viewer360HotspotOverlay\n key={hotspot.id}\n hotspot={hotspot}\n leftPercent={position.leftPercent}\n topPercent={position.topPercent}\n hotspotPin={hotspotPin}\n renderHotspot={renderHotspot}\n onHotspotClick={onHotspotClick}\n />\n );\n })}\n {children}\n </div>\n\n {!imagesLoaded &&\n (renderLoading ? (\n renderLoading()\n ) : (\n <Viewer360LoadingOverlay\n className={mergedClassNames.loading}\n textClassName={mergedClassNames.loadingText}\n label={mergedLabels.loading}\n />\n ))}\n\n {showFrameIndicator &&\n frames.length > 0 &&\n (renderFrameIndicator ? (\n renderFrameIndicator(overlayProps)\n ) : (\n <Viewer360FrameIndicator\n className={mergedClassNames.frameIndicator}\n label={mergedLabels.frameIndicator({\n current: currentFrameIndex + 1,\n total: frames.length,\n label: frameLabel,\n })}\n />\n ))}\n\n {isHotspotMode &&\n onHotspotAdd &&\n (renderHotspotModeBanner ? (\n renderHotspotModeBanner({ labels: mergedLabels })\n ) : (\n <Viewer360AddModeBanner className={mergedClassNames.hotspotModeBanner} label={mergedLabels.hotspotModeActive} />\n ))}\n </div>\n\n {renderToolbar ? renderToolbar(toolbarProps) : showDefaultToolbar ? <Viewer360Toolbar {...toolbarProps} /> : null}\n </Card>\n );\n}\n","import type { Viewer360Labels } from '../types/Viewer360Props';\n\nexport const defaultViewer360Labels: Required<Viewer360Labels> = {\n loading: 'Loading images…',\n dragHint: 'Drag to rotate • Scroll to zoom',\n frameIndicator: ({ current, total, label }) => (label ? `${label} · ${current} / ${total}` : `${current} / ${total}`),\n zoom: (percent) => `${percent}%`,\n hotspotModeActive: 'Click on the image to place a hotspot',\n addHotspot: 'Add hotspot',\n zoomIn: 'Zoom in',\n zoomOut: 'Zoom out',\n resetView: 'Reset view',\n deleteMarker: 'Remove marker',\n};\n","import type { Viewer360ClassNames } from '../types/Viewer360Props';\nimport type { Viewer360MarkerPinClassNames } from '../types/Viewer360Marker';\n\nexport const viewer360ClassNames: Required<Viewer360ClassNames> = {\n root: 'overflow-hidden rounded-lg border bg-card text-card-foreground',\n viewport: 'relative aspect-[16/10] w-full touch-none select-none bg-muted',\n canvas: 'absolute inset-0 size-full',\n overlay: 'pointer-events-none absolute inset-0 overflow-hidden',\n loading: 'absolute inset-0 flex items-center justify-center bg-muted/80',\n loadingText: 'text-sm text-muted-foreground',\n frameIndicator:\n 'pointer-events-none absolute bottom-4 start-4 z-20 rounded-full border bg-background px-4 py-1.5 text-xs font-medium shadow-sm',\n hotspotModeBanner:\n 'pointer-events-none absolute top-4 start-1/2 z-20 -translate-x-1/2 rounded-full border border-amber-200 bg-amber-50 px-4 py-1.5 text-xs font-medium text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400',\n toolbar: 'flex flex-wrap items-center justify-between gap-2 border-t px-4 py-3',\n dragHint: 'hidden text-xs text-muted-foreground sm:block',\n controls: 'ms-auto flex items-center gap-1.5',\n controlButton: '',\n controlButtonActive: '',\n controlButtonDisabled: '',\n zoomDisplay: 'flex min-w-[3rem] items-center justify-center gap-1 rounded-md border bg-background px-2 py-1 text-xs font-medium',\n divider: 'mx-1 h-6 w-px bg-border',\n};\n\nexport const viewer360MarkerPinClassNames: Required<Viewer360MarkerPinClassNames> = {\n root: 'pointer-events-auto absolute z-30 -translate-x-1/2 -translate-y-1/2',\n ping: 'absolute inline-flex size-6 -translate-x-1/4 -translate-y-1/4 animate-ping rounded-full bg-destructive opacity-60',\n dot: 'relative flex size-4 items-center justify-center rounded-full border-2 border-background bg-destructive shadow-md transition-transform duration-200 hover:scale-125 focus:outline-none',\n tooltip: 'absolute bottom-6 left-1/2 z-40 w-64 -translate-x-1/2 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md',\n tooltipHeader: 'flex items-start justify-between gap-2',\n tooltipBody: 'flex min-w-0 flex-col gap-1',\n tooltipTitle: 'text-sm font-medium',\n tooltipDescription: 'mt-2 line-clamp-3 text-xs text-muted-foreground',\n deleteButton: '',\n};\n\n/** @deprecated Use `viewer360ClassNames` */\nexport const defaultViewer360ClassNames = viewer360ClassNames;\n\n/** @deprecated Use `viewer360MarkerPinClassNames` */\nexport const defaultViewer360MarkerPinClassNames = viewer360MarkerPinClassNames;\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import type { CSSProperties } from 'react';\n\nimport { defaultViewer360Labels } from '../constants/viewer360Labels';\nimport { viewer360ClassNames } from '../constants/viewer360ClassNames';\nimport type { Viewer360ClassNames, Viewer360Labels, Viewer360Theme } from '../types';\nimport { cn } from '@/components/utils';\n\nexport function mergeViewer360Labels(labels?: Viewer360Labels): Required<Viewer360Labels> {\n return {\n loading: labels?.loading ?? defaultViewer360Labels.loading,\n dragHint: labels?.dragHint ?? defaultViewer360Labels.dragHint,\n frameIndicator: labels?.frameIndicator ?? defaultViewer360Labels.frameIndicator,\n zoom: labels?.zoom ?? defaultViewer360Labels.zoom,\n hotspotModeActive: labels?.hotspotModeActive ?? defaultViewer360Labels.hotspotModeActive,\n addHotspot: labels?.addHotspot ?? defaultViewer360Labels.addHotspot,\n zoomIn: labels?.zoomIn ?? defaultViewer360Labels.zoomIn,\n zoomOut: labels?.zoomOut ?? defaultViewer360Labels.zoomOut,\n resetView: labels?.resetView ?? defaultViewer360Labels.resetView,\n deleteMarker: labels?.deleteMarker ?? defaultViewer360Labels.deleteMarker,\n };\n}\n\nexport function mergeViewer360ClassNames(classNames?: Viewer360ClassNames): Required<Viewer360ClassNames> {\n return {\n root: cn(viewer360ClassNames.root, classNames?.root),\n viewport: cn(viewer360ClassNames.viewport, classNames?.viewport),\n canvas: cn(viewer360ClassNames.canvas, classNames?.canvas),\n overlay: cn(viewer360ClassNames.overlay, classNames?.overlay),\n loading: cn(viewer360ClassNames.loading, classNames?.loading),\n loadingText: cn(viewer360ClassNames.loadingText, classNames?.loadingText),\n frameIndicator: cn(viewer360ClassNames.frameIndicator, classNames?.frameIndicator),\n hotspotModeBanner: cn(viewer360ClassNames.hotspotModeBanner, classNames?.hotspotModeBanner),\n toolbar: cn(viewer360ClassNames.toolbar, classNames?.toolbar),\n dragHint: cn(viewer360ClassNames.dragHint, classNames?.dragHint),\n controls: cn(viewer360ClassNames.controls, classNames?.controls),\n controlButton: cn(viewer360ClassNames.controlButton, classNames?.controlButton),\n controlButtonActive: cn(viewer360ClassNames.controlButtonActive, classNames?.controlButtonActive),\n controlButtonDisabled: cn(viewer360ClassNames.controlButtonDisabled, classNames?.controlButtonDisabled),\n zoomDisplay: cn(viewer360ClassNames.zoomDisplay, classNames?.zoomDisplay),\n divider: cn(viewer360ClassNames.divider, classNames?.divider),\n };\n}\n\nexport function buildViewer360ThemeStyle(theme?: Viewer360Theme): CSSProperties {\n return theme ? (theme as CSSProperties) : {};\n}\n","import type { PointerEvent as ReactPointerEvent, RefObject, WheelEvent as ReactWheelEvent } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n applyWheelZoom,\n getViewerCursorClass,\n isResetDisabled,\n resolveViewer360Config,\n stepZoomIn,\n stepZoomOut,\n} from '../helpers/adjustViewerZoom';\nimport { computeDragFrameIndex } from '../helpers/computeDragFrameIndex';\nimport {\n computeHotspotPositionFromClick,\n computeHotspotScreenPosition,\n computeViewerImageLayout,\n type ViewerImageLayout,\n} from '../helpers/computeViewerImageLayout';\nimport { computeViewerPanOffset } from '../helpers/computeViewerPanOffset';\nimport {\n drawFrameOnCanvas,\n filterHotspotsByFrame,\n getFramesSignature,\n hasLoadedViewerFrame,\n preloadViewerFrames,\n} from '../helpers/viewerHelpers';\nimport type {\n Viewer360Config,\n Viewer360Frame,\n Viewer360Hotspot,\n Viewer360HotspotPosition,\n Viewer360PanOffset,\n} from '../types';\n\ntype UseViewer360Params<TData> = {\n frames: Viewer360Frame[];\n currentFrameIndex: number;\n onFrameChange: (index: number) => void;\n hotspots?: Viewer360Hotspot<TData>[];\n config?: Viewer360Config;\n hotspotMode?: boolean;\n onHotspotAdd?: (position: Viewer360HotspotPosition) => void;\n};\n\ntype UseViewer360Return<TData> = {\n canvasRef: RefObject<HTMLCanvasElement | null>;\n containerRef: RefObject<HTMLDivElement | null>;\n currentFrame: Viewer360Frame | undefined;\n currentFrameHotspots: Viewer360Hotspot<TData>[];\n imagesLoaded: boolean;\n isHotspotMode: boolean;\n isResetDisabled: boolean;\n maxZoom: number;\n minZoom: number;\n viewerCursorClass: string;\n zoom: number;\n getCurrentImageLayout: () => ViewerImageLayout | null;\n getHotspotScreenPosition: (hotspot: Viewer360Hotspot<TData>) => { leftPercent: number; topPercent: number };\n handleCanvasClick: (event: React.MouseEvent<HTMLDivElement>) => void;\n handlePointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;\n handlePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;\n handlePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;\n handleResetView: () => void;\n handleWheel: (event: ReactWheelEvent<HTMLDivElement>) => void;\n handleZoomIn: () => void;\n handleZoomOut: () => void;\n};\n\nexport function useViewer360<TData = unknown>({\n frames,\n hotspots = [],\n currentFrameIndex,\n onFrameChange,\n config,\n hotspotMode: hotspotModeProp = false,\n onHotspotAdd,\n}: UseViewer360Params<TData>): UseViewer360Return<TData> {\n const resolvedConfig = useMemo(() => resolveViewer360Config(config), [config]);\n const { minZoom, maxZoom } = resolvedConfig;\n\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n const imagesRef = useRef<HTMLImageElement[]>([]);\n const dragStartRef = useRef<{ pointerX: number; frameIndex: number } | null>(null);\n const panStartRef = useRef<{ pointerX: number; pointerY: number; panX: number; panY: number } | null>(null);\n\n const framesSignature = getFramesSignature(frames);\n const [loadedSignature, setLoadedSignature] = useState<string | null>(null);\n const [zoom, setZoom] = useState<number>(minZoom);\n const [pan, setPan] = useState<Viewer360PanOffset>({ panX: 0, panY: 0 });\n const [isDragging, setIsDragging] = useState(false);\n\n const isHotspotMode = hotspotModeProp;\n const imagesLoaded = loadedSignature === framesSignature;\n const currentFrameHotspots = filterHotspotsByFrame(hotspots, currentFrameIndex) as Viewer360Hotspot<TData>[];\n const currentFrame = frames[currentFrameIndex];\n const viewerCursorClass = getViewerCursorClass(isHotspotMode, zoom, isDragging, minZoom);\n const resetDisabled = isResetDisabled(zoom, pan, resolvedConfig);\n\n useEffect(() => {\n let cancelled = false;\n imagesRef.current = [];\n\n async function loadFrames(): Promise<void> {\n const loadedImages = await preloadViewerFrames(frames);\n\n if (!cancelled && hasLoadedViewerFrame(loadedImages)) {\n imagesRef.current = loadedImages;\n setLoadedSignature(framesSignature);\n }\n }\n\n void loadFrames();\n\n return (): void => {\n cancelled = true;\n };\n }, [frames, framesSignature]);\n\n useEffect(() => {\n if (!imagesLoaded) return;\n\n function renderFrame(): void {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n const img = imagesRef.current[currentFrameIndex];\n\n if (!canvas || !container) return;\n\n drawFrameOnCanvas({ canvas, container, image: img, zoom, pan });\n }\n\n renderFrame();\n window.addEventListener('resize', renderFrame);\n\n return (): void => window.removeEventListener('resize', renderFrame);\n }, [imagesLoaded, currentFrameIndex, zoom, pan]);\n\n useEffect(() => {\n if (!resolvedConfig.autoRotate || frames.length <= 1 || isDragging || isHotspotMode || zoom > minZoom) {\n return;\n }\n\n const direction = resolvedConfig.autoRotateDirection === 'backward' ? -1 : 1;\n const interval = window.setInterval(() => {\n onFrameChange((currentFrameIndex + direction + frames.length) % frames.length);\n }, resolvedConfig.autoRotateIntervalMs);\n\n return (): void => window.clearInterval(interval);\n }, [\n resolvedConfig.autoRotate,\n resolvedConfig.autoRotateDirection,\n resolvedConfig.autoRotateIntervalMs,\n frames.length,\n currentFrameIndex,\n isDragging,\n isHotspotMode,\n zoom,\n minZoom,\n onFrameChange,\n ]);\n\n function getCurrentImageLayout(): ViewerImageLayout | null {\n const container = containerRef.current;\n const image = imagesRef.current[currentFrameIndex];\n\n if (!container || !image || !image.complete || !image.naturalWidth) return null;\n\n const rect = container.getBoundingClientRect();\n\n return computeViewerImageLayout({\n containerWidth: rect.width,\n containerHeight: rect.height,\n imageWidth: image.naturalWidth,\n imageHeight: image.naturalHeight,\n pan,\n });\n }\n\n function getHotspotScreenPosition(hotspot: Viewer360Hotspot<TData>): { leftPercent: number; topPercent: number } {\n const layout = getCurrentImageLayout();\n\n if (!layout) {\n return { leftPercent: hotspot.positionX, topPercent: hotspot.positionY };\n }\n\n return computeHotspotScreenPosition(hotspot.positionX, hotspot.positionY, layout, zoom);\n }\n\n function handlePointerDown(event: ReactPointerEvent<HTMLDivElement>): void {\n if (isHotspotMode) return;\n\n event.currentTarget.setPointerCapture(event.pointerId);\n\n if (zoom > minZoom) {\n panStartRef.current = { pointerX: event.clientX, pointerY: event.clientY, panX: pan.panX, panY: pan.panY };\n } else {\n dragStartRef.current = { pointerX: event.clientX, frameIndex: currentFrameIndex };\n }\n\n setIsDragging(true);\n }\n\n function handlePointerMove(event: ReactPointerEvent<HTMLDivElement>): void {\n if (!isDragging) return;\n\n if (panStartRef.current && zoom > minZoom) {\n setPan(computeViewerPanOffset(panStartRef.current, event.clientX, event.clientY));\n return;\n }\n\n if (dragStartRef.current && zoom <= minZoom) {\n const nextFrameIndex = computeDragFrameIndex(\n dragStartRef.current,\n event.clientX,\n frames.length,\n resolvedConfig.dragSensitivity\n );\n\n if (nextFrameIndex !== null) {\n onFrameChange(nextFrameIndex);\n }\n }\n }\n\n function handlePointerUp(event: ReactPointerEvent<HTMLDivElement>): void {\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n\n dragStartRef.current = null;\n panStartRef.current = null;\n setIsDragging(false);\n }\n\n function handleWheel(event: ReactWheelEvent<HTMLDivElement>): void {\n event.preventDefault();\n const { zoom: nextZoom, pan: nextPan } = applyWheelZoom(zoom, event.deltaY, pan, resolvedConfig);\n setZoom(nextZoom);\n setPan(nextPan);\n }\n\n function handleCanvasClick(event: React.MouseEvent<HTMLDivElement>): void {\n if (!isHotspotMode || isDragging || !onHotspotAdd) return;\n\n const frame = frames[currentFrameIndex];\n if (!frame) return;\n\n const layout = getCurrentImageLayout();\n if (!layout) return;\n\n const { positionX, positionY } = computeHotspotPositionFromClick(\n event.clientX,\n event.clientY,\n event.currentTarget.getBoundingClientRect(),\n layout,\n zoom\n );\n\n onHotspotAdd({\n frameIndex: currentFrameIndex,\n frameId: frame.id,\n positionX,\n positionY,\n });\n }\n\n function handleResetView(): void {\n setZoom(minZoom);\n setPan({ panX: 0, panY: 0 });\n }\n\n function handleZoomIn(): void {\n setZoom(stepZoomIn(zoom, resolvedConfig));\n }\n\n function handleZoomOut(): void {\n const { zoom: nextZoom, pan: nextPan } = stepZoomOut(zoom, pan, resolvedConfig);\n setZoom(nextZoom);\n setPan(nextPan);\n }\n\n return {\n canvasRef,\n containerRef,\n currentFrame,\n currentFrameHotspots,\n imagesLoaded,\n isHotspotMode,\n isResetDisabled: resetDisabled,\n maxZoom,\n minZoom,\n viewerCursorClass,\n zoom,\n getCurrentImageLayout,\n getHotspotScreenPosition,\n handleCanvasClick,\n handlePointerDown,\n handlePointerMove,\n handlePointerUp,\n handleResetView,\n handleWheel,\n handleZoomIn,\n handleZoomOut,\n };\n}\n","import type { Viewer360Config } from '../types';\n\nimport type { PanOffset } from './computeViewerImageLayout';\n\ntype ResolvedViewer360Config = Required<Viewer360Config>;\n\nexport function resolveViewer360Config(config?: Viewer360Config): ResolvedViewer360Config {\n return {\n minZoom: config?.minZoom ?? 1,\n maxZoom: config?.maxZoom ?? 3,\n zoomStep: config?.zoomStep ?? 0.15,\n dragSensitivity: config?.dragSensitivity ?? 8,\n autoRotate: config?.autoRotate ?? false,\n autoRotateIntervalMs: config?.autoRotateIntervalMs ?? 100,\n autoRotateDirection: config?.autoRotateDirection ?? 'forward',\n };\n}\n\nexport function clampZoom(zoom: number, config: ResolvedViewer360Config): number {\n return Math.min(config.maxZoom, Math.max(config.minZoom, zoom));\n}\n\nexport function applyWheelZoom(\n currentZoom: number,\n deltaY: number,\n currentPan: PanOffset,\n config: ResolvedViewer360Config\n): { zoom: number; pan: PanOffset } {\n const delta = deltaY > 0 ? -config.zoomStep : config.zoomStep;\n const zoom = clampZoom(currentZoom + delta, config);\n\n return {\n zoom,\n pan: zoom === config.minZoom ? { panX: 0, panY: 0 } : currentPan,\n };\n}\n\nexport function stepZoomIn(currentZoom: number, config: ResolvedViewer360Config): number {\n return clampZoom(currentZoom + config.zoomStep, config);\n}\n\nexport function stepZoomOut(\n currentZoom: number,\n currentPan: PanOffset,\n config: ResolvedViewer360Config\n): { zoom: number; pan: PanOffset } {\n const zoom = clampZoom(currentZoom - config.zoomStep, config);\n\n return {\n zoom,\n pan: zoom === config.minZoom ? { panX: 0, panY: 0 } : currentPan,\n };\n}\n\nexport function isResetDisabled(zoom: number, pan: PanOffset, config: ResolvedViewer360Config): boolean {\n return zoom === config.minZoom && pan.panX === 0 && pan.panY === 0;\n}\n\nexport function getViewerCursorClass(isHotspotMode: boolean, zoom: number, isDragging: boolean, minZoom: number): string {\n if (isHotspotMode) return 'cursor-crosshair';\n if (isDragging) return 'cursor-grabbing';\n if (zoom > minZoom) return 'cursor-grab';\n return 'cursor-ew-resize';\n}\n","export function clampFrameIndex(index: number, frameCount: number): number {\n if (frameCount === 0) return 0;\n return ((index % frameCount) + frameCount) % frameCount;\n}\n\ntype DragStart = {\n pointerX: number;\n frameIndex: number;\n};\n\nexport function computeDragFrameIndex(\n dragStart: DragStart,\n clientX: number,\n frameCount: number,\n dragSensitivity: number\n): number | null {\n const deltaX = clientX - dragStart.pointerX;\n const frameDelta = Math.round(-deltaX / dragSensitivity);\n\n if (frameDelta === 0) return null;\n\n return clampFrameIndex(dragStart.frameIndex + frameDelta, frameCount);\n}\n","export type ViewerImageLayout = {\n width: number;\n height: number;\n centerX: number;\n centerY: number;\n drawWidth: number;\n drawHeight: number;\n offsetX: number;\n offsetY: number;\n};\n\nexport type PanOffset = {\n panX: number;\n panY: number;\n};\n\ntype ComputeViewerImageLayoutParams = {\n containerWidth: number;\n containerHeight: number;\n imageWidth: number;\n imageHeight: number;\n pan?: PanOffset;\n};\n\nexport function computeViewerImageLayout({\n containerWidth,\n containerHeight,\n imageWidth,\n imageHeight,\n pan = { panX: 0, panY: 0 },\n}: ComputeViewerImageLayoutParams): ViewerImageLayout {\n const imgAspect = imageWidth / imageHeight;\n const containerAspect = containerWidth / containerHeight;\n\n let drawWidth: number;\n let drawHeight: number;\n\n if (imgAspect > containerAspect) {\n drawWidth = containerWidth;\n drawHeight = containerWidth / imgAspect;\n } else {\n drawHeight = containerHeight;\n drawWidth = containerHeight * imgAspect;\n }\n\n const offsetX = (containerWidth - drawWidth) / 2 + pan.panX;\n const offsetY = (containerHeight - drawHeight) / 2 + pan.panY;\n\n return {\n width: containerWidth,\n height: containerHeight,\n centerX: containerWidth / 2,\n centerY: containerHeight / 2,\n drawWidth,\n drawHeight,\n offsetX,\n offsetY,\n };\n}\n\nexport function computeHotspotScreenPosition(\n hotspotX: number,\n hotspotY: number,\n layout: ViewerImageLayout,\n zoom: number\n): { leftPercent: number; topPercent: number } {\n const baseOffsetX = (layout.width - layout.drawWidth) / 2;\n const baseOffsetY = (layout.height - layout.drawHeight) / 2;\n\n const containerX = (hotspotX / 100) * layout.width;\n const containerY = (hotspotY / 100) * layout.height;\n\n const imageLocalX = (containerX - baseOffsetX) / layout.drawWidth;\n const imageLocalY = (containerY - baseOffsetY) / layout.drawHeight;\n\n const imagePointX = layout.offsetX + imageLocalX * layout.drawWidth;\n const imagePointY = layout.offsetY + imageLocalY * layout.drawHeight;\n\n const screenX = layout.centerX + zoom * (imagePointX - layout.centerX);\n const screenY = layout.centerY + zoom * (imagePointY - layout.centerY);\n\n return {\n leftPercent: (screenX / layout.width) * 100,\n topPercent: (screenY / layout.height) * 100,\n };\n}\n\nexport function computeHotspotPositionFromClick(\n clientX: number,\n clientY: number,\n containerRect: DOMRect,\n layout: ViewerImageLayout,\n zoom: number\n): { positionX: number; positionY: number } {\n const clickX = clientX - containerRect.left;\n const clickY = clientY - containerRect.top;\n\n const unzoomedX = layout.centerX + (clickX - layout.centerX) / zoom;\n const unzoomedY = layout.centerY + (clickY - layout.centerY) / zoom;\n\n const baseOffsetX = (layout.width - layout.drawWidth) / 2;\n const baseOffsetY = (layout.height - layout.drawHeight) / 2;\n\n const imageLocalX = Math.min(1, Math.max(0, (unzoomedX - layout.offsetX) / layout.drawWidth));\n const imageLocalY = Math.min(1, Math.max(0, (unzoomedY - layout.offsetY) / layout.drawHeight));\n\n const storedX = baseOffsetX + imageLocalX * layout.drawWidth;\n const storedY = baseOffsetY + imageLocalY * layout.drawHeight;\n\n return {\n positionX: Math.min(100, Math.max(0, (storedX / layout.width) * 100)),\n positionY: Math.min(100, Math.max(0, (storedY / layout.height) * 100)),\n };\n}\n","import type { PanOffset } from './computeViewerImageLayout';\n\ntype PanStart = {\n pointerX: number;\n pointerY: number;\n panX: number;\n panY: number;\n};\n\nexport function computeViewerPanOffset(panStart: PanStart, clientX: number, clientY: number): PanOffset {\n const deltaX = clientX - panStart.pointerX;\n const deltaY = clientY - panStart.pointerY;\n\n return {\n panX: panStart.panX + deltaX,\n panY: panStart.panY + deltaY,\n };\n}\n","import type { Viewer360Frame } from '../types';\n\nimport { computeViewerImageLayout, type PanOffset } from './computeViewerImageLayout';\n\ntype DrawFrameOnCanvasParams = {\n canvas: HTMLCanvasElement;\n container: HTMLDivElement;\n image: HTMLImageElement;\n zoom: number;\n pan: PanOffset;\n};\n\nexport function drawFrameOnCanvas({ canvas, container, image, zoom, pan }: DrawFrameOnCanvasParams): void {\n if (!image.complete || !image.naturalWidth) return;\n\n const rect = container.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n\n const layout = computeViewerImageLayout({\n containerWidth: rect.width,\n containerHeight: rect.height,\n imageWidth: image.naturalWidth,\n imageHeight: image.naturalHeight,\n pan,\n });\n\n ctx.save();\n ctx.translate(layout.centerX, layout.centerY);\n ctx.scale(zoom, zoom);\n ctx.translate(-layout.centerX, -layout.centerY);\n ctx.drawImage(image, layout.offsetX, layout.offsetY, layout.drawWidth, layout.drawHeight);\n ctx.restore();\n}\n\nexport function getFramesSignature(frames: Viewer360Frame[]): string {\n return frames.map((frame) => frame.id).join('-');\n}\n\nexport function preloadFrameImage(frame: Viewer360Frame): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = (): void => resolve(img);\n img.onerror = (): void => reject(new Error(`Failed to load frame: ${frame.src}`));\n img.src = frame.src;\n });\n}\n\nexport async function preloadViewerFrames(frames: Viewer360Frame[]): Promise<HTMLImageElement[]> {\n const results = await Promise.allSettled(frames.map(preloadFrameImage));\n\n return results.map((result) => (result.status === 'fulfilled' ? result.value : new Image()));\n}\n\nexport function hasLoadedViewerFrame(images: HTMLImageElement[]): boolean {\n return images.some((image) => image.complete && image.naturalWidth > 0);\n}\n\nexport function filterHotspotsByFrame<TData>(\n hotspots: Array<{ frameIndex: number; data?: TData }>,\n frameIndex: number\n): Array<{ frameIndex: number; data?: TData }> {\n return hotspots.filter((hotspot) => hotspot.frameIndex === frameIndex);\n}\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cn } from '@/components/utils';\n\nfunction Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }): JSX.Element {\n return (\n <div\n data-slot=\"card\"\n data-size={size}\n className={cn(\n 'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n 'gap-0.5 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-title\"\n className={cn('text-lg leading-normal font-semibold group-data-[size=sm]/card:text-sm', className)}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"card-description\" className={cn('text-muted-foreground text-xs font-medium', className)} {...props} />;\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-action\"\n className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}\n {...props}\n />\n );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"card-content\" className={cn('px-6 group-data-[size=sm]/card:px-4', className)} {...props} />;\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\n 'rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nconst badgeVariants = cva(\n 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',\n secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',\n destructive:\n 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',\n outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',\n ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',\n link: 'text-primary underline-offset-4 hover:underline',\n info: 'bg-blue-500 text-white hover:bg-blue-600',\n warning: 'bg-amber-500 text-white hover:bg-amber-600',\n success: 'bg-green-600 text-white hover:bg-green-700',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n }\n);\n\nfunction Badge({\n className,\n variant = 'default',\n asChild = false,\n ...props\n}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }): JSX.Element {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n const Comp = asChild ? Slot.Root : 'span';\n\n return <Comp data-slot=\"badge\" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n","import type { JSX } from 'react';\n\nimport { Badge } from '@/components/ui/Badge';\nimport { cn } from '@/components/utils';\n\ntype Viewer360AddModeBannerProps = {\n className?: string;\n label: string;\n};\n\nexport function Viewer360AddModeBanner({ className, label }: Viewer360AddModeBannerProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Badge variant=\"outline\" className={cn('pointer-events-none', className)}>\n {label}\n </Badge>\n );\n}\n","import type { JSX } from 'react';\n\nimport { Badge } from '@/components/ui/Badge';\nimport { cn } from '@/components/utils';\n\ntype Viewer360FrameIndicatorProps = {\n className?: string;\n label: string;\n};\n\nexport function Viewer360FrameIndicator({ className, label }: Viewer360FrameIndicatorProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Badge variant=\"outline\" className={cn('pointer-events-none shadow-sm', className)}>\n {label}\n </Badge>\n );\n}\n","import type { Viewer360Hotspot, Viewer360Marker } from '../types';\n\nexport function hotspotToViewer360Marker<TData>(hotspot: Viewer360Hotspot<TData>): Viewer360Marker {\n const data = hotspot.data;\n\n if (data && typeof data === 'object' && 'title' in data && typeof (data as { title?: unknown }).title === 'string') {\n const marker = data as unknown as Viewer360Marker;\n\n return {\n id: marker.id ?? hotspot.id,\n title: marker.title,\n description: marker.description,\n };\n }\n\n return {\n id: hotspot.id,\n title: hotspot.id,\n };\n}\n\nexport function toViewer360Hotspots<TData extends Viewer360Marker>(\n markers: Array<Viewer360Marker & { frameIndex: number; positionX: number; positionY: number }>,\n mapData?: (marker: Viewer360Marker & { frameIndex: number; positionX: number; positionY: number }) => TData\n): Viewer360Hotspot<TData>[] {\n return markers.map((marker) => ({\n id: marker.id,\n frameIndex: marker.frameIndex,\n positionX: marker.positionX,\n positionY: marker.positionY,\n data: mapData ? mapData(marker) : (marker as unknown as TData),\n }));\n}\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nconst buttonVariants = cva(\n 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/80',\n outline:\n 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',\n secondary:\n 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',\n ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',\n destructive:\n 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n default:\n 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',\n xs: 'h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5',\n sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5',\n lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3',\n icon: 'size-9',\n 'icon-xs':\n 'size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md',\n 'icon-sm': 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',\n 'icon-lg': 'size-10',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nfunction Button({\n className,\n variant = 'default',\n size = 'default',\n asChild = false,\n ...props\n}: React.ComponentProps<'button'> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }): JSX.Element {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n const Comp = asChild ? Slot.Root : 'button';\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n );\n}\n\nexport { Button, buttonVariants };\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { Separator } from '@/components/ui/Separator';\nimport { cn } from '@/components/utils';\n\nfunction ItemGroup({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n role=\"list\"\n data-slot=\"item-group\"\n className={cn('gap-4 has-[[data-size=sm]]:gap-2.5 has-[[data-size=xs]]:gap-2 group/item-group flex w-full flex-col', className)}\n {...props}\n />\n );\n}\n\nfunction ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>): JSX.Element {\n return <Separator data-slot=\"item-separator\" orientation=\"horizontal\" className={cn('my-2', className)} {...props} />;\n}\n\nconst itemVariants = cva(\n '[a]:hover:bg-muted rounded-md border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors',\n {\n variants: {\n variant: {\n default: 'border-transparent',\n outline: 'border-border',\n muted: 'bg-muted/50 border-transparent',\n },\n size: {\n default: 'gap-3.5 px-4 py-3.5',\n sm: 'gap-2.5 px-3 py-2.5',\n xs: 'gap-2 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nfunction Item({\n className,\n variant = 'default',\n size = 'default',\n asChild = false,\n ...props\n}: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & { asChild?: boolean }): JSX.Element {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n const Comp = asChild ? Slot.Root : 'div';\n return (\n <Comp\n data-slot=\"item\"\n data-variant={variant}\n data-size={size}\n className={cn(itemVariants({ variant, size, className }))}\n {...props}\n />\n );\n}\n\nconst itemMediaVariants = cva(\n 'gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',\n {\n variants: {\n variant: {\n default: 'bg-transparent',\n icon: '[&_svg]:size-4',\n image: 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n }\n);\n\nfunction ItemMedia({\n className,\n variant = 'default',\n ...props\n}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>): JSX.Element {\n return <div data-slot=\"item-media\" data-variant={variant} className={cn(itemMediaVariants({ variant, className }))} {...props} />;\n}\n\nfunction ItemContent({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"item-content\"\n className={cn('gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none', className)}\n {...props}\n />\n );\n}\n\nfunction ItemTitle({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"item-title\"\n className={cn('gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center', className)}\n {...props}\n />\n );\n}\n\nfunction ItemDescription({ className, ...props }: React.ComponentProps<'p'>): JSX.Element {\n return (\n <p\n data-slot=\"item-description\"\n className={cn(\n 'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4',\n className\n )}\n {...props}\n />\n );\n}\n\nfunction ItemActions({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"item-actions\" className={cn('gap-2 flex items-center', className)} {...props} />;\n}\n\nfunction ItemHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"item-header\" className={cn('gap-2 flex basis-full items-center justify-between', className)} {...props} />;\n}\n\nfunction ItemFooter({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"item-footer\" className={cn('gap-2 flex basis-full items-center justify-between', className)} {...props} />;\n}\n\nexport { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter };\n","'use client';\n\nimport * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { Separator as SeparatorPrimitive } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nfunction Separator({\n className,\n orientation = 'horizontal',\n decorative = true,\n ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>): JSX.Element {\n return (\n <SeparatorPrimitive.Root\n data-slot=\"separator\"\n decorative={decorative}\n orientation={orientation}\n className={cn(\n 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch',\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Separator };\n","import type { JSX } from 'react';\nimport { useState } from 'react';\n\nimport { Trash2 } from 'lucide-react';\n\nimport { defaultViewer360MarkerPinLabels } from '../constants/viewer360MarkerLabels';\nimport { viewer360MarkerPinClassNames } from '../constants/viewer360ClassNames';\nimport type { Viewer360MarkerPinProps } from '../types';\nimport { Badge } from '@/components/ui/Badge';\nimport { Button } from '@/components/ui/Button';\nimport {\n Item,\n ItemActions,\n ItemContent,\n ItemDescription,\n ItemTitle,\n} from '@/components/ui/Item';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover';\nimport { cn } from '@/components/utils';\n\nexport function Viewer360MarkerPin<TData = unknown>({\n marker,\n hotspot,\n leftPercent,\n topPercent,\n onDelete,\n isDeletePending = false,\n renderTag,\n classNames,\n labels,\n}: Viewer360MarkerPinProps<TData>): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: States & Constants\n // ----------------------------------------------------------------------------------------------------\n const [isOpen, setIsOpen] = useState(false);\n const deleteLabel = labels?.delete ?? defaultViewer360MarkerPinLabels.delete;\n\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Popover open={isOpen} onOpenChange={setIsOpen}>\n <Item\n size=\"xs\"\n variant=\"default\"\n className={cn(viewer360MarkerPinClassNames.root, classNames?.root, 'w-auto border-transparent p-0')}\n style={{ left: `${leftPercent}%`, top: `${topPercent}%` }}\n onMouseEnter={() => setIsOpen(true)}\n onMouseLeave={() => setIsOpen(false)}\n >\n <Badge\n variant=\"destructive\"\n className={cn(viewer360MarkerPinClassNames.ping, classNames?.ping, 'absolute size-6 border-0 bg-destructive opacity-60')}\n aria-hidden=\"true\"\n />\n\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"destructive\"\n size=\"icon-xs\"\n className={cn(\n viewer360MarkerPinClassNames.dot,\n classNames?.dot,\n 'size-4 min-h-4 min-w-4 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:scale-125 hover:bg-destructive'\n )}\n aria-label={marker.title}\n />\n </PopoverTrigger>\n\n <PopoverContent\n className={cn(viewer360MarkerPinClassNames.tooltip, classNames?.tooltip, 'w-64 p-0')}\n side=\"top\"\n align=\"center\"\n onOpenAutoFocus={(event) => event.preventDefault()}\n >\n <Item\n size=\"sm\"\n variant=\"default\"\n className={cn(viewer360MarkerPinClassNames.tooltipHeader, classNames?.tooltipHeader, 'w-full border-transparent')}\n >\n <ItemContent className={cn(viewer360MarkerPinClassNames.tooltipBody, classNames?.tooltipBody)}>\n <ItemTitle className={cn(viewer360MarkerPinClassNames.tooltipTitle, classNames?.tooltipTitle)}>\n {marker.title}\n </ItemTitle>\n {renderTag?.({ marker, hotspot })}\n {marker.description && (\n <ItemDescription className={cn(viewer360MarkerPinClassNames.tooltipDescription, classNames?.tooltipDescription)}>\n {marker.description}\n </ItemDescription>\n )}\n </ItemContent>\n {onDelete && (\n <ItemActions>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n className={classNames?.deleteButton}\n disabled={isDeletePending}\n aria-label={deleteLabel}\n onClick={(event) => {\n event.stopPropagation();\n onDelete(marker.id);\n }}\n >\n <Trash2 className=\"size-4\" />\n </Button>\n </ItemActions>\n )}\n </Item>\n </PopoverContent>\n </Item>\n </Popover>\n );\n}\n","export const defaultViewer360MarkerPinLabels = {\n delete: 'Remove marker',\n} as const;\n","'use client';\n\nimport * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { Popover as PopoverPrimitive } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nfunction Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>): JSX.Element {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>): JSX.Element {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n className,\n align = 'center',\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>): JSX.Element {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-md p-4 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--radix-popover-content-transform-origin) outline-hidden',\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n );\n}\n\nfunction PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>): JSX.Element {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"popover-header\" className={cn('flex flex-col gap-1 text-sm', className)} {...props} />;\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>): JSX.Element {\n return <div data-slot=\"popover-title\" className={cn('font-medium', className)} {...props} />;\n}\n\nfunction PopoverDescription({ className, ...props }: React.ComponentProps<'p'>): JSX.Element {\n return <p data-slot=\"popover-description\" className={cn('text-muted-foreground', className)} {...props} />;\n}\n\nexport { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };\n","import type { JSX, MouseEvent, ReactNode } from 'react';\n\nimport { hotspotToViewer360Marker } from '../helpers/markerHelpers';\nimport type { Viewer360Hotspot, Viewer360HotspotPinOptions, Viewer360HotspotRenderProps } from '../types';\nimport { Button } from '@/components/ui/Button';\nimport { Item } from '@/components/ui/Item';\nimport { cn } from '@/components/utils';\n\nimport { Viewer360MarkerPin } from './Viewer360MarkerPin';\n\ntype Viewer360HotspotOverlayProps<TData = unknown> = {\n hotspot: Viewer360Hotspot<TData>;\n leftPercent: number;\n topPercent: number;\n hotspotPin?: Viewer360HotspotPinOptions<TData>;\n renderHotspot?: (props: Viewer360HotspotRenderProps<TData>) => ReactNode;\n onHotspotClick?: (hotspot: Viewer360Hotspot<TData>, event: MouseEvent<HTMLDivElement>) => void;\n};\n\nexport function Viewer360HotspotOverlay<TData = unknown>({\n hotspot,\n leftPercent,\n topPercent,\n hotspotPin,\n renderHotspot,\n onHotspotClick,\n}: Viewer360HotspotOverlayProps<TData>): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n if (renderHotspot) {\n return (\n <Item size=\"xs\" variant=\"default\" className=\"pointer-events-auto w-auto border-transparent p-0\">\n {renderHotspot({ hotspot, leftPercent, topPercent })}\n </Item>\n );\n }\n\n if (hotspotPin) {\n const marker = hotspotPin.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);\n\n return (\n <Viewer360MarkerPin\n marker={marker}\n hotspot={hotspot}\n leftPercent={leftPercent}\n topPercent={topPercent}\n onDelete={hotspotPin.onDelete}\n isDeletePending={hotspotPin.deletingMarkerId === hotspot.id}\n renderTag={hotspotPin.renderTag}\n classNames={hotspotPin.classNames}\n labels={hotspotPin.labels}\n />\n );\n }\n\n return (\n <Button\n type=\"button\"\n variant=\"destructive\"\n size=\"icon-xs\"\n className={cn(\n 'pointer-events-auto absolute z-30 size-4 min-h-4 min-w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:bg-destructive'\n )}\n style={{ left: `${leftPercent}%`, top: `${topPercent}%` }}\n aria-label={`Hotspot ${hotspot.id}`}\n onClick={(event) => onHotspotClick?.(hotspot, event as unknown as MouseEvent<HTMLDivElement>)}\n />\n );\n}\n","'use client';\n\nimport * as React from 'react';\n\nimport { cn } from '@/components/utils';\n\nfunction Label({ className, ...props }: React.ComponentProps<'label'>): React.ReactNode {\n return (\n <label\n data-slot=\"label\"\n className={cn(\n 'gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed',\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Label };\n","import type { JSX } from 'react';\n\nimport { Loader2Icon } from 'lucide-react';\n\nimport { cn } from '@/components/utils';\n\nfunction Spinner({ className, ...props }: React.ComponentProps<'svg'>): JSX.Element {\n return <Loader2Icon role=\"status\" aria-label=\"Loading\" className={cn('size-4 animate-spin', className)} {...props} />;\n}\n\nexport { Spinner };\n","import type { JSX } from 'react';\n\nimport { Item } from '@/components/ui/Item';\nimport { Label } from '@/components/ui/Label';\nimport { Spinner } from '@/components/ui/Spinner';\nimport { cn } from '@/components/utils';\n\ntype Viewer360LoadingOverlayProps = {\n className?: string;\n textClassName?: string;\n label: string;\n};\n\nexport function Viewer360LoadingOverlay({ className, textClassName, label }: Viewer360LoadingOverlayProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Item\n size=\"sm\"\n variant=\"muted\"\n className={cn('pointer-events-none w-auto justify-center border-transparent bg-muted/80', className)}\n >\n <Spinner className=\"size-5 text-muted-foreground\" />\n <Label className={cn('font-normal text-muted-foreground', textClassName)}>{label}</Label>\n </Item>\n );\n}\n","import type { JSX } from 'react';\n\nimport { Crosshair, Minus, Plus, RotateCcw, ZoomIn } from 'lucide-react';\n\nimport { viewer360ClassNames } from '../constants/viewer360ClassNames';\nimport type { Viewer360ToolbarRenderProps } from '../types';\nimport { Badge } from '@/components/ui/Badge';\nimport { Button } from '@/components/ui/Button';\nimport { CardFooter } from '@/components/ui/Card';\nimport { Label } from '@/components/ui/Label';\nimport { Separator } from '@/components/ui/Separator';\nimport { cn } from '@/components/utils';\n\ntype Viewer360ToolbarProps = Viewer360ToolbarRenderProps;\n\nexport function Viewer360Toolbar({\n showDragHint,\n showHotspotModeControl,\n showZoomControls,\n showResetControl,\n labels,\n isHotspotMode,\n zoom,\n minZoom,\n maxZoom,\n isResetDisabled,\n onHotspotModeChange,\n onZoomIn,\n onZoomOut,\n onResetView,\n}: Viewer360ToolbarProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <CardFooter className={cn(viewer360ClassNames.toolbar, 'gap-2 border-t px-4 py-3 pt-3')}>\n {showDragHint && (\n <Label className={cn(viewer360ClassNames.dragHint, 'font-normal text-muted-foreground')}>{labels.dragHint}</Label>\n )}\n\n <div className={viewer360ClassNames.controls}>\n {showHotspotModeControl && (\n <>\n <Button variant={isHotspotMode ? 'default' : 'outline'} size=\"sm\" onClick={() => onHotspotModeChange(!isHotspotMode)}>\n <Crosshair className=\"me-1.5 size-4\" />\n {labels.addHotspot}\n </Button>\n <Separator orientation=\"vertical\" className={cn(viewer360ClassNames.divider, 'h-6')} />\n </>\n )}\n\n {showZoomControls && (\n <>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={zoom <= minZoom} aria-label={labels.zoomOut} onClick={onZoomOut}>\n <Minus className=\"size-4\" />\n </Button>\n <Badge variant=\"outline\" className={cn(viewer360ClassNames.zoomDisplay, 'h-8 gap-1 px-2 py-1')}>\n <ZoomIn className=\"size-3 text-muted-foreground\" />\n {labels.zoom(Math.round(zoom * 100))}\n </Badge>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={zoom >= maxZoom} aria-label={labels.zoomIn} onClick={onZoomIn}>\n <Plus className=\"size-4\" />\n </Button>\n </>\n )}\n\n {showResetControl && (\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isResetDisabled} aria-label={labels.resetView} onClick={onResetView}>\n <RotateCcw className=\"size-4\" />\n </Button>\n )}\n </div>\n </CardFooter>\n );\n}\n","export const viewer360Config = {\n minZoom: 1,\n maxZoom: 3,\n zoomStep: 0.15,\n dragSensitivity: 8,\n autoRotate: false,\n autoRotateIntervalMs: 100,\n autoRotateDirection: 'forward' as const,\n};\n\nexport const defaultViewer360Config = viewer360Config;\n"],"mappings":"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","names":["useEffect","useMemo","useState","jsx","jsx","jsx","cva","Slot","jsx","cva","Slot","cva","Slot","jsx","jsx","cva","Slot","jsx","jsx","useState","jsx","jsx","useState","jsx","jsx","jsx","jsx","jsxs","jsx","jsxs","isResetDisabled","jsx","jsxs","useMemo","useState","isResetDisabled","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/feature/Viewer360.tsx","../src/constants/viewer360Labels.ts","../src/constants/viewer360ClassNames.ts","../src/components/utils/index.ts","../src/helpers/viewer360PropsHelpers.ts","../src/hooks/useViewer360.ts","../src/helpers/adjustViewerZoom.ts","../src/helpers/computeDragFrameIndex.ts","../src/helpers/computeViewerImageLayout.ts","../src/helpers/computeViewerPanOffset.ts","../src/helpers/viewerHelpers.ts","../src/components/ui/Card/index.tsx","../src/components/ui/Badge/index.tsx","../src/feature/Viewer360AddModeBanner.tsx","../src/feature/Viewer360FrameIndicator.tsx","../src/helpers/markerHelpers.ts","../src/components/ui/Item/index.tsx","../src/components/ui/Separator/index.tsx","../src/feature/Viewer360MarkerPin.tsx","../src/constants/viewer360MarkerLabels.ts","../src/components/ui/Button/index.tsx","../src/feature/Viewer360HotspotOverlay.tsx","../src/components/ui/Label/index.tsx","../src/components/ui/Spinner/index.tsx","../src/feature/Viewer360LoadingOverlay.tsx","../src/feature/Viewer360Toolbar.tsx","../src/constants/viewer360Config.ts"],"sourcesContent":["import type { JSX } from 'react';\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { buildViewer360ThemeStyle, mergeViewer360ClassNames, mergeViewer360Labels } from '../helpers/viewer360PropsHelpers';\nimport { useViewer360 } from '../hooks/useViewer360';\nimport type { Viewer360OverlayRenderProps, Viewer360Props, Viewer360ToolbarRenderProps } from '../types';\nimport { Card } from '@/components/ui/Card';\nimport { cn } from '@/components/utils';\n\nimport { Viewer360AddModeBanner } from './Viewer360AddModeBanner';\nimport { Viewer360FrameIndicator } from './Viewer360FrameIndicator';\nimport { Viewer360HotspotOverlay } from './Viewer360HotspotOverlay';\nimport { Viewer360LoadingOverlay } from './Viewer360LoadingOverlay';\nimport { Viewer360Toolbar } from './Viewer360Toolbar';\n\nexport function Viewer360<TData = unknown>({\n frames,\n currentFrameIndex: controlledFrameIndex,\n defaultFrameIndex = 0,\n onFrameChange,\n config,\n className,\n classNames,\n style,\n theme,\n labels,\n aspectRatio = '16 / 10',\n showZoomControls = true,\n showResetControl = true,\n showFrameIndicator = true,\n showDragHint = true,\n showHotspotModeControl = false,\n hotspotPin,\n hotspots = [],\n renderHotspot,\n renderLoading,\n renderFrameIndicator,\n renderHotspotModeBanner,\n renderToolbar,\n onHotspotClick,\n hotspotMode: controlledHotspotMode,\n defaultHotspotMode = false,\n onHotspotModeChange,\n onHotspotAdd,\n children,\n}: Viewer360Props<TData>): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: States & Constants\n // ----------------------------------------------------------------------------------------------------\n const mergedLabels = useMemo(() => mergeViewer360Labels(labels), [labels]);\n const mergedClassNames = useMemo(() => mergeViewer360ClassNames(classNames), [classNames]);\n const themeStyle = useMemo(() => buildViewer360ThemeStyle(theme), [theme]);\n\n const [internalFrameIndex, setInternalFrameIndex] = useState(defaultFrameIndex);\n const [internalHotspotMode, setInternalHotspotMode] = useState(defaultHotspotMode);\n\n const currentFrameIndex = controlledFrameIndex ?? internalFrameIndex;\n const hotspotMode = controlledHotspotMode ?? internalHotspotMode;\n\n // ----------------------------------------------------------------------------------------------------\n // MARK: Functions\n // ----------------------------------------------------------------------------------------------------\n function handleFrameChange(index: number): void {\n if (controlledFrameIndex === undefined) {\n setInternalFrameIndex(index);\n }\n\n onFrameChange?.(index);\n }\n\n function handleHotspotModeChange(active: boolean): void {\n if (controlledHotspotMode === undefined) {\n setInternalHotspotMode(active);\n }\n\n onHotspotModeChange?.(active);\n }\n\n const {\n canvasRef,\n containerRef,\n currentFrame,\n currentFrameHotspots,\n imagesLoaded,\n isHotspotMode,\n isResetDisabled,\n maxZoom,\n minZoom,\n viewerCursorClass,\n zoom,\n getHotspotScreenPosition,\n handleCanvasClick,\n handlePointerDown,\n handlePointerMove,\n handlePointerUp,\n handleWheel,\n handleResetView,\n handleZoomIn,\n handleZoomOut,\n } = useViewer360<TData>({\n frames,\n hotspots,\n currentFrameIndex,\n onFrameChange: handleFrameChange,\n config,\n hotspotMode,\n onHotspotAdd,\n });\n\n useEffect(() => {\n if (controlledHotspotMode === undefined) return;\n if (controlledHotspotMode !== internalHotspotMode) {\n setInternalHotspotMode(controlledHotspotMode);\n }\n }, [controlledHotspotMode, internalHotspotMode]);\n\n const frameLabel = currentFrame?.label ?? frames[currentFrameIndex]?.label;\n const overlayProps: Viewer360OverlayRenderProps = {\n currentFrameIndex,\n frameCount: frames.length,\n frameLabel,\n isHotspotMode,\n labels: mergedLabels,\n };\n const toolbarProps: Viewer360ToolbarRenderProps = {\n zoom,\n minZoom,\n maxZoom,\n isResetDisabled,\n isHotspotMode,\n showHotspotModeControl,\n showZoomControls,\n showResetControl,\n showDragHint,\n labels: mergedLabels,\n onZoomIn: handleZoomIn,\n onZoomOut: handleZoomOut,\n onResetView: handleResetView,\n onHotspotModeChange: handleHotspotModeChange,\n };\n const showDefaultToolbar = showZoomControls || showResetControl || showHotspotModeControl || showDragHint;\n\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Card className={cn(mergedClassNames.root, 'gap-0 py-0 shadow-none ring-0', className)} style={{ ...themeStyle, ...style }}>\n <div\n ref={containerRef}\n className={cn(mergedClassNames.viewport, viewerCursorClass)}\n style={{ aspectRatio }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerLeave={handlePointerUp}\n onWheel={handleWheel}\n onClick={handleCanvasClick}\n >\n <canvas ref={canvasRef} className={mergedClassNames.canvas} />\n\n <div className={mergedClassNames.overlay}>\n {currentFrameHotspots.map((hotspot) => {\n const position = getHotspotScreenPosition(hotspot);\n\n return (\n <Viewer360HotspotOverlay\n key={hotspot.id}\n hotspot={hotspot}\n leftPercent={position.leftPercent}\n topPercent={position.topPercent}\n hotspotPin={hotspotPin}\n renderHotspot={renderHotspot}\n onHotspotClick={onHotspotClick}\n />\n );\n })}\n {children}\n </div>\n\n {!imagesLoaded &&\n (renderLoading ? (\n renderLoading()\n ) : (\n <Viewer360LoadingOverlay\n className={mergedClassNames.loading}\n textClassName={mergedClassNames.loadingText}\n label={mergedLabels.loading}\n />\n ))}\n\n {showFrameIndicator &&\n frames.length > 0 &&\n (renderFrameIndicator ? (\n renderFrameIndicator(overlayProps)\n ) : (\n <Viewer360FrameIndicator\n className={mergedClassNames.frameIndicator}\n label={mergedLabels.frameIndicator({\n current: currentFrameIndex + 1,\n total: frames.length,\n label: frameLabel,\n })}\n />\n ))}\n\n {isHotspotMode &&\n onHotspotAdd &&\n (renderHotspotModeBanner ? (\n renderHotspotModeBanner({ labels: mergedLabels })\n ) : (\n <Viewer360AddModeBanner className={mergedClassNames.hotspotModeBanner} label={mergedLabels.hotspotModeActive} />\n ))}\n </div>\n\n {renderToolbar ? renderToolbar(toolbarProps) : showDefaultToolbar ? <Viewer360Toolbar {...toolbarProps} /> : null}\n </Card>\n );\n}\n","import type { Viewer360Labels } from '../types/Viewer360Props';\n\nexport const defaultViewer360Labels: Required<Viewer360Labels> = {\n loading: 'Loading images…',\n dragHint: 'Drag to rotate • Scroll to zoom',\n frameIndicator: ({ current, total, label }) => (label ? `${label} · ${current} / ${total}` : `${current} / ${total}`),\n zoom: (percent) => `${percent}%`,\n hotspotModeActive: 'Click on the image to place a hotspot',\n addHotspot: 'Add hotspot',\n zoomIn: 'Zoom in',\n zoomOut: 'Zoom out',\n resetView: 'Reset view',\n deleteMarker: 'Remove marker',\n};\n","import type { Viewer360ClassNames } from '../types/Viewer360Props';\nimport type { Viewer360MarkerPinClassNames } from '../types/Viewer360Marker';\n\nexport const viewer360ClassNames: Required<Viewer360ClassNames> = {\n root: 'overflow-hidden rounded-lg border bg-card text-card-foreground',\n viewport: 'relative aspect-[16/10] w-full touch-none select-none bg-muted',\n canvas: 'absolute inset-0 size-full',\n overlay: 'pointer-events-none absolute inset-0 overflow-hidden',\n loading: 'absolute inset-0 flex items-center justify-center bg-muted/80',\n loadingText: 'text-sm text-muted-foreground',\n frameIndicator:\n 'pointer-events-none absolute bottom-4 start-4 z-20 rounded-full border bg-background px-4 py-1.5 text-xs font-medium shadow-sm',\n hotspotModeBanner:\n 'pointer-events-none absolute top-4 start-1/2 z-20 -translate-x-1/2 rounded-full border border-amber-200 bg-amber-50 px-4 py-1.5 text-xs font-medium text-amber-800 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400',\n toolbar: 'flex flex-wrap items-center justify-between gap-2 border-t px-4 py-3',\n dragHint: 'hidden text-xs text-muted-foreground sm:block',\n controls: 'ms-auto flex items-center gap-1.5',\n controlButton: '',\n controlButtonActive: '',\n controlButtonDisabled: '',\n zoomDisplay: 'flex min-w-[3rem] items-center justify-center gap-1 rounded-md border bg-background px-2 py-1 text-xs font-medium',\n divider: 'mx-1 h-6 w-px bg-border',\n};\n\nexport const viewer360MarkerPinClassNames: Required<Viewer360MarkerPinClassNames> = {\n root: 'pointer-events-auto absolute z-30 -translate-x-1/2 -translate-y-1/2',\n ping: 'absolute inline-flex size-6 -translate-x-1/4 -translate-y-1/4 animate-ping rounded-full bg-destructive opacity-60',\n dot: 'relative flex size-4 items-center justify-center rounded-full border-2 border-background bg-destructive shadow-md transition-transform duration-200 hover:scale-125 focus:outline-none',\n tooltip:\n 'absolute bottom-6 left-1/2 z-40 w-64 -translate-x-1/2 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md',\n tooltipHeader: 'flex items-start justify-between gap-2',\n tooltipBody: 'flex min-w-0 flex-col gap-1',\n tooltipTitle: 'text-sm font-medium',\n tooltipDescription: 'mt-2 line-clamp-3 text-xs text-muted-foreground',\n deleteButton: '',\n};\n\n/** @deprecated Use `viewer360ClassNames` */\nexport const defaultViewer360ClassNames = viewer360ClassNames;\n\n/** @deprecated Use `viewer360MarkerPinClassNames` */\nexport const defaultViewer360MarkerPinClassNames = viewer360MarkerPinClassNames;\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import type { CSSProperties } from 'react';\n\nimport { defaultViewer360Labels } from '../constants/viewer360Labels';\nimport { viewer360ClassNames } from '../constants/viewer360ClassNames';\nimport type { Viewer360ClassNames, Viewer360Labels, Viewer360Theme } from '../types';\nimport { cn } from '@/components/utils';\n\nexport function mergeViewer360Labels(labels?: Viewer360Labels): Required<Viewer360Labels> {\n return {\n loading: labels?.loading ?? defaultViewer360Labels.loading,\n dragHint: labels?.dragHint ?? defaultViewer360Labels.dragHint,\n frameIndicator: labels?.frameIndicator ?? defaultViewer360Labels.frameIndicator,\n zoom: labels?.zoom ?? defaultViewer360Labels.zoom,\n hotspotModeActive: labels?.hotspotModeActive ?? defaultViewer360Labels.hotspotModeActive,\n addHotspot: labels?.addHotspot ?? defaultViewer360Labels.addHotspot,\n zoomIn: labels?.zoomIn ?? defaultViewer360Labels.zoomIn,\n zoomOut: labels?.zoomOut ?? defaultViewer360Labels.zoomOut,\n resetView: labels?.resetView ?? defaultViewer360Labels.resetView,\n deleteMarker: labels?.deleteMarker ?? defaultViewer360Labels.deleteMarker,\n };\n}\n\nexport function mergeViewer360ClassNames(classNames?: Viewer360ClassNames): Required<Viewer360ClassNames> {\n return {\n root: cn(viewer360ClassNames.root, classNames?.root),\n viewport: cn(viewer360ClassNames.viewport, classNames?.viewport),\n canvas: cn(viewer360ClassNames.canvas, classNames?.canvas),\n overlay: cn(viewer360ClassNames.overlay, classNames?.overlay),\n loading: cn(viewer360ClassNames.loading, classNames?.loading),\n loadingText: cn(viewer360ClassNames.loadingText, classNames?.loadingText),\n frameIndicator: cn(viewer360ClassNames.frameIndicator, classNames?.frameIndicator),\n hotspotModeBanner: cn(viewer360ClassNames.hotspotModeBanner, classNames?.hotspotModeBanner),\n toolbar: cn(viewer360ClassNames.toolbar, classNames?.toolbar),\n dragHint: cn(viewer360ClassNames.dragHint, classNames?.dragHint),\n controls: cn(viewer360ClassNames.controls, classNames?.controls),\n controlButton: cn(viewer360ClassNames.controlButton, classNames?.controlButton),\n controlButtonActive: cn(viewer360ClassNames.controlButtonActive, classNames?.controlButtonActive),\n controlButtonDisabled: cn(viewer360ClassNames.controlButtonDisabled, classNames?.controlButtonDisabled),\n zoomDisplay: cn(viewer360ClassNames.zoomDisplay, classNames?.zoomDisplay),\n divider: cn(viewer360ClassNames.divider, classNames?.divider),\n };\n}\n\nexport function buildViewer360ThemeStyle(theme?: Viewer360Theme): CSSProperties {\n return theme ? (theme as CSSProperties) : {};\n}\n","import type { PointerEvent as ReactPointerEvent, RefObject, WheelEvent as ReactWheelEvent } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n applyWheelZoom,\n getViewerCursorClass,\n isResetDisabled,\n resolveViewer360Config,\n stepZoomIn,\n stepZoomOut,\n} from '../helpers/adjustViewerZoom';\nimport { computeDragFrameIndex } from '../helpers/computeDragFrameIndex';\nimport {\n computeHotspotPositionFromClick,\n computeHotspotScreenPosition,\n computeViewerImageLayout,\n type ViewerImageLayout,\n} from '../helpers/computeViewerImageLayout';\nimport { computeViewerPanOffset } from '../helpers/computeViewerPanOffset';\nimport {\n drawFrameOnCanvas,\n filterHotspotsByFrame,\n getFramesSignature,\n hasLoadedViewerFrame,\n preloadViewerFrames,\n} from '../helpers/viewerHelpers';\nimport type {\n Viewer360Config,\n Viewer360Frame,\n Viewer360Hotspot,\n Viewer360HotspotPosition,\n Viewer360PanOffset,\n} from '../types';\n\ntype UseViewer360Params<TData> = {\n frames: Viewer360Frame[];\n currentFrameIndex: number;\n onFrameChange: (index: number) => void;\n hotspots?: Viewer360Hotspot<TData>[];\n config?: Viewer360Config;\n hotspotMode?: boolean;\n onHotspotAdd?: (position: Viewer360HotspotPosition) => void;\n};\n\ntype UseViewer360Return<TData> = {\n canvasRef: RefObject<HTMLCanvasElement | null>;\n containerRef: RefObject<HTMLDivElement | null>;\n currentFrame: Viewer360Frame | undefined;\n currentFrameHotspots: Viewer360Hotspot<TData>[];\n imagesLoaded: boolean;\n isHotspotMode: boolean;\n isResetDisabled: boolean;\n maxZoom: number;\n minZoom: number;\n viewerCursorClass: string;\n zoom: number;\n getCurrentImageLayout: () => ViewerImageLayout | null;\n getHotspotScreenPosition: (hotspot: Viewer360Hotspot<TData>) => { leftPercent: number; topPercent: number };\n handleCanvasClick: (event: React.MouseEvent<HTMLDivElement>) => void;\n handlePointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;\n handlePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;\n handlePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;\n handleResetView: () => void;\n handleWheel: (event: ReactWheelEvent<HTMLDivElement>) => void;\n handleZoomIn: () => void;\n handleZoomOut: () => void;\n};\n\nexport function useViewer360<TData = unknown>({\n frames,\n hotspots = [],\n currentFrameIndex,\n onFrameChange,\n config,\n hotspotMode: hotspotModeProp = false,\n onHotspotAdd,\n}: UseViewer360Params<TData>): UseViewer360Return<TData> {\n const resolvedConfig = useMemo(() => resolveViewer360Config(config), [config]);\n const { minZoom, maxZoom } = resolvedConfig;\n\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n const imagesRef = useRef<HTMLImageElement[]>([]);\n const dragStartRef = useRef<{ pointerX: number; frameIndex: number } | null>(null);\n const panStartRef = useRef<{ pointerX: number; pointerY: number; panX: number; panY: number } | null>(null);\n\n const framesSignature = getFramesSignature(frames);\n const [loadedSignature, setLoadedSignature] = useState<string | null>(null);\n const [zoom, setZoom] = useState<number>(minZoom);\n const [pan, setPan] = useState<Viewer360PanOffset>({ panX: 0, panY: 0 });\n const [isDragging, setIsDragging] = useState(false);\n\n const isHotspotMode = hotspotModeProp;\n const imagesLoaded = loadedSignature === framesSignature;\n const currentFrameHotspots = filterHotspotsByFrame(hotspots, currentFrameIndex) as Viewer360Hotspot<TData>[];\n const currentFrame = frames[currentFrameIndex];\n const viewerCursorClass = getViewerCursorClass(isHotspotMode, zoom, isDragging, minZoom);\n const resetDisabled = isResetDisabled(zoom, pan, resolvedConfig);\n\n useEffect(() => {\n let cancelled = false;\n imagesRef.current = [];\n\n async function loadFrames(): Promise<void> {\n const loadedImages = await preloadViewerFrames(frames);\n\n if (!cancelled && hasLoadedViewerFrame(loadedImages)) {\n imagesRef.current = loadedImages;\n setLoadedSignature(framesSignature);\n }\n }\n\n void loadFrames();\n\n return (): void => {\n cancelled = true;\n };\n }, [frames, framesSignature]);\n\n useEffect(() => {\n if (!imagesLoaded) return;\n\n function renderFrame(): void {\n const canvas = canvasRef.current;\n const container = containerRef.current;\n const img = imagesRef.current[currentFrameIndex];\n\n if (!canvas || !container) return;\n\n drawFrameOnCanvas({ canvas, container, image: img, zoom, pan });\n }\n\n renderFrame();\n window.addEventListener('resize', renderFrame);\n\n return (): void => window.removeEventListener('resize', renderFrame);\n }, [imagesLoaded, currentFrameIndex, zoom, pan]);\n\n useEffect(() => {\n if (!resolvedConfig.autoRotate || frames.length <= 1 || isDragging || isHotspotMode || zoom > minZoom) {\n return;\n }\n\n const direction = resolvedConfig.autoRotateDirection === 'backward' ? -1 : 1;\n const interval = window.setInterval(() => {\n onFrameChange((currentFrameIndex + direction + frames.length) % frames.length);\n }, resolvedConfig.autoRotateIntervalMs);\n\n return (): void => window.clearInterval(interval);\n }, [\n resolvedConfig.autoRotate,\n resolvedConfig.autoRotateDirection,\n resolvedConfig.autoRotateIntervalMs,\n frames.length,\n currentFrameIndex,\n isDragging,\n isHotspotMode,\n zoom,\n minZoom,\n onFrameChange,\n ]);\n\n function getCurrentImageLayout(): ViewerImageLayout | null {\n const container = containerRef.current;\n const image = imagesRef.current[currentFrameIndex];\n\n if (!container || !image || !image.complete || !image.naturalWidth) return null;\n\n const rect = container.getBoundingClientRect();\n\n return computeViewerImageLayout({\n containerWidth: rect.width,\n containerHeight: rect.height,\n imageWidth: image.naturalWidth,\n imageHeight: image.naturalHeight,\n pan,\n });\n }\n\n function getHotspotScreenPosition(hotspot: Viewer360Hotspot<TData>): { leftPercent: number; topPercent: number } {\n const layout = getCurrentImageLayout();\n\n if (!layout) {\n return { leftPercent: hotspot.positionX, topPercent: hotspot.positionY };\n }\n\n return computeHotspotScreenPosition(hotspot.positionX, hotspot.positionY, layout, zoom);\n }\n\n function handlePointerDown(event: ReactPointerEvent<HTMLDivElement>): void {\n if (isHotspotMode) return;\n\n event.currentTarget.setPointerCapture(event.pointerId);\n\n if (zoom > minZoom) {\n panStartRef.current = { pointerX: event.clientX, pointerY: event.clientY, panX: pan.panX, panY: pan.panY };\n } else {\n dragStartRef.current = { pointerX: event.clientX, frameIndex: currentFrameIndex };\n }\n\n setIsDragging(true);\n }\n\n function handlePointerMove(event: ReactPointerEvent<HTMLDivElement>): void {\n if (!isDragging) return;\n\n if (panStartRef.current && zoom > minZoom) {\n setPan(computeViewerPanOffset(panStartRef.current, event.clientX, event.clientY));\n return;\n }\n\n if (dragStartRef.current && zoom <= minZoom) {\n const nextFrameIndex = computeDragFrameIndex(\n dragStartRef.current,\n event.clientX,\n frames.length,\n resolvedConfig.dragSensitivity\n );\n\n if (nextFrameIndex !== null) {\n onFrameChange(nextFrameIndex);\n }\n }\n }\n\n function handlePointerUp(event: ReactPointerEvent<HTMLDivElement>): void {\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n\n dragStartRef.current = null;\n panStartRef.current = null;\n setIsDragging(false);\n }\n\n function handleWheel(event: ReactWheelEvent<HTMLDivElement>): void {\n event.preventDefault();\n const { zoom: nextZoom, pan: nextPan } = applyWheelZoom(zoom, event.deltaY, pan, resolvedConfig);\n setZoom(nextZoom);\n setPan(nextPan);\n }\n\n function handleCanvasClick(event: React.MouseEvent<HTMLDivElement>): void {\n if (!isHotspotMode || isDragging || !onHotspotAdd) return;\n\n const frame = frames[currentFrameIndex];\n if (!frame) return;\n\n const layout = getCurrentImageLayout();\n if (!layout) return;\n\n const { positionX, positionY } = computeHotspotPositionFromClick(\n event.clientX,\n event.clientY,\n event.currentTarget.getBoundingClientRect(),\n layout,\n zoom\n );\n\n onHotspotAdd({\n frameIndex: currentFrameIndex,\n frameId: frame.id,\n positionX,\n positionY,\n });\n }\n\n function handleResetView(): void {\n setZoom(minZoom);\n setPan({ panX: 0, panY: 0 });\n }\n\n function handleZoomIn(): void {\n setZoom(stepZoomIn(zoom, resolvedConfig));\n }\n\n function handleZoomOut(): void {\n const { zoom: nextZoom, pan: nextPan } = stepZoomOut(zoom, pan, resolvedConfig);\n setZoom(nextZoom);\n setPan(nextPan);\n }\n\n return {\n canvasRef,\n containerRef,\n currentFrame,\n currentFrameHotspots,\n imagesLoaded,\n isHotspotMode,\n isResetDisabled: resetDisabled,\n maxZoom,\n minZoom,\n viewerCursorClass,\n zoom,\n getCurrentImageLayout,\n getHotspotScreenPosition,\n handleCanvasClick,\n handlePointerDown,\n handlePointerMove,\n handlePointerUp,\n handleResetView,\n handleWheel,\n handleZoomIn,\n handleZoomOut,\n };\n}\n","import type { Viewer360Config } from '../types';\n\nimport type { PanOffset } from './computeViewerImageLayout';\n\ntype ResolvedViewer360Config = Required<Viewer360Config>;\n\nexport function resolveViewer360Config(config?: Viewer360Config): ResolvedViewer360Config {\n return {\n minZoom: config?.minZoom ?? 1,\n maxZoom: config?.maxZoom ?? 3,\n zoomStep: config?.zoomStep ?? 0.15,\n dragSensitivity: config?.dragSensitivity ?? 8,\n autoRotate: config?.autoRotate ?? false,\n autoRotateIntervalMs: config?.autoRotateIntervalMs ?? 100,\n autoRotateDirection: config?.autoRotateDirection ?? 'forward',\n };\n}\n\nexport function clampZoom(zoom: number, config: ResolvedViewer360Config): number {\n return Math.min(config.maxZoom, Math.max(config.minZoom, zoom));\n}\n\nexport function applyWheelZoom(\n currentZoom: number,\n deltaY: number,\n currentPan: PanOffset,\n config: ResolvedViewer360Config\n): { zoom: number; pan: PanOffset } {\n const delta = deltaY > 0 ? -config.zoomStep : config.zoomStep;\n const zoom = clampZoom(currentZoom + delta, config);\n\n return {\n zoom,\n pan: zoom === config.minZoom ? { panX: 0, panY: 0 } : currentPan,\n };\n}\n\nexport function stepZoomIn(currentZoom: number, config: ResolvedViewer360Config): number {\n return clampZoom(currentZoom + config.zoomStep, config);\n}\n\nexport function stepZoomOut(\n currentZoom: number,\n currentPan: PanOffset,\n config: ResolvedViewer360Config\n): { zoom: number; pan: PanOffset } {\n const zoom = clampZoom(currentZoom - config.zoomStep, config);\n\n return {\n zoom,\n pan: zoom === config.minZoom ? { panX: 0, panY: 0 } : currentPan,\n };\n}\n\nexport function isResetDisabled(zoom: number, pan: PanOffset, config: ResolvedViewer360Config): boolean {\n return zoom === config.minZoom && pan.panX === 0 && pan.panY === 0;\n}\n\nexport function getViewerCursorClass(isHotspotMode: boolean, zoom: number, isDragging: boolean, minZoom: number): string {\n if (isHotspotMode) return 'cursor-crosshair';\n if (isDragging) return 'cursor-grabbing';\n if (zoom > minZoom) return 'cursor-grab';\n return 'cursor-ew-resize';\n}\n","export function clampFrameIndex(index: number, frameCount: number): number {\n if (frameCount === 0) return 0;\n return ((index % frameCount) + frameCount) % frameCount;\n}\n\ntype DragStart = {\n pointerX: number;\n frameIndex: number;\n};\n\nexport function computeDragFrameIndex(\n dragStart: DragStart,\n clientX: number,\n frameCount: number,\n dragSensitivity: number\n): number | null {\n const deltaX = clientX - dragStart.pointerX;\n const frameDelta = Math.round(-deltaX / dragSensitivity);\n\n if (frameDelta === 0) return null;\n\n return clampFrameIndex(dragStart.frameIndex + frameDelta, frameCount);\n}\n","export type ViewerImageLayout = {\n width: number;\n height: number;\n centerX: number;\n centerY: number;\n drawWidth: number;\n drawHeight: number;\n offsetX: number;\n offsetY: number;\n};\n\nexport type PanOffset = {\n panX: number;\n panY: number;\n};\n\ntype ComputeViewerImageLayoutParams = {\n containerWidth: number;\n containerHeight: number;\n imageWidth: number;\n imageHeight: number;\n pan?: PanOffset;\n};\n\nexport function computeViewerImageLayout({\n containerWidth,\n containerHeight,\n imageWidth,\n imageHeight,\n pan = { panX: 0, panY: 0 },\n}: ComputeViewerImageLayoutParams): ViewerImageLayout {\n const imgAspect = imageWidth / imageHeight;\n const containerAspect = containerWidth / containerHeight;\n\n let drawWidth: number;\n let drawHeight: number;\n\n if (imgAspect > containerAspect) {\n drawWidth = containerWidth;\n drawHeight = containerWidth / imgAspect;\n } else {\n drawHeight = containerHeight;\n drawWidth = containerHeight * imgAspect;\n }\n\n const offsetX = (containerWidth - drawWidth) / 2 + pan.panX;\n const offsetY = (containerHeight - drawHeight) / 2 + pan.panY;\n\n return {\n width: containerWidth,\n height: containerHeight,\n centerX: containerWidth / 2,\n centerY: containerHeight / 2,\n drawWidth,\n drawHeight,\n offsetX,\n offsetY,\n };\n}\n\nexport function computeHotspotScreenPosition(\n hotspotX: number,\n hotspotY: number,\n layout: ViewerImageLayout,\n zoom: number\n): { leftPercent: number; topPercent: number } {\n const baseOffsetX = (layout.width - layout.drawWidth) / 2;\n const baseOffsetY = (layout.height - layout.drawHeight) / 2;\n\n const containerX = (hotspotX / 100) * layout.width;\n const containerY = (hotspotY / 100) * layout.height;\n\n const imageLocalX = (containerX - baseOffsetX) / layout.drawWidth;\n const imageLocalY = (containerY - baseOffsetY) / layout.drawHeight;\n\n const imagePointX = layout.offsetX + imageLocalX * layout.drawWidth;\n const imagePointY = layout.offsetY + imageLocalY * layout.drawHeight;\n\n const screenX = layout.centerX + zoom * (imagePointX - layout.centerX);\n const screenY = layout.centerY + zoom * (imagePointY - layout.centerY);\n\n return {\n leftPercent: (screenX / layout.width) * 100,\n topPercent: (screenY / layout.height) * 100,\n };\n}\n\nexport function computeHotspotPositionFromClick(\n clientX: number,\n clientY: number,\n containerRect: DOMRect,\n layout: ViewerImageLayout,\n zoom: number\n): { positionX: number; positionY: number } {\n const clickX = clientX - containerRect.left;\n const clickY = clientY - containerRect.top;\n\n const unzoomedX = layout.centerX + (clickX - layout.centerX) / zoom;\n const unzoomedY = layout.centerY + (clickY - layout.centerY) / zoom;\n\n const baseOffsetX = (layout.width - layout.drawWidth) / 2;\n const baseOffsetY = (layout.height - layout.drawHeight) / 2;\n\n const imageLocalX = Math.min(1, Math.max(0, (unzoomedX - layout.offsetX) / layout.drawWidth));\n const imageLocalY = Math.min(1, Math.max(0, (unzoomedY - layout.offsetY) / layout.drawHeight));\n\n const storedX = baseOffsetX + imageLocalX * layout.drawWidth;\n const storedY = baseOffsetY + imageLocalY * layout.drawHeight;\n\n return {\n positionX: Math.min(100, Math.max(0, (storedX / layout.width) * 100)),\n positionY: Math.min(100, Math.max(0, (storedY / layout.height) * 100)),\n };\n}\n","import type { PanOffset } from './computeViewerImageLayout';\n\ntype PanStart = {\n pointerX: number;\n pointerY: number;\n panX: number;\n panY: number;\n};\n\nexport function computeViewerPanOffset(panStart: PanStart, clientX: number, clientY: number): PanOffset {\n const deltaX = clientX - panStart.pointerX;\n const deltaY = clientY - panStart.pointerY;\n\n return {\n panX: panStart.panX + deltaX,\n panY: panStart.panY + deltaY,\n };\n}\n","import type { Viewer360Frame } from '../types';\n\nimport { computeViewerImageLayout, type PanOffset } from './computeViewerImageLayout';\n\ntype DrawFrameOnCanvasParams = {\n canvas: HTMLCanvasElement;\n container: HTMLDivElement;\n image: HTMLImageElement;\n zoom: number;\n pan: PanOffset;\n};\n\nexport function drawFrameOnCanvas({ canvas, container, image, zoom, pan }: DrawFrameOnCanvasParams): void {\n if (!image.complete || !image.naturalWidth) return;\n\n const rect = container.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n\n canvas.width = rect.width * dpr;\n canvas.height = rect.height * dpr;\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n\n const layout = computeViewerImageLayout({\n containerWidth: rect.width,\n containerHeight: rect.height,\n imageWidth: image.naturalWidth,\n imageHeight: image.naturalHeight,\n pan,\n });\n\n ctx.save();\n ctx.translate(layout.centerX, layout.centerY);\n ctx.scale(zoom, zoom);\n ctx.translate(-layout.centerX, -layout.centerY);\n ctx.drawImage(image, layout.offsetX, layout.offsetY, layout.drawWidth, layout.drawHeight);\n ctx.restore();\n}\n\nexport function getFramesSignature(frames: Viewer360Frame[]): string {\n return frames.map((frame) => frame.id).join('-');\n}\n\nexport function preloadFrameImage(frame: Viewer360Frame): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = (): void => resolve(img);\n img.onerror = (): void => reject(new Error(`Failed to load frame: ${frame.src}`));\n img.src = frame.src;\n });\n}\n\nexport async function preloadViewerFrames(frames: Viewer360Frame[]): Promise<HTMLImageElement[]> {\n const results = await Promise.allSettled(frames.map(preloadFrameImage));\n\n return results.map((result) => (result.status === 'fulfilled' ? result.value : new Image()));\n}\n\nexport function hasLoadedViewerFrame(images: HTMLImageElement[]): boolean {\n return images.some((image) => image.complete && image.naturalWidth > 0);\n}\n\nexport function filterHotspotsByFrame<TData>(\n hotspots: Array<{ frameIndex: number; data?: TData }>,\n frameIndex: number\n): Array<{ frameIndex: number; data?: TData }> {\n return hotspots.filter((hotspot) => hotspot.frameIndex === frameIndex);\n}\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cn } from '@/components/utils';\n\nfunction Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }): JSX.Element {\n return (\n <div\n data-slot=\"card\"\n data-size={size}\n className={cn(\n 'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n 'gap-0.5 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',\n className\n )}\n {...props}\n />\n );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-title\"\n className={cn('text-lg leading-normal font-semibold group-data-[size=sm]/card:text-sm', className)}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"card-description\" className={cn('text-muted-foreground text-xs font-medium', className)} {...props} />;\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-action\"\n className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}\n {...props}\n />\n );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"card-content\" className={cn('px-6 group-data-[size=sm]/card:px-4', className)} {...props} />;\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\n 'rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center',\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nconst badgeVariants = cva(\n 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',\n secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',\n destructive:\n 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',\n outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',\n ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',\n link: 'text-primary underline-offset-4 hover:underline',\n info: 'bg-blue-500 text-white hover:bg-blue-600',\n warning: 'bg-amber-500 text-white hover:bg-amber-600',\n success: 'bg-green-600 text-white hover:bg-green-700',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n }\n);\n\nfunction Badge({\n className,\n variant = 'default',\n asChild = false,\n ...props\n}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }): JSX.Element {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n const Comp = asChild ? Slot.Root : 'span';\n\n return <Comp data-slot=\"badge\" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n","import type { JSX } from 'react';\n\nimport { Badge } from '@/components/ui/Badge';\nimport { cn } from '@/components/utils';\n\ntype Viewer360AddModeBannerProps = {\n className?: string;\n label: string;\n};\n\nexport function Viewer360AddModeBanner({ className, label }: Viewer360AddModeBannerProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Badge variant=\"outline\" className={cn('pointer-events-none', className)}>\n {label}\n </Badge>\n );\n}\n","import type { JSX } from 'react';\n\nimport { Badge } from '@/components/ui/Badge';\nimport { cn } from '@/components/utils';\n\ntype Viewer360FrameIndicatorProps = {\n className?: string;\n label: string;\n};\n\nexport function Viewer360FrameIndicator({ className, label }: Viewer360FrameIndicatorProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Badge variant=\"outline\" className={cn('pointer-events-none shadow-sm', className)}>\n {label}\n </Badge>\n );\n}\n","import type { Viewer360Hotspot, Viewer360Marker } from '../types';\n\nexport function hotspotToViewer360Marker<TData>(hotspot: Viewer360Hotspot<TData>): Viewer360Marker {\n const data = hotspot.data;\n\n if (data && typeof data === 'object' && 'title' in data && typeof (data as { title?: unknown }).title === 'string') {\n const marker = data as unknown as Viewer360Marker;\n\n return {\n id: marker.id ?? hotspot.id,\n title: marker.title,\n description: marker.description,\n };\n }\n\n return {\n id: hotspot.id,\n title: hotspot.id,\n };\n}\n\nexport function toViewer360Hotspots<TData extends Viewer360Marker>(\n markers: Array<Viewer360Marker & { frameIndex: number; positionX: number; positionY: number }>,\n mapData?: (marker: Viewer360Marker & { frameIndex: number; positionX: number; positionY: number }) => TData\n): Viewer360Hotspot<TData>[] {\n return markers.map((marker) => ({\n id: marker.id,\n frameIndex: marker.frameIndex,\n positionX: marker.positionX,\n positionY: marker.positionY,\n data: mapData ? mapData(marker) : (marker as unknown as TData),\n }));\n}\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { Separator } from '@/components/ui/Separator';\nimport { cn } from '@/components/utils';\n\nfunction ItemGroup({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n role=\"list\"\n data-slot=\"item-group\"\n className={cn('gap-4 has-[[data-size=sm]]:gap-2.5 has-[[data-size=xs]]:gap-2 group/item-group flex w-full flex-col', className)}\n {...props}\n />\n );\n}\n\nfunction ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>): JSX.Element {\n return <Separator data-slot=\"item-separator\" orientation=\"horizontal\" className={cn('my-2', className)} {...props} />;\n}\n\nconst itemVariants = cva(\n '[a]:hover:bg-muted rounded-md border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors',\n {\n variants: {\n variant: {\n default: 'border-transparent',\n outline: 'border-border',\n muted: 'bg-muted/50 border-transparent',\n },\n size: {\n default: 'gap-3.5 px-4 py-3.5',\n sm: 'gap-2.5 px-3 py-2.5',\n xs: 'gap-2 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nfunction Item({\n className,\n variant = 'default',\n size = 'default',\n asChild = false,\n ...props\n}: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & { asChild?: boolean }): JSX.Element {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n const Comp = asChild ? Slot.Root : 'div';\n return (\n <Comp\n data-slot=\"item\"\n data-variant={variant}\n data-size={size}\n className={cn(itemVariants({ variant, size, className }))}\n {...props}\n />\n );\n}\n\nconst itemMediaVariants = cva(\n 'gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',\n {\n variants: {\n variant: {\n default: 'bg-transparent',\n icon: '[&_svg]:size-4',\n image: 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n }\n);\n\nfunction ItemMedia({\n className,\n variant = 'default',\n ...props\n}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>): JSX.Element {\n return <div data-slot=\"item-media\" data-variant={variant} className={cn(itemMediaVariants({ variant, className }))} {...props} />;\n}\n\nfunction ItemContent({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"item-content\"\n className={cn('gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none', className)}\n {...props}\n />\n );\n}\n\nfunction ItemTitle({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return (\n <div\n data-slot=\"item-title\"\n className={cn('gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center', className)}\n {...props}\n />\n );\n}\n\nfunction ItemDescription({ className, ...props }: React.ComponentProps<'p'>): JSX.Element {\n return (\n <p\n data-slot=\"item-description\"\n className={cn(\n 'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4',\n className\n )}\n {...props}\n />\n );\n}\n\nfunction ItemActions({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"item-actions\" className={cn('gap-2 flex items-center', className)} {...props} />;\n}\n\nfunction ItemHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"item-header\" className={cn('gap-2 flex basis-full items-center justify-between', className)} {...props} />;\n}\n\nfunction ItemFooter({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {\n return <div data-slot=\"item-footer\" className={cn('gap-2 flex basis-full items-center justify-between', className)} {...props} />;\n}\n\nexport { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter };\n","'use client';\n\nimport * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { Separator as SeparatorPrimitive } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nfunction Separator({\n className,\n orientation = 'horizontal',\n decorative = true,\n ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>): JSX.Element {\n return (\n <SeparatorPrimitive.Root\n data-slot=\"separator\"\n decorative={decorative}\n orientation={orientation}\n className={cn(\n 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch',\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Separator };\n","import type { JSX, MouseEvent } from 'react';\n\nimport { Trash2 } from 'lucide-react';\n\nimport { defaultViewer360MarkerPinLabels } from '../constants/viewer360MarkerLabels';\nimport { viewer360MarkerPinClassNames } from '../constants/viewer360ClassNames';\nimport type { Viewer360MarkerPinProps } from '../types';\nimport { Badge } from '@/components/ui/Badge';\nimport { Button } from '@/components/ui/Button';\nimport {\n Item,\n ItemActions,\n ItemContent,\n ItemDescription,\n ItemTitle,\n} from '@/components/ui/Item';\nimport { cn } from '@/components/utils';\n\nexport function Viewer360MarkerPin<TData = unknown>({\n marker,\n hotspot,\n leftPercent,\n topPercent,\n onDelete,\n isDeletePending = false,\n onClick,\n renderTag,\n classNames,\n labels,\n}: Viewer360MarkerPinProps<TData>): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: States & Constants\n // ----------------------------------------------------------------------------------------------------\n const deleteLabel = labels?.delete ?? defaultViewer360MarkerPinLabels.delete;\n const showTooltip = Boolean(marker.title || marker.description || onDelete || renderTag);\n\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Item\n size=\"xs\"\n variant=\"default\"\n className={cn(\n viewer360MarkerPinClassNames.root,\n classNames?.root,\n 'group/marker w-auto border-transparent p-0'\n )}\n style={{ left: `${leftPercent}%`, top: `${topPercent}%` }}\n >\n <Badge\n variant=\"destructive\"\n className={cn(viewer360MarkerPinClassNames.ping, classNames?.ping, 'absolute size-6 border-0 bg-destructive opacity-60')}\n aria-hidden=\"true\"\n />\n\n <Button\n type=\"button\"\n variant=\"destructive\"\n size=\"icon-xs\"\n className={cn(\n viewer360MarkerPinClassNames.dot,\n classNames?.dot,\n 'size-4 min-h-4 min-w-4 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:scale-125 hover:bg-destructive'\n )}\n aria-label={marker.title}\n onClick={onClick}\n />\n\n {showTooltip && (\n <div\n className={cn(\n viewer360MarkerPinClassNames.tooltip,\n classNames?.tooltip,\n 'pointer-events-none opacity-0 transition-opacity duration-150 group-hover/marker:pointer-events-auto group-hover/marker:opacity-100 group-focus-within/marker:pointer-events-auto group-focus-within/marker:opacity-100'\n )}\n >\n <Item\n size=\"sm\"\n variant=\"default\"\n className={cn(viewer360MarkerPinClassNames.tooltipHeader, classNames?.tooltipHeader, 'w-full border-transparent')}\n >\n <ItemContent className={cn(viewer360MarkerPinClassNames.tooltipBody, classNames?.tooltipBody)}>\n <ItemTitle className={cn(viewer360MarkerPinClassNames.tooltipTitle, classNames?.tooltipTitle)}>\n {marker.title}\n </ItemTitle>\n {renderTag?.({ marker, hotspot })}\n {marker.description && (\n <ItemDescription className={cn(viewer360MarkerPinClassNames.tooltipDescription, classNames?.tooltipDescription)}>\n {marker.description}\n </ItemDescription>\n )}\n </ItemContent>\n {onDelete && (\n <ItemActions>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n className={classNames?.deleteButton}\n disabled={isDeletePending}\n aria-label={deleteLabel}\n onClick={(event: MouseEvent<HTMLButtonElement>) => {\n event.stopPropagation();\n onDelete(marker.id);\n }}\n >\n <Trash2 className=\"size-4\" />\n </Button>\n </ItemActions>\n )}\n </Item>\n </div>\n )}\n </Item>\n );\n}\n","export const defaultViewer360MarkerPinLabels = {\n delete: 'Remove marker',\n} as const;\n","import * as React from 'react';\nimport type { JSX } from 'react';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '@/components/utils';\n\nconst buttonVariants = cva(\n 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground hover:bg-primary/80',\n outline:\n 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',\n secondary:\n 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',\n ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',\n destructive:\n 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n default:\n 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',\n xs: 'h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5',\n sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5',\n lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3',\n icon: 'size-9',\n 'icon-xs':\n 'size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md',\n 'icon-sm': 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',\n 'icon-lg': 'size-10',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n }\n);\n\nfunction Button({\n className,\n variant = 'default',\n size = 'default',\n asChild = false,\n ...props\n}: React.ComponentProps<'button'> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }): JSX.Element {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n const Comp = asChild ? Slot.Root : 'button';\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n );\n}\n\nexport { Button, buttonVariants };\n","import type { JSX, MouseEvent, ReactNode } from 'react';\n\nimport { hotspotToViewer360Marker } from '../helpers/markerHelpers';\nimport type { Viewer360Hotspot, Viewer360HotspotPinOptions, Viewer360HotspotRenderProps } from '../types';\nimport { Item } from '@/components/ui/Item';\n\nimport { Viewer360MarkerPin } from './Viewer360MarkerPin';\n\ntype Viewer360HotspotOverlayProps<TData = unknown> = {\n hotspot: Viewer360Hotspot<TData>;\n leftPercent: number;\n topPercent: number;\n hotspotPin?: Viewer360HotspotPinOptions<TData>;\n renderHotspot?: (props: Viewer360HotspotRenderProps<TData>) => ReactNode;\n onHotspotClick?: (hotspot: Viewer360Hotspot<TData>, event: MouseEvent<HTMLDivElement>) => void;\n};\n\nexport function Viewer360HotspotOverlay<TData = unknown>({\n hotspot,\n leftPercent,\n topPercent,\n hotspotPin,\n renderHotspot,\n onHotspotClick,\n}: Viewer360HotspotOverlayProps<TData>): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n if (renderHotspot) {\n return (\n <Item size=\"xs\" variant=\"default\" className=\"pointer-events-auto w-auto border-transparent p-0\">\n {renderHotspot({ hotspot, leftPercent, topPercent })}\n </Item>\n );\n }\n\n const marker = hotspotPin?.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);\n\n return (\n <Viewer360MarkerPin\n marker={marker}\n hotspot={hotspot}\n leftPercent={leftPercent}\n topPercent={topPercent}\n onDelete={hotspotPin?.onDelete}\n isDeletePending={hotspotPin?.deletingMarkerId === hotspot.id}\n renderTag={hotspotPin?.renderTag}\n classNames={hotspotPin?.classNames}\n labels={hotspotPin?.labels}\n onClick={\n onHotspotClick\n ? (event) => onHotspotClick(hotspot, event as unknown as MouseEvent<HTMLDivElement>)\n : undefined\n }\n />\n );\n}\n","'use client';\n\nimport * as React from 'react';\n\nimport { cn } from '@/components/utils';\n\nfunction Label({ className, ...props }: React.ComponentProps<'label'>): React.ReactNode {\n return (\n <label\n data-slot=\"label\"\n className={cn(\n 'gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed',\n className\n )}\n {...props}\n />\n );\n}\n\nexport { Label };\n","import type { JSX } from 'react';\n\nimport { Loader2Icon } from 'lucide-react';\n\nimport { cn } from '@/components/utils';\n\nfunction Spinner({ className, ...props }: React.ComponentProps<'svg'>): JSX.Element {\n return <Loader2Icon role=\"status\" aria-label=\"Loading\" className={cn('size-4 animate-spin', className)} {...props} />;\n}\n\nexport { Spinner };\n","import type { JSX } from 'react';\n\nimport { Item } from '@/components/ui/Item';\nimport { Label } from '@/components/ui/Label';\nimport { Spinner } from '@/components/ui/Spinner';\nimport { cn } from '@/components/utils';\n\ntype Viewer360LoadingOverlayProps = {\n className?: string;\n textClassName?: string;\n label: string;\n};\n\nexport function Viewer360LoadingOverlay({ className, textClassName, label }: Viewer360LoadingOverlayProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <Item\n size=\"sm\"\n variant=\"muted\"\n className={cn('pointer-events-none w-auto justify-center border-transparent bg-muted/80', className)}\n >\n <Spinner className=\"size-5 text-muted-foreground\" />\n <Label className={cn('font-normal text-muted-foreground', textClassName)}>{label}</Label>\n </Item>\n );\n}\n","import type { JSX } from 'react';\n\nimport { Crosshair, Minus, Plus, RotateCcw, ZoomIn } from 'lucide-react';\n\nimport { viewer360ClassNames } from '../constants/viewer360ClassNames';\nimport type { Viewer360ToolbarRenderProps } from '../types';\nimport { Badge } from '@/components/ui/Badge';\nimport { Button } from '@/components/ui/Button';\nimport { CardFooter } from '@/components/ui/Card';\nimport { Label } from '@/components/ui/Label';\nimport { Separator } from '@/components/ui/Separator';\nimport { cn } from '@/components/utils';\n\ntype Viewer360ToolbarProps = Viewer360ToolbarRenderProps;\n\nexport function Viewer360Toolbar({\n showDragHint,\n showHotspotModeControl,\n showZoomControls,\n showResetControl,\n labels,\n isHotspotMode,\n zoom,\n minZoom,\n maxZoom,\n isResetDisabled,\n onHotspotModeChange,\n onZoomIn,\n onZoomOut,\n onResetView,\n}: Viewer360ToolbarProps): JSX.Element {\n // ----------------------------------------------------------------------------------------------------\n // MARK: Main Component UI\n // ----------------------------------------------------------------------------------------------------\n return (\n <CardFooter className={cn(viewer360ClassNames.toolbar, 'gap-2 border-t px-4 py-3 pt-3')}>\n {showDragHint && (\n <Label className={cn(viewer360ClassNames.dragHint, 'font-normal text-muted-foreground')}>{labels.dragHint}</Label>\n )}\n\n <div className={viewer360ClassNames.controls}>\n {showHotspotModeControl && (\n <>\n <Button variant={isHotspotMode ? 'default' : 'outline'} size=\"sm\" onClick={() => onHotspotModeChange(!isHotspotMode)}>\n <Crosshair className=\"me-1.5 size-4\" />\n {labels.addHotspot}\n </Button>\n <Separator orientation=\"vertical\" className={cn(viewer360ClassNames.divider, 'h-6')} />\n </>\n )}\n\n {showZoomControls && (\n <>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={zoom <= minZoom} aria-label={labels.zoomOut} onClick={onZoomOut}>\n <Minus className=\"size-4\" />\n </Button>\n <Badge variant=\"outline\" className={cn(viewer360ClassNames.zoomDisplay, 'h-8 gap-1 px-2 py-1')}>\n <ZoomIn className=\"size-3 text-muted-foreground\" />\n {labels.zoom(Math.round(zoom * 100))}\n </Badge>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={zoom >= maxZoom} aria-label={labels.zoomIn} onClick={onZoomIn}>\n <Plus className=\"size-4\" />\n </Button>\n </>\n )}\n\n {showResetControl && (\n <Button variant=\"outline\" size=\"icon-sm\" disabled={isResetDisabled} aria-label={labels.resetView} onClick={onResetView}>\n <RotateCcw className=\"size-4\" />\n </Button>\n )}\n </div>\n </CardFooter>\n );\n}\n","export const viewer360Config = {\n minZoom: 1,\n maxZoom: 3,\n zoomStep: 0.15,\n dragSensitivity: 8,\n autoRotate: false,\n autoRotateIntervalMs: 100,\n autoRotateDirection: 'forward' as const,\n};\n\nexport const defaultViewer360Config = viewer360Config;\n"],"mappings":"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","names":["useEffect","useMemo","useState","jsx","jsx","jsx","cva","Slot","jsx","jsx","cva","Slot","jsx","jsx","cva","Slot","jsx","cva","Slot","jsx","jsx","jsx","jsx","jsx","jsxs","jsx","jsxs","isResetDisabled","jsx","jsxs","useMemo","useState","isResetDisabled","useEffect"]}
|
package/package.json
CHANGED
|
@@ -26,7 +26,8 @@ export const viewer360MarkerPinClassNames: Required<Viewer360MarkerPinClassNames
|
|
|
26
26
|
root: 'pointer-events-auto absolute z-30 -translate-x-1/2 -translate-y-1/2',
|
|
27
27
|
ping: 'absolute inline-flex size-6 -translate-x-1/4 -translate-y-1/4 animate-ping rounded-full bg-destructive opacity-60',
|
|
28
28
|
dot: 'relative flex size-4 items-center justify-center rounded-full border-2 border-background bg-destructive shadow-md transition-transform duration-200 hover:scale-125 focus:outline-none',
|
|
29
|
-
tooltip:
|
|
29
|
+
tooltip:
|
|
30
|
+
'absolute bottom-6 left-1/2 z-40 w-64 -translate-x-1/2 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md',
|
|
30
31
|
tooltipHeader: 'flex items-start justify-between gap-2',
|
|
31
32
|
tooltipBody: 'flex min-w-0 flex-col gap-1',
|
|
32
33
|
tooltipTitle: 'text-sm font-medium',
|
|
@@ -2,9 +2,7 @@ import type { JSX, MouseEvent, ReactNode } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { hotspotToViewer360Marker } from '../helpers/markerHelpers';
|
|
4
4
|
import type { Viewer360Hotspot, Viewer360HotspotPinOptions, Viewer360HotspotRenderProps } from '../types';
|
|
5
|
-
import { Button } from '@/components/ui/Button';
|
|
6
5
|
import { Item } from '@/components/ui/Item';
|
|
7
|
-
import { cn } from '@/components/utils';
|
|
8
6
|
|
|
9
7
|
import { Viewer360MarkerPin } from './Viewer360MarkerPin';
|
|
10
8
|
|
|
@@ -36,35 +34,24 @@ export function Viewer360HotspotOverlay<TData = unknown>({
|
|
|
36
34
|
);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
const marker = hotspotPin.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<Viewer360MarkerPin
|
|
44
|
-
marker={marker}
|
|
45
|
-
hotspot={hotspot}
|
|
46
|
-
leftPercent={leftPercent}
|
|
47
|
-
topPercent={topPercent}
|
|
48
|
-
onDelete={hotspotPin.onDelete}
|
|
49
|
-
isDeletePending={hotspotPin.deletingMarkerId === hotspot.id}
|
|
50
|
-
renderTag={hotspotPin.renderTag}
|
|
51
|
-
classNames={hotspotPin.classNames}
|
|
52
|
-
labels={hotspotPin.labels}
|
|
53
|
-
/>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
37
|
+
const marker = hotspotPin?.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);
|
|
56
38
|
|
|
57
39
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
40
|
+
<Viewer360MarkerPin
|
|
41
|
+
marker={marker}
|
|
42
|
+
hotspot={hotspot}
|
|
43
|
+
leftPercent={leftPercent}
|
|
44
|
+
topPercent={topPercent}
|
|
45
|
+
onDelete={hotspotPin?.onDelete}
|
|
46
|
+
isDeletePending={hotspotPin?.deletingMarkerId === hotspot.id}
|
|
47
|
+
renderTag={hotspotPin?.renderTag}
|
|
48
|
+
classNames={hotspotPin?.classNames}
|
|
49
|
+
labels={hotspotPin?.labels}
|
|
50
|
+
onClick={
|
|
51
|
+
onHotspotClick
|
|
52
|
+
? (event) => onHotspotClick(hotspot, event as unknown as MouseEvent<HTMLDivElement>)
|
|
53
|
+
: undefined
|
|
54
|
+
}
|
|
68
55
|
/>
|
|
69
56
|
);
|
|
70
57
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
1
|
+
import type { JSX, MouseEvent } from 'react';
|
|
3
2
|
|
|
4
3
|
import { Trash2 } from 'lucide-react';
|
|
5
4
|
|
|
@@ -15,7 +14,6 @@ import {
|
|
|
15
14
|
ItemDescription,
|
|
16
15
|
ItemTitle,
|
|
17
16
|
} from '@/components/ui/Item';
|
|
18
|
-
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover';
|
|
19
17
|
import { cn } from '@/components/utils';
|
|
20
18
|
|
|
21
19
|
export function Viewer360MarkerPin<TData = unknown>({
|
|
@@ -25,6 +23,7 @@ export function Viewer360MarkerPin<TData = unknown>({
|
|
|
25
23
|
topPercent,
|
|
26
24
|
onDelete,
|
|
27
25
|
isDeletePending = false,
|
|
26
|
+
onClick,
|
|
28
27
|
renderTag,
|
|
29
28
|
classNames,
|
|
30
29
|
labels,
|
|
@@ -32,47 +31,49 @@ export function Viewer360MarkerPin<TData = unknown>({
|
|
|
32
31
|
// ----------------------------------------------------------------------------------------------------
|
|
33
32
|
// MARK: States & Constants
|
|
34
33
|
// ----------------------------------------------------------------------------------------------------
|
|
35
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
36
34
|
const deleteLabel = labels?.delete ?? defaultViewer360MarkerPinLabels.delete;
|
|
35
|
+
const showTooltip = Boolean(marker.title || marker.description || onDelete || renderTag);
|
|
37
36
|
|
|
38
37
|
// ----------------------------------------------------------------------------------------------------
|
|
39
38
|
// MARK: Main Component UI
|
|
40
39
|
// ----------------------------------------------------------------------------------------------------
|
|
41
40
|
return (
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
<Item
|
|
42
|
+
size="xs"
|
|
43
|
+
variant="default"
|
|
44
|
+
className={cn(
|
|
45
|
+
viewer360MarkerPinClassNames.root,
|
|
46
|
+
classNames?.root,
|
|
47
|
+
'group/marker w-auto border-transparent p-0'
|
|
48
|
+
)}
|
|
49
|
+
style={{ left: `${leftPercent}%`, top: `${topPercent}%` }}
|
|
50
|
+
>
|
|
51
|
+
<Badge
|
|
52
|
+
variant="destructive"
|
|
53
|
+
className={cn(viewer360MarkerPinClassNames.ping, classNames?.ping, 'absolute size-6 border-0 bg-destructive opacity-60')}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</PopoverTrigger>
|
|
57
|
+
<Button
|
|
58
|
+
type="button"
|
|
59
|
+
variant="destructive"
|
|
60
|
+
size="icon-xs"
|
|
61
|
+
className={cn(
|
|
62
|
+
viewer360MarkerPinClassNames.dot,
|
|
63
|
+
classNames?.dot,
|
|
64
|
+
'size-4 min-h-4 min-w-4 rounded-full border-2 border-background bg-destructive p-0 shadow-md hover:scale-125 hover:bg-destructive'
|
|
65
|
+
)}
|
|
66
|
+
aria-label={marker.title}
|
|
67
|
+
onClick={onClick}
|
|
68
|
+
/>
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
{showTooltip && (
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
viewer360MarkerPinClassNames.tooltip,
|
|
74
|
+
classNames?.tooltip,
|
|
75
|
+
'pointer-events-none opacity-0 transition-opacity duration-150 group-hover/marker:pointer-events-auto group-hover/marker:opacity-100 group-focus-within/marker:pointer-events-auto group-focus-within/marker:opacity-100'
|
|
76
|
+
)}
|
|
76
77
|
>
|
|
77
78
|
<Item
|
|
78
79
|
size="sm"
|
|
@@ -98,7 +99,7 @@ export function Viewer360MarkerPin<TData = unknown>({
|
|
|
98
99
|
className={classNames?.deleteButton}
|
|
99
100
|
disabled={isDeletePending}
|
|
100
101
|
aria-label={deleteLabel}
|
|
101
|
-
onClick={(event) => {
|
|
102
|
+
onClick={(event: MouseEvent<HTMLButtonElement>) => {
|
|
102
103
|
event.stopPropagation();
|
|
103
104
|
onDelete(marker.id);
|
|
104
105
|
}}
|
|
@@ -108,8 +109,8 @@ export function Viewer360MarkerPin<TData = unknown>({
|
|
|
108
109
|
</ItemActions>
|
|
109
110
|
)}
|
|
110
111
|
</Item>
|
|
111
|
-
</
|
|
112
|
-
|
|
113
|
-
</
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</Item>
|
|
114
115
|
);
|
|
115
116
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
1
|
+
import type { MouseEvent, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
import type { Viewer360Hotspot } from './Viewer360Hotspot';
|
|
4
4
|
|
|
@@ -36,6 +36,7 @@ export type Viewer360MarkerPinProps<TData = unknown> = {
|
|
|
36
36
|
topPercent: number;
|
|
37
37
|
onDelete?: (id: string) => void;
|
|
38
38
|
isDeletePending?: boolean;
|
|
39
|
+
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
39
40
|
renderTag?: (props: Viewer360MarkerPinRenderProps<TData>) => ReactNode;
|
|
40
41
|
classNames?: Viewer360MarkerPinClassNames;
|
|
41
42
|
labels?: Viewer360MarkerPinLabels;
|