@orsetra/shared-ui 1.0.11 → 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.
- package/components/layout/layout-container.tsx +25 -29
- package/components/ui/asset-logo.tsx +117 -0
- package/components/ui/assets-header.tsx +24 -21
- package/components/ui/editable-title.tsx +90 -0
- package/components/ui/image-crop-dialog.tsx +157 -0
- package/components/ui/index.ts +5 -1
- package/components/ui/page-header.tsx +10 -10
- package/package.json +95 -94
|
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation"
|
|
|
5
5
|
import { MainSidebar, Sidebar, SidebarProvider, useSidebar, type SidebarMode } from "./index"
|
|
6
6
|
import { UserMenu, Button } from "../ui"
|
|
7
7
|
import { getMenuFromPath } from "../../lib/menu-utils"
|
|
8
|
+
import { useIsMobile } from "../../hooks/use-mobile"
|
|
8
9
|
import { Menu } from "lucide-react"
|
|
9
10
|
|
|
10
11
|
export interface SidebarMenus {
|
|
@@ -22,10 +23,19 @@ interface LayoutContainerProps {
|
|
|
22
23
|
function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expanded' }: LayoutContainerProps) {
|
|
23
24
|
const pathname = usePathname()
|
|
24
25
|
const { setOpen } = useSidebar()
|
|
26
|
+
const isMobile = useIsMobile()
|
|
25
27
|
const [isMainSidebarOpen, setIsMainSidebarOpen] = useState(false)
|
|
26
28
|
const [currentMenu, setCurrentMenu] = useState<string>("overview")
|
|
27
29
|
const isMinimized = mode === 'minimized'
|
|
28
|
-
|
|
30
|
+
// Force hidden mode on mobile
|
|
31
|
+
const isHidden = mode === 'hidden' || isMobile
|
|
32
|
+
|
|
33
|
+
// Close sidebar on route change (mobile)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (isMobile) {
|
|
36
|
+
setIsMainSidebarOpen(false)
|
|
37
|
+
}
|
|
38
|
+
}, [pathname, isMobile])
|
|
29
39
|
|
|
30
40
|
useEffect(() => {
|
|
31
41
|
const contextualMenu = getMenuFromPath(pathname)
|
|
@@ -49,7 +59,7 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
|
|
|
49
59
|
|
|
50
60
|
return (
|
|
51
61
|
<div className="flex h-screen w-full bg-white">
|
|
52
|
-
{/*
|
|
62
|
+
{/* Desktop sidebar - hidden on mobile (isHidden is true on mobile) */}
|
|
53
63
|
{!isMinimized && !isHidden && (
|
|
54
64
|
<Sidebar
|
|
55
65
|
currentMenu={currentMenu}
|
|
@@ -57,41 +67,27 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
|
|
|
57
67
|
/>
|
|
58
68
|
)}
|
|
59
69
|
|
|
60
|
-
{/* MainSidebar -
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/>
|
|
70
|
-
)}
|
|
71
|
-
|
|
72
|
-
{/* MainSidebar overlay en mode hidden */}
|
|
73
|
-
{isHidden && (
|
|
74
|
-
<MainSidebar
|
|
75
|
-
isOpen={isMainSidebarOpen}
|
|
76
|
-
onToggle={handleMainSidebarToggle}
|
|
77
|
-
onMenuSelect={handleMenuSelect}
|
|
78
|
-
currentMenu={currentMenu}
|
|
79
|
-
onSecondarySidebarOpen={handleSecondarySidebarOpen}
|
|
80
|
-
mode="expanded"
|
|
81
|
-
/>
|
|
82
|
-
)}
|
|
70
|
+
{/* MainSidebar - always available, opens as overlay when isHidden */}
|
|
71
|
+
<MainSidebar
|
|
72
|
+
isOpen={isMainSidebarOpen}
|
|
73
|
+
onToggle={handleMainSidebarToggle}
|
|
74
|
+
onMenuSelect={handleMenuSelect}
|
|
75
|
+
currentMenu={currentMenu}
|
|
76
|
+
onSecondarySidebarOpen={handleSecondarySidebarOpen}
|
|
77
|
+
mode={isHidden ? "expanded" : mode}
|
|
78
|
+
/>
|
|
83
79
|
|
|
84
|
-
<div className="flex-1 flex flex-col">
|
|
80
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
85
81
|
<header className="h-14 bg-white border-b border-ui-border flex-shrink-0">
|
|
86
|
-
<div className="h-full px-6 flex items-center justify-between">
|
|
87
|
-
{/*
|
|
82
|
+
<div className="h-full px-4 md:px-6 flex items-center justify-between">
|
|
83
|
+
{/* Menu button - shown when sidebar is hidden (including on mobile) */}
|
|
88
84
|
{isHidden ? (
|
|
89
85
|
<Button
|
|
90
86
|
variant="ghost"
|
|
91
87
|
size="sm"
|
|
92
88
|
onClick={handleMainSidebarToggle}
|
|
93
89
|
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
94
|
-
title="Ouvrir le menu
|
|
90
|
+
title="Ouvrir le menu"
|
|
95
91
|
>
|
|
96
92
|
<Menu className="h-5 w-5" />
|
|
97
93
|
</Button>
|
|
@@ -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
|
|
24
|
-
{/*
|
|
25
|
-
<div className="flex items-center gap-3">
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
package/components/ui/index.ts
CHANGED
|
@@ -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
|
-
// -
|
|
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'
|
|
@@ -12,23 +12,23 @@ interface PageHeaderProps {
|
|
|
12
12
|
|
|
13
13
|
export function PageHeader({ title, description, backLink, actions, statusNode}: PageHeaderProps) {
|
|
14
14
|
return (
|
|
15
|
-
<div className="mb-8">
|
|
16
|
-
<div className="flex items-center justify-between">
|
|
17
|
-
<div className="flex items-center space-x-3">
|
|
15
|
+
<div className="mb-4 sm:mb-8">
|
|
16
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
17
|
+
<div className="flex items-center space-x-3 min-w-0">
|
|
18
18
|
{backLink && (
|
|
19
|
-
<Link href={backLink}>
|
|
20
|
-
<ArrowLeft className="h-6 w-6 text-text-secondary" />
|
|
19
|
+
<Link href={backLink} className="flex-shrink-0">
|
|
20
|
+
<ArrowLeft className="h-5 w-5 sm:h-6 sm:w-6 text-text-secondary" />
|
|
21
21
|
</Link>
|
|
22
22
|
)}
|
|
23
|
-
<div>
|
|
23
|
+
<div className="min-w-0">
|
|
24
24
|
<div className="flex items-center space-x-3">
|
|
25
|
-
<h1 className="text-2xl font-semibold text-text-primary">{title}</h1>
|
|
26
|
-
{statusNode && <div className="mt-2">{statusNode}</div>}
|
|
25
|
+
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary truncate">{title}</h1>
|
|
26
|
+
{statusNode && <div className="mt-2 flex-shrink-0">{statusNode}</div>}
|
|
27
27
|
</div>
|
|
28
|
-
{description && <p className="text-base text-text-secondary">{description}</p>}
|
|
28
|
+
{description && <p className="text-sm sm:text-base text-text-secondary line-clamp-2">{description}</p>}
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
|
-
{actions && <div className="flex items-center space-x-4">{actions}</div>}
|
|
31
|
+
{actions && <div className="flex items-center space-x-2 sm:space-x-4 flex-shrink-0">{actions}</div>}
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
)
|
package/package.json
CHANGED
|
@@ -1,94 +1,95 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@orsetra/shared-ui",
|
|
3
|
-
"version": "1.0.
|
|
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-
|
|
45
|
-
"react-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"react
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"@radix-ui/react-
|
|
63
|
-
"@radix-ui/react-
|
|
64
|
-
"@radix-ui/react-
|
|
65
|
-
"@radix-ui/react-
|
|
66
|
-
"@radix-ui/react-
|
|
67
|
-
"@radix-ui/react-
|
|
68
|
-
"@radix-ui/react-
|
|
69
|
-
"@radix-ui/react-
|
|
70
|
-
"@radix-ui/react-
|
|
71
|
-
"@radix-ui/react-
|
|
72
|
-
"@radix-ui/react-
|
|
73
|
-
"@radix-ui/react-
|
|
74
|
-
"@radix-ui/react-
|
|
75
|
-
"@radix-ui/react-
|
|
76
|
-
"@radix-ui/react-
|
|
77
|
-
"@radix-ui/react-
|
|
78
|
-
"@radix-ui/react-
|
|
79
|
-
"@radix-ui/react-
|
|
80
|
-
"@radix-ui/react-
|
|
81
|
-
"@radix-ui/react-
|
|
82
|
-
"@radix-ui/react-
|
|
83
|
-
"@radix-ui/react-
|
|
84
|
-
"@radix-ui/react-
|
|
85
|
-
"@radix-ui/react-
|
|
86
|
-
"@radix-ui/react-toggle
|
|
87
|
-
"@radix-ui/react-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
"
|
|
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
|
+
}
|