@orsetra/shared-ui 1.0.12 → 1.0.13

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.
@@ -0,0 +1,117 @@
1
+ "use client"
2
+
3
+ import { useState, useRef } from "react"
4
+ import { FileCode, Package, Boxes, Pencil } from "lucide-react"
5
+ import { ImageCropDialog } from "./image-crop-dialog"
6
+
7
+ // Get icon for asset type
8
+ export const getAssetIcon = (type?: string) => {
9
+ if (!type) return Boxes
10
+ if (type.startsWith('operation')) return FileCode
11
+ if (type.startsWith('component')) return Package
12
+ return Boxes
13
+ }
14
+
15
+ // Get color for asset type
16
+ export const getAssetColor = (type?: string) => {
17
+ if (!type) return 'text-purple-600 bg-purple-50'
18
+ if (type.startsWith('operation')) return 'text-blue-600 bg-blue-50'
19
+ if (type.startsWith('component')) return 'text-green-600 bg-green-50'
20
+ return 'text-purple-600 bg-purple-50'
21
+ }
22
+
23
+ interface AssetLogoProps {
24
+ type: string
25
+ size?: 'sm' | 'md' | 'lg'
26
+ editable?: boolean
27
+ logoUrl?: string | null
28
+ onLogoChange?: (logoBase64: string) => void
29
+ }
30
+
31
+ export function AssetLogo({ type, size = 'md', editable = false, logoUrl, onLogoChange }: AssetLogoProps) {
32
+ const [isHovered, setIsHovered] = useState(false)
33
+ const [showCropDialog, setShowCropDialog] = useState(false)
34
+ const [tempImageSrc, setTempImageSrc] = useState<string>("")
35
+ const fileInputRef = useRef<HTMLInputElement>(null)
36
+
37
+ const Icon = getAssetIcon(type)
38
+ const colorClass = getAssetColor(type)
39
+
40
+ const sizeClasses = {
41
+ sm: 'w-8 h-8',
42
+ md: 'w-12 h-12',
43
+ lg: 'w-16 h-16'
44
+ }
45
+
46
+ const iconSizes = {
47
+ sm: 'h-4 w-4',
48
+ md: 'h-6 w-6',
49
+ lg: 'h-8 w-8'
50
+ }
51
+
52
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ const file = e.target.files?.[0]
54
+ if (file) {
55
+ const reader = new FileReader()
56
+ reader.onload = () => {
57
+ setTempImageSrc(reader.result as string)
58
+ setShowCropDialog(true)
59
+ }
60
+ reader.readAsDataURL(file)
61
+ }
62
+ }
63
+
64
+ const handleCropComplete = (croppedImageBase64: string) => {
65
+ if (onLogoChange) {
66
+ onLogoChange(croppedImageBase64)
67
+ }
68
+ }
69
+
70
+ const handleEditClick = () => {
71
+ fileInputRef.current?.click()
72
+ }
73
+
74
+ return (
75
+ <>
76
+ <div
77
+ className={`${sizeClasses[size]} rounded-full ${!logoUrl ? colorClass : 'bg-white border-2 border-gray-200'} flex items-center justify-center flex-shrink-0 relative group cursor-pointer`}
78
+ onMouseEnter={() => editable && setIsHovered(true)}
79
+ onMouseLeave={() => editable && setIsHovered(false)}
80
+ onClick={editable ? handleEditClick : undefined}
81
+ >
82
+ {logoUrl ? (
83
+ <img
84
+ src={logoUrl}
85
+ alt="Asset logo"
86
+ className="w-full h-full rounded-full object-cover"
87
+ />
88
+ ) : (
89
+ <Icon className={iconSizes[size]} />
90
+ )}
91
+
92
+ {editable && isHovered && (
93
+ <div className="absolute inset-0 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
94
+ <Pencil className="h-4 w-4 text-white" />
95
+ </div>
96
+ )}
97
+ </div>
98
+
99
+ <input
100
+ ref={fileInputRef}
101
+ type="file"
102
+ accept="image/*"
103
+ onChange={handleFileSelect}
104
+ className="hidden"
105
+ />
106
+
107
+ {showCropDialog && (
108
+ <ImageCropDialog
109
+ open={showCropDialog}
110
+ onClose={() => setShowCropDialog(false)}
111
+ imageSrc={tempImageSrc}
112
+ onCropComplete={handleCropComplete}
113
+ />
114
+ )}
115
+ </>
116
+ )
117
+ }
@@ -20,30 +20,33 @@ export function AssetsHeader({
20
20
  subtitle,
21
21
  }: AssetsHeaderProps) {
22
22
  return (
23
- <div className="border-b bg-white flex items-center justify-between px-3 py-3 sticky top-0 z-10">
24
- {/* Left side - Back link, Logo and Title */}
25
- <div className="flex items-center gap-3">
26
- {backLink && (
27
- <Link href={backLink}>
28
- <ArrowLeft className="h-5 w-5 text-text-secondary" />
29
- </Link>
30
- )}
31
- {logo && <div className="flex-shrink-0">{logo}</div>}
32
- <div className="flex flex-col">
33
- {typeof title === 'string' ? (
34
- <h2 className="text-base font-semibold text-text-primary">{title}</h2>
35
- ) : (
36
- title
37
- )}
38
- {subtitle && (
39
- <p className="text-xs text-gray-500 mt-0.5">{subtitle}</p>
23
+ <div className="border-b bg-white sticky top-0 z-10">
24
+ {/* Mobile: Stack vertically, Desktop: Horizontal */}
25
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-3 sm:px-4 py-3 gap-3 sm:gap-0">
26
+ {/* Left side - Back link, Logo and Title */}
27
+ <div className="flex items-center gap-3 min-w-0">
28
+ {backLink && (
29
+ <Link href={backLink} className="flex-shrink-0">
30
+ <ArrowLeft className="h-5 w-5 text-text-secondary" />
31
+ </Link>
40
32
  )}
33
+ {logo && <div className="flex-shrink-0">{logo}</div>}
34
+ <div className="flex flex-col min-w-0">
35
+ {typeof title === 'string' ? (
36
+ <h2 className="text-sm sm:text-base font-semibold text-text-primary truncate">{title}</h2>
37
+ ) : (
38
+ title
39
+ )}
40
+ {subtitle && (
41
+ <p className="text-xs text-gray-500 mt-0.5 truncate">{subtitle}</p>
42
+ )}
43
+ </div>
41
44
  </div>
42
- </div>
43
45
 
44
- {/* Right side - Actions */}
45
- <div className="flex items-center justify-end gap-2 py-2 pr-3">
46
- {actions}
46
+ {/* Right side - Actions */}
47
+ <div className="flex items-center justify-start sm:justify-end gap-2 flex-wrap">
48
+ {actions}
49
+ </div>
47
50
  </div>
48
51
  </div>
49
52
  )
@@ -0,0 +1,90 @@
1
+ "use client"
2
+
3
+ import { useState, useRef, useEffect } from "react"
4
+ import { Pencil, Check, X } from "lucide-react"
5
+ import { Input } from "./input"
6
+ import { Button } from "./button"
7
+
8
+ interface EditableTitleProps {
9
+ value: string
10
+ onSave: (newValue: string) => void
11
+ className?: string
12
+ }
13
+
14
+ export function EditableTitle({ value, onSave, className = "" }: EditableTitleProps) {
15
+ const [isEditing, setIsEditing] = useState(false)
16
+ const [isHovered, setIsHovered] = useState(false)
17
+ const [editValue, setEditValue] = useState(value)
18
+ const inputRef = useRef<HTMLInputElement>(null)
19
+
20
+ useEffect(() => {
21
+ if (isEditing && inputRef.current) {
22
+ inputRef.current.focus()
23
+ inputRef.current.select()
24
+ }
25
+ }, [isEditing])
26
+
27
+ const handleSave = () => {
28
+ if (editValue.trim() && editValue !== value) {
29
+ onSave(editValue.trim())
30
+ }
31
+ setIsEditing(false)
32
+ }
33
+
34
+ const handleCancel = () => {
35
+ setEditValue(value)
36
+ setIsEditing(false)
37
+ }
38
+
39
+ const handleKeyDown = (e: React.KeyboardEvent) => {
40
+ if (e.key === 'Enter') {
41
+ handleSave()
42
+ } else if (e.key === 'Escape') {
43
+ handleCancel()
44
+ }
45
+ }
46
+
47
+ if (isEditing) {
48
+ return (
49
+ <div className="flex items-center gap-2">
50
+ <Input
51
+ ref={inputRef}
52
+ value={editValue}
53
+ onChange={(e) => setEditValue(e.target.value)}
54
+ onKeyDown={handleKeyDown}
55
+ className="h-8 min-w-[300px]"
56
+ />
57
+ <Button
58
+ size="sm"
59
+ variant="default"
60
+ onClick={handleSave}
61
+ className="h-8 w-8 p-0"
62
+ >
63
+ <Check className="h-4 w-4" />
64
+ </Button>
65
+ <Button
66
+ size="sm"
67
+ variant="secondary"
68
+ onClick={handleCancel}
69
+ className="h-8 w-8 p-0"
70
+ >
71
+ <X className="h-4 w-4" />
72
+ </Button>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ return (
78
+ <div
79
+ className={`flex items-center gap-2 group cursor-pointer ${className}`}
80
+ onMouseEnter={() => setIsHovered(true)}
81
+ onMouseLeave={() => setIsHovered(false)}
82
+ onClick={() => setIsEditing(true)}
83
+ >
84
+ <h2 className="text-base font-semibold text-text-primary">{value}</h2>
85
+ {isHovered && (
86
+ <Pencil className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity" />
87
+ )}
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,157 @@
1
+ "use client"
2
+
3
+ import { useState, useCallback } from "react"
4
+ import Cropper from "react-easy-crop"
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "./dialog"
6
+ import { Button } from "./button"
7
+ import { Slider } from "./slider"
8
+
9
+ interface ImageCropDialogProps {
10
+ open: boolean
11
+ onClose: () => void
12
+ imageSrc: string
13
+ onCropComplete: (croppedImageBase64: string) => void
14
+ }
15
+
16
+ interface Area {
17
+ x: number
18
+ y: number
19
+ width: number
20
+ height: number
21
+ }
22
+
23
+ const createImage = (url: string): Promise<HTMLImageElement> =>
24
+ new Promise((resolve, reject) => {
25
+ const image = new Image()
26
+ image.addEventListener('load', () => resolve(image))
27
+ image.addEventListener('error', (error) => reject(error))
28
+ image.src = url
29
+ })
30
+
31
+ async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise<string> {
32
+ const image = await createImage(imageSrc)
33
+ const canvas = document.createElement('canvas')
34
+ const ctx = canvas.getContext('2d')
35
+
36
+ if (!ctx) {
37
+ throw new Error('No 2d context')
38
+ }
39
+
40
+ // Set canvas size to a square (optimized size for logo)
41
+ const size = 128
42
+ canvas.width = size
43
+ canvas.height = size
44
+
45
+ ctx.drawImage(
46
+ image,
47
+ pixelCrop.x,
48
+ pixelCrop.y,
49
+ pixelCrop.width,
50
+ pixelCrop.height,
51
+ 0,
52
+ 0,
53
+ size,
54
+ size
55
+ )
56
+
57
+ // Use JPEG with quality 0.8 for better compression
58
+ // Falls back to PNG if the image has transparency
59
+ let dataUrl = canvas.toDataURL('image/jpeg', 0.8)
60
+
61
+ // If the compressed image is still too large (>50KB), reduce quality further
62
+ if (dataUrl.length > 50000) {
63
+ dataUrl = canvas.toDataURL('image/jpeg', 0.6)
64
+ }
65
+
66
+ return dataUrl
67
+ }
68
+
69
+ export function ImageCropDialog({ open, onClose, imageSrc, onCropComplete }: ImageCropDialogProps) {
70
+ const [crop, setCrop] = useState({ x: 0, y: 0 })
71
+ const [zoom, setZoom] = useState(1)
72
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null)
73
+ const [isSaving, setIsSaving] = useState(false)
74
+
75
+ const onCropChange = useCallback((crop: { x: number; y: number }) => {
76
+ setCrop(crop)
77
+ }, [])
78
+
79
+ const onZoomChange = useCallback((zoom: number) => {
80
+ setZoom(zoom)
81
+ }, [])
82
+
83
+ const onCropCompleteCallback = useCallback(
84
+ (_croppedArea: Area, croppedAreaPixels: Area) => {
85
+ setCroppedAreaPixels(croppedAreaPixels)
86
+ },
87
+ []
88
+ )
89
+
90
+ const handleSave = async () => {
91
+ if (!croppedAreaPixels) return
92
+
93
+ setIsSaving(true)
94
+ try {
95
+ const croppedImage = await getCroppedImg(imageSrc, croppedAreaPixels)
96
+ onCropComplete(croppedImage)
97
+ onClose()
98
+ } catch (error) {
99
+ console.error('Error cropping image:', error)
100
+ } finally {
101
+ setIsSaving(false)
102
+ }
103
+ }
104
+
105
+ return (
106
+ <Dialog open={open} onOpenChange={onClose}>
107
+ <DialogContent className="sm:max-w-[600px]">
108
+ <DialogHeader>
109
+ <DialogTitle>Crop Image</DialogTitle>
110
+ </DialogHeader>
111
+ <div className="space-y-4">
112
+ <div className="relative h-[400px] bg-ibm-gray-20 rounded-lg">
113
+ <Cropper
114
+ image={imageSrc}
115
+ crop={crop}
116
+ zoom={zoom}
117
+ aspect={1}
118
+ cropShape="round"
119
+ showGrid={false}
120
+ onCropChange={onCropChange}
121
+ onZoomChange={onZoomChange}
122
+ onCropComplete={onCropCompleteCallback}
123
+ />
124
+ </div>
125
+ <div className="space-y-2">
126
+ <label className="text-sm font-medium">Zoom</label>
127
+ <Slider
128
+ value={[zoom]}
129
+ onValueChange={(value) => setZoom(value[0])}
130
+ min={1}
131
+ max={3}
132
+ step={0.1}
133
+ className="w-full"
134
+ />
135
+ </div>
136
+ </div>
137
+ <DialogFooter>
138
+ <Button
139
+ variant="secondary"
140
+ onClick={onClose}
141
+ disabled={isSaving}
142
+ className="rounded-none"
143
+ >
144
+ Cancel
145
+ </Button>
146
+ <Button
147
+ onClick={handleSave}
148
+ disabled={isSaving}
149
+ className="rounded-none"
150
+ >
151
+ {isSaving ? "Saving..." : "OK"}
152
+ </Button>
153
+ </DialogFooter>
154
+ </DialogContent>
155
+ </Dialog>
156
+ )
157
+ }
@@ -11,8 +11,12 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './tool
11
11
  export { PageHeader } from './page-header'
12
12
  export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, type ChartConfig } from './chart'
13
13
  export { SearchInput } from './search-input'
14
+ export { AssetsHeader } from './assets-header'
15
+ export { AssetLogo, getAssetIcon, getAssetColor } from './asset-logo'
16
+ export { EditableTitle } from './editable-title'
17
+ export { ImageCropDialog } from './image-crop-dialog'
14
18
  // Note: The following components are not exported as they depend on app-specific services or models:
15
- // - AssetsHeader, CertificateEditor, EnvironmentSettings, EnvironmentVariablesConfig
19
+ // - CertificateEditor, EnvironmentSettings, EnvironmentVariablesConfig
16
20
  // - FileImport, ProcessStatus, ResourceSettings, SecretExplorer, SecretPropertiesEditor, SelectedAsset
17
21
  export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './accordion'
18
22
  export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from './alert-dialog'
package/package.json CHANGED
@@ -1,94 +1,95 @@
1
- {
2
- "name": "@orsetra/shared-ui",
3
- "version": "1.0.12",
4
- "description": "Shared UI components for Orsetra platform",
5
- "main": "./index.ts",
6
- "types": "./index.ts",
7
- "exports": {
8
- ".": "./index.ts",
9
- "./components/ui": "./components/ui/index.ts",
10
- "./components/layout": "./components/layout/index.ts",
11
- "./hooks": "./hooks/index.ts",
12
- "./lib/*": "./lib/*.ts"
13
- },
14
- "files": [
15
- "index.ts",
16
- "components",
17
- "hooks",
18
- "lib",
19
- "README.md"
20
- ],
21
- "publishConfig": {
22
- "access": "public"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/orsetra/console-ui.git",
27
- "directory": "packages/shared-ui"
28
- },
29
- "keywords": [
30
- "react",
31
- "components",
32
- "ui",
33
- "orsetra",
34
- "radix-ui",
35
- "tailwind"
36
- ],
37
- "peerDependencies": {
38
- "react": "^18.0.0 || ^19.0.0",
39
- "react-dom": "^18.0.0 || ^19.0.0",
40
- "next": "^14.0.0 || ^15.0.0"
41
- },
42
- "dependencies": {
43
- "react-avatar": "^5.0.3",
44
- "react-hook-form": "^7.54.0",
45
- "react-resizable-panels": "^2.1.7",
46
- "@hookform/resolvers": "^3.9.1",
47
- "zod": "^3.24.1",
48
- "clsx": "^2.1.1",
49
- "tailwind-merge": "^2.5.5",
50
- "class-variance-authority": "^0.7.1",
51
- "lucide-react": "^0.454.0",
52
- "react-day-picker": "8.10.1",
53
- "embla-carousel-react": "8.5.1",
54
- "cmdk": "1.0.4",
55
- "recharts": "^2.15.0",
56
- "date-fns": "4.1.0",
57
- "input-otp": "1.4.1",
58
- "vaul": "^1.1.1",
59
- "next-themes": "^0.4.4",
60
- "sonner": "^1.7.1",
61
- "@radix-ui/react-accordion": "1.2.2",
62
- "@radix-ui/react-alert-dialog": "1.1.4",
63
- "@radix-ui/react-aspect-ratio": "1.1.1",
64
- "@radix-ui/react-avatar": "1.1.2",
65
- "@radix-ui/react-checkbox": "1.1.3",
66
- "@radix-ui/react-collapsible": "1.1.2",
67
- "@radix-ui/react-context-menu": "2.2.4",
68
- "@radix-ui/react-dialog": "1.1.4",
69
- "@radix-ui/react-dropdown-menu": "2.1.4",
70
- "@radix-ui/react-hover-card": "1.1.4",
71
- "@radix-ui/react-label": "2.1.1",
72
- "@radix-ui/react-menubar": "1.1.4",
73
- "@radix-ui/react-navigation-menu": "1.2.3",
74
- "@radix-ui/react-popover": "1.1.4",
75
- "@radix-ui/react-progress": "1.1.1",
76
- "@radix-ui/react-radio-group": "1.2.2",
77
- "@radix-ui/react-scroll-area": "1.2.2",
78
- "@radix-ui/react-select": "2.1.4",
79
- "@radix-ui/react-separator": "1.1.1",
80
- "@radix-ui/react-slider": "1.2.2",
81
- "@radix-ui/react-slot": "1.1.1",
82
- "@radix-ui/react-switch": "1.1.2",
83
- "@radix-ui/react-tabs": "1.1.2",
84
- "@radix-ui/react-toast": "1.2.4",
85
- "@radix-ui/react-toggle": "1.1.1",
86
- "@radix-ui/react-toggle-group": "1.1.1",
87
- "@radix-ui/react-tooltip": "1.1.6"
88
- },
89
- "devDependencies": {
90
- "@types/react": "^19",
91
- "next": "^15.0.0",
92
- "typescript": "^5"
93
- }
94
- }
1
+ {
2
+ "name": "@orsetra/shared-ui",
3
+ "version": "1.0.13",
4
+ "description": "Shared UI components for Orsetra platform",
5
+ "main": "./index.ts",
6
+ "types": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./components/ui": "./components/ui/index.ts",
10
+ "./components/layout": "./components/layout/index.ts",
11
+ "./hooks": "./hooks/index.ts",
12
+ "./lib/*": "./lib/*.ts"
13
+ },
14
+ "files": [
15
+ "index.ts",
16
+ "components",
17
+ "hooks",
18
+ "lib",
19
+ "README.md"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/orsetra/console-ui.git",
27
+ "directory": "packages/shared-ui"
28
+ },
29
+ "keywords": [
30
+ "react",
31
+ "components",
32
+ "ui",
33
+ "orsetra",
34
+ "radix-ui",
35
+ "tailwind"
36
+ ],
37
+ "peerDependencies": {
38
+ "react": "^18.0.0 || ^19.0.0",
39
+ "react-dom": "^18.0.0 || ^19.0.0",
40
+ "next": "^14.0.0 || ^15.0.0"
41
+ },
42
+ "dependencies": {
43
+ "react-avatar": "^5.0.3",
44
+ "react-easy-crop": "^5.0.8",
45
+ "react-hook-form": "^7.54.0",
46
+ "react-resizable-panels": "^2.1.7",
47
+ "@hookform/resolvers": "^3.9.1",
48
+ "zod": "^3.24.1",
49
+ "clsx": "^2.1.1",
50
+ "tailwind-merge": "^2.5.5",
51
+ "class-variance-authority": "^0.7.1",
52
+ "lucide-react": "^0.454.0",
53
+ "react-day-picker": "8.10.1",
54
+ "embla-carousel-react": "8.5.1",
55
+ "cmdk": "1.0.4",
56
+ "recharts": "^2.15.0",
57
+ "date-fns": "4.1.0",
58
+ "input-otp": "1.4.1",
59
+ "vaul": "^1.1.1",
60
+ "next-themes": "^0.4.4",
61
+ "sonner": "^1.7.1",
62
+ "@radix-ui/react-accordion": "1.2.2",
63
+ "@radix-ui/react-alert-dialog": "1.1.4",
64
+ "@radix-ui/react-aspect-ratio": "1.1.1",
65
+ "@radix-ui/react-avatar": "1.1.2",
66
+ "@radix-ui/react-checkbox": "1.1.3",
67
+ "@radix-ui/react-collapsible": "1.1.2",
68
+ "@radix-ui/react-context-menu": "2.2.4",
69
+ "@radix-ui/react-dialog": "1.1.4",
70
+ "@radix-ui/react-dropdown-menu": "2.1.4",
71
+ "@radix-ui/react-hover-card": "1.1.4",
72
+ "@radix-ui/react-label": "2.1.1",
73
+ "@radix-ui/react-menubar": "1.1.4",
74
+ "@radix-ui/react-navigation-menu": "1.2.3",
75
+ "@radix-ui/react-popover": "1.1.4",
76
+ "@radix-ui/react-progress": "1.1.1",
77
+ "@radix-ui/react-radio-group": "1.2.2",
78
+ "@radix-ui/react-scroll-area": "1.2.2",
79
+ "@radix-ui/react-select": "2.1.4",
80
+ "@radix-ui/react-separator": "1.1.1",
81
+ "@radix-ui/react-slider": "1.2.2",
82
+ "@radix-ui/react-slot": "1.1.1",
83
+ "@radix-ui/react-switch": "1.1.2",
84
+ "@radix-ui/react-tabs": "1.1.2",
85
+ "@radix-ui/react-toast": "1.2.4",
86
+ "@radix-ui/react-toggle": "1.1.1",
87
+ "@radix-ui/react-toggle-group": "1.1.1",
88
+ "@radix-ui/react-tooltip": "1.1.6"
89
+ },
90
+ "devDependencies": {
91
+ "@types/react": "^19",
92
+ "next": "^15.0.0",
93
+ "typescript": "^5"
94
+ }
95
+ }