@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/index.js +3590 -65
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +11 -18
  7. package/dist/inject-styles.js +0 -21
  8. package/src/components/ui/Badge/index.tsx +0 -45
  9. package/src/components/ui/Button/index.tsx +0 -67
  10. package/src/components/ui/Card/index.tsx +0 -74
  11. package/src/components/ui/Item/index.tsx +0 -136
  12. package/src/components/ui/Label/index.tsx +0 -20
  13. package/src/components/ui/Popover/index.tsx +0 -56
  14. package/src/components/ui/Separator/index.tsx +0 -30
  15. package/src/components/ui/Spinner/index.tsx +0 -11
  16. package/src/components/utils/index.ts +0 -6
  17. package/src/constants/viewer360ClassNames.ts +0 -42
  18. package/src/constants/viewer360Config.ts +0 -11
  19. package/src/constants/viewer360Labels.ts +0 -14
  20. package/src/constants/viewer360MarkerLabels.ts +0 -3
  21. package/src/feature/Viewer360.test.tsx +0 -47
  22. package/src/feature/Viewer360.tsx +0 -223
  23. package/src/feature/Viewer360AddModeBanner.tsx +0 -20
  24. package/src/feature/Viewer360FrameIndicator.tsx +0 -20
  25. package/src/feature/Viewer360HotspotOverlay.tsx +0 -57
  26. package/src/feature/Viewer360LoadingOverlay.tsx +0 -28
  27. package/src/feature/Viewer360MarkerPin.tsx +0 -105
  28. package/src/feature/Viewer360Toolbar.tsx +0 -75
  29. package/src/helpers/adjustViewerZoom.test.ts +0 -29
  30. package/src/helpers/adjustViewerZoom.ts +0 -64
  31. package/src/helpers/computeDragFrameIndex.test.ts +0 -20
  32. package/src/helpers/computeDragFrameIndex.ts +0 -23
  33. package/src/helpers/computeViewerImageLayout.test.ts +0 -48
  34. package/src/helpers/computeViewerImageLayout.ts +0 -114
  35. package/src/helpers/computeViewerPanOffset.ts +0 -18
  36. package/src/helpers/markerHelpers.test.ts +0 -38
  37. package/src/helpers/markerHelpers.ts +0 -33
  38. package/src/helpers/viewer360PropsHelpers.ts +0 -46
  39. package/src/helpers/viewerHelpers.ts +0 -74
  40. package/src/hooks/useViewer360.ts +0 -306
  41. package/src/index.ts +0 -68
  42. package/src/styles.css +0 -80
  43. package/src/types/Viewer360Hotspot.ts +0 -67
  44. package/src/types/Viewer360Marker.ts +0 -52
  45. package/src/types/Viewer360Props.ts +0 -108
  46. package/src/types/index.ts +0 -30
  47. 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,6 +0,0 @@
1
- import { clsx, type ClassValue } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
@@ -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,3 +0,0 @@
1
- export const defaultViewer360MarkerPinLabels = {
2
- delete: 'Remove marker',
3
- } as const;
@@ -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
- }