@mmmmzxe/react-360-viewer 0.1.13 → 0.1.15
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/index.js +3590 -65
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +11 -18
- package/dist/inject-styles.js +0 -21
- package/src/components/ui/Badge/index.tsx +0 -45
- package/src/components/ui/Button/index.tsx +0 -67
- package/src/components/ui/Card/index.tsx +0 -74
- package/src/components/ui/Item/index.tsx +0 -136
- package/src/components/ui/Label/index.tsx +0 -20
- package/src/components/ui/Popover/index.tsx +0 -56
- package/src/components/ui/Separator/index.tsx +0 -30
- package/src/components/ui/Spinner/index.tsx +0 -11
- package/src/components/utils/index.ts +0 -6
- package/src/constants/viewer360ClassNames.ts +0 -42
- package/src/constants/viewer360Config.ts +0 -11
- package/src/constants/viewer360Labels.ts +0 -14
- package/src/constants/viewer360MarkerLabels.ts +0 -3
- package/src/feature/Viewer360.test.tsx +0 -47
- package/src/feature/Viewer360.tsx +0 -223
- package/src/feature/Viewer360AddModeBanner.tsx +0 -20
- package/src/feature/Viewer360FrameIndicator.tsx +0 -20
- package/src/feature/Viewer360HotspotOverlay.tsx +0 -57
- package/src/feature/Viewer360LoadingOverlay.tsx +0 -28
- package/src/feature/Viewer360MarkerPin.tsx +0 -105
- package/src/feature/Viewer360Toolbar.tsx +0 -75
- package/src/helpers/adjustViewerZoom.test.ts +0 -29
- package/src/helpers/adjustViewerZoom.ts +0 -64
- package/src/helpers/computeDragFrameIndex.test.ts +0 -20
- package/src/helpers/computeDragFrameIndex.ts +0 -23
- package/src/helpers/computeViewerImageLayout.test.ts +0 -48
- package/src/helpers/computeViewerImageLayout.ts +0 -114
- package/src/helpers/computeViewerPanOffset.ts +0 -18
- package/src/helpers/markerHelpers.test.ts +0 -38
- package/src/helpers/markerHelpers.ts +0 -33
- package/src/helpers/viewer360PropsHelpers.ts +0 -46
- package/src/helpers/viewerHelpers.ts +0 -74
- package/src/hooks/useViewer360.ts +0 -306
- package/src/index.ts +0 -68
- package/src/styles.css +0 -80
- package/src/types/Viewer360Hotspot.ts +0 -67
- package/src/types/Viewer360Marker.ts +0 -52
- package/src/types/Viewer360Props.ts +0 -108
- package/src/types/index.ts +0 -30
- package/src/utils/index.ts +0 -6
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import type { JSX } from 'react';
|
|
5
|
-
|
|
6
|
-
import { Popover as PopoverPrimitive } from 'radix-ui';
|
|
7
|
-
|
|
8
|
-
import { cn } from '@/components/utils';
|
|
9
|
-
|
|
10
|
-
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>): JSX.Element {
|
|
11
|
-
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>): JSX.Element {
|
|
15
|
-
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function PopoverContent({
|
|
19
|
-
className,
|
|
20
|
-
align = 'center',
|
|
21
|
-
sideOffset = 4,
|
|
22
|
-
...props
|
|
23
|
-
}: React.ComponentProps<typeof PopoverPrimitive.Content>): JSX.Element {
|
|
24
|
-
return (
|
|
25
|
-
<PopoverPrimitive.Portal>
|
|
26
|
-
<PopoverPrimitive.Content
|
|
27
|
-
data-slot="popover-content"
|
|
28
|
-
align={align}
|
|
29
|
-
sideOffset={sideOffset}
|
|
30
|
-
className={cn(
|
|
31
|
-
'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',
|
|
32
|
-
className
|
|
33
|
-
)}
|
|
34
|
-
{...props}
|
|
35
|
-
/>
|
|
36
|
-
</PopoverPrimitive.Portal>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>): JSX.Element {
|
|
41
|
-
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
45
|
-
return <div data-slot="popover-header" className={cn('flex flex-col gap-1 text-sm', className)} {...props} />;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>): JSX.Element {
|
|
49
|
-
return <div data-slot="popover-title" className={cn('font-medium', className)} {...props} />;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function PopoverDescription({ className, ...props }: React.ComponentProps<'p'>): JSX.Element {
|
|
53
|
-
return <p data-slot="popover-description" className={cn('text-muted-foreground', className)} {...props} />;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import type { JSX } from 'react';
|
|
5
|
-
|
|
6
|
-
import { Separator as SeparatorPrimitive } from 'radix-ui';
|
|
7
|
-
|
|
8
|
-
import { cn } from '@/components/utils';
|
|
9
|
-
|
|
10
|
-
function Separator({
|
|
11
|
-
className,
|
|
12
|
-
orientation = 'horizontal',
|
|
13
|
-
decorative = true,
|
|
14
|
-
...props
|
|
15
|
-
}: React.ComponentProps<typeof SeparatorPrimitive.Root>): JSX.Element {
|
|
16
|
-
return (
|
|
17
|
-
<SeparatorPrimitive.Root
|
|
18
|
-
data-slot="separator"
|
|
19
|
-
decorative={decorative}
|
|
20
|
-
orientation={orientation}
|
|
21
|
-
className={cn(
|
|
22
|
-
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch',
|
|
23
|
-
className
|
|
24
|
-
)}
|
|
25
|
-
{...props}
|
|
26
|
-
/>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export { Separator };
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
|
|
3
|
-
import { Loader2Icon } from 'lucide-react';
|
|
4
|
-
|
|
5
|
-
import { cn } from '@/components/utils';
|
|
6
|
-
|
|
7
|
-
function Spinner({ className, ...props }: React.ComponentProps<'svg'>): JSX.Element {
|
|
8
|
-
return <Loader2Icon role="status" aria-label="Loading" className={cn('size-4 animate-spin', className)} {...props} />;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export { Spinner };
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Viewer360ClassNames } from '../types/Viewer360Props';
|
|
2
|
-
import type { Viewer360MarkerPinClassNames } from '../types/Viewer360Marker';
|
|
3
|
-
|
|
4
|
-
export const viewer360ClassNames: Required<Viewer360ClassNames> = {
|
|
5
|
-
root: 'overflow-hidden rounded-lg border bg-card text-card-foreground',
|
|
6
|
-
viewport: 'relative aspect-[16/10] w-full touch-none select-none bg-muted',
|
|
7
|
-
canvas: 'absolute inset-0 size-full',
|
|
8
|
-
overlay: 'pointer-events-none absolute inset-0 overflow-hidden',
|
|
9
|
-
loading: 'absolute inset-0 flex items-center justify-center bg-muted/80',
|
|
10
|
-
loadingText: 'text-sm text-muted-foreground',
|
|
11
|
-
frameIndicator:
|
|
12
|
-
'pointer-events-none absolute bottom-4 start-1/2 z-20 -translate-x-1/2 rounded-full border bg-background px-4 py-1.5 text-xs font-medium shadow-sm whitespace-nowrap',
|
|
13
|
-
hotspotModeBanner:
|
|
14
|
-
'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',
|
|
15
|
-
toolbar: 'flex flex-wrap items-center justify-between gap-2 border-t px-4 py-3',
|
|
16
|
-
dragHint: 'hidden text-xs text-muted-foreground sm:block',
|
|
17
|
-
controls: 'ms-auto flex items-center gap-1.5',
|
|
18
|
-
controlButton: '',
|
|
19
|
-
controlButtonActive: '',
|
|
20
|
-
controlButtonDisabled: '',
|
|
21
|
-
zoomDisplay: 'flex min-w-[3rem] items-center justify-center gap-1 rounded-md border bg-background px-2 py-1 text-xs font-medium',
|
|
22
|
-
divider: 'mx-1 h-6 w-px bg-border',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const viewer360MarkerPinClassNames: Required<Viewer360MarkerPinClassNames> = {
|
|
26
|
-
root: 'pointer-events-auto absolute z-30 -translate-x-1/2 -translate-y-1/2',
|
|
27
|
-
ping: 'pointer-events-none absolute inset-0 inline-flex animate-ping rounded-full bg-destructive opacity-75',
|
|
28
|
-
dot: 'relative z-10 inline-flex size-4 min-h-4 min-w-4 shrink-0 rounded-full border-2 border-background bg-destructive p-0 shadow-md transition-transform duration-200 hover:scale-125 focus:outline-none',
|
|
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',
|
|
31
|
-
tooltipHeader: 'flex items-start justify-between gap-2',
|
|
32
|
-
tooltipBody: 'flex min-w-0 flex-col gap-1',
|
|
33
|
-
tooltipTitle: 'text-sm font-medium',
|
|
34
|
-
tooltipDescription: 'mt-2 line-clamp-3 text-xs text-muted-foreground',
|
|
35
|
-
deleteButton: '',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** @deprecated Use `viewer360ClassNames` */
|
|
39
|
-
export const defaultViewer360ClassNames = viewer360ClassNames;
|
|
40
|
-
|
|
41
|
-
/** @deprecated Use `viewer360MarkerPinClassNames` */
|
|
42
|
-
export const defaultViewer360MarkerPinClassNames = viewer360MarkerPinClassNames;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const viewer360Config = {
|
|
2
|
-
minZoom: 1,
|
|
3
|
-
maxZoom: 3,
|
|
4
|
-
zoomStep: 0.15,
|
|
5
|
-
dragSensitivity: 8,
|
|
6
|
-
autoRotate: false,
|
|
7
|
-
autoRotateIntervalMs: 100,
|
|
8
|
-
autoRotateDirection: 'forward' as const,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const defaultViewer360Config = viewer360Config;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Viewer360Labels } from '../types/Viewer360Props';
|
|
2
|
-
|
|
3
|
-
export const defaultViewer360Labels: Required<Viewer360Labels> = {
|
|
4
|
-
loading: 'Loading images…',
|
|
5
|
-
dragHint: 'Drag to rotate • Scroll to zoom',
|
|
6
|
-
frameIndicator: ({ current, total, label }) => (label ? `${label} · ${current} / ${total}` : `${current} / ${total}`),
|
|
7
|
-
zoom: (percent) => `${percent}%`,
|
|
8
|
-
hotspotModeActive: 'Click on the image to place a hotspot',
|
|
9
|
-
addHotspot: 'Add hotspot',
|
|
10
|
-
zoomIn: 'Zoom in',
|
|
11
|
-
zoomOut: 'Zoom out',
|
|
12
|
-
resetView: 'Reset view',
|
|
13
|
-
deleteMarker: 'Remove marker',
|
|
14
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { Viewer360 } from './Viewer360';
|
|
5
|
-
import type { Viewer360Frame } from '../types';
|
|
6
|
-
|
|
7
|
-
const frames: Viewer360Frame[] = [
|
|
8
|
-
{ id: '1', src: 'https://example.com/1.jpg', label: 'Front' },
|
|
9
|
-
{ id: '2', src: 'https://example.com/2.jpg', label: 'Side' },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
describe('Viewer360', () => {
|
|
13
|
-
it('renders loading state and toolbar labels', () => {
|
|
14
|
-
render(
|
|
15
|
-
<Viewer360
|
|
16
|
-
frames={frames}
|
|
17
|
-
labels={{
|
|
18
|
-
loading: 'Loading test frames',
|
|
19
|
-
dragHint: 'Drag test hint',
|
|
20
|
-
}}
|
|
21
|
-
/>
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
expect(screen.getByText('Loading test frames')).toBeInTheDocument();
|
|
25
|
-
expect(screen.getByText('Drag test hint')).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('calls onFrameChange from auto-rotate when enabled', () => {
|
|
29
|
-
vi.useFakeTimers();
|
|
30
|
-
|
|
31
|
-
const onFrameChange = vi.fn();
|
|
32
|
-
|
|
33
|
-
render(
|
|
34
|
-
<Viewer360
|
|
35
|
-
frames={frames}
|
|
36
|
-
currentFrameIndex={0}
|
|
37
|
-
onFrameChange={onFrameChange}
|
|
38
|
-
config={{ autoRotate: true, autoRotateIntervalMs: 50 }}
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
vi.advanceTimersByTime(60);
|
|
43
|
-
expect(onFrameChange).toHaveBeenCalledWith(1);
|
|
44
|
-
|
|
45
|
-
vi.useRealTimers();
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
import { buildViewer360ThemeStyle, mergeViewer360ClassNames, mergeViewer360Labels } from '../helpers/viewer360PropsHelpers';
|
|
5
|
-
import { useViewer360 } from '../hooks/useViewer360';
|
|
6
|
-
import type { Viewer360OverlayRenderProps, Viewer360Props, Viewer360ToolbarRenderProps } from '../types';
|
|
7
|
-
import { Card } from '@/components/ui/Card';
|
|
8
|
-
import { cn } from '@/components/utils';
|
|
9
|
-
|
|
10
|
-
import { Viewer360AddModeBanner } from './Viewer360AddModeBanner';
|
|
11
|
-
import { Viewer360FrameIndicator } from './Viewer360FrameIndicator';
|
|
12
|
-
import { Viewer360HotspotOverlay } from './Viewer360HotspotOverlay';
|
|
13
|
-
import { Viewer360LoadingOverlay } from './Viewer360LoadingOverlay';
|
|
14
|
-
import { Viewer360Toolbar } from './Viewer360Toolbar';
|
|
15
|
-
|
|
16
|
-
export function Viewer360<TData = unknown>({
|
|
17
|
-
frames,
|
|
18
|
-
currentFrameIndex: controlledFrameIndex,
|
|
19
|
-
defaultFrameIndex = 0,
|
|
20
|
-
onFrameChange,
|
|
21
|
-
config,
|
|
22
|
-
className,
|
|
23
|
-
classNames,
|
|
24
|
-
style,
|
|
25
|
-
theme,
|
|
26
|
-
labels,
|
|
27
|
-
aspectRatio = '16 / 10',
|
|
28
|
-
showZoomControls = true,
|
|
29
|
-
showResetControl = true,
|
|
30
|
-
showFrameIndicator = true,
|
|
31
|
-
showDragHint = true,
|
|
32
|
-
showHotspotModeControl = false,
|
|
33
|
-
hotspotPin,
|
|
34
|
-
hotspots = [],
|
|
35
|
-
renderHotspot,
|
|
36
|
-
renderLoading,
|
|
37
|
-
renderFrameIndicator,
|
|
38
|
-
renderHotspotModeBanner,
|
|
39
|
-
renderToolbar,
|
|
40
|
-
onHotspotClick,
|
|
41
|
-
hotspotMode: controlledHotspotMode,
|
|
42
|
-
defaultHotspotMode = false,
|
|
43
|
-
onHotspotModeChange,
|
|
44
|
-
onHotspotAdd,
|
|
45
|
-
children,
|
|
46
|
-
}: Viewer360Props<TData>): JSX.Element {
|
|
47
|
-
// ----------------------------------------------------------------------------------------------------
|
|
48
|
-
// MARK: States & Constants
|
|
49
|
-
// ----------------------------------------------------------------------------------------------------
|
|
50
|
-
const mergedLabels = useMemo(() => mergeViewer360Labels(labels), [labels]);
|
|
51
|
-
const mergedClassNames = useMemo(() => mergeViewer360ClassNames(classNames), [classNames]);
|
|
52
|
-
const themeStyle = useMemo(() => buildViewer360ThemeStyle(theme), [theme]);
|
|
53
|
-
|
|
54
|
-
const [internalFrameIndex, setInternalFrameIndex] = useState(defaultFrameIndex);
|
|
55
|
-
const [internalHotspotMode, setInternalHotspotMode] = useState(defaultHotspotMode);
|
|
56
|
-
|
|
57
|
-
const currentFrameIndex = controlledFrameIndex ?? internalFrameIndex;
|
|
58
|
-
const hotspotMode = controlledHotspotMode ?? internalHotspotMode;
|
|
59
|
-
|
|
60
|
-
// ----------------------------------------------------------------------------------------------------
|
|
61
|
-
// MARK: Functions
|
|
62
|
-
// ----------------------------------------------------------------------------------------------------
|
|
63
|
-
function handleFrameChange(index: number): void {
|
|
64
|
-
if (controlledFrameIndex === undefined) {
|
|
65
|
-
setInternalFrameIndex(index);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
onFrameChange?.(index);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function handleHotspotModeChange(active: boolean): void {
|
|
72
|
-
if (controlledHotspotMode === undefined) {
|
|
73
|
-
setInternalHotspotMode(active);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
onHotspotModeChange?.(active);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const {
|
|
80
|
-
canvasRef,
|
|
81
|
-
containerRef,
|
|
82
|
-
currentFrame,
|
|
83
|
-
currentFrameHotspots,
|
|
84
|
-
imagesLoaded,
|
|
85
|
-
isHotspotMode,
|
|
86
|
-
isResetDisabled,
|
|
87
|
-
maxZoom,
|
|
88
|
-
minZoom,
|
|
89
|
-
viewerCursorClass,
|
|
90
|
-
zoom,
|
|
91
|
-
getHotspotScreenPosition,
|
|
92
|
-
handleCanvasClick,
|
|
93
|
-
handlePointerDown,
|
|
94
|
-
handlePointerMove,
|
|
95
|
-
handlePointerUp,
|
|
96
|
-
handleWheel,
|
|
97
|
-
handleResetView,
|
|
98
|
-
handleZoomIn,
|
|
99
|
-
handleZoomOut,
|
|
100
|
-
} = useViewer360<TData>({
|
|
101
|
-
frames,
|
|
102
|
-
hotspots,
|
|
103
|
-
currentFrameIndex,
|
|
104
|
-
onFrameChange: handleFrameChange,
|
|
105
|
-
config,
|
|
106
|
-
hotspotMode,
|
|
107
|
-
onHotspotAdd,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (controlledHotspotMode === undefined) return;
|
|
112
|
-
if (controlledHotspotMode !== internalHotspotMode) {
|
|
113
|
-
setInternalHotspotMode(controlledHotspotMode);
|
|
114
|
-
}
|
|
115
|
-
}, [controlledHotspotMode, internalHotspotMode]);
|
|
116
|
-
|
|
117
|
-
const frameLabel = currentFrame?.label ?? frames[currentFrameIndex]?.label;
|
|
118
|
-
const overlayProps: Viewer360OverlayRenderProps = {
|
|
119
|
-
currentFrameIndex,
|
|
120
|
-
frameCount: frames.length,
|
|
121
|
-
frameLabel,
|
|
122
|
-
isHotspotMode,
|
|
123
|
-
labels: mergedLabels,
|
|
124
|
-
frameIndicatorClassName: mergedClassNames.frameIndicator,
|
|
125
|
-
};
|
|
126
|
-
const toolbarProps: Viewer360ToolbarRenderProps = {
|
|
127
|
-
zoom,
|
|
128
|
-
minZoom,
|
|
129
|
-
maxZoom,
|
|
130
|
-
isResetDisabled,
|
|
131
|
-
isHotspotMode,
|
|
132
|
-
showHotspotModeControl,
|
|
133
|
-
showZoomControls,
|
|
134
|
-
showResetControl,
|
|
135
|
-
showDragHint,
|
|
136
|
-
labels: mergedLabels,
|
|
137
|
-
onZoomIn: handleZoomIn,
|
|
138
|
-
onZoomOut: handleZoomOut,
|
|
139
|
-
onResetView: handleResetView,
|
|
140
|
-
onHotspotModeChange: handleHotspotModeChange,
|
|
141
|
-
};
|
|
142
|
-
const showDefaultToolbar = showZoomControls || showResetControl || showHotspotModeControl || showDragHint;
|
|
143
|
-
|
|
144
|
-
// ----------------------------------------------------------------------------------------------------
|
|
145
|
-
// MARK: Main Component UI
|
|
146
|
-
// ----------------------------------------------------------------------------------------------------
|
|
147
|
-
return (
|
|
148
|
-
<Card
|
|
149
|
-
data-viewer-360=""
|
|
150
|
-
className={cn(mergedClassNames.root, 'gap-0 py-0 shadow-none ring-0', className)}
|
|
151
|
-
style={{ ...themeStyle, ...style }}
|
|
152
|
-
>
|
|
153
|
-
<div
|
|
154
|
-
ref={containerRef}
|
|
155
|
-
className={cn(mergedClassNames.viewport, viewerCursorClass)}
|
|
156
|
-
style={{ aspectRatio }}
|
|
157
|
-
onPointerDown={handlePointerDown}
|
|
158
|
-
onPointerMove={handlePointerMove}
|
|
159
|
-
onPointerUp={handlePointerUp}
|
|
160
|
-
onPointerLeave={handlePointerUp}
|
|
161
|
-
onWheel={handleWheel}
|
|
162
|
-
onClick={handleCanvasClick}
|
|
163
|
-
>
|
|
164
|
-
<canvas ref={canvasRef} className={mergedClassNames.canvas} />
|
|
165
|
-
|
|
166
|
-
<div className={mergedClassNames.overlay}>
|
|
167
|
-
{currentFrameHotspots.map((hotspot) => {
|
|
168
|
-
const position = getHotspotScreenPosition(hotspot);
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<Viewer360HotspotOverlay
|
|
172
|
-
key={hotspot.id}
|
|
173
|
-
hotspot={hotspot}
|
|
174
|
-
leftPercent={position.leftPercent}
|
|
175
|
-
topPercent={position.topPercent}
|
|
176
|
-
hotspotPin={hotspotPin}
|
|
177
|
-
renderHotspot={renderHotspot}
|
|
178
|
-
onHotspotClick={onHotspotClick}
|
|
179
|
-
/>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
|
-
{children}
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
{!imagesLoaded &&
|
|
186
|
-
(renderLoading ? (
|
|
187
|
-
renderLoading()
|
|
188
|
-
) : (
|
|
189
|
-
<Viewer360LoadingOverlay
|
|
190
|
-
className={mergedClassNames.loading}
|
|
191
|
-
textClassName={mergedClassNames.loadingText}
|
|
192
|
-
label={mergedLabels.loading}
|
|
193
|
-
/>
|
|
194
|
-
))}
|
|
195
|
-
|
|
196
|
-
{showFrameIndicator &&
|
|
197
|
-
frames.length > 0 &&
|
|
198
|
-
(renderFrameIndicator ? (
|
|
199
|
-
renderFrameIndicator(overlayProps)
|
|
200
|
-
) : (
|
|
201
|
-
<Viewer360FrameIndicator
|
|
202
|
-
className={mergedClassNames.frameIndicator}
|
|
203
|
-
label={mergedLabels.frameIndicator({
|
|
204
|
-
current: currentFrameIndex + 1,
|
|
205
|
-
total: frames.length,
|
|
206
|
-
label: frameLabel,
|
|
207
|
-
})}
|
|
208
|
-
/>
|
|
209
|
-
))}
|
|
210
|
-
|
|
211
|
-
{isHotspotMode &&
|
|
212
|
-
onHotspotAdd &&
|
|
213
|
-
(renderHotspotModeBanner ? (
|
|
214
|
-
renderHotspotModeBanner({ labels: mergedLabels })
|
|
215
|
-
) : (
|
|
216
|
-
<Viewer360AddModeBanner className={mergedClassNames.hotspotModeBanner} label={mergedLabels.hotspotModeActive} />
|
|
217
|
-
))}
|
|
218
|
-
</div>
|
|
219
|
-
|
|
220
|
-
{renderToolbar ? renderToolbar(toolbarProps) : showDefaultToolbar ? <Viewer360Toolbar {...toolbarProps} /> : null}
|
|
221
|
-
</Card>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
|
|
3
|
-
import { Badge } from '@/components/ui/Badge';
|
|
4
|
-
import { cn } from '@/components/utils';
|
|
5
|
-
|
|
6
|
-
type Viewer360AddModeBannerProps = {
|
|
7
|
-
className?: string;
|
|
8
|
-
label: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function Viewer360AddModeBanner({ className, label }: Viewer360AddModeBannerProps): JSX.Element {
|
|
12
|
-
// ----------------------------------------------------------------------------------------------------
|
|
13
|
-
// MARK: Main Component UI
|
|
14
|
-
// ----------------------------------------------------------------------------------------------------
|
|
15
|
-
return (
|
|
16
|
-
<Badge variant="outline" className={cn('pointer-events-none', className)}>
|
|
17
|
-
{label}
|
|
18
|
-
</Badge>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
|
|
3
|
-
import { Badge } from '@/components/ui/Badge';
|
|
4
|
-
import { cn } from '@/components/utils';
|
|
5
|
-
|
|
6
|
-
type Viewer360FrameIndicatorProps = {
|
|
7
|
-
className?: string;
|
|
8
|
-
label: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function Viewer360FrameIndicator({ className, label }: Viewer360FrameIndicatorProps): JSX.Element {
|
|
12
|
-
// ----------------------------------------------------------------------------------------------------
|
|
13
|
-
// MARK: Main Component UI
|
|
14
|
-
// ----------------------------------------------------------------------------------------------------
|
|
15
|
-
return (
|
|
16
|
-
<Badge variant="outline" className={cn('pointer-events-none shadow-sm', className)}>
|
|
17
|
-
{label}
|
|
18
|
-
</Badge>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { JSX, MouseEvent, ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
import { hotspotToViewer360Marker } from '../helpers/markerHelpers';
|
|
4
|
-
import type { Viewer360Hotspot, Viewer360HotspotPinOptions, Viewer360HotspotRenderProps } from '../types';
|
|
5
|
-
import { Item } from '@/components/ui/Item';
|
|
6
|
-
|
|
7
|
-
import { Viewer360MarkerPin } from './Viewer360MarkerPin';
|
|
8
|
-
|
|
9
|
-
type Viewer360HotspotOverlayProps<TData = unknown> = {
|
|
10
|
-
hotspot: Viewer360Hotspot<TData>;
|
|
11
|
-
leftPercent: number;
|
|
12
|
-
topPercent: number;
|
|
13
|
-
hotspotPin?: Viewer360HotspotPinOptions<TData>;
|
|
14
|
-
renderHotspot?: (props: Viewer360HotspotRenderProps<TData>) => ReactNode;
|
|
15
|
-
onHotspotClick?: (hotspot: Viewer360Hotspot<TData>, event: MouseEvent<HTMLDivElement>) => void;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function Viewer360HotspotOverlay<TData = unknown>({
|
|
19
|
-
hotspot,
|
|
20
|
-
leftPercent,
|
|
21
|
-
topPercent,
|
|
22
|
-
hotspotPin,
|
|
23
|
-
renderHotspot,
|
|
24
|
-
onHotspotClick,
|
|
25
|
-
}: Viewer360HotspotOverlayProps<TData>): JSX.Element {
|
|
26
|
-
// ----------------------------------------------------------------------------------------------------
|
|
27
|
-
// MARK: Main Component UI
|
|
28
|
-
// ----------------------------------------------------------------------------------------------------
|
|
29
|
-
if (renderHotspot) {
|
|
30
|
-
return (
|
|
31
|
-
<Item size="xs" variant="default" className="pointer-events-auto w-auto border-transparent p-0">
|
|
32
|
-
{renderHotspot({ hotspot, leftPercent, topPercent })}
|
|
33
|
-
</Item>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const marker = hotspotPin?.getMarker?.(hotspot) ?? hotspotToViewer360Marker(hotspot);
|
|
38
|
-
|
|
39
|
-
return (
|
|
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
|
-
}
|
|
55
|
-
/>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { JSX } from 'react';
|
|
2
|
-
|
|
3
|
-
import { Item } from '@/components/ui/Item';
|
|
4
|
-
import { Label } from '@/components/ui/Label';
|
|
5
|
-
import { Spinner } from '@/components/ui/Spinner';
|
|
6
|
-
import { cn } from '@/components/utils';
|
|
7
|
-
|
|
8
|
-
type Viewer360LoadingOverlayProps = {
|
|
9
|
-
className?: string;
|
|
10
|
-
textClassName?: string;
|
|
11
|
-
label: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function Viewer360LoadingOverlay({ className, textClassName, label }: Viewer360LoadingOverlayProps): JSX.Element {
|
|
15
|
-
// ----------------------------------------------------------------------------------------------------
|
|
16
|
-
// MARK: Main Component UI
|
|
17
|
-
// ----------------------------------------------------------------------------------------------------
|
|
18
|
-
return (
|
|
19
|
-
<Item
|
|
20
|
-
size="sm"
|
|
21
|
-
variant="muted"
|
|
22
|
-
className={cn('pointer-events-none w-auto justify-center border-transparent bg-muted/80', className)}
|
|
23
|
-
>
|
|
24
|
-
<Spinner className="size-5 text-muted-foreground" />
|
|
25
|
-
<Label className={cn('font-normal text-muted-foreground', textClassName)}>{label}</Label>
|
|
26
|
-
</Item>
|
|
27
|
-
);
|
|
28
|
-
}
|