@mmmmzxe/react-360-viewer 0.1.13 → 0.1.14
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/package.json +3 -3
- 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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 maryemmostafa24
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmmzxe/react-360-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "A standalone, configurable 360° image viewer for React with drag rotation, zoom, hotspots, and auto-rotate support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsup && npm run build:css && node scripts/bundle-inject-styles.mjs",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import type { JSX } from 'react';
|
|
3
|
-
|
|
4
|
-
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
-
import { Slot } from 'radix-ui';
|
|
6
|
-
|
|
7
|
-
import { cn } from '@/components/utils';
|
|
8
|
-
|
|
9
|
-
const badgeVariants = cva(
|
|
10
|
-
'h-5 gap-1 rounded-full 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',
|
|
11
|
-
{
|
|
12
|
-
variants: {
|
|
13
|
-
variant: {
|
|
14
|
-
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
15
|
-
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
|
16
|
-
destructive:
|
|
17
|
-
'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',
|
|
18
|
-
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
|
19
|
-
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
|
20
|
-
link: 'text-primary underline-offset-4 hover:underline',
|
|
21
|
-
info: 'bg-blue-500 text-white hover:bg-blue-600',
|
|
22
|
-
warning:
|
|
23
|
-
'rounded-full border-transparent bg-amber-500 px-2.5 text-white shadow-none [a]:hover:bg-amber-600 [&>svg]:text-white',
|
|
24
|
-
success: 'bg-green-600 text-white hover:bg-green-700',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
defaultVariants: {
|
|
28
|
-
variant: 'default',
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
function Badge({
|
|
34
|
-
className,
|
|
35
|
-
variant = 'default',
|
|
36
|
-
asChild = false,
|
|
37
|
-
...props
|
|
38
|
-
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }): JSX.Element {
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
40
|
-
const Comp = asChild ? Slot.Root : 'span';
|
|
41
|
-
|
|
42
|
-
return <Comp data-slot="badge" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { Badge, badgeVariants };
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import type { JSX } from 'react';
|
|
3
|
-
|
|
4
|
-
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
-
import { Slot } from 'radix-ui';
|
|
6
|
-
|
|
7
|
-
import { cn } from '@/components/utils';
|
|
8
|
-
|
|
9
|
-
const buttonVariants = cva(
|
|
10
|
-
'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-lg 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',
|
|
11
|
-
{
|
|
12
|
-
variants: {
|
|
13
|
-
variant: {
|
|
14
|
-
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
|
15
|
-
outline:
|
|
16
|
-
'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',
|
|
17
|
-
secondary:
|
|
18
|
-
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
|
19
|
-
ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
|
|
20
|
-
destructive:
|
|
21
|
-
'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',
|
|
22
|
-
link: 'text-primary underline-offset-4 hover:underline',
|
|
23
|
-
},
|
|
24
|
-
size: {
|
|
25
|
-
default:
|
|
26
|
-
'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',
|
|
27
|
-
xs: 'h-6 gap-1 rounded-md 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',
|
|
28
|
-
sm: 'h-8 gap-1 rounded-md 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',
|
|
29
|
-
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3',
|
|
30
|
-
icon: 'size-9',
|
|
31
|
-
'icon-xs': 'size-6 rounded-md in-data-[slot=button-group]:rounded-md',
|
|
32
|
-
'icon-sm': 'size-8 rounded-md in-data-[slot=button-group]:rounded-md',
|
|
33
|
-
'icon-lg': 'size-10',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
defaultVariants: {
|
|
37
|
-
variant: 'default',
|
|
38
|
-
size: 'default',
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
function Button({
|
|
44
|
-
className,
|
|
45
|
-
variant = 'default',
|
|
46
|
-
size = 'default',
|
|
47
|
-
asChild = false,
|
|
48
|
-
...props
|
|
49
|
-
}: React.ComponentProps<'button'> &
|
|
50
|
-
VariantProps<typeof buttonVariants> & {
|
|
51
|
-
asChild?: boolean;
|
|
52
|
-
}): JSX.Element {
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
54
|
-
const Comp = asChild ? Slot.Root : 'button';
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<Comp
|
|
58
|
-
data-slot="button"
|
|
59
|
-
data-variant={variant}
|
|
60
|
-
data-size={size}
|
|
61
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
62
|
-
{...props}
|
|
63
|
-
/>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export { Button, buttonVariants };
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import type { JSX } from 'react';
|
|
3
|
-
|
|
4
|
-
import { cn } from '@/components/utils';
|
|
5
|
-
|
|
6
|
-
function Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }): JSX.Element {
|
|
7
|
-
return (
|
|
8
|
-
<div
|
|
9
|
-
data-slot="card"
|
|
10
|
-
data-size={size}
|
|
11
|
-
className={cn(
|
|
12
|
-
'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',
|
|
13
|
-
className
|
|
14
|
-
)}
|
|
15
|
-
{...props}
|
|
16
|
-
/>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function CardHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
21
|
-
return (
|
|
22
|
-
<div
|
|
23
|
-
data-slot="card-header"
|
|
24
|
-
className={cn(
|
|
25
|
-
'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]',
|
|
26
|
-
className
|
|
27
|
-
)}
|
|
28
|
-
{...props}
|
|
29
|
-
/>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function CardTitle({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
34
|
-
return (
|
|
35
|
-
<div
|
|
36
|
-
data-slot="card-title"
|
|
37
|
-
className={cn('text-lg leading-normal font-semibold group-data-[size=sm]/card:text-sm', className)}
|
|
38
|
-
{...props}
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function CardDescription({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
44
|
-
return <div data-slot="card-description" className={cn('text-muted-foreground text-xs font-medium', className)} {...props} />;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function CardAction({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
48
|
-
return (
|
|
49
|
-
<div
|
|
50
|
-
data-slot="card-action"
|
|
51
|
-
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
|
52
|
-
{...props}
|
|
53
|
-
/>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function CardContent({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
58
|
-
return <div data-slot="card-content" className={cn('px-6 group-data-[size=sm]/card:px-4', className)} {...props} />;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function CardFooter({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
62
|
-
return (
|
|
63
|
-
<div
|
|
64
|
-
data-slot="card-footer"
|
|
65
|
-
className={cn(
|
|
66
|
-
'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',
|
|
67
|
-
className
|
|
68
|
-
)}
|
|
69
|
-
{...props}
|
|
70
|
-
/>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import type { JSX } from 'react';
|
|
3
|
-
|
|
4
|
-
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
-
import { Slot } from 'radix-ui';
|
|
6
|
-
|
|
7
|
-
import { Separator } from '@/components/ui/Separator';
|
|
8
|
-
import { cn } from '@/components/utils';
|
|
9
|
-
|
|
10
|
-
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
11
|
-
return (
|
|
12
|
-
<div
|
|
13
|
-
role="list"
|
|
14
|
-
data-slot="item-group"
|
|
15
|
-
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)}
|
|
16
|
-
{...props}
|
|
17
|
-
/>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>): JSX.Element {
|
|
22
|
-
return <Separator data-slot="item-separator" orientation="horizontal" className={cn('my-2', className)} {...props} />;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const itemVariants = cva(
|
|
26
|
-
'[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',
|
|
27
|
-
{
|
|
28
|
-
variants: {
|
|
29
|
-
variant: {
|
|
30
|
-
default: 'border-transparent',
|
|
31
|
-
outline: 'border-border',
|
|
32
|
-
muted: 'bg-muted/50 border-transparent',
|
|
33
|
-
},
|
|
34
|
-
size: {
|
|
35
|
-
default: 'gap-3.5 px-4 py-3.5',
|
|
36
|
-
sm: 'gap-2.5 px-3 py-2.5',
|
|
37
|
-
xs: 'gap-2 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0',
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
defaultVariants: {
|
|
41
|
-
variant: 'default',
|
|
42
|
-
size: 'default',
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
function Item({
|
|
48
|
-
className,
|
|
49
|
-
variant = 'default',
|
|
50
|
-
size = 'default',
|
|
51
|
-
asChild = false,
|
|
52
|
-
...props
|
|
53
|
-
}: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & { asChild?: boolean }): JSX.Element {
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
55
|
-
const Comp = asChild ? Slot.Root : 'div';
|
|
56
|
-
return (
|
|
57
|
-
<Comp
|
|
58
|
-
data-slot="item"
|
|
59
|
-
data-variant={variant}
|
|
60
|
-
data-size={size}
|
|
61
|
-
className={cn(itemVariants({ variant, size, className }))}
|
|
62
|
-
{...props}
|
|
63
|
-
/>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const itemMediaVariants = cva(
|
|
68
|
-
'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',
|
|
69
|
-
{
|
|
70
|
-
variants: {
|
|
71
|
-
variant: {
|
|
72
|
-
default: 'bg-transparent',
|
|
73
|
-
icon: '[&_svg]:size-4',
|
|
74
|
-
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',
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
defaultVariants: {
|
|
78
|
-
variant: 'default',
|
|
79
|
-
},
|
|
80
|
-
}
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
function ItemMedia({
|
|
84
|
-
className,
|
|
85
|
-
variant = 'default',
|
|
86
|
-
...props
|
|
87
|
-
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>): JSX.Element {
|
|
88
|
-
return <div data-slot="item-media" data-variant={variant} className={cn(itemMediaVariants({ variant, className }))} {...props} />;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function ItemContent({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
92
|
-
return (
|
|
93
|
-
<div
|
|
94
|
-
data-slot="item-content"
|
|
95
|
-
className={cn('gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none', className)}
|
|
96
|
-
{...props}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
102
|
-
return (
|
|
103
|
-
<div
|
|
104
|
-
data-slot="item-title"
|
|
105
|
-
className={cn('gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center', className)}
|
|
106
|
-
{...props}
|
|
107
|
-
/>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>): JSX.Element {
|
|
112
|
-
return (
|
|
113
|
-
<p
|
|
114
|
-
data-slot="item-description"
|
|
115
|
-
className={cn(
|
|
116
|
-
'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',
|
|
117
|
-
className
|
|
118
|
-
)}
|
|
119
|
-
{...props}
|
|
120
|
-
/>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function ItemActions({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
125
|
-
return <div data-slot="item-actions" className={cn('gap-2 flex items-center', className)} {...props} />;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
129
|
-
return <div data-slot="item-header" className={cn('gap-2 flex basis-full items-center justify-between', className)} {...props} />;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>): JSX.Element {
|
|
133
|
-
return <div data-slot="item-footer" className={cn('gap-2 flex basis-full items-center justify-between', className)} {...props} />;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export { Item, ItemMedia, ItemContent, ItemActions, ItemGroup, ItemSeparator, ItemTitle, ItemDescription, ItemHeader, ItemFooter };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
|
|
5
|
-
import { cn } from '@/components/utils';
|
|
6
|
-
|
|
7
|
-
function Label({ className, ...props }: React.ComponentProps<'label'>): React.ReactNode {
|
|
8
|
-
return (
|
|
9
|
-
<label
|
|
10
|
-
data-slot="label"
|
|
11
|
-
className={cn(
|
|
12
|
-
'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',
|
|
13
|
-
className
|
|
14
|
-
)}
|
|
15
|
-
{...props}
|
|
16
|
-
/>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export { Label };
|
|
@@ -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
|
-
});
|