@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +3 -3
  3. package/src/components/ui/Badge/index.tsx +0 -45
  4. package/src/components/ui/Button/index.tsx +0 -67
  5. package/src/components/ui/Card/index.tsx +0 -74
  6. package/src/components/ui/Item/index.tsx +0 -136
  7. package/src/components/ui/Label/index.tsx +0 -20
  8. package/src/components/ui/Popover/index.tsx +0 -56
  9. package/src/components/ui/Separator/index.tsx +0 -30
  10. package/src/components/ui/Spinner/index.tsx +0 -11
  11. package/src/components/utils/index.ts +0 -6
  12. package/src/constants/viewer360ClassNames.ts +0 -42
  13. package/src/constants/viewer360Config.ts +0 -11
  14. package/src/constants/viewer360Labels.ts +0 -14
  15. package/src/constants/viewer360MarkerLabels.ts +0 -3
  16. package/src/feature/Viewer360.test.tsx +0 -47
  17. package/src/feature/Viewer360.tsx +0 -223
  18. package/src/feature/Viewer360AddModeBanner.tsx +0 -20
  19. package/src/feature/Viewer360FrameIndicator.tsx +0 -20
  20. package/src/feature/Viewer360HotspotOverlay.tsx +0 -57
  21. package/src/feature/Viewer360LoadingOverlay.tsx +0 -28
  22. package/src/feature/Viewer360MarkerPin.tsx +0 -105
  23. package/src/feature/Viewer360Toolbar.tsx +0 -75
  24. package/src/helpers/adjustViewerZoom.test.ts +0 -29
  25. package/src/helpers/adjustViewerZoom.ts +0 -64
  26. package/src/helpers/computeDragFrameIndex.test.ts +0 -20
  27. package/src/helpers/computeDragFrameIndex.ts +0 -23
  28. package/src/helpers/computeViewerImageLayout.test.ts +0 -48
  29. package/src/helpers/computeViewerImageLayout.ts +0 -114
  30. package/src/helpers/computeViewerPanOffset.ts +0 -18
  31. package/src/helpers/markerHelpers.test.ts +0 -38
  32. package/src/helpers/markerHelpers.ts +0 -33
  33. package/src/helpers/viewer360PropsHelpers.ts +0 -46
  34. package/src/helpers/viewerHelpers.ts +0 -74
  35. package/src/hooks/useViewer360.ts +0 -306
  36. package/src/index.ts +0 -68
  37. package/src/styles.css +0 -80
  38. package/src/types/Viewer360Hotspot.ts +0 -67
  39. package/src/types/Viewer360Marker.ts +0 -52
  40. package/src/types/Viewer360Props.ts +0 -108
  41. package/src/types/index.ts +0 -30
  42. 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.13",
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
- "src",
30
- "README.md"
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,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
- });